From 0fbd489e4920d17167d06d908d4b2ec116e67df9 Mon Sep 17 00:00:00 2001 From: "Ray.Hao" <1490493387@qq.com> Date: Mon, 17 Nov 2025 21:43:20 +0800 Subject: [PATCH] chore(deps): upgrade dependencies and refactor AI action handling --- package.json | 26 +- src/components/AiAssistant/index.vue | 2 +- src/composables/index.ts | 2 + src/composables/useAiAction.ts | 108 +++++- src/composables/useTableSelection.ts | 63 ++++ src/views/login/index.vue | 22 ++ src/views/system/user/index.vue | 492 +++++++++++++++------------ 7 files changed, 478 insertions(+), 237 deletions(-) create mode 100644 src/composables/useTableSelection.ts diff --git a/package.json b/package.json index b539fc38..afa7cb7a 100644 --- a/package.json +++ b/package.json @@ -52,21 +52,21 @@ "@wangeditor-next/editor": "^5.6.47", "@wangeditor-next/editor-for-vue": "^5.1.14", "animate.css": "^4.1.1", - "axios": "^1.13.1", + "axios": "^1.13.2", "codemirror": "^5.65.20", "codemirror-editor-vue3": "^2.8.0", "default-passive-events": "^2.0.0", "echarts": "^6.0.0", - "element-plus": "^2.11.7", + "element-plus": "^2.11.8", "exceljs": "^4.4.0", "lodash-es": "^4.17.21", "nprogress": "^0.2.0", "path-browserify": "^1.0.1", "path-to-regexp": "^8.3.0", - "pinia": "^3.0.3", + "pinia": "^3.0.4", "qs": "^6.14.0", "sortablejs": "^1.15.6", - "vue": "^3.5.22", + "vue": "^3.5.24", "vue-draggable-plus": "^0.6.0", "vue-i18n": "^11.1.12", "vue-router": "^4.6.3", @@ -79,15 +79,15 @@ "@iconify/utils": "^2.3.0", "@types/codemirror": "^5.60.17", "@types/lodash-es": "^4.17.12", - "@types/node": "^24.10.0", + "@types/node": "^24.10.1", "@types/nprogress": "^0.2.3", "@types/path-browserify": "^1.0.3", "@types/qs": "^6.14.0", "@types/sortablejs": "^1.15.9", - "@typescript-eslint/eslint-plugin": "^8.46.3", - "@typescript-eslint/parser": "^8.46.3", + "@typescript-eslint/eslint-plugin": "^8.46.4", + "@typescript-eslint/parser": "^8.46.4", "@vitejs/plugin-vue": "^6.0.1", - "autoprefixer": "^10.4.21", + "autoprefixer": "^10.4.22", "commitizen": "^4.3.1", "cz-git": "^1.12.0", "eslint": "^9.39.1", @@ -101,7 +101,7 @@ "postcss-html": "^1.8.0", "postcss-scss": "^4.0.9", "prettier": "^3.6.2", - "sass": "^1.93.3", + "sass": "^1.94.0", "stylelint": "^16.25.0", "stylelint-config-html": "^1.1.0", "stylelint-config-recess-order": "^6.1.0", @@ -109,13 +109,13 @@ "stylelint-config-recommended-scss": "^14.1.0", "stylelint-config-recommended-vue": "^1.6.1", "stylelint-prettier": "^5.0.3", - "terser": "^5.44.0", + "terser": "^5.44.1", "typescript": "^5.9.3", - "typescript-eslint": "^8.46.3", - "unocss": "^66.5.4", + "typescript-eslint": "^8.46.4", + "unocss": "^66.5.6", "unplugin-auto-import": "^19.3.0", "unplugin-vue-components": "^28.8.0", - "vite": "^7.1.12", + "vite": "^7.2.2", "vite-plugin-mock-dev-server": "^2.0.2", "vue-eslint-parser": "^10.2.0", "vue-tsc": "^2.2.12" diff --git a/src/components/AiAssistant/index.vue b/src/components/AiAssistant/index.vue index 4bcb2e65..5458bf59 100644 --- a/src/components/AiAssistant/index.vue +++ b/src/components/AiAssistant/index.vue @@ -183,7 +183,7 @@ const handleExecute = async () => { // 优先检测无需调用 AI 的纯跳转命令 const directNavigation = tryDirectNavigate(rawCommand); - if (directNavigation) { + if (directNavigation && directNavigation.action) { response.value = directNavigation; await executeAction(directNavigation.action); return; diff --git a/src/composables/index.ts b/src/composables/index.ts index f8304c98..4bff69b9 100644 --- a/src/composables/index.ts +++ b/src/composables/index.ts @@ -10,3 +10,5 @@ export { useDeviceDetection } from "./layout/useDeviceDetection"; export { useAiAction } from "./useAiAction"; export type { UseAiActionOptions, AiActionHandler } from "./useAiAction"; + +export { useTableSelection } from "./useTableSelection"; diff --git a/src/composables/useAiAction.ts b/src/composables/useAiAction.ts index 83e43c09..14756751 100644 --- a/src/composables/useAiAction.ts +++ b/src/composables/useAiAction.ts @@ -1,12 +1,25 @@ import { useRoute } from "vue-router"; -import { ElMessage } from "element-plus"; -import { onMounted, onBeforeUnmount, nextTick } from "vue"; import AiCommandApi from "@/api/ai"; /** - * AI 操作处理器类型 + * AI 操作处理器(简化版) + * + * 可以是简单函数,也可以是配置对象 */ -export type AiActionHandler = (args: any) => Promise | void; +export type AiActionHandler = + | ((args: T) => Promise | void) + | { + /** 执行函数 */ + execute: (args: T) => Promise | void; + /** 是否需要确认(默认 true) */ + needConfirm?: boolean; + /** 确认消息(支持函数或字符串) */ + confirmMessage?: string | ((args: T) => string); + /** 成功消息(支持函数或字符串) */ + successMessage?: string | ((args: T) => string); + /** 是否调用后端 API(默认 false,如果为 true 则自动调用 executeCommand) */ + callBackendApi?: boolean; + }; /** * AI 操作配置 @@ -18,6 +31,8 @@ export interface UseAiActionOptions { onRefresh?: () => Promise | void; /** 自动搜索处理函数 */ onAutoSearch?: (keywords: string) => void; + /** 初始化回调(在没有 AI 参数时调用,适合初始加载数据) */ + onInit?: () => Promise | void; /** 当前路由路径(用于执行命令时传递) */ currentRoute?: string; } @@ -32,7 +47,13 @@ export interface UseAiActionOptions { */ export function useAiAction(options: UseAiActionOptions = {}) { const route = useRoute(); - const { actionHandlers = {}, onRefresh, onAutoSearch, currentRoute = route.path } = options; + const { + actionHandlers = {}, + onRefresh, + onAutoSearch, + onInit, + currentRoute = route.path, + } = options; // 用于跟踪是否已卸载,防止在卸载后执行回调 let isUnmounted = false; @@ -40,10 +61,11 @@ export function useAiAction(options: UseAiActionOptions = {}) { let pendingCallbacks: (() => void)[] = []; /** - * 执行 AI 操作 + * 执行 AI 操作(统一处理确认、执行、反馈流程) */ async function executeAiAction(action: any) { if (isUnmounted) return; + // 兼容两种入参:{ functionName, arguments } 或 { functionCall: { name, arguments } } const fnCall = action.functionCall ?? { name: action.functionName, @@ -63,12 +85,68 @@ export function useAiAction(options: UseAiActionOptions = {}) { } try { - await handler(fnCall.arguments); - // 操作成功后刷新数据 + // 判断处理器类型(函数 or 配置对象) + const isSimpleFunction = typeof handler === "function"; + + if (isSimpleFunction) { + // 简单函数形式:直接执行 + await handler(fnCall.arguments); + } else { + // 配置对象形式:统一处理确认、执行、反馈 + const config = handler; + + // 1. 确认阶段(默认需要确认) + if (config.needConfirm !== false) { + const confirmMsg = + typeof config.confirmMessage === "function" + ? config.confirmMessage(fnCall.arguments) + : config.confirmMessage || "确认执行此操作吗?"; + + await ElMessageBox.confirm(confirmMsg, "AI 助手操作确认", { + confirmButtonText: "确认执行", + cancelButtonText: "取消", + type: "warning", + dangerouslyUseHTMLString: true, + }); + } + + // 2. 执行阶段 + if (config.callBackendApi) { + // 自动调用后端 API + await AiCommandApi.executeCommand({ + originalCommand: action.originalCommand || "", + confirmMode: "manual", + userConfirmed: true, + currentRoute, + functionCall: { + name: fnCall.name, + arguments: fnCall.arguments, + }, + }); + } else { + // 执行自定义函数 + await config.execute(fnCall.arguments); + } + + // 3. 成功反馈 + const successMsg = + typeof config.successMessage === "function" + ? config.successMessage(fnCall.arguments) + : config.successMessage || "操作执行成功"; + ElMessage.success(successMsg); + } + + // 4. 刷新数据 if (onRefresh) { await onRefresh(); } } catch (error: any) { + // 处理取消操作 + if (error === "cancel") { + ElMessage.info("已取消操作"); + return; + } + console.error("AI 操作执行失败:", error); ElMessage.error(error.message || "操作执行失败"); } @@ -152,6 +230,9 @@ export function useAiAction(options: UseAiActionOptions = {}) { const autoSearch = route.query.autoSearch as string; const aiActionParam = route.query.aiAction as string; + // 判断是否有 AI 相关参数 + const hasAiParams = (autoSearch === "true" && keywords) || aiActionParam; + // 处理自动搜索 if (autoSearch === "true" && keywords) { const callback = () => { @@ -179,6 +260,17 @@ export function useAiAction(options: UseAiActionOptions = {}) { pendingCallbacks.push(callback); nextTick(callback); } + + // 如果没有 AI 参数,调用 onInit 回调(适合初始加载数据) + if (!hasAiParams && onInit) { + const callback = () => { + if (!isUnmounted) { + onInit(); + } + }; + pendingCallbacks.push(callback); + nextTick(callback); + } } // 组件挂载时自动初始化 diff --git a/src/composables/useTableSelection.ts b/src/composables/useTableSelection.ts new file mode 100644 index 00000000..e960a24c --- /dev/null +++ b/src/composables/useTableSelection.ts @@ -0,0 +1,63 @@ +import { computed, ref } from "vue"; + +/** + * 表格行选择 Composable + * + * @description 提供统一的表格行选择逻辑,包括选中ID管理和清空选择 + * @template T 数据项类型,必须包含 id 属性 + * @returns 返回选中的ID列表、选择变化处理函数、清空选择函数 + * + * @example + * ```typescript + * const { selectedIds, handleSelectionChange, clearSelection } = useTableSelection(); + * ``` + */ +export function useTableSelection() { + /** + * 选中的数据项ID列表 + */ + const selectedIds = ref([]); + + /** + * 表格选中项变化处理 + * @param selection 选中的行数据列表 + */ + function handleSelectionChange(selection: T[]): void { + selectedIds.value = selection.map((item) => item.id); + } + + /** + * 清空选择 + */ + function clearSelection(): void { + selectedIds.value = []; + } + + /** + * 检查指定ID是否被选中 + * @param id 要检查的ID + * @returns 是否被选中 + */ + function isSelected(id: number): boolean { + return selectedIds.value.includes(id); + } + + /** + * 获取选中的数量 + */ + const selectedCount = computed(() => selectedIds.value.length); + + /** + * 是否有选中项 + */ + const hasSelection = computed(() => selectedIds.value.length > 0); + + return { + selectedIds, + selectedCount, + hasSelection, + handleSelectionChange, + clearSelection, + isSelected, + }; +} diff --git a/src/views/login/index.vue b/src/views/login/index.vue index 05b89889..d21de83b 100644 --- a/src/views/login/index.vue +++ b/src/views/login/index.vue @@ -60,6 +60,28 @@ const formComponents = { register: defineAsyncComponent(() => import("./components/Register.vue")), resetPwd: defineAsyncComponent(() => import("./components/ResetPwd.vue")), }; + +// 投票通知 +const voteUrl = "https://gitee.com/activity/2025opensource?ident=I6VXEH"; + +// 显示投票通知 +const showVoteNotification = () => { + ElNotification({ + title: "⭐ Gitee 2025 开源评选 · 诚邀您的支持! 🙏", + message: `我正在参加 Gitee 2025 最受欢迎的开源软件投票活动,快来给我投票吧!
点击投票 →`, + type: "success", + position: "bottom-right", + duration: 0, // 不自动关闭 + dangerouslyUseHTMLString: true, + }); +}; + +// 延迟显示 +onMounted(() => { + setTimeout(() => { + showVoteNotification(); + }, 500); +});