vome-core 0.0.23 → 0.0.25

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 (51) hide show
  1. package/dist/admin/components/vm-action-btn.vue +109 -0
  2. package/dist/admin/components/vm-aside-list-item.vue +124 -0
  3. package/dist/admin/components/vm-card-grid.vue +36 -0
  4. package/dist/admin/components/vm-card.vue +100 -0
  5. package/dist/admin/components/vm-perm-section.vue +57 -0
  6. package/dist/admin/components/vm-split-layout.vue +85 -0
  7. package/dist/admin/config/plugin-dev.js +1 -1
  8. package/dist/admin/crud/components/vm-avatar.vue +103 -0
  9. package/dist/admin/crud/components/vm-copy-btn.vue +101 -0
  10. package/dist/admin/crud/components/vm-datetime-picker.vue +9 -0
  11. package/dist/admin/crud/components/vm-ellipsis-text.vue +34 -0
  12. package/dist/admin/crud/components/vm-form-hint.vue +63 -0
  13. package/dist/admin/crud/components/vm-json-code.vue +40 -0
  14. package/dist/admin/crud/components/vm-json-editor.vue +128 -0
  15. package/dist/admin/crud/components/vm-json-viewer.vue +147 -0
  16. package/dist/admin/crud/components/vm-preview-viewer.vue +56 -2
  17. package/dist/admin/crud/components/vm-status-tag.vue +157 -0
  18. package/dist/admin/crud/components/vm-tag-list.vue +82 -0
  19. package/dist/admin/crud/components/vm-text-link.vue +51 -0
  20. package/dist/admin/crud/config.js +1 -1
  21. package/dist/admin/crud/confirm.js +1 -1
  22. package/dist/admin/crud/dict.js +1 -1
  23. package/dist/admin/crud/index.d.ts +11 -1
  24. package/dist/admin/crud/index.js +1 -1
  25. package/dist/admin/crud/key.js +1 -1
  26. package/dist/admin/crud/mitt.js +1 -1
  27. package/dist/admin/crud/plugins.js +1 -1
  28. package/dist/admin/crud/registry.js +3 -0
  29. package/dist/admin/crud/span.js +1 -1
  30. package/dist/admin/crud/style.js +1 -1
  31. package/dist/admin/crud/validate.js +1 -1
  32. package/dist/admin/directives/perm.js +1 -1
  33. package/dist/admin/hooks/useUpload.js +1 -1
  34. package/dist/admin/lib/browser.js +1 -1
  35. package/dist/admin/lib/cn.js +1 -1
  36. package/dist/admin/lib/dialog-float.js +1 -1
  37. package/dist/admin/lib/export-excel.js +1 -1
  38. package/dist/admin/lib/file-preview.js +1 -1
  39. package/dist/admin/lib/json.d.ts +12 -0
  40. package/dist/admin/lib/json.js +1 -0
  41. package/dist/admin/lib/menu.js +1 -1
  42. package/dist/admin/lib/tree.js +1 -1
  43. package/dist/admin/lib/upload.js +1 -1
  44. package/dist/admin/lib/video-frame.js +1 -1
  45. package/dist/index.js +1 -1
  46. package/dist/server/index.js +1 -1
  47. package/dist/shared/excel.js +1 -1
  48. package/dist/shared/index.js +1 -1
  49. package/dist/shared/tree.js +1 -1
  50. package/package.json +1 -1
  51. package/dist/admin/crud/components/vm-file-preview.vue +0 -90
@@ -0,0 +1,101 @@
1
+ <template>
2
+ <button
3
+ type="button"
4
+ class="vm-copy-btn"
5
+ :class="[`is-${size}`]"
6
+ :disabled="disabled || copying || !text"
7
+ @click="copy"
8
+ >
9
+ <i :class="copying ? 'ri-loader-4-line is-spin' : icon" aria-hidden="true" />
10
+ <span>{{ copying ? copyingText : label }}</span>
11
+ </button>
12
+ </template>
13
+
14
+ <script setup lang="ts">
15
+ import { ref } from 'vue'
16
+
17
+ defineOptions({ name: 'vm-copy-btn' })
18
+
19
+ const props = withDefaults(
20
+ defineProps<{
21
+ text?: string
22
+ label?: string
23
+ copyingText?: string
24
+ icon?: string
25
+ size?: 'sm' | 'md'
26
+ disabled?: boolean
27
+ }>(),
28
+ {
29
+ label: '复制',
30
+ copyingText: '复制中…',
31
+ icon: 'ri-file-copy-line',
32
+ size: 'md',
33
+ disabled: false,
34
+ },
35
+ )
36
+
37
+ const emit = defineEmits<{
38
+ copied: []
39
+ error: [e: unknown]
40
+ }>()
41
+
42
+ const copying = ref(false)
43
+
44
+ async function copy() {
45
+ if (!props.text || copying.value) return
46
+ copying.value = true
47
+ try {
48
+ await navigator.clipboard.writeText(props.text)
49
+ emit('copied')
50
+ } catch (e) {
51
+ emit('error', e)
52
+ } finally {
53
+ copying.value = false
54
+ }
55
+ }
56
+ </script>
57
+
58
+ <style lang="scss" scoped>
59
+ .vm-copy-btn {
60
+ display: inline-flex;
61
+ align-items: center;
62
+ gap: 4px;
63
+ border: none;
64
+ border-radius: 8px;
65
+ background: color-mix(in srgb, var(--brand) 10%, var(--card));
66
+ color: var(--brand);
67
+ font-size: 12px;
68
+ font-weight: 600;
69
+ cursor: pointer;
70
+
71
+ &:hover:not(:disabled) {
72
+ background: color-mix(in srgb, var(--brand) 18%, var(--card));
73
+ }
74
+
75
+ &:disabled {
76
+ opacity: 0.45;
77
+ cursor: not-allowed;
78
+ }
79
+
80
+ &.is-md {
81
+ height: 32px;
82
+ padding: 0 12px;
83
+ }
84
+
85
+ &.is-sm {
86
+ height: 28px;
87
+ padding: 0 10px;
88
+ font-size: 11px;
89
+ }
90
+
91
+ i.is-spin {
92
+ animation: vm-copy-spin 0.8s linear infinite;
93
+ }
94
+ }
95
+
96
+ @keyframes vm-copy-spin {
97
+ to {
98
+ transform: rotate(360deg);
99
+ }
100
+ }
101
+ </style>
@@ -0,0 +1,9 @@
1
+ <template>
2
+ <vm-date-picker v-bind="$attrs" type="datetime" />
3
+ </template>
4
+
5
+ <script setup lang="ts">
6
+ import VmDatePicker from './vm-date-picker.vue'
7
+
8
+ defineOptions({ name: 'vm-datetime-picker', inheritAttrs: false })
9
+ </script>
@@ -0,0 +1,34 @@
1
+ <template>
2
+ <span class="vm-ellipsis-text" :title="titleText">{{ text }}</span>
3
+ </template>
4
+
5
+ <script setup lang="ts">
6
+ import { computed } from 'vue'
7
+
8
+ defineOptions({ name: 'vm-ellipsis-text' })
9
+
10
+ const props = defineProps<{
11
+ modelValue?: unknown
12
+ title?: string
13
+ }>()
14
+
15
+ const text = computed(() => {
16
+ const v = props.modelValue
17
+ if (v == null || v === '') return '—'
18
+ return String(v)
19
+ })
20
+
21
+ const titleText = computed(() => props.title ?? (text.value === '—' ? undefined : text.value))
22
+ </script>
23
+
24
+ <style lang="scss" scoped>
25
+ .vm-ellipsis-text {
26
+ display: inline-block;
27
+ max-width: 100%;
28
+ overflow: hidden;
29
+ text-overflow: ellipsis;
30
+ white-space: nowrap;
31
+ font-size: 12px;
32
+ color: var(--muted-foreground);
33
+ }
34
+ </style>
@@ -0,0 +1,63 @@
1
+ <template>
2
+ <p class="vm-form-hint" :class="[`is-${variant}`]">
3
+ <i v-if="icon" :class="icon" aria-hidden="true" />
4
+ <slot>{{ text }}</slot>
5
+ </p>
6
+ </template>
7
+
8
+ <script setup lang="ts">
9
+ defineOptions({ name: 'vm-form-hint' })
10
+
11
+ withDefaults(
12
+ defineProps<{
13
+ text?: string
14
+ variant?: 'info' | 'warn' | 'success'
15
+ icon?: string
16
+ }>(),
17
+ {
18
+ text: '',
19
+ variant: 'info',
20
+ icon: 'ri-information-line',
21
+ },
22
+ )
23
+ </script>
24
+
25
+ <style lang="scss" scoped>
26
+ .vm-form-hint {
27
+ display: flex;
28
+ align-items: flex-start;
29
+ gap: 8px;
30
+ margin: 0;
31
+ padding: 10px 12px;
32
+ border-radius: 10px;
33
+ font-size: 13px;
34
+ line-height: 1.5;
35
+ color: var(--muted-foreground);
36
+ background: color-mix(in srgb, var(--brand) 8%, var(--card));
37
+
38
+ i {
39
+ flex-shrink: 0;
40
+ margin-top: 2px;
41
+ font-size: 15px;
42
+ color: var(--brand);
43
+ }
44
+
45
+ &.is-warn {
46
+ background: color-mix(in srgb, #f59e0b 10%, var(--card));
47
+ color: color-mix(in srgb, var(--foreground) 72%, #d97706);
48
+
49
+ i {
50
+ color: #d97706;
51
+ }
52
+ }
53
+
54
+ &.is-success {
55
+ background: var(--success-soft);
56
+ color: var(--success);
57
+
58
+ i {
59
+ color: var(--success);
60
+ }
61
+ }
62
+ }
63
+ </style>
@@ -0,0 +1,40 @@
1
+ <template>
2
+ <pre class="vm-json-code vm-scroll">{{ body }}</pre>
3
+ </template>
4
+
5
+ <script setup lang="ts">
6
+ import { computed } from 'vue'
7
+ import { prettyJson } from '../../lib/json'
8
+
9
+ defineOptions({ name: 'vm-json-code' })
10
+
11
+ const props = withDefaults(
12
+ defineProps<{
13
+ modelValue?: unknown
14
+ empty?: string
15
+ }>(),
16
+ {
17
+ empty: '(空)',
18
+ },
19
+ )
20
+
21
+ const body = computed(() => prettyJson(props.modelValue, props.empty))
22
+ </script>
23
+
24
+ <style lang="scss" scoped>
25
+ .vm-json-code {
26
+ flex: 1;
27
+ margin: 0;
28
+ min-height: 0;
29
+ overflow: auto;
30
+ padding: 16px 22px;
31
+ border-radius: var(--radius);
32
+ background: var(--muted);
33
+ color: var(--foreground);
34
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
35
+ font-size: 12px;
36
+ line-height: 1.55;
37
+ white-space: pre-wrap;
38
+ word-break: break-word;
39
+ }
40
+ </style>
@@ -0,0 +1,128 @@
1
+ <template>
2
+ <div class="vm-json-editor">
3
+ <textarea
4
+ class="vm-json-editor__input vm-scroll"
5
+ :class="{ 'is-error': Boolean(error) }"
6
+ :value="inner"
7
+ :disabled="disabled"
8
+ :placeholder="placeholder"
9
+ :rows="rows"
10
+ @input="onInput"
11
+ @blur="validate"
12
+ />
13
+ <p v-if="error" class="vm-json-editor__error">{{ error }}</p>
14
+ </div>
15
+ </template>
16
+
17
+ <script setup lang="ts">
18
+ import { ref, watch } from 'vue'
19
+ import { parseJsonInput, prettyJson, textOfJson } from '../../lib/json'
20
+
21
+ defineOptions({ name: 'vm-json-editor' })
22
+
23
+ const props = withDefaults(
24
+ defineProps<{
25
+ modelValue?: unknown
26
+ placeholder?: string
27
+ rows?: number
28
+ disabled?: boolean
29
+ /** blur 时自动格式化 */
30
+ formatOnBlur?: boolean
31
+ }>(),
32
+ {
33
+ placeholder: '请输入 JSON',
34
+ rows: 8,
35
+ formatOnBlur: true,
36
+ },
37
+ )
38
+
39
+ const emit = defineEmits<{
40
+ 'update:modelValue': [v: unknown]
41
+ change: [v: unknown]
42
+ error: [msg: string]
43
+ }>()
44
+
45
+ const inner = ref('')
46
+ const error = ref('')
47
+
48
+ function syncFromModel() {
49
+ inner.value = prettyJson(props.modelValue, '')
50
+ error.value = ''
51
+ }
52
+
53
+ watch(
54
+ () => props.modelValue,
55
+ () => syncFromModel(),
56
+ { immediate: true },
57
+ )
58
+
59
+ function onInput(e: Event) {
60
+ inner.value = (e.target as HTMLTextAreaElement).value
61
+ error.value = ''
62
+ }
63
+
64
+ function validate() {
65
+ const parsed = parseJsonInput(inner.value)
66
+ if (!parsed.ok) {
67
+ error.value = parsed.error
68
+ emit('error', parsed.error)
69
+ return
70
+ }
71
+ if (props.formatOnBlur) {
72
+ inner.value = prettyJson(parsed.value, '')
73
+ }
74
+ error.value = ''
75
+ emit('update:modelValue', parsed.value)
76
+ emit('change', parsed.value)
77
+ }
78
+
79
+ defineExpose({
80
+ validate,
81
+ getText: () => inner.value,
82
+ setText: (v: unknown) => {
83
+ inner.value = textOfJson(v)
84
+ },
85
+ })
86
+ </script>
87
+
88
+ <style lang="scss" scoped>
89
+ .vm-json-editor {
90
+ display: grid;
91
+ gap: 6px;
92
+ min-width: 0;
93
+ }
94
+
95
+ .vm-json-editor__input {
96
+ width: 100%;
97
+ min-height: 120px;
98
+ padding: 10px 12px;
99
+ border: 1px solid var(--border);
100
+ border-radius: var(--vm-control-radius);
101
+ background: var(--muted);
102
+ color: var(--foreground);
103
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
104
+ font-size: 12px;
105
+ line-height: 1.55;
106
+ resize: vertical;
107
+ outline: none;
108
+
109
+ &:focus {
110
+ border-color: var(--focus-border, var(--brand));
111
+ }
112
+
113
+ &.is-error {
114
+ border-color: var(--danger);
115
+ }
116
+
117
+ &:disabled {
118
+ opacity: 0.6;
119
+ cursor: not-allowed;
120
+ }
121
+ }
122
+
123
+ .vm-json-editor__error {
124
+ margin: 0;
125
+ font-size: 12px;
126
+ color: var(--danger);
127
+ }
128
+ </style>
@@ -0,0 +1,147 @@
1
+ <template>
2
+ <template v-if="mode === 'code'">
3
+ <vm-json-code :model-value="modelValue" :empty="empty" />
4
+ </template>
5
+ <template v-else>
6
+ <vm-text-link
7
+ v-if="hasContent"
8
+ :label="label"
9
+ @click="open = true"
10
+ />
11
+ <span v-else class="vm-json-viewer is-empty">—</span>
12
+ </template>
13
+
14
+ <Teleport to="body">
15
+ <div
16
+ v-if="open"
17
+ class="vm-json-viewer__dialog"
18
+ role="dialog"
19
+ aria-modal="true"
20
+ @click.self="open = false"
21
+ >
22
+ <header class="vm-json-viewer__head">
23
+ <span class="vm-json-viewer__title">{{ title }}</span>
24
+ <div class="vm-json-viewer__actions">
25
+ <vm-copy-btn :text="body" size="sm" label="复制" />
26
+ <button type="button" class="vm-json-viewer__close" title="关闭" @click="open = false">
27
+ <i class="ri-close-line" />
28
+ </button>
29
+ </div>
30
+ </header>
31
+ <div class="vm-json-viewer__body vm-scroll">
32
+ <vm-json-code :model-value="modelValue" :empty="empty" />
33
+ </div>
34
+ </div>
35
+ </Teleport>
36
+ </template>
37
+
38
+ <script setup lang="ts">
39
+ import { computed, ref } from 'vue'
40
+ import { hasJsonContent, prettyJson } from '../../lib/json'
41
+ import VmJsonCode from './vm-json-code.vue'
42
+ import VmTextLink from './vm-text-link.vue'
43
+ import VmCopyBtn from './vm-copy-btn.vue'
44
+
45
+ defineOptions({ name: 'vm-json-viewer' })
46
+
47
+ const props = withDefaults(
48
+ defineProps<{
49
+ modelValue?: unknown
50
+ title?: string
51
+ label?: string
52
+ /** button 打开浮层;code 仅渲染代码块(upsert fill) */
53
+ mode?: 'button' | 'code'
54
+ empty?: string
55
+ }>(),
56
+ {
57
+ title: 'JSON',
58
+ label: '查看',
59
+ mode: 'button',
60
+ empty: '(空)',
61
+ },
62
+ )
63
+
64
+ const open = ref(false)
65
+ const hasContent = computed(() => hasJsonContent(props.modelValue))
66
+ const body = computed(() => prettyJson(props.modelValue, props.empty))
67
+ </script>
68
+
69
+ <style lang="scss" scoped>
70
+ .vm-json-viewer.is-empty {
71
+ color: var(--muted-foreground);
72
+ }
73
+
74
+ .vm-json-viewer__dialog {
75
+ position: fixed;
76
+ inset: 0;
77
+ z-index: 5200;
78
+ display: flex;
79
+ flex-direction: column;
80
+ padding: 24px;
81
+ background: color-mix(in srgb, var(--foreground) 42%, transparent);
82
+ }
83
+
84
+ .vm-json-viewer__head {
85
+ display: flex;
86
+ flex-shrink: 0;
87
+ align-items: center;
88
+ justify-content: space-between;
89
+ gap: 12px;
90
+ margin: 0 auto;
91
+ width: min(960px, 100%);
92
+ padding: 12px 16px;
93
+ border-radius: 14px 14px 0 0;
94
+ background: var(--card);
95
+ border: 1px solid color-mix(in srgb, var(--foreground) 8%, transparent);
96
+ border-bottom: none;
97
+ }
98
+
99
+ .vm-json-viewer__title {
100
+ font-size: 14px;
101
+ font-weight: 700;
102
+ color: var(--foreground);
103
+ }
104
+
105
+ .vm-json-viewer__actions {
106
+ display: inline-flex;
107
+ align-items: center;
108
+ gap: 8px;
109
+ }
110
+
111
+ .vm-json-viewer__close {
112
+ display: inline-flex;
113
+ width: 32px;
114
+ height: 32px;
115
+ align-items: center;
116
+ justify-content: center;
117
+ border: none;
118
+ border-radius: 8px;
119
+ background: transparent;
120
+ color: var(--muted-foreground);
121
+ cursor: pointer;
122
+
123
+ &:hover {
124
+ background: var(--muted);
125
+ color: var(--foreground);
126
+ }
127
+ }
128
+
129
+ .vm-json-viewer__body {
130
+ flex: 1;
131
+ min-height: 0;
132
+ margin: 0 auto;
133
+ width: min(960px, 100%);
134
+ display: flex;
135
+ flex-direction: column;
136
+ border-radius: 0 0 14px 14px;
137
+ background: var(--card);
138
+ border: 1px solid color-mix(in srgb, var(--foreground) 8%, transparent);
139
+ border-top: none;
140
+ overflow: hidden;
141
+
142
+ :deep(.vm-json-code) {
143
+ border-radius: 0;
144
+ background: var(--muted);
145
+ }
146
+ }
147
+ </style>
@@ -40,6 +40,17 @@
40
40
  </template>
41
41
  </button>
42
42
  </div>
43
+ <div v-else-if="src && showButton" class="vm-preview-viewer">
44
+ <button
45
+ type="button"
46
+ class="vm-preview-viewer__btn"
47
+ :disabled="!preview"
48
+ @click="openViewer"
49
+ >
50
+ <i class="ri-file-search-line" aria-hidden="true" />
51
+ <span>{{ label }}</span>
52
+ </button>
53
+ </div>
43
54
  <span v-else-if="!src" class="vm-preview-viewer is-empty">—</span>
44
55
  </template>
45
56
 
@@ -190,15 +201,19 @@ const props = withDefaults(
190
201
  defineProps<{
191
202
  /** CRUD 列绑定的 URL */
192
203
  modelValue?: string | null
193
- /** 仅渲染浮层(vm-upload-item / vm-file-preview 内部) */
204
+ /** 仅渲染浮层(vm-upload-item 内部) */
194
205
  embedded?: boolean
195
206
  /** embedded 时外部控制开关 */
196
207
  open?: boolean
197
208
  /** embedded 时可显式传 src */
198
209
  src?: string
199
210
  fileName?: string
211
+ /** 按钮触发文案(PDF / Excel 等非缩略图类型) */
212
+ label?: string
200
213
  /** 覆盖 URL 推断的媒体类型 */
201
214
  mediaKind?: MediaPreviewKind
215
+ /** thumb | button | auto:触发方式 */
216
+ trigger?: 'thumb' | 'button' | 'auto'
202
217
  /** image | video | auto(缩略图列) */
203
218
  type?: 'image' | 'video' | 'auto'
204
219
  size?: number | string
@@ -208,6 +223,8 @@ const props = withDefaults(
208
223
  }>(),
209
224
  {
210
225
  embedded: false,
226
+ label: '查看',
227
+ trigger: 'auto',
211
228
  type: 'auto',
212
229
  size: 40,
213
230
  radius: 8,
@@ -242,14 +259,24 @@ const isVideoKind = computed(() => {
242
259
  })
243
260
 
244
261
  const showThumb = computed(() => {
262
+ if (props.embedded) return false
263
+ if (props.trigger === 'button') return false
264
+ if (props.trigger === 'thumb') return !!src.value
245
265
  if (props.mediaKind === 'image' || props.mediaKind === 'video') return true
246
266
  if (props.mediaKind) return false
247
267
  if (props.type === 'image' || props.type === 'video') return true
248
268
  if (isVideoKind.value) return true
249
- const k = getMediaPreviewKind(src.value)
269
+ const k = getMediaPreviewKind(props.fileName || src.value)
250
270
  return k === 'image' || k === 'video'
251
271
  })
252
272
 
273
+ const showButton = computed(() => {
274
+ if (props.embedded || !src.value) return false
275
+ if (props.trigger === 'button') return true
276
+ if (props.trigger === 'thumb') return false
277
+ return !showThumb.value
278
+ })
279
+
253
280
  const kind = computed((): MediaPreviewKind => {
254
281
  if (props.mediaKind) return props.mediaKind
255
282
  if (props.type === 'video' || isVideoKind.value) return 'video'
@@ -453,6 +480,33 @@ onUnmounted(() => {
453
480
  line-height: 1;
454
481
  }
455
482
  }
483
+
484
+ .vm-preview-viewer__btn {
485
+ display: inline-flex;
486
+ align-items: center;
487
+ gap: 4px;
488
+ padding: 0;
489
+ border: none;
490
+ background: transparent;
491
+ color: var(--brand);
492
+ font-size: 13px;
493
+ font-weight: 600;
494
+ cursor: pointer;
495
+
496
+ i {
497
+ font-size: 16px;
498
+ line-height: 1;
499
+ }
500
+
501
+ &:hover:not(:disabled) {
502
+ color: var(--brand-deep);
503
+ }
504
+
505
+ &:disabled {
506
+ opacity: 0.55;
507
+ cursor: default;
508
+ }
509
+ }
456
510
  </style>
457
511
 
458
512
  <style lang="scss">