feat: 功能优化

This commit is contained in:
2026-01-18 21:41:44 +08:00
parent 41ebb5017d
commit b76367afed
7 changed files with 240 additions and 172 deletions

View File

@@ -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": []

View File

@@ -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"
}

View File

@@ -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<void>
* @param {Function} [apiHandlers.cancelApi] - 取消任务的 API 函数 (id) => Promise<void>
* @param {Function} [apiHandlers.retryApi] - 重试任务的 API 函数 (id) => Promise<void>
* @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
}
}

View File

@@ -6,6 +6,6 @@
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.js"]
"include": ["vite.config.ts"]
}

View File

@@ -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': '*',
},
},
},
},
}
})

View File

@@ -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'],
},
},
},
},
}
})

View File

@@ -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"
}
}