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