R67 A1-A6:官网风格前端 UI 刷新
这个提交包含在:
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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: {
|
||||
|
||||
在新工单中引用
屏蔽一个用户