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

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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) {

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() {
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
}
}

View File

@@ -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 })
}

View File

@@ -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

View File

@@ -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)

View File

@@ -1528,6 +1528,7 @@ export default {
preparing: 'Preparing...',
totalModules: 'Total Modules',
recheck: 'Recheck',
requestFailed: 'Network request failed',
},
nameTest: {
recognize: 'Recognize',

View File

@@ -1521,6 +1521,7 @@ export default {
preparing: '准备检查...',
totalModules: '总模块数',
recheck: '重新检查',
requestFailed: '网络请求失败',
},
nameTest: {
recognize: '识别',

View File

@@ -1520,6 +1520,7 @@ export default {
preparing: '準備檢查...',
totalModules: '總模組數',
recheck: '重新檢查',
requestFailed: '網路請求失敗',
},
nameTest: {
recognize: '識別',

View File

@@ -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}`)
}

View File

@@ -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,

View File

@@ -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)
}
}

View File

@@ -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')
}
}

View File

@@ -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>