feat: support backend i18n responses

This commit is contained in:
jxxghp
2026-07-06 19:13:25 +08:00
parent 269da6eefe
commit ff9dc26040
15 changed files with 243 additions and 83 deletions
+29 -1
View File
@@ -3,6 +3,7 @@ import router from '@/router'
import { useAuthStore } from '@/stores' import { useAuthStore } from '@/stores'
import { initializeRequestOptimizer } from '@/utils/requestOptimizer' import { initializeRequestOptimizer } from '@/utils/requestOptimizer'
import { useGlobalOfflineStatus } from '@/composables/useOfflineStatus' import { useGlobalOfflineStatus } from '@/composables/useOfflineStatus'
import { getCurrentLocale } from '@/plugins/i18n'
// 创建axios实例 // 创建axios实例
const api = axios.create({ const api = axios.create({
@@ -34,6 +35,9 @@ api.interceptors.request.use(config => {
if (authStore.token) { if (authStore.token) {
config.headers.Authorization = `Bearer ${authStore.token}` config.headers.Authorization = `Bearer ${authStore.token}`
} }
const locale = getCurrentLocale()
config.headers['X-MoviePilot-Locale'] = locale
config.headers['Accept-Language'] = locale
return config return config
}) })
@@ -51,12 +55,33 @@ function resolveConnectionFailureReason(error: AxiosError): 'network-error' | 't
return null 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( api.interceptors.response.use(
response => { response => {
// 任意 API 成功响应都可以证明 MoviePilot 服务当前可达。 // 任意 API 成功响应都可以证明 MoviePilot 服务当前可达。
globalOfflineStatus.markServerOnline() globalOfflineStatus.markServerOnline()
return response.data return normalizeLocalizedMessage(response.data)
}, },
(error: AxiosError) => { (error: AxiosError) => {
if (!error.response) { if (!error.response) {
@@ -81,12 +106,15 @@ api.interceptors.response.use(
// 其他网络错误 // 其他网络错误
return Promise.reject(new Error(error.message || 'Network error')) return Promise.reject(new Error(error.message || 'Network error'))
} else if (error.response.status === 403) { } else if (error.response.status === 403) {
normalizeLocalizedMessage(error.response.data)
// 认证 Store // 认证 Store
const authStore = useAuthStore() const authStore = useAuthStore()
// 清除登录状态信息 // 清除登录状态信息
authStore.logout() authStore.logout()
// token验证失败,跳转到登录页面 // token验证失败,跳转到登录页面
router.push('/login') router.push('/login')
} else {
normalizeLocalizedMessage(error.response.data)
} }
return Promise.reject(error) return Promise.reject(error)
+19
View File
@@ -1072,14 +1072,20 @@ export interface ScheduleProgress {
id?: string id?: string
// 名称 // 名称
name?: string name?: string
// 多语言名称
name_i18n?: string
// 提供者 // 提供者
provider?: string provider?: string
// 多语言提供者
provider_i18n?: string
// 是否正在执行 // 是否正在执行
enable?: boolean enable?: boolean
// 当前完成百分比 // 当前完成百分比
value?: number value?: number
// 当前进度文本 // 当前进度文本
text?: string text?: string
// 多语言进度文本
text_i18n?: string
// 执行状态 // 执行状态
status?: string status?: string
// 最近一次执行是否成功 // 最近一次执行是否成功
@@ -1090,6 +1096,8 @@ export interface ScheduleProgress {
finished_at?: string finished_at?: string
// 最近一次错误信息 // 最近一次错误信息
error?: string error?: string
// 多语言错误信息
error_i18n?: string
// 扩展数据 // 扩展数据
data?: Record<string, unknown> data?: Record<string, unknown>
} }
@@ -1100,16 +1108,26 @@ export interface ScheduleInfo {
id: string id: string
// 名称 // 名称
name: string name: string
// 多语言名称
name_i18n?: string
// 提供者 // 提供者
provider: string provider: string
// 多语言提供者
provider_i18n?: string
// 状态 // 状态
status: string status: string
// 多语言状态
status_i18n?: string
// 下次运行时间 // 下次运行时间
next_run: string next_run: string
// 多语言下次运行时间
next_run_i18n?: string
// 当前完成百分比 // 当前完成百分比
progress?: number progress?: number
// 进度文本 // 进度文本
progress_text?: string progress_text?: string
// 多语言进度文本
progress_text_i18n?: string
// 是否正在更新进度 // 是否正在更新进度
progress_enable?: boolean progress_enable?: boolean
// 进度详情 // 进度详情
@@ -1719,6 +1737,7 @@ export interface SubscribeShareStatistics {
export interface ApiResponse<T = any> { export interface ApiResponse<T = any> {
success: boolean success: boolean
message?: string message?: string
message_i18n?: string
data: T data: T
} }
+41 -21
View File
@@ -4,6 +4,7 @@ import mdLinkAttributes from 'markdown-it-link-attributes'
import { useDisplay } from 'vuetify' import { useDisplay } from 'vuetify'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useAuthStore, useUserStore } from '@/stores' import { useAuthStore, useUserStore } from '@/stores'
import { getCurrentLocale } from '@/plugins/i18n'
type AgentMessageRole = 'user' | 'assistant' type AgentMessageRole = 'user' | 'assistant'
type AgentMessageStatus = 'idle' | 'streaming' | 'done' | 'error' type AgentMessageStatus = 'idle' | 'streaming' | 'done' | 'error'
@@ -88,6 +89,7 @@ interface AgentStreamEvent {
choice?: Omit<AgentChoiceCard, 'status'> choice?: Omit<AgentChoiceCard, 'status'>
content?: string content?: string
message?: string message?: string
message_i18n?: string
message_id?: string message_id?: string
target_message?: Partial<AgentChatMessage> & { id?: string } target_message?: Partial<AgentChatMessage> & { id?: string }
session_id?: string session_id?: string
@@ -622,19 +624,16 @@ function dedupeHistorySessions(sessions: AgentSessionHistoryItem[]) {
// 调用智能助手接口,并统一处理鉴权和标准响应格式。 // 调用智能助手接口,并统一处理鉴权和标准响应格式。
async function fetchAgentApi(path: string, options: RequestInit = {}) { 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), { const response = await fetch(resolveApiUrl(path), {
...options, ...options,
headers, headers: buildAgentRequestHeaders(options.headers),
credentials: 'include', 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() 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 return result.data
} }
@@ -981,6 +980,31 @@ function resolveApiUrl(path: string) {
return `${baseUrl.replace(/\/?$/, '/')}${path.replace(/^\//, '')}` 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 滚动库频繁测量影响手感。 // 消息主列表使用原生滚动,避免流式回复时 JS 滚动库频繁测量影响手感。
function getMessageScrollerElement() { function getMessageScrollerElement() {
return messageListRef.value return messageListRef.value
@@ -1194,7 +1218,7 @@ function applyStreamEvent(event: AgentStreamEvent, assistantMessage: AgentChatMe
case 'error': case 'error':
assistantMessage.status = 'error' assistantMessage.status = 'error'
// 后端流式错误已经以 AI 消息展示,避免底部提示条重复且持续占位。 // 后端流式错误已经以 AI 消息展示,避免底部提示条重复且持续占位。
assistantMessage.content ||= event.message || t('agentAssistant.error') assistantMessage.content ||= event.message_i18n || event.message || t('agentAssistant.error')
emit('assistant-preview', assistantMessage.content) emit('assistant-preview', assistantMessage.content)
markToolsDone(assistantMessage) markToolsDone(assistantMessage)
break break
@@ -1370,17 +1394,15 @@ async function uploadAgentAttachment(file: File) {
const response = await fetch(resolveApiUrl('message/agent/upload'), { const response = await fetch(resolveApiUrl('message/agent/upload'), {
method: 'POST', method: 'POST',
headers: { headers: buildAgentRequestHeaders(),
...(authStore.token ? { Authorization: `Bearer ${authStore.token}` } : {}),
},
body: formData, body: formData,
credentials: 'include', 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() 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 return result.data as AgentMessageAttachment & AgentOutgoingFile
} }
@@ -1452,10 +1474,9 @@ async function streamAgentMessage(
try { try {
const response = await fetch(resolveApiUrl('message/agent/stream'), { const response = await fetch(resolveApiUrl('message/agent/stream'), {
method: 'POST', method: 'POST',
headers: { headers: buildAgentRequestHeaders({
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...(authStore.token ? { Authorization: `Bearer ${authStore.token}` } : {}), }),
},
body: JSON.stringify({ body: JSON.stringify({
text: content, text: content,
display_text: displayContent || content, display_text: displayContent || content,
@@ -1473,7 +1494,7 @@ async function streamAgentMessage(
}) })
if (!response.ok) { if (!response.ok) {
throw new Error(`${response.status} ${response.statusText}`.trim()) throw new Error(await resolveAgentResponseErrorMessage(response))
} }
await readAgentStream(response, assistantMessage) await readAgentStream(response, assistantMessage)
@@ -1698,10 +1719,9 @@ async function handleChoiceClick(message: AgentChatMessage, choice: AgentChoiceC
try { try {
const response = await fetch(resolveApiUrl('message/agent/callback'), { const response = await fetch(resolveApiUrl('message/agent/callback'), {
method: 'POST', method: 'POST',
headers: { headers: buildAgentRequestHeaders({
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...(authStore.token ? { Authorization: `Bearer ${authStore.token}` } : {}), }),
},
body: JSON.stringify({ body: JSON.stringify({
session_id: sessionId.value, session_id: sessionId.value,
callback_data: button.callback_data, callback_data: button.callback_data,
@@ -1711,10 +1731,10 @@ async function handleChoiceClick(message: AgentChatMessage, choice: AgentChoiceC
credentials: 'include', 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() 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 || '') const agentMessage = String(result.data?.message || '')
if (result.data?.traditional) { if (result.data?.traditional) {
+17 -4
View File
@@ -1019,6 +1019,19 @@ function mergePreviewData(target: ManualTransferPreviewData, incoming?: ManualTr
} }
} }
// 从标准响应中提取可展示的整理预览数据,优先保留顶层本地化消息。
function resolvePreviewResponseData(result: ApiResponse<ManualTransferPreviewData>) {
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() { async function previewTransfer() {
if (!props.logids && !normalizedItems.value.length) return if (!props.logids && !normalizedItems.value.length) return
@@ -1046,7 +1059,7 @@ async function previewTransfer() {
}), }),
) )
} else { } else {
mergePreviewData(mergedPreviewData, result.data) mergePreviewData(mergedPreviewData, resolvePreviewResponseData(result))
} }
} catch (err: any) { } catch (err: any) {
console.warn(`预览请求异常: ${err?.message}`) console.warn(`预览请求异常: ${err?.message}`)
@@ -1078,7 +1091,7 @@ async function previewTransfer() {
return return
} }
mergePreviewData(mergedPreviewData, result.data) mergePreviewData(mergedPreviewData, resolvePreviewResponseData(result))
} catch (err: any) { } catch (err: any) {
console.warn(`预览请求异常: ${err?.message}`) console.warn(`预览请求异常: ${err?.message}`)
mergePreviewData( mergePreviewData(
@@ -1114,7 +1127,7 @@ async function previewTransfer() {
return return
} }
mergePreviewData(mergedPreviewData, result.data) mergePreviewData(mergedPreviewData, resolvePreviewResponseData(result))
} catch (err: any) { } catch (err: any) {
console.warn(`预览请求异常: ${err?.message}`) console.warn(`预览请求异常: ${err?.message}`)
mergePreviewData( mergePreviewData(
@@ -1196,7 +1209,7 @@ async function handleTransferLog(logid: number, background: boolean = false) {
function handleProgressMessage(event: MessageEvent) { function handleProgressMessage(event: MessageEvent) {
const progress = JSON.parse(event.data) const progress = JSON.parse(event.data)
if (progress) { if (progress) {
progressText.value = progress.text progressText.value = progress.text_i18n || progress.text
progressValue.value = progress.value progressValue.value = progress.value
} }
} }
+1 -1
View File
@@ -728,7 +728,7 @@ async function batchScrape() {
function handleProgressMessage(event: MessageEvent) { function handleProgressMessage(event: MessageEvent) {
const progress = JSON.parse(event.data) const progress = JSON.parse(event.data)
if (progress) { if (progress) {
progressText.value = progress.text progressText.value = progress.text_i18n || progress.text
progressValue.value = progress.value progressValue.value = progress.value
progressDialogController?.updateProps({ text: progressText.value, value: progressValue.value }) progressDialogController?.updateProps({ text: progressText.value, value: progressValue.value })
} }
+22 -3
View File
@@ -1,11 +1,26 @@
import { getCurrentInstance, onMounted, onUnmounted, ref, type Ref } from 'vue' import { getCurrentInstance, onMounted, onUnmounted, ref, type Ref } from 'vue'
import { sseManagerSingleton, type SSEManagerOptions } from '@/utils/sseManager' import { sseManagerSingleton, type SSEManagerOptions } from '@/utils/sseManager'
import { addBackgroundTimer, removeBackgroundTimer } from '@/utils/backgroundManager' import { addBackgroundTimer, removeBackgroundTimer } from '@/utils/backgroundManager'
import { getCurrentLocale } from '@/plugins/i18n'
type UseSSEOptions = Partial<SSEManagerOptions> & { type UseSSEOptions = Partial<SSEManagerOptions> & {
connectDelay?: number 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连接和定时器,减少后台常驻活动。 * 统一管理SSE连接和定时器,减少后台常驻活动。
@@ -180,12 +195,15 @@ export function useBackground() {
listenerId: string, listenerId: string,
isActive: Ref<boolean>, isActive: Ref<boolean>,
) => { ) => {
const getManager = () => let managerUrl = ''
sseManagerSingleton.getIndependentManager(url, listenerId, { const getManager = () => {
managerUrl = appendLocaleParam(url)
return sseManagerSingleton.getIndependentManager(managerUrl, listenerId, {
backgroundCloseDelay: 1000, // 进度SSE更快关闭 backgroundCloseDelay: 1000, // 进度SSE更快关闭
reconnectDelay: 1000, reconnectDelay: 1000,
maxReconnectAttempts: 5, maxReconnectAttempts: 5,
}) })
}
let manager: ReturnType<typeof getManager> | null = null let manager: ReturnType<typeof getManager> | null = null
let isListening = false let isListening = false
@@ -207,8 +225,9 @@ export function useBackground() {
manager.removeMessageListener(listenerId) manager.removeMessageListener(listenerId)
if (destroyManager) { if (destroyManager) {
sseManagerSingleton.closeIndependentManager(url, listenerId) sseManagerSingleton.closeIndependentManager(managerUrl || appendLocaleParam(url), listenerId)
manager = null manager = null
managerUrl = ''
} }
isListening = false isListening = false
+31 -2
View File
@@ -4,17 +4,44 @@ import { useBackground } from '@/composables/useBackground'
import type { Ref } from 'vue' import type { Ref } from 'vue'
const SCHEDULE_PROGRESS_REFRESH_INTERVAL = 1000 const SCHEDULE_PROGRESS_REFRESH_INTERVAL = 1000
const SCHEDULE_RUNNING_STATUS = '正在运行'
const SCHEDULE_WAITING_STATUS = '等待'
/** 判断定时服务是否仍在运行,兼容列表状态与进度详情两种后端信号。 */ /** 判断定时服务是否仍在运行,兼容列表状态与进度详情两种后端信号。 */
export function isScheduleRunning(schedule: ScheduleInfo) { export function isScheduleRunning(schedule: ScheduleInfo) {
return ( return (
schedule.status === '正在运行' || schedule.status === SCHEDULE_RUNNING_STATUS ||
schedule.progress_enable === true || schedule.progress_enable === true ||
schedule.progress_detail?.enable === true || schedule.progress_detail?.enable === true ||
schedule.progress_detail?.status === 'running' 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<ScheduleInfo[]>, refreshId: string) { export function useScheduleProgress(schedules: Ref<ScheduleInfo[]>, refreshId: string) {
const { useDataRefresh } = useBackground() const { useDataRefresh } = useBackground()
@@ -72,7 +99,9 @@ export function useScheduleProgress(schedules: Ref<ScheduleInfo[]>, refreshId: s
/** 获取任务当前应展示的进度说明。 */ /** 获取任务当前应展示的进度说明。 */
function getScheduleProgressText(schedule: ScheduleInfo) { 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) useDataRefresh(refreshId, refreshRunningProgress, SCHEDULE_PROGRESS_REFRESH_INTERVAL, true)
+1
View File
@@ -1528,6 +1528,7 @@ export default {
preparing: 'Preparing...', preparing: 'Preparing...',
totalModules: 'Total Modules', totalModules: 'Total Modules',
recheck: 'Recheck', recheck: 'Recheck',
requestFailed: 'Network request failed',
}, },
nameTest: { nameTest: {
recognize: 'Recognize', recognize: 'Recognize',
+1
View File
@@ -1521,6 +1521,7 @@ export default {
preparing: '准备检查...', preparing: '准备检查...',
totalModules: '总模块数', totalModules: '总模块数',
recheck: '重新检查', recheck: '重新检查',
requestFailed: '网络请求失败',
}, },
nameTest: { nameTest: {
recognize: '识别', recognize: '识别',
+1
View File
@@ -1520,6 +1520,7 @@ export default {
preparing: '準備檢查...', preparing: '準備檢查...',
totalModules: '總模組數', totalModules: '總模組數',
recheck: '重新檢查', recheck: '重新檢查',
requestFailed: '網路請求失敗',
}, },
nameTest: { nameTest: {
recognize: '識別', recognize: '識別',
+14 -4
View File
@@ -19,6 +19,7 @@ import { useToast } from 'vue-toastification'
import { useKeepAliveRefresh } from '@/composables/useKeepAliveRefresh' import { useKeepAliveRefresh } from '@/composables/useKeepAliveRefresh'
import { useUserStore } from '@/stores' import { useUserStore } from '@/stores'
import { buildUserPermissionContext, hasPermission } from '@/utils/permission' import { buildUserPermissionContext, hasPermission } from '@/utils/permission'
import { getCurrentLocale } from '@/plugins/i18n'
// //
const { t } = useI18n() const { t } = useI18n()
@@ -575,6 +576,7 @@ function buildSearchStreamUrl(params: SearchParams, requestToken?: string) {
if (requestToken) { if (requestToken) {
setSearchParam(url.searchParams, '_ts', requestToken) setSearchParam(url.searchParams, '_ts', requestToken)
} }
setSearchParam(url.searchParams, 'locale', getCurrentLocale())
return url.toString() return url.toString()
} }
@@ -605,8 +607,9 @@ function hasLoadedEmptySearchResult() {
// //
function updateSearchProgress(eventData: { [key: string]: any }, flushNow: boolean = false) { function updateSearchProgress(eventData: { [key: string]: any }, flushNow: boolean = false) {
if (eventData.text) { const text = eventData.text_i18n || eventData.text
pendingProgressText = eventData.text if (text) {
pendingProgressText = text
} }
if (typeof eventData.value === 'number') { if (typeof eventData.value === 'number') {
pendingProgressValue = eventData.value pendingProgressValue = eventData.value
@@ -707,7 +710,7 @@ function getSubtitleItemKey(item: SubtitleInfo, index: number) {
function handleSearchStreamMessage(eventData: { [key: string]: any }) { function handleSearchStreamMessage(eventData: { [key: string]: any }) {
if (eventData.type === 'error') { if (eventData.type === 'error') {
updateSearchProgress(eventData, true) updateSearchProgress(eventData, true)
errorDescription.value = eventData.message || t('resource.noResourceFound') errorDescription.value = eventData.message_i18n || eventData.message || t('resource.noResourceFound')
return return
} }
@@ -1116,7 +1119,14 @@ async function pollAiRecommend() {
console.error('AI功能未启用') console.error('AI功能未启用')
} else { } else {
// status === 'error' success false // 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) console.error('智能推荐错误:', errMsg)
toast.error(`${t('resource.aiRecommendError')}: ${errMsg}`) toast.error(`${t('resource.aiRecommendError')}: ${errMsg}`)
} }
+16 -4
View File
@@ -3,7 +3,14 @@ import api from '@/api'
import type { ScheduleInfo, TransferQueue } from '@/api/types' import type { ScheduleInfo, TransferQueue } from '@/api/types'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useBackground } from '@/composables/useBackground' 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' import { getSchedulerVisual } from '@/utils/schedulerVisual'
// //
@@ -47,9 +54,14 @@ const backgroundTasks = computed<BackgroundTaskItem[]>(() => {
return { return {
id: `schedule-${item.id}`, id: `schedule-${item.id}`,
title: item.name || t('dashboard.scheduler'), title: getScheduleName(item) || t('dashboard.scheduler'),
subtitle: (isRunning && getScheduleProgressText(item)) || item.provider || item.next_run || '', subtitle:
status: isRunning ? t('dashboard.taskRunning') : item.status || t('dashboard.taskWaiting'), (isRunning && getScheduleProgressText(item)) ||
getScheduleProvider(item) ||
getScheduleNextRunText(item),
status: isRunning
? t('dashboard.taskRunning')
: getScheduleStatusText(item) || t('dashboard.taskWaiting'),
icon: visual.icon, icon: visual.icon,
color: visual.color, color: visual.color,
progress: isRunning ? getScheduleProgressValue(item) : undefined, progress: isRunning ? getScheduleProgressValue(item) : undefined,
+2 -2
View File
@@ -799,11 +799,11 @@ async function handleAiRedoProgressMessage(event: MessageEvent) {
const progress = JSON.parse(event.data) const progress = JSON.parse(event.data)
if (!progress) return 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 }) aiRedoProgressDialogController?.updateProps({ text: aiRedoProgressText.value })
if (progress.enable === false) { 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)
} }
} }
+18 -5
View File
@@ -9,6 +9,19 @@ const { t } = useI18n()
// //
const theme = useTheme() 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 // ID
const modules = ref< const modules = ref<
{ {
@@ -38,9 +51,9 @@ async function getModules() {
const moduleList = result.data?.modules const moduleList = result.data?.modules
if (moduleList) { if (moduleList) {
// //
modules.value = moduleList.map((module: { id: string; name: string }, index: number) => ({ modules.value = moduleList.map((module: ModuleListItem, index: number) => ({
id: module.id, id: module.id,
name: module.name, name: module.name_i18n || module.name,
state: undefined, state: undefined,
errmsg: '', errmsg: '',
loading: false, loading: false,
@@ -91,7 +104,7 @@ async function moduleTest(index: number) {
const moduleid = target.id const moduleid = target.id
target.loading = true 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 target.loading = false
if (result.success) { if (result.success) {
@@ -103,14 +116,14 @@ async function moduleTest(index: number) {
} else { } else {
target.state = 'error' target.state = 'error'
target.name = `${target.name} - ${t('moduleTest.error')}` target.name = `${target.name} - ${t('moduleTest.error')}`
target.errmsg = result.message target.errmsg = result.message_i18n || result.message || ''
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)
const target = modules.value[index] const target = modules.value[index]
target.loading = false target.loading = false
target.state = 'error' target.state = 'error'
target.errmsg = '网络请求失败' target.errmsg = t('moduleTest.requestFailed')
} }
} }
+30 -36
View File
@@ -4,7 +4,15 @@ import api from '@/api'
import type { ScheduleInfo } from '@/api/types' import type { ScheduleInfo } from '@/api/types'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useBackground } from '@/composables/useBackground' 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' import { getSchedulerVisual } from '@/utils/schedulerVisual'
// //
@@ -33,31 +41,17 @@ async function loadSchedulerList() {
} }
/** 根据任务状态返回桌面端状态标签颜色。 */ /** 根据任务状态返回桌面端状态标签颜色。 */
function getSchedulerColor(status: string) { function getSchedulerColor(scheduler: ScheduleInfo) {
switch (status) { if (isScheduleRunning(scheduler)) return 'success'
case t('setting.scheduler.running'): if (isScheduleWaiting(scheduler)) return ''
return 'success' return ''
case t('setting.scheduler.stopped'):
return 'error'
case t('setting.scheduler.waiting'):
return ''
default:
return ''
}
} }
/** 根据任务状态返回移动端状态胶囊的语义样式。 */ /** 根据任务状态返回移动端状态胶囊的语义样式。 */
function getSchedulerStatusVariant(status: string) { function getSchedulerStatusVariant(scheduler: ScheduleInfo) {
switch (status) { if (isScheduleRunning(scheduler)) return 'running'
case t('setting.scheduler.running'): if (isScheduleWaiting(scheduler)) return 'waiting'
return 'running' return 'default'
case t('setting.scheduler.stopped'):
return 'stopped'
case t('setting.scheduler.waiting'):
return 'waiting'
default:
return 'default'
}
} }
/** 将后端返回的紧凑时间差转换为更适合移动端展示的文本。 */ /** 将后端返回的紧凑时间差转换为更适合移动端展示的文本。 */
@@ -67,15 +61,15 @@ function formatMobileNextRunTime(nextRun?: string) {
/** 获取移动端状态胶囊文案;等待状态展示为下次运行倒计时。 */ /** 获取移动端状态胶囊文案;等待状态展示为下次运行倒计时。 */
function getMobileSchedulerStatusText(scheduler: ScheduleInfo) { function getMobileSchedulerStatusText(scheduler: ScheduleInfo) {
if (scheduler.status === t('setting.scheduler.waiting')) { if (isScheduleWaiting(scheduler)) {
const readableNextRun = formatMobileNextRunTime(scheduler.next_run) const readableNextRun = formatMobileNextRunTime(getScheduleNextRunText(scheduler))
return readableNextRun return readableNextRun
? t('setting.scheduler.mobileWaitingAfter', { time: readableNextRun }) ? t('setting.scheduler.mobileWaitingAfter', { time: readableNextRun })
: t('setting.scheduler.mobileNoNextRun') : t('setting.scheduler.mobileNoNextRun')
} }
return scheduler.status return getScheduleStatusText(scheduler)
} }
/** 执行指定定时服务,并在短延迟后刷新列表。 */ /** 执行指定定时服务,并在短延迟后刷新列表。 */
@@ -108,7 +102,7 @@ const mobileSchedulerCards = computed(() =>
progressValue: isRunning ? getScheduleProgressValue(scheduler) : 0, progressValue: isRunning ? getScheduleProgressValue(scheduler) : 0,
scheduler, scheduler,
statusText: getMobileSchedulerStatusText(scheduler), statusText: getMobileSchedulerStatusText(scheduler),
statusVariant: getSchedulerStatusVariant(scheduler.status), statusVariant: getSchedulerStatusVariant(scheduler),
visual: getSchedulerVisual(scheduler), visual: getSchedulerVisual(scheduler),
} }
}), }),
@@ -138,10 +132,10 @@ const { loading: schedulerLoading } = useDataRefresh(
<tbody> <tbody>
<tr v-for="scheduler in schedulerList" :key="scheduler.id"> <tr v-for="scheduler in schedulerList" :key="scheduler.id">
<td> <td>
{{ scheduler.provider }} {{ getScheduleProvider(scheduler) }}
</td> </td>
<td class="scheduler-task-cell"> <td class="scheduler-task-cell">
<div>{{ scheduler.name }}</div> <div>{{ getScheduleName(scheduler) }}</div>
<div v-if="isScheduleRunning(scheduler)" class="scheduler-progress"> <div v-if="isScheduleRunning(scheduler)" class="scheduler-progress">
<VProgressLinear <VProgressLinear
:model-value="getScheduleProgressValue(scheduler)" :model-value="getScheduleProgressValue(scheduler)"
@@ -150,18 +144,18 @@ const { loading: schedulerLoading } = useDataRefresh(
rounded rounded
/> />
<div class="scheduler-progress-meta"> <div class="scheduler-progress-meta">
<span>{{ getScheduleProgressText(scheduler) || scheduler.status }}</span> <span>{{ getScheduleProgressText(scheduler) || getScheduleStatusText(scheduler) }}</span>
<strong>{{ Math.round(getScheduleProgressValue(scheduler)) }}%</strong> <strong>{{ Math.round(getScheduleProgressValue(scheduler)) }}%</strong>
</div> </div>
</div> </div>
</td> </td>
<td> <td>
<VChip :color="getSchedulerColor(scheduler.status)"> <VChip :color="getSchedulerColor(scheduler)">
{{ scheduler.status }} {{ getScheduleStatusText(scheduler) }}
</VChip> </VChip>
</td> </td>
<td> <td>
{{ scheduler.next_run }} {{ getScheduleNextRunText(scheduler) }}
</td> </td>
<td> <td>
<VBtn <VBtn
@@ -214,8 +208,8 @@ const { loading: schedulerLoading } = useDataRefresh(
</div> </div>
<div class="mobile-scheduler-content"> <div class="mobile-scheduler-content">
<h3>{{ scheduler.name }}</h3> <h3>{{ getScheduleName(scheduler) }}</h3>
<p>{{ scheduler.provider }}</p> <p>{{ getScheduleProvider(scheduler) }}</p>
</div> </div>
<div class="mobile-scheduler-actions"> <div class="mobile-scheduler-actions">
@@ -236,7 +230,7 @@ const { loading: schedulerLoading } = useDataRefresh(
<div v-if="isRunning" class="mobile-scheduler-progress"> <div v-if="isRunning" class="mobile-scheduler-progress">
<VProgressLinear :model-value="progressValue" color="primary" height="4" rounded /> <VProgressLinear :model-value="progressValue" color="primary" height="4" rounded />
<div class="scheduler-progress-meta"> <div class="scheduler-progress-meta">
<span>{{ progressText || scheduler.status }}</span> <span>{{ progressText || getScheduleStatusText(scheduler) }}</span>
<strong>{{ Math.round(progressValue) }}%</strong> <strong>{{ Math.round(progressValue) }}%</strong>
</div> </div>
</div> </div>