R67 A1-A6:官网风格前端 UI 刷新

这个提交包含在:
2026-04-26 14:07:11 +08:00
父节点 a15bbcbadb
当前提交 a08db8590a
共修改 20 个文件,包含 390 行新增和 185 行删除
@@ -10,8 +10,12 @@ type EmptyStateCardProps = {
export function EmptyStateCard({ title, description, footer, icon }: EmptyStateCardProps) {
return (
<section className="card-std p-8 text-center">
{icon ? <div className="mx-auto mb-4 flex size-12 items-center justify-center rounded-2xl bg-zinc-100 text-zinc-500">{icon}</div> : null}
<h3 className="text-base font-semibold text-zinc-900">{title}</h3>
{icon ? (
<div className="mx-auto mb-4 flex size-12 items-center justify-center rounded-2xl bg-primary/10 text-primary">
{icon}
</div>
) : null}
<h3 className="text-base font-semibold text-[var(--color-text-primary)]">{title}</h3>
<p className="card-desc mx-auto max-w-xl">{description}</p>
{footer ? <div className="mt-5 flex justify-center">{footer}</div> : null}
</section>
+5 -3
查看文件
@@ -15,9 +15,11 @@ export function PageHeader({ title, subtitle, actions, eyebrow, meta, className
return (
<header className={cn("flex flex-wrap items-start justify-between gap-4", className)}>
<div className="min-w-0 max-w-3xl">
{eyebrow ? <div className="text-xs font-semibold uppercase tracking-[0.18em] text-zinc-500">{eyebrow}</div> : null}
<h2 className="mt-1 text-[2rem] font-semibold tracking-tight text-zinc-900">{title}</h2>
{subtitle ? <p className="mt-2 text-sm leading-6 text-zinc-600">{subtitle}</p> : null}
{eyebrow ? (
<div className="text-xs font-bold uppercase tracking-[0.2em] text-primary">{eyebrow}</div>
) : null}
<h2 className="mt-1 text-[2rem] font-semibold tracking-tight text-[var(--color-text-primary)]">{title}</h2>
{subtitle ? <p className="mt-2 text-sm leading-6 text-[var(--color-text-secondary)]">{subtitle}</p> : null}
{meta ? <div className="mt-3 flex flex-wrap gap-2">{meta}</div> : null}
</div>
{actions ? <div className="flex shrink-0 flex-wrap gap-2">{actions}</div> : null}
+11 -11
查看文件
@@ -75,10 +75,10 @@ function renderNavItem(item: ResolvedNavItem, onNavigate?: () => void) {
onClick={() => onNavigate?.()}
className={({ isActive }) =>
cn(
"group flex items-center gap-3 rounded-2xl px-3 py-2.5 text-sm transition-colors",
"group flex items-center gap-3 rounded-2xl px-3 py-2.5 text-sm transition duration-200",
isActive
? "bg-zinc-900 text-white shadow-sm"
: "text-zinc-600 hover:bg-zinc-50 hover:text-zinc-900",
? "bg-primary text-white shadow-sm"
: "text-[var(--color-text-secondary)] hover:-translate-y-0.5 hover:bg-white/70 hover:text-[var(--color-text-primary)] hover:shadow-sm",
)
}
>
@@ -88,8 +88,8 @@ function renderNavItem(item: ResolvedNavItem, onNavigate?: () => void) {
className={cn(
"flex size-9 items-center justify-center rounded-xl border text-[20px] transition-colors",
isActive
? "border-white/15 bg-white/10 text-white"
: "border-zinc-200 bg-white text-zinc-500 group-hover:border-zinc-300 group-hover:text-zinc-900",
? "border-white/15 bg-white/15 text-white"
: "border-slate-200 bg-white text-[var(--color-text-tertiary)] group-hover:border-slate-300 group-hover:text-primary",
)}
>
<span className="material-symbols-outlined text-[20px]">{item.icon}</span>
@@ -109,8 +109,8 @@ function renderSection(title: string, description: string, items: ResolvedNavIte
return (
<section className="space-y-2">
<div className="px-3">
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-zinc-400">{title}</div>
<div className="mt-1 text-xs text-zinc-500">{description}</div>
<div className="text-[11px] font-bold uppercase tracking-[0.2em] text-primary/70">{title}</div>
<div className="mt-1 text-xs leading-5 text-[var(--color-text-tertiary)]">{description}</div>
</div>
<div className="space-y-1.5">{items.map((item) => renderNavItem(item, onNavigate))}</div>
</section>
@@ -123,13 +123,13 @@ function renderBackupSection(items: ResolvedNavItem[], onNavigate?: () => void)
}
return (
<details className="rounded-2xl border border-zinc-200 bg-zinc-50/80 px-3 py-3">
<summary className="flex cursor-pointer list-none items-center justify-between gap-3 rounded-xl px-2 py-1.5 text-sm font-semibold text-zinc-700">
<details className="rounded-2xl border border-slate-200 bg-slate-50/80 px-3 py-3">
<summary className="flex cursor-pointer list-none items-center justify-between gap-3 rounded-xl px-2 py-1.5 text-sm font-semibold text-[var(--color-text-primary)]">
<div>
<div>备用模块</div>
<div className="mt-0.5 text-xs font-normal text-zinc-500">默认收起,不干扰主路径</div>
<div className="mt-0.5 text-xs font-normal text-[var(--color-text-secondary)]">默认收起,不干扰主路径</div>
</div>
<span className="text-xs font-medium text-zinc-400">按需展开</span>
<span className="text-xs font-medium text-[var(--color-text-tertiary)]">按需展开</span>
</summary>
<div className="mt-3 space-y-1.5 pb-1">{items.map((item) => renderNavItem(item, onNavigate))}</div>
</details>
+9 -6
查看文件
@@ -3,16 +3,19 @@ import { NavLinks } from "./NavLinks";
export function Sidebar() {
return (
<aside className="fixed inset-y-0 z-20 hidden w-72 overflow-hidden border-r border-zinc-200 bg-white/95 backdrop-blur lg:flex lg:flex-col">
<div className="border-b border-zinc-200 px-6 py-6">
<aside className="fixed inset-y-0 z-20 hidden w-72 overflow-hidden border-r border-slate-200 bg-white/92 shadow-[18px_0_50px_rgba(15,23,42,0.08)] backdrop-blur-xl lg:flex lg:flex-col">
<div className="border-b border-slate-200 px-6 py-6">
<AppBrand />
<div className="mt-4 rounded-2xl border border-zinc-200 bg-zinc-50 px-4 py-3 text-sm text-zinc-600">
<div className="font-medium text-zinc-900">统一工作台</div>
<div className="mt-1">先看今天要做什么,再进入具体模块处理。</div>
<div className="mt-5 overflow-hidden rounded-3xl border border-slate-200 bg-[linear-gradient(135deg,rgba(0,57,138,0.08),rgba(19,168,184,0.10))] px-4 py-4 text-sm text-[var(--color-text-secondary)]">
<div className="flex items-center gap-2 font-semibold text-[var(--color-text-primary)]">
<span className="size-2 rounded-full bg-primary" />
今日工作台
</div>
<div className="mt-2 leading-6">先处理待办和团队状态,再进入具体模块。</div>
</div>
</div>
<div className="min-h-0 flex-1 overflow-y-auto px-4 py-5">
<div className="min-h-0 flex-1 overflow-y-auto px-4 py-5 [scrollbar-width:thin]">
<NavLinks />
</div>
</aside>
+17 -17
查看文件
@@ -135,7 +135,7 @@ export function Topbar() {
return (
<>
<header className="sticky top-0 z-10 border-b border-zinc-200/80 bg-white/90 px-4 py-4 backdrop-blur sm:px-8">
<header className="sticky top-0 z-10 border-b border-slate-200/80 bg-white/88 px-4 py-3 shadow-[0_10px_30px_rgba(15,23,42,0.06)] backdrop-blur-xl sm:px-8">
<div className="flex items-center justify-between gap-4">
<div className="flex min-w-0 items-center gap-3">
<Dialog open={mobileNavOpen} onOpenChange={setMobileNavOpen}>
@@ -145,9 +145,9 @@ export function Topbar() {
</Button>
</DialogTrigger>
<DialogContent className="left-0 top-0 h-full w-80 max-w-[84vw] rounded-none rounded-r-3xl border-r border-zinc-200 p-0">
<DialogContent className="left-0 top-0 h-full w-80 max-w-[84vw] rounded-none rounded-r-3xl border-r border-slate-200 p-0">
<div className="flex h-full flex-col bg-white">
<div className="flex items-start justify-between border-b border-zinc-200 px-5 py-5">
<div className="flex items-start justify-between border-b border-slate-200 px-5 py-5">
<AppBrand />
<DialogClose asChild>
<Button size="icon" variant="ghost" aria-label="关闭菜单" type="button">
@@ -164,9 +164,9 @@ export function Topbar() {
</Dialog>
<div className="min-w-0">
<div className="text-xs font-semibold uppercase tracking-[0.18em] text-zinc-400">{pageMeta.group}</div>
<div className="text-xs font-bold uppercase tracking-[0.2em] text-primary/70">{pageMeta.group}</div>
<div className="mt-1 flex flex-wrap items-center gap-2">
<p className="truncate text-lg font-semibold tracking-tight text-zinc-900">{pageMeta.title}</p>
<p className="truncate text-lg font-semibold tracking-tight text-[var(--color-text-primary)]">{pageMeta.title}</p>
{wecomEntry ? (
<span
className="badge-neutral border-primary-200 bg-primary-50 text-primary"
@@ -176,7 +176,7 @@ export function Topbar() {
</span>
) : null}
</div>
<div className="mt-1 text-sm text-zinc-500">{me ? `你好,${me.name}` : "欢迎进入海音达工作台"}</div>
<div className="mt-1 text-sm text-[var(--color-text-secondary)]">{me ? `你好,${me.name}` : "欢迎进入海音达工作台"}</div>
</div>
</div>
@@ -224,11 +224,11 @@ export function Topbar() {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-80 max-w-[calc(100vw-2rem)] p-0">
<div className="flex items-center justify-between border-b border-zinc-200 px-4 py-3">
<DropdownMenuContent align="end" className="w-80 max-w-[calc(100vw-2rem)] overflow-hidden rounded-2xl border-slate-200 bg-white p-0">
<div className="flex items-center justify-between border-b border-slate-200 bg-slate-50/80 px-4 py-3">
<div className="flex items-center gap-2">
<span className="text-sm font-semibold text-zinc-900">通知</span>
<span className="rounded-full bg-zinc-100 px-2 py-0.5 text-xs font-medium text-zinc-600">
<span className="text-sm font-semibold text-[var(--color-text-primary)]">通知</span>
<span className="rounded-full bg-white px-2 py-0.5 text-xs font-medium text-[var(--color-text-secondary)]">
{unreadCount}
</span>
</div>
@@ -244,7 +244,7 @@ export function Topbar() {
<div className="max-h-[400px] overflow-y-auto">
{dropdownQuery.isLoading ? (
<div className="px-4 py-6 text-sm text-zinc-500">通知加载中...</div>
<div className="px-4 py-6 text-sm text-[var(--color-text-secondary)]">通知加载中...</div>
) : null}
{dropdownQuery.isError ? (
@@ -257,7 +257,7 @@ export function Topbar() {
<button
key={item.id}
type="button"
className="flex w-full items-start gap-3 border-b border-zinc-100 px-4 py-3 text-left transition hover:bg-zinc-50 disabled:opacity-60"
className="flex w-full items-start gap-3 border-b border-slate-100 px-4 py-3 text-left transition hover:bg-slate-50 disabled:opacity-60"
disabled={markReadMutation.isPending}
onClick={() => void handleNotificationClick(item)}
>
@@ -265,20 +265,20 @@ export function Topbar() {
{!item.readAt ? <span className="h-2 w-2 rounded-full bg-sky-500" /> : null}
</span>
<span className="min-w-0 flex-1">
<span className="block truncate text-sm font-medium text-zinc-900">{item.title}</span>
<span className="mt-1 block text-xs text-zinc-500">
<span className="block truncate text-sm font-medium text-[var(--color-text-primary)]">{item.title}</span>
<span className="mt-1 block text-xs text-[var(--color-text-secondary)]">
{formatNotificationRelativeTime(item.createdAt)}
</span>
</span>
</button>
))
) : (
<div className="px-4 py-6 text-sm text-zinc-500">暂无通知</div>
<div className="px-4 py-6 text-sm text-[var(--color-text-secondary)]">暂无通知</div>
)
) : null}
</div>
<div className="border-t border-zinc-200 px-2 py-2">
<div className="border-t border-slate-200 px-2 py-2">
<Button asChild variant="ghost" className="w-full justify-between" type="button">
<Link to="/notifications" onClick={() => setNotificationsOpen(false)}>
查看全部通知
@@ -295,7 +295,7 @@ export function Topbar() {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuContent align="end" className="w-48 rounded-2xl border-slate-200 bg-white">
<DropdownMenuItem asChild className="cursor-pointer">
<Link to="/settings/profile">个人设置</Link>
</DropdownMenuItem>
@@ -18,7 +18,7 @@ export function EntityOverviewHero({
{subtitle ? <p className="workbench-hero__subtitle">{subtitle}</p> : null}
{meta ? <div className="mt-3 flex flex-wrap gap-2">{meta}</div> : null}
</div>
{actions ? <div className="shrink-0">{actions}</div> : null}
{actions ? <div className="shrink-0 rounded-2xl border border-white/70 bg-white/55 p-2 shadow-sm backdrop-blur">{actions}</div> : null}
</section>
);
}
@@ -18,5 +18,5 @@ export function ActionToolbar({ className, children }: { className?: string; chi
}
export function PrimaryTable({ className, children }: { className?: string; children: ReactNode }) {
return <section className={cn("card-std p-0 overflow-hidden", className)}>{children}</section>;
return <section className={cn("table-container p-0", className)}>{children}</section>;
}
@@ -18,11 +18,14 @@ export function MetricCard({
ariaLabel?: string;
}) {
const content = (
<>
<div className="relative z-[1]">
<div className="metric-card__title">{title}</div>
<div className="metric-card__value">{value}</div>
<div className="flex items-end justify-between gap-3">
<div className="metric-card__value">{value}</div>
{href ? <span className="material-symbols-outlined text-[18px] text-primary/60">arrow_forward</span> : null}
</div>
{note ? <div className="metric-card__note">{note}</div> : null}
</>
</div>
);
const className = cn(
@@ -18,7 +18,7 @@ export function WorkbenchHero({
<h1 className="workbench-hero__title">{title}</h1>
{subtitle ? <p className="workbench-hero__subtitle">{subtitle}</p> : null}
</div>
{actions ? <div className="shrink-0">{actions}</div> : null}
{actions ? <div className="shrink-0 rounded-2xl border border-white/70 bg-white/55 p-2 shadow-sm backdrop-blur">{actions}</div> : null}
</section>
);
}
+6 -4
查看文件
@@ -1,12 +1,14 @@
export function AppBrand({ compact = false }: { compact?: boolean }) {
return (
<div className="flex items-center gap-3">
<div className={`flex items-center justify-center rounded-2xl bg-[var(--color-accent-primary)] font-bold text-white shadow-sm ${compact ? "size-9 text-base" : "size-11 text-lg"}`}>
H
<div className={`flex items-center justify-center text-primary ${compact ? "size-9" : "size-11"}`}>
<svg aria-hidden="true" className="size-full" fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
<path d="M6 6H42L36 24L42 42H6L12 24L6 6Z" fill="currentColor" />
</svg>
</div>
<div>
<div className="text-sm font-semibold tracking-tight text-zinc-900">海音达工作台</div>
<div className="text-xs text-zinc-500">企业微信入口 + 工作台主平台</div>
<div className="text-sm font-semibold tracking-tight text-[var(--color-text-primary)]">海音达工作台</div>
<div className="text-xs text-[var(--color-text-secondary)]">Hyinda OA · 小团队运营中枢</div>
</div>
</div>
);
+5 -5
查看文件
@@ -4,15 +4,15 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "../../lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-xl text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-300 disabled:pointer-events-none disabled:opacity-50",
"inline-flex items-center justify-center whitespace-nowrap rounded-xl text-sm font-medium transition duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/20 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-[var(--color-accent-primary)] text-white hover:bg-[var(--color-accent-primary-hover)] shadow-sm",
secondary: "border border-zinc-200 bg-white text-zinc-700 hover:bg-zinc-50 shadow-sm",
outline: "border border-zinc-200 bg-transparent text-zinc-900 hover:bg-zinc-50",
default: "bg-[var(--color-accent-primary)] text-white shadow-sm hover:-translate-y-0.5 hover:bg-[var(--color-accent-primary-hover)] hover:shadow-md",
secondary: "border border-slate-200 bg-white/90 text-slate-700 shadow-sm hover:-translate-y-0.5 hover:bg-white hover:shadow-md",
outline: "border border-slate-200 bg-transparent text-slate-900 hover:bg-white/80",
destructive: "bg-red-600 text-white hover:bg-red-700 shadow-sm",
ghost: "bg-transparent text-zinc-700 hover:bg-zinc-100",
ghost: "bg-transparent text-slate-700 hover:bg-slate-100",
link: "bg-transparent text-primary underline-offset-4 hover:underline",
},
size: {