diff --git a/.claude/settings.local.json b/.claude/settings.local.json index e8a0a9214f..2d9a1a66c2 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -69,7 +69,15 @@ "Bash(npm run)", "Skill(plan)", "Skill(plan:*)", - "Bash(py:*)" + "Bash(py:*)", + "mcp__firecrawl__firecrawl_search", + "mcp__firecrawl__firecrawl_scrape", + "Bash(winget install:*)", + "mcp__context7__resolve-library-id", + "mcp__context7__query-docs", + "Bash(netstat:*)", + "Bash(npm run dev)", + "Bash(curl:*)" ], "deny": [], "ask": [] diff --git a/frontend/app/web-gold/package.json b/frontend/app/web-gold/package.json index 2081f3d782..53f3805328 100644 --- a/frontend/app/web-gold/package.json +++ b/frontend/app/web-gold/package.json @@ -55,6 +55,7 @@ "tailwindcss": "^4.1.14", "typescript": "^5.7.3", "vite": "^7.1.7", + "vite-plugin-compression2": "^2.4.0", "vite-plugin-vue-devtools": "^8.0.2", "vue-tsc": "^2.1.28" } diff --git a/frontend/app/web-gold/src/views/system/task-management/composables/useTaskOperations.js b/frontend/app/web-gold/src/views/system/task-management/composables/useTaskOperations.js index 3898c489e9..c9dcecd590 100644 --- a/frontend/app/web-gold/src/views/system/task-management/composables/useTaskOperations.js +++ b/frontend/app/web-gold/src/views/system/task-management/composables/useTaskOperations.js @@ -1,106 +1,125 @@ import { message, Modal } from 'ant-design-vue' +// 配置常量 +const API_SUCCESS_CODE = 0 +const DOWNLOAD_INTERVAL = 500 + /** * 任务操作通用逻辑 Composable - * @param {Object} apis - API 对象,包含 deleteApi, cancelApi, retryApi, getSignedUrlsApi - * @param {Function} onSuccess - 操作成功后的回调函数 - * @returns {Object} 操作方法 + * + * @param {Object} apiHandlers - API 处理器对象 + * @param {Function} apiHandlers.deleteApi - 删除任务的 API 函数 (id) => Promise + * @param {Function} [apiHandlers.cancelApi] - 取消任务的 API 函数 (id) => Promise + * @param {Function} [apiHandlers.retryApi] - 重试任务的 API 函数 (id) => Promise + * @param {Function} [apiHandlers.getSignedUrlsApi] - 获取签名URL的 API 函数 (taskId) => Promise<{code: number, data: string[]}> + * @param {Function} [onSuccess] - 操作成功后的回调函数 () => void + * @returns {Object} 操作方法集合 + * + * @example + * ```javascript + * const { handleDelete, handleCancel, handleRetry, handleBatchDownload, handlePreview } = useTaskOperations({ + * deleteApi: (id) => taskApi.delete(id), + * cancelApi: (id) => taskApi.cancel(id), + * retryApi: (id) => taskApi.retry(id), + * getSignedUrlsApi: (taskId) => taskApi.getSignedUrls(taskId) + * }, () => { + * // 刷新列表 + * fetchTasks() + * }) + * ``` */ -export function useTaskOperations(apis, onSuccess) { - const { deleteApi, cancelApi, retryApi, getSignedUrlsApi } = apis +export function useTaskOperations(apiHandlers, onSuccess) { + if (!apiHandlers || typeof apiHandlers !== 'object') { + throw new Error('apiHandlers must be an object') + } - // 删除任务 - const handleDelete = (id) => { + if (!apiHandlers.deleteApi || typeof apiHandlers.deleteApi !== 'function') { + throw new Error('deleteApi is required and must be a function') + } + + const { + deleteApi, + cancelApi, + retryApi, + getSignedUrlsApi + } = apiHandlers + + // 通用模态框确认 + function confirmModal({ title, content, okType = 'primary', onOk }) { Modal.confirm({ - title: '确认删除', - content: '确定删除这个任务吗?删除后无法恢复。', + title, + content, okText: '确认', cancelText: '取消', - okType: 'danger', + okType, onOk: async () => { try { - await deleteApi(id) - message.success('删除成功') + await onOk() onSuccess && onSuccess() } catch (error) { - console.error('删除任务失败:', error) - message.error('删除失败') + message.error('操作失败') } } }) } + // 执行 API 操作并显示成功消息 + async function executeApiOperation(apiFn, successMessage) { + await apiFn() + message.success(successMessage) + } + + // 删除任务 + function deleteTask(id) { + confirmModal({ + title: '确认删除', + content: '确定删除这个任务吗?删除后无法恢复。', + okType: 'danger', + onOk: () => executeApiOperation(() => deleteApi(id), '删除成功') + }) + } + // 取消任务 - const handleCancel = (id) => { - Modal.confirm({ + function cancelTask(id) { + confirmModal({ title: '确认取消', content: '确定要取消这个任务吗?', - okText: '确认', - cancelText: '取消', - onOk: async () => { - try { - await cancelApi(id) - message.success('已取消任务') - onSuccess && onSuccess() - } catch (error) { - console.error('取消任务失败:', error) - message.error('操作失败') - } - } + onOk: () => executeApiOperation(() => cancelApi(id), '已取消任务') }) } // 重试任务 - const handleRetry = (id) => { - Modal.confirm({ + function retryTask(id) { + confirmModal({ title: '确认重试', content: '确定要重新生成这个任务吗?', - okText: '确认', - cancelText: '取消', - onOk: async () => { - try { - await retryApi(id) - message.success('已重新提交任务') - onSuccess && onSuccess() - } catch (error) { - console.error('重试任务失败:', error) - message.error('操作失败') - } - } + onOk: () => executeApiOperation(() => retryApi(id), '已重新提交任务') }) } // 批量删除 - const handleBatchDelete = async (ids, deleteApiFn) => { + async function batchDeleteTasks(ids, deleteApiFn) { if (!ids || ids.length === 0) { message.warning('请选择要删除的任务') return } - Modal.confirm({ + confirmModal({ title: '确认批量删除', content: `确定要删除选中的 ${ids.length} 个任务吗?删除后无法恢复。`, - okText: '确认', - cancelText: '取消', okType: 'danger', onOk: async () => { - try { - const deleteFn = deleteApiFn || deleteApi - for (const id of ids) { - await deleteFn(id) - } - message.success(`成功删除 ${ids.length} 个任务`) - onSuccess && onSuccess() - } catch (error) { - console.error('批量删除失败:', error) - message.error('批量删除失败') + const deleteFn = deleteApiFn || deleteApi + for (const id of ids) { + await deleteFn(id) } + message.success(`成功删除 ${ids.length} 个任务`) } }) } // 下载单个文件 - const handleDownload = (url, filename = 'download') => { + function downloadFile(url, filename = 'download') { const link = document.createElement('a') link.href = url link.download = filename @@ -110,66 +129,67 @@ export function useTaskOperations(apis, onSuccess) { document.body.removeChild(link) } - // 批量下载 - const handleBatchDownload = async (urls, getSignedUrlsApi, taskId) => { - if (!urls || urls.length === 0) { - message.warning('没有可下载的文件') - return + // 获取签名URL + async function fetchSignedUrl(getSignedUrlsApi, taskId) { + const res = await getSignedUrlsApi(taskId) + if (res.code === API_SUCCESS_CODE && res.data) { + return res.data } + throw new Error('获取签名URL失败') + } + // 批量下载 + async function batchDownload(urls, getSignedUrlsApi, taskId) { try { - message.loading('正在获取下载链接...', 0) + message.loading('正在准备下载...', 0) let downloadUrls = urls // 如果需要获取签名URL if (getSignedUrlsApi && taskId) { - const res = await getSignedUrlsApi(taskId) - if (res.code === 0 && res.data) { - downloadUrls = res.data - } else { - message.warning('获取下载链接失败') - return - } + downloadUrls = await fetchSignedUrl(getSignedUrlsApi, taskId) + } + + // 检查下载链接 + if (!downloadUrls || downloadUrls.length === 0) { + message.warning('没有可下载的文件') + return } message.destroy() - message.loading('正在准备下载...', 0) + message.loading('正在下载文件...', 0) // 逐个触发下载,避免浏览器阻止多个弹窗 downloadUrls.forEach((url, index) => { setTimeout(() => { - handleDownload(url) - }, index * 500) // 每个下载间隔500ms + downloadFile(url) + }, index * DOWNLOAD_INTERVAL) }) message.destroy() message.success(`已触发 ${downloadUrls.length} 个文件的下载`) } catch (error) { - console.error('批量下载失败:', error) message.destroy() - message.error('获取下载链接失败') + message.error('下载失败,请稍后重试') } } - // 获取签名URL - const getSignedUrl = async (getSignedUrlsApi, taskId, index) => { + // 获取单个签名URL + async function getSignedUrl(getSignedUrlsApi, taskId, index) { try { const res = await getSignedUrlsApi(taskId) - if (res.code === 0 && res.data && res.data[index]) { + if (res.code === API_SUCCESS_CODE && res.data && res.data[index]) { return res.data[index] - } else { - message.warning('获取预览链接失败') - return null } + message.warning('获取预览链接失败') + return null } catch (error) { - console.error('获取签名URL失败:', error) message.error('获取预览链接失败') return null } } - // 预览 - const handlePreview = async (getSignedUrlsApi, taskId, index) => { + // 预览文件 + async function previewFile(getSignedUrlsApi, taskId, index) { try { message.loading('正在获取预览链接...', 0) const url = await getSignedUrl(getSignedUrlsApi, taskId, index) @@ -179,7 +199,6 @@ export function useTaskOperations(apis, onSuccess) { window.open(url, '_blank') } } catch (error) { - console.error('预览失败:', error) message.destroy() message.error('预览失败') } @@ -187,17 +206,17 @@ export function useTaskOperations(apis, onSuccess) { return { // 基本操作 - handleDelete, - handleCancel, - handleRetry, + handleDelete: deleteTask, + handleCancel: cancelTask, + handleRetry: retryTask, // 批量操作 - handleBatchDelete, - handleBatchDownload, + handleBatchDelete: batchDeleteTasks, + handleBatchDownload: batchDownload, // 下载和预览 - handleDownload, - handlePreview, + handleDownload: downloadFile, + handlePreview: previewFile, getSignedUrl } } diff --git a/frontend/app/web-gold/tsconfig.node.json b/frontend/app/web-gold/tsconfig.node.json index e8c30d389b..e428d50605 100644 --- a/frontend/app/web-gold/tsconfig.node.json +++ b/frontend/app/web-gold/tsconfig.node.json @@ -6,6 +6,6 @@ "moduleResolution": "bundler", "allowSyntheticDefaultImports": true }, - "include": ["vite.config.js"] + "include": ["vite.config.ts"] } diff --git a/frontend/app/web-gold/vite.config.js b/frontend/app/web-gold/vite.config.js deleted file mode 100644 index a0cf7c3683..0000000000 --- a/frontend/app/web-gold/vite.config.js +++ /dev/null @@ -1,78 +0,0 @@ -/* eslint-env node */ -import path from 'node:path' -import { defineConfig, loadEnv } from 'vite' -import { fileURLToPath, URL } from 'node:url' -import tailwindcss from '@tailwindcss/vite' -import process from 'node:process' -import vue from '@vitejs/plugin-vue' -import vueJsx from '@vitejs/plugin-vue-jsx' -import vueDevTools from 'vite-plugin-vue-devtools' -import UnoCSS from 'unocss/vite' -// import electron from 'vite-plugin-electron/simple' - -/** - * Vite 配置文件 - * 支持 TypeScript 和 JavaScript - * @param {Object} param0 - 配置参数 - * @param {string} param0.mode - 环境模式 - * @returns {import('vite').UserConfig} - */ -export default defineConfig(({ mode }) => { - const env = loadEnv(mode, process.cwd(), '') - const DEV_TOKEN = env.VITE_DEV_TOKEN || '' - const TENANT_ID = env.VITE_TENANT_ID || '1' - const API_TARGET = env.VITE_PROXY_TARGET || 'http://localhost:9900' - - return { - plugins: [ - vue(), - vueJsx(), - UnoCSS(), - vueDevTools(), - tailwindcss() - - ], - resolve: { - alias: { - '@': fileURLToPath(new URL('./src', import.meta.url)), - '@gold': fileURLToPath(new URL('../../', import.meta.url)) - }, - }, - server: { - proxy: { - // 代理 /webApi 开头的请求 - '/webApi': { - target: API_TARGET, - changeOrigin: true, - rewrite: (path) => path.replace(/^\/webApi/, ''), - configure: (proxy, options) => { - proxy.on('proxyReq', (proxyReq, req) => { - // 从客户端请求头中获取 authorization - const authHeader = req.headers?.authorization - if (authHeader) { - proxyReq.setHeader('authorization', authHeader) - } else if (DEV_TOKEN) { - // 兜底:使用环境变量中的 token - proxyReq.setHeader('authorization', `Bearer ${DEV_TOKEN}`) - } - - // 添加 RuoYi 租户 ID - proxyReq.setHeader('tenant-id', TENANT_ID) - }) - }, - }, - // OSS 代理 - 用于直传上传 - '/oss': { - target: 'https://muye-ai-chat.oss-cn-hangzhou.aliyuncs.com', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/oss/, ''), - headers: { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', - 'Access-Control-Allow-Headers': '*', - }, - }, - }, - }, - } -}) diff --git a/frontend/app/web-gold/vite.config.ts b/frontend/app/web-gold/vite.config.ts new file mode 100644 index 0000000000..371b76f68a --- /dev/null +++ b/frontend/app/web-gold/vite.config.ts @@ -0,0 +1,117 @@ +import { defineConfig, loadEnv } from 'vite' +import { fileURLToPath, URL } from 'node:url' +import tailwindcss from '@tailwindcss/vite' +import process from 'node:process' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' +import vueDevTools from 'vite-plugin-vue-devtools' +import UnoCSS from 'unocss/vite' +import { compression, defineAlgorithm } from 'vite-plugin-compression2' +import zlib from 'zlib' + +// 压缩配置常量 +const COMPRESSION_THRESHOLD = 1024 + +// 环境变量默认值 +const DEFAULT_ENV_VALUES = { + DEV_TOKEN: '', + TENANT_ID: '1', + API_TARGET: 'http://localhost:9900', +} + +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd(), '') + const DEV_TOKEN = env.VITE_DEV_TOKEN || DEFAULT_ENV_VALUES.DEV_TOKEN + const TENANT_ID = env.VITE_TENANT_ID || DEFAULT_ENV_VALUES.TENANT_ID + const API_TARGET = env.VITE_PROXY_TARGET || DEFAULT_ENV_VALUES.API_TARGET + + return { + plugins: [ + vue(), + vueJsx(), + UnoCSS(), + vueDevTools(), + tailwindcss(), + // 压缩配置:同时生成 gzip 和 brotli 压缩文件 + compression({ + algorithms: [ + defineAlgorithm('gzip', { level: 9 }), + defineAlgorithm('brotliCompress', { + params: { + [zlib.constants.BROTLI_PARAM_QUALITY]: 11 + } + }) + ], + threshold: COMPRESSION_THRESHOLD, + }), + ], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + '@gold': fileURLToPath(new URL('../../', import.meta.url)), + }, + }, + server: { + proxy: { + '/webApi': { + target: API_TARGET, + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/webApi/, ''), + configure: (proxy: any) => { + proxy.on('proxyReq', (proxyReq: any, req: any) => { + const authHeader = req.headers?.authorization + if (authHeader) { + proxyReq.setHeader('authorization', authHeader) + } else if (DEV_TOKEN) { + proxyReq.setHeader('authorization', `Bearer ${DEV_TOKEN}`) + } + proxyReq.setHeader('tenant-id', TENANT_ID) + }) + }, + }, + '/oss': { + target: 'https://muye-ai-chat.oss-cn-hangzhou.aliyuncs.com', + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/oss/, ''), + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', + 'Access-Control-Allow-Headers': '*', + }, + }, + }, + }, + build: { + // 生产环境去除 console 和 debugger + esbuild: { + drop: mode === 'production' ? ['console', 'debugger'] : [], + }, + // 使用 esbuild 压缩 + minify: 'esbuild', + // 关闭 source map 以减小体积 + sourcemap: false, + // 块大小警告阈值 + chunkSizeWarningLimit: 1500, + // 浏览器兼容性目标(使用最新的广泛支持版本) + target: 'baseline-widely-available', + // Rollup 配置优化 + rollupOptions: { + output: { + // 手动代码分割 + manualChunks: { + // Vue 生态系统 + 'vue-vendor': ['vue', 'vue-router', 'pinia'], + // UI 组件库 + 'ui-vendor': ['ant-design-vue', '@ant-design/icons-vue'], + // 工具库 + 'utils': ['dayjs', 'qs', 'path-to-regexp'], + // AI 相关 + 'ai-sdk': ['@ai-sdk/anthropic', '@ai-sdk/openai', 'ai'], + // 文件处理 + 'file-tools': ['xlsx', 'xlsx-js-style'], + }, + }, + }, + }, + } +}) diff --git a/frontend/package.json b/frontend/package.json index e1b2d3aebc..d5ae75eadd 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,6 +16,7 @@ "github-markdown-css": "^5.8.1", "localforage": "^1.10.0", "unocss": "^66.5.4", + "vite-plugin-compression2": "^2.4.0", "web-storage-cache": "^1.1.1" } }