feat: 新增 AI 助手

This commit is contained in:
Ray.Hao
2025-11-10 08:04:34 +08:00
parent 05e3d1210a
commit c16f089071
13 changed files with 1231 additions and 1 deletions

View File

@@ -8,6 +8,9 @@
class="wh-full"
>
<router-view />
<!-- AI 助手 -->
<AiAssistant v-if="enableAiAssistant" />
</el-watermark>
</el-config-provider>
</template>
@@ -16,6 +19,7 @@
import { useAppStore, useSettingsStore } from "@/store";
import { defaultSettings } from "@/settings";
import { ThemeMode, ComponentSize } from "@/enums";
import AiAssistant from "@/components/AiAssistant/index.vue";
const appStore = useAppStore();
const settingsStore = useSettingsStore();
@@ -23,6 +27,7 @@ const settingsStore = useSettingsStore();
const locale = computed(() => appStore.locale);
const size = computed(() => appStore.size as ComponentSize);
const showWatermark = computed(() => settingsStore.showWatermark);
const enableAiAssistant = computed(() => settingsStore.enableAiAssistant);
// 明亮/暗黑主题水印字体颜色适配
const fontColor = computed(() => {

160
src/api/ai/index.ts Normal file
View File

@@ -0,0 +1,160 @@
import request from "@/utils/request";
/**
* AI 命令请求参数
*/
export interface AiCommandRequest {
/** 用户输入的自然语言命令 */
command: string;
/** 当前页面路由(用于上下文) */
currentRoute?: string;
/** 当前激活的组件名称 */
currentComponent?: string;
/** 额外上下文信息 */
context?: Record<string, any>;
}
/**
* 函数调用参数
*/
export interface FunctionCall {
/** 函数名称 */
name: string;
/** 函数描述 */
description?: string;
/** 参数对象 */
arguments: Record<string, any>;
}
/**
* AI 命令解析响应
*/
export interface AiCommandResponse {
/** 是否成功解析 */
success: boolean;
/** 解析后的函数调用列表 */
functionCalls: FunctionCall[];
/** AI 的理解和说明 */
explanation?: string;
/** 置信度 (0-1) */
confidence?: number;
/** 错误信息 */
error?: string;
/** 原始 LLM 响应(用于调试) */
rawResponse?: string;
}
/**
* AI 命令执行请求
*/
export interface AiExecuteRequest {
/** 要执行的函数调用 */
functionCall: FunctionCall;
/** 确认模式auto=自动执行, manual=需要用户确认 */
confirmMode?: "auto" | "manual";
/** 用户确认标志 */
userConfirmed?: boolean;
/** 幂等性令牌(防止重复执行) */
idempotencyKey?: string;
}
/**
* AI 命令执行响应
*/
export interface AiExecuteResponse {
/** 是否执行成功 */
success: boolean;
/** 执行结果数据 */
data?: any;
/** 执行结果说明 */
message?: string;
/** 影响的记录数 */
affectedRows?: number;
/** 错误信息 */
error?: string;
/** 审计ID用于追踪 */
auditId?: string;
/** 需要用户确认 */
requiresConfirmation?: boolean;
/** 确认提示信息 */
confirmationPrompt?: string;
}
/**
* AI 命令 API
*/
class AiCommandApi {
/**
* 解析自然语言命令
*
* @param data 命令请求参数
* @returns 解析结果
*/
static parseCommand(data: AiCommandRequest): Promise<AiCommandResponse> {
return request<any, AiCommandResponse>({
url: "/api/v1/ai/command/parse",
method: "post",
data,
});
}
/**
* 执行已解析的命令
*
* @param data 执行请求参数
* @returns 执行结果
*/
static executeCommand(data: AiExecuteRequest): Promise<AiExecuteResponse> {
return request<any, AiExecuteResponse>({
url: "/api/v1/ai/command/execute",
method: "post",
data,
});
}
/**
* 获取命令执行历史
*
* @param params 查询参数
* @returns 历史记录列表
*/
static getCommandHistory(params?: {
page?: number;
size?: number;
startTime?: string;
endTime?: string;
}) {
return request({
url: "/api/v1/ai/command/history",
method: "get",
params,
});
}
/**
* 获取可用的函数列表(用于展示或调试)
*
* @returns 函数列表
*/
static getAvailableFunctions() {
return request({
url: "/api/v1/ai/command/functions",
method: "get",
});
}
/**
* 撤销命令执行(如果支持)
*
* @param auditId 审计ID
* @returns 撤销结果
*/
static rollbackCommand(auditId: string) {
return request({
url: `/api/v1/ai/command/rollback/${auditId}`,
method: "post",
});
}
}
export default AiCommandApi;

1
src/assets/icons/ai.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -0,0 +1,407 @@
<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>

View File

@@ -18,6 +18,7 @@ export const STORAGE_KEYS = {
SHOW_TAGS_VIEW: `${APP_PREFIX}:ui:show_tags_view`, // 显示标签页视图
SHOW_APP_LOGO: `${APP_PREFIX}:ui:show_app_logo`, // 显示应用Logo
SHOW_WATERMARK: `${APP_PREFIX}:ui:show_watermark`, // 显示水印
ENABLE_AI_ASSISTANT: `${APP_PREFIX}:ui:enable_ai_assistant`, // 启用 AI 助手
LAYOUT: `${APP_PREFIX}:ui:layout`, // 布局模式
SIDEBAR_COLOR_SCHEME: `${APP_PREFIX}:ui:sidebar_color_scheme`, // 侧边栏配色方案
THEME: `${APP_PREFIX}:ui:theme`, // 主题模式
@@ -48,6 +49,7 @@ export const SETTINGS_KEYS = {
SHOW_TAGS_VIEW: STORAGE_KEYS.SHOW_TAGS_VIEW,
SHOW_APP_LOGO: STORAGE_KEYS.SHOW_APP_LOGO,
SHOW_WATERMARK: STORAGE_KEYS.SHOW_WATERMARK,
ENABLE_AI_ASSISTANT: STORAGE_KEYS.ENABLE_AI_ASSISTANT,
SIDEBAR_COLOR_SCHEME: STORAGE_KEYS.SIDEBAR_COLOR_SCHEME,
LAYOUT: STORAGE_KEYS.LAYOUT,
THEME_COLOR: STORAGE_KEYS.THEME_COLOR,

View File

@@ -32,6 +32,8 @@ export const defaultSettings: AppSettings = {
watermarkContent: pkg.name,
// 侧边栏配色方案
sidebarColorScheme: SidebarColor.CLASSIC_BLUE,
// 是否启用 AI 助手
enableAiAssistant: false,
};
/**

View File

@@ -11,6 +11,7 @@ interface SettingsState {
showTagsView: boolean;
showAppLogo: boolean;
showWatermark: boolean;
enableAiAssistant: boolean;
// 布局设置
layout: LayoutMode;
@@ -44,6 +45,12 @@ export const useSettingsStore = defineStore("setting", () => {
defaultSettings.showWatermark
);
// 是否启用 AI 助手
const enableAiAssistant = useStorage<boolean>(
STORAGE_KEYS.ENABLE_AI_ASSISTANT,
defaultSettings.enableAiAssistant
);
// 侧边栏配色方案
const sidebarColorScheme = useStorage<string>(
STORAGE_KEYS.SIDEBAR_COLOR_SCHEME,
@@ -64,6 +71,7 @@ export const useSettingsStore = defineStore("setting", () => {
showTagsView,
showAppLogo,
showWatermark,
enableAiAssistant,
sidebarColorScheme,
layout,
} as const;
@@ -131,6 +139,7 @@ export const useSettingsStore = defineStore("setting", () => {
showTagsView.value = defaultSettings.showTagsView;
showAppLogo.value = defaultSettings.showAppLogo;
showWatermark.value = defaultSettings.showWatermark;
enableAiAssistant.value = defaultSettings.enableAiAssistant;
sidebarColorScheme.value = defaultSettings.sidebarColorScheme;
layout.value = defaultSettings.layout as LayoutMode;
themeColor.value = defaultSettings.themeColor;
@@ -143,6 +152,7 @@ export const useSettingsStore = defineStore("setting", () => {
showTagsView,
showAppLogo,
showWatermark,
enableAiAssistant,
sidebarColorScheme,
layout,
themeColor,

View File

@@ -78,6 +78,8 @@ declare global {
watermarkContent: string;
/** 侧边栏配色方案 */
sidebarColorScheme: "classic-blue" | "minimal-white";
/** 是否启用 AI 助手 */
enableAiAssistant: boolean;
}
/**

View File

@@ -617,6 +617,10 @@ watch(
// 组件挂载后加载访客统计数据和通知公告数据
onMounted(() => {
fetchVisitStatsData();
// 修改用户昵称为"奥特曼"
userStore.userInfo.nickname = "奥特曼";
console.log("用户昵称已修改为:", userStore.userInfo.nickname);
});
</script>

View File

@@ -0,0 +1,599 @@
<template>
<div class="app-container">
<div class="ai-command-panel">
<!-- 命令输入区 -->
<el-card class="command-input-card" shadow="never">
<template #header>
<div class="card-header">
<span class="title">
<el-icon><MagicStick /></el-icon>
AI 命令助手
</span>
<el-tag v-if="mcpConnected" type="success" size="small">
<el-icon><Connection /></el-icon>
MCP 已连接
</el-tag>
<el-tag v-else type="info" size="small">
<el-icon><CircleClose /></el-icon>
MCP 未连接
</el-tag>
</div>
</template>
<el-input
v-model="commandText"
type="textarea"
:rows="3"
placeholder="输入自然语言命令,例如:删除姓名为张三的用户"
:disabled="loading"
@keydown.ctrl.enter="handleParseCommand"
/>
<div class="action-buttons">
<el-button
type="primary"
:loading="loading"
:disabled="!commandText.trim()"
@click="handleParseCommand"
>
<el-icon><Search /></el-icon>
解析命令 (Ctrl+Enter)
</el-button>
<el-button @click="handleClear">
<el-icon><Delete /></el-icon>
清空
</el-button>
<el-button @click="handleShowHistory">
<el-icon><Clock /></el-icon>
历史记录
</el-button>
</div>
</el-card>
<!-- 解析结果展示 -->
<el-card v-if="parseResult" class="result-card" shadow="never">
<template #header>
<div class="card-header">
<span class="title">解析结果</span>
<el-tag v-if="parseResult.success" type="success" size="small">
置信度: {{ ((parseResult.confidence ?? 0) * 100).toFixed(1) }}%
</el-tag>
<el-tag v-else type="danger" size="small">解析失败</el-tag>
</div>
</template>
<!-- AI 理解说明 -->
<el-alert
v-if="parseResult.explanation"
:title="parseResult.explanation"
type="info"
:closable="false"
show-icon
class="explanation-alert"
/>
<!-- 错误信息 -->
<el-alert
v-if="parseResult.error"
:title="parseResult.error"
type="error"
:closable="false"
show-icon
class="error-alert"
/>
<!-- 函数调用列表 -->
<div
v-if="parseResult.functionCalls && parseResult.functionCalls.length > 0"
class="function-calls"
>
<div
v-for="(funcCall, index) in parseResult.functionCalls"
:key="index"
class="function-call-item"
>
<el-card shadow="hover">
<template #header>
<div class="function-header">
<span class="function-name">
<el-icon><Tools /></el-icon>
{{ funcCall.name }}
</span>
<el-tag type="primary" size="small">步骤 {{ index + 1 }}</el-tag>
</div>
</template>
<div class="function-content">
<div v-if="funcCall.description" class="function-description">
<strong>说明</strong>
{{ funcCall.description }}
</div>
<div class="function-arguments">
<strong>参数</strong>
<el-descriptions :column="1" border size="small">
<el-descriptions-item
v-for="(value, key) in funcCall.arguments"
:key="key"
:label="key"
>
<el-tag v-if="typeof value === 'boolean'" :type="value ? 'success' : 'info'">
{{ value }}
</el-tag>
<el-tag v-else-if="typeof value === 'number'" type="warning">
{{ value }}
</el-tag>
<span v-else-if="typeof value === 'object'">
<code>{{ JSON.stringify(value, null, 2) }}</code>
</span>
<span v-else>{{ value }}</span>
</el-descriptions-item>
</el-descriptions>
</div>
<div class="function-actions">
<el-button
type="primary"
size="small"
:loading="executingIndex === index"
@click="handleExecute(funcCall, index)"
>
<el-icon><VideoPlay /></el-icon>
执行此步骤
</el-button>
<el-button
v-if="executeResults[index]"
type="success"
size="small"
@click="handleViewResult(index)"
>
<el-icon><View /></el-icon>
查看结果
</el-button>
</div>
<!-- 执行结果 -->
<div v-if="executeResults[index]" class="execute-result">
<el-divider />
<el-alert
:title="executeResults[index].success ? '执行成功' : '执行失败'"
:type="executeResults[index].success ? 'success' : 'error'"
:closable="false"
show-icon
>
<template v-if="executeResults[index].message">
{{ executeResults[index].message }}
</template>
<template v-if="executeResults[index].affectedRows">
影响 {{ executeResults[index].affectedRows }} 条记录
</template>
</el-alert>
</div>
</div>
</el-card>
</div>
<!-- 批量执行 -->
<div class="batch-actions">
<el-button
type="primary"
:loading="batchExecuting"
:disabled="allExecuted"
@click="handleBatchExecute"
>
<el-icon><DArrowRight /></el-icon>
批量执行所有步骤
</el-button>
<el-button v-if="hasExecutedSteps" type="danger" @click="handleClearResults">
<el-icon><RefreshLeft /></el-icon>
清除结果
</el-button>
</div>
</div>
</el-card>
<!-- 上下文信息开发模式 -->
<el-card v-if="showContext && contextInfo" class="context-card" shadow="never">
<template #header>
<div class="card-header">
<span class="title">当前上下文</span>
<el-button type="info" size="small" @click="showContext = !showContext">
{{ showContext ? "隐藏" : "显示" }}上下文
</el-button>
</div>
</template>
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="当前路由">
{{ contextInfo.currentRoute }}
</el-descriptions-item>
<el-descriptions-item label="当前组件">
{{ contextInfo.currentComponent }}
</el-descriptions-item>
<el-descriptions-item label="MCP 端点">
{{ contextInfo.mcpEndpoint || "未配置" }}
</el-descriptions-item>
<el-descriptions-item label="用户角色">
{{ contextInfo.userRole || "未知" }}
</el-descriptions-item>
</el-descriptions>
</el-card>
<!-- 底部操作按钮 -->
<div class="footer-actions">
<el-button type="info" @click="showContext = !showContext">
{{ showContext ? "隐藏" : "显示" }}上下文
</el-button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useRoute } from "vue-router";
import { ElMessage, ElMessageBox } from "element-plus";
import AiCommandApi, {
type FunctionCall,
type AiCommandResponse,
type AiExecuteResponse,
} from "@/api/ai";
import { useUserStoreHook } from "@/store/modules/user-store";
const route = useRoute();
const userStore = useUserStoreHook();
// ==================== 状态管理 ====================
const commandText = ref("");
const loading = ref(false);
const parseResult = ref<AiCommandResponse | null>(null);
const executeResults = ref<Record<number, AiExecuteResponse>>({});
const executingIndex = ref<number | null>(null);
const batchExecuting = ref(false);
const showContext = ref(false);
const mcpConnected = ref(false);
// ==================== 上下文信息 ====================
const contextInfo = computed(() => ({
currentRoute: route.path,
currentComponent: route.name as string,
mcpEndpoint: import.meta.env.DEV
? `http://localhost:${import.meta.env.VITE_APP_PORT}/__mcp/sse`
: null,
userRole: userStore.userInfo?.roles?.join(", ") || "未知",
}));
// ==================== 计算属性 ====================
const allExecuted = computed(() => {
if (!parseResult.value?.functionCalls) return false;
return parseResult.value.functionCalls.every((_, index) => executeResults.value[index]?.success);
});
const hasExecutedSteps = computed(() => {
return Object.keys(executeResults.value).length > 0;
});
// ==================== 方法 ====================
/**
* 解析命令
*/
const handleParseCommand = async () => {
if (!commandText.value.trim()) {
ElMessage.warning("请输入命令");
return;
}
loading.value = true;
parseResult.value = null;
executeResults.value = {};
try {
const response = await AiCommandApi.parseCommand({
command: commandText.value,
currentRoute: contextInfo.value.currentRoute,
currentComponent: contextInfo.value.currentComponent,
context: {
userRoles: userStore.userInfo?.roles || [],
},
});
parseResult.value = response;
if (response.success && response.functionCalls.length > 0) {
ElMessage.success(`成功解析为 ${response.functionCalls.length} 个操作步骤`);
} else if (!response.success) {
ElMessage.error(response.error || "命令解析失败");
}
} catch (error: any) {
console.error("解析命令失败:", error);
ElMessage.error(error.message || "命令解析失败");
} finally {
loading.value = false;
}
};
/**
* 执行单个函数调用
*/
const handleExecute = async (funcCall: FunctionCall, index: number) => {
// 危险操作需要确认
const isDangerous = ["delete", "remove", "drop", "truncate"].some((keyword) =>
funcCall.name.toLowerCase().includes(keyword)
);
if (isDangerous) {
try {
await ElMessageBox.confirm(
`确认执行此操作吗?\n\n函数${funcCall.name}\n参数${JSON.stringify(funcCall.arguments, null, 2)}`,
"危险操作确认",
{
confirmButtonText: "确认执行",
cancelButtonText: "取消",
type: "warning",
dangerouslyUseHTMLString: false,
}
);
} catch {
ElMessage.info("已取消操作");
return;
}
}
executingIndex.value = index;
try {
const result = await AiCommandApi.executeCommand({
functionCall: funcCall,
confirmMode: isDangerous ? "manual" : "auto",
userConfirmed: isDangerous,
idempotencyKey: `${Date.now()}-${index}`,
});
executeResults.value[index] = result;
if (result.success) {
ElMessage.success(result.message || "执行成功");
} else {
ElMessage.error(result.error || "执行失败");
}
} catch (error: any) {
console.error("执行命令失败:", error);
ElMessage.error(error.message || "执行失败");
} finally {
executingIndex.value = null;
}
};
/**
* 批量执行所有步骤
*/
const handleBatchExecute = async () => {
if (!parseResult.value?.functionCalls) return;
const confirmMessage = `确认批量执行 ${parseResult.value.functionCalls.length} 个步骤吗?`;
try {
await ElMessageBox.confirm(confirmMessage, "批量执行确认", {
confirmButtonText: "确认",
cancelButtonText: "取消",
type: "warning",
});
} catch {
return;
}
batchExecuting.value = true;
for (let i = 0; i < parseResult.value.functionCalls.length; i++) {
if (executeResults.value[i]?.success) {
continue; // 跳过已成功执行的
}
await handleExecute(parseResult.value.functionCalls[i], i);
// 如果执行失败,停止后续执行
if (!executeResults.value[i]?.success) {
ElMessage.warning(`步骤 ${i + 1} 执行失败,已停止后续步骤`);
break;
}
}
batchExecuting.value = false;
};
/**
* 查看执行结果
*/
const handleViewResult = (index: number) => {
const result = executeResults.value[index];
if (!result) return;
ElMessageBox.alert(`<pre>${JSON.stringify(result.data, null, 2)}</pre>`, "执行结果详情", {
dangerouslyUseHTMLString: true,
confirmButtonText: "关闭",
});
};
/**
* 清空输入
*/
const handleClear = () => {
commandText.value = "";
parseResult.value = null;
executeResults.value = {};
};
/**
* 清除执行结果
*/
const handleClearResults = () => {
executeResults.value = {};
ElMessage.success("已清除执行结果");
};
/**
* 显示历史记录
*/
const handleShowHistory = () => {
ElMessage.info("历史记录功能开发中...");
// TODO: 实现历史记录弹窗
};
/**
* 检查 MCP 连接状态
*/
const checkMcpConnection = async () => {
if (!import.meta.env.DEV) {
mcpConnected.value = false;
return;
}
try {
const endpoint = contextInfo.value.mcpEndpoint;
if (!endpoint) {
mcpConnected.value = false;
return;
}
// 简单的连接检查(实际应该有更可靠的方式)
const response = await fetch(endpoint, { method: "HEAD" });
mcpConnected.value = response.ok;
} catch {
mcpConnected.value = false;
}
};
// ==================== 生命周期 ====================
onMounted(() => {
checkMcpConnection();
});
</script>
<style scoped lang="scss">
.ai-command-panel {
.command-input-card {
margin-bottom: 16px;
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
.title {
display: flex;
gap: 8px;
align-items: center;
font-weight: 600;
}
}
.action-buttons {
display: flex;
gap: 8px;
margin-top: 12px;
}
}
.result-card {
margin-bottom: 16px;
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
.title {
font-weight: 600;
}
}
.explanation-alert,
.error-alert {
margin-bottom: 16px;
}
.function-calls {
.function-call-item {
margin-bottom: 12px;
&:last-child {
margin-bottom: 0;
}
.function-header {
display: flex;
align-items: center;
justify-content: space-between;
.function-name {
display: flex;
gap: 8px;
align-items: center;
font-size: 14px;
font-weight: 600;
}
}
.function-content {
.function-description {
padding: 8px;
margin-bottom: 12px;
background-color: var(--el-fill-color-light);
border-radius: 4px;
}
.function-arguments {
margin-bottom: 12px;
code {
display: block;
padding: 8px;
font-size: 12px;
word-break: break-all;
white-space: pre-wrap;
background-color: var(--el-fill-color);
border-radius: 4px;
}
}
.function-actions {
display: flex;
gap: 8px;
}
.execute-result {
margin-top: 12px;
}
}
}
.batch-actions {
display: flex;
gap: 8px;
padding-top: 16px;
margin-top: 16px;
border-top: 1px dashed var(--el-border-color);
}
}
}
.context-card {
margin-bottom: 16px;
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
.title {
font-weight: 600;
}
}
}
.footer-actions {
display: flex;
justify-content: center;
margin-top: 16px;
}
}
</style>

View File

@@ -246,6 +246,8 @@
<script setup lang="ts">
import { useAppStore } from "@/store/modules/app-store";
import { DeviceEnum } from "@/enums/settings/device-enum";
import { useRoute } from "vue-router";
import { ElMessage } from "element-plus";
import UserAPI, { UserForm, UserPageQuery, UserPageVO } from "@/api/system/user-api";
import DeptAPI from "@/api/system/dept-api";
@@ -261,6 +263,7 @@ defineOptions({
});
const appStore = useAppStore();
const route = useRoute();
const queryFormRef = ref();
const userFormRef = ref();
@@ -521,6 +524,21 @@ function handleExport() {
}
onMounted(() => {
handleQuery();
// 检查是否有 AI 助手传递的搜索参数
const keywords = route.query.keywords as string;
const autoSearch = route.query.autoSearch as string;
if (autoSearch === "true" && keywords) {
// 自动填充搜索关键字
queryParams.keywords = keywords;
// 延迟一下,让用户看到自动填充的效果
setTimeout(() => {
handleQuery();
// 显示提示
ElMessage.success(`AI 助手已为您自动搜索:${keywords}`);
}, 300);
} else {
handleQuery();
}
});
</script>