@gadmin2n/schematics 0.0.111 → 0.0.112

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.
@@ -0,0 +1,449 @@
1
+ import React, { useCallback, useMemo, useState } from 'react';
2
+ import { Input, Popover, Tooltip, Button } from 'antd';
3
+ import {
4
+ SearchOutlined,
5
+ CaretDownOutlined,
6
+ CaretRightOutlined,
7
+ AppstoreOutlined,
8
+ MenuFoldOutlined,
9
+ MenuUnfoldOutlined,
10
+ } from '@ant-design/icons';
11
+ import ComponentThumbnail from './ComponentThumbnail';
12
+ import { CANVAS_COMPONENTS, CANVAS_DEFAULTS } from './canvasDefaults';
13
+ import { useTranslation } from 'react-i18next';
14
+ import { getComponentLabel, getVariantLabel } from './canvasI18n';
15
+
16
+ // ─── 组件分类 ─────────────────────────────────────────────────────────────────
17
+
18
+ interface CategoryDef {
19
+ key: string;
20
+ /** i18n key under canvas.libraryCategories.* */
21
+ labelKey: string;
22
+ defaultLabel: string;
23
+ /** 包含的组件 type,按显示顺序 */
24
+ types: string[];
25
+ }
26
+
27
+ const CATEGORIES: CategoryDef[] = [
28
+ {
29
+ key: 'data',
30
+ labelKey: 'data',
31
+ defaultLabel: '数据展示',
32
+ types: ['NumCard', 'Table'],
33
+ },
34
+ {
35
+ key: 'chart',
36
+ labelKey: 'chart',
37
+ defaultLabel: '图表',
38
+ types: ['BarChart', 'LineChart', 'PieChart', 'RadarChart', 'TreemapChart'],
39
+ },
40
+ {
41
+ key: 'map',
42
+ labelKey: 'map',
43
+ defaultLabel: '地图',
44
+ types: ['WorldMap'],
45
+ },
46
+ ];
47
+
48
+ // ─── Props ───────────────────────────────────────────────────────────────────
49
+
50
+ interface CanvasComponentLibraryProps {
51
+ /** 是否折叠成窄条(仅显示展开按钮) */
52
+ collapsed: boolean;
53
+ onToggleCollapsed: () => void;
54
+ /** 点击 item 时直接添加到画布。code 为指定 variant 代码,留空走默认 */
55
+ onAdd: (componentType: string, code?: string) => void;
56
+ }
57
+
58
+ // ─── Component ───────────────────────────────────────────────────────────────
59
+
60
+ const CanvasComponentLibrary: React.FC<CanvasComponentLibraryProps> = ({
61
+ collapsed,
62
+ onToggleCollapsed,
63
+ onAdd,
64
+ }) => {
65
+ const { t } = useTranslation();
66
+ const [keyword, setKeyword] = useState('');
67
+ const [openCats, setOpenCats] = useState<Record<string, boolean>>({
68
+ data: true,
69
+ chart: true,
70
+ map: true,
71
+ });
72
+
73
+ const handleVariantDragStart = useCallback(
74
+ (e: React.DragEvent, componentType: string, variantCode: string) => {
75
+ e.dataTransfer.setData('componentType', componentType);
76
+ e.dataTransfer.setData('variantCode', variantCode);
77
+ e.dataTransfer.setData(
78
+ `x-component-type/${componentType.toLowerCase()}`,
79
+ '',
80
+ );
81
+ e.dataTransfer.effectAllowed = 'copy';
82
+ },
83
+ [],
84
+ );
85
+
86
+ // ── 关键字过滤 ──
87
+ const filteredCats = useMemo(() => {
88
+ const kw = keyword.trim().toLowerCase();
89
+ return CATEGORIES.map((cat) => ({
90
+ ...cat,
91
+ types: cat.types.filter((type) => {
92
+ if (!CANVAS_COMPONENTS.includes(type)) return false;
93
+ if (!kw) return true;
94
+ const label = getComponentLabel(type, t).toLowerCase();
95
+ return label.includes(kw) || type.toLowerCase().includes(kw);
96
+ }),
97
+ })).filter((cat) => cat.types.length > 0);
98
+ }, [keyword, t]);
99
+
100
+ // ── 折叠态:窄条只展示展开按钮 ──
101
+ if (collapsed) {
102
+ return (
103
+ <div
104
+ style={{
105
+ width: 40,
106
+ flexShrink: 0,
107
+ background: '#fff',
108
+ borderRight: '1px solid #eaeaea',
109
+ display: 'flex',
110
+ flexDirection: 'column',
111
+ alignItems: 'center',
112
+ paddingTop: 12,
113
+ }}
114
+ >
115
+ <Tooltip
116
+ title={t('canvas.library.expand', { defaultValue: '展开组件库' })}
117
+ placement="right"
118
+ >
119
+ <Button
120
+ type="text"
121
+ size="small"
122
+ icon={<MenuUnfoldOutlined />}
123
+ onClick={onToggleCollapsed}
124
+ />
125
+ </Tooltip>
126
+ <div
127
+ style={{
128
+ marginTop: 8,
129
+ color: '#bbb',
130
+ fontSize: 11,
131
+ writingMode: 'vertical-rl',
132
+ letterSpacing: 2,
133
+ }}
134
+ >
135
+ <AppstoreOutlined style={{ marginRight: 4 }} />
136
+ {t('canvas.componentLib')}
137
+ </div>
138
+ </div>
139
+ );
140
+ }
141
+
142
+ return (
143
+ <div
144
+ style={{
145
+ width: 240,
146
+ flexShrink: 0,
147
+ background: '#fff',
148
+ borderRight: '1px solid #eaeaea',
149
+ display: 'flex',
150
+ flexDirection: 'column',
151
+ overflow: 'hidden',
152
+ }}
153
+ >
154
+ {/* ── Header ── */}
155
+ <div
156
+ style={{
157
+ display: 'flex',
158
+ alignItems: 'center',
159
+ justifyContent: 'space-between',
160
+ padding: '10px 12px',
161
+ borderBottom: '1px solid #f0f0f0',
162
+ }}
163
+ >
164
+ <span
165
+ style={{
166
+ fontSize: 13,
167
+ fontWeight: 600,
168
+ color: '#1a1a1a',
169
+ display: 'inline-flex',
170
+ alignItems: 'center',
171
+ gap: 6,
172
+ }}
173
+ >
174
+ <AppstoreOutlined style={{ color: '#4361ee' }} />
175
+ {t('canvas.componentLib')}
176
+ </span>
177
+ <Tooltip title={t('canvas.library.collapse', { defaultValue: '收起' })}>
178
+ <Button
179
+ type="text"
180
+ size="small"
181
+ icon={<MenuFoldOutlined />}
182
+ onClick={onToggleCollapsed}
183
+ />
184
+ </Tooltip>
185
+ </div>
186
+
187
+ {/* ── Search ── */}
188
+ <div style={{ padding: '8px 12px' }}>
189
+ <Input
190
+ allowClear
191
+ size="small"
192
+ placeholder={t('canvas.library.searchPlaceholder', {
193
+ defaultValue: '搜索组件',
194
+ })}
195
+ prefix={<SearchOutlined style={{ color: '#bfbfbf' }} />}
196
+ value={keyword}
197
+ onChange={(e) => setKeyword(e.target.value)}
198
+ />
199
+ </div>
200
+
201
+ {/* ── Categories ── */}
202
+ <div style={{ flex: 1, overflowY: 'auto', padding: '4px 8px 12px' }}>
203
+ {filteredCats.length === 0 && (
204
+ <div
205
+ style={{
206
+ textAlign: 'center',
207
+ color: '#bfbfbf',
208
+ fontSize: 12,
209
+ padding: '24px 0',
210
+ }}
211
+ >
212
+ {t('canvas.library.empty', { defaultValue: '没有匹配的组件' })}
213
+ </div>
214
+ )}
215
+ {filteredCats.map((cat) => {
216
+ const open = openCats[cat.key] ?? true;
217
+ return (
218
+ <div key={cat.key} style={{ marginBottom: 4 }}>
219
+ {/* Category header */}
220
+ <div
221
+ onClick={() =>
222
+ setOpenCats((prev) => ({ ...prev, [cat.key]: !open }))
223
+ }
224
+ style={{
225
+ display: 'flex',
226
+ alignItems: 'center',
227
+ gap: 4,
228
+ padding: '6px 4px',
229
+ cursor: 'pointer',
230
+ userSelect: 'none',
231
+ fontSize: 12,
232
+ fontWeight: 600,
233
+ color: '#666',
234
+ }}
235
+ >
236
+ {open ? <CaretDownOutlined /> : <CaretRightOutlined />}
237
+ {t(`canvas.library.categories.${cat.labelKey}`, {
238
+ defaultValue: cat.defaultLabel,
239
+ })}
240
+ <span
241
+ style={{ color: '#bfbfbf', fontWeight: 400, marginLeft: 2 }}
242
+ >
243
+ ({cat.types.length})
244
+ </span>
245
+ </div>
246
+ {/* Items */}
247
+ {open && (
248
+ <div
249
+ style={{
250
+ display: 'grid',
251
+ gridTemplateColumns: '1fr 1fr',
252
+ gap: 6,
253
+ padding: '4px 0',
254
+ }}
255
+ >
256
+ {cat.types.map((type) => (
257
+ <LibraryItem
258
+ key={type}
259
+ type={type}
260
+ onDragStart={handleVariantDragStart}
261
+ onAdd={onAdd}
262
+ />
263
+ ))}
264
+ </div>
265
+ )}
266
+ </div>
267
+ );
268
+ })}
269
+ </div>
270
+ </div>
271
+ );
272
+ };
273
+
274
+ export default CanvasComponentLibrary;
275
+
276
+ // ─── LibraryItem ─────────────────────────────────────────────────────────────
277
+
278
+ interface LibraryItemProps {
279
+ type: string;
280
+ onDragStart: (
281
+ e: React.DragEvent,
282
+ componentType: string,
283
+ variantCode: string,
284
+ ) => void;
285
+ onAdd: (componentType: string, code?: string) => void;
286
+ }
287
+
288
+ const LibraryItem: React.FC<LibraryItemProps> = ({
289
+ type,
290
+ onDragStart,
291
+ onAdd,
292
+ }) => {
293
+ const { t } = useTranslation();
294
+ const def = CANVAS_DEFAULTS[type];
295
+ const variants = def?.variants ?? [];
296
+ const hasMultiVariant = variants.length > 1;
297
+ const primaryCode = variants[0]?.code ?? def?.code ?? '';
298
+
299
+ const card = (
300
+ <Tooltip
301
+ title={t('canvas.library.itemTooltip', {
302
+ defaultValue: '点击添加,或拖到画布',
303
+ })}
304
+ placement="right"
305
+ mouseEnterDelay={0.4}
306
+ >
307
+ <div
308
+ draggable
309
+ onDragStart={(e) => onDragStart(e, type, primaryCode)}
310
+ onClick={() => onAdd(type, primaryCode)}
311
+ style={{
312
+ display: 'flex',
313
+ flexDirection: 'column',
314
+ alignItems: 'center',
315
+ gap: 4,
316
+ padding: 6,
317
+ cursor: 'pointer',
318
+ userSelect: 'none',
319
+ border: '1px solid #ececec',
320
+ borderRadius: 8,
321
+ background: '#fff',
322
+ transition: 'all 160ms ease',
323
+ }}
324
+ onMouseEnter={(e) => {
325
+ e.currentTarget.style.borderColor = '#4361ee';
326
+ e.currentTarget.style.boxShadow = '0 3px 10px rgba(67,97,238,0.10)';
327
+ e.currentTarget.style.transform = 'translateY(-1px)';
328
+ }}
329
+ onMouseLeave={(e) => {
330
+ e.currentTarget.style.borderColor = '#ececec';
331
+ e.currentTarget.style.boxShadow = 'none';
332
+ e.currentTarget.style.transform = 'translateY(0)';
333
+ }}
334
+ >
335
+ <div
336
+ style={{
337
+ border: '1px solid #f0f0f0',
338
+ borderRadius: 6,
339
+ overflow: 'hidden',
340
+ background: '#fafafa',
341
+ width: 96,
342
+ }}
343
+ >
344
+ <ComponentThumbnail componentType={type} width={96} />
345
+ </div>
346
+ <span
347
+ style={{
348
+ fontSize: 11,
349
+ color: '#555',
350
+ fontWeight: 500,
351
+ textAlign: 'center',
352
+ pointerEvents: 'none',
353
+ maxWidth: '100%',
354
+ overflow: 'hidden',
355
+ textOverflow: 'ellipsis',
356
+ whiteSpace: 'nowrap',
357
+ }}
358
+ >
359
+ {getComponentLabel(type, t)}
360
+ </span>
361
+ </div>
362
+ </Tooltip>
363
+ );
364
+
365
+ if (!hasMultiVariant) return card;
366
+
367
+ // 多 variant:hover popover 展示子样式
368
+ const popoverContent = (
369
+ <div
370
+ style={{
371
+ display: 'flex',
372
+ alignItems: 'center',
373
+ gap: 12,
374
+ padding: 4,
375
+ maxWidth: 460,
376
+ flexWrap: 'wrap',
377
+ }}
378
+ >
379
+ {variants.map((v) => (
380
+ <div
381
+ key={v.name}
382
+ draggable
383
+ onDragStart={(e) => onDragStart(e, type, v.code)}
384
+ onClick={() => onAdd(type, v.code)}
385
+ style={{
386
+ display: 'flex',
387
+ flexDirection: 'column',
388
+ alignItems: 'center',
389
+ gap: 4,
390
+ cursor: 'pointer',
391
+ userSelect: 'none',
392
+ }}
393
+ >
394
+ <div
395
+ style={{
396
+ border: '1px solid #e8e8e8',
397
+ borderRadius: 6,
398
+ overflow: 'hidden',
399
+ background: '#fff',
400
+ transition: 'all 160ms ease',
401
+ }}
402
+ onMouseEnter={(e) => {
403
+ e.currentTarget.style.borderColor = '#4361ee';
404
+ e.currentTarget.style.transform = 'translateY(-2px)';
405
+ }}
406
+ onMouseLeave={(e) => {
407
+ e.currentTarget.style.borderColor = '#e8e8e8';
408
+ e.currentTarget.style.transform = 'translateY(0)';
409
+ }}
410
+ >
411
+ <ComponentThumbnail
412
+ componentType={type}
413
+ code={v.code}
414
+ width={120}
415
+ />
416
+ </div>
417
+ <span
418
+ style={{
419
+ fontSize: 10,
420
+ color: '#666',
421
+ fontWeight: 500,
422
+ textAlign: 'center',
423
+ pointerEvents: 'none',
424
+ }}
425
+ >
426
+ {getVariantLabel(v.label, t)}
427
+ </span>
428
+ </div>
429
+ ))}
430
+ </div>
431
+ );
432
+
433
+ return (
434
+ <Popover
435
+ content={popoverContent}
436
+ title={
437
+ <span style={{ fontSize: 12, fontWeight: 600, color: '#333' }}>
438
+ {getComponentLabel(type, t)}
439
+ </span>
440
+ }
441
+ trigger="hover"
442
+ placement="rightTop"
443
+ mouseEnterDelay={0.2}
444
+ mouseLeaveDelay={0.3}
445
+ >
446
+ {card}
447
+ </Popover>
448
+ );
449
+ };