-
+
+
@@ -226,8 +292,22 @@ function onEdit(template) {
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/subtitle/SubtitleTemplateEditor.vue b/src/components/subtitle/SubtitleTemplateEditor.vue
new file mode 100644
index 0000000..9f52ce1
--- /dev/null
+++ b/src/components/subtitle/SubtitleTemplateEditor.vue
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+ 正在编辑: {{ draft.name }}
+
+
+
+
修改完成后请点击底部的「保存模板」按钮
+
+
+
+
+
diff --git a/src/components/subtitle/TitleLineStyleEditor.vue b/src/components/subtitle/TitleLineStyleEditor.vue
new file mode 100644
index 0000000..e3381ae
--- /dev/null
+++ b/src/components/subtitle/TitleLineStyleEditor.vue
@@ -0,0 +1,133 @@
+
+
+
+
+
+
字体设置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
颜色设置
+
+
+ {{ line.fontColor }}
+
+
+
+
+
描边设置
+
+
+
+
+ {{ line.outlineWidth }}
+
+
+
+
+
+
+
+
+
+
+
+
阴影设置(可选)
+
+
+
+
+
+
+
+
+
+
效果预览
+
+ {{ line.text || "标题文字" }}
+
+
+
+
diff --git a/src/components/subtitle/TitleSubtitleSettings.vue b/src/components/subtitle/TitleSubtitleSettings.vue
new file mode 100644
index 0000000..210caba
--- /dev/null
+++ b/src/components/subtitle/TitleSubtitleSettings.vue
@@ -0,0 +1,133 @@
+
+
+
+
+
+
+ 标题字幕
+
+
+
+
+
+ 当前标题(自动读取步骤 04「标题标签关键词」生成结果):
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
分行预览(共 {{ linePreviews.length }} 行)
+
+
+ 第{{ i + 1 }}行:{{ line.text }}
+
+
+
+
+
+
每行样式配置
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/config/subtitleStylePresets.js b/src/config/subtitleStylePresets.js
new file mode 100644
index 0000000..0c73267
--- /dev/null
+++ b/src/config/subtitleStylePresets.js
@@ -0,0 +1,162 @@
+/** 字幕样式字体与快速预设(对齐 Electron SubtitleStyleSelector) */
+
+export const SUBTITLE_FONT_OPTIONS = [
+ { label: "胡晓波男神体", value: "胡晓波男神体" },
+ { label: "优设标题黑", value: "优设标题黑" },
+ { label: "微软雅黑", value: "Microsoft YaHei" },
+ { label: "微软雅黑 UI", value: "Microsoft YaHei UI" },
+ { label: "月星楷", value: "月星楷" },
+ { label: "USMCCyuanjiantecu", value: "USMCCyuanjiantecu" },
+ { label: "庞门正道标题体", value: "庞门正道标题体免费版" },
+ { label: "霞鹜文楷", value: "霞鹜文楷-Light" },
+ { label: "文鼎PL简中楷", value: "文鼎PL简中楷" },
+ { label: "杨任东竹石体", value: "杨任东竹石体" },
+ { label: "千图厚黑体", value: "千图厚黑体" },
+ { label: "Arial", value: "Arial" },
+];
+
+export const SUBTITLE_STYLE_PRESETS = [
+ {
+ label: "黑底白字",
+ style: {
+ fontColor: "#FFFFFF",
+ backgroundColor: "#000000",
+ backgroundOpacity: 0.8,
+ outlineWidth: 0,
+ },
+ },
+ {
+ label: "白底黑字",
+ style: {
+ fontColor: "#000000",
+ backgroundColor: "#FFFFFF",
+ backgroundOpacity: 0.9,
+ outlineWidth: 0,
+ },
+ },
+ {
+ label: "抖音风格",
+ style: {
+ fontColor: "#FFFFFF",
+ backgroundColor: "transparent",
+ backgroundOpacity: 0,
+ outlineColor: "#000000",
+ outlineWidth: 3,
+ shadowOffset: 2,
+ shadowColor: "#000000",
+ },
+ },
+ {
+ label: "红底白字",
+ style: {
+ fontColor: "#FFFFFF",
+ backgroundColor: "#FF0000",
+ backgroundOpacity: 0.85,
+ outlineWidth: 0,
+ },
+ },
+ {
+ label: "黄底黑字",
+ style: {
+ fontColor: "#000000",
+ backgroundColor: "#FFD700",
+ backgroundOpacity: 0.9,
+ outlineWidth: 0,
+ },
+ },
+ {
+ label: "蓝底白字",
+ style: {
+ fontColor: "#FFFFFF",
+ backgroundColor: "#2563EB",
+ backgroundOpacity: 0.85,
+ outlineWidth: 0,
+ },
+ },
+ {
+ label: "绿色高亮",
+ style: {
+ fontColor: "#00FF88",
+ backgroundColor: "transparent",
+ backgroundOpacity: 0,
+ outlineColor: "#000000",
+ outlineWidth: 2,
+ },
+ },
+ {
+ label: "紫色高亮",
+ style: {
+ fontColor: "#E879F9",
+ backgroundColor: "transparent",
+ backgroundOpacity: 0,
+ outlineColor: "#000000",
+ outlineWidth: 2,
+ },
+ },
+ {
+ label: "立体阴影",
+ style: {
+ fontColor: "#FFFFFF",
+ backgroundColor: "transparent",
+ backgroundOpacity: 0,
+ outlineWidth: 0,
+ shadowOffset: 3,
+ shadowBlur: 4,
+ shadowColor: "#000000",
+ },
+ },
+ {
+ label: "霓虹发光",
+ style: {
+ fontColor: "#00FFFF",
+ backgroundColor: "transparent",
+ backgroundOpacity: 0,
+ outlineColor: "#FF00FF",
+ outlineWidth: 2,
+ shadowOffset: 0,
+ shadowBlur: 8,
+ shadowColor: "#00FFFF",
+ },
+ },
+];
+
+/**
+ * @param {Record
} style
+ */
+export function buildSubtitlePreviewBoxStyle(style) {
+ const fontSize = Number(style?.fontSize) || 35;
+ const outline = Number(style?.outlineWidth) || 0;
+ const offset = Number(style?.shadowOffset) || 0;
+ const shadows = [];
+ if (outline > 0) {
+ const c = style?.outlineColor || "#000000";
+ const w = Math.max(1, outline);
+ shadows.push(
+ `${w}px 0 0 ${c}`,
+ `-${w}px 0 0 ${c}`,
+ `0 ${w}px 0 ${c}`,
+ `0 -${w}px 0 ${c}`,
+ );
+ }
+ if (offset > 0) {
+ shadows.push(
+ `${offset}px ${offset}px ${Number(style?.shadowBlur) || 4}px ${style?.shadowColor || "#000000"}`,
+ );
+ }
+ const bg =
+ style?.backgroundColor &&
+ style.backgroundColor !== "transparent" &&
+ (Number(style?.backgroundOpacity) ?? 0) > 0
+ ? style.backgroundColor
+ : "transparent";
+ return {
+ fontFamily: style?.fontName ? `'${style.fontName}'` : "Microsoft YaHei UI",
+ fontSize: `${Math.min(fontSize, 42)}px`,
+ color: style?.fontColor || "#FFFFFF",
+ backgroundColor: bg,
+ padding: bg !== "transparent" ? "4px 12px" : "0",
+ borderRadius: bg !== "transparent" ? "4px" : "0",
+ textShadow: shadows.length ? shadows.join(", ") : "0 1px 2px rgba(0,0,0,0.5)",
+ fontWeight: "bold",
+ };
+}
diff --git a/src/services/subtitleTemplateCatalog.js b/src/services/subtitleTemplateCatalog.js
index 18f65e4..b340404 100644
--- a/src/services/subtitleTemplateCatalog.js
+++ b/src/services/subtitleTemplateCatalog.js
@@ -50,6 +50,26 @@ export function saveCustomSubtitleTemplates(templates) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(templates));
}
+/**
+ * 保存或更新自定义模板
+ * @param {SubtitleTemplateItem} template
+ * @returns {SubtitleTemplateItem}
+ */
+export function upsertCustomSubtitleTemplate(template) {
+ const custom = loadCustomSubtitleTemplates();
+ const item = {
+ ...template,
+ isSystem: false,
+ is_system: 0,
+ createdAt: template.createdAt || Date.now(),
+ };
+ const idx = custom.findIndex((t) => t.id === item.id);
+ if (idx >= 0) custom[idx] = item;
+ else custom.unshift(item);
+ saveCustomSubtitleTemplates(custom);
+ return item;
+}
+
/**
* @returns {Promise}
*/
diff --git a/src/utils/subtitleTemplateDraft.js b/src/utils/subtitleTemplateDraft.js
new file mode 100644
index 0000000..f2253c7
--- /dev/null
+++ b/src/utils/subtitleTemplateDraft.js
@@ -0,0 +1,101 @@
+/**
+ * @template T
+ * @param {T} value
+ * @returns {T}
+ */
+export function deepClone(value) {
+ return JSON.parse(JSON.stringify(value));
+}
+
+/**
+ * @param {import('../services/subtitleTemplateCatalog.js').SubtitleTemplateItem} template
+ */
+export function createEditDraft(template) {
+ const draft = deepClone(template);
+ if (!draft.config) draft.config = {};
+ if (!draft.config.subtitleStyle) {
+ draft.config.subtitleStyle = {
+ fontName: "Microsoft YaHei",
+ fontSize: 35,
+ fontColor: "#FFFFFF",
+ outlineColor: "#000000",
+ outlineWidth: 0,
+ backgroundColor: "transparent",
+ backgroundOpacity: 0,
+ position: "bottom",
+ shadowOffset: 2,
+ shadowBlur: 4,
+ shadowColor: "#000000",
+ };
+ }
+ if (draft.config.subtitleFontSizeScale == null) {
+ draft.config.subtitleFontSizeScale = 100;
+ }
+ if (!draft.config.subtitlePosition) {
+ draft.config.subtitlePosition = draft.config.subtitleStyle.position || "bottom";
+ }
+ if (!draft.config.titleSubtitleConfig) {
+ draft.config.titleSubtitleConfig = {
+ enabled: false,
+ lines: [],
+ maxCharsPerLine: 8,
+ lineSpacingPercent: 5,
+ topMarginPercent: 5,
+ };
+ }
+ if (!Array.isArray(draft.config.keywordGroupsStyles)) {
+ draft.config.keywordGroupsStyles = [];
+ }
+ if (draft.config.enableKeywordEffects == null) {
+ draft.config.enableKeywordEffects = true;
+ }
+ if (!draft.config.keywordRenderMode) {
+ draft.config.keywordRenderMode = "inline-emphasis";
+ }
+ return draft;
+}
+
+/**
+ * @param {string} title
+ * @param {number} maxChars
+ */
+export function splitTitleLines(title, maxChars = 8) {
+ const text = String(title || "").trim();
+ if (!text) return [];
+ const lines = [];
+ for (let i = 0; i < text.length; i += maxChars) {
+ lines.push(text.slice(i, i + maxChars));
+ }
+ return lines;
+}
+
+/**
+ * @param {import('../services/subtitleTemplateCatalog.js').SubtitleTemplateItem} draft
+ * @param {string} [referenceTitle]
+ */
+export function syncTitleLinesFromReference(draft, referenceTitle) {
+ const cfg = draft.config.titleSubtitleConfig;
+ const max = cfg.maxCharsPerLine || 8;
+ const parts = splitTitleLines(referenceTitle || cfg.sourceTitle || "", max);
+ const previewTitles = draft.config.previewConfig?.titleTexts;
+ const texts =
+ previewTitles?.length > 0 ? previewTitles : parts.length ? parts : ["示例标题第一行", "示例标题第二行"];
+ const baseLine = cfg.lines?.[0] || {
+ fontName: "优设标题黑",
+ fontSize: 80,
+ fontColor: "#ffff00",
+ backgroundColor: "transparent",
+ backgroundOpacity: 0,
+ outlineColor: "#000000",
+ outlineWidth: 1,
+ shadowColor: "#000000",
+ shadowOffset: 2,
+ };
+ cfg.lines = texts.map((text, i) => ({
+ ...deepClone(cfg.lines?.[i] || baseLine),
+ text,
+ }));
+ cfg.sourceTitle = referenceTitle || cfg.sourceTitle || texts.join("");
+ if (!draft.config.previewConfig) draft.config.previewConfig = {};
+ draft.config.previewConfig.titleTexts = texts;
+}