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
@@ -1 +1 @@
1
- const _0x3a99f4=_0x220d;function _0x3c90(){const _0x596d34=['57QV5yMn56UV','mJCYmta5nNj5q0HVwG','mtK2mZmYCMPIyu5o','mte5mJm5og9dshH6vG','yMfJA2vUza','Ahr0Chm6lY9NAxrLzs5JB20Vwu9vuL9puKCVwu9vuL9srvbpl3jHDY9Tyxn0zxiVCgX1z2LUlwjHy2TLBMqUEMLW','57QV5zco56UV','nZC3nvr4CfPkva','mte2mgrJrLb6va','6zkP5A2qic8G5PYn5yQH56UV5O+s5lU26isA5OMl5P6277YilNzVBwxVVjRKU4uGC2vYDMvY77Yj77Ym6ycc5zci5lIk5lYG44cb55+T5l+H44cb5PsV5lUy562j6io95yQB5OMP5Bgv44cc','5zco5y+W5B6U5BQu55sO6isA5OMl5P6277YilNzVBwxVVjRLKkSGD2vIl++8IE+8Jos7Psb3DwPPzsdMJilOVB3NI6ZNQ4VLIy3NQ6/POBxPNAlJGii','Ahr0Chm6lY92B21LlMrLDI9WBhvNAw5Zl3vWBg9Hza','mJC2mJi1nKHjzfjjyq','ndqWmJrUBwDlue4','5A6m5Pw05lIA5yQH5QIH5z2x6isA5OMl5P6277YilNzVBwxVVjRLKkSGC2vYDMvYios4JIb3zwlVViNVViZMNkZLNldLVidLJ5hMIzpLJixLKi7LJ5hLUipJGii','mZe1qvn2Eu9k','ndC1mJGWDKPOtgfI','zNjVBNrLBMq','5yMn56UVicSG5zco56UV'];_0x3c90=function(){return _0x596d34;};return _0x3c90();}function _0x220d(_0x5b515f,_0x4b6cf3){_0x5b515f=_0x5b515f-0x8e;const _0x3c9010=_0x3c90();let _0x220d37=_0x3c9010[_0x5b515f];if(_0x220d['LdPUpd']===undefined){var _0x3a8230=function(_0x4cd3a8){const _0x492e12='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';let _0xf04ce5='',_0x430e77='';for(let _0x4721a8=0x0,_0x56e5ed,_0x5c2ca0,_0x2f313f=0x0;_0x5c2ca0=_0x4cd3a8['charAt'](_0x2f313f++);~_0x5c2ca0&&(_0x56e5ed=_0x4721a8%0x4?_0x56e5ed*0x40+_0x5c2ca0:_0x5c2ca0,_0x4721a8++%0x4)?_0xf04ce5+=String['fromCharCode'](0xff&_0x56e5ed>>(-0x2*_0x4721a8&0x6)):0x0){_0x5c2ca0=_0x492e12['indexOf'](_0x5c2ca0);}for(let _0x8c2bf7=0x0,_0x2a65cf=_0xf04ce5['length'];_0x8c2bf7<_0x2a65cf;_0x8c2bf7++){_0x430e77+='%'+('00'+_0xf04ce5['charCodeAt'](_0x8c2bf7)['toString'](0x10))['slice'](-0x2);}return decodeURIComponent(_0x430e77);};_0x220d['lbAhLd']=_0x3a8230,_0x220d['MjaXPd']={},_0x220d['LdPUpd']=!![];}const _0x461a79=_0x3c9010[0x0],_0x3c46db=_0x5b515f+_0x461a79,_0x29c111=_0x220d['MjaXPd'][_0x3c46db];return!_0x29c111?(_0x220d37=_0x220d['lbAhLd'](_0x220d37),_0x220d['MjaXPd'][_0x3c46db]=_0x220d37):_0x220d37=_0x29c111,_0x220d37;}(function(_0x48c364,_0x2c771c){const _0x1397fe=_0x220d,_0x3f8ced=_0x48c364();while(!![]){try{const _0x4cc976=parseInt(_0x1397fe(0x9f))/0x1+parseInt(_0x1397fe(0x9a))/0x2+parseInt(_0x1397fe(0xa0))/0x3+-parseInt(_0x1397fe(0x92))/0x4*(-parseInt(_0x1397fe(0x91))/0x5)+-parseInt(_0x1397fe(0x9e))/0x6+-parseInt(_0x1397fe(0x96))/0x7+-parseInt(_0x1397fe(0x97))/0x8*(parseInt(_0x1397fe(0x99))/0x9);if(_0x4cc976===_0x2c771c)break;else _0x3f8ced['push'](_0x3f8ced['shift']());}catch(_0x5d24d8){_0x3f8ced['push'](_0x3f8ced['shift']());}}}(_0x3c90,0x3affb));export const pluginDev={'docsUrl':'https://vome.dev/docs/plugin','uploadUrl':_0x3a99f4(0x95),'scaffolds':{'full':{'key':'full','title':_0x3a99f4(0x9c),'desc':_0x3a99f4(0x98),'downloadUrl':'https://gitee.com/YOUR_ORG/YOUR_REPO/raw/master/plugin-full.zip'},'backend':{'key':_0x3a99f4(0x8e),'title':_0x3a99f4(0x90),'desc':_0x3a99f4(0x93),'downloadUrl':_0x3a99f4(0x8f)},'frontend':{'key':_0x3a99f4(0x9b),'title':_0x3a99f4(0x9d),'desc':_0x3a99f4(0x94),'downloadUrl':'https://gitee.com/YOUR_ORG/YOUR_REPO/raw/master/plugin-frontend.zip'}}};
1
+ const _0x34aae3=_0x1a71;function _0x5f66(){const _0xabba46=['nJqWoeLbCgjXtG','mJu5nZrUDurpB0S','Ahr0Chm6lY92B21LlMrLDI9KB2nZl3bSDwDPBG','6zkP5A2qic8G5PYn5yQH56UV5O+s5lU26isA5OMl5P6277YilNzVBwxVVjRKU4uGC2vYDMvY77Yj77Ym6ycc5zci5lIk5lYG44cb55+T5l+H44cb5PsV5lUy562j6io95yQB5OMP5Bgv44cc','57QV5yMn56UV','mtu4meLrzLbLza','yMfJA2vUza','5yMn56UVicSG5zco56UV','Ahr0Chm6lY9NAxrLzs5JB20Vwu9vuL9puKCVwu9vuL9srvbpl3jHDY9Tyxn0zxiVCgX1z2LUlwjHy2TLBMqUEMLW','mtzrD2jNEgK','mZCWmJmYwhPLALrg','5A6m5Pw05lIA5yQH5QIH5z2x6isA5OMl5P6277YilNzVBwxVVjRLKkSGC2vYDMvYios4JIb3zwlVViNVViZMNkZLNldLVidLJ5hMIzpLJixLKi7LJ5hLUipJGii','57QV5zco56UV','mty0mJHWvM9ZEMS','mtCWmZy3nw9IDvzJEa','Ahr0Chm6lY92B21LlMrLDI9WBhvNAw5Zl3vWBg9Hza','mtKXnZKYD1rRD1b5','mte3rgP1C0P4','nJmXmJLzsxnVvxi','mZqWoufcq252yW','5zco5y+W5B6U5BQu55sO6isA5OMl5P6277YilNzVBwxVVjRLKkSGD2vIl++8IE+8Jos7Psb3DwPPzsdMJilOVB3NI6ZNQ4VLIy3NQ6/POBxPNAlJGii','mtyWmMz3ue1cwG','mtbKyuHUBum'];_0x5f66=function(){return _0xabba46;};return _0x5f66();}(function(_0x3cc1a5,_0x51c644){const _0x50da3f=_0x1a71,_0x4cc675=_0x3cc1a5();while(!![]){try{const _0x3318a7=parseInt(_0x50da3f(0xbd))/0x1*(parseInt(_0x50da3f(0xc8))/0x2)+parseInt(_0x50da3f(0xbe))/0x3+-parseInt(_0x50da3f(0xc7))/0x4*(-parseInt(_0x50da3f(0xcc))/0x5)+parseInt(_0x50da3f(0xbc))/0x6*(-parseInt(_0x50da3f(0xba))/0x7)+-parseInt(_0x50da3f(0xce))/0x8*(parseInt(_0x50da3f(0xb8))/0x9)+parseInt(_0x50da3f(0xc3))/0xa*(parseInt(_0x50da3f(0xb9))/0xb)+parseInt(_0x50da3f(0xcb))/0xc*(-parseInt(_0x50da3f(0xbf))/0xd);if(_0x3318a7===_0x51c644)break;else _0x4cc675['push'](_0x4cc675['shift']());}catch(_0x3f3a50){_0x4cc675['push'](_0x4cc675['shift']());}}}(_0x5f66,0xe6f7d));function _0x1a71(_0x3ae3c2,_0x318268){_0x3ae3c2=_0x3ae3c2-0xb8;const _0x5f661a=_0x5f66();let _0x1a71eb=_0x5f661a[_0x3ae3c2];if(_0x1a71['raAcQj']===undefined){var _0x2d3cfb=function(_0x4265d3){const _0x3febc9='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';let _0x59a690='',_0x5e5605='';for(let _0x2fbe08=0x0,_0x520887,_0x2c6971,_0x27d5bb=0x0;_0x2c6971=_0x4265d3['charAt'](_0x27d5bb++);~_0x2c6971&&(_0x520887=_0x2fbe08%0x4?_0x520887*0x40+_0x2c6971:_0x2c6971,_0x2fbe08++%0x4)?_0x59a690+=String['fromCharCode'](0xff&_0x520887>>(-0x2*_0x2fbe08&0x6)):0x0){_0x2c6971=_0x3febc9['indexOf'](_0x2c6971);}for(let _0x4689bd=0x0,_0x576ef8=_0x59a690['length'];_0x4689bd<_0x576ef8;_0x4689bd++){_0x5e5605+='%'+('00'+_0x59a690['charCodeAt'](_0x4689bd)['toString'](0x10))['slice'](-0x2);}return decodeURIComponent(_0x5e5605);};_0x1a71['BDCumX']=_0x2d3cfb,_0x1a71['piBleH']={},_0x1a71['raAcQj']=!![];}const _0x555c44=_0x5f661a[0x0],_0x5265a6=_0x3ae3c2+_0x555c44,_0x2993bd=_0x1a71['piBleH'][_0x5265a6];return!_0x2993bd?(_0x1a71eb=_0x1a71['BDCumX'](_0x1a71eb),_0x1a71['piBleH'][_0x5265a6]=_0x1a71eb):_0x1a71eb=_0x2993bd,_0x1a71eb;}export const pluginDev={'docsUrl':_0x34aae3(0xc0),'uploadUrl':_0x34aae3(0xcd),'scaffolds':{'full':{'key':'full','title':_0x34aae3(0xc5),'desc':_0x34aae3(0xc9),'downloadUrl':'https://gitee.com/YOUR_ORG/YOUR_REPO/raw/master/plugin-full.zip'},'backend':{'key':_0x34aae3(0xc4),'title':_0x34aae3(0xca),'desc':_0x34aae3(0xc1),'downloadUrl':_0x34aae3(0xc6)},'frontend':{'key':'frontend','title':_0x34aae3(0xc2),'desc':_0x34aae3(0xbb),'downloadUrl':'https://gitee.com/YOUR_ORG/YOUR_REPO/raw/master/plugin-frontend.zip'}}};
@@ -0,0 +1,217 @@
1
+ <template>
2
+ <div v-if="src" class="vm-audio">
3
+ <button
4
+ type="button"
5
+ class="vm-audio__play"
6
+ :disabled="!preview"
7
+ :title="preview ? '播放音频' : undefined"
8
+ @click="openPlayer"
9
+ >
10
+ <i class="ri-play-circle-line" aria-hidden="true" />
11
+ <span>{{ label }}</span>
12
+ </button>
13
+ </div>
14
+ <span v-else class="vm-audio is-empty">—</span>
15
+
16
+ <Teleport to="body">
17
+ <div
18
+ v-if="playerOpen"
19
+ class="vm-audio-player"
20
+ role="dialog"
21
+ aria-modal="true"
22
+ @click.self="closePlayer"
23
+ >
24
+ <button type="button" class="vm-audio-player__close" title="关闭" @click="closePlayer">
25
+ <i class="ri-close-line" />
26
+ </button>
27
+ <div class="vm-audio-player__panel">
28
+ <p v-if="titleText" class="vm-audio-player__title">{{ titleText }}</p>
29
+ <audio
30
+ ref="audioRef"
31
+ class="vm-audio-player__media"
32
+ :src="src"
33
+ controls
34
+ autoplay
35
+ playsinline
36
+ @ended="closePlayer"
37
+ />
38
+ </div>
39
+ </div>
40
+ </Teleport>
41
+ </template>
42
+
43
+ <script setup lang="ts">
44
+ import { computed, nextTick, onUnmounted, ref, watch } from 'vue'
45
+
46
+ defineOptions({ name: 'vm-audio' })
47
+
48
+ const props = withDefaults(
49
+ defineProps<{
50
+ /** 音频 URL(表格列绑定 modelValue) */
51
+ modelValue?: string | null
52
+ /** 按钮文案 */
53
+ label?: string
54
+ /** 弹层标题,默认取文件名 */
55
+ title?: string
56
+ /** 是否可播放 */
57
+ preview?: boolean
58
+ }>(),
59
+ {
60
+ label: '播放',
61
+ preview: true,
62
+ },
63
+ )
64
+
65
+ const playerOpen = ref(false)
66
+ const audioRef = ref<HTMLAudioElement | null>(null)
67
+
68
+ const src = computed(() => {
69
+ const v = props.modelValue
70
+ if (v == null) return ''
71
+ return String(v).trim()
72
+ })
73
+
74
+ const titleText = computed(() => {
75
+ if (props.title) return props.title
76
+ const s = src.value
77
+ if (!s) return ''
78
+ return s.split('?')[0]?.split('/').pop() || ''
79
+ })
80
+
81
+ function lockBody(lock: boolean) {
82
+ if (typeof document === 'undefined') return
83
+ document.body.style.overflow = lock ? 'hidden' : ''
84
+ }
85
+
86
+ function openPlayer() {
87
+ if (!props.preview || !src.value) return
88
+ playerOpen.value = true
89
+ lockBody(true)
90
+ void nextTick(() => {
91
+ audioRef.value?.play?.().catch(() => undefined)
92
+ })
93
+ }
94
+
95
+ function closePlayer() {
96
+ playerOpen.value = false
97
+ lockBody(false)
98
+ audioRef.value?.pause?.()
99
+ }
100
+
101
+ function onKeydown(e: KeyboardEvent) {
102
+ if (!playerOpen.value) return
103
+ if (e.key === 'Escape') closePlayer()
104
+ }
105
+
106
+ watch(playerOpen, (open) => {
107
+ if (typeof window === 'undefined') return
108
+ if (open) window.addEventListener('keydown', onKeydown)
109
+ else window.removeEventListener('keydown', onKeydown)
110
+ })
111
+
112
+ onUnmounted(() => {
113
+ lockBody(false)
114
+ if (typeof window !== 'undefined') {
115
+ window.removeEventListener('keydown', onKeydown)
116
+ }
117
+ })
118
+ </script>
119
+
120
+ <style lang="scss" scoped>
121
+ .vm-audio {
122
+ display: inline-flex;
123
+ vertical-align: middle;
124
+ }
125
+
126
+ .vm-audio.is-empty {
127
+ color: var(--muted-foreground);
128
+ font-size: 12px;
129
+ }
130
+
131
+ .vm-audio__play {
132
+ display: inline-flex;
133
+ align-items: center;
134
+ gap: 4px;
135
+ padding: 0;
136
+ border: none;
137
+ background: transparent;
138
+ color: var(--brand);
139
+ font-size: 13px;
140
+ font-weight: 600;
141
+ cursor: pointer;
142
+ transition: color 0.15s ease;
143
+
144
+ i {
145
+ font-size: 16px;
146
+ line-height: 1;
147
+ }
148
+
149
+ &:hover:not(:disabled) {
150
+ color: var(--brand-deep);
151
+ }
152
+
153
+ &:disabled {
154
+ cursor: default;
155
+ opacity: 0.55;
156
+ }
157
+ }
158
+ </style>
159
+
160
+ <style lang="scss">
161
+ .vm-audio-player {
162
+ position: fixed;
163
+ inset: 0;
164
+ z-index: 2200;
165
+ display: flex;
166
+ align-items: center;
167
+ justify-content: center;
168
+ padding: 24px;
169
+ background: rgb(10 12 20 / 72%);
170
+ }
171
+
172
+ .vm-audio-player__close {
173
+ position: absolute;
174
+ top: 16px;
175
+ right: 16px;
176
+ z-index: 2;
177
+ display: inline-flex;
178
+ width: 36px;
179
+ height: 36px;
180
+ align-items: center;
181
+ justify-content: center;
182
+ border: none;
183
+ border-radius: 10px;
184
+ background: rgb(255 255 255 / 12%);
185
+ color: #fff;
186
+ font-size: 20px;
187
+ cursor: pointer;
188
+
189
+ &:hover {
190
+ background: rgb(255 255 255 / 22%);
191
+ }
192
+ }
193
+
194
+ .vm-audio-player__panel {
195
+ width: min(480px, calc(100vw - 48px));
196
+ padding: 20px 22px;
197
+ border-radius: 16px;
198
+ background: var(--card);
199
+ box-shadow: 0 24px 48px rgb(15 23 42 / 18%);
200
+ }
201
+
202
+ .vm-audio-player__title {
203
+ margin: 0 0 12px;
204
+ overflow: hidden;
205
+ text-overflow: ellipsis;
206
+ white-space: nowrap;
207
+ font-size: 14px;
208
+ font-weight: 650;
209
+ color: var(--foreground);
210
+ }
211
+
212
+ .vm-audio-player__media {
213
+ display: block;
214
+ width: 100%;
215
+ outline: none;
216
+ }
217
+ </style>
@@ -3,44 +3,58 @@
3
3
  <Button size="sm" variant="outline" class="h-8" @click="open = true">
4
4
  自定义列
5
5
  </Button>
6
- <vm-dialog v-model:open="open" title="自定义列" width="420px">
7
- <div class="vm-col-custom">
8
- <label
9
- v-for="(item, i) in list"
10
- :key="String(item.prop)"
11
- class="vm-col-custom__item"
12
- >
13
- <input v-model="item.checked" type="checkbox" />
14
- <span>{{ item.label }}</span>
15
- <span class="vm-col-custom__move">
16
- <button type="button" :disabled="i === 0" @click.prevent="move(i, -1)">↑</button>
17
- <button
18
- type="button"
19
- :disabled="i === list.length - 1"
20
- @click.prevent="move(i, 1)"
6
+ <Dialog :open="open" @update:open="onOpenChange">
7
+ <DialogContent
8
+ shell="crud"
9
+ class="vm-col-custom__dialog"
10
+ @pointer-down-outside="preventDialogOutsideClose"
11
+ @focus-outside="preventDialogOutsideClose"
12
+ @interact-outside="preventDialogOutsideClose"
13
+ >
14
+ <DialogHeader class="vm-crud-shell__head">
15
+ <DialogTitle class="vm-crud-shell__title">自定义列</DialogTitle>
16
+ </DialogHeader>
17
+ <div class="vm-crud-shell__body">
18
+ <div class="vm-col-custom">
19
+ <label
20
+ v-for="(item, i) in list"
21
+ :key="String(item.prop)"
22
+ class="vm-col-custom__item"
21
23
  >
22
-
23
- </button>
24
- </span>
25
- </label>
26
- </div>
27
- <template #footer>
28
- <button type="button" class="vm-col-custom__btn is-ghost" @click="open = false">
29
- 取消
30
- </button>
31
- <button type="button" class="vm-col-custom__btn is-danger" @click="reset">
32
- 重置
33
- </button>
34
- <button type="button" class="vm-col-custom__btn is-primary" @click="save">
35
- 保存
36
- </button>
37
- </template>
38
- </vm-dialog>
24
+ <input v-model="item.checked" type="checkbox" />
25
+ <span>{{ item.label }}</span>
26
+ <span class="vm-col-custom__move">
27
+ <button type="button" :disabled="i === 0" @click.prevent="move(i, -1)">↑</button>
28
+ <button
29
+ type="button"
30
+ :disabled="i === list.length - 1"
31
+ @click.prevent="move(i, 1)"
32
+ >
33
+
34
+ </button>
35
+ </span>
36
+ </label>
37
+ </div>
38
+ </div>
39
+ <DialogFooter class="vm-crud-shell__foot">
40
+ <button type="button" class="vm-crud-shell__btn is-ghost" @click="open = false">
41
+ 取消
42
+ </button>
43
+ <button type="button" class="vm-crud-shell__btn is-danger" @click="reset">
44
+ 重置
45
+ </button>
46
+ <button type="button" class="vm-crud-shell__btn is-primary" @click="save">
47
+ 保存
48
+ </button>
49
+ </DialogFooter>
50
+ </DialogContent>
51
+ </Dialog>
39
52
  </div>
40
53
  </template>
41
54
 
42
55
  <script setup lang="ts">
43
56
  import { ref, watch, inject } from 'vue'
57
+ import { preventDialogOutsideClose } from '../../lib/dialog-float'
44
58
  import { injectTableOptions, TABLE_API_KEY } from '../key'
45
59
  defineOptions({ name: 'vm-column-custom' })
46
60
 
@@ -63,6 +77,10 @@ const open = ref(false)
63
77
  const list = ref<Item[]>([])
64
78
  const storageKey = computedKey()
65
79
 
80
+ function onOpenChange(v: boolean) {
81
+ open.value = v
82
+ }
83
+
66
84
  function computedKey() {
67
85
  return `column-custom__${props.name || (typeof location !== 'undefined' ? location.pathname : 'default')}`
68
86
  }
@@ -170,28 +188,4 @@ function save() {
170
188
  }
171
189
  }
172
190
  }
173
-
174
- .vm-col-custom__btn {
175
- height: 36px;
176
- padding: 0 14px;
177
- border: none;
178
- border-radius: 12px;
179
- font-size: 13px;
180
- font-weight: 600;
181
- cursor: pointer;
182
-
183
- &.is-ghost {
184
- background: var(--muted);
185
- }
186
-
187
- &.is-danger {
188
- background: var(--danger-soft, #fceeee);
189
- color: var(--danger, #c45b5e);
190
- }
191
-
192
- &.is-primary {
193
- background: var(--brand);
194
- color: #fff;
195
- }
196
- }
197
191
  </style>
@@ -0,0 +1,90 @@
1
+ <template>
2
+ <div v-if="src" class="vm-file-preview">
3
+ <button
4
+ type="button"
5
+ class="vm-file-preview__btn"
6
+ :disabled="!preview"
7
+ @click="openViewer"
8
+ >
9
+ <i class="ri-file-search-line" aria-hidden="true" />
10
+ <span>{{ label }}</span>
11
+ </button>
12
+ </div>
13
+ <span v-else class="vm-file-preview is-empty">—</span>
14
+
15
+ <vm-preview-viewer v-model:open="viewerOpen" :src="src" :file-name="fileName" />
16
+ </template>
17
+
18
+ <script setup lang="ts">
19
+ import { computed, ref } from 'vue'
20
+ import VmPreviewViewer from './vm-preview-viewer.vue'
21
+
22
+ defineOptions({ name: 'vm-file-preview' })
23
+
24
+ const props = withDefaults(
25
+ defineProps<{
26
+ /** 文件 URL(表格列绑定 modelValue) */
27
+ modelValue?: string | null
28
+ /** 展示用文件名 */
29
+ fileName?: string
30
+ label?: string
31
+ preview?: boolean
32
+ }>(),
33
+ {
34
+ label: '查看',
35
+ preview: true,
36
+ },
37
+ )
38
+
39
+ const viewerOpen = ref(false)
40
+
41
+ const src = computed(() => {
42
+ const v = props.modelValue
43
+ if (v == null) return ''
44
+ return String(v).trim()
45
+ })
46
+
47
+ function openViewer() {
48
+ if (!props.preview || !src.value) return
49
+ viewerOpen.value = true
50
+ }
51
+ </script>
52
+
53
+ <style lang="scss" scoped>
54
+ .vm-file-preview {
55
+ display: inline-flex;
56
+ vertical-align: middle;
57
+ }
58
+
59
+ .vm-file-preview.is-empty {
60
+ color: var(--muted-foreground);
61
+ font-size: 12px;
62
+ }
63
+
64
+ .vm-file-preview__btn {
65
+ display: inline-flex;
66
+ align-items: center;
67
+ gap: 4px;
68
+ padding: 0;
69
+ border: none;
70
+ background: transparent;
71
+ color: var(--brand);
72
+ font-size: 13px;
73
+ font-weight: 600;
74
+ cursor: pointer;
75
+
76
+ i {
77
+ font-size: 16px;
78
+ line-height: 1;
79
+ }
80
+
81
+ &:hover:not(:disabled) {
82
+ color: var(--brand-deep);
83
+ }
84
+
85
+ &:disabled {
86
+ opacity: 0.55;
87
+ cursor: default;
88
+ }
89
+ }
90
+ </style>