refactor(ai): optimize AI action initialization and user state handling
This commit is contained in:
@@ -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;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user