@mindbase/mindbase 1.0.0

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/LICENSE +201 -0
  2. package/bin/gitlog.ts +3 -0
  3. package/bin/iconfont-editor.ts +3 -0
  4. package/bin/index.ts +29 -0
  5. package/bin/nodeclear.ts +3 -0
  6. package/bin/npmpublish.ts +3 -0
  7. package/bin/shared.ts +141 -0
  8. package/package.json +63 -0
  9. package/scripts/ensure-tsx.cjs +18 -0
  10. package/src/clear/app.ts +152 -0
  11. package/src/clear/config.ts +108 -0
  12. package/src/clear/file-cleaner.ts +137 -0
  13. package/src/clear/index.ts +28 -0
  14. package/src/clear/scanner.ts +154 -0
  15. package/src/gitlog/app.ts +272 -0
  16. package/src/gitlog/config.ts +91 -0
  17. package/src/gitlog/display.ts +178 -0
  18. package/src/gitlog/index.ts +5 -0
  19. package/src/gitlog/log-fetcher.ts +150 -0
  20. package/src/gitlog/pager.ts +178 -0
  21. package/src/gitlog/scanner.ts +60 -0
  22. package/src/iconfont/frontend/index.html +12 -0
  23. package/src/iconfont/frontend/src/App.vue +14 -0
  24. package/src/iconfont/frontend/src/main.ts +8 -0
  25. package/src/iconfont/frontend/src/views/IconEditor.vue +819 -0
  26. package/src/iconfont/frontend/vite.config.ts +15 -0
  27. package/src/iconfont/lib/css-generator.js +37 -0
  28. package/src/iconfont/lib/font-builder.js +82 -0
  29. package/src/iconfont/lib/glyph-extractor.js +69 -0
  30. package/src/iconfont/server/api.js +256 -0
  31. package/src/iconfont/server/index.js +64 -0
  32. package/src/index.ts +0 -0
  33. package/src/publish/app.ts +316 -0
  34. package/src/publish/builder.ts +120 -0
  35. package/src/publish/dependency.ts +144 -0
  36. package/src/publish/detector.ts +93 -0
  37. package/src/publish/index.ts +66 -0
  38. package/src/publish/npm-query.ts +244 -0
  39. package/src/publish/registry/adapters/npm.ts +128 -0
  40. package/src/publish/registry/registry-manager.ts +107 -0
  41. package/src/publish/scanner.ts +90 -0
  42. package/src/publish/types.ts +98 -0
  43. package/src/publish/version.ts +108 -0
  44. package/src/shared/config-manager.ts +57 -0
  45. package/src/shared/index.ts +1 -0
@@ -0,0 +1,819 @@
1
+ <template>
2
+ <div
3
+ class="icon-editor"
4
+ @dragover.prevent="onDragOver"
5
+ @dragleave.prevent="onDragLeave"
6
+ @drop.prevent="onDrop">
7
+
8
+ <!-- 全屏拖拽遮罩 -->
9
+ <div v-if="isDragging" class="drop-overlay">
10
+ <div class="drop-hint">
11
+ <el-icon :size="64"><Upload /></el-icon>
12
+ <p>释放以添加 SVG 图标</p>
13
+ </div>
14
+ </div>
15
+
16
+ <!-- 顶部工具栏 -->
17
+ <div class="toolbar">
18
+ <el-autocomplete
19
+ v-model="searchQuery"
20
+ :fetch-suggestions="querySearch"
21
+ placeholder="搜索图标名称或 class..."
22
+ clearable
23
+ style="width: 300px; margin-right: 16px"
24
+ @select="onSearchSelect" />
25
+ <div class="actions">
26
+ <el-button type="primary" @click="triggerFileUpload">
27
+ 上传 SVG
28
+ </el-button>
29
+ <input
30
+ ref="fileInputRef"
31
+ type="file"
32
+ accept=".svg"
33
+ multiple
34
+ style="display: none"
35
+ @change="onFileSelected" />
36
+ <el-button type="success" @click="downloadCSS">
37
+ 生成 CSS
38
+ </el-button>
39
+ <el-button type="warning" @click="saveToSource">
40
+ 保存到源目录
41
+ </el-button>
42
+ <el-button
43
+ type="danger"
44
+ @click="confirmDelete"
45
+ :disabled="selectedIndices.size === 0">
46
+ 删除图标
47
+ </el-button>
48
+ </div>
49
+ </div>
50
+
51
+ <!-- 主内容区 -->
52
+ <div class="main-content">
53
+ <!-- 左侧图标网格 -->
54
+ <div class="icon-grid-container">
55
+ <div class="grid-header">
56
+ <span>图标列表 ({{ json.glyphs.length }})</span>
57
+ <span class="hint">Ctrl+V 粘贴 SVG | 拖拽 SVG 到页面 | 方向键选择 | Shift+方向键移动</span>
58
+ </div>
59
+ <div class="icon-grid" ref="gridRef">
60
+ <div
61
+ v-for="(glyph, index) in json.glyphs"
62
+ :key="glyph.icon_id"
63
+ :ref="(el: any) => setCardRef(el, index)"
64
+ :class="['icon-card', { selected: selectedIndices.has(index) }]"
65
+ @click="selectIndex(index, $event.ctrlKey)"
66
+ tabindex="0">
67
+ <div class="icon-preview">
68
+ <i :class="`${fontFamily} ${json.css_prefix_text}${glyph.font_class}`"></i>
69
+ </div>
70
+ <div class="icon-info">
71
+ <div class="icon-name" :title="glyph.name">{{ glyph.name }}</div>
72
+ <div class="icon-class">{{ glyph.font_class }}</div>
73
+ </div>
74
+ </div>
75
+ </div>
76
+ </div>
77
+
78
+ <!-- 右侧编辑面板 -->
79
+ <div class="edit-panel">
80
+ <div class="panel-header">编辑图标</div>
81
+ <div class="panel-content" v-if="selectedGlyph">
82
+ <div class="preview-large">
83
+ <i :class="`${fontFamily} ${json.css_prefix_text}${selectedGlyph.font_class}`"></i>
84
+ </div>
85
+
86
+ <el-form label-width="80px">
87
+ <el-form-item label="名称">
88
+ <el-input v-model="selectedGlyph.name" />
89
+ </el-form-item>
90
+ <el-form-item label="Class">
91
+ <el-input v-model="selectedGlyph.font_class" />
92
+ </el-form-item>
93
+ <el-form-item label="Unicode">
94
+ <el-input :model-value="selectedGlyph.unicode" disabled />
95
+ </el-form-item>
96
+ </el-form>
97
+
98
+ <div class="panel-actions">
99
+ <el-button type="primary" @click="exportSvg" :loading="exportingSvg">
100
+ 导出 SVG
101
+ </el-button>
102
+ </div>
103
+ </div>
104
+ </div>
105
+ </div>
106
+
107
+ <!-- 底部状态栏 -->
108
+ <div class="status-bar">
109
+ <span>共 {{ json.glyphs.length }} 个图标</span>
110
+ <span v-if="hasChanges" class="modified">已修改</span>
111
+ </div>
112
+ </div>
113
+ </template>
114
+
115
+ <script setup lang="ts">
116
+ import { ref, computed, onMounted, onUnmounted, nextTick, watch } from "vue";
117
+ import { ElMessage, ElMessageBox } from "element-plus";
118
+ import { Upload } from "@element-plus/icons-vue";
119
+
120
+ interface Glyph {
121
+ icon_id: string;
122
+ name: string;
123
+ font_class: string;
124
+ unicode: string;
125
+ unicode_decimal: number;
126
+ }
127
+
128
+ interface IconfontJson {
129
+ id: string;
130
+ name: string;
131
+ font_family: string;
132
+ css_prefix_text: string;
133
+ description: string;
134
+ glyphs: Glyph[];
135
+ }
136
+
137
+ // 状态
138
+ const json = ref<IconfontJson>({
139
+ id: "", name: "", font_family: "", css_prefix_text: "", description: "", glyphs: [],
140
+ });
141
+ const originalJson = ref("");
142
+ const searchQuery = ref("");
143
+ const selectedIndices = ref<Set<number>>(new Set());
144
+ const gridRef = ref<HTMLElement | null>(null);
145
+ const cardRefs = ref<Map<number, HTMLElement>>(new Map());
146
+ const columnsPerRow = ref(1);
147
+ const isDragging = ref(false);
148
+ const fileInputRef = ref<HTMLInputElement | null>(null);
149
+ const exportingSvg = ref(false);
150
+ let resizeObserver: ResizeObserver | null = null;
151
+
152
+ // 字体族名(从 json 动态读取)
153
+ const fontFamily = computed(() => json.value.font_family || "iconfont");
154
+
155
+ // 选中项
156
+ const selectedIndex = computed(() => {
157
+ if (selectedIndices.value.size !== 1) return null;
158
+ return [...selectedIndices.value][0];
159
+ });
160
+
161
+ const selectedGlyph = computed(() => {
162
+ const idx = selectedIndex.value;
163
+ if (idx === null) return null;
164
+ return json.value.glyphs[idx];
165
+ });
166
+
167
+ const hasChanges = computed(() => JSON.stringify(json.value) !== originalJson.value);
168
+
169
+ // ========== 选中/搜索 ==========
170
+ function selectIndex(index: number, isCtrl: boolean) {
171
+ if (isCtrl) {
172
+ selectedIndices.value.has(index)
173
+ ? selectedIndices.value.delete(index)
174
+ : selectedIndices.value.add(index);
175
+ } else {
176
+ selectedIndices.value = new Set([index]);
177
+ }
178
+ }
179
+
180
+ function setCardRef(el: any, index: number) {
181
+ if (el) cardRefs.value.set(index, el as HTMLElement);
182
+ }
183
+
184
+ function querySearch(queryString: string, cb: any) {
185
+ const query = queryString.toLowerCase();
186
+ const results = json.value.glyphs
187
+ .filter((g) => g.name.toLowerCase().includes(query) || g.font_class.toLowerCase().includes(query))
188
+ .map((g) => ({ value: `${g.name} (${g.font_class})`, glyph: g }));
189
+ cb(results);
190
+ }
191
+
192
+ function onSearchSelect(item: any) {
193
+ const idx = json.value.glyphs.findIndex((g) => g.icon_id === item.glyph.icon_id);
194
+ if (idx === -1) return;
195
+ selectedIndices.value = new Set([idx]);
196
+ searchQuery.value = "";
197
+ requestAnimationFrame(() => {
198
+ cardRefs.value.get(idx)?.scrollIntoView({ behavior: "smooth", block: "center" });
199
+ });
200
+ }
201
+
202
+ function scrollToSelected() {
203
+ if (selectedIndex.value === null) return;
204
+ requestAnimationFrame(() => {
205
+ cardRefs.value.get(selectedIndex.value!)?.scrollIntoView({ behavior: "smooth", block: "nearest" });
206
+ });
207
+ }
208
+
209
+ // ========== 列数计算 ==========
210
+ function calculateColumnsPerRow() {
211
+ if (!gridRef.value || json.value.glyphs.length === 0) {
212
+ columnsPerRow.value = 1;
213
+ return;
214
+ }
215
+ const containerWidth = gridRef.value.clientWidth;
216
+ const cardMinWidth = 120;
217
+ const gap = 12;
218
+ let estimated = Math.floor((containerWidth + gap) / (cardMinWidth + gap)) || 1;
219
+
220
+ const firstCard = cardRefs.value.get(0);
221
+ if (firstCard) {
222
+ const firstRect = firstCard.getBoundingClientRect();
223
+ const testCard = cardRefs.value.get(estimated);
224
+ if (testCard && Math.abs(testCard.getBoundingClientRect().top - firstRect.top) < 1) {
225
+ while (estimated < json.value.glyphs.length - 1) {
226
+ estimated++;
227
+ const nextCard = cardRefs.value.get(estimated);
228
+ if (nextCard && Math.abs(nextCard.getBoundingClientRect().top - firstRect.top) > 1) {
229
+ estimated--;
230
+ break;
231
+ }
232
+ }
233
+ }
234
+ }
235
+ columnsPerRow.value = Math.max(1, Math.min(estimated, json.value.glyphs.length));
236
+ }
237
+
238
+ // ========== CSS 生成/下载 ==========
239
+ function generateCSS(data: IconfontJson): string {
240
+ const t = Date.now();
241
+ const ff = data.font_family || "iconfont";
242
+ const prefix = data.css_prefix_text || "icon-";
243
+ let css = `@font-face {\n font-family: "${ff}";\n`;
244
+ css += ` src: url('iconfont.woff2?t=${t}') format('woff2'),\n`;
245
+ css += ` url('iconfont.woff?t=${t}') format('woff'),\n`;
246
+ css += ` url('iconfont.ttf?t=${t}') format('truetype');\n}\n\n`;
247
+ css += `.${ff} {\n font-family: "${ff}" !important;\n font-size: 16px;\n font-style: normal;\n`;
248
+ css += ` -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n\n`;
249
+ for (const g of data.glyphs) {
250
+ css += `.${prefix}${g.font_class}:before {\n content: "\\${g.unicode}";\n}\n\n`;
251
+ }
252
+ return css;
253
+ }
254
+
255
+ function downloadCSS() {
256
+ const css = generateCSS(json.value);
257
+ const blob = new Blob([css], { type: "text/css" });
258
+ const url = URL.createObjectURL(blob);
259
+ const a = document.createElement("a");
260
+ a.href = url;
261
+ a.download = "iconfont.css";
262
+ a.click();
263
+ URL.revokeObjectURL(url);
264
+ ElMessage.success("CSS 已生成");
265
+ }
266
+
267
+ // ========== 保存到源目录 ==========
268
+ async function saveToSource() {
269
+ try {
270
+ const prevGlyphCount = JSON.parse(originalJson.value).glyphs.length;
271
+ const currGlyphCount = json.value.glyphs.length;
272
+ const needFont = currGlyphCount < prevGlyphCount;
273
+
274
+ const saveData = { ...json.value };
275
+
276
+ const response = await fetch("/api/save", {
277
+ method: "POST",
278
+ headers: { "Content-Type": "application/json" },
279
+ body: JSON.stringify({ json: saveData, generateFont: needFont }),
280
+ });
281
+
282
+ if (response.ok) {
283
+ originalJson.value = JSON.stringify(json.value);
284
+ ElMessage.success("保存成功!");
285
+ } else {
286
+ const err = await response.json();
287
+ throw new Error(err.error || "保存失败");
288
+ }
289
+ } catch (error: any) {
290
+ ElMessage.error(error.message || "保存失败");
291
+ }
292
+ }
293
+
294
+ // ========== 删除 ==========
295
+ async function confirmDelete() {
296
+ if (selectedIndices.value.size === 0) return;
297
+ try {
298
+ const count = selectedIndices.value.size;
299
+ const msg = count === 1
300
+ ? `确定要删除图标 "${selectedGlyph.value?.name}" 吗?`
301
+ : `确定要删除选中的 ${count} 个图标吗?`;
302
+ await ElMessageBox.confirm(msg, "确认删除", {
303
+ confirmButtonText: "删除", cancelButtonText: "取消", type: "warning",
304
+ });
305
+ const sorted = [...selectedIndices.value].sort((a, b) => b - a);
306
+ for (const idx of sorted) {
307
+ json.value.glyphs.splice(idx, 1);
308
+ }
309
+ selectedIndices.value = new Set();
310
+ ElMessage.success(`已删除 ${count} 个图标`);
311
+ } catch { /* 取消 */ }
312
+ }
313
+
314
+ // ========== 导出单个 SVG ==========
315
+ async function exportSvg() {
316
+ if (!selectedGlyph.value) return;
317
+ exportingSvg.value = true;
318
+ try {
319
+ const response = await fetch(`/api/export-svg/${selectedGlyph.value.unicode}`);
320
+ if (!response.ok) throw new Error("导出失败");
321
+ const blob = await response.blob();
322
+ const url = URL.createObjectURL(blob);
323
+ const a = document.createElement("a");
324
+ a.href = url;
325
+ a.download = `${selectedGlyph.value.font_class}.svg`;
326
+ a.click();
327
+ URL.revokeObjectURL(url);
328
+ ElMessage.success("SVG 已导出");
329
+ } catch (error: any) {
330
+ ElMessage.error(error.message || "导出失败");
331
+ } finally {
332
+ exportingSvg.value = false;
333
+ }
334
+ }
335
+
336
+ // ========== SVG 上传 ==========
337
+ function triggerFileUpload() {
338
+ fileInputRef.value?.click();
339
+ }
340
+
341
+ async function onFileSelected(e: Event) {
342
+ const input = e.target as HTMLInputElement;
343
+ if (!input.files?.length) return;
344
+ for (const file of Array.from(input.files)) {
345
+ const svg = await file.text();
346
+ await uploadSvg(svg, file.name.replace(/\.svg$/i, ""));
347
+ }
348
+ input.value = "";
349
+ }
350
+
351
+ // 拖拽
352
+ function onDragOver() {
353
+ isDragging.value = true;
354
+ }
355
+
356
+ function onDragLeave(e: DragEvent) {
357
+ // 只有真正离开窗口才隐藏遮罩
358
+ if (!e.relatedTarget) isDragging.value = false;
359
+ }
360
+
361
+ async function onDrop(e: DragEvent) {
362
+ isDragging.value = false;
363
+ const files = e.dataTransfer?.files;
364
+ if (!files?.length) return;
365
+
366
+ for (const file of Array.from(files)) {
367
+ if (!file.name.endsWith(".svg")) {
368
+ ElMessage.warning(`跳过非 SVG 文件: ${file.name}`);
369
+ continue;
370
+ }
371
+ const svg = await file.text();
372
+ await uploadSvg(svg, file.name.replace(/\.svg$/i, ""));
373
+ }
374
+ }
375
+
376
+ // 粘贴
377
+ async function onPaste(e: ClipboardEvent) {
378
+ // 如果焦点在输入框,不处理
379
+ if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
380
+
381
+ // 检查是否有文件
382
+ const files = e.clipboardData?.files;
383
+ if (files?.length) {
384
+ for (const file of Array.from(files)) {
385
+ if (file.type === "image/svg+xml" || file.name.endsWith(".svg")) {
386
+ const svg = await file.text();
387
+ await uploadSvg(svg, file.name.replace(/\.svg$/i, ""));
388
+ e.preventDefault();
389
+ return;
390
+ }
391
+ }
392
+ }
393
+
394
+ // 检查文本中是否有 SVG
395
+ const text = e.clipboardData?.getData("text");
396
+ if (text?.includes("<svg")) {
397
+ e.preventDefault();
398
+ await uploadSvg(text, "");
399
+ }
400
+ }
401
+
402
+ // 上传 SVG 的核心逻辑
403
+ async function uploadSvg(svgContent: string, suggestedName: string) {
404
+ try {
405
+ // 让用户确认名称和 class
406
+ const defaultName = suggestedName || "new-icon";
407
+ const { value: formValues } = await ElMessageBox.prompt(
408
+ `请确认图标信息(来源: ${defaultName})`,
409
+ "添加 SVG 图标",
410
+ {
411
+ confirmButtonText: "添加",
412
+ cancelButtonText: "取消",
413
+ inputPlaceholder: "图标名称(同时作为 font_class)",
414
+ inputValue: defaultName,
415
+ }
416
+ );
417
+
418
+ const name = (formValues as string).trim();
419
+ if (!name) return;
420
+
421
+ const fontClass = name.replace(/\s+/g, "-").toLowerCase();
422
+
423
+ const response = await fetch("/api/upload-svg", {
424
+ method: "POST",
425
+ headers: { "Content-Type": "application/json" },
426
+ body: JSON.stringify({ svg: svgContent, name, fontClass }),
427
+ });
428
+
429
+ if (!response.ok) {
430
+ const err = await response.json();
431
+ throw new Error(err.error || "上传失败");
432
+ }
433
+
434
+ const result = await response.json();
435
+
436
+ // 更新本地数据
437
+ json.value = result.json;
438
+ originalJson.value = JSON.stringify(result.json);
439
+
440
+ // 刷新字体(重新加载 @font-face)
441
+ await loadFontStyle();
442
+
443
+ // 选中新增的图标
444
+ const newIdx = json.value.glyphs.findIndex((g) => g.icon_id === result.glyph.icon_id);
445
+ if (newIdx !== -1) {
446
+ selectedIndices.value = new Set([newIdx]);
447
+ requestAnimationFrame(() => {
448
+ cardRefs.value.get(newIdx)?.scrollIntoView({ behavior: "smooth", block: "center" });
449
+ });
450
+ }
451
+
452
+ ElMessage.success(`图标 "${name}" 添加成功`);
453
+ } catch (error: any) {
454
+ if (error !== "cancel" && error?.message !== "cancel") {
455
+ ElMessage.error(error.message || "上传失败");
456
+ }
457
+ }
458
+ }
459
+
460
+ // ========== 剪切/粘贴图标 ==========
461
+ const clipboard = ref<Glyph[] | null>(null);
462
+
463
+ function cutSelected() {
464
+ if (selectedIndices.value.size === 0) return;
465
+ const sorted = [...selectedIndices.value].sort((a, b) => a - b);
466
+ clipboard.value = sorted.map((i) => ({ ...json.value.glyphs[i] }));
467
+ for (const i of [...sorted].reverse()) {
468
+ json.value.glyphs.splice(i, 1);
469
+ }
470
+ selectedIndices.value = new Set();
471
+ ElMessage.info(`已剪切 ${sorted.length} 个图标`);
472
+ }
473
+
474
+ function pasteClipboard() {
475
+ if (!clipboard.value) return;
476
+ const insertAt = selectedIndices.value.size > 0
477
+ ? Math.min(...selectedIndices.value)
478
+ : json.value.glyphs.length;
479
+ json.value.glyphs.splice(insertAt, 0, ...clipboard.value);
480
+ const newIndices = new Set<number>();
481
+ for (let i = 0; i < clipboard.value.length; i++) {
482
+ newIndices.add(insertAt + i);
483
+ }
484
+ selectedIndices.value = newIndices;
485
+ clipboard.value = null;
486
+ }
487
+
488
+ function moveSelected(step: number, rowMode: boolean) {
489
+ const indices = [...selectedIndices.value].sort((a, b) => a - b);
490
+ if (indices.length === 0) return;
491
+ const arr = json.value.glyphs;
492
+
493
+ if (rowMode) {
494
+ for (const i of indices) {
495
+ if (i + step < 0 || i + step >= arr.length) return;
496
+ }
497
+ }
498
+
499
+ const ordered = step < 0 ? indices : [...indices].reverse();
500
+ const newSelected = new Set(selectedIndices.value);
501
+
502
+ for (const i of ordered) {
503
+ const target = i + step;
504
+ if (target < 0 || target >= arr.length) continue;
505
+ if (newSelected.has(target)) continue;
506
+ [arr[i], arr[target]] = [arr[target], arr[i]];
507
+ newSelected.delete(i);
508
+ newSelected.add(target);
509
+ }
510
+ selectedIndices.value = newSelected;
511
+ }
512
+
513
+ // ========== 键盘事件 ==========
514
+ function handleKeydown(e: KeyboardEvent) {
515
+ if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
516
+
517
+ if (e.key === "Delete") {
518
+ e.preventDefault();
519
+ confirmDelete();
520
+ return;
521
+ }
522
+ if (e.ctrlKey && e.key === "x") {
523
+ e.preventDefault();
524
+ cutSelected();
525
+ return;
526
+ }
527
+ if (e.ctrlKey && e.key === "v" && clipboard.value) {
528
+ e.preventDefault();
529
+ pasteClipboard();
530
+ return;
531
+ }
532
+
533
+ const directionMap: Record<string, { step: number; rowMode: boolean }> = {
534
+ ArrowLeft: { step: -1, rowMode: false },
535
+ ArrowRight: { step: 1, rowMode: false },
536
+ ArrowUp: { step: -1, rowMode: true },
537
+ ArrowDown: { step: 1, rowMode: true },
538
+ };
539
+
540
+ const config = directionMap[e.key];
541
+ if (!config) return;
542
+ e.preventDefault();
543
+
544
+ if (e.shiftKey) {
545
+ const step = (config.rowMode ? columnsPerRow.value : 1) * config.step;
546
+ if (selectedIndices.value.size === 1) {
547
+ const i = selectedIndex.value!;
548
+ const arr = json.value.glyphs;
549
+ const targetIndex = config.step < 0
550
+ ? Math.max(0, i + step)
551
+ : Math.min(arr.length - 1, i + step);
552
+ if (targetIndex === i) return;
553
+ const [item] = arr.splice(i, 1);
554
+ arr.splice(targetIndex, 0, item);
555
+ selectedIndices.value = new Set([targetIndex]);
556
+ } else if (selectedIndices.value.size > 1) {
557
+ moveSelected(step, config.rowMode);
558
+ }
559
+ } else {
560
+ if (selectedIndices.value.size !== 1 || json.value.glyphs.length === 0) return;
561
+ const i = selectedIndex.value!;
562
+ const step = config.rowMode ? columnsPerRow.value : 1;
563
+ const targetIndex = config.step < 0
564
+ ? Math.max(0, i + config.step * step)
565
+ : Math.min(json.value.glyphs.length - 1, i + config.step * step);
566
+ selectedIndices.value = new Set([targetIndex]);
567
+ scrollToSelected();
568
+ }
569
+ }
570
+
571
+ // ========== 加载数据 ==========
572
+ async function loadFontStyle() {
573
+ // 移除旧的动态样式
574
+ const oldStyle = document.getElementById("dynamic-icon-font");
575
+ oldStyle?.remove();
576
+ const oldLink = document.getElementById("dynamic-icon-css");
577
+ oldLink?.remove();
578
+
579
+ // 动态加载 iconfont.css
580
+ const link = document.createElement("link");
581
+ link.id = "dynamic-icon-css";
582
+ link.rel = "stylesheet";
583
+ link.href = `/css?t=${Date.now()}`;
584
+ document.head.appendChild(link);
585
+ }
586
+
587
+ async function loadData() {
588
+ try {
589
+ const response = await fetch("/api/read");
590
+ if (!response.ok) throw new Error("加载失败");
591
+ const data = await response.json();
592
+ json.value = data;
593
+ originalJson.value = JSON.stringify(data);
594
+
595
+ await loadFontStyle();
596
+ await nextTick();
597
+ calculateColumnsPerRow();
598
+ } catch (error: any) {
599
+ ElMessage.error(error.message || "加载数据失败");
600
+ }
601
+ }
602
+
603
+ // ========== ResizeObserver ==========
604
+ function setupResizeObserver() {
605
+ if (!gridRef.value) return;
606
+ resizeObserver = new ResizeObserver(() => calculateColumnsPerRow());
607
+ resizeObserver.observe(gridRef.value);
608
+ }
609
+
610
+ // ========== 生命周期 ==========
611
+ onMounted(() => {
612
+ loadData();
613
+ document.addEventListener("keydown", handleKeydown);
614
+ document.addEventListener("paste", onPaste);
615
+ setupResizeObserver();
616
+ });
617
+
618
+ onUnmounted(() => {
619
+ document.removeEventListener("keydown", handleKeydown);
620
+ document.removeEventListener("paste", onPaste);
621
+ resizeObserver?.disconnect();
622
+ });
623
+ </script>
624
+
625
+ <style scoped>
626
+ .icon-editor {
627
+ display: flex;
628
+ flex-direction: column;
629
+ height: 100vh;
630
+ background: #f5f5f5;
631
+ position: relative;
632
+ }
633
+
634
+ /* 拖拽遮罩 */
635
+ .drop-overlay {
636
+ position: fixed;
637
+ inset: 0;
638
+ z-index: 9999;
639
+ background: rgba(64, 158, 255, 0.15);
640
+ border: 3px dashed #409eff;
641
+ display: flex;
642
+ align-items: center;
643
+ justify-content: center;
644
+ pointer-events: none;
645
+ }
646
+
647
+ .drop-hint {
648
+ text-align: center;
649
+ color: #409eff;
650
+ font-size: 20px;
651
+ }
652
+
653
+ .drop-hint p {
654
+ margin-top: 16px;
655
+ }
656
+
657
+ .toolbar {
658
+ display: flex;
659
+ align-items: center;
660
+ padding: 16px;
661
+ background: white;
662
+ border-bottom: 1px solid #e4e7ed;
663
+ }
664
+
665
+ .actions {
666
+ display: flex;
667
+ gap: 8px;
668
+ margin-left: auto;
669
+ }
670
+
671
+ .main-content {
672
+ display: flex;
673
+ flex: 1;
674
+ overflow: hidden;
675
+ }
676
+
677
+ .icon-grid-container {
678
+ flex: 1;
679
+ display: flex;
680
+ flex-direction: column;
681
+ overflow: hidden;
682
+ }
683
+
684
+ .grid-header {
685
+ display: flex;
686
+ justify-content: space-between;
687
+ align-items: center;
688
+ padding: 12px 16px;
689
+ background: white;
690
+ border-bottom: 1px solid #e4e7ed;
691
+ font-weight: 500;
692
+ }
693
+
694
+ .hint {
695
+ font-size: 12px;
696
+ color: #909399;
697
+ }
698
+
699
+ .icon-grid {
700
+ flex: 1;
701
+ display: grid;
702
+ grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
703
+ gap: 12px;
704
+ padding: 16px;
705
+ overflow-y: auto;
706
+ }
707
+
708
+ .icon-card {
709
+ display: flex;
710
+ flex-direction: column;
711
+ align-items: center;
712
+ padding: 16px;
713
+ background: white;
714
+ border: 2px solid transparent;
715
+ border-radius: 8px;
716
+ cursor: pointer;
717
+ transition: all 0.2s;
718
+ }
719
+
720
+ .icon-card:hover {
721
+ border-color: #409eff;
722
+ box-shadow: 0 2px 8px rgba(64, 158, 255, 0.2);
723
+ }
724
+
725
+ .icon-card.selected {
726
+ border-color: #409eff;
727
+ background: #ecf5ff;
728
+ }
729
+
730
+ .icon-preview {
731
+ font-size: 36px;
732
+ margin-bottom: 8px;
733
+ color: #333;
734
+ }
735
+
736
+ .icon-preview i {
737
+ font-size: 60px;
738
+ }
739
+
740
+ .icon-info {
741
+ text-align: center;
742
+ width: 100%;
743
+ }
744
+
745
+ .icon-name {
746
+ font-size: 14px;
747
+ font-weight: 500;
748
+ color: #333;
749
+ white-space: nowrap;
750
+ overflow: hidden;
751
+ text-overflow: ellipsis;
752
+ }
753
+
754
+ .icon-class {
755
+ font-size: 12px;
756
+ color: #909399;
757
+ margin-top: 4px;
758
+ }
759
+
760
+ .edit-panel {
761
+ width: 320px;
762
+ background: white;
763
+ border-left: 1px solid #e4e7ed;
764
+ display: flex;
765
+ flex-direction: column;
766
+ }
767
+
768
+ .panel-header {
769
+ padding: 12px 16px;
770
+ background: white;
771
+ border-bottom: 1px solid #e4e7ed;
772
+ font-weight: 500;
773
+ }
774
+
775
+ .panel-content {
776
+ flex: 1;
777
+ padding: 16px;
778
+ overflow-y: auto;
779
+ }
780
+
781
+ .preview-large {
782
+ display: flex;
783
+ justify-content: center;
784
+ align-items: center;
785
+ height: 120px;
786
+ margin-bottom: 16px;
787
+ background: #f5f5f5;
788
+ border-radius: 8px;
789
+ }
790
+
791
+ .preview-large i {
792
+ font-size: 64px;
793
+ color: #333;
794
+ }
795
+
796
+ .panel-actions {
797
+ margin-top: 16px;
798
+ }
799
+
800
+ .panel-actions .el-button {
801
+ width: 100%;
802
+ }
803
+
804
+ .status-bar {
805
+ display: flex;
806
+ justify-content: space-between;
807
+ align-items: center;
808
+ padding: 8px 16px;
809
+ background: white;
810
+ border-top: 1px solid #e4e7ed;
811
+ font-size: 12px;
812
+ color: #909399;
813
+ }
814
+
815
+ .modified {
816
+ color: #e6a23c;
817
+ font-weight: 500;
818
+ }
819
+ </style>