feat(log): 添加软件日志功能

- 新增 SoftwareLog 相关接口和数据结构
- 实现日志数据的收集和展示
- 添加日志相关的国际化支持
- 优化控制页面布局,支持日志切换显示
This commit is contained in:
himeditator
2025-08-19 22:23:54 +08:00
parent 01936d5f12
commit 771f7ad002
21 changed files with 446 additions and 166 deletions

View File

@@ -284,6 +284,16 @@
**数据类型:** `Controls` **数据类型:** `Controls`
### `control.softwareLog.add`
**介绍:** 添加一条新的日志数据
**发起方:** 后端
**接收方:** 前端控制窗口
**数据类型:** `SoftwareLog`
### `both.styles.set` ### `both.styles.set`
**介绍:** 后端将最新字幕样式发送给前端,前端进行设置 **介绍:** 后端将最新字幕样式发送给前端,前端进行设置

View File

@@ -76,7 +76,7 @@ if __name__ == "__main__":
# vosk only # vosk only
parser.add_argument('-m', '--model_path', default='', help='The path to the vosk model.') parser.add_argument('-m', '--model_path', default='', help='The path to the vosk model.')
args = parser.parse_args() args = parser.parse_args()
if int(args.port) == 0: if int(args.port) == 0:
thread_data.status = "running" thread_data.status = "running"
else: else:

View File

@@ -7,8 +7,10 @@ import icon from '../../build/icon.png?asset'
import { captionWindow } from './CaptionWindow' import { captionWindow } from './CaptionWindow'
import { allConfig } from './utils/AllConfig' import { allConfig } from './utils/AllConfig'
import { captionEngine } from './utils/CaptionEngine' import { captionEngine } from './utils/CaptionEngine'
import { Log } from './utils/Log'
class ControlWindow { class ControlWindow {
mounted: boolean = false;
window: BrowserWindow | undefined; window: BrowserWindow | undefined;
public createWindow(): void { public createWindow(): void {
@@ -34,6 +36,7 @@ class ControlWindow {
}) })
this.window.on('closed', () => { this.window.on('closed', () => {
this.mounted = false
this.window = undefined this.window = undefined
allConfig.writeConfig() allConfig.writeConfig()
}) })
@@ -63,7 +66,8 @@ class ControlWindow {
}) })
ipcMain.handle('both.window.mounted', () => { ipcMain.handle('both.window.mounted', () => {
return allConfig.getFullConfig() this.mounted = true
return allConfig.getFullConfig(Log.getAndClearLogQueue())
}) })
ipcMain.handle('control.nativeTheme.get', () => { ipcMain.handle('control.nativeTheme.get', () => {

View File

@@ -45,6 +45,13 @@ export interface CaptionItem {
translation: string translation: string
} }
export interface SoftwareLogItem {
type: "INFO" | "WARN" | "ERROR",
index: number,
time: string,
text: string
}
export interface FullConfig { export interface FullConfig {
platform: string, platform: string,
uiLanguage: UILanguage, uiLanguage: UILanguage,
@@ -53,7 +60,8 @@ export interface FullConfig {
leftBarWidth: number, leftBarWidth: number,
styles: Styles, styles: Styles,
controls: Controls, controls: Controls,
captionLog: CaptionItem[] captionLog: CaptionItem[],
softwareLog: SoftwareLogItem[]
} }
export interface EngineInfo { export interface EngineInfo {

View File

@@ -1,6 +1,6 @@
import { import {
UILanguage, UITheme, Styles, Controls, UILanguage, UITheme, Styles, Controls,
CaptionItem, FullConfig CaptionItem, FullConfig, SoftwareLogItem
} from '../types' } from '../types'
import { Log } from './Log' import { Log } from './Log'
import { app, BrowserWindow } from 'electron' import { app, BrowserWindow } from 'electron'
@@ -88,7 +88,7 @@ class AllConfig {
Log.info('Write Config to:', configPath) Log.info('Write Config to:', configPath)
} }
public getFullConfig(): FullConfig { public getFullConfig(softwareLog: SoftwareLogItem[]): FullConfig {
return { return {
platform: process.platform, platform: process.platform,
uiLanguage: this.uiLanguage, uiLanguage: this.uiLanguage,
@@ -97,7 +97,8 @@ class AllConfig {
leftBarWidth: this.leftBarWidth, leftBarWidth: this.leftBarWidth,
styles: this.styles, styles: this.styles,
controls: this.controls, controls: this.controls,
captionLog: this.captionLog captionLog: this.captionLog,
softwareLog: softwareLog
} }
} }

View File

@@ -85,12 +85,16 @@ export class CaptionEngine {
} }
} }
Log.info('Engine Path:', this.appPath) Log.info('Engine Path:', this.appPath)
Log.info('Engine Command:', this.command) if(this.command.length > 2 && this.command.at(-2) === '-k') {
const _command = [...this.command]
_command[_command.length -1] = _command[_command.length -1].replace(/./g, '*')
Log.info('Engine Command:', _command)
}
else Log.info('Engine Command:', this.command)
return true return true
} }
public connect() { public connect() {
Log.info('Connecting to caption engine server...')
if(this.client) { Log.warn('Client already exists, ignoring...') } if(this.client) { Log.warn('Client already exists, ignoring...') }
this.client = net.createConnection({ port: this.port }, () => { this.client = net.createConnection({ port: this.port }, () => {
Log.info('Connected to caption engine server'); Log.info('Connected to caption engine server');
@@ -177,7 +181,6 @@ export class CaptionEngine {
this.client = undefined this.client = undefined
} }
this.status = 'stopping' this.status = 'stopping'
Log.info('Caption engine process stopping...')
this.timerID = setTimeout(() => { this.timerID = setTimeout(() => {
if(this.status !== 'stopping') return if(this.status !== 'stopping') return
Log.warn('Engine process still not stopped, trying to kill...') Log.warn('Engine process still not stopped, trying to kill...')
@@ -226,7 +229,7 @@ function handleEngineData(data: any) {
Log.info('Engine Info:', data.content) Log.info('Engine Info:', data.content)
} }
else if(data.command === 'usage') { else if(data.command === 'usage') {
Log.info('Engine Usage: ', data.content) Log.info('Engine Token Usage: ', data.content)
} }
else { else {
Log.warn('Unknown command:', data) Log.warn('Unknown command:', data)

View File

@@ -1,3 +1,9 @@
import { controlWindow } from "../ControlWindow"
import { type SoftwareLogItem } from "../types"
let logIndex = 0
const logQueue: SoftwareLogItem[] = []
function getTimeString() { function getTimeString() {
const now = new Date() const now = new Date()
const HH = String(now.getHours()).padStart(2, '0') const HH = String(now.getHours()).padStart(2, '0')
@@ -8,15 +14,45 @@ function getTimeString() {
} }
export class Log { export class Log {
static getAndClearLogQueue() {
const copiedQueue = structuredClone(logQueue)
logQueue.length = 0
return copiedQueue
}
static handleLog(logType: "INFO" | "WARN" | "ERROR", ...msg: any[]) {
const timeStr = getTimeString()
const logPre = `[${logType} ${timeStr}]`
let logStr = ""
for(let i = 0; i < msg.length; i++) {
logStr += i ? " " : ""
if(typeof msg[i] === "string") logStr += msg[i]
else logStr += JSON.stringify(msg[i], undefined, 2)
}
console.log(logPre, logStr)
const logItem: SoftwareLogItem = {
type: logType,
index: ++logIndex,
time: timeStr,
text: logStr
}
if(controlWindow.mounted && controlWindow.window) {
controlWindow.window.webContents.send('control.softwareLog.add', logItem)
}
else {
logQueue.push(logItem)
}
}
static info(...msg: any[]){ static info(...msg: any[]){
console.log(`[INFO ${getTimeString()}]`, ...msg) this.handleLog("INFO", ...msg)
} }
static warn(...msg: any[]){ static warn(...msg: any[]){
console.warn(`[WARN ${getTimeString()}]`, ...msg) this.handleLog("WARN", ...msg)
} }
static error(...msg: any[]){ static error(...msg: any[]){
console.error(`[ERROR ${getTimeString()}]`, ...msg) this.handleLog("ERROR", ...msg)
} }
} }

View File

@@ -2,7 +2,7 @@
<html> <html>
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<title>Auto Caption</title> <title>Auto Caption v0.7.0</title>
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP --> <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta <meta
http-equiv="Content-Security-Policy" http-equiv="Content-Security-Policy"

View File

@@ -6,6 +6,7 @@
import { onMounted } from 'vue' import { onMounted } from 'vue'
import { FullConfig } from './types' import { FullConfig } from './types'
import { useCaptionLogStore } from './stores/captionLog' import { useCaptionLogStore } from './stores/captionLog'
import { useSoftwareLogStore } from './stores/softwareLog'
import { useCaptionStyleStore } from './stores/captionStyle' import { useCaptionStyleStore } from './stores/captionStyle'
import { useEngineControlStore } from './stores/engineControl' import { useEngineControlStore } from './stores/engineControl'
import { useGeneralSettingStore } from './stores/generalSetting' import { useGeneralSettingStore } from './stores/generalSetting'
@@ -20,6 +21,7 @@ onMounted(() => {
useEngineControlStore().platform = data.platform useEngineControlStore().platform = data.platform
useEngineControlStore().setControls(data.controls) useEngineControlStore().setControls(data.controls)
useCaptionLogStore().captionData = data.captionLog useCaptionLogStore().captionData = data.captionLog
useSoftwareLogStore().softwareLogs = data.softwareLog
}) })
}) })
</script> </script>

View File

@@ -1,141 +1,139 @@
<template> <template>
<div class="caption-list"> <div>
<div> <div class="caption-title">
<a-app class="caption-title"> <span style="margin-right: 30px;">{{ $t('log.title') }}</span>
<span style="margin-right: 30px;">{{ $t('log.title') }}</span>
</a-app>
<a-popover :title="$t('log.baseTime')">
<template #content>
<div class="base-time">
<div class="base-time-container">
<a-input
type="number" min="0"
v-model:value="baseHH"
></a-input>
<span class="base-time-label">{{ $t('log.hour') }}</span>
</div>
</div><span style="margin: 0 4px;">:</span>
<div class="base-time">
<div class="base-time-container">
<a-input
type="number" min="0" max="59"
v-model:value="baseMM"
></a-input>
<span class="base-time-label">{{ $t('log.min') }}</span>
</div>
</div><span style="margin: 0 4px;">:</span>
<div class="base-time">
<div class="base-time-container">
<a-input
type="number" min="0" max="59"
v-model:value="baseSS"
></a-input>
<span class="base-time-label">{{ $t('log.sec') }}</span>
</div>
</div><span style="margin: 0 4px;">.</span>
<div class="base-time">
<div class="base-time-container">
<a-input
type="number" min="0" max="999"
v-model:value="baseMS"
></a-input>
<span class="base-time-label">{{ $t('log.ms') }}</span>
</div>
</div>
</template>
<a-button
type="primary"
style="margin-right: 20px;"
@click="changeBaseTime"
:disabled="captionData.length === 0"
>{{ $t('log.changeTime') }}</a-button>
</a-popover>
<a-popover :title="$t('log.exportOptions')">
<template #content>
<div class="input-item">
<span class="input-label">{{ $t('log.exportFormat') }}</span>
<a-radio-group v-model:value="exportFormat">
<a-radio-button value="srt"><code>.srt</code></a-radio-button>
<a-radio-button value="json"><code>.json</code></a-radio-button>
</a-radio-group>
</div>
<div class="input-item">
<span class="input-label">{{ $t('log.exportContent') }}</span>
<a-radio-group v-model:value="contentOption">
<a-radio-button value="both">{{ $t('log.both') }}</a-radio-button>
<a-radio-button value="source">{{ $t('log.source') }}</a-radio-button>
<a-radio-button value="target">{{ $t('log.translation') }}</a-radio-button>
</a-radio-group>
</div>
</template>
<a-button
style="margin-right: 20px;"
@click="exportCaptions"
:disabled="captionData.length === 0"
>{{ $t('log.export') }}</a-button>
</a-popover>
<a-popover :title="$t('log.copyOptions')">
<template #content>
<div class="input-item">
<span class="input-label">{{ $t('log.addIndex') }}</span>
<a-switch v-model:checked="showIndex" />
<span class="input-label">{{ $t('log.copyTime') }}</span>
<a-switch v-model:checked="copyTime" />
</div>
<div class="input-item">
<span class="input-label">{{ $t('log.copyContent') }}</span>
<a-radio-group v-model:value="contentOption">
<a-radio-button value="both">{{ $t('log.both') }}</a-radio-button>
<a-radio-button value="source">{{ $t('log.source') }}</a-radio-button>
<a-radio-button value="target">{{ $t('log.translation') }}</a-radio-button>
</a-radio-group>
</div>
<div class="input-item">
<span class="input-label">{{ $t('log.copyNum') }}</span>
<a-radio-group v-model:value="copyNum">
<a-radio-button :value="0"><code>[:]</code></a-radio-button>
<a-radio-button :value="1"><code>[-1:]</code></a-radio-button>
<a-radio-button :value="2"><code>[-2:]</code></a-radio-button>
<a-radio-button :value="3"><code>[-3:]</code></a-radio-button>
</a-radio-group>
</div>
</template>
<a-button
style="margin-right: 20px;"
@click="copyCaptions"
>{{ $t('log.copy') }}</a-button>
</a-popover>
<a-button
danger
@click="clearCaptions"
>{{ $t('log.clear') }}</a-button>
</div> </div>
<a-popover :title="$t('log.baseTime')">
<a-table <template #content>
:columns="columns" <div class="base-time">
:data-source="captionData" <div class="base-time-container">
v-model:pagination="pagination" <a-input
style="margin-top: 10px;" type="number" min="0"
> v-model:value="baseHH"
<template #bodyCell="{ column, record }"> ></a-input>
<template v-if="column.key === 'index'"> <span class="base-time-label">{{ $t('log.hour') }}</span>
{{ 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> </div>
</template> </div><span style="margin: 0 4px;">:</span>
<template v-if="column.key === 'content'"> <div class="base-time">
<div class="caption-content"> <div class="base-time-container">
<div class="caption-text">{{ record.text }}</div> <a-input
<div class="caption-translation">{{ record.translation }}</div> type="number" min="0" max="59"
v-model:value="baseMM"
></a-input>
<span class="base-time-label">{{ $t('log.min') }}</span>
</div> </div>
</template> </div><span style="margin: 0 4px;">:</span>
<div class="base-time">
<div class="base-time-container">
<a-input
type="number" min="0" max="59"
v-model:value="baseSS"
></a-input>
<span class="base-time-label">{{ $t('log.sec') }}</span>
</div>
</div><span style="margin: 0 4px;">.</span>
<div class="base-time">
<div class="base-time-container">
<a-input
type="number" min="0" max="999"
v-model:value="baseMS"
></a-input>
<span class="base-time-label">{{ $t('log.ms') }}</span>
</div>
</div>
</template> </template>
</a-table> <a-button
type="primary"
style="margin-right: 20px;"
@click="changeBaseTime"
:disabled="captionData.length === 0"
>{{ $t('log.changeTime') }}</a-button>
</a-popover>
<a-popover :title="$t('log.exportOptions')">
<template #content>
<div class="input-item">
<span class="input-label">{{ $t('log.exportFormat') }}</span>
<a-radio-group v-model:value="exportFormat">
<a-radio-button value="srt"><code>.srt</code></a-radio-button>
<a-radio-button value="json"><code>.json</code></a-radio-button>
</a-radio-group>
</div>
<div class="input-item">
<span class="input-label">{{ $t('log.exportContent') }}</span>
<a-radio-group v-model:value="contentOption">
<a-radio-button value="both">{{ $t('log.both') }}</a-radio-button>
<a-radio-button value="source">{{ $t('log.source') }}</a-radio-button>
<a-radio-button value="target">{{ $t('log.translation') }}</a-radio-button>
</a-radio-group>
</div>
</template>
<a-button
style="margin-right: 20px;"
@click="exportCaptions"
:disabled="captionData.length === 0"
>{{ $t('log.export') }}</a-button>
</a-popover>
<a-popover :title="$t('log.copyOptions')">
<template #content>
<div class="input-item">
<span class="input-label">{{ $t('log.addIndex') }}</span>
<a-switch v-model:checked="showIndex" />
<span class="input-label">{{ $t('log.copyTime') }}</span>
<a-switch v-model:checked="copyTime" />
</div>
<div class="input-item">
<span class="input-label">{{ $t('log.copyContent') }}</span>
<a-radio-group v-model:value="contentOption">
<a-radio-button value="both">{{ $t('log.both') }}</a-radio-button>
<a-radio-button value="source">{{ $t('log.source') }}</a-radio-button>
<a-radio-button value="target">{{ $t('log.translation') }}</a-radio-button>
</a-radio-group>
</div>
<div class="input-item">
<span class="input-label">{{ $t('log.copyNum') }}</span>
<a-radio-group v-model:value="copyNum">
<a-radio-button :value="0"><code>[:]</code></a-radio-button>
<a-radio-button :value="1"><code>[-1:]</code></a-radio-button>
<a-radio-button :value="2"><code>[-2:]</code></a-radio-button>
<a-radio-button :value="3"><code>[-3:]</code></a-radio-button>
</a-radio-group>
</div>
</template>
<a-button
style="margin-right: 20px;"
@click="copyCaptions"
>{{ $t('log.copy') }}</a-button>
</a-popover>
<a-button
danger
@click="clearCaptions"
>{{ $t('log.clear') }}</a-button>
</div> </div>
<a-table
:columns="columns"
:data-source="captionData"
v-model:pagination="pagination"
style="margin-top: 10px;"
>
<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>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -195,7 +193,7 @@ const columns = [
title: 'time', title: 'time',
dataIndex: 'time', dataIndex: 'time',
key: 'time', key: 'time',
width: 160, width: 150,
sorter: (a: CaptionItem, b: CaptionItem) => { sorter: (a: CaptionItem, b: CaptionItem) => {
if(a.time_s <= b.time_s) return -1 if(a.time_s <= b.time_s) return -1
return 1 return 1
@@ -300,7 +298,7 @@ function clearCaptions() {
display: inline-block; display: inline-block;
font-size: 24px; font-size: 24px;
font-weight: bold; font-weight: bold;
margin-bottom: 10px; margin: 10px 0;
} }
.base-time { .base-time {

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="caption-stat"> <div class="caption-stat">
<a-row> <a-row>
<a-col :span="6"> <a-col :span="5">
<a-statistic <a-statistic
:title="$t('status.engine')" :title="$t('status.engine')"
:value="customized?$t('status.customized'):engine" :value="customized?$t('status.customized'):engine"
@@ -36,7 +36,7 @@
</a-col> </a-col>
</a-row> </a-row>
</template> </template>
<a-col :span="6" @mouseenter="getEngineInfo" style="cursor: pointer;"> <a-col :span="5" @mouseenter="getEngineInfo" style="cursor: pointer;">
<a-statistic <a-statistic
:title="$t('status.status')" :title="$t('status.status')"
:value="engineEnabled?$t('status.started'):$t('status.stopped')" :value="engineEnabled?$t('status.started'):$t('status.stopped')"
@@ -47,10 +47,13 @@
</a-statistic> </a-statistic>
</a-col> </a-col>
</a-popover> </a-popover>
<a-col :span="6"> <a-col :span="5">
<a-statistic :title="$t('status.logNumber')" :value="captionData.length" /> <a-statistic :title="$t('status.logNumber')" :value="captionData.length" />
</a-col> </a-col>
<a-col :span="6"> <a-col :span="5">
<a-statistic :title="$t('status.logNumber2')" :value="softwareLogs.length" />
</a-col>
<a-col :span="4">
<div class="about-tag">{{ $t('status.aboutProj') }}</div> <div class="about-tag">{{ $t('status.aboutProj') }}</div>
<GithubOutlined class="proj-info" @click="showAbout = true"/> <GithubOutlined class="proj-info" @click="showAbout = true"/>
</a-col> </a-col>
@@ -128,14 +131,17 @@ import { EngineInfo } from '@renderer/types'
import { ref, watch } from 'vue' import { ref, watch } from 'vue'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { useCaptionLogStore } from '@renderer/stores/captionLog' import { useCaptionLogStore } from '@renderer/stores/captionLog'
import { useSoftwareLogStore } from '@renderer/stores/softwareLog'
import { useEngineControlStore } from '@renderer/stores/engineControl' import { useEngineControlStore } from '@renderer/stores/engineControl'
import { GithubOutlined, InfoCircleOutlined } from '@ant-design/icons-vue'; import { GithubOutlined, InfoCircleOutlined } from '@ant-design/icons-vue'
const showAbout = ref(false) const showAbout = ref(false)
const pending = ref(false) const pending = ref(false)
const captionLog = useCaptionLogStore() const captionLog = useCaptionLogStore()
const { captionData } = storeToRefs(captionLog) const { captionData } = storeToRefs(captionLog)
const softwareLog = useSoftwareLogStore()
const { softwareLogs } = storeToRefs(softwareLog)
const engineControl = useEngineControlStore() const engineControl = useEngineControlStore()
const { engineEnabled, engine, customized, errorSignal } = storeToRefs(engineControl) const { engineEnabled, engine, customized, errorSignal } = storeToRefs(engineControl)

View File

@@ -33,10 +33,11 @@
<a-radio-group v-model:value="uiColor"> <a-radio-group v-model:value="uiColor">
<template v-for="color in colorList" :key="color"> <template v-for="color in colorList" :key="color">
<a-radio-button :value="color" <a-radio-button :value="color"
:style="{ :style="{backgroundColor: color}"
backgroundColor: color >
}" <CheckOutlined style="color: white;" v-if="color === uiColor" />
>&nbsp;</a-radio-button> <span v-else>&nbsp;</span>
</a-radio-button>
</template> </template>
</a-radio-group> </a-radio-group>
</div> </div>
@@ -55,7 +56,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { useGeneralSettingStore } from '@renderer/stores/generalSetting' import { useGeneralSettingStore } from '@renderer/stores/generalSetting'
import { InfoCircleOutlined } from '@ant-design/icons-vue'; import { InfoCircleOutlined, CheckOutlined } from '@ant-design/icons-vue'
const colorList = [ const colorList = [
'#1677ff', '#1677ff',

View File

@@ -0,0 +1,111 @@
<template>
<div>
<div class="log-title">
<span style="margin-right: 30px;">{{ $t('log.title2') }}</span>
</div>
<a-button
danger
@click="softwareLog.clear()"
>{{ $t('log.clear') }}</a-button>
</div>
<a-table
:columns="columns"
:data-source="softwareLogs"
v-model:pagination="pagination"
style="margin-top: 10px;"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'type'">
<span :class="record.type">{{ record.type }}</span>
</template>
<template v-if="column.key === 'index'">
{{ record.index }}
</template>
<template v-if="column.key === 'content'">
<code>{{ record.text }}</code>
</template>
</template>
</a-table>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useSoftwareLogStore } from '@renderer/stores/softwareLog'
import { type SoftwareLogItem } from '../types'
const softwareLog = useSoftwareLogStore()
const { softwareLogs } = storeToRefs(softwareLog)
const pagination = ref({
current: 1,
pageSize: 20,
showSizeChanger: true,
pageSizeOptions: ['10', '20', '50', '100'],
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: 'index',
dataIndex: 'index',
key: 'index',
width: 80,
sorter: (a: SoftwareLogItem, b: SoftwareLogItem) => {
if(a.index <= b.index) return -1
return 1
},
sortDirections: ['descend'],
defaultSortOrder: 'descend',
},
{
title: 'type',
dataIndex: 'type',
key: 'type',
width: 80,
sorter: (a: SoftwareLogItem, b: SoftwareLogItem) => {
if(a.type <= b.type) return -1
return 1
},
},
{
title: 'time',
dataIndex: 'time',
key: 'time',
width: 120,
sortDirections: ['descend'],
},
{
title: 'content',
dataIndex: 'content',
key: 'content',
},
]
</script>
<style scoped>
.log-title {
display: inline-block;
font-size: 24px;
font-weight: bold;
margin: 10px 0;
}
.WARN {
color: #ff7c05;
font-weight: bold;
}
.ERROR {
color: #ff0000;
font-weight: bold;
}
</style>

View File

@@ -0,0 +1,41 @@
import { h } from 'vue';
import { OrderedListOutlined, FileTextOutlined } from '@ant-design/icons-vue'
export const logMenu = {
zh: [
{
key: 'captionLog',
icon: () => h(OrderedListOutlined),
label: '字幕记录',
},
{
key: 'projLog',
icon: () => h(FileTextOutlined),
label: '日志记录',
},
],
en: [
{
key: 'captionLog',
icon: () => h(OrderedListOutlined),
label: 'Caption Log',
},
{
key: 'projLog',
icon: () => h(FileTextOutlined),
label: 'Software Log',
},
],
ja: [
{
key: 'captionLog',
icon: () => h(OrderedListOutlined),
label: '字幕記録',
},
{
key: 'projLog',
icon: () => h(FileTextOutlined),
label: 'ログ記録',
},
]
}

View File

@@ -18,3 +18,4 @@ export * from './config/engine'
export * from './config/audio' export * from './config/audio'
export * from './config/theme' export * from './config/theme'
export * from './config/linebreak' export * from './config/linebreak'
export * from './config/logMenu'

View File

@@ -106,6 +106,7 @@ export default {
"started": "Started", "started": "Started",
"stopped": "Not Started", "stopped": "Not Started",
"logNumber": "Caption Count", "logNumber": "Caption Count",
"logNumber2": "Log Count",
"aboutProj": "About Project", "aboutProj": "About Project",
"openCaption": "Open Caption Window", "openCaption": "Open Caption Window",
"startEngine": "Start Caption Engine", "startEngine": "Start Caption Engine",
@@ -146,6 +147,7 @@ export default {
"copyNum": "Copy Count", "copyNum": "Copy Count",
"all": "All", "all": "All",
"copySuccess": "Subtitle copied to clipboard", "copySuccess": "Subtitle copied to clipboard",
"clear": "Clear Log" "clear": "Clear Log",
"title2": "Software Log"
} }
} }

View File

@@ -106,6 +106,7 @@ export default {
"started": "開始済み", "started": "開始済み",
"stopped": "未開始", "stopped": "未開始",
"logNumber": "字幕数", "logNumber": "字幕数",
"logNumber2": "ログ数",
"aboutProj": "プロジェクト情報", "aboutProj": "プロジェクト情報",
"openCaption": "字幕ウィンドウを開く", "openCaption": "字幕ウィンドウを開く",
"startEngine": "字幕エンジンを開始", "startEngine": "字幕エンジンを開始",
@@ -124,7 +125,7 @@ export default {
} }
}, },
log: { log: {
"title": "字幕ログ", "title": "字幕記録",
"changeTime": "時間を変更", "changeTime": "時間を変更",
"baseTime": "最初の字幕開始時間", "baseTime": "最初の字幕開始時間",
"hour": "時", "hour": "時",
@@ -132,7 +133,7 @@ export default {
"sec": "秒", "sec": "秒",
"ms": "ミリ秒", "ms": "ミリ秒",
"export": "エクスポート", "export": "エクスポート",
"copy": "ログをコピー", "copy": "記録をコピー",
"exportOptions": "エクスポートオプション", "exportOptions": "エクスポートオプション",
"exportFormat": "形式", "exportFormat": "形式",
"exportContent": "内容", "exportContent": "内容",
@@ -146,6 +147,7 @@ export default {
"copyNum": "コピー数", "copyNum": "コピー数",
"all": "すべて", "all": "すべて",
"copySuccess": "字幕がクリップボードにコピーされました", "copySuccess": "字幕がクリップボードにコピーされました",
"clear": "ログをクリア" "clear": "記録をクリア",
"title2": "ログ記録"
} }
} }

View File

@@ -106,6 +106,7 @@ export default {
"started": "已启动", "started": "已启动",
"stopped": "未启动", "stopped": "未启动",
"logNumber": "字幕数量", "logNumber": "字幕数量",
"logNumber2": "日志数量",
"aboutProj": "项目关于", "aboutProj": "项目关于",
"openCaption": "打开字幕窗口", "openCaption": "打开字幕窗口",
"startEngine": "启动字幕引擎", "startEngine": "启动字幕引擎",
@@ -146,6 +147,7 @@ export default {
"copyNum": "复制数量", "copyNum": "复制数量",
"all": "全部", "all": "全部",
"copySuccess": "字幕已复制到剪贴板", "copySuccess": "字幕已复制到剪贴板",
"clear": "清空记录" "clear": "清空记录",
"title2": "日志记录"
} }
} }

View File

@@ -0,0 +1,21 @@
import { ref } from 'vue'
import { defineStore } from 'pinia'
import { type SoftwareLogItem } from '../types'
export const useSoftwareLogStore = defineStore('softwareLog', () => {
const softwareLogs = ref<SoftwareLogItem[]>([])
function clear() {
softwareLogs.value = []
}
window.electron.ipcRenderer.on('control.softwareLog.add', (_, log) => {
softwareLogs.value.push(log)
console.log(log)
})
return {
softwareLogs,
clear
}
})

View File

@@ -45,6 +45,13 @@ export interface CaptionItem {
translation: string translation: string
} }
export interface SoftwareLogItem {
type: "INFO" | "WARN" | "ERROR",
index: number,
time: string,
text: string
}
export interface FullConfig { export interface FullConfig {
platform: string, platform: string,
uiLanguage: UILanguage, uiLanguage: UILanguage,
@@ -53,7 +60,8 @@ export interface FullConfig {
leftBarWidth: number, leftBarWidth: number,
styles: Styles, styles: Styles,
controls: Controls, controls: Controls,
captionLog: CaptionItem[] captionLog: CaptionItem[],
softwareLog: SoftwareLogItem[]
} }
export interface EngineInfo { export interface EngineInfo {

View File

@@ -11,7 +11,13 @@
<a-col :span="24 - leftBarWidth"> <a-col :span="24 - leftBarWidth">
<div class="caption-data"> <div class="caption-data">
<EngineStatus /> <EngineStatus />
<CaptionLog /> <div class="log-container">
<a-menu v-model:selectedKeys="current" mode="horizontal" :items="items" />
<div style="padding: 16px;">
<CaptionLog v-if="current[0] === 'captionLog'" />
<SoftwareLog v-else />
</div>
</div>
</div> </div>
</a-col> </a-col>
</a-row> </a-row>
@@ -24,11 +30,22 @@ import CaptionStyle from '../components/CaptionStyle.vue'
import EngineControl from '../components/EngineControl.vue' import EngineControl from '../components/EngineControl.vue'
import EngineStatus from '@renderer/components/EngineStatus.vue' import EngineStatus from '@renderer/components/EngineStatus.vue'
import CaptionLog from '../components/CaptionLog.vue' import CaptionLog from '../components/CaptionLog.vue'
import SoftwareLog from '@renderer/components/SoftwareLog.vue'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { useGeneralSettingStore } from '@renderer/stores/generalSetting' import { useGeneralSettingStore } from '@renderer/stores/generalSetting'
import { ref, watch } from 'vue'
import { MenuProps } from 'ant-design-vue'
import { logMenu } from '@renderer/i18n'
const generalSettingStore = useGeneralSettingStore() const generalSettingStore = useGeneralSettingStore()
const { leftBarWidth, antdTheme } = storeToRefs(generalSettingStore) const { leftBarWidth, antdTheme, uiLanguage } = storeToRefs(generalSettingStore)
const current = ref<string[]>(['captionLog'])
const items = ref<MenuProps['items']>(logMenu[uiLanguage.value])
watch(uiLanguage, (val) => {
items.value = logMenu[val]
})
</script> </script>
<style scoped> <style scoped>
@@ -53,4 +70,10 @@ const { leftBarWidth, antdTheme } = storeToRefs(generalSettingStore)
.caption-data::-webkit-scrollbar { .caption-data::-webkit-scrollbar {
display: none; display: none;
} }
.log-container {
padding: 20px 10px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
</style> </style>