11
This commit is contained in:
@@ -1,8 +1,24 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from "vue";
|
import { computed, ref } from "vue";
|
||||||
|
import {
|
||||||
|
SUBTITLE_FONT_OPTIONS,
|
||||||
|
buildSubtitlePreviewBoxStyle,
|
||||||
|
} from "../../config/subtitleStylePresets.js";
|
||||||
|
|
||||||
const config = defineModel("config", { type: Object, required: true });
|
const config = defineModel("config", { type: Object, required: true });
|
||||||
|
|
||||||
|
const importInputRef = ref(null);
|
||||||
|
const newGroupName = ref("");
|
||||||
|
const editingGroupIndex = ref(-1);
|
||||||
|
const keywordInputs = ref({});
|
||||||
|
|
||||||
|
const renderModeOptions = [
|
||||||
|
{ label: "内联字幕", value: "inline-emphasis" },
|
||||||
|
{ label: "浮动特效", value: "floating" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const builtinGroupNames = new Set(["行动词", "情感词", "描述词", "重点词/成语词"]);
|
||||||
|
|
||||||
const enableKeywordEffects = computed({
|
const enableKeywordEffects = computed({
|
||||||
get: () => config.value.enableKeywordEffects !== false,
|
get: () => config.value.enableKeywordEffects !== false,
|
||||||
set: (v) => {
|
set: (v) => {
|
||||||
@@ -17,7 +33,128 @@ const keywordRenderMode = computed({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const groups = computed(() => config.value.keywordGroupsStyles || []);
|
const groups = computed({
|
||||||
|
get: () => config.value.keywordGroupsStyles || [],
|
||||||
|
set: (value) => {
|
||||||
|
config.value.keywordGroupsStyles = value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function ensureGroups() {
|
||||||
|
if (!Array.isArray(config.value.keywordGroupsStyles)) {
|
||||||
|
config.value.keywordGroupsStyles = [];
|
||||||
|
}
|
||||||
|
return config.value.keywordGroupsStyles;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureStyleOverride(group) {
|
||||||
|
if (!group.styleOverride) {
|
||||||
|
group.styleOverride = {
|
||||||
|
fontName: config.value.subtitleStyle?.fontName || "胡晓波男神体",
|
||||||
|
fontSize: Math.max(Number(config.value.subtitleStyle?.fontSize) || 24, 24),
|
||||||
|
fontColor: group.color || "#FFFF00",
|
||||||
|
outlineColor: "#000000",
|
||||||
|
outlineWidth: 2,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!group.color) group.color = group.styleOverride.fontColor || "#1890ff";
|
||||||
|
return group.styleOverride;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createGroup() {
|
||||||
|
const name = newGroupName.value.trim();
|
||||||
|
if (!name) return;
|
||||||
|
const list = ensureGroups();
|
||||||
|
list.push({
|
||||||
|
groupName: name,
|
||||||
|
description: `模板分组${name}`,
|
||||||
|
keywords: [],
|
||||||
|
styleOverride: {
|
||||||
|
fontName: config.value.subtitleStyle?.fontName || "胡晓波男神体",
|
||||||
|
fontSize: 24,
|
||||||
|
fontColor: "#FFFF00",
|
||||||
|
outlineColor: "#000000",
|
||||||
|
outlineWidth: 2,
|
||||||
|
},
|
||||||
|
effectId: null,
|
||||||
|
effectDuration: null,
|
||||||
|
color: "#1890ff",
|
||||||
|
soundEffectId: null,
|
||||||
|
soundEffectConfig: null,
|
||||||
|
});
|
||||||
|
newGroupName.value = "";
|
||||||
|
editingGroupIndex.value = list.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeGroup(index) {
|
||||||
|
const group = groups.value[index];
|
||||||
|
if (!group || builtinGroupNames.has(group.groupName)) return;
|
||||||
|
if (!window.confirm(`确定删除分组「${group.groupName || `分组 ${index + 1}`}」?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
groups.value.splice(index, 1);
|
||||||
|
if (editingGroupIndex.value === index) editingGroupIndex.value = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupKeywords(group) {
|
||||||
|
return group.keywords || group.words || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function addKeyword(group, index) {
|
||||||
|
const word = String(keywordInputs.value[index] || "").trim();
|
||||||
|
if (!word) return;
|
||||||
|
if (!Array.isArray(group.keywords)) group.keywords = groupKeywords(group).slice();
|
||||||
|
if (!group.keywords.includes(word)) group.keywords.push(word);
|
||||||
|
keywordInputs.value[index] = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeKeyword(group, keyword) {
|
||||||
|
group.keywords = groupKeywords(group).filter((item) => item !== keyword);
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupPreviewStyle(group) {
|
||||||
|
return buildSubtitlePreviewBoxStyle({
|
||||||
|
...(config.value.subtitleStyle || {}),
|
||||||
|
...(group.styleOverride || {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportGroups() {
|
||||||
|
const blob = new Blob(
|
||||||
|
[
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
version: "1.0.0",
|
||||||
|
exportedAt: new Date().toISOString(),
|
||||||
|
keywordGroupsStyles: groups.value,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
{ type: "application/json" },
|
||||||
|
);
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = "keyword-groups-config.json";
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function importGroups(event) {
|
||||||
|
const file = event.target.files?.[0];
|
||||||
|
event.target.value = "";
|
||||||
|
if (!file) return;
|
||||||
|
const data = JSON.parse(await file.text());
|
||||||
|
const list = data.keywordGroupsStyles || data.groups || data;
|
||||||
|
if (!Array.isArray(list)) {
|
||||||
|
window.alert("无效的关键词分组配置");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
groups.value = list;
|
||||||
|
editingGroupIndex.value = -1;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -49,10 +186,7 @@ const groups = computed(() => config.value.keywordGroupsStyles || []);
|
|||||||
</div>
|
</div>
|
||||||
<SelectButton
|
<SelectButton
|
||||||
v-model="keywordRenderMode"
|
v-model="keywordRenderMode"
|
||||||
:options="[
|
:options="renderModeOptions"
|
||||||
{ label: '内联字幕', value: 'inline-emphasis' },
|
|
||||||
{ label: '浮动特效', value: 'floating' },
|
|
||||||
]"
|
|
||||||
option-label="label"
|
option-label="label"
|
||||||
option-value="value"
|
option-value="value"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -80,23 +214,181 @@ const groups = computed(() => config.value.keywordGroupsStyles || []);
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-lg border border-slate-600/40 bg-slate-900/40 p-3">
|
<div class="rounded-lg border border-slate-600/40 bg-slate-900/40 p-3">
|
||||||
<h6 class="mb-3 text-sm font-medium text-slate-200">关键词分组({{ groups.length }})</h6>
|
<h6 class="mb-3 text-sm font-medium text-slate-200">关键词分组管理</h6>
|
||||||
<div class="max-h-48 space-y-2 overflow-y-auto">
|
|
||||||
|
<div class="mb-4 rounded bg-slate-800/60 p-3">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<InputText
|
||||||
|
v-model="newGroupName"
|
||||||
|
size="small"
|
||||||
|
class="flex-1"
|
||||||
|
placeholder="输入分组名称"
|
||||||
|
@keydown.enter="createGroup"
|
||||||
|
/>
|
||||||
|
<Button label="创建分组" size="small" @click="createGroup" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div class="text-sm font-medium text-slate-200">已有分组 ({{ groups.length }})</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-for="(group, i) in groups"
|
v-for="(group, i) in groups"
|
||||||
:key="i"
|
:key="`${group.groupName || 'group'}-${i}`"
|
||||||
class="rounded border border-slate-600/30 bg-slate-800/50 p-2 text-sm"
|
class="rounded-lg border border-slate-600/40 bg-slate-800/50 p-3"
|
||||||
:style="{ borderLeftWidth: '4px', borderLeftColor: group.color || '#3b82f6' }"
|
:style="{ borderLeftWidth: '4px', borderLeftColor: group.color || '#1890ff' }"
|
||||||
>
|
>
|
||||||
<div class="font-medium text-slate-200">{{ group.groupName || `分组 ${i + 1}` }}</div>
|
<div class="mb-2 flex items-start justify-between gap-2">
|
||||||
<div v-if="group.styleOverride?.fontName" class="mt-1 text-xs text-slate-400">
|
<div class="min-w-0 flex-1">
|
||||||
强调字体:{{ group.styleOverride.fontName }} ·
|
<div class="flex items-center gap-2 font-medium text-slate-100">
|
||||||
{{ group.styleOverride.fontColor || group.color }}
|
<span>{{ group.groupName || `分组 ${i + 1}` }}</span>
|
||||||
|
</div>
|
||||||
|
<InputText
|
||||||
|
v-model="group.description"
|
||||||
|
size="small"
|
||||||
|
class="mt-2 w-full"
|
||||||
|
placeholder="分组描述"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex shrink-0 gap-2">
|
||||||
|
<Button
|
||||||
|
:label="editingGroupIndex === i ? '收起样式' : '编辑样式'"
|
||||||
|
size="small"
|
||||||
|
text
|
||||||
|
@click="
|
||||||
|
ensureStyleOverride(group);
|
||||||
|
editingGroupIndex = editingGroupIndex === i ? -1 : i;
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
v-if="!builtinGroupNames.has(group.groupName)"
|
||||||
|
label="删除"
|
||||||
|
size="small"
|
||||||
|
text
|
||||||
|
severity="danger"
|
||||||
|
@click="removeGroup(i)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-2">
|
||||||
|
<div class="mb-1 text-xs text-slate-400">关键词 ({{ groupKeywords(group).length }})</div>
|
||||||
|
<div v-if="groupKeywords(group).length" class="flex flex-wrap gap-1">
|
||||||
|
<Tag
|
||||||
|
v-for="keyword in groupKeywords(group)"
|
||||||
|
:key="keyword"
|
||||||
|
:value="keyword"
|
||||||
|
rounded
|
||||||
|
:style="{
|
||||||
|
backgroundColor: group.color || '#1890ff',
|
||||||
|
color: '#fff',
|
||||||
|
border: 'none',
|
||||||
|
}"
|
||||||
|
class="cursor-pointer"
|
||||||
|
@click="removeKeyword(group, keyword)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-xs text-slate-500">暂无关键词</div>
|
||||||
|
<div class="mt-2 flex gap-2">
|
||||||
|
<InputText
|
||||||
|
v-model="keywordInputs[i]"
|
||||||
|
size="small"
|
||||||
|
class="flex-1"
|
||||||
|
placeholder="添加关键词"
|
||||||
|
@keydown.enter="addKeyword(group, i)"
|
||||||
|
/>
|
||||||
|
<Button label="添加" size="small" severity="secondary" @click="addKeyword(group, i)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="editingGroupIndex === i"
|
||||||
|
class="mt-3 rounded-lg border border-slate-600/40 bg-white p-3 text-slate-800"
|
||||||
|
>
|
||||||
|
<div class="mb-3 text-sm font-medium">分组样式</div>
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label class="mb-1 block text-xs text-slate-500">字体</label>
|
||||||
|
<Select
|
||||||
|
v-model="group.styleOverride.fontName"
|
||||||
|
:options="SUBTITLE_FONT_OPTIONS"
|
||||||
|
option-label="label"
|
||||||
|
option-value="value"
|
||||||
|
class="w-full"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-xs text-slate-500">字体大小</label>
|
||||||
|
<InputNumber
|
||||||
|
v-model="group.styleOverride.fontSize"
|
||||||
|
:min="12"
|
||||||
|
:max="120"
|
||||||
|
class="w-full"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-xs text-slate-500">标签颜色</label>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<input v-model="group.color" type="color" class="h-8 w-10" />
|
||||||
|
<InputText v-model="group.color" size="small" class="flex-1" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-xs text-slate-500">文字颜色</label>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<input v-model="group.styleOverride.fontColor" type="color" class="h-8 w-10" />
|
||||||
|
<InputText v-model="group.styleOverride.fontColor" size="small" class="flex-1" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-xs text-slate-500">描边颜色</label>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<input v-model="group.styleOverride.outlineColor" type="color" class="h-8 w-10" />
|
||||||
|
<InputText v-model="group.styleOverride.outlineColor" size="small" class="flex-1" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label class="mb-1 block text-xs text-slate-500">描边宽度</label>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Slider
|
||||||
|
v-model="group.styleOverride.outlineWidth"
|
||||||
|
:min="0"
|
||||||
|
:max="10"
|
||||||
|
class="flex-1"
|
||||||
|
/>
|
||||||
|
<span class="w-10 text-right font-mono text-sm">
|
||||||
|
{{ group.styleOverride.outlineWidth }}px
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 rounded bg-slate-800 p-3 text-center">
|
||||||
|
<span :style="groupPreviewStyle(group)">示例关键词</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-2 text-xs text-slate-500">分组样式编辑与关键词标记将在后续版本开放</p>
|
|
||||||
|
<div class="mt-4 flex gap-2">
|
||||||
|
<Button label="导出配置" size="small" severity="secondary" @click="exportGroups" />
|
||||||
|
<Button
|
||||||
|
label="导入配置"
|
||||||
|
size="small"
|
||||||
|
severity="secondary"
|
||||||
|
@click="importInputRef?.click()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
ref="importInputRef"
|
||||||
|
type="file"
|
||||||
|
accept="application/json,.json"
|
||||||
|
class="hidden"
|
||||||
|
@change="importGroups"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -58,6 +58,13 @@ const noBackground = computed({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const shadowBlurPreview = computed({
|
||||||
|
get: () => subtitleStyle.value?.shadowBlur ?? 4,
|
||||||
|
set: (value) => {
|
||||||
|
if (subtitleStyle.value) subtitleStyle.value.shadowBlur = value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const previewStyle = computed(() =>
|
const previewStyle = computed(() =>
|
||||||
buildSubtitlePreviewBoxStyle(subtitleStyle.value),
|
buildSubtitlePreviewBoxStyle(subtitleStyle.value),
|
||||||
);
|
);
|
||||||
@@ -189,6 +196,24 @@ function applyPreset(preset) {
|
|||||||
<InputText v-model="subtitleStyle.shadowColor" size="small" class="flex-1" />
|
<InputText v-model="subtitleStyle.shadowColor" size="small" class="flex-1" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label class="mb-2 block text-sm text-slate-400">阴影模糊(仅预览有效)</label>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Slider
|
||||||
|
v-model="shadowBlurPreview"
|
||||||
|
:min="0"
|
||||||
|
:max="20"
|
||||||
|
class="flex-1"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
<span class="w-10 text-right font-mono text-sm text-slate-400">
|
||||||
|
{{ shadowBlurPreview }}px
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p class="mt-1 text-xs text-slate-400">
|
||||||
|
注:FFmpeg 不支持阴影模糊,实际视频中仅显示阴影偏移效果
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="col-span-2">
|
<div class="col-span-2">
|
||||||
<label class="mb-2 block text-sm text-slate-600">背景颜色</label>
|
<label class="mb-2 block text-sm text-slate-600">背景颜色</label>
|
||||||
|
|||||||
@@ -20,6 +20,13 @@ const enableBackground = computed({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const backgroundOpacityPercent = computed({
|
||||||
|
get: () => Math.round((Number(line.value.backgroundOpacity) || 0) * 100),
|
||||||
|
set: (value) => {
|
||||||
|
line.value.backgroundOpacity = Math.max(0, Math.min(100, Number(value) || 0)) / 100;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const enableShadow = computed({
|
const enableShadow = computed({
|
||||||
get: () => (line.value.shadowOffset ?? 0) > 0,
|
get: () => (line.value.shadowOffset ?? 0) > 0,
|
||||||
set: (on) => {
|
set: (on) => {
|
||||||
@@ -44,7 +51,7 @@ const previewStyle = computed(() => {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
fontFamily: line.value.fontName ? `'${line.value.fontName}'` : "优设标题黑",
|
fontFamily: line.value.fontName ? `'${line.value.fontName}'` : "优设标题黑",
|
||||||
fontSize: `${Math.min(Number(line.value.fontSize) || 80, 48)}px`,
|
fontSize: `${Number(line.value.fontSize) || 80}px`,
|
||||||
color: line.value.fontColor || "#fff",
|
color: line.value.fontColor || "#fff",
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
enableBackground.value && line.value.backgroundColor !== "transparent"
|
enableBackground.value && line.value.backgroundColor !== "transparent"
|
||||||
@@ -52,6 +59,8 @@ const previewStyle = computed(() => {
|
|||||||
: "transparent",
|
: "transparent",
|
||||||
textShadow: shadows.join(", ") || "none",
|
textShadow: shadows.join(", ") || "none",
|
||||||
padding: "8px 16px",
|
padding: "8px 16px",
|
||||||
|
display: "inline-block",
|
||||||
|
lineHeight: 1.12,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -108,6 +117,22 @@ const previewStyle = computed(() => {
|
|||||||
<Checkbox v-model="enableBackground" binary />
|
<Checkbox v-model="enableBackground" binary />
|
||||||
启用背景
|
启用背景
|
||||||
</label>
|
</label>
|
||||||
|
<div v-if="enableBackground" class="mt-2 grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-xs text-slate-500">背景颜色</label>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<input v-model="line.backgroundColor" type="color" class="h-8 w-10" />
|
||||||
|
<InputText v-model="line.backgroundColor" size="small" class="flex-1" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-xs text-slate-500">背景透明度</label>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Slider v-model="backgroundOpacityPercent" :min="0" :max="100" class="flex-1" />
|
||||||
|
<span class="w-10 text-right text-xs">{{ backgroundOpacityPercent }}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -116,17 +141,26 @@ const previewStyle = computed(() => {
|
|||||||
<Checkbox v-model="enableShadow" binary />
|
<Checkbox v-model="enableShadow" binary />
|
||||||
启用阴影
|
启用阴影
|
||||||
</label>
|
</label>
|
||||||
<div v-if="enableShadow" class="flex items-center gap-2">
|
<div v-if="enableShadow" class="space-y-2">
|
||||||
<label class="text-xs text-slate-500">阴影偏移</label>
|
<div class="flex items-center gap-2">
|
||||||
<Slider v-model="line.shadowOffset" :min="1" :max="10" class="flex-1" />
|
<label class="text-xs text-slate-500">阴影偏移</label>
|
||||||
<input v-model="line.shadowColor" type="color" class="h-8 w-10" />
|
<Slider v-model="line.shadowOffset" :min="1" :max="10" class="flex-1" />
|
||||||
|
<span class="w-8 text-right text-xs">{{ line.shadowOffset }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<label class="text-xs text-slate-500">阴影颜色</label>
|
||||||
|
<input v-model="line.shadowColor" type="color" class="h-8 w-10" />
|
||||||
|
<InputText v-model="line.shadowColor" size="small" class="flex-1" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="mb-2 text-sm font-medium text-slate-700">效果预览</div>
|
<div class="mb-2 text-sm font-medium text-slate-700">效果预览</div>
|
||||||
<div class="rounded bg-slate-100 py-4 text-center" :style="previewStyle">
|
<div class="overflow-hidden rounded bg-slate-100 py-4 text-center">
|
||||||
{{ line.text || "标题文字" }}
|
<div :style="previewStyle">
|
||||||
|
{{ line.text || "标题文字" }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from "vue";
|
import { computed, ref, watch } from "vue";
|
||||||
import TitleLineStyleEditor from "./TitleLineStyleEditor.vue";
|
import TitleLineStyleEditor from "./TitleLineStyleEditor.vue";
|
||||||
import { syncTitleLinesFromReference } from "../../utils/subtitleTemplateDraft.js";
|
import { syncTitleLinesFromReference } from "../../utils/subtitleTemplateDraft.js";
|
||||||
|
|
||||||
@@ -37,6 +37,19 @@ function reloadFromReference() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const linePreviews = computed(() => titleConfig.value?.lines || []);
|
const linePreviews = computed(() => titleConfig.value?.lines || []);
|
||||||
|
|
||||||
|
const maxCharMarks = [8, 12, 15, 20];
|
||||||
|
|
||||||
|
function markLeft(value) {
|
||||||
|
return `${((value - 8) / (20 - 8)) * 100}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => titleConfig.value?.maxCharsPerLine,
|
||||||
|
() => {
|
||||||
|
reloadFromReference();
|
||||||
|
},
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -57,36 +70,48 @@ const linePreviews = computed(() => titleConfig.value?.lines || []);
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-4 md:grid-cols-2">
|
<div class="space-y-4">
|
||||||
<div>
|
<div class="flex items-center gap-3">
|
||||||
<label class="mb-1 block text-xs text-slate-400">每行最大字符数</label>
|
<div class="w-28 shrink-0 text-sm text-slate-300">每行最大字符数:</div>
|
||||||
<InputNumber
|
<div class="relative flex-1 pb-5">
|
||||||
v-model="titleConfig.maxCharsPerLine"
|
<Slider
|
||||||
:min="8"
|
v-model="titleConfig.maxCharsPerLine"
|
||||||
:max="20"
|
:min="8"
|
||||||
size="small"
|
:max="20"
|
||||||
class="w-full"
|
:step="1"
|
||||||
/>
|
class="w-full"
|
||||||
|
/>
|
||||||
|
<div class="pointer-events-none absolute inset-x-0 top-6 text-xs text-slate-500">
|
||||||
|
<span
|
||||||
|
v-for="mark in maxCharMarks"
|
||||||
|
:key="mark"
|
||||||
|
class="absolute -translate-x-1/2"
|
||||||
|
:style="{ left: markLeft(mark) }"
|
||||||
|
>
|
||||||
|
{{ mark }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="flex items-center gap-3">
|
||||||
<label class="mb-1 block text-xs text-slate-400">行间距(%)</label>
|
<div class="w-28 shrink-0 text-sm text-slate-300">行间距(%):</div>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
v-model="titleConfig.lineSpacingPercent"
|
v-model="titleConfig.lineSpacingPercent"
|
||||||
:min="3"
|
:min="3"
|
||||||
:max="15"
|
:max="15"
|
||||||
size="small"
|
class="w-32"
|
||||||
class="w-full"
|
|
||||||
/>
|
/>
|
||||||
|
<span class="text-xs text-slate-500">相对于视频高度</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="flex items-center gap-3">
|
||||||
<label class="mb-1 block text-xs text-slate-400">顶部边距(%)</label>
|
<div class="w-28 shrink-0 text-sm text-slate-300">顶部边距(%):</div>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
v-model="titleConfig.topMarginPercent"
|
v-model="titleConfig.topMarginPercent"
|
||||||
:min="2"
|
:min="2"
|
||||||
:max="20"
|
:max="20"
|
||||||
size="small"
|
class="w-32"
|
||||||
class="w-full"
|
|
||||||
/>
|
/>
|
||||||
|
<span class="text-xs text-slate-500">相对于视频高度</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user