@wzyjs/uis 0.3.10 → 0.3.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/package.json +2 -2
  2. package/src/antd/form/FileUploader/index.tsx +163 -0
  3. package/src/antd/form/RadioButton/index.tsx +3 -1
  4. package/src/antd/form/index.ts +1 -0
  5. package/src/antd/index.ts +2 -0
  6. package/src/components/Crud/components/CardList/index.tsx +174 -0
  7. package/src/components/Crud/components/CreateUpdate/index.tsx +99 -0
  8. package/src/components/Crud/components/Provider/index.tsx +73 -0
  9. package/src/components/Crud/components/Remove/index.tsx +56 -0
  10. package/src/components/Crud/components/index.ts +4 -0
  11. package/src/components/Crud/hooks/index.ts +4 -0
  12. package/src/components/Crud/hooks/useColumns.tsx +169 -0
  13. package/src/components/Crud/hooks/useList.ts +54 -0
  14. package/src/components/Crud/hooks/useOrderable.tsx +107 -0
  15. package/src/components/Crud/hooks/useRequest.ts +41 -0
  16. package/src/components/Crud/index.tsx +91 -0
  17. package/src/components/Crud/types/index.ts +188 -0
  18. package/src/components/Crud/utils/index.ts +87 -0
  19. package/src/components/MindMap/context.tsx +29 -0
  20. package/src/components/MindMap/hooks/useAlignmentSnap.ts +220 -0
  21. package/src/components/MindMap/hooks/useCopyPaste.ts +272 -0
  22. package/src/components/MindMap/hooks/useDropToReparent.ts +288 -0
  23. package/src/components/MindMap/hooks/useExpandCollapse.ts +146 -0
  24. package/src/components/MindMap/hooks/useMoveDescendants.ts +136 -0
  25. package/src/components/MindMap/hooks/useUndoRedo.ts +232 -0
  26. package/src/components/MindMap/index.tsx +117 -0
  27. package/src/components/ProgressButton/index.module.scss +65 -0
  28. package/src/components/ProgressButton/index.tsx +96 -0
  29. package/src/components/TimelineBar/components/CurrentWeekHighlight/index.tsx +64 -0
  30. package/src/components/TimelineBar/components/Guides/index.tsx +61 -0
  31. package/src/components/TimelineBar/components/Ticks/index.tsx +56 -0
  32. package/src/components/TimelineBar/components/TodayIndicator/index.tsx +54 -0
  33. package/src/components/TimelineBar/components/index.ts +4 -0
  34. package/src/components/TimelineBar/const.ts +3 -0
  35. package/src/components/TimelineBar/hooks/index.ts +5 -0
  36. package/src/components/TimelineBar/hooks/useHighlightRange.ts +21 -0
  37. package/src/components/TimelineBar/hooks/useMonthGuides.ts +40 -0
  38. package/src/components/TimelineBar/hooks/useTickValues.ts +18 -0
  39. package/src/components/TimelineBar/hooks/useVisibleRange.ts +43 -0
  40. package/src/components/TimelineBar/hooks/useWeekGuides.ts +39 -0
  41. package/src/components/TimelineBar/index.tsx +63 -0
  42. package/src/components/TimelineBar/utils.ts +27 -0
  43. package/src/components/index.ts +5 -0
  44. package/src/rn.ts +1 -0
  45. package/src/rns/index.ts +0 -0
@@ -0,0 +1,232 @@
1
+ 'use client'
2
+
3
+ import { useCallback, useEffect, useMemo, useRef } from 'react'
4
+ import type { Edge, EdgeChange, Node, NodeChange } from '@xyflow/react'
5
+
6
+ interface Snapshot<TNodeData extends Record<string, unknown>> {
7
+ nodes: Array<Node<TNodeData>>
8
+ edges: Edge[]
9
+ }
10
+
11
+ interface UseUndoRedoParams<TNodeData extends Record<string, unknown>> {
12
+ enabled?: boolean
13
+ nodes: Array<Node<TNodeData>>
14
+ edges: Edge[]
15
+ onNodesChange?: (changes: Array<NodeChange<Node<TNodeData>>>) => void
16
+ onEdgesChange?: (changes: Array<EdgeChange<Edge>>) => void
17
+ }
18
+
19
+ interface UseUndoRedoResult<TNodeData extends Record<string, unknown>> {
20
+ onNodesChange: (changes: Array<NodeChange<Node<TNodeData>>>) => void
21
+ onEdgesChange: (changes: Array<EdgeChange<Edge>>) => void
22
+ }
23
+
24
+ const clone = <T,>(value: T): T => {
25
+ const anyGlobal = globalThis as unknown as { structuredClone?: <V>(v: V) => V }
26
+ if (typeof anyGlobal.structuredClone === 'function') {
27
+ return anyGlobal.structuredClone(value)
28
+ }
29
+ return JSON.parse(JSON.stringify(value)) as T
30
+ }
31
+
32
+ const isEditableTarget = (target: EventTarget | null): boolean => {
33
+ const el = target as HTMLElement | null
34
+ if (!el) {
35
+ return false
36
+ }
37
+ const tag = el.tagName?.toLowerCase()
38
+ if (tag === 'input' || tag === 'textarea') {
39
+ return true
40
+ }
41
+ return el.isContentEditable
42
+ }
43
+
44
+ const buildNodeChanges = <TNodeData extends Record<string, unknown>,>(
45
+ current: Array<Node<TNodeData>>,
46
+ next: Array<Node<TNodeData>>,
47
+ ): Array<NodeChange<Node<TNodeData>>> => {
48
+ const currentById = new Map(current.map(n => [n.id, n]))
49
+ const nextById = new Map(next.map(n => [n.id, n]))
50
+
51
+ const changes: Array<NodeChange<Node<TNodeData>>> = []
52
+ current.forEach(node => {
53
+ if (!nextById.has(node.id)) {
54
+ changes.push({ type: 'remove', id: node.id })
55
+ }
56
+ })
57
+ next.forEach(node => {
58
+ if (!currentById.has(node.id)) {
59
+ changes.push({ type: 'add', item: clone(node) })
60
+ return
61
+ }
62
+ changes.push({ type: 'replace', id: node.id, item: clone(node) })
63
+ })
64
+ return changes
65
+ }
66
+
67
+ const buildEdgeChanges = (current: Edge[], next: Edge[]): Array<EdgeChange<Edge>> => {
68
+ const currentById = new Map(current.map(e => [e.id, e]))
69
+ const nextById = new Map(next.map(e => [e.id, e]))
70
+
71
+ const changes: Array<EdgeChange<Edge>> = []
72
+ current.forEach(edge => {
73
+ if (!nextById.has(edge.id)) {
74
+ changes.push({ type: 'remove', id: edge.id })
75
+ }
76
+ })
77
+ next.forEach(edge => {
78
+ if (!currentById.has(edge.id)) {
79
+ changes.push({ type: 'add', item: clone(edge) })
80
+ return
81
+ }
82
+ changes.push({ type: 'replace', id: edge.id, item: clone(edge) })
83
+ })
84
+ return changes
85
+ }
86
+
87
+ export const useUndoRedo = <TNodeData extends Record<string, unknown>,>(
88
+ params: UseUndoRedoParams<TNodeData>,
89
+ ): UseUndoRedoResult<TNodeData> => {
90
+ const { enabled = true, nodes, edges, onNodesChange, onEdgesChange } = params
91
+
92
+ const snapshot = useMemo((): Snapshot<TNodeData> => {
93
+ return { nodes: clone(nodes), edges: clone(edges) }
94
+ }, [edges, nodes])
95
+
96
+ const pastRef = useRef<Array<Snapshot<TNodeData>>>([])
97
+ const futureRef = useRef<Array<Snapshot<TNodeData>>>([])
98
+ const applyingRef = useRef<boolean>(false)
99
+ const commitNextRef = useRef<boolean>(false)
100
+
101
+ useEffect(() => {
102
+ if (!enabled) {
103
+ pastRef.current = []
104
+ futureRef.current = []
105
+ return
106
+ }
107
+ if (pastRef.current.length === 0) {
108
+ pastRef.current = [snapshot]
109
+ futureRef.current = []
110
+ return
111
+ }
112
+ if (applyingRef.current) {
113
+ return
114
+ }
115
+ if (!commitNextRef.current) {
116
+ return
117
+ }
118
+ commitNextRef.current = false
119
+ pastRef.current = [...pastRef.current, snapshot]
120
+ futureRef.current = []
121
+ }, [enabled, snapshot])
122
+
123
+ const applySnapshot = useCallback((next: Snapshot<TNodeData>): void => {
124
+ applyingRef.current = true
125
+ commitNextRef.current = false
126
+
127
+ const nodeChanges = onNodesChange ? buildNodeChanges(nodes, next.nodes) : []
128
+ const edgeChanges = onEdgesChange ? buildEdgeChanges(edges, next.edges) : []
129
+
130
+ if (nodeChanges.length > 0) {
131
+ onNodesChange?.(nodeChanges)
132
+ }
133
+ if (edgeChanges.length > 0) {
134
+ onEdgesChange?.(edgeChanges)
135
+ }
136
+
137
+ queueMicrotask(() => {
138
+ applyingRef.current = false
139
+ })
140
+ }, [edges, nodes, onEdgesChange, onNodesChange])
141
+
142
+ const undo = useCallback((): void => {
143
+ const past = pastRef.current
144
+ const current = past.at(-1)
145
+ const prev = past.at(-2)
146
+ if (!current || !prev) {
147
+ return
148
+ }
149
+
150
+ pastRef.current = past.slice(0, -1)
151
+ futureRef.current = [current, ...futureRef.current]
152
+ applySnapshot(prev)
153
+ }, [applySnapshot])
154
+
155
+ const redo = useCallback((): void => {
156
+ const next = futureRef.current.at(0)
157
+ if (!next) {
158
+ return
159
+ }
160
+
161
+ futureRef.current = futureRef.current.slice(1)
162
+ pastRef.current = [...pastRef.current, next]
163
+ applySnapshot(next)
164
+ }, [applySnapshot])
165
+
166
+ const onNodesChangeWrapped = useCallback((changes: Array<NodeChange<Node<TNodeData>>>): void => {
167
+ onNodesChange?.(changes)
168
+ if (!enabled) {
169
+ return
170
+ }
171
+ const shouldCommit = changes.some(change => {
172
+ if (change.type === 'position') {
173
+ return change.dragging !== true
174
+ }
175
+ return change.type === 'add' || change.type === 'remove'
176
+ })
177
+ if (shouldCommit) {
178
+ commitNextRef.current = true
179
+ }
180
+ }, [enabled, onNodesChange])
181
+
182
+ const onEdgesChangeWrapped = useCallback((changes: Array<EdgeChange<Edge>>): void => {
183
+ onEdgesChange?.(changes)
184
+ if (!enabled) {
185
+ return
186
+ }
187
+ const shouldCommit = changes.some(change => {
188
+ return change.type === 'add' || change.type === 'remove'
189
+ })
190
+ if (shouldCommit) {
191
+ commitNextRef.current = true
192
+ }
193
+ }, [enabled, onEdgesChange])
194
+
195
+ useEffect(() => {
196
+ if (!enabled) {
197
+ return
198
+ }
199
+
200
+ const onKeyDown = (event: KeyboardEvent): void => {
201
+ if (isEditableTarget(event.target)) {
202
+ return
203
+ }
204
+
205
+ const isMac = /Mac|iPhone|iPad|iPod/.test(navigator.platform)
206
+ const mod = isMac ? event.metaKey : event.ctrlKey
207
+ if (!mod) {
208
+ return
209
+ }
210
+ if (event.key.toLowerCase() !== 'z') {
211
+ return
212
+ }
213
+
214
+ event.preventDefault()
215
+ if (event.shiftKey) {
216
+ redo()
217
+ return
218
+ }
219
+ undo()
220
+ }
221
+
222
+ document.addEventListener('keydown', onKeyDown)
223
+ return () => {
224
+ document.removeEventListener('keydown', onKeyDown)
225
+ }
226
+ }, [enabled, redo, undo])
227
+
228
+ return {
229
+ onNodesChange: onNodesChangeWrapped,
230
+ onEdgesChange: onEdgesChangeWrapped,
231
+ }
232
+ }
@@ -0,0 +1,117 @@
1
+ 'use client'
2
+
3
+ import type { ReactElement } from 'react'
4
+
5
+ import { ReactFlow, type Edge, type EdgeChange, type EdgeTypes, type Node, type NodeChange, type NodeTypes } from '@xyflow/react'
6
+ import '@xyflow/react/dist/style.css'
7
+
8
+ import { useAlignmentSnap } from './hooks/useAlignmentSnap'
9
+ import { useMoveDescendants } from './hooks/useMoveDescendants'
10
+ import { useDropToReparent } from './hooks/useDropToReparent'
11
+ import { useCopyPaste } from './hooks/useCopyPaste'
12
+ import { useUndoRedo } from './hooks/useUndoRedo'
13
+ import { useExpandCollapse } from './hooks/useExpandCollapse'
14
+ import { MindMapDndProvider, useMindMapDndState } from './context'
15
+
16
+ export interface MindMapNodeData extends Record<string, unknown> {
17
+ label: string
18
+ }
19
+
20
+ export { useMindMapDndState, useExpandCollapse }
21
+
22
+ interface MindMapProps {
23
+ className?: string
24
+ enableSnap?: boolean // 是否开启对齐线与自动吸附
25
+ moveDescendants?: boolean // 是否拖动父节点时联动后代节点一起移动
26
+ enableDropToNode?: boolean // 是否允许拖拽到节点上建立父子关系
27
+ enableCopyPaste?: boolean // 是否开启复制粘贴
28
+ enableUndoRedo?: boolean // 是否开启撤销重做
29
+ dropHighlightClassName?: string // 可放下时的高亮样式
30
+ canDropOnNode?: (params: { draggingNode: Node<MindMapNodeData>, targetNode: Node<MindMapNodeData> }) => boolean // 外部控制是否可放下
31
+ nodeTypes?: NodeTypes
32
+ edgeTypes?: EdgeTypes
33
+ nodes: Array<Node<MindMapNodeData>>
34
+ edges?: Edge[]
35
+ onNodesChange?: (changes: Array<NodeChange<Node<MindMapNodeData>>>) => void
36
+ onEdgesChange?: (changes: Array<EdgeChange<Edge>>) => void
37
+ }
38
+
39
+ export const MindMap = (props: MindMapProps): ReactElement => {
40
+ const {
41
+ nodes,
42
+ edges = [],
43
+ onNodesChange,
44
+ onEdgesChange,
45
+ enableSnap = false,
46
+ moveDescendants = true,
47
+ enableDropToNode = true,
48
+ enableCopyPaste = true,
49
+ enableUndoRedo = true,
50
+ dropHighlightClassName,
51
+ canDropOnNode,
52
+ nodeTypes,
53
+ edgeTypes,
54
+ className = 'w-full h-full',
55
+ } = props
56
+
57
+ // 先把“联动后代移动”合并进节点变更回调,后续其它能力(如吸附)再基于它叠加
58
+ const { onNodesChange: onNodesChangeWithDescendants } = useMoveDescendants<MindMapNodeData>({
59
+ enabled: moveDescendants,
60
+ nodes,
61
+ edges,
62
+ onNodesChange,
63
+ })
64
+
65
+ // 在节点变更回调上叠加对齐线与吸附逻辑,并由 hook 直接给出对齐线元素
66
+ const { helperLines, onMove, onNodesChange: onNodesChangeWithSnap } = useAlignmentSnap<MindMapNodeData>({
67
+ enabled: enableSnap,
68
+ nodes,
69
+ onNodesChange: onNodesChangeWithDescendants,
70
+ })
71
+
72
+ const { onNodesChange: onNodesChangeWithUndoRedo, onEdgesChange: onEdgesChangeWithUndoRedo } = useUndoRedo<MindMapNodeData>({
73
+ enabled: enableUndoRedo,
74
+ nodes,
75
+ edges,
76
+ onNodesChange: onNodesChangeWithSnap,
77
+ onEdgesChange,
78
+ })
79
+
80
+ const { onNodeDrag, onNodeDragStop, draggingNodeId, dropTargetNodeId } = useDropToReparent<MindMapNodeData>({
81
+ enabled: enableDropToNode,
82
+ nodes,
83
+ edges,
84
+ dropHighlightClassName,
85
+ canDropOnNode,
86
+ onNodesChange: onNodesChangeWithUndoRedo,
87
+ onEdgesChange: onEdgesChangeWithUndoRedo,
88
+ })
89
+
90
+ useCopyPaste<MindMapNodeData>({
91
+ enabled: enableCopyPaste,
92
+ nodes,
93
+ edges,
94
+ onNodesChange: onNodesChangeWithUndoRedo,
95
+ onEdgesChange: onEdgesChangeWithUndoRedo,
96
+ })
97
+
98
+ return (
99
+ <MindMapDndProvider value={{ draggingNodeId, dropTargetNodeId }}>
100
+ <div className={`relative ${className}`}>
101
+ <ReactFlow
102
+ fitView
103
+ nodes={nodes}
104
+ edges={edges}
105
+ nodeTypes={nodeTypes}
106
+ edgeTypes={edgeTypes}
107
+ onNodesChange={onNodesChangeWithUndoRedo}
108
+ onEdgesChange={onEdgesChangeWithUndoRedo}
109
+ onMove={enableSnap ? onMove : undefined}
110
+ onNodeDrag={onNodeDrag}
111
+ onNodeDragStop={onNodeDragStop}
112
+ />
113
+ {helperLines}
114
+ </div>
115
+ </MindMapDndProvider>
116
+ )
117
+ }
@@ -0,0 +1,65 @@
1
+ .progressButton {
2
+ @keyframes complete {
3
+ 0% {
4
+ transform: scale(1) rotateY(0deg);
5
+ }
6
+
7
+ 20% {
8
+ transform: scale(0.9) rotateY(36deg);
9
+ }
10
+
11
+ 40% {
12
+ transform: scale(0.95) rotateY(72deg);
13
+ }
14
+
15
+ 60% {
16
+ transform: scale(1) rotateY(108deg);
17
+ }
18
+
19
+ 80% {
20
+ transform: scale(1.05) rotateY(144deg);
21
+ }
22
+
23
+ 100% {
24
+ transform: scale(1) rotateY(180deg);
25
+ }
26
+ }
27
+
28
+ @keyframes counter-rotate {
29
+ 0% {
30
+ transform: rotateY(0deg);
31
+ }
32
+
33
+ 20% {
34
+ transform: rotateY(-36deg);
35
+ }
36
+
37
+ 40% {
38
+ transform: rotateY(-72deg);
39
+ }
40
+
41
+ 60% {
42
+ transform: rotateY(-108deg);
43
+ }
44
+
45
+ 80% {
46
+ transform: rotateY(-144deg);
47
+ }
48
+
49
+ 100% {
50
+ transform: rotateY(-180deg);
51
+ }
52
+ }
53
+
54
+ .animate-complete {
55
+ animation: complete 0.6s cubic-bezier(0, 0, 1, 1);
56
+ }
57
+
58
+ .animate-counter-rotate {
59
+ animation: counter-rotate 0.6s cubic-bezier(0, 0, 1, 1);
60
+ }
61
+
62
+ .perspective {
63
+ perspective: 1000px;
64
+ }
65
+ }
@@ -0,0 +1,96 @@
1
+ import { type ReactNode } from 'react'
2
+ import styles from './index.module.scss'
3
+
4
+ interface ProgressButtonProps {
5
+ colors?: { bg: string, ring: string }
6
+ showRing?: boolean
7
+ progress?: number
8
+ padding?: number
9
+ size?: number
10
+ strokeWidth?: number
11
+ children?: ReactNode
12
+ className?: string
13
+ }
14
+
15
+ export const ProgressButton = (props: ProgressButtonProps) => {
16
+ const {
17
+ colors, // 按钮颜色配置
18
+ showRing = false, // 是否显示进度环
19
+ progress = 0, // 进度值(0-1)
20
+ padding = 0, // 内边距
21
+ strokeWidth = 5, // 进度环宽度
22
+ size = 50, // 按钮大小
23
+ children, // 子元素
24
+ className = '', // 自定义className
25
+ } = props
26
+
27
+ // 计算进度环的半径
28
+ const radius = (size / 2) - strokeWidth - padding + 2.5
29
+ // 计算进度环的周长
30
+ const circumference = 2 * Math.PI * radius
31
+ // 计算进度环的偏移量,用于显示进度
32
+ const offset = circumference * (1 - progress)
33
+
34
+ return (
35
+ <div className={styles.progressButton}>
36
+ <div
37
+ style={{ width: size, height: size }}
38
+ className={`relative rounded-full
39
+ flex items-center justify-center
40
+ cursor-pointer hover:opacity-90
41
+ focus:outline-none focus:ring-2 focus:ring-offset-2
42
+ ${colors?.bg}
43
+ ${colors?.ring}
44
+ transform-gpu transition-all duration-300
45
+ perspective
46
+ ${className}`}
47
+ >
48
+ {showRing && (
49
+ <>
50
+ <svg
51
+ className='absolute inset-0 w-full h-full'
52
+ viewBox={`0 0 ${size} ${size}`}
53
+ >
54
+ <circle
55
+ cx={size / 2}
56
+ cy={size / 2}
57
+ r={radius}
58
+ fill='none'
59
+ stroke='currentColor'
60
+ strokeWidth={strokeWidth}
61
+ className='opacity-30'
62
+ />
63
+ </svg>
64
+
65
+ <svg
66
+ className='absolute inset-0 w-full h-full -rotate-90'
67
+ viewBox={`0 0 ${size} ${size}`}
68
+ >
69
+ <circle
70
+ cx={size / 2}
71
+ cy={size / 2}
72
+ r={radius}
73
+ fill='none'
74
+ stroke='currentColor'
75
+ strokeWidth={strokeWidth}
76
+ strokeDasharray={circumference}
77
+ strokeDashoffset={offset}
78
+ strokeLinecap='round'
79
+ className='transition-all duration-300'
80
+ />
81
+ </svg>
82
+ </>
83
+ )}
84
+
85
+ <div
86
+ className={`
87
+ relative z-10 text-white text-xs text-center whitespace-pre select-none
88
+ ${className ? 'animate-counter-rotate' : ''}
89
+ `}
90
+ >
91
+ {children}
92
+ </div>
93
+ </div>
94
+ </div>
95
+ )
96
+ }
@@ -0,0 +1,64 @@
1
+ import React from 'react'
2
+ import type { ReactElement } from 'react'
3
+
4
+ import { dayjs } from '@/utils'
5
+ import { HEIGHT } from '../../const'
6
+ import { useHighlightRange } from '../../hooks'
7
+
8
+ interface CurrentWeekHighlightProps {
9
+ originTime: number
10
+ timePxPerDay: number
11
+ }
12
+
13
+ export const CurrentWeekHighlight = (props: CurrentWeekHighlightProps): ReactElement | null => {
14
+ const { originTime, timePxPerDay } = props
15
+
16
+ const today = dayjs().startOf('day')
17
+
18
+ const weekStart = today.startOf('week').startOf('day')
19
+ const weekEnd = weekStart.add(1, 'week')
20
+
21
+ const currentWeekHighlight = useHighlightRange({
22
+ originTime,
23
+ timePxPerDay,
24
+ startTime: weekStart.valueOf(),
25
+ endTime: weekEnd.valueOf(),
26
+ })
27
+
28
+ const monthStart = today.startOf('month').startOf('day')
29
+ const monthEnd = monthStart.add(1, 'month')
30
+
31
+ const currentMonthHighlight = useHighlightRange({
32
+ originTime,
33
+ timePxPerDay,
34
+ startTime: monthStart.valueOf(),
35
+ endTime: monthEnd.valueOf(),
36
+ })
37
+
38
+ return (
39
+ <>
40
+ <div
41
+ style={{
42
+ position: 'absolute',
43
+ top: 0,
44
+ left: currentMonthHighlight.left,
45
+ width: currentMonthHighlight.width,
46
+ height: HEIGHT,
47
+ background: 'linear-gradient(180deg, rgba(0,0,0,0.04), rgba(0,0,0,0.04))',
48
+ pointerEvents: 'none',
49
+ }}
50
+ />
51
+ <div
52
+ style={{
53
+ position: 'absolute',
54
+ top: 0,
55
+ left: currentWeekHighlight.left,
56
+ width: currentWeekHighlight.width,
57
+ height: HEIGHT,
58
+ background: 'linear-gradient(180deg, rgba(0,0,0,0.05), rgba(0,0,0,0.05))',
59
+ pointerEvents: 'none',
60
+ }}
61
+ />
62
+ </>
63
+ )
64
+ }
@@ -0,0 +1,61 @@
1
+ import React from 'react'
2
+ import type { CSSProperties, ReactElement } from 'react'
3
+
4
+ import { HEIGHT } from '../../const'
5
+ import { useMonthGuides, useWeekGuides } from '../../hooks'
6
+
7
+ interface MonthGuidesProps {
8
+ originTime: number
9
+ visibleDates: Date[]
10
+ timePxPerDay: number
11
+ }
12
+
13
+ const baseStyles: CSSProperties = {
14
+ position: 'absolute',
15
+ pointerEvents: 'none',
16
+ width: 1,
17
+ background: '#cbd5e1',
18
+ }
19
+
20
+ export const Guides = (props: MonthGuidesProps): ReactElement => {
21
+ const { originTime, visibleDates, timePxPerDay } = props
22
+
23
+ const monthGuides = useMonthGuides({
24
+ originTime,
25
+ visibleDates,
26
+ timePxPerDay,
27
+ })
28
+
29
+ const weekGuides = useWeekGuides({
30
+ originTime,
31
+ visibleDates,
32
+ timePxPerDay,
33
+ })
34
+
35
+ return (
36
+ <>
37
+ {weekGuides.map(week => (
38
+ <div
39
+ key={`week-line-${week.key}`}
40
+ style={{
41
+ ...baseStyles,
42
+ bottom: 0,
43
+ height: 20,
44
+ left: week.offset,
45
+ }}
46
+ />
47
+ ))}
48
+ {monthGuides.map(month => (
49
+ <div
50
+ key={`month-line-${month.key}`}
51
+ style={{
52
+ ...baseStyles,
53
+ top: 0,
54
+ height: HEIGHT / 1.75,
55
+ left: month.offset,
56
+ }}
57
+ />
58
+ ))}
59
+ </>
60
+ )
61
+ }
@@ -0,0 +1,56 @@
1
+ import React, { type ReactElement, useMemo } from 'react'
2
+
3
+ import { dayjs } from '@/utils'
4
+ import { WEEKDAY_LABELS } from '../../const'
5
+ import { calcOffsetByTime } from '../../utils'
6
+
7
+ interface TicksProps {
8
+ originTime: number
9
+ timePxPerDay: number
10
+ visibleDates: Date[]
11
+ }
12
+
13
+ export const Ticks = (props: TicksProps): ReactElement => {
14
+ const { visibleDates, timePxPerDay, originTime } = props
15
+
16
+ const ticks = useMemo(() => {
17
+ return visibleDates.map(value => {
18
+ const d = dayjs(value)
19
+ const time = d.valueOf()
20
+
21
+ return {
22
+ time,
23
+ monthLabel: d.date() === 15 ? d.format('YYYY-M') : '',
24
+ weekLabel: WEEKDAY_LABELS[d.day()],
25
+ dayLabel: d.format('DD'),
26
+ offset: calcOffsetByTime({ originTime, time, timePxPerDay })
27
+ }
28
+ })
29
+ }, [visibleDates])
30
+
31
+ return (
32
+ <>
33
+ {ticks.map(tick => (
34
+ <div key={tick.time}>
35
+ <div
36
+ className="absolute text-center pt-[2] pb-[4] top-0 h-full text-[11px] leading-[12px] text-slate-600 whitespace-nowrap pointer-events-none"
37
+ style={{ left: tick.offset, width: timePxPerDay }}
38
+ >
39
+ <div className="flex h-full flex-col">
40
+ <div className="flex-1 text-[10px] text-slate-500">
41
+ {tick.monthLabel || ''}
42
+ </div>
43
+ <div className="flex-1 text-[11px] text-slate-600">
44
+ {tick.dayLabel}
45
+ </div>
46
+ <div className={'flex-1 text-[10px] text-slate-500'}>
47
+ {tick.weekLabel}
48
+ </div>
49
+ </div>
50
+ </div>
51
+ </div>
52
+ )
53
+ )}
54
+ </>
55
+ )
56
+ }