This repository has been archived on 2026-08-27. You can view files and clone it, but cannot push or open issues or pull requests.
Files
RECS-WEB-MODLE-REACT/app/page.tsx
2026-08-07 13:58:25 +08:00

253 lines
25 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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>;
}