Skip to content

Commit 1c92ccf

Browse files
committed
feat: deployment flow
1 parent 88eaa3d commit 1c92ccf

31 files changed

Lines changed: 1184 additions & 11 deletions

File tree

.storybook/main.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,6 @@ const config: StorybookConfig = {
6767
'@stateful/*': path.resolve(rootDir, 'src/components/stateful/*'),
6868
'@hooks/*': path.resolve(rootDir, 'src/components/hooks/*'),
6969
'@app-hooks/*': path.resolve(rootDir, 'src/app-hooks/*'),
70-
'@container/*': path.resolve(rootDir, 'src/components/container/*'),
7170
'@assets/*': path.resolve(rootDir, 'src/assets/*'),
7271
'@pages/*': path.resolve(rootDir, 'src/pages/*'),
7372
'@routers/*': path.resolve(rootDir, 'src/routers/*'),

docs/VITE_VS_WEBPACK.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424
## 路径别名
2525

26-
- 两端保持一致:`@`/`@src`/`@stateless`/`@stateful`/`@hooks`/`@app-hooks`/`@container`/`@assets`/`@pages`/`@routers`/`@utils`/`@theme`
26+
- 两端保持一致:`@`/`@src`/`@stateless`/`@stateful`/`@hooks`/`@app-hooks`/`@assets`/`@pages`/`@routers`/`@utils`/`@theme`
2727

2828
## 构建与输出
2929

scripts/depcheck.mjs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,6 @@ const ignoreMissingPrefixes = [
220220
'@utils/',
221221
'@assets/',
222222
'@app-hooks/',
223-
'@container/',
224223
'@theme/',
225224
]
226225

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { useCallback, useEffect, useRef, useState } from 'react'
2+
3+
type RootRef = HTMLElement | null | React.RefObject<HTMLElement>
4+
5+
export interface UseIntersectionOptions {
6+
/** IntersectionObserver threshold */
7+
threshold?: number
8+
/** Root element or ref to observe within */
9+
root?: RootRef
10+
/** Root margin */
11+
rootMargin?: string
12+
/** Whether to disconnect after first intersection (default: true) */
13+
once?: boolean
14+
}
15+
16+
export const useIntersection = (options: UseIntersectionOptions = {}) => {
17+
const { threshold = 0.15, root = null, rootMargin = '0px', once = true } = options
18+
19+
// internal storage
20+
const internalRef = useRef<HTMLElement | null>(null)
21+
const [observedNode, setObservedNode] = useState<HTMLElement | null>(null)
22+
const [isVisible, setIsVisible] = useState(false)
23+
24+
// callback ref so we react to node mount/unmount
25+
const ref = useCallback((node: HTMLElement | null) => {
26+
internalRef.current = node
27+
setObservedNode(node)
28+
}, [])
29+
30+
// resolve root (may be a ref)
31+
const resolvedRoot =
32+
root && 'current' in (root as React.RefObject<HTMLElement>)
33+
? (root as React.RefObject<HTMLElement>).current
34+
: (root as HTMLElement | null)
35+
36+
useEffect(() => {
37+
if (typeof window === 'undefined' || typeof IntersectionObserver === 'undefined') return
38+
39+
const node = observedNode
40+
if (!node) return
41+
42+
// if no root provided or ref not ready, try to find common FixTabPanel container as fallback
43+
const rootCandidate =
44+
resolvedRoot ?? (document.querySelector('.fix-tab-panel-scroll-container') as HTMLElement | null)
45+
46+
// debug helpers (enable by setting window.__DEBUG_USE_INTERSECTION = true)
47+
const isDebug = typeof window !== 'undefined' && (window as any).__DEBUG_USE_INTERSECTION
48+
if (isDebug) {
49+
console.debug(
50+
'[useIntersection] observed node:',
51+
node,
52+
'resolvedRoot:',
53+
resolvedRoot,
54+
'rootCandidate:',
55+
rootCandidate
56+
)
57+
}
58+
59+
let observer: IntersectionObserver | null = new IntersectionObserver(
60+
([entry]) => {
61+
if (isDebug) console.debug('[useIntersection] observer cb, isIntersecting=', entry.isIntersecting)
62+
if (entry.isIntersecting) {
63+
setIsVisible(true)
64+
if (once && observer) observer.disconnect()
65+
}
66+
},
67+
{ threshold, root: rootCandidate ?? undefined, rootMargin }
68+
)
69+
70+
try {
71+
observer.observe(node)
72+
} catch (e) {
73+
if (isDebug) console.warn('[useIntersection] observer.observe failed:', e)
74+
}
75+
76+
// Fallback: attach scroll/resize listener to manually check intersection in case
77+
// the observer doesn't trigger due to timing or root issues (robustness)
78+
const checkIntersect = () => {
79+
if (!node) return
80+
const nodeRect = node.getBoundingClientRect()
81+
const rootRect = rootCandidate
82+
? (rootCandidate as HTMLElement).getBoundingClientRect()
83+
: {
84+
top: 0,
85+
left: 0,
86+
right: window.innerWidth,
87+
bottom: window.innerHeight,
88+
width: window.innerWidth,
89+
height: window.innerHeight,
90+
}
91+
92+
const verticallyIn = nodeRect.bottom > rootRect.top && nodeRect.top < rootRect.bottom
93+
const horizontallyIn = nodeRect.right > rootRect.left && nodeRect.left < rootRect.right
94+
95+
const inView = verticallyIn && horizontallyIn
96+
if (isDebug && inView) console.debug('[useIntersection] checkIntersect -> inView', { nodeRect, rootRect })
97+
98+
if (inView) {
99+
setIsVisible(true)
100+
if (once) {
101+
if (observer) {
102+
observer.disconnect()
103+
observer = null
104+
}
105+
removeListeners()
106+
}
107+
}
108+
}
109+
110+
const removeListeners = () => {
111+
try {
112+
if (rootCandidate) {
113+
;(rootCandidate as HTMLElement).removeEventListener('scroll', checkIntersect)
114+
} else {
115+
window.removeEventListener('scroll', checkIntersect)
116+
}
117+
window.removeEventListener('resize', checkIntersect)
118+
} catch (e) {
119+
/* swallow */
120+
}
121+
}
122+
123+
if (rootCandidate) {
124+
;(rootCandidate as HTMLElement).addEventListener('scroll', checkIntersect, { passive: true })
125+
} else {
126+
window.addEventListener('scroll', checkIntersect, { passive: true })
127+
}
128+
window.addEventListener('resize', checkIntersect)
129+
130+
// initial check
131+
checkIntersect()
132+
133+
return () => {
134+
if (observer) {
135+
observer.disconnect()
136+
observer = null
137+
}
138+
removeListeners()
139+
}
140+
}, [threshold, resolvedRoot, rootMargin, once, observedNode])
141+
142+
return [ref, isVisible] as const
143+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import React from 'react'
2+
3+
interface AnimationInViewProps {
4+
children: React.ReactNode
5+
variants?: {
6+
hidden: { opacity: number; y: number }
7+
visible: { opacity: number; y: number }
8+
}
9+
transition?: Record<string, unknown>
10+
viewOptions?: Record<string, unknown>
11+
as?: string
12+
className?: string
13+
style?: React.CSSProperties
14+
once?: boolean
15+
amount?: number
16+
margin?: string
17+
scrollContainerRef?: React.RefObject<HTMLElement>
18+
}
19+
20+
declare const AnimationInView: React.FC<AnimationInViewProps>
21+
22+
export default AnimationInView

src/components/stateless/AnimInView/index.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@ const AnimationInView = ({
1818
once = false,
1919
amount = 0.2,
2020
margin,
21+
scrollContainerRef,
2122
}) => {
2223
const ref = useRef(null)
2324
const isInView = useInView(ref, {
2425
once,
2526
amount,
2627
margin,
28+
root: scrollContainerRef,
2729
...viewOptions,
2830
})
2931

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
@color-git: #0277bd;
2+
@light-git: #e1f5fe;
3+
@color-ci: #ef6c00;
4+
@light-ci: #fff3e0;
5+
@color-cd: #2e7d32;
6+
@light-cd: #e8f5e9;
7+
@color-ops: #6a1b9a;
8+
@light-ops: #f3e5f5;
9+
@color-alert: #c62828;
10+
@light-alert: #ffebee;
11+
12+
@card-shadow: 0 4px 12px rgba(0,0,0,0.08), 0 1px 3px rgba(0,0,0,0.05);
13+
14+
.container {
15+
max-width: 1100px;
16+
margin: 50px auto;
17+
position: relative;
18+
padding: 20px;
19+
}
20+
21+
/* 时间轴线条与流动动画 */
22+
.timelineLine {
23+
position: absolute;
24+
left: 50%;
25+
top: 0;
26+
bottom: 0;
27+
width: 4px;
28+
background: #e0e0e0;
29+
transform: translateX(-50%);
30+
border-radius: 2px;
31+
z-index: 0;
32+
overflow: hidden;
33+
}
34+
35+
.timelineLine::after {
36+
content: '';
37+
position: absolute;
38+
top: -50%;
39+
left: 0;
40+
width: 100%;
41+
height: 50%;
42+
background: linear-gradient(to bottom, transparent, rgba(33, 150, 243, 0.6), transparent);
43+
animation: flowDown 2.5s linear infinite;
44+
}
45+
46+
@keyframes flowDown {
47+
0% { top: -50%; }
48+
100% { top: 150%; }
49+
}
50+
51+
/* 节点布局 */
52+
.node {
53+
position: relative;
54+
margin-bottom: 20px;
55+
width: 100%;
56+
display: flex;
57+
justify-content: center;
58+
opacity: 1;
59+
transform: translateY(40px);
60+
transition: all 0.8s cubic-bezier(0.2, 0.8, 0.2, 1);
61+
62+
&.visible {
63+
opacity: 1;
64+
transform: translateY(0);
65+
}
66+
}
67+
68+
.cardWrapper {
69+
width: 48%;
70+
position: relative;
71+
z-index: 1;
72+
73+
&.left {
74+
margin-right: auto;
75+
padding-right: 40px;
76+
text-align: right;
77+
}
78+
79+
&.right {
80+
margin-left: auto;
81+
padding-left: 40px;
82+
text-align: left;
83+
}
84+
}
85+
86+
/* 响应式适配 */
87+
@media (max-width: 768px) {
88+
.timelineLine { left: 20px; }
89+
.cardWrapper {
90+
width: 85% !important;
91+
margin-left: 50px !important;
92+
margin-right: 0 !important;
93+
padding: 0 !important;
94+
text-align: left !important;
95+
}
96+
}
97+
98+
/* 圆点定位 - 修复版 */
99+
.dot {
100+
position: absolute;
101+
left: 50%;
102+
top: 24px;
103+
width: 24px;
104+
height: 24px;
105+
background: white;
106+
border: 5px solid #fff;
107+
box-shadow: 0 0 0 2px rgba(0,0,0,0.1);
108+
border-radius: 50%;
109+
z-index: 2;
110+
transform: translateX(-50%);
111+
transition: transform 0.3s;
112+
}
113+
114+
.node:hover .dot {
115+
transform: translateX(-50%) scale(1.2);
116+
}
117+
118+
/* 单独定义类名以便 CSS Modules 导出 */
119+
.dot.git { background-color: #0277bd; }
120+
.dot.ci { background-color: #ef6c00; }
121+
.dot.env { background-color: #2e7d32; }
122+
.dot.strategy { background-color: #6a1b9a; }
123+
.dot.ops { background-color: #c62828; }
124+
125+
@media (max-width: 768px) {
126+
.dot {
127+
left: 20px;
128+
transform: none;
129+
}
130+
.node:hover .dot {
131+
transform: scale(1.2);
132+
}
133+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import Phase01 from './phases/Phase01_Code'
2+
import Phase02 from './phases/Phase02_CICD'
3+
import Phase03 from './phases/Phase03_Env'
4+
import Phase04 from './phases/Phase04_Strategy'
5+
import Phase05 from './phases/Phase05_Ops'
6+
import TimelineDot from './shared/TimelineDot'
7+
import styles from './index.module.less'
8+
9+
const phases = [
10+
{ id: 'phase-01', component: Phase01, type: 'git' as const },
11+
{ id: 'phase-02', component: Phase02, type: 'ci' as const },
12+
{ id: 'phase-03', component: Phase03, type: 'env' as const },
13+
{ id: 'phase-04', component: Phase04, type: 'strategy' as const },
14+
{ id: 'phase-05', component: Phase05, type: 'ops' as const },
15+
]
16+
17+
interface Props {
18+
scrollContainerRef?: React.RefObject<HTMLElement>
19+
}
20+
21+
const DeploymentFlow: React.FC<Props> = ({ scrollContainerRef }) => {
22+
return (
23+
<div className={styles.container}>
24+
<div className={styles.timelineLine}></div>
25+
26+
{phases.map((phase, index) => {
27+
const isLeft = index % 2 === 0
28+
const PhaseComponent = phase.component
29+
30+
return (
31+
<div key={phase.id} className={`${styles.node} ${styles[phase.type]}`}>
32+
<TimelineDot type={phase.type} />
33+
<div className={`${styles.cardWrapper} ${isLeft ? styles.left : styles.right}`}>
34+
<PhaseComponent isLeft={isLeft} scrollContainerRef={scrollContainerRef} />
35+
</div>
36+
</div>
37+
)
38+
})}
39+
</div>
40+
)
41+
}
42+
43+
export default DeploymentFlow

0 commit comments

Comments
 (0)