mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-06 16:16:42 +08:00
feat: support backend i18n responses
This commit is contained in:
+29
-1
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -1521,6 +1521,7 @@ export default {
|
|||||||
preparing: '准备检查...',
|
preparing: '准备检查...',
|
||||||
totalModules: '总模块数',
|
totalModules: '总模块数',
|
||||||
recheck: '重新检查',
|
recheck: '重新检查',
|
||||||
|
requestFailed: '网络请求失败',
|
||||||
},
|
},
|
||||||
nameTest: {
|
nameTest: {
|
||||||
recognize: '识别',
|
recognize: '识别',
|
||||||
|
|||||||
@@ -1520,6 +1520,7 @@ export default {
|
|||||||
preparing: '準備檢查...',
|
preparing: '準備檢查...',
|
||||||
totalModules: '總模組數',
|
totalModules: '總模組數',
|
||||||
recheck: '重新檢查',
|
recheck: '重新檢查',
|
||||||
|
requestFailed: '網路請求失敗',
|
||||||
},
|
},
|
||||||
nameTest: {
|
nameTest: {
|
||||||
recognize: '識別',
|
recognize: '識別',
|
||||||
|
|||||||
+14
-4
@@ -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}`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,32 +41,18 @@ 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'
|
|
||||||
case t('setting.scheduler.stopped'):
|
|
||||||
return 'error'
|
|
||||||
case t('setting.scheduler.waiting'):
|
|
||||||
return ''
|
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'
|
|
||||||
case t('setting.scheduler.stopped'):
|
|
||||||
return 'stopped'
|
|
||||||
case t('setting.scheduler.waiting'):
|
|
||||||
return 'waiting'
|
|
||||||
default:
|
|
||||||
return 'default'
|
return 'default'
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/** 将后端返回的紧凑时间差转换为更适合移动端展示的文本。 */
|
/** 将后端返回的紧凑时间差转换为更适合移动端展示的文本。 */
|
||||||
function formatMobileNextRunTime(nextRun?: string) {
|
function formatMobileNextRunTime(nextRun?: string) {
|
||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user