feat:前端修改
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import {
|
||||
Bell,
|
||||
ChevronDown,
|
||||
Database,
|
||||
FileText,
|
||||
LayoutDashboard,
|
||||
ListChecks,
|
||||
LogOut,
|
||||
PanelLeftClose,
|
||||
PanelLeftOpen,
|
||||
PhoneCall,
|
||||
ShieldAlert,
|
||||
Users,
|
||||
X,
|
||||
FileSignature,
|
||||
} from "lucide-react";
|
||||
import { useState, type ReactNode } from "react";
|
||||
import { clearToken, getRoleLabel } from "@/lib/auth";
|
||||
import { canUseSection, isAdmin, isStaff, type WorkspaceSection } from "@/lib/roles";
|
||||
import type { UserPayload } from "@/types/api";
|
||||
import { Badge, cn } from "@/components/admin/ui";
|
||||
|
||||
export interface ShellNotification {
|
||||
id: string;
|
||||
title: string;
|
||||
body: string;
|
||||
time: string;
|
||||
tone: "alert" | "info" | "check";
|
||||
section: WorkspaceSection;
|
||||
}
|
||||
|
||||
const NAV_ITEMS: {
|
||||
section: WorkspaceSection;
|
||||
label: string;
|
||||
icon: typeof Users;
|
||||
requires: "staff" | "advisor" | "risk" | "admin";
|
||||
}[] = [
|
||||
{ section: "overview", label: "工作台概览", icon: LayoutDashboard, requires: "staff" },
|
||||
{ section: "todos", label: "我的待办", icon: ListChecks, requires: "advisor" },
|
||||
{ section: "customers", label: "客户管理", icon: Users, requires: "advisor" },
|
||||
{ section: "drafts", label: "投顾草稿", icon: FileSignature, requires: "advisor" },
|
||||
{ section: "visits", label: "客户回访", icon: PhoneCall, requires: "advisor" },
|
||||
{ section: "risk", label: "风险预警", icon: ShieldAlert, requires: "risk" },
|
||||
{ section: "workorders", label: "工单处理", icon: FileText, requires: "risk" },
|
||||
{ section: "funds", label: "基金数据", icon: Database, requires: "staff" },
|
||||
];
|
||||
|
||||
const REQUIREMENT_TEXT: Record<string, string> = {
|
||||
staff: "仅员工账号可用",
|
||||
advisor: "需要投顾权限",
|
||||
risk: "需要风控权限",
|
||||
admin: "需要管理员权限",
|
||||
};
|
||||
|
||||
export function AdminShell({
|
||||
user,
|
||||
section,
|
||||
onSectionChange,
|
||||
title,
|
||||
subtitle,
|
||||
notifications = [],
|
||||
children,
|
||||
}: {
|
||||
user: UserPayload;
|
||||
section: WorkspaceSection;
|
||||
onSectionChange: (section: WorkspaceSection) => void;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
notifications?: ShellNotification[];
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [showNotifications, setShowNotifications] = useState(false);
|
||||
|
||||
const roleLabel = isAdmin(user) ? "管理员" : getRoleLabel(user.employee_role);
|
||||
const accessible = isStaff(user);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-50 text-slate-900">
|
||||
<aside
|
||||
className={cn(
|
||||
"fixed inset-y-0 left-0 z-40 flex flex-col bg-slate-950 text-white transition-all duration-200",
|
||||
collapsed ? "w-16" : "w-56"
|
||||
)}
|
||||
>
|
||||
<div className={cn("flex items-center gap-3 px-5 py-6", collapsed && "justify-center px-3")}>
|
||||
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-indigo-500">
|
||||
<LayoutDashboard className="h-5 w-5" />
|
||||
</div>
|
||||
{!collapsed && (
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-semibold">华夏基金</p>
|
||||
<p className="truncate text-xs text-slate-400">统一员工工作台</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 space-y-1 px-3">
|
||||
{NAV_ITEMS.map((item) => {
|
||||
const allowed = canUseSection(user, item.section) && accessible;
|
||||
const active = section === item.section;
|
||||
return (
|
||||
<button
|
||||
key={item.section}
|
||||
onClick={() => onSectionChange(item.section)}
|
||||
title={allowed ? item.label : `${item.label}(${REQUIREMENT_TEXT[item.requires]})`}
|
||||
className={cn(
|
||||
"flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-sm transition-colors",
|
||||
collapsed && "justify-center px-0",
|
||||
active
|
||||
? "bg-white/10 text-white"
|
||||
: allowed
|
||||
? "text-slate-400 hover:bg-white/5 hover:text-white"
|
||||
: "text-slate-600 hover:bg-white/5"
|
||||
)}
|
||||
>
|
||||
<item.icon className="h-[18px] w-[18px] shrink-0" />
|
||||
{!collapsed && <span className="truncate">{item.label}</span>}
|
||||
{!collapsed && !allowed && (
|
||||
<span className="ml-auto rounded bg-slate-800 px-1.5 py-0.5 text-[10px] text-slate-400">
|
||||
无权限
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<div className={cn("border-t border-white/10 p-4", collapsed && "px-2")}>
|
||||
<Link
|
||||
href="/"
|
||||
className={cn(
|
||||
"mb-3 block text-xs text-slate-400 hover:text-white",
|
||||
collapsed && "text-center"
|
||||
)}
|
||||
>
|
||||
返回官网
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => {
|
||||
clearToken();
|
||||
window.location.href = "/login";
|
||||
}}
|
||||
className={cn(
|
||||
"flex items-center gap-2 text-sm text-slate-300 transition-colors hover:text-white",
|
||||
collapsed && "justify-center w-full"
|
||||
)}
|
||||
>
|
||||
<LogOut className="h-4 w-4 shrink-0" />
|
||||
{!collapsed && "退出登录"}
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"min-h-screen transition-all duration-200",
|
||||
collapsed ? "ml-16" : "ml-56"
|
||||
)}
|
||||
>
|
||||
<header className="sticky top-0 z-30 flex h-16 items-center justify-between border-b border-slate-200 bg-white px-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => setCollapsed(!collapsed)}
|
||||
aria-label={collapsed ? "展开侧边栏" : "收起侧边栏"}
|
||||
className="rounded-lg p-2 text-slate-500 transition-colors hover:bg-slate-100"
|
||||
>
|
||||
{collapsed ? <PanelLeftOpen className="h-5 w-5" /> : <PanelLeftClose className="h-5 w-5" />}
|
||||
</button>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-slate-800">{title}</p>
|
||||
<p className="text-xs text-slate-500">{subtitle}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setShowNotifications(!showNotifications)}
|
||||
aria-label="通知与提醒"
|
||||
className="relative rounded-lg p-2 text-slate-500 transition-colors hover:bg-slate-100"
|
||||
>
|
||||
<Bell className="h-5 w-5" />
|
||||
{notifications.length > 0 && (
|
||||
<span className="absolute right-1 top-1 flex h-4 min-w-4 items-center justify-center rounded-full bg-red-500 px-1 text-[10px] font-medium text-white">
|
||||
{notifications.length > 99 ? "99+" : notifications.length}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{showNotifications && (
|
||||
<div className="absolute right-0 top-full mt-2 w-80 rounded-xl border border-slate-200 bg-white shadow-lg z-50">
|
||||
<div className="flex items-center justify-between border-b border-slate-100 px-4 py-3">
|
||||
<span className="text-sm font-semibold text-slate-900">通知与提醒</span>
|
||||
<button
|
||||
onClick={() => setShowNotifications(false)}
|
||||
aria-label="关闭通知"
|
||||
className="rounded p-1 text-slate-400 transition-colors hover:bg-slate-100"
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
{notifications.length === 0 ? (
|
||||
<p className="px-4 py-8 text-center text-sm text-slate-400">暂无待处理提醒</p>
|
||||
) : (
|
||||
<>
|
||||
<ul className="max-h-80 divide-y divide-slate-50 overflow-y-auto">
|
||||
{notifications.map((item) => (
|
||||
<li key={item.id}>
|
||||
<button
|
||||
onClick={() => {
|
||||
onSectionChange(item.section);
|
||||
setShowNotifications(false);
|
||||
}}
|
||||
className="w-full px-4 py-3 text-left transition-colors hover:bg-slate-50"
|
||||
>
|
||||
<div className="flex items-start gap-2">
|
||||
<span
|
||||
className={cn(
|
||||
"mt-1.5 h-2 w-2 shrink-0 rounded-full",
|
||||
item.tone === "alert" && "bg-amber-500",
|
||||
item.tone === "info" && "bg-indigo-500",
|
||||
item.tone === "check" && "bg-emerald-500"
|
||||
)}
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-medium text-slate-900">{item.title}</p>
|
||||
<p className="mt-0.5 text-xs leading-snug text-slate-500">{item.body}</p>
|
||||
<p className="mt-1 text-xs text-slate-400">{item.time}</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="border-t border-slate-100 px-4 py-2.5">
|
||||
<span className="text-xs text-slate-400">点击提醒可跳转到对应功能区</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 border-l border-slate-200 pl-4">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-indigo-50 text-sm font-medium text-indigo-600">
|
||||
{(user.real_name ?? user.username ?? "员").slice(0, 1)}
|
||||
</div>
|
||||
<div className="hidden sm:block">
|
||||
<p className="text-sm font-medium leading-tight text-slate-700">
|
||||
{user.real_name ?? user.username}
|
||||
</p>
|
||||
<p className="text-xs leading-tight text-slate-400">{roleLabel}</p>
|
||||
</div>
|
||||
<ChevronDown className="h-4 w-4 text-slate-400" />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="mx-auto max-w-[1400px] p-6">
|
||||
{!accessible && (
|
||||
<div className="mb-6">
|
||||
<Badge tone="danger">当前账号不是员工账号,部分功能不可用</Badge>
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user