This commit is contained in:
beabigegg
2025-09-12 08:56:44 +08:00
commit 0bc8c4c81c
86 changed files with 23146 additions and 0 deletions

411
frontend/src/stores/jobs.js Normal file
View File

@@ -0,0 +1,411 @@
import { defineStore } from 'pinia'
import { jobsAPI, filesAPI } from '@/services/jobs'
import { ElMessage, ElNotification } from 'element-plus'
import { saveAs } from 'file-saver'
export const useJobsStore = defineStore('jobs', {
state: () => ({
jobs: [],
currentJob: null,
pagination: {
page: 1,
per_page: 20,
total: 0,
pages: 0
},
loading: false,
uploadProgress: 0,
filters: {
status: 'all',
search: ''
},
// 輪詢管理
pollingIntervals: new Map() // 存儲每個任務的輪詢間隔 ID
}),
getters: {
// 按狀態分組的任務
pendingJobs: (state) => state.jobs.filter(job => job.status === 'PENDING'),
processingJobs: (state) => state.jobs.filter(job => job.status === 'PROCESSING'),
completedJobs: (state) => state.jobs.filter(job => job.status === 'COMPLETED'),
failedJobs: (state) => state.jobs.filter(job => job.status === 'FAILED'),
retryJobs: (state) => state.jobs.filter(job => job.status === 'RETRY'),
// 根據 UUID 查找任務
getJobByUuid: (state) => (uuid) => {
return state.jobs.find(job => job.job_uuid === uuid)
},
// 統計資訊
jobStats: (state) => ({
total: state.jobs.length,
pending: state.jobs.filter(job => job.status === 'PENDING').length,
processing: state.jobs.filter(job => job.status === 'PROCESSING').length,
completed: state.jobs.filter(job => job.status === 'COMPLETED').length,
failed: state.jobs.filter(job => job.status === 'FAILED').length
})
},
actions: {
/**
* 取得任務列表
* @param {Object} options - 查詢選項
*/
async fetchJobs(options = {}) {
try {
this.loading = true
const params = {
page: options.page || this.pagination.page,
per_page: options.per_page || this.pagination.per_page,
status: options.status || this.filters.status
}
const response = await jobsAPI.getJobs(params)
if (response.success) {
this.jobs = response.data.jobs
this.pagination = response.data.pagination
return response.data
}
} catch (error) {
console.error('取得任務列表失敗:', error)
ElMessage.error('載入任務列表失敗')
} finally {
this.loading = false
}
},
/**
* 上傳檔案
* @param {FormData} formData - 表單資料
* @param {Function} onProgress - 進度回調
*/
async uploadFile(formData, onProgress) {
try {
this.uploadProgress = 0
// 設定進度回調
if (onProgress) {
formData.onUploadProgress = (progressEvent) => {
const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total)
this.uploadProgress = progress
onProgress(progress)
}
}
const response = await jobsAPI.uploadFile(formData)
if (response.success) {
// 將新任務添加到列表頂部
const newJob = response.data
this.jobs.unshift(newJob)
ElMessage.success('檔案上傳成功,已加入翻譯佇列')
return newJob
}
} catch (error) {
console.error('檔案上傳失敗:', error)
throw error
} finally {
this.uploadProgress = 0
}
},
/**
* 取得任務詳情
* @param {string} jobUuid - 任務 UUID
*/
async fetchJobDetail(jobUuid) {
try {
const response = await jobsAPI.getJobDetail(jobUuid)
if (response && response.success) {
this.currentJob = response.data.job
return response.data
} else {
console.error('API 響應格式錯誤:', response)
throw new Error('API 響應格式錯誤')
}
} catch (error) {
console.error('取得任務詳情失敗:', error)
ElMessage.error('載入任務詳情失敗')
throw error
}
},
/**
* 重試失敗任務
* @param {string} jobUuid - 任務 UUID
*/
async retryJob(jobUuid) {
try {
const response = await jobsAPI.retryJob(jobUuid)
if (response.success) {
// 更新本地任務狀態
const jobIndex = this.jobs.findIndex(job => job.job_uuid === jobUuid)
if (jobIndex !== -1) {
this.jobs[jobIndex] = { ...this.jobs[jobIndex], ...response.data }
}
ElMessage.success('任務已重新加入佇列')
return response.data
}
} catch (error) {
console.error('重試任務失敗:', error)
ElMessage.error('重試任務失敗')
}
},
/**
* 取消任務
* @param {string} jobUuid - 任務 UUID
*/
async cancelJob(jobUuid) {
try {
const response = await jobsAPI.cancelJob(jobUuid)
if (response.success) {
const jobIndex = this.jobs.findIndex(job => job.job_uuid === jobUuid)
if (jobIndex !== -1) {
this.jobs[jobIndex] = {
...this.jobs[jobIndex],
status: 'FAILED',
error_message: '使用者取消任務'
}
}
ElMessage.success('任務已取消')
}
} catch (error) {
console.error('取消任務失敗:', error)
ElMessage.error('取消任務失敗')
}
},
/**
* 刪除任務
* @param {string} jobUuid - 任務 UUID
*/
async deleteJob(jobUuid) {
try {
const response = await jobsAPI.deleteJob(jobUuid)
if (response.success) {
// 先停止輪詢
this.unsubscribeFromJobUpdates(jobUuid)
// 從列表中移除任務
const jobIndex = this.jobs.findIndex(job => job.job_uuid === jobUuid)
if (jobIndex !== -1) {
this.jobs.splice(jobIndex, 1)
}
ElMessage.success('任務已刪除')
}
} catch (error) {
console.error('刪除任務失敗:', error)
ElMessage.error('刪除任務失敗')
}
},
/**
* 下載檔案
* @param {string} jobUuid - 任務 UUID
* @param {string} languageCode - 語言代碼
* @param {string} filename - 檔案名稱
*/
async downloadFile(jobUuid, languageCode, filename) {
try {
const response = await filesAPI.downloadFile(jobUuid, languageCode)
// 使用 FileSaver.js 下載檔案
const blob = new Blob([response], { type: 'application/octet-stream' })
saveAs(blob, filename)
ElMessage.success('檔案下載完成')
} catch (error) {
console.error('下載檔案失敗:', error)
ElMessage.error('檔案下載失敗')
}
},
/**
* 批量下載檔案
* @param {string} jobUuid - 任務 UUID
* @param {string} filename - 壓縮檔名稱
*/
async downloadAllFiles(jobUuid, filename) {
try {
const response = await filesAPI.downloadAllFiles(jobUuid)
const blob = new Blob([response], { type: 'application/zip' })
saveAs(blob, filename || `${jobUuid}.zip`)
ElMessage.success('檔案打包下載完成')
} catch (error) {
console.error('批量下載失敗:', error)
ElMessage.error('批量下載失敗')
}
},
/**
* 更新任務狀態(用於 WebSocket 即時更新)
* @param {string} jobUuid - 任務 UUID
* @param {Object} statusUpdate - 狀態更新資料
*/
updateJobStatus(jobUuid, statusUpdate) {
const jobIndex = this.jobs.findIndex(job => job.job_uuid === jobUuid)
if (jobIndex !== -1) {
this.jobs[jobIndex] = { ...this.jobs[jobIndex], ...statusUpdate }
// 如果是當前查看的任務詳情,也要更新
if (this.currentJob && this.currentJob.job_uuid === jobUuid) {
this.currentJob = { ...this.currentJob, ...statusUpdate }
}
// 任務完成時顯示通知
if (statusUpdate.status === 'COMPLETED') {
ElNotification({
title: '翻譯完成',
message: `檔案「${this.jobs[jobIndex].original_filename}」翻譯完成`,
type: 'success',
duration: 5000
})
} else if (statusUpdate.status === 'FAILED') {
ElNotification({
title: '翻譯失敗',
message: `檔案「${this.jobs[jobIndex].original_filename}」翻譯失敗`,
type: 'error',
duration: 5000
})
}
}
},
/**
* 設定篩選條件
* @param {Object} filters - 篩選條件
*/
setFilters(filters) {
this.filters = { ...this.filters, ...filters }
},
/**
* 訂閱任務更新 (輪詢機制)
* @param {string} jobUuid - 任務 UUID
*/
subscribeToJobUpdates(jobUuid) {
// 如果已經在輪詢這個任務,先停止舊的輪詢
if (this.pollingIntervals.has(jobUuid)) {
this.unsubscribeFromJobUpdates(jobUuid)
}
console.log(`[DEBUG] 開始訂閱任務更新: ${jobUuid}`)
const pollInterval = setInterval(async () => {
try {
const job = await this.fetchJobDetail(jobUuid)
if (job) {
// 任務存在,更新本地狀態
const existingJobIndex = this.jobs.findIndex(j => j.job_uuid === jobUuid)
if (existingJobIndex !== -1) {
// 更新現有任務
this.jobs[existingJobIndex] = { ...this.jobs[existingJobIndex], ...job }
}
// 檢查任務是否已完成
if (['COMPLETED', 'FAILED'].includes(job.status)) {
console.log(`[DEBUG] 任務 ${jobUuid} 已完成 (${job.status}),停止輪詢`)
this.unsubscribeFromJobUpdates(jobUuid)
// 顯示完成通知
if (job.status === 'COMPLETED') {
ElNotification({
title: '翻譯完成',
message: `檔案 "${job.original_filename}" 翻譯完成`,
type: 'success',
duration: 5000
})
}
}
} else {
// 任務不存在(可能被刪除),停止輪詢
console.log(`[DEBUG] 任務 ${jobUuid} 不存在,停止輪詢`)
this.unsubscribeFromJobUpdates(jobUuid)
// 從本地列表中移除任務
const existingJobIndex = this.jobs.findIndex(j => j.job_uuid === jobUuid)
if (existingJobIndex !== -1) {
this.jobs.splice(existingJobIndex, 1)
}
}
} catch (error) {
console.error(`輪詢任務 ${jobUuid} 狀態失敗:`, error)
// 檢查是否是 404 錯誤(任務不存在)
if (error.response?.status === 404) {
console.log(`[DEBUG] 任務 ${jobUuid} 已被刪除,停止輪詢`)
this.unsubscribeFromJobUpdates(jobUuid)
// 從本地列表中移除任務
const existingJobIndex = this.jobs.findIndex(j => j.job_uuid === jobUuid)
if (existingJobIndex !== -1) {
this.jobs.splice(existingJobIndex, 1)
}
} else {
// 其他錯誤,繼續輪詢但記錄錯誤
console.warn(`輪詢任務 ${jobUuid} 時發生錯誤,將繼續重試:`, error.message)
}
}
}, 3000) // 每 3 秒檢查一次
// 儲存輪詢間隔 ID
this.pollingIntervals.set(jobUuid, pollInterval)
},
/**
* 取消訂閱任務更新
* @param {string} jobUuid - 任務 UUID
*/
unsubscribeFromJobUpdates(jobUuid) {
const intervalId = this.pollingIntervals.get(jobUuid)
if (intervalId) {
clearInterval(intervalId)
this.pollingIntervals.delete(jobUuid)
console.log(`[DEBUG] 已取消任務 ${jobUuid} 的輪詢訂閱`)
}
},
/**
* 停止所有輪詢
*/
stopAllPolling() {
for (const [jobUuid, intervalId] of this.pollingIntervals) {
clearInterval(intervalId)
console.log(`[DEBUG] 已停止任務 ${jobUuid} 的輪詢`)
}
this.pollingIntervals.clear()
},
/**
* 重置任務列表
*/
resetJobs() {
// 先停止所有輪詢
this.stopAllPolling()
this.jobs = []
this.currentJob = null
this.pagination = {
page: 1,
per_page: 20,
total: 0,
pages: 0
}
}
}
})