diff --git a/src/api/index.ts b/src/api/index.ts index 5a1bc41c..97840b0b 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -3,6 +3,7 @@ import router from '@/router' import { useAuthStore } from '@/stores' import { initializeRequestOptimizer } from '@/utils/requestOptimizer' import { useGlobalOfflineStatus } from '@/composables/useOfflineStatus' +import { getCurrentLocale } from '@/plugins/i18n' // 创建axios实例 const api = axios.create({ @@ -34,6 +35,9 @@ api.interceptors.request.use(config => { if (authStore.token) { config.headers.Authorization = `Bearer ${authStore.token}` } + const locale = getCurrentLocale() + config.headers['X-MoviePilot-Locale'] = locale + config.headers['Accept-Language'] = locale return config }) @@ -51,12 +55,33 @@ function resolveConnectionFailureReason(error: AxiosError): 'network-error' | 't return null } +interface LocalizedApiPayload { + detail?: unknown + detail_i18n?: unknown + message?: unknown + message_i18n?: unknown +} + +/** 前端展示默认使用后端提供的多语言消息,同时不改变后端接口兼容字段。 */ +function normalizeLocalizedMessage(payload: any): any { + if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return payload + + const localizedPayload = payload as LocalizedApiPayload + if (typeof localizedPayload.message_i18n === 'string' && localizedPayload.message_i18n) { + localizedPayload.message = localizedPayload.message_i18n + } + if (typeof localizedPayload.detail_i18n === 'string' && localizedPayload.detail_i18n) { + localizedPayload.detail = localizedPayload.detail_i18n + } + return payload +} + // 添加响应拦截器 api.interceptors.response.use( response => { // 任意 API 成功响应都可以证明 MoviePilot 服务当前可达。 globalOfflineStatus.markServerOnline() - return response.data + return normalizeLocalizedMessage(response.data) }, (error: AxiosError) => { if (!error.response) { @@ -81,12 +106,15 @@ api.interceptors.response.use( // 其他网络错误 return Promise.reject(new Error(error.message || 'Network error')) } else if (error.response.status === 403) { + normalizeLocalizedMessage(error.response.data) // 认证 Store const authStore = useAuthStore() // 清除登录状态信息 authStore.logout() // token验证失败,跳转到登录页面 router.push('/login') + } else { + normalizeLocalizedMessage(error.response.data) } return Promise.reject(error) diff --git a/src/api/types.ts b/src/api/types.ts index 03d6ecf4..4f973f77 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -1072,14 +1072,20 @@ export interface ScheduleProgress { id?: string // 名称 name?: string + // 多语言名称 + name_i18n?: string // 提供者 provider?: string + // 多语言提供者 + provider_i18n?: string // 是否正在执行 enable?: boolean // 当前完成百分比 value?: number // 当前进度文本 text?: string + // 多语言进度文本 + text_i18n?: string // 执行状态 status?: string // 最近一次执行是否成功 @@ -1090,6 +1096,8 @@ export interface ScheduleProgress { finished_at?: string // 最近一次错误信息 error?: string + // 多语言错误信息 + error_i18n?: string // 扩展数据 data?: Record } @@ -1100,16 +1108,26 @@ export interface ScheduleInfo { id: string // 名称 name: string + // 多语言名称 + name_i18n?: string // 提供者 provider: string + // 多语言提供者 + provider_i18n?: string // 状态 status: string + // 多语言状态 + status_i18n?: string // 下次运行时间 next_run: string + // 多语言下次运行时间 + next_run_i18n?: string // 当前完成百分比 progress?: number // 进度文本 progress_text?: string + // 多语言进度文本 + progress_text_i18n?: string // 是否正在更新进度 progress_enable?: boolean // 进度详情 @@ -1719,6 +1737,7 @@ export interface SubscribeShareStatistics { export interface ApiResponse { success: boolean message?: string + message_i18n?: string data: T } diff --git a/src/components/agent/AgentAssistantPanel.vue b/src/components/agent/AgentAssistantPanel.vue index 32ffaf5e..d8195e09 100644 --- a/src/components/agent/AgentAssistantPanel.vue +++ b/src/components/agent/AgentAssistantPanel.vue @@ -4,6 +4,7 @@ import mdLinkAttributes from 'markdown-it-link-attributes' import { useDisplay } from 'vuetify' import { useI18n } from 'vue-i18n' import { useAuthStore, useUserStore } from '@/stores' +import { getCurrentLocale } from '@/plugins/i18n' type AgentMessageRole = 'user' | 'assistant' type AgentMessageStatus = 'idle' | 'streaming' | 'done' | 'error' @@ -88,6 +89,7 @@ interface AgentStreamEvent { choice?: Omit content?: string message?: string + message_i18n?: string message_id?: string target_message?: Partial & { id?: string } session_id?: string @@ -622,19 +624,16 @@ function dedupeHistorySessions(sessions: AgentSessionHistoryItem[]) { // 调用智能助手接口,并统一处理鉴权和标准响应格式。 async function fetchAgentApi(path: string, options: RequestInit = {}) { - const headers = new Headers(options.headers || {}) - if (authStore.token) headers.set('Authorization', `Bearer ${authStore.token}`) - const response = await fetch(resolveApiUrl(path), { ...options, - headers, + headers: buildAgentRequestHeaders(options.headers), credentials: 'include', }) - if (!response.ok) throw new Error(`${response.status} ${response.statusText}`.trim()) + if (!response.ok) throw new Error(await resolveAgentResponseErrorMessage(response)) const result = await response.json() - if (!result?.success) throw new Error(result?.message || t('agentAssistant.error')) + if (!result?.success) throw new Error(result?.message_i18n || result?.message || t('agentAssistant.error')) return result.data } @@ -981,6 +980,31 @@ function resolveApiUrl(path: string) { return `${baseUrl.replace(/\/?$/, '/')}${path.replace(/^\//, '')}` } +// 构造智能助手 fetch 请求头,补齐 Axios 拦截器无法覆盖的鉴权和语言信息。 +function buildAgentRequestHeaders(headers?: HeadersInit) { + const requestHeaders = new Headers(headers || {}) + const locale = getCurrentLocale() + + if (authStore.token) requestHeaders.set('Authorization', `Bearer ${authStore.token}`) + requestHeaders.set('X-MoviePilot-Locale', locale) + requestHeaders.set('Accept-Language', locale) + + return requestHeaders +} + +// 解析智能助手 fetch 失败响应,优先使用后端返回的本地化错误文本。 +async function resolveAgentResponseErrorMessage(response: Response) { + try { + const payload = await response.clone().json() + const message = payload?.detail_i18n || payload?.message_i18n || payload?.detail || payload?.message + if (typeof message === 'string' && message) return message + } catch { + // 非 JSON 错误响应保留 HTTP 状态文本,避免吞掉原始错误。 + } + + return `${response.status} ${response.statusText}`.trim() +} + // 消息主列表使用原生滚动,避免流式回复时 JS 滚动库频繁测量影响手感。 function getMessageScrollerElement() { return messageListRef.value @@ -1194,7 +1218,7 @@ function applyStreamEvent(event: AgentStreamEvent, assistantMessage: AgentChatMe case 'error': assistantMessage.status = 'error' // 后端流式错误已经以 AI 消息展示,避免底部提示条重复且持续占位。 - assistantMessage.content ||= event.message || t('agentAssistant.error') + assistantMessage.content ||= event.message_i18n || event.message || t('agentAssistant.error') emit('assistant-preview', assistantMessage.content) markToolsDone(assistantMessage) break @@ -1370,17 +1394,15 @@ async function uploadAgentAttachment(file: File) { const response = await fetch(resolveApiUrl('message/agent/upload'), { method: 'POST', - headers: { - ...(authStore.token ? { Authorization: `Bearer ${authStore.token}` } : {}), - }, + headers: buildAgentRequestHeaders(), body: formData, credentials: 'include', }) - if (!response.ok) throw new Error(`${response.status} ${response.statusText}`.trim()) + if (!response.ok) throw new Error(await resolveAgentResponseErrorMessage(response)) const result = await response.json() - if (!result?.success) throw new Error(result?.message || t('agentAssistant.uploadFailed')) + if (!result?.success) throw new Error(result?.message_i18n || result?.message || t('agentAssistant.uploadFailed')) return result.data as AgentMessageAttachment & AgentOutgoingFile } @@ -1452,10 +1474,9 @@ async function streamAgentMessage( try { const response = await fetch(resolveApiUrl('message/agent/stream'), { method: 'POST', - headers: { + headers: buildAgentRequestHeaders({ 'Content-Type': 'application/json', - ...(authStore.token ? { Authorization: `Bearer ${authStore.token}` } : {}), - }, + }), body: JSON.stringify({ text: content, display_text: displayContent || content, @@ -1473,7 +1494,7 @@ async function streamAgentMessage( }) if (!response.ok) { - throw new Error(`${response.status} ${response.statusText}`.trim()) + throw new Error(await resolveAgentResponseErrorMessage(response)) } await readAgentStream(response, assistantMessage) @@ -1698,10 +1719,9 @@ async function handleChoiceClick(message: AgentChatMessage, choice: AgentChoiceC try { const response = await fetch(resolveApiUrl('message/agent/callback'), { method: 'POST', - headers: { + headers: buildAgentRequestHeaders({ 'Content-Type': 'application/json', - ...(authStore.token ? { Authorization: `Bearer ${authStore.token}` } : {}), - }, + }), body: JSON.stringify({ session_id: sessionId.value, callback_data: button.callback_data, @@ -1711,10 +1731,10 @@ async function handleChoiceClick(message: AgentChatMessage, choice: AgentChoiceC credentials: 'include', }) - if (!response.ok) throw new Error(`${response.status} ${response.statusText}`.trim()) + if (!response.ok) throw new Error(await resolveAgentResponseErrorMessage(response)) const result = await response.json() - if (!result?.success) throw new Error(result?.message || t('agentAssistant.choiceExpired')) + if (!result?.success) throw new Error(result?.message_i18n || result?.message || t('agentAssistant.choiceExpired')) const agentMessage = String(result.data?.message || '') if (result.data?.traditional) { diff --git a/src/components/dialog/ReorganizeDialog.vue b/src/components/dialog/ReorganizeDialog.vue index a8769fa7..c4593665 100644 --- a/src/components/dialog/ReorganizeDialog.vue +++ b/src/components/dialog/ReorganizeDialog.vue @@ -1019,6 +1019,19 @@ function mergePreviewData(target: ManualTransferPreviewData, incoming?: ManualTr } } +// 从标准响应中提取可展示的整理预览数据,优先保留顶层本地化消息。 +function resolvePreviewResponseData(result: ApiResponse) { + if (!result.data) return result.data + + const message = result.message_i18n || result.message || result.data.message + if (!message || message === result.data.message) return result.data + + return { + ...result.data, + message, + } +} + // 预览整理结果 async function previewTransfer() { if (!props.logids && !normalizedItems.value.length) return @@ -1046,7 +1059,7 @@ async function previewTransfer() { }), ) } else { - mergePreviewData(mergedPreviewData, result.data) + mergePreviewData(mergedPreviewData, resolvePreviewResponseData(result)) } } catch (err: any) { console.warn(`预览请求异常: ${err?.message}`) @@ -1078,7 +1091,7 @@ async function previewTransfer() { return } - mergePreviewData(mergedPreviewData, result.data) + mergePreviewData(mergedPreviewData, resolvePreviewResponseData(result)) } catch (err: any) { console.warn(`预览请求异常: ${err?.message}`) mergePreviewData( @@ -1114,7 +1127,7 @@ async function previewTransfer() { return } - mergePreviewData(mergedPreviewData, result.data) + mergePreviewData(mergedPreviewData, resolvePreviewResponseData(result)) } catch (err: any) { console.warn(`预览请求异常: ${err?.message}`) mergePreviewData( @@ -1196,7 +1209,7 @@ async function handleTransferLog(logid: number, background: boolean = false) { function handleProgressMessage(event: MessageEvent) { const progress = JSON.parse(event.data) if (progress) { - progressText.value = progress.text + progressText.value = progress.text_i18n || progress.text progressValue.value = progress.value } } diff --git a/src/components/filebrowser/FileList.vue b/src/components/filebrowser/FileList.vue index faf890ff..8b4c420b 100644 --- a/src/components/filebrowser/FileList.vue +++ b/src/components/filebrowser/FileList.vue @@ -728,7 +728,7 @@ async function batchScrape() { function handleProgressMessage(event: MessageEvent) { const progress = JSON.parse(event.data) if (progress) { - progressText.value = progress.text + progressText.value = progress.text_i18n || progress.text progressValue.value = progress.value progressDialogController?.updateProps({ text: progressText.value, value: progressValue.value }) } diff --git a/src/composables/useBackground.ts b/src/composables/useBackground.ts index eb1dcf81..10c1067f 100644 --- a/src/composables/useBackground.ts +++ b/src/composables/useBackground.ts @@ -1,11 +1,26 @@ import { getCurrentInstance, onMounted, onUnmounted, ref, type Ref } from 'vue' import { sseManagerSingleton, type SSEManagerOptions } from '@/utils/sseManager' import { addBackgroundTimer, removeBackgroundTimer } from '@/utils/backgroundManager' +import { getCurrentLocale } from '@/plugins/i18n' type UseSSEOptions = Partial & { connectDelay?: number } +/** 为 SSE 请求补充当前前端语言,弥补 EventSource 不能设置自定义请求头的问题。 */ +function appendLocaleParam(url: string) { + const locale = getCurrentLocale() + + try { + const parsedUrl = new URL(url, window.location.origin) + parsedUrl.searchParams.set('locale', locale) + return parsedUrl.toString() + } catch { + const separator = url.includes('?') ? '&' : '?' + return `${url}${separator}locale=${encodeURIComponent(locale)}` + } +} + /** * 后台任务组合函数 * 统一管理SSE连接和定时器,减少后台常驻活动。 @@ -180,12 +195,15 @@ export function useBackground() { listenerId: string, isActive: Ref, ) => { - const getManager = () => - sseManagerSingleton.getIndependentManager(url, listenerId, { + let managerUrl = '' + const getManager = () => { + managerUrl = appendLocaleParam(url) + return sseManagerSingleton.getIndependentManager(managerUrl, listenerId, { backgroundCloseDelay: 1000, // 进度SSE更快关闭 reconnectDelay: 1000, maxReconnectAttempts: 5, }) + } let manager: ReturnType | null = null let isListening = false @@ -207,8 +225,9 @@ export function useBackground() { manager.removeMessageListener(listenerId) if (destroyManager) { - sseManagerSingleton.closeIndependentManager(url, listenerId) + sseManagerSingleton.closeIndependentManager(managerUrl || appendLocaleParam(url), listenerId) manager = null + managerUrl = '' } isListening = false diff --git a/src/composables/useScheduleProgress.ts b/src/composables/useScheduleProgress.ts index 52908af0..f12bac27 100644 --- a/src/composables/useScheduleProgress.ts +++ b/src/composables/useScheduleProgress.ts @@ -4,17 +4,44 @@ import { useBackground } from '@/composables/useBackground' import type { Ref } from 'vue' const SCHEDULE_PROGRESS_REFRESH_INTERVAL = 1000 +const SCHEDULE_RUNNING_STATUS = '正在运行' +const SCHEDULE_WAITING_STATUS = '等待' /** 判断定时服务是否仍在运行,兼容列表状态与进度详情两种后端信号。 */ export function isScheduleRunning(schedule: ScheduleInfo) { return ( - schedule.status === '正在运行' || + schedule.status === SCHEDULE_RUNNING_STATUS || schedule.progress_enable === true || schedule.progress_detail?.enable === true || schedule.progress_detail?.status === 'running' ) } +/** 判断定时服务是否处于等待执行状态。 */ +export function isScheduleWaiting(schedule: ScheduleInfo) { + return schedule.status === SCHEDULE_WAITING_STATUS +} + +/** 获取定时服务展示名称,优先使用后端按当前语言生成的文本。 */ +export function getScheduleName(schedule: ScheduleInfo) { + return schedule.name_i18n || schedule.name || '' +} + +/** 获取定时服务提供者展示名称,优先使用后端按当前语言生成的文本。 */ +export function getScheduleProvider(schedule: ScheduleInfo) { + return schedule.provider_i18n || schedule.provider || '' +} + +/** 获取定时服务状态展示文本,优先使用后端按当前语言生成的文本。 */ +export function getScheduleStatusText(schedule: ScheduleInfo) { + return schedule.status_i18n || schedule.status || '' +} + +/** 获取定时服务下次运行展示文本,优先使用后端按当前语言生成的文本。 */ +export function getScheduleNextRunText(schedule: ScheduleInfo) { + return schedule.next_run_i18n || schedule.next_run || '' +} + /** 为定时服务列表提供仅针对运行中任务的实时进度轮询。 */ export function useScheduleProgress(schedules: Ref, refreshId: string) { const { useDataRefresh } = useBackground() @@ -72,7 +99,9 @@ export function useScheduleProgress(schedules: Ref, refreshId: s /** 获取任务当前应展示的进度说明。 */ function getScheduleProgressText(schedule: ScheduleInfo) { - return progressById.value[schedule.id]?.text || schedule.progress_text || '' + const progress = progressById.value[schedule.id] + + return progress?.text_i18n || progress?.text || schedule.progress_text_i18n || schedule.progress_text || '' } useDataRefresh(refreshId, refreshRunningProgress, SCHEDULE_PROGRESS_REFRESH_INTERVAL, true) diff --git a/src/locales/en-US.ts b/src/locales/en-US.ts index 339cffcd..9a26409b 100644 --- a/src/locales/en-US.ts +++ b/src/locales/en-US.ts @@ -1528,6 +1528,7 @@ export default { preparing: 'Preparing...', totalModules: 'Total Modules', recheck: 'Recheck', + requestFailed: 'Network request failed', }, nameTest: { recognize: 'Recognize', diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts index 89b5dd9e..4f073287 100644 --- a/src/locales/zh-CN.ts +++ b/src/locales/zh-CN.ts @@ -1521,6 +1521,7 @@ export default { preparing: '准备检查...', totalModules: '总模块数', recheck: '重新检查', + requestFailed: '网络请求失败', }, nameTest: { recognize: '识别', diff --git a/src/locales/zh-TW.ts b/src/locales/zh-TW.ts index adde00e8..d0c66907 100644 --- a/src/locales/zh-TW.ts +++ b/src/locales/zh-TW.ts @@ -1520,6 +1520,7 @@ export default { preparing: '準備檢查...', totalModules: '總模組數', recheck: '重新檢查', + requestFailed: '網路請求失敗', }, nameTest: { recognize: '識別', diff --git a/src/pages/resource.vue b/src/pages/resource.vue index 33f6ac96..4c41fab3 100644 --- a/src/pages/resource.vue +++ b/src/pages/resource.vue @@ -19,6 +19,7 @@ import { useToast } from 'vue-toastification' import { useKeepAliveRefresh } from '@/composables/useKeepAliveRefresh' import { useUserStore } from '@/stores' import { buildUserPermissionContext, hasPermission } from '@/utils/permission' +import { getCurrentLocale } from '@/plugins/i18n' // 国际化 const { t } = useI18n() @@ -575,6 +576,7 @@ function buildSearchStreamUrl(params: SearchParams, requestToken?: string) { if (requestToken) { setSearchParam(url.searchParams, '_ts', requestToken) } + setSearchParam(url.searchParams, 'locale', getCurrentLocale()) return url.toString() } @@ -605,8 +607,9 @@ function hasLoadedEmptySearchResult() { // 更新搜索进度 function updateSearchProgress(eventData: { [key: string]: any }, flushNow: boolean = false) { - if (eventData.text) { - pendingProgressText = eventData.text + const text = eventData.text_i18n || eventData.text + if (text) { + pendingProgressText = text } if (typeof eventData.value === 'number') { pendingProgressValue = eventData.value @@ -707,7 +710,7 @@ function getSubtitleItemKey(item: SubtitleInfo, index: number) { function handleSearchStreamMessage(eventData: { [key: string]: any }) { if (eventData.type === 'error') { updateSearchProgress(eventData, true) - errorDescription.value = eventData.message || t('resource.noResourceFound') + errorDescription.value = eventData.message_i18n || eventData.message || t('resource.noResourceFound') return } @@ -1116,7 +1119,14 @@ async function pollAiRecommend() { console.error('AI功能未启用') } else { // 错误情况(status === 'error' 或 success 为 false) - const errMsg = result.message || data?.error || data?.message || 'Unknown error' + const errMsg = + result.message_i18n || + result.message || + data?.error_i18n || + data?.error || + data?.message_i18n || + data?.message || + 'Unknown error' console.error('智能推荐错误:', errMsg) toast.error(`${t('resource.aiRecommendError')}: ${errMsg}`) } diff --git a/src/views/dashboard/AnalyticsScheduler.vue b/src/views/dashboard/AnalyticsScheduler.vue index d9a11386..9d981966 100644 --- a/src/views/dashboard/AnalyticsScheduler.vue +++ b/src/views/dashboard/AnalyticsScheduler.vue @@ -3,7 +3,14 @@ import api from '@/api' import type { ScheduleInfo, TransferQueue } from '@/api/types' import { useI18n } from 'vue-i18n' import { useBackground } from '@/composables/useBackground' -import { isScheduleRunning, useScheduleProgress } from '@/composables/useScheduleProgress' +import { + getScheduleName, + getScheduleNextRunText, + getScheduleProvider, + getScheduleStatusText, + isScheduleRunning, + useScheduleProgress, +} from '@/composables/useScheduleProgress' import { getSchedulerVisual } from '@/utils/schedulerVisual' // 国际化 @@ -47,9 +54,14 @@ const backgroundTasks = computed(() => { return { id: `schedule-${item.id}`, - title: item.name || t('dashboard.scheduler'), - subtitle: (isRunning && getScheduleProgressText(item)) || item.provider || item.next_run || '', - status: isRunning ? t('dashboard.taskRunning') : item.status || t('dashboard.taskWaiting'), + title: getScheduleName(item) || t('dashboard.scheduler'), + subtitle: + (isRunning && getScheduleProgressText(item)) || + getScheduleProvider(item) || + getScheduleNextRunText(item), + status: isRunning + ? t('dashboard.taskRunning') + : getScheduleStatusText(item) || t('dashboard.taskWaiting'), icon: visual.icon, color: visual.color, progress: isRunning ? getScheduleProgressValue(item) : undefined, diff --git a/src/views/reorganize/TransferHistoryView.vue b/src/views/reorganize/TransferHistoryView.vue index 18687bba..00128c19 100644 --- a/src/views/reorganize/TransferHistoryView.vue +++ b/src/views/reorganize/TransferHistoryView.vue @@ -799,11 +799,11 @@ async function handleAiRedoProgressMessage(event: MessageEvent) { const progress = JSON.parse(event.data) if (!progress) return - aiRedoProgressText.value = progress.text || t('transferHistory.actions.aiRedoPending') + aiRedoProgressText.value = progress.text_i18n || progress.text || t('transferHistory.actions.aiRedoPending') aiRedoProgressDialogController?.updateProps({ text: aiRedoProgressText.value }) if (progress.enable === false) { - await finishAiRedo(progress.data?.success !== false, progress.data?.error) + await finishAiRedo(progress.data?.success !== false, progress.data?.error_i18n || progress.data?.error) } } diff --git a/src/views/system/ModuleTestView.vue b/src/views/system/ModuleTestView.vue index 53a48c20..e9595350 100644 --- a/src/views/system/ModuleTestView.vue +++ b/src/views/system/ModuleTestView.vue @@ -9,6 +9,19 @@ const { t } = useI18n() // 主题 const theme = useTheme() +interface ModuleListItem { + id: string + name: string + name_i18n?: string + name_key?: string +} + +interface ModuleTestResponse { + success: boolean + message?: string + message_i18n?: string +} + // 定义所有的模块ID、名称列表 const modules = ref< { @@ -38,9 +51,9 @@ async function getModules() { const moduleList = result.data?.modules if (moduleList) { // 初始化模块列表 - modules.value = moduleList.map((module: { id: string; name: string }, index: number) => ({ + modules.value = moduleList.map((module: ModuleListItem, index: number) => ({ id: module.id, - name: module.name, + name: module.name_i18n || module.name, state: undefined, errmsg: '', loading: false, @@ -91,7 +104,7 @@ async function moduleTest(index: number) { const moduleid = target.id target.loading = true - const result: { [key: string]: any } = await api.get(`system/moduletest/${moduleid}`) + const result = (await api.get(`system/moduletest/${moduleid}`)) as ModuleTestResponse target.loading = false if (result.success) { @@ -103,14 +116,14 @@ async function moduleTest(index: number) { } else { target.state = 'error' target.name = `${target.name} - ${t('moduleTest.error')}!` - target.errmsg = result.message + target.errmsg = result.message_i18n || result.message || '' } } catch (error) { console.error(error) const target = modules.value[index] target.loading = false target.state = 'error' - target.errmsg = '网络请求失败' + target.errmsg = t('moduleTest.requestFailed') } } diff --git a/src/views/system/ServiceView.vue b/src/views/system/ServiceView.vue index d46531f4..101f6da3 100644 --- a/src/views/system/ServiceView.vue +++ b/src/views/system/ServiceView.vue @@ -4,7 +4,15 @@ import api from '@/api' import type { ScheduleInfo } from '@/api/types' import { useI18n } from 'vue-i18n' import { useBackground } from '@/composables/useBackground' -import { isScheduleRunning, useScheduleProgress } from '@/composables/useScheduleProgress' +import { + getScheduleName, + getScheduleNextRunText, + getScheduleProvider, + getScheduleStatusText, + isScheduleRunning, + isScheduleWaiting, + useScheduleProgress, +} from '@/composables/useScheduleProgress' import { getSchedulerVisual } from '@/utils/schedulerVisual' // 国际化 @@ -33,31 +41,17 @@ async function loadSchedulerList() { } /** 根据任务状态返回桌面端状态标签颜色。 */ -function getSchedulerColor(status: string) { - switch (status) { - case t('setting.scheduler.running'): - return 'success' - case t('setting.scheduler.stopped'): - return 'error' - case t('setting.scheduler.waiting'): - return '' - default: - return '' - } +function getSchedulerColor(scheduler: ScheduleInfo) { + if (isScheduleRunning(scheduler)) return 'success' + if (isScheduleWaiting(scheduler)) return '' + return '' } /** 根据任务状态返回移动端状态胶囊的语义样式。 */ -function getSchedulerStatusVariant(status: string) { - switch (status) { - case t('setting.scheduler.running'): - return 'running' - case t('setting.scheduler.stopped'): - return 'stopped' - case t('setting.scheduler.waiting'): - return 'waiting' - default: - return 'default' - } +function getSchedulerStatusVariant(scheduler: ScheduleInfo) { + if (isScheduleRunning(scheduler)) return 'running' + if (isScheduleWaiting(scheduler)) return 'waiting' + return 'default' } /** 将后端返回的紧凑时间差转换为更适合移动端展示的文本。 */ @@ -67,15 +61,15 @@ function formatMobileNextRunTime(nextRun?: string) { /** 获取移动端状态胶囊文案;等待状态展示为下次运行倒计时。 */ function getMobileSchedulerStatusText(scheduler: ScheduleInfo) { - if (scheduler.status === t('setting.scheduler.waiting')) { - const readableNextRun = formatMobileNextRunTime(scheduler.next_run) + if (isScheduleWaiting(scheduler)) { + const readableNextRun = formatMobileNextRunTime(getScheduleNextRunText(scheduler)) return readableNextRun ? t('setting.scheduler.mobileWaitingAfter', { time: readableNextRun }) : t('setting.scheduler.mobileNoNextRun') } - return scheduler.status + return getScheduleStatusText(scheduler) } /** 执行指定定时服务,并在短延迟后刷新列表。 */ @@ -108,7 +102,7 @@ const mobileSchedulerCards = computed(() => progressValue: isRunning ? getScheduleProgressValue(scheduler) : 0, scheduler, statusText: getMobileSchedulerStatusText(scheduler), - statusVariant: getSchedulerStatusVariant(scheduler.status), + statusVariant: getSchedulerStatusVariant(scheduler), visual: getSchedulerVisual(scheduler), } }), @@ -138,10 +132,10 @@ const { loading: schedulerLoading } = useDataRefresh( - {{ scheduler.provider }} + {{ getScheduleProvider(scheduler) }} -
{{ scheduler.name }}
+
{{ getScheduleName(scheduler) }}
- {{ getScheduleProgressText(scheduler) || scheduler.status }} + {{ getScheduleProgressText(scheduler) || getScheduleStatusText(scheduler) }} {{ Math.round(getScheduleProgressValue(scheduler)) }}%
- - {{ scheduler.status }} + + {{ getScheduleStatusText(scheduler) }} - {{ scheduler.next_run }} + {{ getScheduleNextRunText(scheduler) }}
-

{{ scheduler.name }}

-

{{ scheduler.provider }}

+

{{ getScheduleName(scheduler) }}

+

{{ getScheduleProvider(scheduler) }}

@@ -236,7 +230,7 @@ const { loading: schedulerLoading } = useDataRefresh(
- {{ progressText || scheduler.status }} + {{ progressText || getScheduleStatusText(scheduler) }} {{ Math.round(progressValue) }}%