Complete admin operations management
This commit is contained in:
@@ -15,6 +15,15 @@ import type {
|
||||
ModelItem,
|
||||
} from "./types/api";
|
||||
|
||||
type UserKnowledgePermissionRow = {
|
||||
knowledgeId: number;
|
||||
name: string;
|
||||
status: number;
|
||||
assigned: boolean;
|
||||
effectiveAt: string;
|
||||
expiredAt: string;
|
||||
};
|
||||
|
||||
const admin = ref<AdminProfile | null>(null);
|
||||
const activeMenu = ref("dashboard");
|
||||
const loading = ref(false);
|
||||
@@ -34,6 +43,11 @@ const chatDetail = ref<ChatDetail | null>(null);
|
||||
const chatDetailOpen = ref(false);
|
||||
const recordTab = ref("chats");
|
||||
const editingModelId = ref<number | null>(null);
|
||||
const editingKnowledgeId = ref<number | null>(null);
|
||||
const userPermissionOpen = ref(false);
|
||||
const userPermissionLoading = ref(false);
|
||||
const permissionUser = ref<AdminUser | null>(null);
|
||||
const userKnowledgePermissionRows = ref<UserKnowledgePermissionRow[]>([]);
|
||||
const agentDebugging = ref(false);
|
||||
const agentPreviewMessages = ref<{ role: "user" | "assistant" | "system"; content: string }[]>([
|
||||
{ role: "assistant", content: "选择模型和知识库后,可以在这里调试 Agent 的真实问答效果。" },
|
||||
@@ -245,13 +259,103 @@ async function saveUser(row: AdminUser) {
|
||||
await loadCurrentMenu();
|
||||
}
|
||||
|
||||
async function saveKnowledge() {
|
||||
await api.createKnowledge({ ...knowledgeForm });
|
||||
ElMessage.success("知识库已新增");
|
||||
Object.assign(knowledgeForm, { name: "", feishuSpaceId: "", feishuNodeId: "", status: 1, remark: "" });
|
||||
async function deleteUser(row: AdminUser) {
|
||||
if (!window.confirm(`确认删除用户 ${row.phone} 吗?删除后前台将无法继续使用该账号。`)) return;
|
||||
await api.deleteUser(row.id);
|
||||
ElMessage.success("用户已删除");
|
||||
await loadCurrentMenu();
|
||||
}
|
||||
|
||||
async function openUserPermissions(row: AdminUser) {
|
||||
permissionUser.value = row;
|
||||
userPermissionOpen.value = true;
|
||||
userPermissionLoading.value = true;
|
||||
try {
|
||||
const [knowledgeRows, detail] = await Promise.all([api.knowledge(), api.userKnowledgePermissions(row.id)]);
|
||||
knowledge.value = knowledgeRows;
|
||||
const permissionMap = new Map(detail.permissions.map((item) => [item.knowledgeId, item]));
|
||||
userKnowledgePermissionRows.value = knowledgeRows.map((item) => {
|
||||
const permission = permissionMap.get(item.id);
|
||||
return {
|
||||
knowledgeId: item.id,
|
||||
name: item.name,
|
||||
status: item.status,
|
||||
assigned: Boolean(permission),
|
||||
effectiveAt: permission?.effectiveAt ?? "",
|
||||
expiredAt: permission?.expiredAt ?? "",
|
||||
};
|
||||
});
|
||||
} finally {
|
||||
userPermissionLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveUserPermissions() {
|
||||
if (!permissionUser.value) return;
|
||||
await api.saveUserKnowledgePermissions(permissionUser.value.id, {
|
||||
permissions: userKnowledgePermissionRows.value
|
||||
.filter((item) => item.assigned)
|
||||
.map((item) => ({
|
||||
knowledgeId: item.knowledgeId,
|
||||
effectiveAt: item.effectiveAt || null,
|
||||
expiredAt: item.expiredAt || null,
|
||||
})),
|
||||
});
|
||||
ElMessage.success("知识库权限已保存");
|
||||
userPermissionOpen.value = false;
|
||||
}
|
||||
|
||||
async function saveKnowledge() {
|
||||
if (!knowledgeForm.name || !knowledgeForm.feishuSpaceId || !knowledgeForm.feishuNodeId) {
|
||||
ElMessage.error("请填写知识库名称、SpaceID 和 NodeID");
|
||||
return;
|
||||
}
|
||||
if (editingKnowledgeId.value) {
|
||||
await api.updateKnowledge(editingKnowledgeId.value, { ...knowledgeForm });
|
||||
ElMessage.success("知识库已更新");
|
||||
} else {
|
||||
await api.createKnowledge({ ...knowledgeForm });
|
||||
ElMessage.success("知识库已新增");
|
||||
}
|
||||
resetKnowledgeForm();
|
||||
await loadCurrentMenu();
|
||||
}
|
||||
|
||||
async function saveKnowledgeRow(row: KnowledgeItem) {
|
||||
await api.updateKnowledge(row.id, {
|
||||
name: row.name,
|
||||
feishuSpaceId: row.feishuSpaceId,
|
||||
feishuNodeId: row.feishuNodeId,
|
||||
status: row.status,
|
||||
remark: row.remark ?? "",
|
||||
});
|
||||
ElMessage.success("知识库状态已更新");
|
||||
}
|
||||
|
||||
function editKnowledge(row: KnowledgeItem) {
|
||||
editingKnowledgeId.value = row.id;
|
||||
Object.assign(knowledgeForm, {
|
||||
name: row.name,
|
||||
feishuSpaceId: row.feishuSpaceId,
|
||||
feishuNodeId: row.feishuNodeId,
|
||||
status: row.status,
|
||||
remark: row.remark ?? "",
|
||||
});
|
||||
}
|
||||
|
||||
async function deleteKnowledge(row: KnowledgeItem) {
|
||||
if (!window.confirm(`确认删除知识库 ${row.name} 吗?已分配给用户的授权也会失效。`)) return;
|
||||
await api.deleteKnowledge(row.id);
|
||||
ElMessage.success("知识库已删除");
|
||||
if (editingKnowledgeId.value === row.id) resetKnowledgeForm();
|
||||
await loadCurrentMenu();
|
||||
}
|
||||
|
||||
function resetKnowledgeForm() {
|
||||
editingKnowledgeId.value = null;
|
||||
Object.assign(knowledgeForm, { name: "", feishuSpaceId: "", feishuNodeId: "", status: 1, remark: "" });
|
||||
}
|
||||
|
||||
async function savePrompt() {
|
||||
const result = await api.savePrompt(promptContent.value);
|
||||
promptContent.value = result.promptContent;
|
||||
@@ -473,14 +577,16 @@ async function debugAgent() {
|
||||
ElMessage.error("请输入调试问题");
|
||||
return;
|
||||
}
|
||||
agentPreviewMessages.value.push({ role: "user", content: agentForm.question.trim() });
|
||||
const question = agentForm.question.trim();
|
||||
agentPreviewMessages.value.push({ role: "user", content: question });
|
||||
agentForm.question = "";
|
||||
agentDebugging.value = true;
|
||||
try {
|
||||
const result: AgentDebugResult = await api.debugAgent({
|
||||
promptContent: promptContent.value,
|
||||
modelId: agentForm.modelId,
|
||||
knowledgeIds: agentForm.knowledgeIds,
|
||||
question: agentForm.question.trim(),
|
||||
question,
|
||||
temperature: agentForm.temperature,
|
||||
topP: agentForm.topP,
|
||||
topK: agentForm.topK,
|
||||
@@ -599,8 +705,10 @@ function buildChatQuery() {
|
||||
<el-input v-model="userKeyword" placeholder="手机号或姓名" clearable @change="loadCurrentMenu" />
|
||||
</div>
|
||||
<el-table :data="users" stripe>
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="phone" label="手机号" width="150" />
|
||||
<el-table-column prop="name" label="姓名" width="140" />
|
||||
<el-table-column prop="nickname" 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>
|
||||
@@ -608,25 +716,55 @@ function buildChatQuery() {
|
||||
<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 prop="lastLoginAt" label="最近登录" width="180" />
|
||||
<el-table-column label="操作" width="250" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="saveUser(row)">保存</el-button>
|
||||
<el-button size="small" type="primary" @click="openUserPermissions(row)">知识库权限</el-button>
|
||||
<el-button size="small" type="danger" @click="deleteUser(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-form class="knowledge-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-input v-model="knowledgeForm.remark" placeholder="备注" />
|
||||
<div class="form-switch-row">
|
||||
<span>启用</span>
|
||||
<el-switch v-model="knowledgeForm.status" :active-value="1" :inactive-value="0" />
|
||||
</div>
|
||||
<div class="form-actions-inline">
|
||||
<el-button type="primary" @click="saveKnowledge">{{ editingKnowledgeId ? "保存" : "新增" }}</el-button>
|
||||
<el-button @click="resetKnowledgeForm">清空</el-button>
|
||||
</div>
|
||||
</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-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="name" label="名称" min-width="160" />
|
||||
<el-table-column prop="feishuSpaceId" label="SpaceID" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="feishuNodeId" label="NodeID" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="remark" label="备注" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="状态" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-switch
|
||||
v-model="row.status"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
@change="saveKnowledgeRow(row)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="160" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="editKnowledge(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="deleteKnowledge(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
|
||||
@@ -676,17 +814,8 @@ function buildChatQuery() {
|
||||
<el-input-number v-model="agentForm.frequencyPenalty" :min="-2" :max="2" :step="0.1" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item label="调试问题">
|
||||
<el-input
|
||||
v-model="agentForm.question"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="输入一个真实用户可能会问的问题"
|
||||
/>
|
||||
</el-form-item>
|
||||
<div class="actions">
|
||||
<el-button type="primary" @click="savePrompt">保存 Agent 提示词</el-button>
|
||||
<el-button type="primary" :loading="agentDebugging" @click="debugAgent">发送调试</el-button>
|
||||
<el-button @click="clearAgentPreview">清空预览</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
@@ -707,6 +836,17 @@ function buildChatQuery() {
|
||||
<pre>{{ message.content }}</pre>
|
||||
</article>
|
||||
</div>
|
||||
<div class="agent-preview-composer">
|
||||
<el-input
|
||||
v-model="agentForm.question"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
resize="none"
|
||||
placeholder="输入测试问题"
|
||||
@keydown.enter.exact.prevent="debugAgent"
|
||||
/>
|
||||
<el-button type="primary" :loading="agentDebugging" @click="debugAgent">发送</el-button>
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
</template>
|
||||
@@ -907,6 +1047,62 @@ function buildChatQuery() {
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<el-drawer v-model="userPermissionOpen" size="760px" title="知识库权限分配">
|
||||
<template v-if="permissionUser">
|
||||
<section class="permission-summary">
|
||||
<div>
|
||||
<span>用户</span>
|
||||
<strong>{{ permissionUser.name || '-' }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>手机号</span>
|
||||
<strong>{{ permissionUser.phone }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>每日额度</span>
|
||||
<strong>{{ permissionUser.dailyChatLimit }}</strong>
|
||||
</div>
|
||||
</section>
|
||||
<el-table v-loading="userPermissionLoading" :data="userKnowledgePermissionRows" stripe>
|
||||
<el-table-column label="授权" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-switch v-model="row.assigned" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="name" label="知识库" min-width="170" />
|
||||
<el-table-column label="状态" width="90">
|
||||
<template #default="{ row }">{{ row.status === 1 ? "启用" : "禁用" }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="生效时间" width="220">
|
||||
<template #default="{ row }">
|
||||
<el-date-picker
|
||||
v-model="row.effectiveAt"
|
||||
type="datetime"
|
||||
value-format="YYYY-MM-DDTHH:mm:ss"
|
||||
placeholder="不限制"
|
||||
:disabled="!row.assigned"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="失效时间" width="220">
|
||||
<template #default="{ row }">
|
||||
<el-date-picker
|
||||
v-model="row.expiredAt"
|
||||
type="datetime"
|
||||
value-format="YYYY-MM-DDTHH:mm:ss"
|
||||
placeholder="不限制"
|
||||
:disabled="!row.assigned"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="drawer-actions">
|
||||
<el-button type="primary" @click="saveUserPermissions">保存权限</el-button>
|
||||
<el-button @click="userPermissionOpen = false">取消</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-drawer>
|
||||
|
||||
<el-drawer v-model="chatDetailOpen" size="760px" title="聊天详情">
|
||||
<template v-if="chatDetail">
|
||||
<section class="chat-summary">
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
DashboardStats,
|
||||
KnowledgeItem,
|
||||
ModelItem,
|
||||
UserKnowledgePermissionDetail,
|
||||
} from "../types/api";
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_BASE_URL ?? "http://127.0.0.1:8100/api";
|
||||
@@ -75,11 +76,20 @@ export const api = {
|
||||
users: (keyword = "") => request<AdminUser[]>(`/admin/user/list?keyword=${encodeURIComponent(keyword)}`),
|
||||
updateUser: (id: number, payload: Record<string, unknown>) =>
|
||||
request<AdminUser>(`/admin/user/${id}`, { method: "PUT", body: JSON.stringify(payload) }),
|
||||
deleteUser: (id: number) => request<null>(`/admin/user/${id}`, { method: "DELETE" }),
|
||||
userKnowledgePermissions: (id: number) =>
|
||||
request<UserKnowledgePermissionDetail>(`/admin/user/${id}/knowledge-permissions`),
|
||||
saveUserKnowledgePermissions: (id: number, payload: Record<string, unknown>) =>
|
||||
request<UserKnowledgePermissionDetail>(`/admin/user/${id}/knowledge-permissions`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
knowledge: () => request<KnowledgeItem[]>("/admin/knowledge/list"),
|
||||
createKnowledge: (payload: Record<string, unknown>) =>
|
||||
request<KnowledgeItem>("/admin/knowledge", { method: "POST", body: JSON.stringify(payload) }),
|
||||
updateKnowledge: (id: number, payload: Record<string, unknown>) =>
|
||||
request<KnowledgeItem>(`/admin/knowledge/${id}`, { method: "PUT", body: JSON.stringify(payload) }),
|
||||
deleteKnowledge: (id: number) => request<null>(`/admin/knowledge/${id}`, { method: "DELETE" }),
|
||||
prompt: () => request<{ promptContent: string }>("/admin/prompt"),
|
||||
savePrompt: (promptContent: string) =>
|
||||
request<{ promptContent: string }>("/admin/prompt", { method: "PUT", body: JSON.stringify({ promptContent }) }),
|
||||
|
||||
@@ -185,6 +185,31 @@ textarea {
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.knowledge-form {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(160px, 1.1fr) minmax(180px, 1fr) minmax(180px, 1fr) minmax(180px, 1.2fr) 100px 150px;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form-switch-row,
|
||||
.form-actions-inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.form-switch-row {
|
||||
height: 32px;
|
||||
color: #40524b;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.form-actions-inline {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.model-config-panel {
|
||||
margin-bottom: 18px;
|
||||
padding: 18px;
|
||||
@@ -256,6 +281,12 @@ textarea {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.agent-preview-panel {
|
||||
height: clamp(520px, calc(100vh - 170px), 680px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.agent-config-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
@@ -290,7 +321,8 @@ textarea {
|
||||
}
|
||||
|
||||
.agent-preview-chat {
|
||||
height: 560px;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -327,10 +359,64 @@ textarea {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.agent-preview-composer {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 76px;
|
||||
gap: 10px;
|
||||
align-items: end;
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #e4ece9;
|
||||
}
|
||||
|
||||
.agent-preview-composer .el-button {
|
||||
height: 54px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.permission-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.permission-summary div {
|
||||
padding: 12px;
|
||||
border: 1px solid #dfe8e5;
|
||||
border-radius: 8px;
|
||||
background: #f8fbfa;
|
||||
}
|
||||
|
||||
.permission-summary span {
|
||||
display: block;
|
||||
color: #6b7d77;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.permission-summary strong {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
overflow: hidden;
|
||||
color: #1f2d2a;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.drawer-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.el-drawer .el-date-editor.el-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.record-filter {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -23,11 +23,13 @@ export interface AdminUser {
|
||||
id: number;
|
||||
phone: string;
|
||||
name: string;
|
||||
nickname?: string | null;
|
||||
status: number;
|
||||
dailyChatLimit: number;
|
||||
dailyChatUsed: number;
|
||||
expiredAt?: string | null;
|
||||
lastLoginAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
}
|
||||
|
||||
export interface KnowledgeItem {
|
||||
@@ -37,6 +39,19 @@ export interface KnowledgeItem {
|
||||
feishuNodeId: string;
|
||||
status: number;
|
||||
remark?: string | null;
|
||||
createdAt?: string | null;
|
||||
updatedAt?: string | null;
|
||||
}
|
||||
|
||||
export interface UserKnowledgePermissionItem {
|
||||
knowledgeId: number;
|
||||
effectiveAt?: string | null;
|
||||
expiredAt?: string | null;
|
||||
}
|
||||
|
||||
export interface UserKnowledgePermissionDetail {
|
||||
user: AdminUser;
|
||||
permissions: UserKnowledgePermissionItem[];
|
||||
}
|
||||
|
||||
export interface ModelItem {
|
||||
|
||||
Reference in New Issue
Block a user