feat(admin): 增加中枢设置(自定义 ID 与名称,右上角设置入口)
This commit is contained in:
+22
-3
@@ -4,16 +4,28 @@ import { getToken, setToken, api } from './api/admin.js';
|
||||
import Login from './views/Login.vue';
|
||||
import Users from './views/Users.vue';
|
||||
import Logs from './views/Logs.vue';
|
||||
import HubSettings from './components/HubSettings.vue';
|
||||
|
||||
const authed = ref(false);
|
||||
const page = ref('overview');
|
||||
const user = ref(null);
|
||||
const stats = ref({ users: 0, total: 0, denied: 0, p95: null });
|
||||
const hub = ref({ hub_id: '', hub_name: '蜂群中枢' });
|
||||
const settingsOpen = ref(false);
|
||||
|
||||
const PAGES = { overview: '概览', users: '用户管理', logs: '日志查看' };
|
||||
|
||||
function onUnauthorized() { setToken(''); authed.value = false; }
|
||||
|
||||
async function loadHub() {
|
||||
try {
|
||||
const r = await api.settings();
|
||||
hub.value = r.settings;
|
||||
} catch (e) {
|
||||
if (e.status === 401) onUnauthorized();
|
||||
}
|
||||
}
|
||||
|
||||
async function loadStats() {
|
||||
try {
|
||||
const [u, l] = await Promise.all([api.users(), api.logs({ page: 1, page_size: 1 })]);
|
||||
@@ -32,6 +44,7 @@ async function init() {
|
||||
const r = await api.me();
|
||||
user.value = r.user;
|
||||
authed.value = true;
|
||||
loadHub();
|
||||
loadStats();
|
||||
} catch (e) {
|
||||
setToken('');
|
||||
@@ -39,8 +52,9 @@ async function init() {
|
||||
}
|
||||
}
|
||||
|
||||
function onLogin(u) { user.value = u; authed.value = true; page.value = 'overview'; loadStats(); }
|
||||
function onLogin(u) { user.value = u; authed.value = true; page.value = 'overview'; loadHub(); loadStats(); }
|
||||
async function onLogout() { try { await api.logout(); } catch {} setToken(''); authed.value = false; }
|
||||
function onSettingsSaved(s) { hub.value = s; settingsOpen.value = false; }
|
||||
|
||||
onMounted(init);
|
||||
</script>
|
||||
@@ -51,7 +65,7 @@ onMounted(init);
|
||||
<aside class="sidebar">
|
||||
<div class="side-brand">
|
||||
<div class="hex">蜂</div>
|
||||
<div><div class="brand-name">蜂群中枢</div><div class="brand-sub">PlantAI 管理后台</div></div>
|
||||
<div><div class="brand-name">{{ hub.hub_name }}</div><div class="brand-sub">{{ hub.hub_id.slice(0, 8) }}</div></div>
|
||||
</div>
|
||||
<nav class="nav">
|
||||
<div class="nav-item" :class="{ active: page === 'overview' }" @click="page = 'overview'"><span>◈</span> 概览</div>
|
||||
@@ -67,7 +81,11 @@ onMounted(init);
|
||||
</div>
|
||||
</aside>
|
||||
<main class="main">
|
||||
<div class="topbar"><h1>{{ PAGES[page] }}</h1><span class="crumb" style="color:var(--text-3);font-size:13px">中枢</span></div>
|
||||
<div class="topbar">
|
||||
<h1>{{ PAGES[page] }}</h1>
|
||||
<span style="color: var(--text-3); font-size: 13px">{{ hub.hub_name }}</span>
|
||||
<button class="btn btn-ghost" style="margin-left: auto" @click="settingsOpen = true">⚙ 设置</button>
|
||||
</div>
|
||||
<div class="content">
|
||||
<section v-if="page === 'overview'">
|
||||
<div class="stats">
|
||||
@@ -81,6 +99,7 @@ onMounted(init);
|
||||
<Logs v-else @unauthorized="onUnauthorized" />
|
||||
</div>
|
||||
</main>
|
||||
<HubSettings :open="settingsOpen" :hub="hub" @close="settingsOpen = false" @saved="onSettingsSaved" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -43,4 +43,6 @@ export const api = {
|
||||
const qs = q.toString();
|
||||
return request('/logs' + (qs ? '?' + qs : ''));
|
||||
},
|
||||
settings: () => request('/settings'),
|
||||
updateSettings: (body) => request('/settings', { method: 'PUT', body }),
|
||||
};
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { api } from '../api/admin.js';
|
||||
|
||||
const props = defineProps({ open: Boolean, hub: Object });
|
||||
const emit = defineEmits(['close', 'saved']);
|
||||
|
||||
const hubId = ref('');
|
||||
const hubName = ref('');
|
||||
const busy = ref(false);
|
||||
const error = ref('');
|
||||
|
||||
watch(() => props.open, (open) => {
|
||||
if (open) {
|
||||
hubId.value = (props.hub && props.hub.hub_id) || '';
|
||||
hubName.value = (props.hub && props.hub.hub_name) || '';
|
||||
error.value = '';
|
||||
busy.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
async function save() {
|
||||
if (busy.value) return;
|
||||
if (!hubName.value.trim()) { error.value = '中枢名称不能为空'; return; }
|
||||
busy.value = true;
|
||||
error.value = '';
|
||||
try {
|
||||
const r = await api.updateSettings({ hub_id: hubId.value.trim(), hub_name: hubName.value.trim() });
|
||||
emit('saved', r.settings);
|
||||
} catch (e) {
|
||||
error.value = e.message || '保存失败';
|
||||
} finally {
|
||||
busy.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="open" class="overlay" @click.self="emit('close')">
|
||||
<div class="modal">
|
||||
<div class="modal-head">中枢设置</div>
|
||||
<div class="modal-body">
|
||||
<div class="field"><label>中枢名称</label><input v-model="hubName" placeholder="例如:湘湖实验室中枢"></div>
|
||||
<div class="field" style="margin-bottom: 0"><label>中枢 ID</label><input v-model="hubId" class="mono" placeholder="唯一标识,留空则由系统生成"></div>
|
||||
<p v-if="error" class="err">{{ error }}</p>
|
||||
</div>
|
||||
<div class="modal-foot">
|
||||
<button class="btn btn-ghost" @click="emit('close')">取消</button>
|
||||
<button class="btn btn-primary" :disabled="busy" @click="save">{{ busy ? '保存中…' : '保存' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.err { color: var(--red); font-size: 12px; margin: 12px 0 0; }
|
||||
</style>
|
||||
@@ -146,6 +146,11 @@ CREATE TABLE IF NOT EXISTS auth_tokens (
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_auth_tokens_user ON auth_tokens(user_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
);
|
||||
"""
|
||||
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ from ..services.auth import (
|
||||
delete_user,
|
||||
)
|
||||
from ..services.logview import query_logs
|
||||
from ..services.settings import get_hub_settings, set_hub_settings
|
||||
from ..upstream import envelope
|
||||
|
||||
router = APIRouter(prefix="/api/admin", tags=["admin"])
|
||||
@@ -44,6 +45,11 @@ class UserPatch(BaseModel):
|
||||
password: str | None = None
|
||||
|
||||
|
||||
class HubSettingsBody(BaseModel):
|
||||
hub_id: str | None = None
|
||||
hub_name: str | None = None
|
||||
|
||||
|
||||
@router.post("/login")
|
||||
async def login(body: LoginBody, db: Database = Depends(get_db)):
|
||||
user = await verify_login(db, body.username, body.password)
|
||||
@@ -79,6 +85,16 @@ async def units(_user: dict = Depends(require_admin)):
|
||||
return {"ok": True, "units": list(AUTHORIZATION_UNITS)}
|
||||
|
||||
|
||||
@router.get("/settings")
|
||||
async def get_settings_api(db: Database = Depends(get_db), _user: dict = Depends(require_admin)):
|
||||
return {"ok": True, "settings": await get_hub_settings(db)}
|
||||
|
||||
|
||||
@router.put("/settings")
|
||||
async def put_settings_api(body: HubSettingsBody, db: Database = Depends(get_db), _user: dict = Depends(require_admin)):
|
||||
return {"ok": True, "settings": await set_hub_settings(db, body.hub_id, body.hub_name)}
|
||||
|
||||
|
||||
@router.get("/users")
|
||||
async def users(db: Database = Depends(get_db), _user: dict = Depends(require_admin)):
|
||||
return {"ok": True, "users": await list_users(db)}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
"""services/settings.py —— 中枢级键值设置(hub_id / hub_name)。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
from ..db import Database
|
||||
|
||||
_HUB_ID_KEY = "hub_id"
|
||||
_HUB_NAME_KEY = "hub_name"
|
||||
DEFAULT_HUB_NAME = "蜂群中枢"
|
||||
|
||||
|
||||
async def get_setting(db: Database, key: str, default: str | None = None) -> str | None:
|
||||
row = await db.fetchone("SELECT value FROM settings WHERE key = ?", (key,))
|
||||
return row["value"] if row else default
|
||||
|
||||
|
||||
async def set_setting(db: Database, key: str, value: str) -> None:
|
||||
row = await db.fetchone("SELECT 1 FROM settings WHERE key = ?", (key,))
|
||||
if row is not None:
|
||||
await db.execute("UPDATE settings SET value = ? WHERE key = ?", (value, key))
|
||||
else:
|
||||
await db.execute("INSERT INTO settings (key, value) VALUES (?, ?)", (key, value))
|
||||
|
||||
|
||||
async def get_hub_settings(db: Database) -> dict:
|
||||
hub_id = await get_setting(db, _HUB_ID_KEY)
|
||||
if not hub_id:
|
||||
hub_id = str(uuid.uuid4())
|
||||
await set_setting(db, _HUB_ID_KEY, hub_id)
|
||||
hub_name = (await get_setting(db, _HUB_NAME_KEY, DEFAULT_HUB_NAME)) or DEFAULT_HUB_NAME
|
||||
return {"hub_id": hub_id, "hub_name": hub_name}
|
||||
|
||||
|
||||
async def set_hub_settings(db: Database, hub_id: str | None, hub_name: str | None) -> dict:
|
||||
if hub_id and hub_id.strip():
|
||||
await set_setting(db, _HUB_ID_KEY, hub_id.strip())
|
||||
if hub_name and hub_name.strip():
|
||||
await set_setting(db, _HUB_NAME_KEY, hub_name.strip())
|
||||
return await get_hub_settings(db)
|
||||
Reference in New Issue
Block a user