From 90eaf5395b9c01dec3850728a40c269c5e63f0e8 Mon Sep 17 00:00:00 2001 From: hnu202326010131 <2950457847@qq.com> Date: Thu, 18 Dec 2025 18:08:51 +0000 Subject: [PATCH] feat(profile): use /api/v1/users; normalize roles; fix charts sizing and status colors; docs: backend integration; ui(login): center register button --- BACKEND_INTEGRATION.md | 81 ++++++++++ frontend-vue/.vite/deps/_metadata.json | 16 +- frontend-vue/src/app/components/CpuChart.vue | 11 +- .../src/app/components/MemoryChart.vue | 11 +- frontend-vue/src/app/components/Sidebar.vue | 4 +- frontend-vue/src/app/stores/auth.ts | 17 ++- frontend-vue/src/app/views/ClusterList.vue | 4 +- frontend-vue/src/app/views/Dashboard.vue | 6 +- frontend-vue/src/app/views/Diagnosis.vue | 139 +++++++++++++----- frontend-vue/src/app/views/Login.vue | 2 +- frontend-vue/src/app/views/Profile.vue | 49 ++++-- 11 files changed, 266 insertions(+), 74 deletions(-) create mode 100644 BACKEND_INTEGRATION.md diff --git a/BACKEND_INTEGRATION.md b/BACKEND_INTEGRATION.md new file mode 100644 index 0000000..130c6b4 --- /dev/null +++ b/BACKEND_INTEGRATION.md @@ -0,0 +1,81 @@ +# 后端联调指南 + +## 环境与代理 + +- 前端目录:`frontend-vue` +- 开发代理:`frontend-vue/vite.config.ts:25-30` 将前端的 `/api` 代理到后端 `VITE_API_TARGET` +- 环境变量: + - `frontend-vue/.env.development` 示例:`VITE_API_TARGET=https://your-backend.example.com` + - 可选:`VITE_DEV_HOST`、`VITE_ALLOWED_HOSTS` 控制本地开发主机与允许的外网访问 +- 启动与构建: + - 开发:`cd frontend-vue && npm i && npm run dev` + - 预览:`npm run preview` + +## 鉴权与凭证 + +- 认证头:`Authorization: Bearer ` +- 前端存储: + - `localStorage` 键名:`cm_user`、`cm_token` + - 应用启动恢复:`frontend-vue/src/app/main.ts:10-12` +- 路由守卫:`frontend-vue/src/app/router/index.ts:23-30` 未登录重定向到登录页;基于 `meta.roles` 进行角色校验 + +## 关键接口规范 + +- 健康检查:`GET /v1/health` +- 登录:`POST /v1/user/login` + - 请求体:`{ username, password }` + - 响应体:`{ token, user: { username, role, email? } }` +- 注册:`POST /v1/user/register` + - 请求体:`{ username, email, password, fullName }` + - 响应体:`{ token?, user: { username, role, email? } }` +- 用户列表:`GET /api/v1/users` + - 响应体:`{ users: Array<{ username, role, email }> }` 或 `Array<{ username, role, email }>` + - 前端选择逻辑:仅匹配当前登录用户名;未匹配则显示错误提示 + - 权限不足(例如操作员 403):页面显示权限提示 + - 角色值规范化:后端返回的 `role` 会统一转为小写并支持常见别名(如 `administrator`→`admin`、`ops`→`operator`) +- 集群与节点: + - `GET /v1/clusters` → `[{ uuid, host, ip, count, health }]` + - `POST /v1/clusters`、`DELETE /v1/clusters/:id`、`POST /v1/clusters/:id/start|stop` + - `GET /v1/nodes?cluster=` → `[{ name, ip, status, cpu, mem, updated }]` + - `POST /v1/nodes/:name/start|stop`、`DELETE /v1/nodes/:name` +- 指标: + - CPU 趋势:`GET /v1/metrics/cpu_trend?cluster=` → `{ times: string[], values: number[] }` + - 内存使用:`GET /v1/metrics/memory_usage?cluster=` → `{ used: number, free: number }` +- 诊断: + - 故障摘要:`GET /v1/faults/summary?node=|cluster=` → `{ code, time, scope }` + - AI 对话历史:`GET /v1/ai/history?sessionId=` → `{ messages: Array<{ role, content, reasoning? }> }` + - AI 对话:`POST /v1/ai/chat` → `{ reply, reasoning? }` + +## 前端数据绑定要点 + +- 个人主页:`frontend-vue/src/app/views/Profile.vue` + - 仅使用后端数据;加载中与错误态可视化 + - 角色标签映射:`frontend-vue/src/app/constants/roles.ts` +- 角色来源: + - 登录/注册优先采用后端返回的 `user.role` 或 `role`(`frontend-vue/src/app/stores/auth.ts:55-72,74-91`) +- 诊断页与导航: + - 路由授权:`diagnosis` 允许 `admin/operator`(`frontend-vue/src/app/router/index.ts:12`) + - 侧边栏入口基于角色显示(`frontend-vue/src/app/components/Sidebar.vue:7`) + +## 联调步骤 + +- 设置 `VITE_API_TARGET` 指向后端地址,确保后端允许来自前端的 CORS 与 Host +- 启动前端后检查 `GET /v1/health` 返回正常 +- 使用真实账号登录,确认: + - `GET /v1/user/me` 返回用户信息,个人主页字段显示为后端数据 + - 侧边栏与页面访问受角色控制 + - 仪表板与诊断页数据来自后端接口 +- 常见问题: + - Token 无效或过期 → 返回 `401`,需要重新登录 + - 代理失败 → 检查 `VITE_API_TARGET` 与后端协议/端口 + - 外网访问本地开发 → 配置 `VITE_ALLOWED_HOSTS`,参考 `Cloudflare-Tunnel-Guide.md` + +## 参考文件 + +- `frontend-vue/vite.config.ts` +- `frontend-vue/src/app/main.ts` +- `frontend-vue/src/app/router/index.ts` +- `frontend-vue/src/app/stores/auth.ts` +- `frontend-vue/src/app/views/Profile.vue` +- `frontend-vue/src/app/components/Sidebar.vue` +- `frontend-vue/src/app/constants/roles.ts` diff --git a/frontend-vue/.vite/deps/_metadata.json b/frontend-vue/.vite/deps/_metadata.json index f494819..0385e07 100644 --- a/frontend-vue/.vite/deps/_metadata.json +++ b/frontend-vue/.vite/deps/_metadata.json @@ -1,37 +1,37 @@ { - "hash": "4ef6bb89", + "hash": "6cbacb0f", "configHash": "8580b9f5", - "lockfileHash": "82745bba", - "browserHash": "63ea4e11", + "lockfileHash": "ef5c83d3", + "browserHash": "a05a1ddb", "optimized": { "axios": { "src": "../../node_modules/axios/index.js", "file": "axios.js", - "fileHash": "00fa363e", + "fileHash": "633af732", "needsInterop": false }, "echarts": { "src": "../../node_modules/echarts/index.js", "file": "echarts.js", - "fileHash": "411d0844", + "fileHash": "4b0e29e6", "needsInterop": false }, "pinia": { "src": "../../node_modules/pinia/dist/pinia.mjs", "file": "pinia.js", - "fileHash": "547a4649", + "fileHash": "99c547e7", "needsInterop": false }, "vue": { "src": "../../node_modules/vue/dist/vue.runtime.esm-bundler.js", "file": "vue.js", - "fileHash": "cdee61b0", + "fileHash": "858c012f", "needsInterop": false }, "vue-router": { "src": "../../node_modules/vue-router/dist/vue-router.mjs", "file": "vue-router.js", - "fileHash": "6b0fa1a5", + "fileHash": "8f1ed449", "needsInterop": false } }, diff --git a/frontend-vue/src/app/components/CpuChart.vue b/frontend-vue/src/app/components/CpuChart.vue index 701b18b..4e1c7cd 100644 --- a/frontend-vue/src/app/components/CpuChart.vue +++ b/frontend-vue/src/app/components/CpuChart.vue @@ -1,9 +1,9 @@ diff --git a/frontend-vue/src/app/components/MemoryChart.vue b/frontend-vue/src/app/components/MemoryChart.vue index 838c62d..0de7ea3 100644 --- a/frontend-vue/src/app/components/MemoryChart.vue +++ b/frontend-vue/src/app/components/MemoryChart.vue @@ -1,9 +1,9 @@ diff --git a/frontend-vue/src/app/components/Sidebar.vue b/frontend-vue/src/app/components/Sidebar.vue index 3893191..5963d02 100644 --- a/frontend-vue/src/app/components/Sidebar.vue +++ b/frontend-vue/src/app/components/Sidebar.vue @@ -41,8 +41,8 @@ function isActive(p: string) { return route.path === p } function can(roles: string[]) { return roles.includes(role.value || '') } const configOpen = ref(false) const permOpen = ref(false) -function toggleConfig(){ closeAll(); configOpen.value = !configOpen.value } -function togglePerm(){ closeAll(); permOpen.value = !permOpen.value } +function toggleConfig(){ const next = !configOpen.value; closeAll(); configOpen.value = next } +function togglePerm(){ const next = !permOpen.value; closeAll(); permOpen.value = next } function closeAll(){ configOpen.value = false; permOpen.value = false } function onDocClick(){ closeAll() } onMounted(()=>{ document.addEventListener('click', onDocClick) }) diff --git a/frontend-vue/src/app/stores/auth.ts b/frontend-vue/src/app/stores/auth.ts index 24525f2..73c82db 100644 --- a/frontend-vue/src/app/stores/auth.ts +++ b/frontend-vue/src/app/stores/auth.ts @@ -9,6 +9,15 @@ function makeDemoToken() { return `demo.${body}.${Math.random().toString(36).slice(2)}` } +function normalizeRole(r: string): 'admin'|'operator'|'observer'|'' { + const v = String(r || '').trim().toLowerCase() + if (!v) return '' + if (v === 'admin' || v === 'administrator') return 'admin' + if (v === 'operator' || v === 'ops' || v === 'op') return 'operator' + if (v === 'observer' || v === 'obs' || v === 'view') return 'observer' + return '' +} + export const useAuthStore = defineStore('auth', { state: () => ({ user: null as User|null, token: null as string|null }), getters: { @@ -53,8 +62,10 @@ export const useAuthStore = defineStore('auth', { } try { const r = await api.post('/v1/user/login', { username, password }) - const role = username === 'admin' ? 'admin' : username === 'ops' ? 'operator' : username === 'obs' ? 'observer' : 'observer' const token = r?.data?.token + const backendRoleRaw = (r?.data?.user?.role || r?.data?.role || '') as string + const backendRole = normalizeRole(backendRoleRaw) + const role: 'admin'|'operator'|'observer' = backendRole || (username === 'admin' ? 'admin' : username === 'ops' ? 'operator' : username === 'obs' ? 'observer' : 'observer') if (!token) { return { ok: false, message: '登录失败' } } @@ -74,7 +85,9 @@ export const useAuthStore = defineStore('auth', { async register(username: string, email: string, password: string, fullName: string) { try { const r = await api.post('/v1/user/register', { username, email, password, fullName }) - const role = username === 'admin' ? 'admin' : username === 'ops' ? 'operator' : username === 'obs' ? 'observer' : 'observer' + const backendRoleRaw = (r?.data?.user?.role || r?.data?.role || '') as string + const backendRole = normalizeRole(backendRoleRaw) + const role: 'admin'|'operator'|'observer' = backendRole || (username === 'admin' ? 'admin' : username === 'ops' ? 'operator' : username === 'obs' ? 'observer' : 'observer') this.user = { username, role } this.token = r?.data?.token || null this.persist() diff --git a/frontend-vue/src/app/views/ClusterList.vue b/frontend-vue/src/app/views/ClusterList.vue index 10291d9..fdf66fd 100644 --- a/frontend-vue/src/app/views/ClusterList.vue +++ b/frontend-vue/src/app/views/ClusterList.vue @@ -17,10 +17,10 @@
{{ err }}
- + - +
主机名IP节点数健康                             操作
集群名节点数健康                              操作
{{ c.host }}{{ c.ip }}{{ c.count }}{{ c.host }}{{ c.count }} {{ c.healthText }} diff --git a/frontend-vue/src/app/views/Dashboard.vue b/frontend-vue/src/app/views/Dashboard.vue index 8cf10ec..728e061 100644 --- a/frontend-vue/src/app/views/Dashboard.vue +++ b/frontend-vue/src/app/views/Dashboard.vue @@ -4,7 +4,7 @@

仪表板 · 集群概览

更新时间:{{ updateTime }} - 当前集群:{{ meta.uuid }} | 主机名:{{ meta.host }} | 主IP:{{ meta.ip }} + 当前集群:{{ meta.uuid }} | 集群名:{{ meta.host }}
@@ -128,7 +128,7 @@ async function detail(name:string){ try{ await api.get(`/v1/nodes/${encodeURICom .status-indicator{ display:inline-flex; align-items:center; gap:6px } .status-dot{ width:8px; height:8px; border-radius:50% } .status-dot--running{ background:#16a34a } -.status-dot--warning{ background:#f59e0b } -.status-dot--error{ background:#dc2626 } +.status-dot--warning{ background:#16a34a } +.status-dot--error{ background:#16a34a } .status-text{ font-size:12px } diff --git a/frontend-vue/src/app/views/Diagnosis.vue b/frontend-vue/src/app/views/Diagnosis.vue index b3133c1..a5d297e 100644 --- a/frontend-vue/src/app/views/Diagnosis.vue +++ b/frontend-vue/src/app/views/Diagnosis.vue @@ -50,12 +50,12 @@
{{ filterSummary }}
-
    -
  • +
  • {{ n }}
  • @@ -69,14 +69,15 @@

    故障信息

    -
    故障代码FLT-20251107-0001
    -
    发生时间2025-11-07 10:15:00
    -
    影响范围CL-3333-CCCC-003
    +
    故障代码{{ fault?.code || '—' }}
    +
    发生时间{{ fault?.time || '—' }}
    +
    影响范围{{ fault?.scope || '—' }}
    +
    {{ faultErr }}
    -
    +
- +