@wzyjs/uis 0.3.10 → 0.3.16
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.
- package/package.json +2 -2
- package/src/antd/form/FileUploader/index.tsx +163 -0
- package/src/antd/form/RadioButton/index.tsx +3 -1
- package/src/antd/form/index.ts +1 -0
- package/src/antd/index.ts +2 -0
- package/src/components/Crud/components/CardList/index.tsx +174 -0
- package/src/components/Crud/components/CreateUpdate/index.tsx +99 -0
- package/src/components/Crud/components/Provider/index.tsx +73 -0
- package/src/components/Crud/components/Remove/index.tsx +56 -0
- package/src/components/Crud/components/index.ts +4 -0
- package/src/components/Crud/hooks/index.ts +4 -0
- package/src/components/Crud/hooks/useColumns.tsx +169 -0
- package/src/components/Crud/hooks/useList.ts +54 -0
- package/src/components/Crud/hooks/useOrderable.tsx +107 -0
- package/src/components/Crud/hooks/useRequest.ts +41 -0
- package/src/components/Crud/index.tsx +91 -0
- package/src/components/Crud/types/index.ts +188 -0
- package/src/components/Crud/utils/index.ts +87 -0
- package/src/components/MindMap/context.tsx +29 -0
- package/src/components/MindMap/hooks/useAlignmentSnap.ts +220 -0
- package/src/components/MindMap/hooks/useCopyPaste.ts +272 -0
- package/src/components/MindMap/hooks/useDropToReparent.ts +288 -0
- package/src/components/MindMap/hooks/useExpandCollapse.ts +146 -0
- package/src/components/MindMap/hooks/useMoveDescendants.ts +136 -0
- package/src/components/MindMap/hooks/useUndoRedo.ts +232 -0
- package/src/components/MindMap/index.tsx +117 -0
- package/src/components/ProgressButton/index.module.scss +65 -0
- package/src/components/ProgressButton/index.tsx +96 -0
- package/src/components/TimelineBar/components/CurrentWeekHighlight/index.tsx +64 -0
- package/src/components/TimelineBar/components/Guides/index.tsx +61 -0
- package/src/components/TimelineBar/components/Ticks/index.tsx +56 -0
- package/src/components/TimelineBar/components/TodayIndicator/index.tsx +54 -0
- package/src/components/TimelineBar/components/index.ts +4 -0
- package/src/components/TimelineBar/const.ts +3 -0
- package/src/components/TimelineBar/hooks/index.ts +5 -0
- package/src/components/TimelineBar/hooks/useHighlightRange.ts +21 -0
- package/src/components/TimelineBar/hooks/useMonthGuides.ts +40 -0
- package/src/components/TimelineBar/hooks/useTickValues.ts +18 -0
- package/src/components/TimelineBar/hooks/useVisibleRange.ts +43 -0
- package/src/components/TimelineBar/hooks/useWeekGuides.ts +39 -0
- package/src/components/TimelineBar/index.tsx +63 -0
- package/src/components/TimelineBar/utils.ts +27 -0
- package/src/components/index.ts +4 -0
- package/src/rn.ts +1 -0
- 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
|
+
}
|