vome-core 0.0.20 → 0.0.21

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 (58) hide show
  1. package/dist/admin/config/plugin-dev.js +1 -1
  2. package/dist/admin/crud/components/vm-audio.vue +217 -0
  3. package/dist/admin/crud/components/vm-column-custom.vue +50 -56
  4. package/dist/admin/crud/components/vm-file-preview.vue +90 -0
  5. package/dist/admin/crud/components/vm-image.vue +413 -0
  6. package/dist/admin/crud/components/vm-preview-viewer.vue +520 -0
  7. package/dist/admin/crud/components/vm-richtext.vue +275 -0
  8. package/dist/admin/crud/components/vm-upload-item.vue +92 -31
  9. package/dist/admin/crud/components/vm-upload.vue +2 -6
  10. package/dist/admin/crud/components/vm-user-select.vue +48 -49
  11. package/dist/admin/crud/config.js +1 -1
  12. package/dist/admin/crud/confirm.js +1 -1
  13. package/dist/admin/crud/dict.js +1 -1
  14. package/dist/admin/crud/index.d.ts +5 -3
  15. package/dist/admin/crud/index.js +1 -1
  16. package/dist/admin/crud/key.js +1 -1
  17. package/dist/admin/crud/mitt.js +1 -1
  18. package/dist/admin/crud/plugins.js +1 -1
  19. package/dist/admin/crud/span.js +1 -1
  20. package/dist/admin/crud/style.js +1 -1
  21. package/dist/admin/crud/validate.js +1 -1
  22. package/dist/admin/crud/vm-adv-search.vue +39 -32
  23. package/dist/admin/crud/vm-crud.vue +3 -8
  24. package/dist/admin/crud/{vm-flex1.vue → vm-flex.vue} +3 -3
  25. package/dist/admin/crud/vm-upsert.vue +9 -92
  26. package/dist/admin/directives/perm.js +1 -1
  27. package/dist/admin/hooks/useUpload.js +1 -1
  28. package/dist/admin/lib/browser.js +1 -1
  29. package/dist/admin/lib/cn.js +1 -1
  30. package/dist/admin/lib/dialog-float.d.ts +2 -0
  31. package/dist/admin/lib/dialog-float.js +1 -1
  32. package/dist/admin/lib/export-excel.js +1 -1
  33. package/dist/admin/lib/file-preview.d.ts +11 -0
  34. package/dist/admin/lib/file-preview.js +1 -0
  35. package/dist/admin/lib/menu.js +1 -1
  36. package/dist/admin/lib/tree.js +1 -1
  37. package/dist/admin/lib/upload.js +1 -1
  38. package/dist/admin/lib/video-frame.d.ts +9 -0
  39. package/dist/admin/lib/video-frame.js +1 -0
  40. package/dist/admin/styles/base.css +100 -0
  41. package/dist/index.js +1 -1
  42. package/dist/server/index.js +1 -1
  43. package/dist/shared/excel.js +1 -1
  44. package/dist/shared/index.js +1 -1
  45. package/dist/shared/tree.js +1 -1
  46. package/package.json +1 -1
  47. package/typings/admin/comm/crud.d.ts +2 -2
  48. package/typings/admin/comm/crud.ts +2 -3
  49. package/dist/admin/crud/components/vm-file-icon.vue +0 -55
  50. package/dist/admin/crud/vm-dialog.vue +0 -235
  51. package/dist/admin/static/fileicon/demo.css +0 -539
  52. package/dist/admin/static/fileicon/demo_index.html +0 -901
  53. package/dist/admin/static/fileicon/iconfont.css +0 -139
  54. package/dist/admin/static/fileicon/iconfont.js +0 -1
  55. package/dist/admin/static/fileicon/iconfont.json +0 -226
  56. package/dist/admin/static/fileicon/iconfont.ttf +0 -0
  57. package/dist/admin/static/fileicon/iconfont.woff +0 -0
  58. package/dist/admin/static/fileicon/iconfont.woff2 +0 -0
@@ -0,0 +1,520 @@
1
+ <template>
2
+ <Teleport to="body">
3
+ <div
4
+ v-if="open"
5
+ class="vm-file-preview-viewer"
6
+ role="dialog"
7
+ aria-modal="true"
8
+ @click.self="close"
9
+ >
10
+ <header class="vm-file-preview-viewer__head">
11
+ <div class="vm-file-preview-viewer__meta">
12
+ <span class="vm-file-preview-viewer__name">{{ displayName }}</span>
13
+ <span class="vm-file-preview-viewer__tag">{{ kindLabel }}</span>
14
+ </div>
15
+ <div class="vm-file-preview-viewer__actions">
16
+ <a
17
+ class="vm-file-preview-viewer__link"
18
+ :href="src"
19
+ target="_blank"
20
+ rel="noopener noreferrer"
21
+ >
22
+ 新窗口打开
23
+ </a>
24
+ <button type="button" class="vm-file-preview-viewer__close" title="关闭" @click="close">
25
+ <i class="ri-close-line" />
26
+ </button>
27
+ </div>
28
+ </header>
29
+
30
+ <div
31
+ class="vm-file-preview-viewer__body vm-scroll"
32
+ :class="{ 'is-media': kind === 'image' || kind === 'video' }"
33
+ >
34
+ <p v-if="loading" class="vm-file-preview-viewer__hint">加载中…</p>
35
+ <p v-else-if="error" class="vm-file-preview-viewer__hint is-error">{{ error }}</p>
36
+
37
+ <template v-else-if="kind === 'image'">
38
+ <div class="vm-file-preview-viewer__image-stage" @wheel.prevent="onWheel">
39
+ <img
40
+ class="vm-file-preview-viewer__image"
41
+ :src="src"
42
+ :alt="displayName"
43
+ :style="viewerImgStyle"
44
+ draggable="false"
45
+ @load="resetView"
46
+ />
47
+ </div>
48
+ <div class="vm-file-preview-viewer__toolbar" @click.stop>
49
+ <button type="button" title="缩小" @click="zoomOut">
50
+ <i class="ri-subtract-line" />
51
+ </button>
52
+ <button type="button" title="放大" @click="zoomIn">
53
+ <i class="ri-add-line" />
54
+ </button>
55
+ <button type="button" title="原始尺寸" @click="resetView">
56
+ <i class="ri-aspect-ratio-line" />
57
+ </button>
58
+ <button type="button" title="逆时针旋转" @click="rotateLeft">
59
+ <i class="ri-anticlockwise-line" />
60
+ </button>
61
+ <button type="button" title="顺时针旋转" @click="rotateRight">
62
+ <i class="ri-clockwise-line" />
63
+ </button>
64
+ </div>
65
+ </template>
66
+
67
+ <div v-else-if="kind === 'video'" class="vm-file-preview-viewer__video-wrap">
68
+ <video
69
+ ref="videoRef"
70
+ class="vm-file-preview-viewer__video"
71
+ :src="src"
72
+ controls
73
+ autoplay
74
+ playsinline
75
+ />
76
+ </div>
77
+
78
+ <audio
79
+ v-else-if="kind === 'audio'"
80
+ class="vm-file-preview-viewer__audio"
81
+ :src="src"
82
+ controls
83
+ />
84
+
85
+ <iframe
86
+ v-else-if="kind === 'pdf'"
87
+ class="vm-file-preview-viewer__iframe"
88
+ :src="src"
89
+ title="PDF 预览"
90
+ />
91
+
92
+ <div
93
+ v-else-if="kind === 'excel' && excelHtml"
94
+ class="vm-file-preview-viewer__excel"
95
+ v-html="excelHtml"
96
+ />
97
+
98
+ <pre v-else-if="kind === 'text' && textContent" class="vm-file-preview-viewer__text">{{
99
+ textContent
100
+ }}</pre>
101
+
102
+ <div v-else-if="kind === 'office'" class="vm-file-preview-viewer__fallback">
103
+ <i class="ri-file-3-line" aria-hidden="true" />
104
+ <p>Word / PPT 等 Office 文档暂不支持浏览器内预览。</p>
105
+ <p>请点击上方「新窗口打开」或下载后使用本地 Office / WPS 查看。</p>
106
+ </div>
107
+
108
+ <div v-else class="vm-file-preview-viewer__fallback">
109
+ <i class="ri-file-3-line" aria-hidden="true" />
110
+ <p>该文件类型暂不支持在线预览。</p>
111
+ <p>请点击上方「新窗口打开」下载查看。</p>
112
+ </div>
113
+ </div>
114
+ </div>
115
+ </Teleport>
116
+ </template>
117
+
118
+ <script setup lang="ts">
119
+ import { computed, nextTick, onUnmounted, ref, watch } from 'vue'
120
+ import DOMPurify from 'dompurify'
121
+ import {
122
+ fileDisplayName,
123
+ getMediaPreviewKind,
124
+ loadExcelPreviewHtml,
125
+ fetchFileText,
126
+ type MediaPreviewKind,
127
+ } from '../../lib/file-preview'
128
+
129
+ defineOptions({ name: 'vm-preview-viewer' })
130
+
131
+ const KIND_LABEL: Record<MediaPreviewKind, string> = {
132
+ image: '图片',
133
+ video: '视频',
134
+ audio: '音频',
135
+ pdf: 'PDF',
136
+ excel: 'Excel',
137
+ text: '文本',
138
+ office: 'Office',
139
+ unsupported: '文件',
140
+ }
141
+
142
+ const props = defineProps<{
143
+ open: boolean
144
+ src: string
145
+ fileName?: string
146
+ }>()
147
+
148
+ const emit = defineEmits<{ 'update:open': [v: boolean] }>()
149
+
150
+ const loading = ref(false)
151
+ const error = ref('')
152
+ const textContent = ref('')
153
+ const excelHtml = ref('')
154
+ const scale = ref(1)
155
+ const rotate = ref(0)
156
+ const videoRef = ref<HTMLVideoElement | null>(null)
157
+
158
+ const kind = computed(() => getMediaPreviewKind(props.fileName || props.src))
159
+
160
+ const kindLabel = computed(() => KIND_LABEL[kind.value])
161
+
162
+ const displayName = computed(() =>
163
+ fileDisplayName(props.fileName || props.src, '未命名文件'),
164
+ )
165
+
166
+ const viewerImgStyle = computed(() => ({
167
+ transform: `scale(${scale.value}) rotate(${rotate.value}deg)`,
168
+ }))
169
+
170
+ function resetView() {
171
+ scale.value = 1
172
+ rotate.value = 0
173
+ }
174
+
175
+ function zoomIn() {
176
+ scale.value = Math.min(5, +(scale.value + 0.2).toFixed(2))
177
+ }
178
+
179
+ function zoomOut() {
180
+ scale.value = Math.max(0.2, +(scale.value - 0.2).toFixed(2))
181
+ }
182
+
183
+ function rotateLeft() {
184
+ rotate.value -= 90
185
+ }
186
+
187
+ function rotateRight() {
188
+ rotate.value += 90
189
+ }
190
+
191
+ function onWheel(e: WheelEvent) {
192
+ if (kind.value !== 'image') return
193
+ if (e.deltaY < 0) zoomIn()
194
+ else zoomOut()
195
+ }
196
+
197
+ function lockBody(lock: boolean) {
198
+ if (typeof document === 'undefined') return
199
+ document.body.style.overflow = lock ? 'hidden' : ''
200
+ }
201
+
202
+ async function loadPreview() {
203
+ if (!props.src) return
204
+ loading.value = true
205
+ error.value = ''
206
+ textContent.value = ''
207
+ excelHtml.value = ''
208
+
209
+ try {
210
+ if (kind.value === 'text') {
211
+ textContent.value = await fetchFileText(props.src)
212
+ } else if (kind.value === 'excel') {
213
+ excelHtml.value = DOMPurify.sanitize(await loadExcelPreviewHtml(props.src), {
214
+ USE_PROFILES: { html: true },
215
+ })
216
+ }
217
+ } catch (e) {
218
+ error.value = e instanceof Error ? e.message : '预览失败'
219
+ } finally {
220
+ loading.value = false
221
+ }
222
+ }
223
+
224
+ function close() {
225
+ emit('update:open', false)
226
+ }
227
+
228
+ function onKeydown(e: KeyboardEvent) {
229
+ if (!props.open) return
230
+ if (e.key === 'Escape') close()
231
+ }
232
+
233
+ watch(
234
+ () => props.open,
235
+ (open) => {
236
+ lockBody(open)
237
+ if (typeof window === 'undefined') return
238
+ if (open) {
239
+ resetView()
240
+ window.addEventListener('keydown', onKeydown)
241
+ if (kind.value === 'text' || kind.value === 'excel') void loadPreview()
242
+ if (kind.value === 'video') {
243
+ void nextTick(() => {
244
+ videoRef.value?.play?.().catch(() => undefined)
245
+ })
246
+ }
247
+ } else {
248
+ window.removeEventListener('keydown', onKeydown)
249
+ loading.value = false
250
+ error.value = ''
251
+ textContent.value = ''
252
+ excelHtml.value = ''
253
+ videoRef.value?.pause?.()
254
+ }
255
+ },
256
+ )
257
+
258
+ onUnmounted(() => {
259
+ lockBody(false)
260
+ if (typeof window !== 'undefined') {
261
+ window.removeEventListener('keydown', onKeydown)
262
+ }
263
+ })
264
+ </script>
265
+
266
+ <style lang="scss">
267
+ .vm-file-preview-viewer {
268
+ position: fixed;
269
+ inset: 0;
270
+ z-index: 2200;
271
+ display: flex;
272
+ flex-direction: column;
273
+ background: rgb(10 12 20 / 88%);
274
+ }
275
+
276
+ .vm-file-preview-viewer__head {
277
+ display: flex;
278
+ flex-shrink: 0;
279
+ align-items: center;
280
+ justify-content: space-between;
281
+ gap: 12px;
282
+ padding: 12px 18px;
283
+ border-bottom: 1px solid rgb(255 255 255 / 10%);
284
+ background: rgb(24 28 40 / 92%);
285
+ }
286
+
287
+ .vm-file-preview-viewer__meta {
288
+ display: flex;
289
+ min-width: 0;
290
+ align-items: center;
291
+ gap: 10px;
292
+ }
293
+
294
+ .vm-file-preview-viewer__name {
295
+ overflow: hidden;
296
+ text-overflow: ellipsis;
297
+ white-space: nowrap;
298
+ font-size: 14px;
299
+ font-weight: 650;
300
+ color: #fff;
301
+ }
302
+
303
+ .vm-file-preview-viewer__tag {
304
+ flex-shrink: 0;
305
+ padding: 2px 8px;
306
+ border-radius: 999px;
307
+ background: rgb(255 255 255 / 12%);
308
+ color: rgb(255 255 255 / 88%);
309
+ font-size: 11px;
310
+ font-weight: 600;
311
+ text-transform: uppercase;
312
+ }
313
+
314
+ .vm-file-preview-viewer__actions {
315
+ display: flex;
316
+ flex-shrink: 0;
317
+ align-items: center;
318
+ gap: 10px;
319
+ }
320
+
321
+ .vm-file-preview-viewer__link {
322
+ color: rgb(147 197 253);
323
+ font-size: 13px;
324
+ font-weight: 600;
325
+ text-decoration: none;
326
+
327
+ &:hover {
328
+ text-decoration: underline;
329
+ }
330
+ }
331
+
332
+ .vm-file-preview-viewer__close {
333
+ display: inline-flex;
334
+ width: 34px;
335
+ height: 34px;
336
+ align-items: center;
337
+ justify-content: center;
338
+ border: none;
339
+ border-radius: 10px;
340
+ background: rgb(255 255 255 / 12%);
341
+ color: #fff;
342
+ font-size: 18px;
343
+ cursor: pointer;
344
+
345
+ &:hover {
346
+ background: rgb(255 255 255 / 22%);
347
+ }
348
+ }
349
+
350
+ .vm-file-preview-viewer__body {
351
+ flex: 1 1 auto;
352
+ min-height: 0;
353
+ padding: 16px;
354
+ overflow: auto;
355
+
356
+ &.is-media {
357
+ display: flex;
358
+ flex-direction: column;
359
+ padding: 0;
360
+ overflow: hidden;
361
+ }
362
+ }
363
+
364
+ .vm-file-preview-viewer__hint {
365
+ margin: 24px auto;
366
+ color: rgb(255 255 255 / 80%);
367
+ text-align: center;
368
+
369
+ &.is-error {
370
+ color: #fca5a5;
371
+ }
372
+ }
373
+
374
+ .vm-file-preview-viewer__image-stage {
375
+ display: flex;
376
+ flex: 1 1 auto;
377
+ min-height: 0;
378
+ width: 100%;
379
+ align-items: center;
380
+ justify-content: center;
381
+ overflow: hidden;
382
+ }
383
+
384
+ .vm-file-preview-viewer__image {
385
+ display: block;
386
+ width: auto;
387
+ height: auto;
388
+ max-width: min(1200px, calc(100vw - 48px));
389
+ max-height: calc(100vh - 140px);
390
+ object-fit: contain;
391
+ transition: transform 0.15s ease;
392
+ user-select: none;
393
+ }
394
+
395
+ .vm-file-preview-viewer__toolbar {
396
+ position: relative;
397
+ z-index: 2;
398
+ display: inline-flex;
399
+ flex-shrink: 0;
400
+ align-items: center;
401
+ gap: 4px;
402
+ margin: 0 auto 20px;
403
+ padding: 6px 8px;
404
+ border-radius: 12px;
405
+ background: rgb(24 28 40 / 92%);
406
+
407
+ button {
408
+ display: inline-flex;
409
+ width: 36px;
410
+ height: 36px;
411
+ align-items: center;
412
+ justify-content: center;
413
+ border: none;
414
+ border-radius: 8px;
415
+ background: transparent;
416
+ color: rgb(255 255 255 / 88%);
417
+ font-size: 18px;
418
+ cursor: pointer;
419
+
420
+ &:hover {
421
+ background: rgb(255 255 255 / 12%);
422
+ color: #fff;
423
+ }
424
+ }
425
+ }
426
+
427
+ .vm-file-preview-viewer__video-wrap {
428
+ display: flex;
429
+ flex: 1 1 auto;
430
+ min-height: 0;
431
+ width: 100%;
432
+ align-items: center;
433
+ justify-content: center;
434
+ padding: 16px;
435
+ }
436
+
437
+ .vm-file-preview-viewer__video {
438
+ display: block;
439
+ width: min(960px, calc(100vw - 48px));
440
+ max-height: calc(100vh - 120px);
441
+ border-radius: 12px;
442
+ background: #000;
443
+ outline: none;
444
+ }
445
+
446
+ .vm-file-preview-viewer__audio {
447
+ display: block;
448
+ width: min(480px, 100%);
449
+ margin: 48px auto 0;
450
+ }
451
+
452
+ .vm-file-preview-viewer__iframe {
453
+ display: block;
454
+ width: min(1100px, 100%);
455
+ height: calc(100vh - 88px);
456
+ margin: 0 auto;
457
+ border: none;
458
+ border-radius: 12px;
459
+ background: #fff;
460
+ }
461
+
462
+ .vm-file-preview-viewer__excel {
463
+ max-width: min(1100px, 100%);
464
+ margin: 0 auto;
465
+ padding: 12px;
466
+ border-radius: 12px;
467
+ background: #fff;
468
+ overflow: auto;
469
+
470
+ table {
471
+ width: 100%;
472
+ border-collapse: collapse;
473
+ font-size: 13px;
474
+ }
475
+
476
+ td,
477
+ th {
478
+ padding: 6px 8px;
479
+ border: 1px solid #e5e7eb;
480
+ white-space: nowrap;
481
+ }
482
+ }
483
+
484
+ .vm-file-preview-viewer__text {
485
+ max-width: min(960px, 100%);
486
+ margin: 0 auto;
487
+ padding: 16px 18px;
488
+ border-radius: 12px;
489
+ background: rgb(15 23 42 / 92%);
490
+ color: #e2e8f0;
491
+ font-size: 13px;
492
+ line-height: 1.6;
493
+ white-space: pre-wrap;
494
+ word-break: break-word;
495
+ }
496
+
497
+ .vm-file-preview-viewer__fallback {
498
+ display: grid;
499
+ gap: 8px;
500
+ max-width: 420px;
501
+ margin: 48px auto;
502
+ padding: 28px 24px;
503
+ border-radius: 16px;
504
+ background: rgb(255 255 255 / 8%);
505
+ color: rgb(255 255 255 / 88%);
506
+ text-align: center;
507
+
508
+ i {
509
+ font-size: 42px;
510
+ line-height: 1;
511
+ color: rgb(255 255 255 / 72%);
512
+ }
513
+
514
+ p {
515
+ margin: 0;
516
+ font-size: 14px;
517
+ line-height: 1.6;
518
+ }
519
+ }
520
+ </style>