feat(renderer): 修改和编写前端字幕控制和字幕记录界面

This commit is contained in:
himeditator
2025-06-15 22:35:40 +08:00
parent a07c3283b7
commit 4ef62eed03
7 changed files with 338 additions and 98 deletions

View File

@@ -3,9 +3,8 @@
<a-row>
<a-col :span="controlSpan">
<div class="caption-control">
<a-card size="small" title="页面设置">
<a-card size="small" title="页面宽度">
<div>
<span class="span-label">页面宽度</span>
<a-input
type="range" class="span-input"
min="6" max="18"
@@ -18,7 +17,9 @@
</div>
</a-col>
<a-col :span="24 - controlSpan">
<CaptionData />
<div class="caption-data">
<CaptionData />
</div>
</a-col>
</a-row>
</div>
@@ -30,8 +31,7 @@ import CaptionStyle from './components/CaptionStyle.vue'
import CaptionControl from './components/CaptionControl.vue';
import CaptionData from './components/CaptionData.vue'
import { ref } from 'vue'
const controlSpan = ref(9)
const controlSpan = ref(8)
</script>
<style scoped>
@@ -43,11 +43,11 @@ const controlSpan = ref(9)
scrollbar-width: thin;
}
.span-label {
display: inline-block;
width: 80px;
text-align: right;
margin-right: 10px;
.caption-data {
height: 100vh;
padding: 20px;
overflow-y: auto;
scrollbar-width: thin;
}
.span-input {

View File

@@ -1,14 +1,16 @@
<template>
<div style="height: 20px;"></div>
<a-card size="small" title="字幕控制">
<template #extra><a href="#">应用</a></template>
<template #extra>
<a @click="applyControl">应用</a>
</template>
<div class="control-item">
<span class="control-label">源语言</span>
<a-select
class="control-input"
ref="select"
v-model:value="sourceLang"
:options="languages"
v-model:value="currentSourceLang"
:options="langList"
></a-select>
</div>
<div class="control-item">
@@ -16,8 +18,8 @@
<a-select
class="control-input"
ref="select"
v-model:value="targetLang"
:options="languages"
v-model:value="currentTargetLang"
:options="langList.filter((item) => item.value !== 'auto')"
></a-select>
</div>
<div class="control-item">
@@ -25,35 +27,57 @@
<a-select
class="control-input"
ref="select"
v-model:value="engine"
v-model:value="currentEngine"
:options="captionEngine"
></a-select>
</div>
<div class="control-item">
<span class="control-label">端口号</span>
<a-input
class="control-input"
ref="select"
type="number"
v-model:value="currentPort"
></a-input>
</div>
<div class="control-item">
<span class="control-label">启用翻译</span>
<a-switch v-model:checked="translation" />
<a-switch v-model:checked="currentTranslation" />
</div>
</a-card>
<div style="height: 20px;"></div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ref, computed } from 'vue'
import { storeToRefs } from 'pinia'
import { useCaptionControlStore } from '@renderer/stores/captionControl'
const languages = ref([
{ value: 'en', label: '英语' },
{ value: 'zh', label: '简体中文' },
{ value: 'ja', label: '日语' },
])
const captionEngine = ref([
{value: 'gummy', label: '云端-阿里云-Gummy'}
])
const captionControl = useCaptionControlStore()
const { captionEngine } = storeToRefs(captionControl)
const currentSourceLang = ref('auto')
const currentTargetLang = ref('zh')
const currentEngine = ref('gummy')
const currentPort = ref(8765)
const currentTranslation = ref<boolean>(false)
const sourceLang = ref('en')
const targetLang = ref('zh')
const engine = ref('gummy')
const translation = ref<boolean>(true)
const langList = computed(() => {
for(let item of captionEngine.value){
if(item.value === currentEngine.value) {
return item.languages
}
}
return []
})
function applyControl(){
captionControl.sourceLang = currentSourceLang.value
captionControl.targetLang = currentTargetLang.value
captionControl.engine = currentEngine.value
captionControl.port = currentPort.value
captionControl.translation = currentTranslation.value
}
</script>
<style scoped>
@@ -65,7 +89,6 @@ const translation = ref<boolean>(true)
display: inline-block;
width: 80px;
text-align: right;
margin-right: 10px;
}

View File

@@ -1,14 +1,163 @@
<template>
<div>
<div class="caption-stat">
<a-row>
<a-col :span="12">col-12</a-col>
<a-col :span="12">col-12</a-col>
</a-row>
<a-col :span="6">
<a-statistic title="字幕引擎" :value="'gummy'" />
</a-col>
<a-col :span="6">
<a-statistic title="字幕引擎状态" :value="'未连接'" />
</a-col>
<a-col :span="6">
<a-statistic title="已记录字幕" :value="captionData.length" />
</a-col>
</a-row>
</div>
<div class="caption-control">
<a-button type="primary" class="control-button">打开字幕窗口</a-button>
<a-button class="control-button">启动字幕引擎</a-button>
<a-button danger class="control-button">关闭字幕引擎</a-button>
</div>
<div class="caption-list">
<div class="caption-title">字幕记录</div>
<a-table
:columns="columns"
:data-source="captionData"
v-model:pagination="pagination"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'index'">
{{ record.index }}
</template>
<template v-if="column.key === 'time'">
<div class="time-cell">
<div class="time-start">{{ record.time_s }}</div>
<div class="time-end">{{ record.time_t }}</div>
</div>
</template>
<template v-if="column.key === 'content'">
<div class="caption-content">
<div class="caption-text">{{ record.text }}</div>
<div class="caption-translation">{{ record.translation }}</div>
</div>
</template>
</template>
</a-table>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const captionData = ref([
{index: 1, time_s: "00:00:00", time_t: "00:00:00", text: "Long time no see.", translation: "好久不见"},
{index: 2, time_s: "00:00:00", time_t: "00:00:00", text: "How have you been?", translation: "你最近怎么样?"},
{index: 3, time_s: "00:00:00", time_t: "00:00:00", text: "I've missed you a lot.", translation: "我非常想念你。"},
{index: 4, time_s: "00:00:00", time_t: "00:00:00", text: "It's good to see you again.", translation: "很高兴再次见到你。"},
{index: 5, time_s: "00:00:00", time_t: "00:00:00", text: "What have you been up to?", translation: "你最近在忙什么?"},
{index: 6, time_s: "00:00:00", time_t: "00:00:00", text: "Let's catch up over coffee.", translation: "我们去喝杯咖啡聊聊天吧。"},
{index: 7, time_s: "00:00:00", time_t: "00:00:00", text: "You look great!", translation: "你看起来很棒!"},
{index: 8, time_s: "00:00:00", time_t: "00:00:00", text: "I can't believe it's been so long.", translation: "真不敢相信已经这么久了。"},
{index: 9, time_s: "00:00:00", time_t: "00:00:00", text: "We should do this more often.", translation: "我们应该多聚聚。"},
{index: 10, time_s: "00:00:00", time_t: "00:00:00", text: "Thanks for coming to see me.", translation: "谢谢你来看我。"},
{index: 11, time_s: "00:00:00", time_t: "00:00:00", text: "We show case the utility of Macformer when combined with molecular docking simulations and wet lab based experimental validation, by applying it to the prospective design of macrocyclic JAK2 inhibitors.", translation: "我们通过将其应用于大环JAK2抑制剂的前瞻性设计展示了Macformer与分子对接模拟和湿实验验证相结合的实用性。"},
{index: 12, time_s: "00:00:00", time_t: "00:00:00", text: "Macrocycles, typically defined as cyclic small molecules or peptides with ring structures consisting of 12 or more atoms, has emerged as promising chemical scaffolds in the field of new drug discovery1,2. The distinct physicochemical properties, including high molecular weight and abundant hydrogen bond donors3, render this structural class occupy a chemical space beyond Lipinski's rule of five4.", translation: "大环分子通常定义为具有由 12 个或更多原子组成的环状结构的环状小分子或肽,已成为新药发现领域中具有前景的化学骨架 [1,2]。其独特的理化性质(包括高分子量和丰富的氢键供体)[3],使这类结构占据了超越 Lipinski 五规则 [4] 的化学空间。"}
])
const pagination = ref({
current: 1,
pageSize: 10,
showSizeChanger: true,
pageSizeOptions: ['10', '20', '50'],
showTotal: (total: number) => `${total} 条记录`,
onChange: (page: number, pageSize: number) => {
pagination.value.current = page
pagination.value.pageSize = pageSize
},
onShowSizeChange: (current: number, size: number) => {
pagination.value.current = current
pagination.value.pageSize = size
}
})
const columns = [
{
title: '序号',
dataIndex: 'index',
key: 'index',
width: 80,
},
{
title: '时间',
dataIndex: 'time',
key: 'time',
width: 160,
},
{
title: '字幕内容',
dataIndex: 'content',
key: 'content',
},
]
</script>
<style scoped></style>
<style scoped>
.caption-control {
display: flex;
flex-wrap: wrap;
justify-content: center;
margin: 30px;
}
.control-button {
height: 40px;
margin: 20px;
font-size: 16px;
}
.caption-list {
background: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.caption-title {
font-size: 24px;
font-weight: bold;
margin-bottom: 10px;
}
.time-cell {
display: flex;
flex-direction: column;
gap: 4px;
font-size: 14px;
}
.time-start {
color: #1677ff;
}
.time-end {
color: #ff4d4f;
}
.caption-content {
padding: 8px 0;
}
.caption-text {
font-size: 16px;
color: #333;
margin-bottom: 4px;
}
.caption-translation {
font-size: 14px;
color: #666;
padding-left: 16px;
border-left: 3px solid #1890ff;
}
</style>

View File

@@ -1,7 +1,8 @@
<template>
<a-card size="small" title="字幕样式设置">
<template #extra>
<a>应用样式</a> | <a>取消更改</a>
<a @click="applyStyle">应用样式</a> |
<a @click="resetStyle">取消更改</a>
</template>
<div class="style-item">
<span class="style-label">字体族</span>
@@ -10,14 +11,6 @@
v-model:value="currentFontFamily"
/>
</div>
<div class="style-item">
<span class="style-label">字体大小</span>
<a-input
class="style-input"
type="number"
v-model:value="currentFontSize"
/>
</div>
<div class="style-item">
<span class="style-label">字体颜色</span>
<a-input
@@ -27,6 +20,16 @@
/>
<div class="style-item-value">{{ currentFontColor }}</div>
</div>
<div class="style-item">
<span class="style-label">字体大小</span>
<a-input
class="style-input"
type="range"
min="0" max="64"
v-model:value="currentFontSize"
/>
<div class="style-item-value">{{ currentFontSize }}px</div>
</div>
<div class="style-item">
<span class="style-label">背景颜色</span>
<a-input
@@ -39,7 +42,7 @@
<div class="style-item">
<span class="style-label">背景透明度</span>
<a-input
class="style-input range-input"
class="style-input"
type="range"
min="0"
max="100"
@@ -52,10 +55,10 @@
<span class="style-label">显示预览</span>
<a-switch v-model:checked="displayPreview" />
<span class="style-label">显示翻译</span>
<a-switch v-model:checked="currentTranslation" />
<a-switch v-model:checked="currentTransDisplay" />
</div>
<div v-show="currentTranslation">
<div v-show="currentTransDisplay">
<a-card size="small" title="翻译样式设置">
<template #extra>
<a @click="useSameStyle">使用相同样式</a>
@@ -67,14 +70,6 @@
v-model:value="currentTransFontFamily"
/>
</div>
<div class="style-item">
<span class="style-label">翻译大小</span>
<a-input
class="style-input"
type="number"
v-model:value="currentTransFontSize"
/>
</div>
<div class="style-item">
<span class="style-label">翻译颜色</span>
<a-input
@@ -84,6 +79,16 @@
/>
<div class="style-item-value">{{ currentTransFontColor }}</div>
</div>
<div class="style-item">
<span class="style-label">翻译大小</span>
<a-input
class="style-input"
type="range"
min="0" max="64"
v-model:value="currentTransFontSize"
/>
<div class="style-item-value">{{ currentTransFontSize }}px</div>
</div>
</a-card>
</div>
@@ -97,21 +102,21 @@
backgroundColor: addOpicityToColor(currentBackground, currentOpacity)
}"
>
<div class="preview-caption"
<p class="preview-caption"
:style="{
fontFamily: currentFontFamily,
fontSize: currentFontSize + 'px',
color: currentFontColor
}">
{{ "This is a preview of subtitle styles." }}
</div>
<div class="preview-translation" v-if="currentTranslation"
</p>
<p class="preview-translation" v-if="currentTransDisplay"
:style="{
fontFamily: currentTransFontFamily,
fontSize: currentTransFontSize + 'px',
color: currentTransFontColor
}"
>这是字幕样式预览(翻译)</div>
>这是字幕样式预览(翻译)</p>
</div>
</Teleport>
@@ -119,23 +124,21 @@
<script setup lang="ts">
import { ref } from 'vue'
// import { useCaptionStore } from '../stores/caption'
// const caption = useCaptionStore()
import { useCaptionStyleStore } from '@renderer/stores/captionStyle'
const captionStyle = useCaptionStyleStore()
const currentFontFamily = ref<string>('sans-serif')
const currentFontSize = ref<number>(24)
const currentFontColor = ref<string>('#000000')
const currentBackground = ref<string>('#dbe2ef')
const currentOpacity = ref<number>(50)
const currentTranslation = ref<boolean>(true)
const currentTransDisplay = ref<boolean>(true)
const currentTransFontFamily = ref<string>('sans-serif')
const currentTransFontSize = ref<number>(24)
const currentTransFontColor = ref<string>('#000000')
const displayPreview = ref<boolean>(true)
function addOpicityToColor(color: string, opicity: number) {
if (color.length !== 7 || color[0] !== '#') {
throw new Error('Invalid color format. Please use a valid hex color like #AABBCC.');
}
const opicityValue = Math.round(opicity * 255 / 100);
const opicityHex = opicityValue.toString(16).padStart(2, '0');
return `${color}${opicityHex}`;
@@ -146,17 +149,35 @@ function useSameStyle(){
currentTransFontSize.value = currentFontSize.value;
currentTransFontColor.value = currentFontColor.value;
}
function applyStyle(){
captionStyle.fontFamily = currentFontFamily.value;
captionStyle.fontSize = currentFontSize.value;
captionStyle.fontColor = currentFontColor.value;
captionStyle.background = currentBackground.value;
captionStyle.opacity = currentOpacity.value;
captionStyle.transDisplay = currentTransDisplay.value;
captionStyle.transFontFamily = currentTransFontFamily.value;
captionStyle.transFontSize = currentTransFontSize.value;
captionStyle.transFontColor = currentTransFontColor.value;
}
function resetStyle(){
currentFontFamily.value = captionStyle.fontFamily;
currentFontSize.value = captionStyle.fontSize;
currentFontColor.value = captionStyle.fontColor;
currentBackground.value = captionStyle.background;
currentOpacity.value = captionStyle.opacity;
currentTransDisplay.value = captionStyle.transDisplay;
currentTransFontFamily.value = captionStyle.transFontFamily;
currentTransFontSize.value = captionStyle.transFontSize;
currentTransFontColor.value = captionStyle.transFontColor;
}
</script>
<style scoped>
.caption-style {
height: 100vh;
border-right: 1px solid #7774;
padding: 20px;
overflow-y: auto;
scrollbar-width: thin;
}
.caption-button {
display: flex;
justify-content: center;
@@ -185,15 +206,6 @@ function useSameStyle(){
color: #666
}
.range-input {
width: calc(100% - 110px);
min-width: 90px;
padding-left: 0 !important;
padding-right: 0 !important;
margin-left: 5px;
margin-right: 5px;
}
.preview-container {
line-height: 2em;
width: 60%;
@@ -205,4 +217,9 @@ function useSameStyle(){
transform: translateX(-50%);
bottom: 20px;
}
.preview-container p {
margin: 0;
line-height: 1.5em;
}
</style>

View File

@@ -1,15 +0,0 @@
import { ref } from 'vue'
import { defineStore } from 'pinia'
export const useCaptionStore = defineStore('caption', () => {
const captionFontFamily = ref<string>('sans-serif')
const captionFontSize = ref<number>(24)
const captionFontColor = ref<string>('#ffffff')
const backgroundColor = ref<string>('#000000')
return {
captionFontFamily,
captionFontSize,
captionFontColor,
backgroundColor
}
})

View File

@@ -0,0 +1,41 @@
import { ref } from 'vue'
import { defineStore } from 'pinia'
export const useCaptionControlStore = defineStore('captionControl', () => {
const captionEngine = ref([
{
value: 'gummy',
label: '云端-阿里云-Gummy',
languages: [
{ value: 'auto', label: '自动检测' },
{ value: 'en', label: '英语' },
{ value: 'zh', label: '简体中文' },
{ value: 'ja', label: '日语' },
{ value: 'ko', label: '韩语' }
]
},
{
value: 'whisper',
label: '本地-OpenAI-Whisper',
languages: [
{ value: 'auto', label: '自动检测' },
{ value: 'en', label: '英语' },
{ value: 'zh', label: '简体中文' }
]
},
])
const sourceLang = ref<string>('auto')
const targetLang = ref<string>('')
const engine = ref<string>('gummy')
const port = ref<number>(8765)
const translation = ref<boolean>(false)
return {
captionEngine, // 字幕引擎
sourceLang, // 源语言
targetLang, // 目标语言
engine, // 字幕引擎
port, // 端口
translation // 是否启用翻译
}
})

View File

@@ -0,0 +1,25 @@
import { ref } from 'vue'
import { defineStore } from 'pinia'
export const useCaptionStyleStore = defineStore('captionStyle', () => {
const fontFamily = ref<string>('sans-serif')
const fontSize = ref<number>(24)
const fontColor = ref<string>('#000000')
const background = ref<string>('#dbe2ef')
const opacity = ref<number>(50)
const transDisplay = ref<boolean>(true)
const transFontFamily = ref<string>('sans-serif')
const transFontSize = ref<number>(24)
const transFontColor = ref<string>('#000000')
return {
fontFamily, // 字体族
fontSize, // 字体大小
fontColor, // 字体颜色
background, // 背景颜色
opacity, // 背景透明度
transDisplay, // 是否显示翻译
transFontFamily, // 翻译字体族
transFontSize, // 翻译字体大小
transFontColor // 翻译字体颜色
}
})