@arronqzy/vue-view 0.1.0
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/README.md +50 -0
- package/package.json +49 -0
- package/src/env.d.ts +62 -0
- package/src/index.ts +4 -0
- package/src/panel/VueViewOnlinePreview.vue +276 -0
- package/src/panel/VueViewPanel.vue +871 -0
- package/src/panel/components/ConfigHintIcon.vue +34 -0
- package/src/panel/components/ElementsLayer.vue +165 -0
- package/src/panel/components/MaterialPreview.vue +135 -0
- package/src/panel/components/MaterialSidebar.vue +526 -0
- package/src/panel/components/MaterialSidebarTreeNode.vue +305 -0
- package/src/panel/components/MoveableLayer.vue +859 -0
- package/src/panel/components/PanelCanvas.vue +630 -0
- package/src/panel/components/PanelConfigSidebar.vue +397 -0
- package/src/panel/components/PanelRulers.vue +177 -0
- package/src/panel/components/SelectLayer.vue +115 -0
- package/src/panel/components/ViewElementScopePanel.vue +76 -0
- package/src/panel/components/WorkspaceConfigSidebar.vue +147 -0
- package/src/panel/components/WorkspaceProjectNav.vue +192 -0
- package/src/panel/components/WorkspaceStageSplit.vue +258 -0
- package/src/panel/components/config/ConfigColorField.vue +52 -0
- package/src/panel/components/config/ConfigFieldGroup.vue +20 -0
- package/src/panel/components/config/ConfigSection.vue +50 -0
- package/src/panel/components/config/PanelConfigAudioSection.vue +256 -0
- package/src/panel/components/config/PanelConfigChartSection.vue +650 -0
- package/src/panel/components/config/PanelConfigGeometrySection.vue +209 -0
- package/src/panel/components/config/PanelConfigGridChildSpan.vue +68 -0
- package/src/panel/components/config/PanelConfigGridSection.vue +103 -0
- package/src/panel/components/config/PanelConfigImageSection.vue +136 -0
- package/src/panel/components/config/PanelConfigMultiSelect.vue +434 -0
- package/src/panel/components/config/PanelConfigNodeInfo.vue +165 -0
- package/src/panel/components/config/PanelConfigReferenceSection.vue +77 -0
- package/src/panel/components/config/PanelConfigStyleSections.vue +208 -0
- package/src/panel/components/config/PanelConfigTextSection.vue +195 -0
- package/src/panel/components/config/PanelConfigVideoSection.vue +107 -0
- package/src/panel/components/config/shared.ts +74 -0
- package/src/panel/components/elementsLayerNodes.ts +830 -0
- package/src/panel/components/materialSidebarData.ts +85 -0
- package/src/panel/components/scope-config/ScopeConfigProvider.vue +153 -0
- package/src/panel/components/scope-config/ScopeTemplateAutocompleteHost.vue +234 -0
- package/src/panel/components/scope-config/ScopeTemplatePreviewHost.vue +192 -0
- package/src/panel/components/scope-config/ScopeTemplatePreviewPanel.vue +42 -0
- package/src/panel/components/scope-config/ScopeTemplateUsageHint.vue +20 -0
- package/src/panel/components/scope-config/ScopeTemplateWarningsPanel.vue +63 -0
- package/src/panel/components/scope-config/scopeConfigContext.ts +17 -0
- package/src/panel/components/scope-config/useScopeConfig.ts +11 -0
- package/src/panel/constants/messages.ts +34 -0
- package/src/panel/constants/zIndex.ts +6 -0
- package/src/panel/hooks/usePanelElements.ts +1075 -0
- package/src/panel/hooks/useRafThrottledScroll.ts +25 -0
- package/src/panel/hooks/useWorkspaceProjects.ts +240 -0
- package/src/panel/lib/panel-ruler-canvas.ts +139 -0
- package/src/panel/library/workspace-project-cache.ts +23 -0
- package/src/panel/library/workspace-project-db.ts +111 -0
- package/src/panel/library/workspace-project-sync.ts +41 -0
- package/src/panel/library/workspace-snapshot.ts +30 -0
- package/src/panel/parseOnlinePreviewSearchParams.ts +13 -0
- package/src/panel/scope/view-scope-store.ts +82 -0
- package/src/panel/types.ts +127 -0
- package/src/panel/utils/chartOptionBuilder.ts +327 -0
- package/src/panel/utils/gridPlacement.ts +189 -0
- package/src/panel/utils/mappingLayerOps.ts +142 -0
- package/src/panel/utils/panelElementDefaults.ts +161 -0
- package/src/panel/utils/panelElementNodes.ts +35 -0
- package/src/panel/utils/panelStateIO.ts +124 -0
- package/src/panel/utils/scope-autocomplete.ts +114 -0
- package/src/panel/utils/scope-field-labels.ts +46 -0
- package/src/panel/utils/scope-template-chart.ts +92 -0
- package/src/panel/utils/scope-template-preview.ts +124 -0
- package/src/panel/utils/scope-template-spread.ts +229 -0
- package/src/panel/utils/scope-template-warnings.ts +243 -0
- package/src/panel/utils/scope-template.ts +97 -0
- package/src/panel/utils/updateElementDraft.ts +221 -0
- package/src/panel/viewportZoom.ts +26 -0
- package/src/tailwind.css +43 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
import { Input } from "ant-design-vue";
|
|
4
|
+
|
|
5
|
+
const props = defineProps<{
|
|
6
|
+
label: string;
|
|
7
|
+
value: string;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
}>();
|
|
10
|
+
|
|
11
|
+
const emit = defineEmits<{
|
|
12
|
+
"update:value": [value: string];
|
|
13
|
+
}>();
|
|
14
|
+
|
|
15
|
+
const pickerValue = computed(() =>
|
|
16
|
+
/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(props.value || "")
|
|
17
|
+
? props.value
|
|
18
|
+
: "#000000"
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
function onTextChange(v: string) {
|
|
22
|
+
emit("update:value", v);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function onColorInput(e: Event) {
|
|
26
|
+
const target = e.target as HTMLInputElement;
|
|
27
|
+
emit("update:value", target.value);
|
|
28
|
+
}
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<template>
|
|
32
|
+
<label class="block space-y-1.5">
|
|
33
|
+
<div class="text-[11px] text-gray-500">{{ label }}</div>
|
|
34
|
+
<div class="flex items-center gap-2">
|
|
35
|
+
<Input
|
|
36
|
+
size="small"
|
|
37
|
+
:value="value"
|
|
38
|
+
:disabled="disabled"
|
|
39
|
+
placeholder="#000000"
|
|
40
|
+
@update:value="onTextChange"
|
|
41
|
+
/>
|
|
42
|
+
<input
|
|
43
|
+
type="color"
|
|
44
|
+
class="h-7 w-10 shrink-0 cursor-pointer rounded border border-gray-200 p-0.5"
|
|
45
|
+
:value="pickerValue"
|
|
46
|
+
:disabled="disabled"
|
|
47
|
+
:aria-label="`${label}调色盘`"
|
|
48
|
+
@input="onColorInput"
|
|
49
|
+
/>
|
|
50
|
+
</div>
|
|
51
|
+
</label>
|
|
52
|
+
</template>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import ConfigHintIcon from "../ConfigHintIcon.vue";
|
|
3
|
+
|
|
4
|
+
defineProps<{
|
|
5
|
+
title: string;
|
|
6
|
+
hint?: boolean;
|
|
7
|
+
}>();
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<template>
|
|
11
|
+
<div class="space-y-2.5 rounded-lg border border-gray-200/80 bg-white/80 p-2.5">
|
|
12
|
+
<div class="flex items-center gap-1">
|
|
13
|
+
<div class="text-[11px] font-semibold text-gray-500">{{ title }}</div>
|
|
14
|
+
<ConfigHintIcon v-if="$slots.hint" :label="title">
|
|
15
|
+
<slot name="hint" />
|
|
16
|
+
</ConfigHintIcon>
|
|
17
|
+
</div>
|
|
18
|
+
<slot />
|
|
19
|
+
</div>
|
|
20
|
+
</template>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
import ConfigHintIcon from "../ConfigHintIcon.vue";
|
|
4
|
+
|
|
5
|
+
const props = withDefaults(
|
|
6
|
+
defineProps<{
|
|
7
|
+
title: string;
|
|
8
|
+
open: boolean;
|
|
9
|
+
forceOpen?: boolean;
|
|
10
|
+
}>(),
|
|
11
|
+
{ forceOpen: false }
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
const emit = defineEmits<{
|
|
15
|
+
"update:open": [value: boolean];
|
|
16
|
+
}>();
|
|
17
|
+
|
|
18
|
+
const isOpen = computed(() => props.forceOpen || props.open);
|
|
19
|
+
|
|
20
|
+
function toggle() {
|
|
21
|
+
if (props.forceOpen) return;
|
|
22
|
+
emit("update:open", !props.open);
|
|
23
|
+
}
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<template>
|
|
27
|
+
<div class="rounded-xl border border-gray-200/80 bg-white shadow-sm">
|
|
28
|
+
<div class="flex items-center gap-1.5 px-3 py-2">
|
|
29
|
+
<button
|
|
30
|
+
type="button"
|
|
31
|
+
class="flex h-6 w-6 items-center justify-center rounded-md text-xs hover:bg-gray-100"
|
|
32
|
+
@click="toggle"
|
|
33
|
+
>
|
|
34
|
+
{{ isOpen ? "▾" : "▸" }}
|
|
35
|
+
</button>
|
|
36
|
+
<div class="flex min-w-0 flex-1 items-center gap-1">
|
|
37
|
+
<div class="text-[11px] font-semibold tracking-wide text-gray-500">{{ title }}</div>
|
|
38
|
+
<ConfigHintIcon v-if="$slots.hint" :label="title">
|
|
39
|
+
<slot name="hint" />
|
|
40
|
+
</ConfigHintIcon>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
<div
|
|
44
|
+
v-show="isOpen"
|
|
45
|
+
class="space-y-3 border-t border-gray-200/60 bg-gray-50/50 px-3 pb-3 pt-2.5"
|
|
46
|
+
>
|
|
47
|
+
<slot />
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</template>
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { onUnmounted, ref } from "vue";
|
|
3
|
+
import { Checkbox, Input, Select } from "ant-design-vue";
|
|
4
|
+
import type { PanelElement } from "../../types";
|
|
5
|
+
import {
|
|
6
|
+
PANEL_MESSAGES,
|
|
7
|
+
readFileAsDataUrl,
|
|
8
|
+
uploadFileToRemote,
|
|
9
|
+
} from "./shared";
|
|
10
|
+
import ConfigFieldGroup from "./ConfigFieldGroup.vue";
|
|
11
|
+
import ConfigSection from "./ConfigSection.vue";
|
|
12
|
+
import ConfigHintIcon from "../ConfigHintIcon.vue";
|
|
13
|
+
|
|
14
|
+
const props = defineProps<{
|
|
15
|
+
element: PanelElement;
|
|
16
|
+
isEditable: boolean;
|
|
17
|
+
open: boolean;
|
|
18
|
+
forceOpen?: boolean;
|
|
19
|
+
updateElement: (
|
|
20
|
+
id: string,
|
|
21
|
+
patch: Partial<PanelElement>,
|
|
22
|
+
options?: { batchId?: string; meta?: Record<string, unknown> }
|
|
23
|
+
) => void;
|
|
24
|
+
}>();
|
|
25
|
+
|
|
26
|
+
const emit = defineEmits<{
|
|
27
|
+
"update:open": [value: boolean];
|
|
28
|
+
}>();
|
|
29
|
+
|
|
30
|
+
const audioStatus = ref("");
|
|
31
|
+
const isRecordingAudio = ref(false);
|
|
32
|
+
const recorderRef = ref<MediaRecorder | null>(null);
|
|
33
|
+
const recordStreamRef = ref<MediaStream | null>(null);
|
|
34
|
+
const audioChunksRef = ref<BlobPart[]>([]);
|
|
35
|
+
|
|
36
|
+
onUnmounted(() => {
|
|
37
|
+
recordStreamRef.value?.getTracks().forEach((track) => track.stop());
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
function patch(patch: Partial<PanelElement>) {
|
|
41
|
+
if (props.element.materialType !== "audio") return;
|
|
42
|
+
props.updateElement(props.element.id, patch);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function handleUploadAudioFile(file: File) {
|
|
46
|
+
const base64 = await readFileAsDataUrl(file, PANEL_MESSAGES.readAudioFailed);
|
|
47
|
+
patch({ audioSrc: base64 });
|
|
48
|
+
audioStatus.value = PANEL_MESSAGES.audioLocalSaved;
|
|
49
|
+
const url = await uploadFileToRemote(file);
|
|
50
|
+
if (url) {
|
|
51
|
+
patch({ audioRemoteUrl: url });
|
|
52
|
+
audioStatus.value = PANEL_MESSAGES.audioRemoteUploaded;
|
|
53
|
+
} else {
|
|
54
|
+
audioStatus.value = PANEL_MESSAGES.audioServerUploadFailed;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function handleUploadAudioPoster(file: File) {
|
|
59
|
+
const base64 = await readFileAsDataUrl(file, PANEL_MESSAGES.readImageFailed);
|
|
60
|
+
patch({ audioPosterImage: base64 });
|
|
61
|
+
audioStatus.value = PANEL_MESSAGES.audioPosterSet;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function stopRecordingAudio() {
|
|
65
|
+
recorderRef.value?.stop();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function startRecordingAudio() {
|
|
69
|
+
if (props.element.materialType !== "audio") return;
|
|
70
|
+
if (!navigator.mediaDevices?.getUserMedia) {
|
|
71
|
+
audioStatus.value = PANEL_MESSAGES.audioRecordUnsupported;
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
76
|
+
recordStreamRef.value = stream;
|
|
77
|
+
const recorder = new MediaRecorder(stream);
|
|
78
|
+
recorderRef.value = recorder;
|
|
79
|
+
audioChunksRef.value = [];
|
|
80
|
+
recorder.ondataavailable = (event) => {
|
|
81
|
+
if (event.data.size > 0) audioChunksRef.value.push(event.data);
|
|
82
|
+
};
|
|
83
|
+
recorder.onstop = async () => {
|
|
84
|
+
const blob = new Blob(audioChunksRef.value, {
|
|
85
|
+
type: recorder.mimeType || "audio/webm",
|
|
86
|
+
});
|
|
87
|
+
const dataUrl = await readFileAsDataUrl(
|
|
88
|
+
new File([blob], "recording.webm", { type: blob.type }),
|
|
89
|
+
PANEL_MESSAGES.readRecordAudioFailed
|
|
90
|
+
);
|
|
91
|
+
patch({ audioSrc: dataUrl });
|
|
92
|
+
audioStatus.value = PANEL_MESSAGES.audioRecordSaved;
|
|
93
|
+
recordStreamRef.value?.getTracks().forEach((track) => track.stop());
|
|
94
|
+
recordStreamRef.value = null;
|
|
95
|
+
recorderRef.value = null;
|
|
96
|
+
isRecordingAudio.value = false;
|
|
97
|
+
};
|
|
98
|
+
recorder.start();
|
|
99
|
+
isRecordingAudio.value = true;
|
|
100
|
+
audioStatus.value = PANEL_MESSAGES.audioRecording;
|
|
101
|
+
} catch {
|
|
102
|
+
audioStatus.value = PANEL_MESSAGES.audioRecordStartFailed;
|
|
103
|
+
isRecordingAudio.value = false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function onAudioFileChange(e: Event) {
|
|
108
|
+
const file = (e.target as HTMLInputElement).files?.[0];
|
|
109
|
+
(e.target as HTMLInputElement).value = "";
|
|
110
|
+
if (file) void handleUploadAudioFile(file);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function onPosterFileChange(e: Event) {
|
|
114
|
+
const file = (e.target as HTMLInputElement).files?.[0];
|
|
115
|
+
(e.target as HTMLInputElement).value = "";
|
|
116
|
+
if (file) void handleUploadAudioPoster(file);
|
|
117
|
+
}
|
|
118
|
+
</script>
|
|
119
|
+
|
|
120
|
+
<template>
|
|
121
|
+
<ConfigSection
|
|
122
|
+
title="音频配置"
|
|
123
|
+
:open="open"
|
|
124
|
+
:force-open="forceOpen"
|
|
125
|
+
@update:open="emit('update:open', $event)"
|
|
126
|
+
>
|
|
127
|
+
<ConfigFieldGroup title="音频来源">
|
|
128
|
+
<label class="block space-y-1">
|
|
129
|
+
<div>音频 URL</div>
|
|
130
|
+
<Input
|
|
131
|
+
size="small"
|
|
132
|
+
:value="element.audioRemoteUrl ?? ''"
|
|
133
|
+
:disabled="!isEditable"
|
|
134
|
+
placeholder="https://example.com/audio.mp3"
|
|
135
|
+
@update:value="(v: string) => patch({
|
|
136
|
+
audioRemoteUrl: v || undefined,
|
|
137
|
+
audioSrc: v || element.audioSrc,
|
|
138
|
+
})"
|
|
139
|
+
/>
|
|
140
|
+
</label>
|
|
141
|
+
<div class="flex items-center gap-2">
|
|
142
|
+
<label
|
|
143
|
+
class="inline-flex cursor-pointer items-center rounded border border-gray-200 px-2 py-1 text-[11px] hover:bg-gray-50"
|
|
144
|
+
:class="{ 'pointer-events-none opacity-50': !isEditable }"
|
|
145
|
+
>
|
|
146
|
+
上传音频
|
|
147
|
+
<input type="file" accept="audio/*" class="hidden" :disabled="!isEditable" @change="onAudioFileChange" />
|
|
148
|
+
</label>
|
|
149
|
+
<button
|
|
150
|
+
type="button"
|
|
151
|
+
class="rounded border border-gray-200 px-2 py-1 text-[11px] hover:bg-gray-50 disabled:opacity-50"
|
|
152
|
+
:disabled="!isEditable"
|
|
153
|
+
@click="isRecordingAudio ? stopRecordingAudio() : startRecordingAudio()"
|
|
154
|
+
>
|
|
155
|
+
{{ isRecordingAudio ? "停止录音" : "开始录音" }}
|
|
156
|
+
</button>
|
|
157
|
+
</div>
|
|
158
|
+
<div
|
|
159
|
+
v-if="audioStatus"
|
|
160
|
+
class="rounded border border-gray-200/60 bg-white px-2 py-1.5 text-[11px] text-gray-500"
|
|
161
|
+
>
|
|
162
|
+
{{ audioStatus }}
|
|
163
|
+
</div>
|
|
164
|
+
<audio
|
|
165
|
+
controls
|
|
166
|
+
class="h-8 w-full"
|
|
167
|
+
:src="element.audioSrc || element.audioRemoteUrl || ''"
|
|
168
|
+
/>
|
|
169
|
+
</ConfigFieldGroup>
|
|
170
|
+
<ConfigFieldGroup title="展示样式">
|
|
171
|
+
<label class="flex items-center gap-2">
|
|
172
|
+
<Checkbox
|
|
173
|
+
:checked="element.mediaAutoPauseOnEdit !== false"
|
|
174
|
+
:disabled="!isEditable"
|
|
175
|
+
@update:checked="(v) => patch({ mediaAutoPauseOnEdit: v !== false })"
|
|
176
|
+
/>
|
|
177
|
+
<span>编辑时自动暂停媒体</span>
|
|
178
|
+
</label>
|
|
179
|
+
<label class="block space-y-1">
|
|
180
|
+
<div>预设喇叭图标</div>
|
|
181
|
+
<Select
|
|
182
|
+
size="small"
|
|
183
|
+
class="w-full"
|
|
184
|
+
:value="element.audioIconPreset ?? '__none__'"
|
|
185
|
+
:disabled="!isEditable"
|
|
186
|
+
@update:value="(v) => patch({
|
|
187
|
+
audioIconPreset: v === '__none__' ? undefined : v as PanelElement['audioIconPreset'],
|
|
188
|
+
})"
|
|
189
|
+
>
|
|
190
|
+
<Select.Option value="__none__">默认(显示进度条)</Select.Option>
|
|
191
|
+
<Select.Option value="speaker">喇叭</Select.Option>
|
|
192
|
+
<Select.Option value="music">音符</Select.Option>
|
|
193
|
+
<Select.Option value="headphone">耳机</Select.Option>
|
|
194
|
+
<Select.Option value="wave">声波</Select.Option>
|
|
195
|
+
</Select>
|
|
196
|
+
</label>
|
|
197
|
+
<label class="block space-y-1">
|
|
198
|
+
<div>播放动效</div>
|
|
199
|
+
<Select
|
|
200
|
+
size="small"
|
|
201
|
+
class="w-full"
|
|
202
|
+
:value="element.audioVisualEffect ?? 'pulse'"
|
|
203
|
+
:disabled="!isEditable"
|
|
204
|
+
@update:value="(v) => patch({ audioVisualEffect: v as PanelElement['audioVisualEffect'] })"
|
|
205
|
+
>
|
|
206
|
+
<Select.Option value="none">无动效</Select.Option>
|
|
207
|
+
<Select.Option value="pulse">呼吸高亮</Select.Option>
|
|
208
|
+
<Select.Option value="ripple">波纹扩散</Select.Option>
|
|
209
|
+
</Select>
|
|
210
|
+
</label>
|
|
211
|
+
<label class="block space-y-1">
|
|
212
|
+
<div>动效速度</div>
|
|
213
|
+
<Select
|
|
214
|
+
size="small"
|
|
215
|
+
class="w-full"
|
|
216
|
+
:value="element.audioVisualSpeed ?? 'normal'"
|
|
217
|
+
:disabled="!isEditable"
|
|
218
|
+
@update:value="(v) => patch({ audioVisualSpeed: v as PanelElement['audioVisualSpeed'] })"
|
|
219
|
+
>
|
|
220
|
+
<Select.Option value="slow">慢</Select.Option>
|
|
221
|
+
<Select.Option value="normal">中</Select.Option>
|
|
222
|
+
<Select.Option value="fast">快</Select.Option>
|
|
223
|
+
</Select>
|
|
224
|
+
</label>
|
|
225
|
+
<div class="flex items-center gap-2">
|
|
226
|
+
<label
|
|
227
|
+
class="inline-flex cursor-pointer items-center rounded border border-gray-200 px-2 py-1 text-[11px] hover:bg-gray-50"
|
|
228
|
+
:class="{ 'pointer-events-none opacity-50': !isEditable }"
|
|
229
|
+
>
|
|
230
|
+
上传占位图
|
|
231
|
+
<input type="file" accept="image/*" class="hidden" :disabled="!isEditable" @change="onPosterFileChange" />
|
|
232
|
+
</label>
|
|
233
|
+
<button
|
|
234
|
+
type="button"
|
|
235
|
+
class="rounded border border-gray-200 px-2 py-1 text-[11px] hover:bg-gray-50 disabled:opacity-50"
|
|
236
|
+
:disabled="!isEditable"
|
|
237
|
+
@click="patch({ audioPosterImage: undefined })"
|
|
238
|
+
>
|
|
239
|
+
清空占位图
|
|
240
|
+
</button>
|
|
241
|
+
</div>
|
|
242
|
+
<img
|
|
243
|
+
v-if="element.audioPosterImage"
|
|
244
|
+
:src="element.audioPosterImage"
|
|
245
|
+
alt="音频占位图预览"
|
|
246
|
+
class="h-20 w-full rounded border border-gray-200/60 object-cover"
|
|
247
|
+
/>
|
|
248
|
+
<div class="flex items-center gap-1">
|
|
249
|
+
<div class="text-[11px] text-gray-500">音频占位图</div>
|
|
250
|
+
<ConfigHintIcon label="音频占位图">
|
|
251
|
+
设置占位图或图标后,节点上将隐藏进度条,改为点击图标播放/暂停。
|
|
252
|
+
</ConfigHintIcon>
|
|
253
|
+
</div>
|
|
254
|
+
</ConfigFieldGroup>
|
|
255
|
+
</ConfigSection>
|
|
256
|
+
</template>
|