vome-core 0.0.24 → 0.0.26
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.
- package/dist/admin/components/vm-action-btn.vue +109 -0
- package/dist/admin/components/vm-aside-list-item.vue +124 -0
- package/dist/admin/components/vm-card-grid.vue +36 -0
- package/dist/admin/components/vm-card.vue +100 -0
- package/dist/admin/components/vm-perm-section.vue +57 -0
- package/dist/admin/components/vm-split-layout.vue +85 -0
- package/dist/admin/config/plugin-dev.js +1 -1
- package/dist/admin/crud/components/vm-avatar.vue +103 -0
- package/dist/admin/crud/components/vm-copy-btn.vue +101 -0
- package/dist/admin/crud/components/vm-datetime-picker.vue +9 -0
- package/dist/admin/crud/components/vm-ellipsis-text.vue +34 -0
- package/dist/admin/crud/components/vm-form-hint.vue +63 -0
- package/dist/admin/crud/components/vm-json-code.vue +40 -0
- package/dist/admin/crud/components/vm-json-editor.vue +128 -0
- package/dist/admin/crud/components/vm-json-viewer.vue +147 -0
- package/dist/admin/crud/components/vm-preview-viewer.vue +39 -6
- package/dist/admin/crud/components/vm-status-tag.vue +157 -0
- package/dist/admin/crud/components/vm-tag-list.vue +82 -0
- package/dist/admin/crud/components/vm-text-link.vue +51 -0
- package/dist/admin/crud/config.js +1 -1
- package/dist/admin/crud/confirm.js +1 -1
- package/dist/admin/crud/dict.js +1 -1
- package/dist/admin/crud/index.d.ts +11 -1
- package/dist/admin/crud/index.js +1 -1
- package/dist/admin/crud/key.js +1 -1
- package/dist/admin/crud/mitt.js +1 -1
- package/dist/admin/crud/plugins.js +1 -1
- package/dist/admin/crud/registry.js +1 -0
- package/dist/admin/crud/span.js +1 -1
- package/dist/admin/crud/style.js +1 -1
- package/dist/admin/crud/validate.js +1 -1
- package/dist/admin/directives/perm.js +1 -1
- package/dist/admin/hooks/useUpload.js +1 -1
- package/dist/admin/lib/browser.js +1 -1
- package/dist/admin/lib/cn.js +1 -1
- package/dist/admin/lib/dialog-float.js +1 -1
- package/dist/admin/lib/export-excel.js +1 -1
- package/dist/admin/lib/file-preview.js +1 -1
- package/dist/admin/lib/json.d.ts +12 -0
- package/dist/admin/lib/json.js +1 -0
- package/dist/admin/lib/menu.js +1 -1
- package/dist/admin/lib/tree.js +1 -1
- package/dist/admin/lib/upload.js +1 -1
- package/dist/admin/lib/video-frame.js +1 -1
- package/dist/index.js +1 -1
- package/dist/server/index.js +1 -1
- package/dist/shared/excel.js +1 -1
- package/dist/shared/index.js +1 -1
- package/dist/shared/tree.js +1 -1
- package/package.json +1 -1
- package/dist/admin/crud/components/vm-audio.vue +0 -217
|
@@ -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,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>
|
|
@@ -45,10 +45,11 @@
|
|
|
45
45
|
type="button"
|
|
46
46
|
class="vm-preview-viewer__btn"
|
|
47
47
|
:disabled="!preview"
|
|
48
|
+
:title="preview ? triggerTitle : undefined"
|
|
48
49
|
@click="openViewer"
|
|
49
50
|
>
|
|
50
|
-
<i class="
|
|
51
|
-
<span>{{
|
|
51
|
+
<i :class="triggerIcon" aria-hidden="true" />
|
|
52
|
+
<span>{{ displayLabel }}</span>
|
|
52
53
|
</button>
|
|
53
54
|
</div>
|
|
54
55
|
<span v-else-if="!src" class="vm-preview-viewer is-empty">—</span>
|
|
@@ -132,9 +133,12 @@
|
|
|
132
133
|
|
|
133
134
|
<audio
|
|
134
135
|
v-else-if="kind === 'audio'"
|
|
136
|
+
ref="audioRef"
|
|
135
137
|
class="vm-file-preview-viewer__audio"
|
|
136
138
|
:src="src"
|
|
137
139
|
controls
|
|
140
|
+
autoplay
|
|
141
|
+
playsinline
|
|
138
142
|
/>
|
|
139
143
|
|
|
140
144
|
<iframe
|
|
@@ -214,8 +218,8 @@ const props = withDefaults(
|
|
|
214
218
|
mediaKind?: MediaPreviewKind
|
|
215
219
|
/** thumb | button | auto:触发方式 */
|
|
216
220
|
trigger?: 'thumb' | 'button' | 'auto'
|
|
217
|
-
/** image | video | auto
|
|
218
|
-
type?: 'image' | 'video' | 'auto'
|
|
221
|
+
/** image | video | audio | auto(缩略图列或类型覆盖) */
|
|
222
|
+
type?: 'image' | 'video' | 'audio' | 'auto'
|
|
219
223
|
size?: number | string
|
|
220
224
|
radius?: number | string
|
|
221
225
|
preview?: boolean
|
|
@@ -243,6 +247,7 @@ const excelHtml = ref('')
|
|
|
243
247
|
const scale = ref(1)
|
|
244
248
|
const rotate = ref(0)
|
|
245
249
|
const videoRef = ref<HTMLVideoElement | null>(null)
|
|
250
|
+
const audioRef = ref<HTMLAudioElement | null>(null)
|
|
246
251
|
|
|
247
252
|
const src = computed(() => {
|
|
248
253
|
const raw = props.embedded ? props.src : props.modelValue
|
|
@@ -252,12 +257,19 @@ const src = computed(() => {
|
|
|
252
257
|
|
|
253
258
|
const isVideoKind = computed(() => {
|
|
254
259
|
if (props.mediaKind === 'video') return true
|
|
255
|
-
if (props.mediaKind === 'image') return false
|
|
260
|
+
if (props.mediaKind === 'image' || props.mediaKind === 'audio') return false
|
|
256
261
|
if (props.type === 'video') return true
|
|
257
|
-
if (props.type === 'image') return false
|
|
262
|
+
if (props.type === 'image' || props.type === 'audio') return false
|
|
258
263
|
return VIDEO_EXT.test(src.value)
|
|
259
264
|
})
|
|
260
265
|
|
|
266
|
+
const isAudioKind = computed(() => {
|
|
267
|
+
if (props.mediaKind === 'audio') return true
|
|
268
|
+
if (props.type === 'audio') return true
|
|
269
|
+
if (props.mediaKind || props.type === 'image' || props.type === 'video') return false
|
|
270
|
+
return getMediaPreviewKind(props.fileName || src.value) === 'audio'
|
|
271
|
+
})
|
|
272
|
+
|
|
261
273
|
const showThumb = computed(() => {
|
|
262
274
|
if (props.embedded) return false
|
|
263
275
|
if (props.trigger === 'button') return false
|
|
@@ -279,11 +291,26 @@ const showButton = computed(() => {
|
|
|
279
291
|
|
|
280
292
|
const kind = computed((): MediaPreviewKind => {
|
|
281
293
|
if (props.mediaKind) return props.mediaKind
|
|
294
|
+
if (props.type === 'audio' || isAudioKind.value) return 'audio'
|
|
282
295
|
if (props.type === 'video' || isVideoKind.value) return 'video'
|
|
283
296
|
if (props.type === 'image') return 'image'
|
|
284
297
|
return getMediaPreviewKind(props.fileName || src.value)
|
|
285
298
|
})
|
|
286
299
|
|
|
300
|
+
const displayLabel = computed(() => {
|
|
301
|
+
if (props.label !== '查看') return props.label
|
|
302
|
+
if (kind.value === 'audio') return '播放'
|
|
303
|
+
return props.label
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
const triggerIcon = computed(() =>
|
|
307
|
+
kind.value === 'audio' ? 'ri-play-circle-line' : 'ri-file-search-line',
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
const triggerTitle = computed(() =>
|
|
311
|
+
kind.value === 'audio' ? '播放音频' : '查看文件',
|
|
312
|
+
)
|
|
313
|
+
|
|
287
314
|
const kindLabel = computed(() => KIND_LABEL[kind.value])
|
|
288
315
|
|
|
289
316
|
const displayName = computed(() =>
|
|
@@ -399,6 +426,11 @@ watch(viewerOpen, (open) => {
|
|
|
399
426
|
videoRef.value?.play?.().catch(() => undefined)
|
|
400
427
|
})
|
|
401
428
|
}
|
|
429
|
+
if (kind.value === 'audio') {
|
|
430
|
+
void nextTick(() => {
|
|
431
|
+
audioRef.value?.play?.().catch(() => undefined)
|
|
432
|
+
})
|
|
433
|
+
}
|
|
402
434
|
} else {
|
|
403
435
|
window.removeEventListener('keydown', onKeydown)
|
|
404
436
|
loading.value = false
|
|
@@ -406,6 +438,7 @@ watch(viewerOpen, (open) => {
|
|
|
406
438
|
textContent.value = ''
|
|
407
439
|
excelHtml.value = ''
|
|
408
440
|
videoRef.value?.pause?.()
|
|
441
|
+
audioRef.value?.pause?.()
|
|
409
442
|
}
|
|
410
443
|
})
|
|
411
444
|
|