diff --git a/src/App.tsx b/src/App.tsx index e6d86c6..945c31a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,5 @@ import { Navigate, Outlet, Route, Routes } from "react-router-dom"; import { AuthProvider, useAuth } from "@/lib/auth"; -import { isParkUser } from "@/lib/parkAuth"; import { hasPermission } from "@/lib/rbac"; import { AppShell } from "@/components/layout/AppShell"; import LoginPage from "@/pages/login"; @@ -19,15 +18,20 @@ import ContentPage from "@/pages/content"; import TasksPage from "@/pages/tasks"; import ProvidersPage from "@/pages/providers"; import ConfigPage from "@/pages/config"; -import ParkPage from "@/pages/park"; +import ParkLayout from "@/pages/park"; +import ParkScreensPage from "@/pages/park-screens"; +import { + ScreensPage, ScreenPage, CompaniesPage, KbPage, AgentPage, + ScreenDataPage, VisionPage, AdmissionsPage, TransfersPage, +} from "@/pages/park/pages"; import AdmissionPage from "@/pages/admission"; import ParkTransferPage from "@/pages/park-transfer"; import SettingsPage from "@/pages/settings"; function RequireAuth() { const { isAuthed } = useAuth(); - // 平台(运营端) 或 园区账密(园区端)均可进入管理台 - if (!isAuthed && !isParkUser()) return ; + // 平台统一登录(运营端/园区端载体方均为平台账号)→ 持平台令牌即可进入管理台 + if (!isAuthed) return ; return ; } @@ -69,10 +73,22 @@ export default function App() { } /> } /> } /> + } /> } /> - {/* 园区管理:平台(运营端)全园管理 / 园区账密(园区端)仅本园区 —— 园区端无平台角色,故不套 RoleRoute */} - } /> + {/* 园区:平台(operator)→平台园区管理页(ParkLayout 内判断);园区账密(carrier)→园区工作台布局+子路由。园区端无平台角色,故不套 RoleRoute */} + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> diff --git a/src/components/layout/AppShell.tsx b/src/components/layout/AppShell.tsx index ad8b2b6..9a94f59 100644 --- a/src/components/layout/AppShell.tsx +++ b/src/components/layout/AppShell.tsx @@ -20,8 +20,16 @@ import { export function AppShell() { const { role, user, logout } = useAuth(); const groups = navForRole(role); - // 展示文本标签(账号类型): 运营方/载体方/OPC/服务方 —— 后端下发 account_type_label, 不做代码映射 - const roleLabel = (user as { account_type_label?: string })?.account_type_label || role || "运营方"; + // 展示文本标签(账号类型): 运营方/载体方/OPC/服务方。 + // 优先按 role 映射(取自身份/端口角色, 可靠), 兜底用后端下发的 account_type_label。 + const roleToLabel: Record = { + operator: "运营方", + carrier: "载体方", + opc_member: "OPC", + service: "服务方", + }; + const atypeLabel = (user as { account_type_label?: string })?.account_type_label; + const roleLabel = roleToLabel[role ?? ""] || atypeLabel || role || "运营方"; return (
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); }}> - - 账号 - 短信 - 小程序 - - - -
-
- - setUsername(e.target.value)} autoComplete="username" /> -
-
- - setPassword(e.target.value)} autoComplete="current-password" /> -
- {error &&

{error}

} - -
-
- - -
-
- - setPhone(e.target.value)} placeholder="请输入 11 位手机号" /> -
-
- -
- setSmsCode(e.target.value)} placeholder="请输入验证码" /> - -
-
- {error &&

{error}

} - -
-
- - - - -
-
- - -
+ +
- - setUsername(e.target.value)} autoComplete="username" /> + + setUsername(e.target.value)} autoComplete="username" />
- - setPassword(e.target.value)} autoComplete="current-password" /> + + setPassword(e.target.value)} autoComplete="current-password" />
{error &&

{error}

}
+ + +
+
+ + setPhone(e.target.value)} placeholder="请输入 11 位手机号" autoComplete="tel" /> +
+
+ +
+ setSmsCode(e.target.value)} placeholder="请输入验证码" /> + +
+
+ {error &&

{error}

} + +
+
+ + + +