Add V2 admin console
This commit is contained in:
300
ai_knowledge_base_v2/apps/admin-web/src/App.vue
Normal file
300
ai_knowledge_base_v2/apps/admin-web/src/App.vue
Normal file
@@ -0,0 +1,300 @@
|
||||
<script setup lang="ts">
|
||||
import { ElMessage } from "element-plus";
|
||||
import { onMounted, reactive, ref } from "vue";
|
||||
|
||||
import { api, clearToken, getToken, saveToken } from "./services/api";
|
||||
import type { AdminProfile, AdminUser, DashboardStats, KnowledgeItem, ModelItem } from "./types/api";
|
||||
|
||||
const admin = ref<AdminProfile | null>(null);
|
||||
const activeMenu = ref("dashboard");
|
||||
const loading = ref(false);
|
||||
|
||||
const loginForm = reactive({ username: "admin", password: "admin123456" });
|
||||
const stats = ref<DashboardStats | null>(null);
|
||||
const users = ref<AdminUser[]>([]);
|
||||
const userKeyword = ref("");
|
||||
const knowledge = ref<KnowledgeItem[]>([]);
|
||||
const models = ref<ModelItem[]>([]);
|
||||
const configs = ref<Record<string, unknown>[]>([]);
|
||||
const chats = ref<Record<string, unknown>[]>([]);
|
||||
const aiLogs = ref<Record<string, unknown>[]>([]);
|
||||
const operationLogs = ref<Record<string, unknown>[]>([]);
|
||||
const promptContent = ref("");
|
||||
|
||||
const knowledgeForm = reactive({
|
||||
name: "",
|
||||
feishuSpaceId: "",
|
||||
feishuNodeId: "",
|
||||
status: 1,
|
||||
remark: "",
|
||||
});
|
||||
|
||||
const modelForm = reactive({
|
||||
provider: "openai-compatible",
|
||||
modelName: "",
|
||||
apiUrl: "",
|
||||
apiKey: "",
|
||||
temperature: 0.2,
|
||||
maxToken: 1024,
|
||||
timeoutSecond: 30,
|
||||
});
|
||||
|
||||
const configForm = reactive({
|
||||
configKey: "daily_chat_limit",
|
||||
configValue: "100",
|
||||
description: "默认每日聊天次数",
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
if (!getToken()) return;
|
||||
try {
|
||||
admin.value = await api.profile();
|
||||
await loadCurrentMenu();
|
||||
} catch {
|
||||
clearToken();
|
||||
}
|
||||
});
|
||||
|
||||
async function login() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const result = await api.login(loginForm.username, loginForm.password);
|
||||
saveToken(result.token);
|
||||
admin.value = result.admin;
|
||||
await loadCurrentMenu();
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : "登录失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function logout() {
|
||||
clearToken();
|
||||
admin.value = null;
|
||||
}
|
||||
|
||||
async function switchMenu(menu: string) {
|
||||
activeMenu.value = menu;
|
||||
await loadCurrentMenu();
|
||||
}
|
||||
|
||||
async function loadCurrentMenu() {
|
||||
loading.value = true;
|
||||
try {
|
||||
if (activeMenu.value === "dashboard") stats.value = await api.dashboard();
|
||||
if (activeMenu.value === "users") users.value = await api.users(userKeyword.value);
|
||||
if (activeMenu.value === "knowledge") knowledge.value = await api.knowledge();
|
||||
if (activeMenu.value === "prompt") promptContent.value = (await api.prompt()).promptContent;
|
||||
if (activeMenu.value === "models") models.value = await api.models();
|
||||
if (activeMenu.value === "configs") configs.value = await api.configs();
|
||||
if (activeMenu.value === "records") {
|
||||
chats.value = await api.chats();
|
||||
aiLogs.value = await api.aiLogs();
|
||||
operationLogs.value = await api.operationLogs();
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveUser(row: AdminUser) {
|
||||
await api.updateUser(row.id, { status: row.status, dailyChatLimit: row.dailyChatLimit });
|
||||
ElMessage.success("用户已更新");
|
||||
await loadCurrentMenu();
|
||||
}
|
||||
|
||||
async function saveKnowledge() {
|
||||
await api.createKnowledge({ ...knowledgeForm });
|
||||
ElMessage.success("知识库已新增");
|
||||
Object.assign(knowledgeForm, { name: "", feishuSpaceId: "", feishuNodeId: "", status: 1, remark: "" });
|
||||
await loadCurrentMenu();
|
||||
}
|
||||
|
||||
async function savePrompt() {
|
||||
const result = await api.savePrompt(promptContent.value);
|
||||
promptContent.value = result.promptContent;
|
||||
ElMessage.success("Prompt 已保存");
|
||||
}
|
||||
|
||||
async function resetPrompt() {
|
||||
const result = await api.resetPrompt();
|
||||
promptContent.value = result.promptContent;
|
||||
ElMessage.success("Prompt 已恢复默认");
|
||||
}
|
||||
|
||||
async function saveModel() {
|
||||
await api.createModel({ ...modelForm });
|
||||
ElMessage.success("模型已新增");
|
||||
Object.assign(modelForm, {
|
||||
provider: "openai-compatible",
|
||||
modelName: "",
|
||||
apiUrl: "",
|
||||
apiKey: "",
|
||||
temperature: 0.2,
|
||||
maxToken: 1024,
|
||||
timeoutSecond: 30,
|
||||
});
|
||||
await loadCurrentMenu();
|
||||
}
|
||||
|
||||
async function enableModel(id: number) {
|
||||
await api.enableModel(id);
|
||||
ElMessage.success("模型已启用");
|
||||
await loadCurrentMenu();
|
||||
}
|
||||
|
||||
async function saveConfig() {
|
||||
await api.saveConfig({ ...configForm });
|
||||
ElMessage.success("配置已保存");
|
||||
await loadCurrentMenu();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main v-if="!admin" class="login-page">
|
||||
<section class="login-box">
|
||||
<div class="brand">AI</div>
|
||||
<h1>AI 知识库后台</h1>
|
||||
<p>用于维护用户、知识库、Prompt、模型和审计记录。</p>
|
||||
<el-form label-position="top" @submit.prevent="login">
|
||||
<el-form-item label="管理员账号">
|
||||
<el-input v-model="loginForm.username" />
|
||||
</el-form-item>
|
||||
<el-form-item label="密码">
|
||||
<el-input v-model="loginForm.password" type="password" show-password />
|
||||
</el-form-item>
|
||||
<el-button type="primary" :loading="loading" class="full-btn" @click="login">登录</el-button>
|
||||
</el-form>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<main v-else class="admin-shell">
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-title">AI 知识库</div>
|
||||
<button :class="{ active: activeMenu === 'dashboard' }" @click="switchMenu('dashboard')">Dashboard</button>
|
||||
<button :class="{ active: activeMenu === 'users' }" @click="switchMenu('users')">用户管理</button>
|
||||
<button :class="{ active: activeMenu === 'knowledge' }" @click="switchMenu('knowledge')">知识库管理</button>
|
||||
<button :class="{ active: activeMenu === 'prompt' }" @click="switchMenu('prompt')">Prompt 管理</button>
|
||||
<button :class="{ active: activeMenu === 'models' }" @click="switchMenu('models')">模型管理</button>
|
||||
<button :class="{ active: activeMenu === 'configs' }" @click="switchMenu('configs')">系统配置</button>
|
||||
<button :class="{ active: activeMenu === 'records' }" @click="switchMenu('records')">记录审计</button>
|
||||
</aside>
|
||||
|
||||
<section class="workspace">
|
||||
<header class="topbar">
|
||||
<div>
|
||||
<strong>{{ admin.name }}</strong>
|
||||
<span>管理员</span>
|
||||
</div>
|
||||
<el-button @click="logout">退出</el-button>
|
||||
</header>
|
||||
|
||||
<section v-loading="loading" class="content">
|
||||
<template v-if="activeMenu === 'dashboard'">
|
||||
<div class="page-head">
|
||||
<h2>Dashboard</h2>
|
||||
<p>核心业务数据概览。</p>
|
||||
</div>
|
||||
<div class="stats-grid">
|
||||
<div class="stat"><span>用户数</span><strong>{{ stats?.userCount ?? 0 }}</strong></div>
|
||||
<div class="stat"><span>会话数</span><strong>{{ stats?.sessionCount ?? 0 }}</strong></div>
|
||||
<div class="stat"><span>消息数</span><strong>{{ stats?.messageCount ?? 0 }}</strong></div>
|
||||
<div class="stat"><span>AI 请求</span><strong>{{ stats?.aiRequestCount ?? 0 }}</strong></div>
|
||||
<div class="stat"><span>知识库</span><strong>{{ stats?.knowledgeCount ?? 0 }}</strong></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="activeMenu === 'users'">
|
||||
<div class="page-head inline">
|
||||
<div><h2>用户管理</h2><p>查询用户、启用禁用和调整额度。</p></div>
|
||||
<el-input v-model="userKeyword" placeholder="手机号或姓名" clearable @change="loadCurrentMenu" />
|
||||
</div>
|
||||
<el-table :data="users" stripe>
|
||||
<el-table-column prop="phone" label="手机号" width="150" />
|
||||
<el-table-column prop="name" label="姓名" width="140" />
|
||||
<el-table-column label="状态" width="130">
|
||||
<template #default="{ row }"><el-switch v-model="row.status" :active-value="1" :inactive-value="0" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="每日额度" width="150">
|
||||
<template #default="{ row }"><el-input-number v-model="row.dailyChatLimit" :min="0" size="small" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="dailyChatUsed" label="已用" width="90" />
|
||||
<el-table-column label="操作" width="110">
|
||||
<template #default="{ row }"><el-button size="small" @click="saveUser(row)">保存</el-button></template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
|
||||
<template v-if="activeMenu === 'knowledge'">
|
||||
<div class="page-head"><h2>知识库管理</h2><p>维护飞书知识库 SpaceID 和 NodeID。</p></div>
|
||||
<el-form class="inline-form" :model="knowledgeForm">
|
||||
<el-input v-model="knowledgeForm.name" placeholder="知识库名称" />
|
||||
<el-input v-model="knowledgeForm.feishuSpaceId" placeholder="SpaceID" />
|
||||
<el-input v-model="knowledgeForm.feishuNodeId" placeholder="NodeID" />
|
||||
<el-button type="primary" @click="saveKnowledge">新增</el-button>
|
||||
</el-form>
|
||||
<el-table :data="knowledge" stripe>
|
||||
<el-table-column prop="name" label="名称" />
|
||||
<el-table-column prop="feishuSpaceId" label="SpaceID" />
|
||||
<el-table-column prop="feishuNodeId" label="NodeID" />
|
||||
<el-table-column prop="status" label="状态" width="90" />
|
||||
</el-table>
|
||||
</template>
|
||||
|
||||
<template v-if="activeMenu === 'prompt'">
|
||||
<div class="page-head inline">
|
||||
<div><h2>Prompt 管理</h2><p>保存后会影响后续问答 Prompt 组装。</p></div>
|
||||
<el-button @click="resetPrompt">恢复默认</el-button>
|
||||
</div>
|
||||
<el-input v-model="promptContent" type="textarea" :rows="16" />
|
||||
<div class="actions"><el-button type="primary" @click="savePrompt">保存 Prompt</el-button></div>
|
||||
</template>
|
||||
|
||||
<template v-if="activeMenu === 'models'">
|
||||
<div class="page-head"><h2>模型管理</h2><p>同一时间只启用一个模型。</p></div>
|
||||
<el-form class="model-form" label-position="top" :model="modelForm">
|
||||
<el-input v-model="modelForm.provider" placeholder="Provider" />
|
||||
<el-input v-model="modelForm.modelName" placeholder="模型名" />
|
||||
<el-input v-model="modelForm.apiUrl" placeholder="API URL" />
|
||||
<el-input v-model="modelForm.apiKey" placeholder="API Key" show-password />
|
||||
<el-button type="primary" @click="saveModel">新增模型</el-button>
|
||||
</el-form>
|
||||
<el-table :data="models" stripe>
|
||||
<el-table-column prop="provider" label="Provider" />
|
||||
<el-table-column prop="modelName" label="模型" />
|
||||
<el-table-column prop="apiUrl" label="API URL" />
|
||||
<el-table-column prop="enabled" label="启用" width="90" />
|
||||
<el-table-column label="操作" width="110">
|
||||
<template #default="{ row }"><el-button size="small" @click="enableModel(row.id)">启用</el-button></template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
|
||||
<template v-if="activeMenu === 'configs'">
|
||||
<div class="page-head"><h2>系统配置</h2><p>维护运行时配置项。</p></div>
|
||||
<el-form class="inline-form" :model="configForm">
|
||||
<el-input v-model="configForm.configKey" placeholder="配置 Key" />
|
||||
<el-input v-model="configForm.configValue" placeholder="配置值" />
|
||||
<el-input v-model="configForm.description" placeholder="说明" />
|
||||
<el-button type="primary" @click="saveConfig">保存</el-button>
|
||||
</el-form>
|
||||
<el-table :data="configs" stripe>
|
||||
<el-table-column prop="configKey" label="Key" />
|
||||
<el-table-column prop="configValue" label="Value" />
|
||||
<el-table-column prop="description" label="说明" />
|
||||
</el-table>
|
||||
</template>
|
||||
|
||||
<template v-if="activeMenu === 'records'">
|
||||
<div class="page-head"><h2>记录审计</h2><p>聊天、AI 请求和后台操作记录。</p></div>
|
||||
<el-tabs>
|
||||
<el-tab-pane label="聊天会话"><el-table :data="chats" stripe><el-table-column prop="title" label="标题" /><el-table-column prop="messageCount" label="消息数" width="100" /><el-table-column prop="updatedAt" label="更新时间" /></el-table></el-tab-pane>
|
||||
<el-tab-pane label="AI 请求"><el-table :data="aiLogs" stripe><el-table-column prop="modelName" label="模型" /><el-table-column prop="retrieveCount" label="命中" width="90" /><el-table-column prop="status" label="状态" width="100" /><el-table-column prop="errorMessage" label="错误" /></el-table></el-tab-pane>
|
||||
<el-tab-pane label="操作日志"><el-table :data="operationLogs" stripe><el-table-column prop="module" label="模块" /><el-table-column prop="action" label="动作" /><el-table-column prop="result" label="结果" /><el-table-column prop="createdAt" label="时间" /></el-table></el-tab-pane>
|
||||
</el-tabs>
|
||||
</template>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
Reference in New Issue
Block a user