refactor(ai): optimize AI action initialization and user state handling

This commit is contained in:
Ray.Hao
2025-11-18 00:18:25 +08:00
parent 0fbd489e49
commit 0ce931ee39
5 changed files with 103 additions and 124 deletions

View File

@@ -16,23 +16,24 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useAppStore, useSettingsStore } from "@/store"; import { useAppStore, useSettingsStore, useUserStore } from "@/store";
import { defaultSettings } from "@/settings"; import { defaultSettings } from "@/settings";
import { ThemeMode, ComponentSize } from "@/enums"; import { ThemeMode, ComponentSize } from "@/enums";
import AiAssistant from "@/components/AiAssistant/index.vue"; import AiAssistant from "@/components/AiAssistant/index.vue";
import { AuthStorage } from "@/utils/auth";
const appStore = useAppStore(); const appStore = useAppStore();
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const userStore = useUserStore();
const locale = computed(() => appStore.locale); const locale = computed(() => appStore.locale);
const size = computed(() => appStore.size as ComponentSize); const size = computed(() => appStore.size as ComponentSize);
const showWatermark = computed(() => settingsStore.showWatermark); const showWatermark = computed(() => settingsStore.showWatermark);
// 只有在启用 AI 助手且用户已登录时才显示 // 只有在启用 AI 助手且用户已登录时才显示
// 使用 userInfo 作为响应式依赖,当用户退出登录时会自动更新
const enableAiAssistant = computed(() => { const enableAiAssistant = computed(() => {
const isEnabled = settingsStore.enableAiAssistant; const isEnabled = settingsStore.enableAiAssistant;
const isLoggedIn = !!AuthStorage.getAccessToken(); const isLoggedIn = userStore.userInfo && Object.keys(userStore.userInfo).length > 0;
return isEnabled && isLoggedIn; return isEnabled && isLoggedIn;
}); });

View File

@@ -1,5 +1,6 @@
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import AiCommandApi from "@/api/ai"; import AiCommandApi from "@/api/ai";
import { ElMessage, ElMessageBox } from "element-plus";
/** /**
* AI 操作处理器(简化版) * AI 操作处理器(简化版)
@@ -57,8 +58,6 @@ export function useAiAction(options: UseAiActionOptions = {}) {
// 用于跟踪是否已卸载,防止在卸载后执行回调 // 用于跟踪是否已卸载,防止在卸载后执行回调
let isUnmounted = false; let isUnmounted = false;
// 存储待清理的 nextTick 回调
let pendingCallbacks: (() => void)[] = [];
/** /**
* 执行 AI 操作(统一处理确认、执行、反馈流程) * 执行 AI 操作(统一处理确认、执行、反馈流程)
@@ -174,7 +173,6 @@ export function useAiAction(options: UseAiActionOptions = {}) {
// 如果需要确认,先显示确认对话框 // 如果需要确认,先显示确认对话框
if (needConfirm && confirmMessage) { if (needConfirm && confirmMessage) {
const { ElMessageBox } = await import("element-plus");
try { try {
await ElMessageBox.confirm(confirmMessage, "AI 助手操作确认", { await ElMessageBox.confirm(confirmMessage, "AI 助手操作确认", {
confirmButtonText: "确认执行", confirmButtonText: "确认执行",
@@ -222,7 +220,7 @@ export function useAiAction(options: UseAiActionOptions = {}) {
/** /**
* 初始化:处理 URL 参数中的 AI 操作 * 初始化:处理 URL 参数中的 AI 操作
*/ */
function init() { async function init() {
if (isUnmounted) return; if (isUnmounted) return;
// 检查是否有 AI 助手传递的搜索参数 // 检查是否有 AI 助手传递的搜索参数
@@ -230,47 +228,35 @@ export function useAiAction(options: UseAiActionOptions = {}) {
const autoSearch = route.query.autoSearch as string; const autoSearch = route.query.autoSearch as string;
const aiActionParam = route.query.aiAction as string; const aiActionParam = route.query.aiAction as string;
// 判断是否有 AI 相关参数 // 在 nextTick 中执行,确保 DOM 已更新
const hasAiParams = (autoSearch === "true" && keywords) || aiActionParam; nextTick(async () => {
if (isUnmounted) return;
// 处理自动搜索 // 1. 优先执行 onInit 初始化数据(如果配置了)
if (autoSearch === "true" && keywords) { if (onInit) {
const callback = () => { try {
if (!isUnmounted) { await onInit();
handleAutoSearch(keywords); } catch (error) {
console.error("初始化数据失败:", error);
} }
}; }
pendingCallbacks.push(callback);
nextTick(callback);
}
// 处理 AI 操作 // 2. 处理自动搜索
if (aiActionParam) { if (autoSearch === "true" && keywords) {
const callback = () => { handleAutoSearch(keywords);
if (!isUnmounted) { }
try {
const aiAction = JSON.parse(decodeURIComponent(aiActionParam));
executeAiAction(aiAction);
} catch (error) {
console.error("解析 AI 操作失败:", error);
ElMessage.error("AI 操作参数解析失败");
}
}
};
pendingCallbacks.push(callback);
nextTick(callback);
}
// 如果没有 AI 参数,调用 onInit 回调(适合初始加载数据 // 3. 处理 AI 操作(在数据加载后执行
if (!hasAiParams && onInit) { if (aiActionParam) {
const callback = () => { try {
if (!isUnmounted) { const aiAction = JSON.parse(decodeURIComponent(aiActionParam));
onInit(); await executeAiAction(aiAction);
} catch (error) {
console.error("解析 AI 操作失败:", error);
ElMessage.error("AI 操作参数解析失败");
} }
}; }
pendingCallbacks.push(callback); });
nextTick(callback);
}
} }
// 组件挂载时自动初始化 // 组件挂载时自动初始化
@@ -281,8 +267,6 @@ export function useAiAction(options: UseAiActionOptions = {}) {
// 组件卸载时清理 // 组件卸载时清理
onBeforeUnmount(() => { onBeforeUnmount(() => {
isUnmounted = true; isUnmounted = true;
// 清理待执行的回调(虽然 nextTick 的回调无法直接取消,但至少标记为已卸载)
pendingCallbacks = [];
}); });
return { return {

View File

@@ -12,11 +12,11 @@ import { computed, ref } from "vue";
* const { selectedIds, handleSelectionChange, clearSelection } = useTableSelection<UserVO>(); * const { selectedIds, handleSelectionChange, clearSelection } = useTableSelection<UserVO>();
* ``` * ```
*/ */
export function useTableSelection<T extends { id: number }>() { export function useTableSelection<T extends { id: string | number }>() {
/** /**
* 选中的数据项ID列表 * 选中的数据项ID列表
*/ */
const selectedIds = ref<number[]>([]); const selectedIds = ref<(string | number)[]>([]);
/** /**
* 表格选中项变化处理 * 表格选中项变化处理
@@ -38,7 +38,7 @@ export function useTableSelection<T extends { id: number }>() {
* @param id 要检查的ID * @param id 要检查的ID
* @returns 是否被选中 * @returns 是否被选中
*/ */
function isSelected(id: number): boolean { function isSelected(id: string | number): boolean {
return selectedIds.value.includes(id); return selectedIds.value.includes(id);
} }

View File

@@ -63,15 +63,17 @@ const formComponents = {
// 投票通知 // 投票通知
const voteUrl = "https://gitee.com/activity/2025opensource?ident=I6VXEH"; const voteUrl = "https://gitee.com/activity/2025opensource?ident=I6VXEH";
// 保存通知实例,用于在组件卸载时关闭
let notificationInstance: ReturnType<typeof ElNotification> | null = null;
// 显示投票通知 // 显示投票通知
const showVoteNotification = () => { const showVoteNotification = () => {
ElNotification({ notificationInstance = ElNotification({
title: "⭐ Gitee 2025 开源评选 · 诚邀您的支持! 🙏", title: "⭐ Gitee 2025 开源评选 · 诚邀您的支持! 🙏",
message: `我正在参加 Gitee 2025 最受欢迎的开源软件投票活动,快来给我投票吧!<br/><a href="${voteUrl}" target="_blank" style="color: var(--el-color-primary); text-decoration: none; font-weight: 500;">点击投票 →</a>`, message: `我正在参加 Gitee 2025 最受欢迎的开源软件投票活动,快来给我投票吧!<br/><a href="${voteUrl}" target="_blank" style="color: var(--el-color-primary); text-decoration: none; font-weight: 500;">点击投票 →</a>`,
type: "success", type: "success",
position: "bottom-right", position: "bottom-right",
duration: 0, // 不自动关闭 duration: 0,
dangerouslyUseHTMLString: true, dangerouslyUseHTMLString: true,
}); });
}; };
@@ -82,6 +84,14 @@ onMounted(() => {
showVoteNotification(); showVoteNotification();
}, 500); }, 500);
}); });
// 组件卸载时关闭通知
onBeforeUnmount(() => {
if (notificationInstance) {
notificationInstance.close();
notificationInstance = null;
}
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -394,56 +394,10 @@ async function fetchUserList(): Promise<void> {
// ==================== 表格选择 ==================== // ==================== 表格选择 ====================
const { selectedIds, hasSelection, handleSelectionChange } = useTableSelection<UserPageVO>(); const { selectedIds, hasSelection, handleSelectionChange } = useTableSelection<UserPageVO>();
// ==================== AI 助手相关 ====================
useAiAction({
actionHandlers: {
/**
* AI 修改用户昵称
* 使用配置对象方式:自动处理确认、执行、反馈
*/
updateUserNickname: {
needConfirm: true,
callBackendApi: true, // 自动调用后端 API
confirmMessage: (args: any) =>
`AI 助手将执行以下操作:<br/>
<strong>修改用户:</strong> ${args.username}<br/>
<strong>新昵称:</strong> ${args.nickname}<br/><br/>
确认执行吗?`,
successMessage: (args: any) => `已将用户 ${args.username} 的昵称修改为 ${args.nickname}`,
execute: async () => {
// callBackendApi=true 时execute 可以为空
// Composable 会自动调用后端 API
},
},
/**
* AI 查询用户
* 使用配置对象方式:查询操作不需要确认
*/
queryUser: {
needConfirm: false, // 查询操作无需确认
successMessage: (args: any) => `已搜索:${args.keywords}`,
execute: async (args: any) => {
queryParams.keywords = args.keywords;
await handleQuery();
},
},
},
onRefresh: fetchData,
onAutoSearch: (keywords: string) => {
queryParams.keywords = keywords;
setTimeout(() => {
handleQuery();
ElMessage.success(`AI 助手已为您自动搜索:${keywords}`);
}, 300);
},
onInit: handleQuery,
});
// ==================== 查询操作 ==================== // ==================== 查询操作 ====================
/** /**
* 查询用户列表(重置到第一页) * 查询用户列表
*/ */
function handleQuery(): Promise<void> { function handleQuery(): Promise<void> {
queryParams.pageNum = 1; queryParams.pageNum = 1;
@@ -459,9 +413,6 @@ function handleResetQuery(): void {
queryParams.createTime = undefined; queryParams.createTime = undefined;
handleQuery(); handleQuery();
} }
// handleSelectionChange 已由 useTableSelection 提供
// ==================== 用户操作 ==================== // ==================== 用户操作 ====================
/** /**
@@ -569,8 +520,8 @@ const handleSubmit = useDebounceFn(async () => {
* 删除用户 * 删除用户
* @param id 用户ID单个删除时传入 * @param id 用户ID单个删除时传入
*/ */
function handleDelete(id?: number): void { function handleDelete(id?: string): void {
const userIds = id ? String(id) : selectedIds.value.join(","); const userIds = id ? id : selectedIds.value.join(",");
if (!userIds) { if (!userIds) {
ElMessage.warning("请勾选删除项"); ElMessage.warning("请勾选删除项");
@@ -578,9 +529,16 @@ function handleDelete(id?: number): void {
} }
// 安全检查:防止删除当前登录用户 // 安全检查:防止删除当前登录用户
if (isCurrentUserInDeleteList(id)) { const currentUserId = userStore.userInfo?.userId;
ElMessage.error("不能删除当前登录用户"); if (currentUserId) {
return; const isCurrentUserInList = id
? id === currentUserId
: selectedIds.value.some((selectedId) => String(selectedId) === currentUserId);
if (isCurrentUserInList) {
ElMessage.error("不能删除当前登录用户");
return;
}
} }
ElMessageBox.confirm("确认删除选中的用户吗?", "警告", { ElMessageBox.confirm("确认删除选中的用户吗?", "警告", {
@@ -606,23 +564,6 @@ function handleDelete(id?: number): void {
}); });
} }
/**
* 检查删除列表中是否包含当前登录用户
* @param singleId 单个删除的用户ID
*/
function isCurrentUserInDeleteList(singleId?: number): boolean {
const currentUserId = userStore.userInfo?.userId;
if (!currentUserId) return false;
// 单个删除检查
if (singleId) {
return String(singleId) === currentUserId;
}
// 批量删除检查
return selectedIds.value.some((id) => String(id) === currentUserId);
}
// ==================== 导入导出 ==================== // ==================== 导入导出 ====================
/** /**
@@ -666,6 +607,49 @@ async function handleExport(): Promise<void> {
} }
} }
// 初始化数据加载由 useAiAction 的 onInit 回调统一处理 // ==================== AI 助手相关 ====================
// 无需手动在 onMounted 中调用 handleQuery() useAiAction({
actionHandlers: {
/**
* AI 修改用户昵称
* 使用配置对象方式:自动处理确认、执行、反馈
*/
updateUserNickname: {
needConfirm: true,
callBackendApi: true, // 自动调用后端 API
confirmMessage: (args: any) =>
`AI 助手将执行以下操作:<br/>
<strong>修改用户:</strong> ${args.username}<br/>
<strong>新昵称:</strong> ${args.nickname}<br/><br/>
确认执行吗?`,
successMessage: (args: any) => `已将用户 ${args.username} 的昵称修改为 ${args.nickname}`,
execute: async () => {
// callBackendApi=true 时execute 可以为空
// Composable 会自动调用后端 API
},
},
/**
* AI 查询用户
* 使用配置对象方式:查询操作不需要确认
*/
queryUser: {
needConfirm: false, // 查询操作无需确认
successMessage: (args: any) => `已搜索:${args.keywords}`,
execute: async (args: any) => {
queryParams.keywords = args.keywords;
await handleQuery();
},
},
},
onRefresh: fetchUserList,
onAutoSearch: (keywords: string) => {
queryParams.keywords = keywords;
setTimeout(() => {
handleQuery();
ElMessage.success(`AI 助手已为您自动搜索:${keywords}`);
}, 300);
},
onInit: handleQuery,
});
</script> </script>