Build OrbitFlow frontend admin template

This commit is contained in:
Codex
2026-08-07 13:53:40 +08:00
commit 8322800b57
19 changed files with 11340 additions and 0 deletions

252
app/page.tsx Normal file
View File

@@ -0,0 +1,252 @@
"use client";
import { useEffect, useMemo, useState } from "react";
import mockData from "./data/mock-data.json";
type View = "overview" | "components" | "content" | "board" | "schedule";
type Toast = { id: number; message: string };
type BoardColumn = (typeof mockData.board)[number];
const navGroups = [
{
label: "工作台",
items: [
{ id: "overview" as View, icon: "⌂", label: "概览" },
{ id: "components" as View, icon: "◈", label: "组件样式", badge: "18" },
],
},
{
label: "业务管理",
items: [
{ id: "content" as View, icon: "▤", label: "内容管理", children: ["图文列表", "卡片内容", "分类标签"] },
{ id: "board" as View, icon: "▦", label: "流程泳道", badge: "9" },
{ id: "schedule" as View, icon: "◷", label: "定时任务" },
],
},
{
label: "系统设置",
items: [
{ id: "components" as View, icon: "♙", label: "成员与权限" },
{ id: "components" as View, icon: "⚙", label: "基础设置" },
],
},
];
const pageMeta: Record<View, { title: string; subtitle: string }> = {
overview: { title: "下午好,林知夏", subtitle: "这里是团队今天的工作概览,祝你高效完成每一项计划。" },
components: { title: "组件样式", subtitle: "可直接复用的标签、按钮、菜单与交互状态。" },
content: { title: "内容管理", subtitle: "通过图片文字列表与卡片视图管理业务内容。" },
board: { title: "流程泳道", subtitle: "拖动任务卡片,快速同步团队当前进度。" },
schedule: { title: "定时任务", subtitle: "纯前端模拟任务配置、状态切换与执行记录。" },
};
export default function Home() {
const [activeView, setActiveView] = useState<View>("overview");
const [expandedMenus, setExpandedMenus] = useState<string[]>(["内容管理"]);
const [sidebarOpen, setSidebarOpen] = useState(false);
const [search, setSearch] = useState("");
const [toasts, setToasts] = useState<Toast[]>([]);
const [board, setBoard] = useState<BoardColumn[]>(mockData.board);
const [dragged, setDragged] = useState<{ cardId: string; columnId: string } | null>(null);
const [schedules, setSchedules] = useState(mockData.schedules);
const [scheduleModal, setScheduleModal] = useState(false);
const [profileOpen, setProfileOpen] = useState(false);
useEffect(() => {
const saved = window.localStorage.getItem("orbit-schedules");
if (saved) {
try { setSchedules(JSON.parse(saved)); }
catch { window.localStorage.removeItem("orbit-schedules"); }
}
}, []);
useEffect(() => {
window.localStorage.setItem("orbit-schedules", JSON.stringify(schedules));
}, [schedules]);
const notify = (message: string) => {
const id = Date.now();
setToasts((current) => [...current, { id, message }]);
window.setTimeout(() => setToasts((current) => current.filter((item) => item.id !== id)), 2600);
};
const navigate = (view: View) => {
setActiveView(view);
setSidebarOpen(false);
setSearch("");
};
const filteredProjects = useMemo(
() => mockData.projects.filter((project) => `${project.title}${project.description}${project.tag}`.toLowerCase().includes(search.toLowerCase())),
[search],
);
const moveCard = (targetColumnId: string) => {
if (!dragged || dragged.columnId === targetColumnId) return;
let movingCard: BoardColumn["cards"][number] | undefined;
const nextBoard = board.map((column) => {
if (column.id !== dragged.columnId) return column;
movingCard = column.cards.find((card) => card.id === dragged.cardId);
return { ...column, cards: column.cards.filter((card) => card.id !== dragged.cardId) };
});
if (!movingCard) return;
setBoard(nextBoard.map((column) => column.id === targetColumnId ? { ...column, cards: [...column.cards, movingCard!] } : column));
setDragged(null);
notify("任务状态已更新");
};
const toggleSchedule = (id: number) => {
setSchedules((current) => current.map((schedule) => schedule.id === id ? { ...schedule, enabled: !schedule.enabled } : schedule));
notify("任务状态已保存到本地");
};
return (
<div className="app-shell">
<aside className={`sidebar ${sidebarOpen ? "is-open" : ""}`}>
<button className="brand" onClick={() => navigate("overview")} aria-label="返回概览">
<span className="brand-mark"><i /><i /><i /></span>
<span>Orbit<span>Flow</span></span>
</button>
<nav className="main-nav" aria-label="主导航">
{navGroups.map((group) => (
<div className="nav-group" key={group.label}>
<p>{group.label}</p>
{group.items.map((item) => {
const expanded = expandedMenus.includes(item.label);
return (
<div key={`${group.label}-${item.label}`}>
<button className={`nav-item ${activeView === item.id ? "active" : ""}`} onClick={() => {
navigate(item.id);
if (item.children) setExpandedMenus((current) => current.includes(item.label) ? current.filter((label) => label !== item.label) : [...current, item.label]);
}}>
<span className="nav-icon">{item.icon}</span><span>{item.label}</span>
{item.badge && <span className="nav-badge">{item.badge}</span>}
{item.children && <span className={`chevron ${expanded ? "expanded" : ""}`}></span>}
</button>
{item.children && expanded && (
<div className="subnav">
{item.children.map((child, index) => <button className={index === 0 && activeView === "content" ? "active" : ""} key={child} onClick={() => navigate("content")}>{child}</button>)}
</div>
)}
</div>
);
})}
</div>
))}
</nav>
<div className="sidebar-help"><span>?</span><div><strong></strong><small>使</small></div><button onClick={() => notify("帮助中心即将开放")}></button></div>
</aside>
{sidebarOpen && <button className="sidebar-backdrop" aria-label="关闭菜单" onClick={() => setSidebarOpen(false)} />}
<main className="main-area">
<header className="topbar">
<button className="mobile-menu" aria-label="打开菜单" onClick={() => setSidebarOpen(true)}></button>
<label className="global-search"><span></span><input value={search} onChange={(event) => setSearch(event.target.value)} placeholder="搜索项目、任务或成员..." /><kbd> K</kbd></label>
<div className="top-actions">
<button className="icon-button" aria-label="帮助" onClick={() => notify("你可以从左侧菜单浏览全部模板")}>?</button>
<button className="icon-button notification" aria-label="通知" onClick={() => notify("你有 3 条未读通知")}><i /></button>
<span className="divider" />
<div className="profile-wrap">
<button className="profile" onClick={() => setProfileOpen(!profileOpen)}><span className="avatar"></span><span><strong></strong><small></small></span><b></b></button>
{profileOpen && <div className="profile-menu"><button onClick={() => notify("个人资料功能为模板占位")}></button><button onClick={() => notify("偏好设置功能为模板占位")}></button><button onClick={() => setProfileOpen(false)}></button></div>}
</div>
</div>
</header>
<section className="page-content">
<div className="page-heading">
<div><p className="eyebrow">{activeView === "overview" ? "2026 年 8 月 7 日 · 星期五" : "ORBITFLOW TEMPLATE"}</p><h1>{pageMeta[activeView].title}</h1><p>{pageMeta[activeView].subtitle}</p></div>
<div className="heading-actions"><button className="button secondary" onClick={() => notify("示例数据已刷新")}> </button><button className="button primary" onClick={() => activeView === "schedule" ? setScheduleModal(true) : notify("已创建一条示例记录")}> {activeView === "schedule" ? "新建任务" : "快速创建"}</button></div>
</div>
{activeView === "overview" && <Overview onNavigate={navigate} notify={notify} />}
{activeView === "components" && <ComponentsShowcase notify={notify} />}
{activeView === "content" && <ContentView projects={filteredProjects} notify={notify} />}
{activeView === "board" && <BoardView board={board} dragged={dragged} setDragged={setDragged} moveCard={moveCard} notify={notify} />}
{activeView === "schedule" && <ScheduleView schedules={schedules} toggleSchedule={toggleSchedule} notify={notify} onCreate={() => setScheduleModal(true)} />}
</section>
</main>
{scheduleModal && <ScheduleModal onClose={() => setScheduleModal(false)} onSave={(name, frequency) => {
setSchedules((current) => [...current, { id: Math.max(...current.map((item) => item.id), 0) + 1, name, type: "自定义任务", cron: frequency === "每天" ? "0 9 * * *" : "0 9 * * 1", frequency: frequency === "每天" ? "每天 09:00" : "每周一 09:00", lastRun: "尚未执行", nextRun: frequency === "每天" ? "明天 09:00" : "下周一 09:00", enabled: true, success: 100 }]);
setScheduleModal(false); notify("新任务已保存到本地");
}} />}
<div className="toast-stack" aria-live="polite">{toasts.map((toast) => <div className="toast" key={toast.id}><span></span>{toast.message}</div>)}</div>
</div>
);
}
function Overview({ onNavigate, notify }: { onNavigate: (view: View) => void; notify: (message: string) => void }) {
return (
<>
<div className="stats-grid">
{mockData.stats.map((stat, index) => <article className="stat-card" key={stat.label}><div className={`stat-icon ${stat.tone}`}>{["▣", "✓", "♙", "↗"][index]}</div><div><p>{stat.label}</p><strong>{stat.value}</strong></div><span className={index === 1 ? "muted" : "positive"}>{stat.change}</span></article>)}
</div>
<div className="dashboard-grid">
<section className="panel project-panel">
<PanelHeader title="重点项目" subtitle="本周需要关注的项目进展" action="查看全部" onAction={() => onNavigate("content")} />
<div className="project-list compact">
{mockData.projects.map((project) => <article className="project-row" key={project.id}><img src={project.image} alt="" /><div className="project-info"><div className="project-title-line"><strong>{project.title}</strong><span className={`tag status-${project.status}`}>{project.status}</span></div><p>{project.description}</p><div className="progress-line"><span><i style={{ width: `${project.progress}%` }} /></span><b>{project.progress}%</b></div></div><div className="project-owner"><span className="mini-avatar">{project.owner.slice(-1)}</span><small>{project.owner}</small><b>{project.date}</b></div></article>)}
</div>
</section>
<section className="panel todo-panel">
<PanelHeader title="今日待办" subtitle="已完成 6 / 9" action="管理任务" onAction={() => onNavigate("board")} />
<div className="completion-bar"><i /></div>
<div className="todo-list">
{[["评审新版工作台原型", "10:00", true], ["与研发确认排期", "11:30", true], ["整理组件规范文档", "14:00", false], ["增长实验周会", "16:30", false]].map(([title, time, done]) => <label key={String(title)} className={done ? "done" : ""}><input type="checkbox" defaultChecked={Boolean(done)} onChange={() => notify("待办状态已更新")} /><span>{String(title)}</span><time>{String(time)}</time></label>)}
</div>
<button className="text-button add-todo" onClick={() => notify("已添加一条空白待办")}> </button>
</section>
</div>
<section className="panel activity-panel">
<PanelHeader title="团队动态" subtitle="项目中的最新协作记录" action="全部动态" onAction={() => notify("暂无更多动态")} />
<div className="activity-list">
{[["周", "周屿", "完成了任务", "后台筛选器交互优化", "12 分钟前", "green"], ["许", "许言", "上传了文件", "八月增长计划-v3.pdf", "35 分钟前", "blue"], ["陈", "陈序", "评论了", "确认新用户引导文案", "1 小时前", "orange"], ["林", "林知夏", "创建了项目", "新版工作台视觉升级", "2 小时前", "purple"]].map(([avatar, name, verb, target, time, tone]) => <div className="activity-item" key={String(target)}><span className={`activity-avatar ${tone}`}>{avatar}</span><p><strong>{name}</strong> {verb} <b>{target}</b><small>{time}</small></p><button aria-label="更多" onClick={() => notify("更多操作")}></button></div>)}
</div>
</section>
</>
);
}
function ComponentsShowcase({ notify }: { notify: (message: string) => void }) {
const [tab, setTab] = useState("基础组件");
return (
<div className="component-layout">
<div className="component-tabs">{["基础组件", "状态样式", "菜单示例"].map((item) => <button className={tab === item ? "active" : ""} key={item} onClick={() => setTab(item)}>{item}</button>)}</div>
{tab === "基础组件" && <>
<section className="panel specimen"><PanelHeader title="按钮 Buttons" subtitle="用于操作、提交和页面导航" /><div className="specimen-row"><button className="button primary" onClick={() => notify("主要按钮")}></button><button className="button secondary" onClick={() => notify("次要按钮")}></button><button className="button soft" onClick={() => notify("轻量按钮")}></button><button className="button danger" onClick={() => notify("危险操作示例")}></button><button className="button icon-combo" onClick={() => notify("下载开始")}> </button><button className="button secondary" disabled></button></div></section>
<section className="panel specimen"><PanelHeader title="标签 Tags" subtitle="用于分类、筛选与状态提示" /><div className="specimen-row tags-demo"><span className="tag blue"></span><span className="tag green"></span><span className="tag orange"></span><span className="tag purple"></span><span className="tag red"></span><span className="tag neutral"></span><span className="tag outlined"> ×</span></div></section>
<section className="panel specimen"><PanelHeader title="表单控件 Forms" subtitle="统一输入、选择与开关样式" /><div className="form-showcase"><label><span></span><input defaultValue="新版工作台升级" /></label><label><span></span><select defaultValue="design"><option value="design"></option><option></option></select></label><label><span></span><div className="input-with-icon"><i></i><input placeholder="搜索关键词" /></div></label><label className="switch-field"><span><small></small></span><input type="checkbox" defaultChecked /></label></div></section>
</>}
{tab === "状态样式" && <section className="panel specimen state-gallery"><PanelHeader title="反馈与状态" subtitle="覆盖成功、提示、警告和错误场景" /><div className="alert success"><b></b><span><strong></strong></span><button>×</button></div><div className="alert info"><b>i</b><span><strong></strong> JSON </span><button>×</button></div><div className="alert warning"><b>!</b><span><strong></strong></span><button>×</button></div><div className="alert error"><b>×</b><span><strong></strong></span><button>×</button></div></section>}
{tab === "菜单示例" && <section className="panel specimen menu-gallery"><PanelHeader title="菜单 Menu" subtitle="展示导航、操作菜单和分组层级" /><div className="menu-examples"><div className="demo-menu"><p></p><button> <kbd> V</kbd></button><button> <kbd> E</kbd></button><hr /><button> </button><button className="danger-text"> </button></div><div className="demo-menu dark"><p></p><button className="active"> </button><button> <span>9</span></button><button> </button><small>访</small><button> </button></div></div></section>}
</div>
);
}
function ContentView({ projects, notify }: { projects: typeof mockData.projects; notify: (message: string) => void }) {
const [display, setDisplay] = useState<"list" | "cards">("list");
return (
<section className="panel content-panel">
<div className="content-toolbar"><div className="filter-chips"><button className="active"> <b>24</b></button><button> <b>8</b></button><button> <b>16</b></button></div><div className="view-switch"><button className={display === "list" ? "active" : ""} onClick={() => setDisplay("list")}></button><button className={display === "cards" ? "active" : ""} onClick={() => setDisplay("cards")}></button></div></div>
{projects.length === 0 ? <div className="empty-state"><span></span><h3></h3><p></p></div> : display === "list" ? <div className="image-list">{projects.map((project) => <article key={project.id}><img src={project.image} alt={`${project.title}项目配图`} /><div><div className="content-title"><span className="tag blue">{project.tag}</span><span className={`tag status-${project.status}`}>{project.status}</span></div><h3>{project.title}</h3><p>{project.description}</p><small> {project.owner} · {project.date}</small></div><div className="content-progress"><strong>{project.progress}%</strong><span><i style={{ width: `${project.progress}%` }} /></span><button onClick={() => notify(`打开:${project.title}`)}> </button></div></article>)}</div> : <div className="content-cards">{projects.map((project) => <article key={project.id}><div className="card-image"><img src={project.image} alt="" /><span className="tag white">{project.tag}</span></div><div className="card-body"><span className={`tag status-${project.status}`}>{project.status}</span><h3>{project.title}</h3><p>{project.description}</p><div><span className="mini-avatar">{project.owner.slice(-1)}</span><small>{project.owner}</small><b>{project.progress}%</b></div></div></article>)}</div>}
</section>
);
}
function BoardView({ board, dragged, setDragged, moveCard, notify }: { board: BoardColumn[]; dragged: { cardId: string; columnId: string } | null; setDragged: (value: { cardId: string; columnId: string } | null) => void; moveCard: (columnId: string) => void; notify: (message: string) => void }) {
return <><div className="board-toolbar"><div className="avatar-stack"><span></span><span></span><span></span><span></span><b>+8</b></div><div><button className="button secondary"> </button><button className="button secondary"> </button></div></div><div className="kanban-board">{board.map((column) => <section className={`kanban-column ${dragged ? "dragging" : ""}`} key={column.id} onDragOver={(event) => event.preventDefault()} onDrop={() => moveCard(column.id)}><header><div><i style={{ background: column.color }} /><strong>{column.title}</strong><span>{column.cards.length}</span></div><button onClick={() => notify(`在“${column.title}”中添加任务`)}></button></header><div className="kanban-cards">{column.cards.map((card) => <article draggable key={card.id} onDragStart={() => setDragged({ cardId: card.id, columnId: column.id })} onDragEnd={() => setDragged(null)}><div className="kanban-card-top"><span>{card.id}</span><button onClick={() => notify("更多任务操作")}></button></div><h3>{card.title}</h3><div className="kanban-tags"><span className="tag neutral">{card.tag}</span>{card.priority === "紧急" && <span className="tag red"></span>}</div><footer><span className="mini-avatar">{card.owner.slice(-1)}</span><small>{card.owner}</small><time className={card.due === "今天" ? "today" : ""}> {card.due}</time></footer></article>)}<button className="add-card" onClick={() => notify("添加卡片为模板示例")}> </button></div></section>)}</div></>;
}
function ScheduleView({ schedules, toggleSchedule, notify, onCreate }: { schedules: typeof mockData.schedules; toggleSchedule: (id: number) => void; notify: (message: string) => void; onCreate: () => void }) {
const activeCount = schedules.filter((item) => item.enabled).length;
return <><div className="schedule-summary"><div><span className="summary-icon blue"></span><p><strong>{schedules.length}</strong></p></div><div><span className="summary-icon green"></span><p><strong>{activeCount}</strong></p></div><div><span className="summary-icon purple"></span><p><strong>128</strong></p></div><div><span className="summary-icon orange"></span><p><strong>98.5%</strong></p></div></div><section className="panel schedule-panel"><div className="schedule-toolbar"><div><button className="active"></button><button></button><button></button></div><button className="button primary" onClick={onCreate}> </button></div><div className="table-wrap"><table><thead><tr><th></th><th></th><th></th><th></th><th></th><th></th><th /></tr></thead><tbody>{schedules.map((schedule) => <tr key={schedule.id}><td><span className="task-type-icon">{schedule.type === "消息通知" ? "✧" : schedule.type === "文件归档" ? "▤" : "↻"}</span><div><strong>{schedule.name}</strong><small>{schedule.type}</small></div></td><td><strong>{schedule.frequency}</strong><code>{schedule.cron}</code></td><td>{schedule.lastRun}</td><td>{schedule.nextRun}</td><td><span className="success-rate"><i style={{ width: `${schedule.success}%` }} /></span>{schedule.success}%</td><td><button className={`toggle ${schedule.enabled ? "on" : ""}`} aria-label={`${schedule.enabled ? "停用" : "启用"}${schedule.name}`} onClick={() => toggleSchedule(schedule.id)}><i /></button><span className={schedule.enabled ? "running" : "disabled"}>{schedule.enabled ? "运行中" : "已停用"}</span></td><td><button className="row-action" onClick={() => notify(`编辑:${schedule.name}`)}></button></td></tr>)}</tbody></table></div></section></>;
}
function ScheduleModal({ onClose, onSave }: { onClose: () => void; onSave: (name: string, frequency: string) => void }) {
const [name, setName] = useState(""); const [frequency, setFrequency] = useState("每天");
return <div className="modal-backdrop" onMouseDown={onClose}><form className="modal" onMouseDown={(event) => event.stopPropagation()} onSubmit={(event) => { event.preventDefault(); if (name.trim()) onSave(name.trim(), frequency); }}><header><div><h2></h2><p></p></div><button type="button" onClick={onClose}>×</button></header><label><span></span><input autoFocus value={name} onChange={(event) => setName(event.target.value)} placeholder="例如:每日数据汇总" required /></label><label><span></span><select><option></option><option></option><option></option><option></option></select></label><fieldset><legend></legend><label><input type="radio" name="frequency" checked={frequency === "每天"} onChange={() => setFrequency("每天")} /></label><label><input type="radio" name="frequency" checked={frequency === "每周"} onChange={() => setFrequency("每周")} /></label></fieldset><div className="time-row"><label><span></span><input type="time" defaultValue="09:00" /></label><label><span></span><select><option>Asia/Shanghai</option></select></label></div><footer><button type="button" className="button secondary" onClick={onClose}></button><button className="button primary" type="submit"></button></footer></form></div>;
}
function PanelHeader({ title, subtitle, action, onAction }: { title: string; subtitle: string; action?: string; onAction?: () => void }) {
return <div className="panel-header"><div><h2>{title}</h2><p>{subtitle}</p></div>{action && <button onClick={onAction}>{action} </button>}</div>;
}