Files
yaoayanui/src/components/subtitle/SubtitleTemplateDialog.vue
fengchuanhn@gmail.com 8f05a8e414 1
2026-05-25 02:47:32 +08:00

240 lines
7.2 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>