feat: 新增 AI 助手
This commit is contained in:
@@ -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
160
src/api/ai/index.ts
Normal 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
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 |
407
src/components/AiAssistant/index.vue
Normal file
407
src/components/AiAssistant/index.vue
Normal 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="试试说:查询姓名为张三用户 或者:跳转到用户管理 按 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>
|
||||
@@ -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,
|
||||
|
||||
@@ -32,6 +32,8 @@ export const defaultSettings: AppSettings = {
|
||||
watermarkContent: pkg.name,
|
||||
// 侧边栏配色方案
|
||||
sidebarColorScheme: SidebarColor.CLASSIC_BLUE,
|
||||
// 是否启用 AI 助手
|
||||
enableAiAssistant: false,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
|
||||
2
src/types/global.d.ts
vendored
2
src/types/global.d.ts
vendored
@@ -78,6 +78,8 @@ declare global {
|
||||
watermarkContent: string;
|
||||
/** 侧边栏配色方案 */
|
||||
sidebarColorScheme: "classic-blue" | "minimal-white";
|
||||
/** 是否启用 AI 助手 */
|
||||
enableAiAssistant: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -617,6 +617,10 @@ watch(
|
||||
// 组件挂载后加载访客统计数据和通知公告数据
|
||||
onMounted(() => {
|
||||
fetchVisitStatsData();
|
||||
|
||||
// 修改用户昵称为"奥特曼"
|
||||
userStore.userInfo.nickname = "奥特曼";
|
||||
console.log("用户昵称已修改为:", userStore.userInfo.nickname);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
599
src/views/demo/ai-command.vue
Normal file
599
src/views/demo/ai-command.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user