backup
This commit is contained in:
411
frontend/src/stores/jobs.js
Normal file
411
frontend/src/stores/jobs.js
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
Reference in New Issue
Block a user