vome-core 0.0.20 → 0.0.22

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 +197 -0
  6. package/dist/admin/crud/components/vm-preview-viewer.vue +524 -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,524 @@
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
+ /** 覆盖 URL 推断的媒体类型(vm-image 传 image / video) */
147
+ mediaKind?: MediaPreviewKind
148
+ }>()
149
+
150
+ const emit = defineEmits<{ 'update:open': [v: boolean] }>()
151
+
152
+ const loading = ref(false)
153
+ const error = ref('')
154
+ const textContent = ref('')
155
+ const excelHtml = ref('')
156
+ const scale = ref(1)
157
+ const rotate = ref(0)
158
+ const videoRef = ref<HTMLVideoElement | null>(null)
159
+
160
+ const kind = computed(
161
+ () => props.mediaKind ?? getMediaPreviewKind(props.fileName || props.src),
162
+ )
163
+
164
+ const kindLabel = computed(() => KIND_LABEL[kind.value])
165
+
166
+ const displayName = computed(() =>
167
+ fileDisplayName(props.fileName || props.src, '未命名文件'),
168
+ )
169
+
170
+ const viewerImgStyle = computed(() => ({
171
+ transform: `scale(${scale.value}) rotate(${rotate.value}deg)`,
172
+ }))
173
+
174
+ function resetView() {
175
+ scale.value = 1
176
+ rotate.value = 0
177
+ }
178
+
179
+ function zoomIn() {
180
+ scale.value = Math.min(5, +(scale.value + 0.2).toFixed(2))
181
+ }
182
+
183
+ function zoomOut() {
184
+ scale.value = Math.max(0.2, +(scale.value - 0.2).toFixed(2))
185
+ }
186
+
187
+ function rotateLeft() {
188
+ rotate.value -= 90
189
+ }
190
+
191
+ function rotateRight() {
192
+ rotate.value += 90
193
+ }
194
+
195
+ function onWheel(e: WheelEvent) {
196
+ if (kind.value !== 'image') return
197
+ if (e.deltaY < 0) zoomIn()
198
+ else zoomOut()
199
+ }
200
+
201
+ function lockBody(lock: boolean) {
202
+ if (typeof document === 'undefined') return
203
+ document.body.style.overflow = lock ? 'hidden' : ''
204
+ }
205
+
206
+ async function loadPreview() {
207
+ if (!props.src) return
208
+ loading.value = true
209
+ error.value = ''
210
+ textContent.value = ''
211
+ excelHtml.value = ''
212
+
213
+ try {
214
+ if (kind.value === 'text') {
215
+ textContent.value = await fetchFileText(props.src)
216
+ } else if (kind.value === 'excel') {
217
+ excelHtml.value = DOMPurify.sanitize(await loadExcelPreviewHtml(props.src), {
218
+ USE_PROFILES: { html: true },
219
+ })
220
+ }
221
+ } catch (e) {
222
+ error.value = e instanceof Error ? e.message : '预览失败'
223
+ } finally {
224
+ loading.value = false
225
+ }
226
+ }
227
+
228
+ function close() {
229
+ emit('update:open', false)
230
+ }
231
+
232
+ function onKeydown(e: KeyboardEvent) {
233
+ if (!props.open) return
234
+ if (e.key === 'Escape') close()
235
+ }
236
+
237
+ watch(
238
+ () => props.open,
239
+ (open) => {
240
+ lockBody(open)
241
+ if (typeof window === 'undefined') return
242
+ if (open) {
243
+ resetView()
244
+ window.addEventListener('keydown', onKeydown)
245
+ if (kind.value === 'text' || kind.value === 'excel') void loadPreview()
246
+ if (kind.value === 'video') {
247
+ void nextTick(() => {
248
+ videoRef.value?.play?.().catch(() => undefined)
249
+ })
250
+ }
251
+ } else {
252
+ window.removeEventListener('keydown', onKeydown)
253
+ loading.value = false
254
+ error.value = ''
255
+ textContent.value = ''
256
+ excelHtml.value = ''
257
+ videoRef.value?.pause?.()
258
+ }
259
+ },
260
+ )
261
+
262
+ onUnmounted(() => {
263
+ lockBody(false)
264
+ if (typeof window !== 'undefined') {
265
+ window.removeEventListener('keydown', onKeydown)
266
+ }
267
+ })
268
+ </script>
269
+
270
+ <style lang="scss">
271
+ .vm-file-preview-viewer {
272
+ position: fixed;
273
+ inset: 0;
274
+ z-index: 2200;
275
+ display: flex;
276
+ flex-direction: column;
277
+ background: rgb(10 12 20 / 88%);
278
+ }
279
+
280
+ .vm-file-preview-viewer__head {
281
+ display: flex;
282
+ flex-shrink: 0;
283
+ align-items: center;
284
+ justify-content: space-between;
285
+ gap: 12px;
286
+ padding: 12px 18px;
287
+ border-bottom: 1px solid rgb(255 255 255 / 10%);
288
+ background: rgb(24 28 40 / 92%);
289
+ }
290
+
291
+ .vm-file-preview-viewer__meta {
292
+ display: flex;
293
+ min-width: 0;
294
+ align-items: center;
295
+ gap: 10px;
296
+ }
297
+
298
+ .vm-file-preview-viewer__name {
299
+ overflow: hidden;
300
+ text-overflow: ellipsis;
301
+ white-space: nowrap;
302
+ font-size: 14px;
303
+ font-weight: 650;
304
+ color: #fff;
305
+ }
306
+
307
+ .vm-file-preview-viewer__tag {
308
+ flex-shrink: 0;
309
+ padding: 2px 8px;
310
+ border-radius: 999px;
311
+ background: rgb(255 255 255 / 12%);
312
+ color: rgb(255 255 255 / 88%);
313
+ font-size: 11px;
314
+ font-weight: 600;
315
+ text-transform: uppercase;
316
+ }
317
+
318
+ .vm-file-preview-viewer__actions {
319
+ display: flex;
320
+ flex-shrink: 0;
321
+ align-items: center;
322
+ gap: 10px;
323
+ }
324
+
325
+ .vm-file-preview-viewer__link {
326
+ color: rgb(147 197 253);
327
+ font-size: 13px;
328
+ font-weight: 600;
329
+ text-decoration: none;
330
+
331
+ &:hover {
332
+ text-decoration: underline;
333
+ }
334
+ }
335
+
336
+ .vm-file-preview-viewer__close {
337
+ display: inline-flex;
338
+ width: 34px;
339
+ height: 34px;
340
+ align-items: center;
341
+ justify-content: center;
342
+ border: none;
343
+ border-radius: 10px;
344
+ background: rgb(255 255 255 / 12%);
345
+ color: #fff;
346
+ font-size: 18px;
347
+ cursor: pointer;
348
+
349
+ &:hover {
350
+ background: rgb(255 255 255 / 22%);
351
+ }
352
+ }
353
+
354
+ .vm-file-preview-viewer__body {
355
+ flex: 1 1 auto;
356
+ min-height: 0;
357
+ padding: 16px;
358
+ overflow: auto;
359
+
360
+ &.is-media {
361
+ display: flex;
362
+ flex-direction: column;
363
+ padding: 0;
364
+ overflow: hidden;
365
+ }
366
+ }
367
+
368
+ .vm-file-preview-viewer__hint {
369
+ margin: 24px auto;
370
+ color: rgb(255 255 255 / 80%);
371
+ text-align: center;
372
+
373
+ &.is-error {
374
+ color: #fca5a5;
375
+ }
376
+ }
377
+
378
+ .vm-file-preview-viewer__image-stage {
379
+ display: flex;
380
+ flex: 1 1 auto;
381
+ min-height: 0;
382
+ width: 100%;
383
+ align-items: center;
384
+ justify-content: center;
385
+ overflow: hidden;
386
+ }
387
+
388
+ .vm-file-preview-viewer__image {
389
+ display: block;
390
+ width: auto;
391
+ height: auto;
392
+ max-width: min(1200px, calc(100vw - 48px));
393
+ max-height: calc(100vh - 140px);
394
+ object-fit: contain;
395
+ transition: transform 0.15s ease;
396
+ user-select: none;
397
+ }
398
+
399
+ .vm-file-preview-viewer__toolbar {
400
+ position: relative;
401
+ z-index: 2;
402
+ display: inline-flex;
403
+ flex-shrink: 0;
404
+ align-items: center;
405
+ gap: 4px;
406
+ margin: 0 auto 20px;
407
+ padding: 6px 8px;
408
+ border-radius: 12px;
409
+ background: rgb(24 28 40 / 92%);
410
+
411
+ button {
412
+ display: inline-flex;
413
+ width: 36px;
414
+ height: 36px;
415
+ align-items: center;
416
+ justify-content: center;
417
+ border: none;
418
+ border-radius: 8px;
419
+ background: transparent;
420
+ color: rgb(255 255 255 / 88%);
421
+ font-size: 18px;
422
+ cursor: pointer;
423
+
424
+ &:hover {
425
+ background: rgb(255 255 255 / 12%);
426
+ color: #fff;
427
+ }
428
+ }
429
+ }
430
+
431
+ .vm-file-preview-viewer__video-wrap {
432
+ display: flex;
433
+ flex: 1 1 auto;
434
+ min-height: 0;
435
+ width: 100%;
436
+ align-items: center;
437
+ justify-content: center;
438
+ padding: 16px;
439
+ }
440
+
441
+ .vm-file-preview-viewer__video {
442
+ display: block;
443
+ width: min(960px, calc(100vw - 48px));
444
+ max-height: calc(100vh - 120px);
445
+ border-radius: 12px;
446
+ background: #000;
447
+ outline: none;
448
+ }
449
+
450
+ .vm-file-preview-viewer__audio {
451
+ display: block;
452
+ width: min(480px, 100%);
453
+ margin: 48px auto 0;
454
+ }
455
+
456
+ .vm-file-preview-viewer__iframe {
457
+ display: block;
458
+ width: min(1100px, 100%);
459
+ height: calc(100vh - 88px);
460
+ margin: 0 auto;
461
+ border: none;
462
+ border-radius: 12px;
463
+ background: #fff;
464
+ }
465
+
466
+ .vm-file-preview-viewer__excel {
467
+ max-width: min(1100px, 100%);
468
+ margin: 0 auto;
469
+ padding: 12px;
470
+ border-radius: 12px;
471
+ background: #fff;
472
+ overflow: auto;
473
+
474
+ table {
475
+ width: 100%;
476
+ border-collapse: collapse;
477
+ font-size: 13px;
478
+ }
479
+
480
+ td,
481
+ th {
482
+ padding: 6px 8px;
483
+ border: 1px solid #e5e7eb;
484
+ white-space: nowrap;
485
+ }
486
+ }
487
+
488
+ .vm-file-preview-viewer__text {
489
+ max-width: min(960px, 100%);
490
+ margin: 0 auto;
491
+ padding: 16px 18px;
492
+ border-radius: 12px;
493
+ background: rgb(15 23 42 / 92%);
494
+ color: #e2e8f0;
495
+ font-size: 13px;
496
+ line-height: 1.6;
497
+ white-space: pre-wrap;
498
+ word-break: break-word;
499
+ }
500
+
501
+ .vm-file-preview-viewer__fallback {
502
+ display: grid;
503
+ gap: 8px;
504
+ max-width: 420px;
505
+ margin: 48px auto;
506
+ padding: 28px 24px;
507
+ border-radius: 16px;
508
+ background: rgb(255 255 255 / 8%);
509
+ color: rgb(255 255 255 / 88%);
510
+ text-align: center;
511
+
512
+ i {
513
+ font-size: 42px;
514
+ line-height: 1;
515
+ color: rgb(255 255 255 / 72%);
516
+ }
517
+
518
+ p {
519
+ margin: 0;
520
+ font-size: 14px;
521
+ line-height: 1.6;
522
+ }
523
+ }
524
+ </style>