1
This commit is contained in:
239
src/components/subtitle/SubtitleTemplateDialog.vue
Normal file
239
src/components/subtitle/SubtitleTemplateDialog.vue
Normal file
@@ -0,0 +1,239 @@
|
||||
<script setup>
|
||||
import { computed, ref, watch } from "vue";
|
||||
import SubtitlePreviewCard from "./SubtitlePreviewCard.vue";
|
||||
import {
|
||||
cloneSubtitleTemplate,
|
||||
downloadSubtitleTemplatesJson,
|
||||
isSystemSubtitleTemplateId,
|
||||
loadAllSubtitleTemplates,
|
||||
loadCustomSubtitleTemplates,
|
||||
parseImportedSubtitleTemplates,
|
||||
saveCustomSubtitleTemplates,
|
||||
} from "../../services/subtitleTemplateCatalog.js";
|
||||
|
||||
const visible = defineModel("visible", { type: Boolean, default: false });
|
||||
|
||||
const props = defineProps({
|
||||
selectedId: { type: String, default: "" },
|
||||
});
|
||||
|
||||
const emit = defineEmits(["select", "update:selectedId"]);
|
||||
|
||||
const templates = ref([]);
|
||||
const loading = ref(false);
|
||||
const feedback = ref({ severity: "", message: "" });
|
||||
const fileInputRef = ref(null);
|
||||
|
||||
const selectedTemplateId = computed({
|
||||
get: () => props.selectedId,
|
||||
set: (id) => emit("update:selectedId", id),
|
||||
});
|
||||
|
||||
async function refreshTemplates() {
|
||||
loading.value = true;
|
||||
feedback.value = { severity: "", message: "" };
|
||||
try {
|
||||
templates.value = await loadAllSubtitleTemplates();
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
feedback.value = { severity: "error", message: `加载模板失败:${msg}` };
|
||||
templates.value = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch(visible, (open) => {
|
||||
if (open) refreshTemplates();
|
||||
});
|
||||
|
||||
function showMsg(severity, message) {
|
||||
feedback.value = { severity, message };
|
||||
}
|
||||
|
||||
function selectTemplate(template) {
|
||||
selectedTemplateId.value = template.id;
|
||||
emit("select", template);
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
function onExportAll() {
|
||||
downloadSubtitleTemplatesJson(templates.value, "subtitle-templates-all.json");
|
||||
showMsg("success", "已导出全部模板");
|
||||
}
|
||||
|
||||
function onExportOne(template) {
|
||||
downloadSubtitleTemplatesJson([template], `${template.name || template.id}.json`);
|
||||
showMsg("success", `已导出「${template.name}」`);
|
||||
}
|
||||
|
||||
function onImportClick() {
|
||||
fileInputRef.value?.click();
|
||||
}
|
||||
|
||||
async function onImportFile(event) {
|
||||
const file = event.target.files?.[0];
|
||||
event.target.value = "";
|
||||
if (!file) return;
|
||||
try {
|
||||
const imported = await parseImportedSubtitleTemplates(file);
|
||||
const custom = loadCustomSubtitleTemplates();
|
||||
const merged = [...custom];
|
||||
for (const item of imported) {
|
||||
const idx = merged.findIndex((t) => t.id === item.id);
|
||||
if (idx >= 0) merged[idx] = item;
|
||||
else merged.push(item);
|
||||
}
|
||||
saveCustomSubtitleTemplates(merged);
|
||||
await refreshTemplates();
|
||||
showMsg("success", `已导入 ${imported.length} 个模板`);
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
showMsg("error", `导入失败:${msg}`);
|
||||
}
|
||||
}
|
||||
|
||||
function onNewTemplate() {
|
||||
const base = templates.value.find((t) => t.id === "template_system_11") || templates.value[0];
|
||||
if (!base) {
|
||||
showMsg("error", "暂无可用模板作为新建基础");
|
||||
return;
|
||||
}
|
||||
const created = cloneSubtitleTemplate({
|
||||
...base,
|
||||
name: "新建模板",
|
||||
});
|
||||
const custom = loadCustomSubtitleTemplates();
|
||||
custom.unshift(created);
|
||||
saveCustomSubtitleTemplates(custom);
|
||||
refreshTemplates();
|
||||
selectedTemplateId.value = created.id;
|
||||
emit("select", created);
|
||||
showMsg("success", "已创建新模板,可在列表中继续编辑样式");
|
||||
}
|
||||
|
||||
function onClone(template) {
|
||||
const created = cloneSubtitleTemplate(template);
|
||||
const custom = loadCustomSubtitleTemplates();
|
||||
custom.unshift(created);
|
||||
saveCustomSubtitleTemplates(custom);
|
||||
refreshTemplates();
|
||||
showMsg("success", `已复制为「${created.name}」`);
|
||||
}
|
||||
|
||||
function onDelete(template) {
|
||||
if (isSystemSubtitleTemplateId(template.id)) {
|
||||
showMsg("error", "系统模板不可删除");
|
||||
return;
|
||||
}
|
||||
if (!window.confirm(`确定删除模板「${template.name}」?`)) return;
|
||||
const custom = loadCustomSubtitleTemplates().filter((t) => t.id !== template.id);
|
||||
saveCustomSubtitleTemplates(custom);
|
||||
if (selectedTemplateId.value === template.id) {
|
||||
selectedTemplateId.value = "template_system_11";
|
||||
}
|
||||
refreshTemplates();
|
||||
showMsg("success", "模板已删除");
|
||||
}
|
||||
|
||||
function onEdit(template) {
|
||||
selectTemplate(template);
|
||||
showMsg("success", "已选用该模板;完整样式编辑器后续版本提供");
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
v-model:visible="visible"
|
||||
header="字幕设置"
|
||||
modal
|
||||
:style="{ width: 'min(1120px, 96vw)' }"
|
||||
:content-style="{ padding: 0 }"
|
||||
>
|
||||
<div class="flex max-h-[70vh] flex-col">
|
||||
<div class="flex-1 space-y-4 overflow-y-auto p-4">
|
||||
<div
|
||||
class="rounded-lg border border-blue-500/30 bg-gradient-to-r from-blue-900/20 to-indigo-900/20 p-4"
|
||||
>
|
||||
<div class="mb-4 flex items-center justify-between gap-2">
|
||||
<h5 class="font-medium text-slate-100">字幕模板</h5>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Button
|
||||
label="导出全部"
|
||||
size="small"
|
||||
icon="pi pi-download"
|
||||
:disabled="!templates.length"
|
||||
@click="onExportAll"
|
||||
/>
|
||||
<Button
|
||||
label="导入模板"
|
||||
size="small"
|
||||
outlined
|
||||
icon="pi pi-upload"
|
||||
@click="onImportClick"
|
||||
/>
|
||||
<Button
|
||||
label="新建模板"
|
||||
size="small"
|
||||
icon="pi pi-plus"
|
||||
@click="onNewTemplate"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p
|
||||
v-if="feedback.message"
|
||||
class="mb-3 text-sm"
|
||||
:class="feedback.severity === 'error' ? 'text-red-400' : 'text-emerald-400'"
|
||||
>
|
||||
{{ feedback.message }}
|
||||
</p>
|
||||
|
||||
<div v-if="loading" class="py-12 text-center text-sm text-slate-400">
|
||||
正在加载字幕模板…
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="templates.length"
|
||||
class="grid grid-cols-2 gap-4 lg:grid-cols-4"
|
||||
>
|
||||
<SubtitlePreviewCard
|
||||
v-for="tpl in templates"
|
||||
:key="tpl.id"
|
||||
:template-name="tpl.name"
|
||||
:subtitle-style="tpl.config?.subtitleStyle"
|
||||
:title-subtitle-config="tpl.config?.titleSubtitleConfig"
|
||||
:keyword-groups-styles="tpl.config?.keywordGroupsStyles"
|
||||
:keyword-render-mode="tpl.config?.keywordRenderMode"
|
||||
:preview-config="tpl.config?.previewConfig"
|
||||
:is-selected="selectedTemplateId === tpl.id"
|
||||
:preview-scale="0.33"
|
||||
:is-system="tpl.is_system === 1 || tpl.isSystem || isSystemSubtitleTemplateId(tpl.id)"
|
||||
@select="selectTemplate(tpl)"
|
||||
@edit="onEdit(tpl)"
|
||||
@clone="onClone(tpl)"
|
||||
@export="onExportOne(tpl)"
|
||||
@delete="onDelete(tpl)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="rounded-lg border border-dashed border-slate-600 py-10 text-center text-sm text-slate-400"
|
||||
>
|
||||
<div class="mb-2 text-lg">📝</div>
|
||||
<div>暂无字幕模板,点击「新建模板」创建</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input
|
||||
ref="fileInputRef"
|
||||
type="file"
|
||||
accept="application/json,.json"
|
||||
class="hidden"
|
||||
@change="onImportFile"
|
||||
/>
|
||||
</Dialog>
|
||||
</template>
|
||||
Reference in New Issue
Block a user