mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-07-14 00:42:31 +08:00
feat: support backend i18n responses
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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<string, unknown>
|
||||
}
|
||||
@@ -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<T = any> {
|
||||
success: boolean
|
||||
message?: string
|
||||
message_i18n?: string
|
||||
data: T
|
||||
}
|
||||
|
||||
|
||||
@@ -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<AgentChoiceCard, 'status'>
|
||||
content?: string
|
||||
message?: string
|
||||
message_i18n?: string
|
||||
message_id?: string
|
||||
target_message?: Partial<AgentChatMessage> & { 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) {
|
||||
|
||||
@@ -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() {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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<SSEManagerOptions> & {
|
||||
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<boolean>,
|
||||
) => {
|
||||
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<typeof getManager> | 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
|
||||
|
||||
@@ -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<ScheduleInfo[]>, refreshId: string) {
|
||||
const { useDataRefresh } = useBackground()
|
||||
@@ -72,7 +99,9 @@ export function useScheduleProgress(schedules: Ref<ScheduleInfo[]>, 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)
|
||||
|
||||
@@ -1528,6 +1528,7 @@ export default {
|
||||
preparing: 'Preparing...',
|
||||
totalModules: 'Total Modules',
|
||||
recheck: 'Recheck',
|
||||
requestFailed: 'Network request failed',
|
||||
},
|
||||
nameTest: {
|
||||
recognize: 'Recognize',
|
||||
|
||||
@@ -1521,6 +1521,7 @@ export default {
|
||||
preparing: '准备检查...',
|
||||
totalModules: '总模块数',
|
||||
recheck: '重新检查',
|
||||
requestFailed: '网络请求失败',
|
||||
},
|
||||
nameTest: {
|
||||
recognize: '识别',
|
||||
|
||||
@@ -1520,6 +1520,7 @@ export default {
|
||||
preparing: '準備檢查...',
|
||||
totalModules: '總模組數',
|
||||
recheck: '重新檢查',
|
||||
requestFailed: '網路請求失敗',
|
||||
},
|
||||
nameTest: {
|
||||
recognize: '識別',
|
||||
|
||||
@@ -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}`)
|
||||
}
|
||||
|
||||
@@ -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<BackgroundTaskItem[]>(() => {
|
||||
|
||||
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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
<tbody>
|
||||
<tr v-for="scheduler in schedulerList" :key="scheduler.id">
|
||||
<td>
|
||||
{{ scheduler.provider }}
|
||||
{{ getScheduleProvider(scheduler) }}
|
||||
</td>
|
||||
<td class="scheduler-task-cell">
|
||||
<div>{{ scheduler.name }}</div>
|
||||
<div>{{ getScheduleName(scheduler) }}</div>
|
||||
<div v-if="isScheduleRunning(scheduler)" class="scheduler-progress">
|
||||
<VProgressLinear
|
||||
:model-value="getScheduleProgressValue(scheduler)"
|
||||
@@ -150,18 +144,18 @@ const { loading: schedulerLoading } = useDataRefresh(
|
||||
rounded
|
||||
/>
|
||||
<div class="scheduler-progress-meta">
|
||||
<span>{{ getScheduleProgressText(scheduler) || scheduler.status }}</span>
|
||||
<span>{{ getScheduleProgressText(scheduler) || getScheduleStatusText(scheduler) }}</span>
|
||||
<strong>{{ Math.round(getScheduleProgressValue(scheduler)) }}%</strong>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<VChip :color="getSchedulerColor(scheduler.status)">
|
||||
{{ scheduler.status }}
|
||||
<VChip :color="getSchedulerColor(scheduler)">
|
||||
{{ getScheduleStatusText(scheduler) }}
|
||||
</VChip>
|
||||
</td>
|
||||
<td>
|
||||
{{ scheduler.next_run }}
|
||||
{{ getScheduleNextRunText(scheduler) }}
|
||||
</td>
|
||||
<td>
|
||||
<VBtn
|
||||
@@ -214,8 +208,8 @@ const { loading: schedulerLoading } = useDataRefresh(
|
||||
</div>
|
||||
|
||||
<div class="mobile-scheduler-content">
|
||||
<h3>{{ scheduler.name }}</h3>
|
||||
<p>{{ scheduler.provider }}</p>
|
||||
<h3>{{ getScheduleName(scheduler) }}</h3>
|
||||
<p>{{ getScheduleProvider(scheduler) }}</p>
|
||||
</div>
|
||||
|
||||
<div class="mobile-scheduler-actions">
|
||||
@@ -236,7 +230,7 @@ const { loading: schedulerLoading } = useDataRefresh(
|
||||
<div v-if="isRunning" class="mobile-scheduler-progress">
|
||||
<VProgressLinear :model-value="progressValue" color="primary" height="4" rounded />
|
||||
<div class="scheduler-progress-meta">
|
||||
<span>{{ progressText || scheduler.status }}</span>
|
||||
<span>{{ progressText || getScheduleStatusText(scheduler) }}</span>
|
||||
<strong>{{ Math.round(progressValue) }}%</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user