feat: enhance admin UI with responsive styles and new components

- Updated styles in src/styles.css for better responsiveness and layout adjustments.
- Added new CSS classes for edit drawer and item rail components.
- Introduced animations for drawer transitions.
- Created new types in src/types/admin.ts for better type safety in admin features.
- Modified vite.config.ts to change server port and enable strict port settings for development.
This commit is contained in:
duanshuwen
2026-07-01 16:55:23 +08:00
parent b9a57fe03c
commit 462498660e
20 changed files with 2840 additions and 1554 deletions

View File

@@ -0,0 +1,135 @@
import { useEffect, useMemo, useState } from "react";
import { Blocks, Home, LogOut, MapPin, Package, UserRoundCheck } from "lucide-react";
import { clearToken, getDestinations } from "@/api";
import type { Destination } from "@/api";
import { ToastStack } from "@/components/admin/ToastStack";
import { Button } from "@/components/ui/button";
import { LeadsPage } from "@/pages/leads/LeadsPage";
import { ProductsPage } from "@/pages/products/ProductsPage";
import { StructurePage } from "@/pages/structure/StructurePage";
import type { Tab, Toast } from "@/types/admin";
const sidebarGroups: { id: string; label: string; items: { id: Tab; label: string; icon: typeof Home; description: string }[] }[] = [
{
id: "workspace",
label: "工作台",
items: [{ id: "structure", label: "维护地图", icon: Blocks, description: "前台结构总览" }],
},
{
id: "frontend",
label: "前台页面",
items: [
{ id: "home", label: "小程序首页", icon: Home, description: "轮播、主题、CTA" },
{ id: "destinations", label: "目的地页", icon: MapPin, description: "字典、搜索、商品" },
{ id: "products", label: "商品维护", icon: Package, description: "列表、卡片、详情" },
],
},
{
id: "operations",
label: "业务处理",
items: [{ id: "leads", label: "需求线索", icon: UserRoundCheck, description: "表单和跟进" }],
},
];
const tabs = sidebarGroups.flatMap((group) => group.items);
export function AdminShell({ onLogout }: { onLogout: () => void }) {
const [activeTab, setActiveTab] = useState<Tab>("structure");
const [destinations, setDestinations] = useState<Destination[]>([]);
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
const [toasts, setToasts] = useState<Toast[]>([]);
useEffect(() => {
getDestinations()
.then((result) => setDestinations(result.items))
.catch(() => setDestinations([]));
}, [activeTab]);
useEffect(() => {
setHasUnsavedChanges(false);
}, [activeTab]);
const activeMeta = useMemo(() => tabs.find((tab) => tab.id === activeTab), [activeTab]);
const activeLabel = activeMeta?.label ?? "";
const activeDescription = activeMeta?.description ?? "";
const notify = (toast: Omit<Toast, "id">) => {
const id = `toast-${Date.now()}-${Math.random().toString(16).slice(2)}`;
setToasts((items) => [...items, { ...toast, id }]);
window.setTimeout(() => {
setToasts((items) => items.filter((item) => item.id !== id));
}, 4200);
};
const logout = () => {
clearToken();
onLogout();
};
return (
<main className="admin-shell">
<aside className="sidebar">
<div className="brand-block">
<b></b>
<span></span>
</div>
<nav>
{sidebarGroups.map((group) => (
<section className="sidebar-group" key={group.id} aria-label={group.label}>
<span>{group.label}</span>
{group.items.map((tab) => {
const Icon = tab.icon;
return (
<Button
variant="ghost"
aria-label={tab.label}
className={activeTab === tab.id ? "active" : ""}
key={tab.id}
onClick={() => setActiveTab(tab.id)}
title={tab.label}
>
<Icon size={18} />
<span>
<b>{tab.label}</b>
<small>{tab.description}</small>
</span>
</Button>
);
})}
</section>
))}
</nav>
<Button variant="ghost" aria-label="退出登录" className="logout-button" onClick={logout} title="退出登录">
<LogOut size={17} />
退
</Button>
</aside>
<section className="main-panel">
<header className="topbar">
<div>
<span>{activeLabel}</span>
<small>{activeDescription} · · </small>
</div>
<div className="topbar-actions">
<span className={hasUnsavedChanges ? "admin-dirty-chip is-dirty" : "admin-dirty-chip"}>
{hasUnsavedChanges ? "有未提交修改" : "内容已同步"}
</span>
</div>
</header>
{activeTab === "structure" ? (
<StructurePage onJump={setActiveTab} onDirtyChange={setHasUnsavedChanges} notify={notify} />
) : activeTab === "home" ? (
<StructurePage key="home-workbench" fixedPage="home" onJump={setActiveTab} onDirtyChange={setHasUnsavedChanges} notify={notify} />
) : activeTab === "destinations" ? (
<StructurePage key="destination-workbench" fixedPage="destination" onJump={setActiveTab} onDirtyChange={setHasUnsavedChanges} notify={notify} />
) : activeTab === "products" ? (
<ProductsPage destinations={destinations} onDirtyChange={setHasUnsavedChanges} notify={notify} />
) : (
<LeadsPage notify={notify} />
)}
</section>
<ToastStack toasts={toasts} onClose={(id) => setToasts((items) => items.filter((item) => item.id !== id))} />
</main>
);
}