下线仪表盘页面并统一后台默认入口

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
2026-05-02 13:51:06 +08:00
parent bbe0fc9277
commit 19a39e0433
13 changed files with 94 additions and 250 deletions
+19 -7
View File
@@ -464,11 +464,11 @@
## 前端路由展示口径(2026-04-23
- 用户可见后台地址默认不再带 `/admin` 前缀(例如 `/users``/requirements``/dashboard`)。
- 用户可见后台地址默认不再带 `/admin` 前缀(例如 `/users``/requirements`)。
- 兼容策略:
- 无前缀地址通过前端中间层 rewrite 到现有 `app/admin/**` 页面实现;
- 历史 `/admin/**` 地址继续可访问,并自动重定向到无前缀地址。
- 后台首页映射口径:`/admin` 对外统一为 `/dashboard`
- 后台首页映射口径:`/admin` 对外统一为 `/users`
- 菜单路径展示口径:前端渲染菜单时将后端返回的 `/admin/**` 路径规范化为无前缀地址,避免导航条继续暴露 `/admin`
## 后台功能下线口径(2026-04-23
@@ -546,10 +546,10 @@
## 首页与登录口径(2026-04-23
- `/` 默认作为登录入口页,不再承载“已登录后停留的首页面板”。
- 登录态(含刷新会话恢复)进入 `/` 时,前端立即跳转 `/dashboard`,实现“登录后直达后台”。
- 登录态(含刷新会话恢复)进入 `/` 时,前端立即跳转 `/users`,实现“登录后直达后台”。
- 后台壳层文案对齐:
- 未登录访问后台时提示“前往登录”(`/`);
- 账号菜单提供“后台首页”(`/dashboard`),不再出现“返回首页”歧义入口。
- 账号菜单提供“用户管理”(`/users`)作为默认后台入口。
- 退出登录口径:统一跳转到登录页 `/`(不保留在当前后台路由)。
## 站点标题口径(2026-04-24
@@ -563,14 +563,14 @@
- 左侧为品牌与机器人主题视觉区;
- 右侧为白色登录卡片(品牌头、表单、主操作按钮、辅助链接)。
- 交互口径保持:
- 登录态进入 `/` 仍自动跳转 `/dashboard`
- 登录态进入 `/` 仍自动跳转 `/users`
- 登录/注册逻辑不变,视觉改造不改变鉴权接口契约。
## 后台账号入口口径(2026-04-23
- 后台右上角账号入口采用 AntD `Avatar` 作为下拉触发器,不再使用“账号”文字按钮。
- Avatar 文案默认使用用户名首字母(大写),空值兜底 `U`
- 下拉内容口径保持不变:账号信息 + 后台首页 + 退出登录。
- 下拉内容口径保持不变:账号信息 + 默认后台入口 + 退出登录。
## 后台左侧导航口径(2026-04-24
@@ -639,7 +639,19 @@
-`NEXT_PUBLIC_API_BASE_URL` 指向 loopback`localhost/127.0.0.1/::1`)时,前端运行时应保证 API host 与当前页面 host 对齐。
- 只要“配置 host 是 loopback 且与 `window.location.hostname` 不一致”,就自动重写为当前页面 host,并保留配置端口(默认 `8000`)。
- 目的:避免 `localhost``127.0.0.1` 混用导致 refresh cookie 跨站语义,从而在 `/dashboard` 硬刷新时误判未登录。
- 目的:避免 `localhost``127.0.0.1` 混用导致 refresh cookie 跨站语义,从而在后台路由硬刷新时误判未登录。
## 仪表盘下线口径(2026-05-02
- 后台“仪表盘”页面已下线,不再作为可访问菜单和默认首页。
- 前端路由口径:
- `/admin``/dashboard` 统一重定向到 `/users`
- 登录态进入 `/` 默认跳转到 `/users`
- `web/src/app/admin/page.tsx` 改为重定向页,不再渲染卡片工作台。
- 后端菜单口径:
- `seed_service.DEFAULT_MENUS` 删除 `dashboard`
- `ROLE_MENU_BINDINGS` 删除 admin/user 对 `dashboard` 的绑定;
- `legacy_authz_service``legacy_admin_rbac_service``admin_service``dashboard` 统一加入下线过滤集合,屏蔽历史库残留菜单记录。
## 后台顶部滚动口径(2026-04-25
+2 -1
View File
@@ -32,6 +32,7 @@ AUDIT_LOG_LOAD_OPTIONS = (
)
REMOVED_MENU_CODES = {
"dashboard",
"admin.wxapp",
"admin.system_message",
"admin.inbox",
@@ -403,7 +404,7 @@ def update_menu(db: Session, menu_id: int, payload: MenuUpdateRequest) -> MenuPu
def delete_menu(db: Session, menu_id: int) -> bool:
menu = get_menu_by_id(db, menu_id)
if not menu or menu.code in {"dashboard", "admin.users", "admin.roles", "admin.menus", "admin.system_params", "admin.power_lines", "admin.lightning_currents", "admin.lightning_distribution", "admin.workers", "admin.task_monitor", "admin.atp_models", "admin.files", "admin.elevation", "admin.syslog", "admin.wine_runner"}:
if not menu or menu.code in {"admin.users", "admin.roles", "admin.menus", "admin.system_params", "admin.power_lines", "admin.lightning_currents", "admin.lightning_distribution", "admin.workers", "admin.task_monitor", "admin.atp_models", "admin.files", "admin.elevation", "admin.syslog", "admin.wine_runner"}:
return False
child_exists = db.scalar(select(Menu.id).where(Menu.parent_id == menu_id))
if child_exists is not None:
@@ -30,6 +30,7 @@ from .user_service import queue_users_auth_refresh
PROTECTED_ROLE_IDS = {"admin", "user", "sys_mgr"}
REMOVED_MENU_CODES = {
"dashboard",
"admin.wxapp",
"admin.system_message",
"admin.inbox",
@@ -62,7 +63,6 @@ REMOVED_MENU_CODES = {
}
PROTECTED_MENU_CODES = {
"dashboard",
"admin.users",
"admin.roles",
"admin.menus",
+1 -1
View File
@@ -47,6 +47,7 @@ ADMIN_ROLE_IDS = {
}
DISABLED_MENU_CODES: set[str] = {
"dashboard",
"admin.wxapp",
"admin.system_message",
"admin.inbox",
@@ -97,7 +98,6 @@ MENU_CODE_PERMISSION_MAP: dict[str, set[str]] = {
"admin.lightning_currents": {"lightning.read", "lightning.manage"},
"admin.lightning_distribution": {"lightning.read", "lightning.manage"},
"admin.wine_runner": {"wine.read", "wine.manage"},
"dashboard": {"menu.read"},
}
SYNTHETIC_LEGACY_MENU_ROWS: list[dict[str, Any]] = [
+3 -16
View File
@@ -82,19 +82,6 @@ DEFAULT_ROLES: dict[str, dict[str, object]] = {
}
DEFAULT_MENUS: list[dict[str, object]] = [
{
"code": "dashboard",
"name": "仪表盘",
"path": "/admin",
"icon": "LayoutDashboard",
"parent_code": None,
"type": "menu",
"sort_order": 10,
"status": "enabled",
"visible": True,
"cacheable": False,
"permission_code": None,
},
{
"code": "admin.users",
"name": "用户管理",
@@ -102,7 +89,7 @@ DEFAULT_MENUS: list[dict[str, object]] = [
"icon": "Users",
"parent_code": None,
"type": "menu",
"sort_order": 20,
"sort_order": 10,
"status": "enabled",
"visible": True,
"cacheable": False,
@@ -280,8 +267,8 @@ DEFAULT_MENUS: list[dict[str, object]] = [
]
ROLE_MENU_BINDINGS: dict[str, list[str]] = {
"admin": ["dashboard", "admin.users", "admin.roles", "admin.menus", "admin.system_params", "admin.power_lines", "admin.lightning_currents", "admin.lightning_distribution", "admin.workers", "admin.task_monitor", "admin.atp_models", "admin.files", "admin.elevation", "admin.syslog", "admin.wine_runner"],
"user": ["dashboard"],
"admin": ["admin.users", "admin.roles", "admin.menus", "admin.system_params", "admin.power_lines", "admin.lightning_currents", "admin.lightning_distribution", "admin.workers", "admin.task_monitor", "admin.atp_models", "admin.files", "admin.elevation", "admin.syslog", "admin.wine_runner"],
"user": [],
}
-1
View File
@@ -17,7 +17,6 @@ TOPIC_RULES: dict[str, TopicRule] = {
"system": TopicRule(allow_any_authenticated_user=True, auto_subscribe=True),
"auth": TopicRule(allow_any_authenticated_user=True, auto_subscribe=True),
"notifications": TopicRule(allow_any_authenticated_user=True, auto_subscribe=True),
"admin.dashboard": TopicRule(allow_any_authenticated_user=True),
"admin.users": TopicRule(any_permission_codes={"user.manage"}),
"admin.roles": TopicRule(any_permission_codes={"role.read", "role.manage"}),
"admin.menus": TopicRule(any_permission_codes={"menu.read", "menu.manage"}),
+48
View File
@@ -764,3 +764,51 @@
- 验证:
- 代码走读确认 `seed_service._default_file_storage_backends/_default_file_storage_mounts` 基于 `settings.minio_enabled` 分支,默认行为已切换到 MinIO。
## Work Log - 去掉仪表盘页面(2026-05-02
- 背景:
- Issue `FL-166` 要求去掉后台“仪表盘页面”。
- 本次改动(最小闭环):
- 前端路由与默认跳转收口:
- `web/src/middleware.ts`
- `/admin``/dashboard` 统一重定向到 `/users`
- 历史 `/admin/**` 仍保持去前缀重定向;
- 无前缀路径继续 rewrite 到 `app/admin/**`
- `web/src/app/page.tsx`
- 登录态从跳转 `/dashboard` 改为跳转 `/users`
- `web/src/app/admin/page.tsx`
- 仪表盘页面改为纯重定向页(`router.replace("/users")`),不再渲染工作台卡片。
- `web/src/app/admin/layout.tsx`
- `/admin` 归一化路径改为 `/users`
- 账号下拉“后台首页”改为“用户管理”(`/users`)。
- `web/src/app/admin/roles/page.tsx`
- 无权限兜底按钮改为返回 `/users`
- `web/src/app/admin/menus/page.tsx`
- 受保护菜单集合移除 `dashboard`
- 后端菜单与权限链路清理:
- `api/app/services/seed_service.py`
- 删除 `dashboard` 种子菜单;
- `admin`/`user` 默认角色菜单绑定删除 `dashboard`
- `admin.users` 排序调整为首位(`sort_order=10`)。
- `api/app/services/admin_service.py`
- `REMOVED_MENU_CODES` 增加 `dashboard`
- 菜单删除保护集合移除 `dashboard`
- `api/app/services/legacy_authz_service.py`
- `DISABLED_MENU_CODES` 增加 `dashboard`
- `MENU_CODE_PERMISSION_MAP` 删除 `dashboard` 映射。
- `api/app/services/legacy_admin_rbac_service.py`
- `REMOVED_MENU_CODES` 增加 `dashboard`
- `PROTECTED_MENU_CODES` 移除 `dashboard`
- `api/app/services/topic_registry.py`
- 删除 `admin.dashboard` 主题规则。
- 记忆更新:
- `MEMORY.md` 新增“仪表盘下线口径(2026-05-02)”,并同步修正历史 `/dashboard` 默认入口描述为 `/users`
- 验证:
- `rg -n "dashboard|/dashboard"` 检查运行代码,确认仅保留兼容重定向/过滤语义,不再保留仪表盘页面入口逻辑。
- 风险与影响:
- 历史收藏的 `/dashboard` 链接会跳转到 `/users`(兼容行为)。
- 默认 `user` 角色在无其他菜单绑定时,后台菜单可能为空;该改动符合“移除仪表盘菜单”的目标口径。
+2 -2
View File
@@ -63,7 +63,7 @@ function normalizeAdminPath(path: string | null): string | null {
return path;
}
if (path === "/admin" || path === "/admin/") {
return "/dashboard";
return "/users";
}
if (path.startsWith("/admin/")) {
return path.slice("/admin".length);
@@ -272,7 +272,7 @@ export default function AdminLayout({ children }: { children: ReactNode }) {
{
key: "home",
icon: <HomeOutlined />,
label: <Link href="/dashboard"></Link>,
label: <Link href="/users"></Link>,
},
{
key: "logout",
-1
View File
@@ -57,7 +57,6 @@ const SORT_OPTIONS: Array<{ value: SortKey; label: string }> = [
];
const PROTECTED_MENU_CODES = new Set([
"dashboard",
"admin.users",
"admin.roles",
"admin.menus",
+8 -214
View File
@@ -1,220 +1,14 @@
"use client";
import Link from "next/link";
import {
AppstoreOutlined,
AuditOutlined,
CodeOutlined,
DatabaseOutlined,
DeploymentUnitOutlined,
FileSearchOutlined,
FolderOpenOutlined,
GlobalOutlined,
GoldOutlined,
LineChartOutlined,
SafetyCertificateOutlined,
SettingOutlined,
TeamOutlined,
} from "@ant-design/icons";
import { Avatar, Card, Col, Empty, Row, Space, Statistic, Tag, Typography, type CardProps } from "antd";
import type { ComponentType, ReactNode } from "react";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
import { useAuth } from "@/components/auth-provider";
export default function AdminEntryRedirectPage() {
const router = useRouter();
const AntCard = Card as unknown as ComponentType<CardProps>;
useEffect(() => {
router.replace("/users");
}, [router]);
type DashboardCard = {
href: string;
title: string;
description: string;
category: string;
icon: ReactNode;
visible: (hasPermission: (code: string) => boolean) => boolean;
};
const CARDS: DashboardCard[] = [
{
href: "/users",
title: "用户管理",
description: "查看用户、分配角色、维护账号状态。",
category: "权限",
icon: <TeamOutlined />,
visible: (hasPermission) => hasPermission("user.manage"),
},
{
href: "/roles",
title: "角色管理",
description: "配置角色并分配菜单可见范围。",
category: "权限",
icon: <SafetyCertificateOutlined />,
visible: (hasPermission) => hasPermission("role.read") || hasPermission("role.manage"),
},
{
href: "/menus",
title: "菜单管理",
description: "维护后台导航结构、菜单层级与展示状态。",
category: "权限",
icon: <AppstoreOutlined />,
visible: (hasPermission) => hasPermission("menu.read") || hasPermission("menu.manage"),
},
{
href: "/system-params",
title: "系统参数",
description: "维护系统级参数键值、启停状态与变更说明。",
category: "系统",
icon: <SettingOutlined />,
visible: (hasPermission) => hasPermission("system_param.read") || hasPermission("system_param.manage"),
},
{
href: "/files",
title: "文件管理",
description: "统一管理本地/SFTP/S3 文件目录,支持上传、重命名、移动和下载。",
category: "内容",
icon: <FolderOpenOutlined />,
visible: (hasPermission) => hasPermission("file.read") || hasPermission("file.manage"),
},
{
href: "/workers",
title: "Worker监控",
description: "查看 Worker 在线状态、并发、活跃队列和每个 Worker 的任务快照。",
category: "协作",
icon: <DeploymentUnitOutlined />,
visible: (hasPermission) => hasPermission("celery.read") || hasPermission("celery.manage"),
},
{
href: "/task-monitor",
title: "任务监控",
description: "监控 Celery Worker、队列积压与任务执行状态,快速定位失败与阻塞。",
category: "协作",
icon: <AuditOutlined />,
visible: (hasPermission) => hasPermission("celery.read") || hasPermission("celery.manage"),
},
{
href: "/power-lines",
title: "线路管理",
description: "维护输电线路与杆塔参数,支持导入导出和风险字段管理。",
category: "电力",
icon: <GoldOutlined />,
visible: (hasPermission) =>
hasPermission("line.read") || hasPermission("line.manage") || hasPermission("tower.read") || hasPermission("tower.manage"),
},
{
href: "/power-lines/atp-viewer",
title: "ATP查看器",
description: "将 ATP 文本转换为 JSON,并用 maxGraph 进行电路图查看。",
category: "电力",
icon: <GoldOutlined />,
visible: (hasPermission) =>
hasPermission("line.read") || hasPermission("line.manage") || hasPermission("tower.read") || hasPermission("tower.manage"),
},
{
href: "/lightning-currents",
title: "雷电幅值统计",
description: "导入雷电流原始序列并自动提取防雷计算参数。",
category: "电力",
icon: <LineChartOutlined />,
visible: (hasPermission) => hasPermission("lightning.read") || hasPermission("lightning.manage"),
},
{
href: "/lightning-distribution",
title: "雷电分布统计",
description: "计算 Ng、空间网格、热力散点和杆塔缓冲区风险。",
category: "电力",
icon: <GlobalOutlined />,
visible: (hasPermission) => hasPermission("lightning.read") || hasPermission("lightning.manage"),
},
{
href: "/elevation",
title: "高程数据管理",
description: "维护高程数据集并执行线路杆塔高程回填任务。",
category: "电力",
icon: <DatabaseOutlined />,
visible: (hasPermission) => hasPermission("elevation.read") || hasPermission("elevation.manage"),
},
{
href: "/syslog",
title: "系统日志",
description: "查看鉴权与会话类审计日志,支持动作与用户筛选。",
category: "系统",
icon: <FileSearchOutlined />,
visible: (hasPermission) => hasPermission("menu.read") || hasPermission("menu.manage"),
},
{
href: "/wine-runner",
title: "Wine执行器",
description: "通过 Wine 执行 Windows EXE,并实时查看测试日志。",
category: "研发",
icon: <CodeOutlined />,
visible: (hasPermission) => hasPermission("wine.read") || hasPermission("wine.manage"),
},
];
export default function AdminHomePage() {
const { hasPermission, user } = useAuth();
const visibleCards = CARDS.filter((item) => item.visible(hasPermission));
const categoryCount = new Set(visibleCards.map((item) => item.category)).size;
if (visibleCards.length === 0) {
return (
<AntCard>
<Empty description="当前账号暂无可访问的后台模块。" image={Empty.PRESENTED_IMAGE_SIMPLE} />
</AntCard>
);
}
return (
<Space direction="vertical" size={24} style={{ width: "100%" }}>
<Row gutter={[16, 16]}>
<Col xs={24} md={8}>
<AntCard>
<Statistic title="可访问模块" value={visibleCards.length} suffix="个" />
</AntCard>
</Col>
<Col xs={24} md={8}>
<AntCard>
<Statistic title="业务分组" value={categoryCount} suffix="类" />
</AntCard>
</Col>
<Col xs={24} md={8}>
<AntCard>
<Statistic title="当前角色" value={user?.role_codes.length ?? 0} suffix="个" />
</AntCard>
</Col>
</Row>
<div>
<Space align="baseline" style={{ marginBottom: 16 }}>
<Typography.Title level={4} style={{ margin: 0 }}>
</Typography.Title>
<Typography.Text type="secondary"> Ant Design </Typography.Text>
</Space>
<Row gutter={[16, 16]}>
{visibleCards.map((item) => (
<Col key={item.href} xs={24} sm={12} xl={8} xxl={6}>
<Link href={item.href} style={{ display: "block", height: "100%" }}>
<AntCard hoverable style={{ height: "100%" }}>
<Space align="start" size={12}>
<Avatar
icon={item.icon}
shape="square"
style={{ backgroundColor: "var(--ant-color-primary)" }}
/>
<Space direction="vertical" size={4}>
<Space size={8} wrap>
<Typography.Text strong>{item.title}</Typography.Text>
<Tag color="blue">{item.category}</Tag>
</Space>
<Typography.Text type="secondary">{item.description}</Typography.Text>
</Space>
</Space>
</AntCard>
</Link>
</Col>
))}
</Row>
</div>
</Space>
);
return null;
}
+1 -1
View File
@@ -340,7 +340,7 @@ export default function AdminRolesPage() {
subTitle="你没有访问该页面的权限(需要 role.read)。"
extra={(
<Button>
<Link href="/dashboard"></Link>
<Link href="/users"></Link>
</Button>
)}
/>
+1 -1
View File
@@ -24,7 +24,7 @@ export default function Home() {
useEffect(() => {
if (!initializing && user) {
router.replace("/dashboard");
router.replace("/users");
}
}, [initializing, router, user]);
+8 -4
View File
@@ -32,22 +32,26 @@ export function middleware(request: NextRequest) {
// Keep backward compatibility for existing /admin links.
if (pathname === "/admin" || pathname === "/admin/") {
const url = request.nextUrl.clone();
url.pathname = "/dashboard";
url.pathname = "/users";
return NextResponse.redirect(url);
}
if (pathname === "/dashboard" || pathname === "/dashboard/") {
const url = request.nextUrl.clone();
url.pathname = "/users";
return NextResponse.redirect(url);
}
if (pathname.startsWith("/admin/")) {
const url = request.nextUrl.clone();
url.pathname = pathname.slice("/admin".length) || "/dashboard";
url.pathname = pathname.slice("/admin".length) || "/users";
return NextResponse.redirect(url);
}
// New public URLs without /admin are internally rewritten to existing routes.
const url = request.nextUrl.clone();
url.pathname = pathname === "/dashboard" ? "/admin" : `/admin${pathname}`;
url.pathname = `/admin${pathname}`;
return NextResponse.rewrite(url);
}
export const config = {
matcher: "/:path*",
};