Files
vue3-element-admin/src/components/AiAssistant/index.vue
2025-11-10 08:04:34 +08:00

408 lines
9.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<!-- 悬浮按钮 -->
<div class="ai-assistant">
<!-- AI 助手图标按钮 -->
<el-button
v-if="!dialogVisible"
class="ai-fab-button"
type="primary"
circle
size="large"
@click="handleOpen"
>
<div class="i-svg:ai ai-icon" />
</el-button>
<!-- AI 对话框 -->
<el-dialog
v-model="dialogVisible"
title="AI 智能助手"
width="600px"
:close-on-click-modal="false"
draggable
class="ai-assistant-dialog"
>
<template #header>
<div class="dialog-header">
<div class="i-svg:ai header-icon" />
<span class="title">AI 智能助手</span>
</div>
</template>
<!-- 命令输入 -->
<div class="command-input">
<el-input
v-model="command"
type="textarea"
:rows="3"
placeholder="试试说:查询姓名为张三用户&#10;或者:跳转到用户管理&#10;按 Ctrl+Enter 快速发送"
:disabled="loading"
@keydown.ctrl.enter="handleExecute"
/>
</div>
<!-- 快捷命令示例 -->
<div class="quick-commands">
<div class="section-title">💡 试试这些命令</div>
<el-tag
v-for="example in examples"
:key="example"
class="command-tag"
@click="command = example"
>
{{ example }}
</el-tag>
</div>
<!-- AI 响应结果 -->
<div v-if="response" class="ai-response">
<el-alert :title="response.explanation" type="success" :closable="false" show-icon />
<!-- 将要执行的操作 -->
<div v-if="response.action" class="action-preview">
<div class="action-title">🎯 将要执行</div>
<div class="action-content">
<div v-if="response.action.type === 'navigate'">
<el-icon><Position /></el-icon>
跳转到
<strong>{{ response.action.pageName }}</strong>
<span v-if="response.action.query" class="query-info">
并搜索
<el-tag type="warning" size="small">{{ response.action.query }}</el-tag>
</span>
</div>
<div v-if="response.action.type === 'execute'">
<el-icon><Tools /></el-icon>
执行
<strong>{{ response.action.functionName }}</strong>
</div>
</div>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" :loading="loading" @click="handleExecute">
<el-icon><MagicStick /></el-icon>
执行命令
</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { useRouter } from "vue-router";
import { ElMessage } from "element-plus";
import AiCommandApi from "@/api/ai";
const router = useRouter();
// 状态管理
const dialogVisible = ref(false);
const command = ref("");
const loading = ref(false);
const response = ref<any>(null);
// 快捷命令示例
const examples = ["查询姓名为张三的用户", "跳转到用户管理", "打开角色管理页面", "查看系统日志"];
// 打开对话框
const handleOpen = () => {
dialogVisible.value = true;
command.value = "";
response.value = null;
};
// 关闭对话框
const handleClose = () => {
dialogVisible.value = false;
command.value = "";
response.value = null;
};
// 执行命令
const handleExecute = async () => {
if (!command.value.trim()) {
ElMessage.warning("请输入命令");
return;
}
loading.value = true;
try {
// 调用 AI API 解析命令
const result = await AiCommandApi.parseCommand({
command: command.value,
currentRoute: router.currentRoute.value.path,
currentComponent: router.currentRoute.value.name as string,
context: {
userRoles: [],
},
});
if (!result.success) {
ElMessage.error(result.error || "命令解析失败");
return;
}
// 解析 AI 返回的操作类型
const action = parseAction(result);
response.value = {
explanation: result.explanation,
action,
};
// 等待用户确认后执行
if (action) {
await executeAction(action);
}
} catch (error: any) {
console.error("AI 命令执行失败:", error);
ElMessage.error(error.message || "命令执行失败");
} finally {
loading.value = false;
}
};
// 解析 AI 返回的操作类型
const parseAction = (result: any) => {
const cmd = command.value.toLowerCase();
// 判断是否是导航命令
if (
cmd.includes("跳转") ||
cmd.includes("打开") ||
cmd.includes("进入") ||
cmd.includes("查询")
) {
// 提取关键字
let keyword = "";
let routePath = "";
let pageName = "";
if (cmd.includes("用户")) {
routePath = "/system/user";
pageName = "用户管理";
// 提取查询关键字
const match = cmd.match(/查询.*?([^\s]+).*?用户|用户.*?([^\s]+)/);
if (match) {
keyword = match[1] || match[2] || "";
// 去掉常见的修饰词
keyword = keyword.replace(/姓名为|名字叫|叫做|为/g, "");
}
} else if (cmd.includes("角色")) {
routePath = "/system/role";
pageName = "角色管理";
} else if (cmd.includes("菜单")) {
routePath = "/system/menu";
pageName = "菜单管理";
} else if (cmd.includes("部门")) {
routePath = "/system/dept";
pageName = "部门管理";
} else if (cmd.includes("字典")) {
routePath = "/system/dict";
pageName = "字典管理";
} else if (cmd.includes("日志")) {
routePath = "/system/log";
pageName = "系统日志";
}
if (routePath) {
return {
type: "navigate",
path: routePath,
pageName,
query: keyword || undefined,
};
}
}
// 如果不是导航命令,则是执行命令
if (result.functionCalls && result.functionCalls.length > 0) {
return {
type: "execute",
functionName: result.functionCalls[0].name,
functionCall: result.functionCalls[0],
};
}
return null;
};
// 执行操作
const executeAction = async (action: any) => {
if (action.type === "navigate") {
// 检查是否已经在目标页面
const currentPath = router.currentRoute.value.path;
if (currentPath === action.path) {
// 如果已经在目标页面
if (action.query) {
// 有查询关键字,直接在当前页面执行搜索
ElMessage.info(`您已在 ${action.pageName} 页面,为您执行搜索:${action.query}`);
// 触发路由更新,让页面执行搜索
router.replace({
path: action.path,
query: {
keywords: action.query,
autoSearch: "true",
_t: Date.now().toString(), // 添加时间戳强制刷新
},
});
} else {
// 没有查询关键字,只是跳转,给出提示
ElMessage.warning(`您已经在 ${action.pageName} 页面了`);
}
// 关闭对话框
handleClose();
return;
}
// 不在目标页面,正常跳转
ElMessage.success(`正在跳转到 ${action.pageName}...`);
// 延迟一下让用户看到提示
setTimeout(() => {
// 跳转并传递查询参数
router.push({
path: action.path,
query: action.query
? {
keywords: action.query, // 传递关键字参数
autoSearch: "true", // 标记自动搜索
}
: undefined,
});
// 关闭对话框
handleClose();
}, 800);
} else if (action.type === "execute") {
// 执行函数调用
ElMessage.info("功能开发中,请前往 AI 命令助手页面体验完整功能");
// 可以跳转到完整的 AI 命令页面
setTimeout(() => {
router.push("/function/ai-command");
handleClose();
}, 1000);
}
};
</script>
<style scoped lang="scss">
.ai-assistant {
.ai-fab-button {
position: fixed;
right: 30px;
bottom: 80px;
z-index: 9999;
width: 60px;
height: 60px;
box-shadow: 0 4px 12px rgba(2, 119, 252, 0.4);
transition: all 0.3s ease;
&:hover {
box-shadow: 0 6px 20px rgba(2, 119, 252, 0.6);
transform: scale(1.1);
}
.ai-icon {
width: 32px;
height: 32px;
}
}
}
.ai-assistant-dialog {
.dialog-header {
display: flex;
gap: 12px;
align-items: center;
.header-icon {
width: 28px;
height: 28px;
}
.title {
font-size: 18px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
.command-input {
margin-bottom: 16px;
}
.quick-commands {
margin-bottom: 20px;
.section-title {
margin-bottom: 8px;
font-size: 14px;
color: var(--el-text-color-secondary);
}
.command-tag {
margin-right: 8px;
margin-bottom: 8px;
cursor: pointer;
transition: all 0.3s;
&:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
}
}
.ai-response {
margin-top: 16px;
.action-preview {
padding: 12px;
margin-top: 12px;
background-color: var(--el-fill-color-light);
border-radius: 8px;
.action-title {
margin-bottom: 8px;
font-size: 14px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.action-content {
display: flex;
gap: 8px;
align-items: center;
color: var(--el-text-color-regular);
.el-icon {
color: var(--el-color-primary);
}
.query-info {
margin-left: 8px;
}
}
}
}
.dialog-footer {
display: flex;
gap: 12px;
justify-content: flex-end;
}
}
</style>