feat: 看板优化——中文标题+Token消耗+时间段筛选+品牌名统一
- 学员端:LoginPanel/App.vue 描述文案优化,去掉生硬表述 - 后台品牌统一:index.html/App.vue 标题改为'大本营千问千答' - 看板tab:Dashboard -> 数据看板 - 看板新增:输入Token、输出Token、总Token 统计 - 看板支持按时间段筛选:后端API增加start/end参数,前端加日期选择器 - stats-grid布局从5列调整为4列,响应式改为2列
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>大本营答疑助手后台</title>
|
<title>大本营千问千答</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ const loading = ref(false);
|
|||||||
|
|
||||||
const loginForm = reactive({ username: "admin", password: "admin123456" });
|
const loginForm = reactive({ username: "admin", password: "admin123456" });
|
||||||
const stats = ref<DashboardStats | null>(null);
|
const stats = ref<DashboardStats | null>(null);
|
||||||
|
const dashboardDateRange = ref<[string, string] | null>(null);
|
||||||
const users = ref<AdminUser[]>([]);
|
const users = ref<AdminUser[]>([]);
|
||||||
const userKeyword = ref("");
|
const userKeyword = ref("");
|
||||||
const knowledge = ref<KnowledgeItem[]>([]);
|
const knowledge = ref<KnowledgeItem[]>([]);
|
||||||
@@ -502,10 +503,15 @@ async function switchMenu(menu: string) {
|
|||||||
await loadCurrentMenu();
|
await loadCurrentMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadDashboard() {
|
||||||
|
const [start, end] = dashboardDateRange.value ?? ["", ""];
|
||||||
|
stats.value = await api.dashboard(start, end);
|
||||||
|
}
|
||||||
|
|
||||||
async function loadCurrentMenu() {
|
async function loadCurrentMenu() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
if (activeMenu.value === "dashboard") stats.value = await api.dashboard();
|
if (activeMenu.value === "dashboard") await loadDashboard();
|
||||||
if (activeMenu.value === "users") users.value = await api.users(userKeyword.value);
|
if (activeMenu.value === "users") users.value = await api.users(userKeyword.value);
|
||||||
if (activeMenu.value === "knowledge") knowledge.value = await api.knowledge();
|
if (activeMenu.value === "knowledge") knowledge.value = await api.knowledge();
|
||||||
if (activeMenu.value === "prompt") {
|
if (activeMenu.value === "prompt") {
|
||||||
@@ -1036,7 +1042,7 @@ function buildChatQuery() {
|
|||||||
<div class="login-hero-main">
|
<div class="login-hero-main">
|
||||||
<div class="login-brand">
|
<div class="login-brand">
|
||||||
<span>答</span>
|
<span>答</span>
|
||||||
<strong>大本营答疑助手后台</strong>
|
<strong>大本营千问千答</strong>
|
||||||
</div>
|
</div>
|
||||||
<h1>把大本营答疑、知识库和学员运营放在同一个控制台里。</h1>
|
<h1>把大本营答疑、知识库和学员运营放在同一个控制台里。</h1>
|
||||||
<p>统一维护学员名单、开放知识库、模型接入、Agent 调试、问答记录和系统运行参数,方便排查与交接。</p>
|
<p>统一维护学员名单、开放知识库、模型接入、Agent 调试、问答记录和系统运行参数,方便排查与交接。</p>
|
||||||
@@ -1072,8 +1078,8 @@ function buildChatQuery() {
|
|||||||
|
|
||||||
<main v-else class="admin-shell">
|
<main v-else class="admin-shell">
|
||||||
<aside class="sidebar">
|
<aside class="sidebar">
|
||||||
<div class="sidebar-title">大本营答疑</div>
|
<div class="sidebar-title">大本营千问千答</div>
|
||||||
<button :class="{ active: activeMenu === 'dashboard' }" @click="switchMenu('dashboard')">Dashboard</button>
|
<button :class="{ active: activeMenu === 'dashboard' }" @click="switchMenu('dashboard')">数据看板</button>
|
||||||
<button :class="{ active: activeMenu === 'users' }" @click="switchMenu('users')">用户管理</button>
|
<button :class="{ active: activeMenu === 'users' }" @click="switchMenu('users')">用户管理</button>
|
||||||
<button :class="{ active: activeMenu === 'knowledge' }" @click="switchMenu('knowledge')">知识库管理</button>
|
<button :class="{ active: activeMenu === 'knowledge' }" @click="switchMenu('knowledge')">知识库管理</button>
|
||||||
<button :class="{ active: activeMenu === 'prompt' }" @click="switchMenu('prompt')">Agent 管理</button>
|
<button :class="{ active: activeMenu === 'prompt' }" @click="switchMenu('prompt')">Agent 管理</button>
|
||||||
@@ -1093,9 +1099,17 @@ function buildChatQuery() {
|
|||||||
|
|
||||||
<section v-loading="loading" class="content">
|
<section v-loading="loading" class="content">
|
||||||
<template v-if="activeMenu === 'dashboard'">
|
<template v-if="activeMenu === 'dashboard'">
|
||||||
<div class="page-head">
|
<div class="page-head inline">
|
||||||
<h2>Dashboard</h2>
|
<div><h2>数据看板</h2><p>核心业务数据概览。</p></div>
|
||||||
<p>核心业务数据概览。</p>
|
<el-date-picker
|
||||||
|
v-model="dashboardDateRange"
|
||||||
|
type="daterange"
|
||||||
|
range-separator="至"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
@change="loadDashboard"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="stats-grid">
|
<div class="stats-grid">
|
||||||
<div class="stat"><span>用户数</span><strong>{{ stats?.userCount ?? 0 }}</strong></div>
|
<div class="stat"><span>用户数</span><strong>{{ stats?.userCount ?? 0 }}</strong></div>
|
||||||
@@ -1103,6 +1117,9 @@ function buildChatQuery() {
|
|||||||
<div class="stat"><span>消息数</span><strong>{{ stats?.messageCount ?? 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>AI 请求</span><strong>{{ stats?.aiRequestCount ?? 0 }}</strong></div>
|
||||||
<div class="stat"><span>知识库</span><strong>{{ stats?.knowledgeCount ?? 0 }}</strong></div>
|
<div class="stat"><span>知识库</span><strong>{{ stats?.knowledgeCount ?? 0 }}</strong></div>
|
||||||
|
<div class="stat"><span>输入 Token</span><strong>{{ stats?.inputToken ?? 0 }}</strong></div>
|
||||||
|
<div class="stat"><span>输出 Token</span><strong>{{ stats?.outputToken ?? 0 }}</strong></div>
|
||||||
|
<div class="stat"><span>总 Token</span><strong>{{ stats?.totalToken ?? 0 }}</strong></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -85,7 +85,13 @@ export const api = {
|
|||||||
body: JSON.stringify({ username, password }),
|
body: JSON.stringify({ username, password }),
|
||||||
}),
|
}),
|
||||||
profile: () => request<AdminProfile>("/admin/profile"),
|
profile: () => request<AdminProfile>("/admin/profile"),
|
||||||
dashboard: () => request<DashboardStats>("/admin/dashboard"),
|
dashboard: (start?: string, end?: string) => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (start) params.set("start", start);
|
||||||
|
if (end) params.set("end", end);
|
||||||
|
const qs = params.toString();
|
||||||
|
return request<DashboardStats>(`/admin/dashboard${qs ? "?" + qs : ""}`);
|
||||||
|
},
|
||||||
users: (keyword = "") => request<AdminUser[]>(`/admin/user/list?keyword=${encodeURIComponent(keyword)}`),
|
users: (keyword = "") => request<AdminUser[]>(`/admin/user/list?keyword=${encodeURIComponent(keyword)}`),
|
||||||
createUser: (payload: Record<string, unknown>) =>
|
createUser: (payload: Record<string, unknown>) =>
|
||||||
request<AdminUser>("/admin/user", { method: "POST", body: JSON.stringify(payload) }),
|
request<AdminUser>("/admin/user", { method: "POST", body: JSON.stringify(payload) }),
|
||||||
|
|||||||
@@ -264,7 +264,7 @@ textarea {
|
|||||||
|
|
||||||
.stats-grid {
|
.stats-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1002,7 +1002,10 @@ textarea {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats-grid,
|
.stats-grid {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
.chat-summary {
|
.chat-summary {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ export interface DashboardStats {
|
|||||||
messageCount: number;
|
messageCount: number;
|
||||||
aiRequestCount: number;
|
aiRequestCount: number;
|
||||||
knowledgeCount: number;
|
knowledgeCount: number;
|
||||||
|
inputToken: number;
|
||||||
|
outputToken: number;
|
||||||
|
totalToken: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AdminUser {
|
export interface AdminUser {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
from datetime import datetime
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, Query
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.core.database import get_db
|
from app.core.database import get_db
|
||||||
@@ -15,8 +17,16 @@ router = APIRouter()
|
|||||||
|
|
||||||
@router.get("/dashboard")
|
@router.get("/dashboard")
|
||||||
def dashboard(
|
def dashboard(
|
||||||
|
start: str = Query(default="", description="开始日期 YYYY-MM-DD"),
|
||||||
|
end: str = Query(default="", description="结束日期 YYYY-MM-DD"),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_admin: Admin = Depends(get_current_admin),
|
current_admin: Admin = Depends(get_current_admin),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
stats = AdminDashboardService.stats(db)
|
start_dt = datetime.strptime(start, "%Y-%m-%d") if start else None
|
||||||
|
if end:
|
||||||
|
end_dt = datetime.strptime(end, "%Y-%m-%d")
|
||||||
|
end_dt = end_dt.replace(hour=23, minute=59, second=59)
|
||||||
|
else:
|
||||||
|
end_dt = None
|
||||||
|
stats = AdminDashboardService.stats(db, start_dt, end_dt)
|
||||||
return api_success(DashboardStats.model_validate(stats).model_dump())
|
return api_success(DashboardStats.model_validate(stats).model_dump())
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ class DashboardStats(BaseModel):
|
|||||||
messageCount: int
|
messageCount: int
|
||||||
aiRequestCount: int
|
aiRequestCount: int
|
||||||
knowledgeCount: int
|
knowledgeCount: int
|
||||||
|
inputToken: int
|
||||||
|
outputToken: int
|
||||||
|
totalToken: int
|
||||||
|
|
||||||
|
|
||||||
class AdminUserUpdateRequest(BaseModel):
|
class AdminUserUpdateRequest(BaseModel):
|
||||||
|
|||||||
@@ -60,13 +60,35 @@ class AdminAuthService:
|
|||||||
|
|
||||||
class AdminDashboardService:
|
class AdminDashboardService:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def stats(db: Session) -> dict:
|
def stats(db: Session, start: datetime | None = None, end: datetime | None = None) -> dict:
|
||||||
|
from sqlalchemy import and_
|
||||||
|
|
||||||
|
# 基础表查询条件(时间范围)
|
||||||
|
user_filter = [User.is_deleted == 0]
|
||||||
|
session_filter = [ChatSession.is_deleted == 0]
|
||||||
|
msg_filter = []
|
||||||
|
ai_filter = []
|
||||||
|
|
||||||
|
if start:
|
||||||
|
user_filter.append(User.created_at >= start)
|
||||||
|
session_filter.append(ChatSession.created_at >= start)
|
||||||
|
msg_filter.append(ChatMessage.created_at >= start)
|
||||||
|
ai_filter.append(AiRequestLog.created_at >= start)
|
||||||
|
if end:
|
||||||
|
user_filter.append(User.created_at <= end)
|
||||||
|
session_filter.append(ChatSession.created_at <= end)
|
||||||
|
msg_filter.append(ChatMessage.created_at <= end)
|
||||||
|
ai_filter.append(AiRequestLog.created_at <= end)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"userCount": db.scalar(select(func.count(User.id)).where(User.is_deleted == 0)) or 0,
|
"userCount": db.scalar(select(func.count(User.id)).where(and_(*user_filter))) or 0,
|
||||||
"sessionCount": db.scalar(select(func.count(ChatSession.id)).where(ChatSession.is_deleted == 0)) or 0,
|
"sessionCount": db.scalar(select(func.count(ChatSession.id)).where(and_(*session_filter))) or 0,
|
||||||
"messageCount": db.scalar(select(func.count(ChatMessage.id))) or 0,
|
"messageCount": db.scalar(select(func.count(ChatMessage.id)).where(and_(*msg_filter))) or 0,
|
||||||
"aiRequestCount": db.scalar(select(func.count(AiRequestLog.id))) or 0,
|
"aiRequestCount": db.scalar(select(func.count(AiRequestLog.id)).where(and_(*ai_filter))) or 0,
|
||||||
"knowledgeCount": db.scalar(select(func.count(Knowledge.id))) or 0,
|
"knowledgeCount": db.scalar(select(func.count(Knowledge.id))) or 0,
|
||||||
|
"inputToken": db.scalar(select(func.coalesce(func.sum(AiRequestLog.input_token), 0)).where(and_(*ai_filter))) or 0,
|
||||||
|
"outputToken": db.scalar(select(func.coalesce(func.sum(AiRequestLog.output_token), 0)).where(and_(*ai_filter))) or 0,
|
||||||
|
"totalToken": db.scalar(select(func.coalesce(func.sum(AiRequestLog.total_token), 0)).where(and_(*ai_filter))) or 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ async function scrollToBottom() {
|
|||||||
<section class="chat-area">
|
<section class="chat-area">
|
||||||
<div v-if="messages.length === 0" class="empty-state">
|
<div v-if="messages.length === 0" class="empty-state">
|
||||||
<strong>可以开始提问了</strong>
|
<strong>可以开始提问了</strong>
|
||||||
<p>当前已接入大本营知识库检索链路,若外部服务未配置或权限不足,会在回复区提示具体原因。</p>
|
<p>输入问题,我会尽力为你解答。</p>
|
||||||
</div>
|
</div>
|
||||||
<ChatMessage
|
<ChatMessage
|
||||||
v-for="message in messages"
|
v-for="message in messages"
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ function showError(text: string) {
|
|||||||
<section class="login-panel">
|
<section class="login-panel">
|
||||||
<div class="brand-mark">答</div>
|
<div class="brand-mark">答</div>
|
||||||
<h1>大本营答疑助手</h1>
|
<h1>大本营答疑助手</h1>
|
||||||
<p>围绕大本营知识库,为学员提供及时、可信的答疑支持。</p>
|
<p>有问题随时问,帮你快速找到答案。</p>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
手机号
|
手机号
|
||||||
|
|||||||
Reference in New Issue
Block a user