diff --git a/src/lib/auth.tsx b/src/lib/auth.tsx
index b37aef2..b7fd8a5 100644
--- a/src/lib/auth.tsx
+++ b/src/lib/auth.tsx
@@ -104,9 +104,8 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
identities,
currentIdentity,
isAuthed: !!token,
- // 运营管理端只保留运营/园区单一身份:角色恒取账号真实角色(user.role),不随
- // 首个身份(currentIdentity[0])跳动 —— 避免「登录后显示无权限,刷新后才恢复」。
- role: user?.role || currentIdentity?.role || null,
+ // 账号单一角色:role 恒取账号真实类型(user.role),不再多身份/currentIdentity。
+ role: user?.role || null,
permissions: user?.permissions ?? [],
username: user?.username ?? null,
login,
diff --git a/src/lib/rbac.tsx b/src/lib/rbac.tsx
index 0af7821..333aaca 100644
--- a/src/lib/rbac.tsx
+++ b/src/lib/rbac.tsx
@@ -14,9 +14,14 @@ import {
Store,
Settings,
UserCircle2,
+ Monitor,
+ MonitorPlay,
+ BookOpen,
+ Sparkles,
+ Video,
+ ArrowRightLeft,
} from "lucide-react";
import { useAuth } from "@/lib/auth";
-import { isParkUser } from "@/lib/parkAuth";
export interface NavItem {
to: string;
@@ -24,6 +29,8 @@ export interface NavItem {
icon: LucideIcon;
/** 允许访问的端口角色(identity.role) */
roles?: string[];
+ /** 需要的权限码(menu:*),账号权限集含才显示;缺省则恒显示 */
+ perm?: string;
}
export interface NavGroup {
@@ -35,65 +42,86 @@ export interface NavGroup {
export const NAV: NavGroup[] = [
{
items: [
- { to: "/", label: "总览", icon: LayoutDashboard, roles: ["operator"] },
+ { to: "/", label: "总览", icon: LayoutDashboard, roles: ["operator"], perm: "menu:operator_dashboard" },
],
},
{
title: "平台管理",
items: [
- { to: "/users", label: "用户管理", icon: Users, roles: ["operator"] },
- { to: "/certification", label: "OPC认证", icon: BadgeCheck, roles: ["operator"] },
- { to: "/admission", label: "入驻审核", icon: Building2, roles: ["operator"] },
- { to: "/park-transfer", label: "转园审核", icon: Building2, roles: ["operator"] },
- { to: "/compute", label: "算力管理", icon: Cpu, roles: ["operator"] },
- { to: "/park", label: "园区管理", icon: Building2, roles: ["operator", "carrier"] },
+ { to: "/users", label: "用户管理", icon: Users, roles: ["operator"], perm: "menu:admin_user_mgmt" },
+ { to: "/certification", label: "OPC认证", icon: BadgeCheck, roles: ["operator"], perm: "menu:opc_cert" },
+ { to: "/admission", label: "入驻审核", icon: Building2, roles: ["operator"], perm: "menu:admission" },
+ { to: "/park-transfer", label: "转园审核", icon: Building2, roles: ["operator"], perm: "menu:park_transfer" },
+ { to: "/compute", label: "算力管理", icon: Cpu, roles: ["operator"], perm: "menu:admin_compute" },
+ { to: "/park", label: "平台园区", icon: Building2, roles: ["operator"], perm: "menu:admin_park" },
+ { to: "/park-screens", label: "屏幕绑定", icon: Monitor, roles: ["operator"], perm: "menu:admin_park_screens" },
{ to: "/settings", label: "个人中心", icon: UserCircle2, roles: ["operator"] },
],
},
{
title: "培训内容",
items: [
- { to: "/courses", label: "课程管理", icon: GraduationCap, roles: ["operator"] },
- { to: "/activities", label: "活动管理", icon: Calendar, roles: ["operator"] },
- { to: "/bookings", label: "报名管理", icon: ClipboardList, roles: ["operator"] },
- { to: "/tests", label: "测评管理", icon: FileCheck2, roles: ["operator"] },
- { to: "/surveys", label: "调研结果", icon: ClipboardList, roles: ["operator"] },
- { to: "/policies", label: "政策结果", icon: FileCheck2, roles: ["operator"] },
- { to: "/plans", label: "流程启动", icon: Flag, roles: ["operator"] },
+ { to: "/courses", label: "课程管理", icon: GraduationCap, roles: ["operator"], perm: "menu:admin_courses" },
+ { to: "/activities", label: "活动管理", icon: Calendar, roles: ["operator"], perm: "menu:admin_activities" },
+ { to: "/bookings", label: "报名管理", icon: ClipboardList, roles: ["operator"], perm: "menu:admin_bookings" },
+ { to: "/tests", label: "测评管理", icon: FileCheck2, roles: ["operator"], perm: "menu:admin_tests" },
+ { to: "/surveys", label: "调研结果", icon: ClipboardList, roles: ["operator"], perm: "menu:admin_surveys" },
+ { to: "/policies", label: "政策结果", icon: FileCheck2, roles: ["operator"], perm: "menu:admin_policies" },
+ { to: "/plans", label: "流程启动", icon: Flag, roles: ["operator"], perm: "menu:admin_plans" },
],
},
{
title: "业务运营",
items: [
- { to: "/content", label: "资讯管理", icon: Newspaper, roles: ["operator"] },
- { to: "/tasks", label: "任务管理", icon: Flag, roles: ["operator"] },
- { to: "/providers", label: "服务商管理", icon: Store, roles: ["operator"] },
- { to: "/config", label: "系统配置", icon: Settings, roles: ["operator"] },
+ { to: "/content", label: "资讯管理", icon: Newspaper, roles: ["operator"], perm: "menu:admin_content" },
+ { to: "/tasks", label: "任务管理", icon: Flag, roles: ["operator"], perm: "menu:admin_tasks" },
+ { to: "/providers", label: "服务商管理", icon: Store, roles: ["operator"], perm: "menu:admin_providers" },
+ { to: "/config", label: "系统配置", icon: Settings, roles: ["operator"], perm: "menu:admin_config" },
],
},
];
-// 按当前身份角色过滤导航
+// 园区端(载体方 carrier)工作台菜单:功能区按主题分组(本园区)
+export const CARRIER_NAV: NavGroup[] = [
+ {
+ title: "园区运营",
+ items: [
+ { to: "/park/screens", label: "屏幕管理", icon: Monitor, perm: "menu:park_screens" },
+ { to: "/park/screen", label: "大屏控制台", icon: MonitorPlay, perm: "menu:park_screen" },
+ { to: "/park/companies", label: "入驻企业", icon: Building2, perm: "menu:park_companies" },
+ { to: "/park/agent", label: "园区智能体", icon: Sparkles, perm: "menu:park_agent" },
+ ],
+ },
+ {
+ title: "园区数据",
+ items: [
+ { to: "/park/kb", label: "园区知识库", icon: BookOpen, perm: "menu:park_kb" },
+ { to: "/park/data", label: "大屏数据", icon: MonitorPlay, perm: "menu:park_data" },
+ { to: "/park/vision", label: "视频识别", icon: Video, perm: "menu:park_vision" },
+ ],
+ },
+ {
+ title: "园区审核",
+ items: [
+ { to: "/park/admissions", label: "入园申请", icon: ClipboardList, perm: "menu:park_admissions" },
+ { to: "/park/transfers", label: "转园申请", icon: ArrowRightLeft, perm: "menu:park_transfers" },
+ ],
+ },
+];
+
+// 按当前身份角色(role)分流 + 权限码(permissions)精细过滤 —— 无权限菜单一律不显示
export function navForRole(role: string | null): NavGroup[] {
- const { isAuthed } = useAuth();
- // 平台管理方:持平台令牌即恒显示完整运营管理导航。
- // 不再叠加 !isParkUser() —— 避免「本会话曾登录过园区端/残留园区凭据」导致登录后只见园区管理,
- // 只有刷新/重新拉取身份后才恢复。平台令牌存在即平台优先。
- if (isAuthed) {
- const r = role === "operator" ? role : "operator";
- return NAV.map((g) => ({
- ...g,
- items: g.items.filter((i) => !i.roles || i.roles.includes(r)),
- })).filter((g) => g.items.length > 0);
- }
- // 园区端(园区账密登录,无平台令牌):只显示园区管理(本园区)
- if (isParkUser()) {
- return [{ title: "园区管理", items: [{ to: "/park", label: "园区管理", icon: Building2 }] }];
- }
- if (!role) return [];
+ const { isAuthed, permissions } = useAuth();
+ if (!isAuthed) return [];
+ // 平台统一登录:只按账号类型(role)分流到对应菜单组;组内再按权限码(perm)过滤。
+ // operator(平台)→平台管理 NAV;carrier(园区/载体)→园区工作台 CARRIER_NAV;
+ // 其它(opc_member/service/无)→ 无运营菜单(走各自 C 端,不在此管理台显示)。
+ const allowed = (i: NavItem) => !i.perm || !permissions?.length || permissions.includes(i.perm);
+ if (role === "carrier") return CARRIER_NAV.map((g) => ({ ...g, items: g.items.filter(allowed) })).filter((g) => g.items.length > 0);
+ if (role !== "operator") return [];
return NAV.map((g) => ({
...g,
- items: g.items.filter((i) => !i.roles || i.roles.includes(role)),
+ items: g.items.filter((i) => allowed(i) && (!i.roles || i.roles.includes("operator"))),
})).filter((g) => g.items.length > 0);
}
diff --git a/src/pages/login/index.tsx b/src/pages/login/index.tsx
index f4ae01a..8a2481d 100644
--- a/src/pages/login/index.tsx
+++ b/src/pages/login/index.tsx
@@ -1,7 +1,7 @@
import * as React from "react";
-import { useNavigate, Navigate } from "react-router-dom";
+import { Navigate, useNavigate } from "react-router-dom";
import { useAuth } from "@/lib/auth";
-import { parkLogin, isParkUser, clearParkAuth } from "@/lib/parkAuth";
+import { clearParkAuth } from "@/lib/parkAuth";
import * as authApi from "@/api/auth";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
@@ -46,7 +46,7 @@ function PollingQr({
setQrImage(qr_image || "");
setStatus("pending");
const timer = window.setInterval(async () => {
- if (busy) return; // 防重入
+ if (busy) return;
setBusy(true);
try {
const res = await pollRef.current(scene);
@@ -59,9 +59,7 @@ function PollingQr({
setStatus("error");
setError("二维码已过期,请刷新");
}
- } finally {
- setBusy(false);
- }
+ } finally { setBusy(false); }
}, 2000);
} catch (err) {
setStatus("error");
@@ -93,7 +91,6 @@ function PollingQr({
export default function LoginPage() {
const { isAuthed, login, loginWithResponse } = useAuth();
const navigate = useNavigate();
- const [mode, setMode] = React.useState<"platform" | "park">("platform");
const [pmode, setPmode] = React.useState<"account" | "sms" | "miniprogram">("account");
const [username, setUsername] = React.useState("pine");
const [password, setPassword] = React.useState("123456");
@@ -105,13 +102,12 @@ export default function LoginPage() {
const [loading, setLoading] = React.useState(false);
if (isAuthed) return
;
- if (isParkUser()) return
;
- /** 平台登录结果:登录即以运营端进入(单一身份,无需选择/切换身份) */
+ /** 平台登录结果:按账号类型(role)直接进对应端口(carrier→园区工作台,其余→平台) */
async function applyPlatformResult(res: LoginResponse | null) {
if (!res) return;
clearParkAuth();
- navigate("/", { replace: true });
+ navigate(res.role === "carrier" ? "/park" : "/", { replace: true });
}
async function onAccountSubmit(e: React.FormEvent) {
@@ -149,111 +145,72 @@ export default function LoginPage() {
}
function onQrResult(res: LoginResponse) {
- // 与短信/账号一致:先 loginWithResponse 真正落库(设 token + 单身份自动选中),再跳转,
- // 否则 RequireAuth 判未登录又回 login。
void loginWithResponse(res).then((r) => applyPlatformResult(r));
}
- async function onParkSubmit(e: React.FormEvent) {
- e.preventDefault();
- setError(null); setLoading(true);
- try {
- await parkLogin(username, password);
- navigate("/park", { replace: true });
- } catch (err) {
- setError(err instanceof Error ? err.message : "登录失败");
- } finally { setLoading(false); }
- }
-
return (
云超服 · 运营管理平台
- 平台运营(运营端) 或 园区账密(园区端) 登录
+ 平台统一登录:运营端 / 园区端(载体方) 按账号身份进入
- { setMode(v as "platform" | "park"); setError(null); }}>
-
- 平台登录
- 园区端登录
+ { setPmode(v as typeof pmode); setError(null); }}>
+
+ 账号
+ 短信
+ 小程序
-
- { setPmode(v as typeof pmode); setError(null); }}>
-
- 账号
- 短信
- 小程序
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-