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: {
+21 -15
查看文件
@@ -1,25 +1,31 @@
:root {
--color-bg-canvas: #f5f7fa;
--color-bg-canvas: #f5f7f8;
--color-bg-canvas-deep: #e8eef5;
--color-bg-surface: #ffffff;
--color-bg-surface-subtle: #fafafb;
--color-bg-primary-soft: #f2f5f8;
--color-text-primary: #111827;
--color-text-secondary: #667085;
--color-text-tertiary: #98a2b3;
--color-border-default: #e4e7ec;
--color-border-strong: #d0d5dd;
--color-accent-primary: #111827;
--color-accent-primary-hover: #1f2937;
--color-bg-surface-subtle: #f8fafc;
--color-bg-surface-raised: rgba(255, 255, 255, 0.92);
--color-bg-primary-soft: #e6ecf4;
--color-text-primary: #0f172a;
--color-text-secondary: #475569;
--color-text-tertiary: #94a3b8;
--color-border-default: #e2e8f0;
--color-border-strong: #cbd5e1;
--color-accent-primary: #00398a;
--color-accent-primary-hover: #002e6e;
--color-accent-secondary: #13a8b8;
--color-accent-ink: #001f4d;
--color-success: #067647;
--color-warning: #b54708;
--color-danger: #b42318;
--color-info: #155eef;
--color-info: #00398a;
--radius-sm: 0.5rem;
--radius-md: 0.75rem;
--radius-lg: 1rem;
--radius-xl: 1.25rem;
--radius-panel: 1rem;
--shadow-xs: 0 1px 2px rgba(16, 24, 40, 0.04);
--shadow-sm: 0 1px 3px rgba(16, 24, 40, 0.08), 0 1px 2px rgba(16, 24, 40, 0.04);
--shadow-md: 0 8px 24px rgba(16, 24, 40, 0.08);
--radius-2xl: 1.5rem;
--radius-panel: 1.25rem;
--shadow-xs: 0 1px 2px rgba(15, 23, 42, 0.05);
--shadow-sm: 0 8px 24px rgba(15, 23, 42, 0.08), 0 1px 2px rgba(15, 23, 42, 0.05);
--shadow-md: 0 22px 55px rgba(0, 57, 138, 0.14);
--shadow-focus: 0 0 0 4px rgba(0, 57, 138, 0.14);
}
+21 -15
查看文件
@@ -1,32 +1,38 @@
export const semanticTokens = {
colors: {
bgCanvas: "#f5f7fa",
bgCanvas: "#f5f7f8",
bgCanvasDeep: "#e8eef5",
bgSurface: "#ffffff",
bgSurfaceSubtle: "#fafafb",
bgPrimarySoft: "#f2f5f8",
textPrimary: "#111827",
textSecondary: "#667085",
textTertiary: "#98a2b3",
borderDefault: "#e4e7ec",
borderStrong: "#d0d5dd",
accentPrimary: "#111827",
accentPrimaryHover: "#1f2937",
bgSurfaceSubtle: "#f8fafc",
bgSurfaceRaised: "rgba(255, 255, 255, 0.92)",
bgPrimarySoft: "#e6ecf4",
textPrimary: "#0f172a",
textSecondary: "#475569",
textTertiary: "#94a3b8",
borderDefault: "#e2e8f0",
borderStrong: "#cbd5e1",
accentPrimary: "#00398a",
accentPrimaryHover: "#002e6e",
accentSecondary: "#13a8b8",
accentInk: "#001f4d",
success: "#067647",
warning: "#b54708",
danger: "#b42318",
info: "#155eef",
info: "#00398a",
},
radius: {
sm: "0.5rem",
md: "0.75rem",
lg: "1rem",
xl: "1.25rem",
panel: "1rem",
"2xl": "1.5rem",
panel: "1.25rem",
},
shadow: {
xs: "0 1px 2px rgba(16, 24, 40, 0.04)",
sm: "0 1px 3px rgba(16, 24, 40, 0.08), 0 1px 2px rgba(16, 24, 40, 0.04)",
md: "0 8px 24px rgba(16, 24, 40, 0.08)",
xs: "0 1px 2px rgba(15, 23, 42, 0.05)",
sm: "0 8px 24px rgba(15, 23, 42, 0.08), 0 1px 2px rgba(15, 23, 42, 0.05)",
md: "0 22px 55px rgba(0, 57, 138, 0.14)",
focus: "0 0 0 4px rgba(0, 57, 138, 0.14)",
},
spacing: {
pageX: "1.5rem",
+212 -41
查看文件
@@ -14,12 +14,18 @@
@layer base {
html {
color-scheme: light;
background: var(--color-bg-canvas);
}
body {
background: var(--color-bg-canvas);
min-height: 100vh;
background:
radial-gradient(circle at 10% -8%, rgba(0, 57, 138, 0.10), transparent 30rem),
radial-gradient(circle at 92% 0%, rgba(19, 168, 184, 0.12), transparent 32rem),
linear-gradient(180deg, #ffffff 0%, var(--color-bg-canvas) 18rem, #eef2f6 100%);
background-attachment: fixed;
color: var(--color-text-primary);
@apply font-sans antialiased selection:bg-zinc-900 selection:text-white;
@apply font-sans antialiased selection:bg-primary selection:text-white;
}
#root {
@@ -33,142 +39,234 @@
@layer components {
.layout-container {
@apply mx-auto w-full px-4 sm:px-6 lg:px-8;
@apply mx-auto w-full px-0 sm:px-1 lg:px-2;
}
.page-scaffold {
animation: page-reveal 420ms ease-out both;
@apply space-y-6;
}
.workbench-hero {
background: linear-gradient(135deg, rgba(17, 24, 39, 0.03), rgba(255, 255, 255, 0.96));
border: 1px solid var(--color-border-default);
border-radius: var(--radius-xl);
box-shadow: var(--shadow-xs);
@apply flex flex-col gap-4 px-6 py-6 md:flex-row md:items-end md:justify-between;
position: relative;
overflow: hidden;
background:
radial-gradient(circle at 12% 4%, rgba(0, 57, 138, 0.14), transparent 17rem),
radial-gradient(circle at 86% 12%, rgba(19, 168, 184, 0.15), transparent 19rem),
linear-gradient(135deg, rgba(255, 255, 255, 0.98), rgba(230, 236, 244, 0.94));
border: 1px solid rgba(226, 232, 240, 0.92);
border-radius: var(--radius-2xl);
box-shadow: var(--shadow-sm);
@apply flex flex-col gap-5 px-6 py-7 md:flex-row md:items-end md:justify-between md:px-7;
}
.workbench-hero::after {
position: absolute;
inset: auto -5rem -7rem auto;
width: 18rem;
height: 18rem;
content: "";
background:
repeating-radial-gradient(ellipse at center, rgba(0, 57, 138, 0.13) 0 1px, transparent 1px 16px);
border-radius: 9999px;
transform: rotate(-8deg);
}
.workbench-hero > * {
position: relative;
z-index: 1;
}
.workbench-hero__eyebrow {
@apply text-xs font-semibold uppercase tracking-[0.18em] text-zinc-500;
color: var(--color-accent-primary);
@apply text-xs font-bold uppercase tracking-[0.22em];
}
.workbench-hero__title {
@apply mt-2 text-3xl font-semibold tracking-tight text-zinc-900;
color: var(--color-text-primary);
@apply mt-2 text-3xl font-semibold tracking-tight md:text-4xl;
}
.workbench-hero__subtitle {
@apply mt-2 max-w-3xl text-sm leading-6 text-zinc-600;
color: var(--color-text-secondary);
@apply mt-3 max-w-3xl text-sm leading-6 md:text-base md:leading-7;
}
.card-std {
background: var(--color-bg-surface);
border: 1px solid var(--color-border-default);
background: var(--color-bg-surface-raised);
border: 1px solid rgba(226, 232, 240, 0.92);
border-radius: var(--radius-panel);
box-shadow: var(--shadow-sm);
backdrop-filter: blur(18px);
}
.card-title {
@apply text-xs font-semibold uppercase tracking-[0.18em] text-zinc-500;
color: var(--color-text-tertiary);
@apply text-xs font-semibold uppercase tracking-[0.18em];
}
.card-desc {
@apply mt-2 text-sm leading-6 text-zinc-600;
color: var(--color-text-secondary);
@apply mt-2 text-sm leading-6;
}
.card-value {
@apply text-3xl font-semibold tracking-tight text-zinc-900;
color: var(--color-text-primary);
@apply text-3xl font-semibold tracking-tight;
}
.section-panel {
@apply p-6;
position: relative;
overflow: hidden;
@apply p-5 md:p-6;
}
.section-panel::before {
position: absolute;
inset: 0 0 auto 0;
height: 3px;
content: "";
background: linear-gradient(90deg, var(--color-accent-primary), rgba(19, 168, 184, 0.72), transparent);
opacity: 0.7;
}
.section-panel__header {
position: relative;
z-index: 1;
@apply flex flex-wrap items-start justify-between gap-3;
}
.section-panel__title {
@apply text-base font-semibold text-zinc-900;
color: var(--color-text-primary);
@apply text-base font-semibold tracking-tight;
}
.section-panel__desc {
@apply mt-1 text-sm text-zinc-500;
color: var(--color-text-secondary);
@apply mt-1 text-sm leading-6;
}
.section-panel__body {
position: relative;
z-index: 1;
@apply mt-5;
}
.metric-card {
background: var(--color-bg-surface);
border: 1px solid var(--color-border-default);
border-radius: var(--radius-lg);
position: relative;
overflow: hidden;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(248, 250, 252, 0.86));
border: 1px solid rgba(226, 232, 240, 0.96);
border-radius: var(--radius-panel);
box-shadow: var(--shadow-xs);
@apply block px-5 py-5 transition-colors hover:bg-zinc-50;
@apply block px-5 py-5 transition duration-200 hover:-translate-y-0.5 hover:shadow-sm;
}
.metric-card::before {
position: absolute;
inset: 0 auto 0 0;
width: 4px;
content: "";
background: linear-gradient(180deg, var(--color-accent-primary), rgba(19, 168, 184, 0.72));
opacity: 0.82;
}
.metric-card--warning {
@apply border-amber-200 bg-amber-50 hover:bg-amber-100;
background: linear-gradient(180deg, rgba(255, 251, 235, 0.98), rgba(254, 243, 199, 0.78));
@apply border-amber-200;
}
.metric-card--warning::before {
@apply bg-amber-500;
}
.metric-card--danger {
@apply border-red-200 bg-red-50 hover:bg-red-100;
background: linear-gradient(180deg, rgba(254, 242, 242, 0.98), rgba(254, 226, 226, 0.76));
@apply border-red-200;
}
.metric-card--danger::before {
@apply bg-red-500;
}
.metric-card__title {
@apply text-sm font-medium text-zinc-500;
color: var(--color-text-secondary);
@apply text-sm font-medium;
}
.metric-card__value {
@apply mt-2 text-2xl font-semibold tracking-tight text-zinc-900;
color: var(--color-text-primary);
@apply mt-2 text-2xl font-semibold tracking-tight;
}
.metric-card__note {
@apply mt-2 text-sm text-zinc-500;
color: var(--color-text-secondary);
@apply mt-2 text-sm leading-6;
}
.info-chip {
@apply inline-flex items-center rounded-full border border-zinc-200 bg-white px-3 py-1 text-xs font-medium text-zinc-600;
border-color: rgba(203, 213, 225, 0.86);
background: rgba(255, 255, 255, 0.82);
color: var(--color-text-secondary);
@apply inline-flex items-center rounded-full border px-3 py-1 text-xs font-medium;
}
.surface-muted {
@apply rounded-2xl border border-zinc-200 bg-zinc-50;
border-color: rgba(226, 232, 240, 0.92);
background: rgba(248, 250, 252, 0.86);
@apply rounded-2xl border;
}
.input-std {
@apply h-11 w-full rounded-xl border border-zinc-200 bg-white px-3 py-2 text-sm text-zinc-900 shadow-sm outline-none transition-colors placeholder:text-zinc-400 focus:border-zinc-400 focus:ring-2 focus:ring-zinc-200;
border-color: rgba(203, 213, 225, 0.92);
background: rgba(255, 255, 255, 0.96);
color: var(--color-text-primary);
@apply h-11 w-full rounded-xl border px-3 py-2 text-sm shadow-sm outline-none transition placeholder:text-zinc-400 focus:border-primary focus:ring-2 focus:ring-primary/15;
}
.select-std {
@apply h-11 w-full rounded-xl border border-zinc-200 bg-white px-3 py-2 text-sm text-zinc-700 shadow-sm outline-none transition-colors focus:border-zinc-400 focus:ring-2 focus:ring-zinc-200;
border-color: rgba(203, 213, 225, 0.92);
background: rgba(255, 255, 255, 0.96);
color: var(--color-text-primary);
@apply h-11 w-full rounded-xl border px-3 py-2 text-sm shadow-sm outline-none transition focus:border-primary focus:ring-2 focus:ring-primary/15;
}
.select-sm {
@apply h-8 rounded-lg border border-zinc-200 bg-white px-2 py-1 text-xs font-medium text-zinc-600 shadow-sm outline-none transition-colors focus:border-zinc-400 focus:ring-2 focus:ring-zinc-200;
border-color: rgba(203, 213, 225, 0.92);
background: rgba(255, 255, 255, 0.96);
color: var(--color-text-secondary);
@apply h-8 rounded-lg border px-2 py-1 text-xs font-medium shadow-sm outline-none transition focus:border-primary focus:ring-2 focus:ring-primary/15;
}
.table-container {
@apply overflow-hidden rounded-2xl border border-zinc-200 bg-white shadow-sm;
border-color: rgba(226, 232, 240, 0.92);
background: var(--color-bg-surface-raised);
@apply overflow-hidden rounded-2xl border shadow-sm backdrop-blur;
}
.table-header {
@apply border-b border-zinc-100 bg-zinc-50/70;
border-color: rgba(226, 232, 240, 0.82);
background: rgba(248, 250, 252, 0.9);
@apply border-b;
}
.table-th {
@apply px-6 py-4 text-left text-xs font-semibold uppercase tracking-wide text-zinc-500;
color: var(--color-text-tertiary);
@apply px-6 py-4 text-left text-xs font-semibold uppercase tracking-wide;
}
.table-row {
@apply transition-colors hover:bg-zinc-50/80;
@apply transition-colors hover:bg-slate-50/90;
}
.table-td {
@apply px-6 py-4 text-sm text-zinc-600;
color: var(--color-text-secondary);
@apply px-6 py-4 text-sm;
}
.table-td-strong {
@apply px-6 py-4 text-sm font-semibold text-zinc-900;
color: var(--color-text-primary);
@apply px-6 py-4 text-sm font-semibold;
}
.page-shell-grid {
@@ -180,7 +278,7 @@
}
.badge-success {
@apply inline-flex items-center rounded-full bg-emerald-50 px-2.5 py-0.5 text-xs font-semibold text-emerald-700 ring-1 ring-inset ring-emerald-600/10;
@apply inline-flex items-center rounded-full bg-emerald-50 px-2.5 py-0.5 text-xs font-semibold text-emerald-700 ring-1 ring-inset ring-emerald-600/15;
}
.badge-warning {
@@ -192,6 +290,79 @@
}
.badge-neutral {
@apply inline-flex items-center rounded-full bg-zinc-100 px-2.5 py-0.5 text-xs font-semibold text-zinc-700 ring-1 ring-inset ring-zinc-600/10;
background: rgba(241, 245, 249, 0.92);
color: var(--color-text-secondary);
@apply inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold ring-1 ring-inset ring-slate-300/55;
}
.portal-list-card {
background: rgba(255, 255, 255, 0.92);
border: 1px solid rgba(226, 232, 240, 0.94);
border-radius: var(--radius-panel);
box-shadow: var(--shadow-xs);
@apply block px-4 py-4 transition duration-200;
}
.portal-list-card:hover {
background: #ffffff;
box-shadow: var(--shadow-sm);
transform: translateY(-1px);
}
.portal-list-card--warning {
background: rgba(255, 251, 235, 0.9);
border-color: rgba(245, 158, 11, 0.36);
}
.portal-action-tile {
min-height: 100%;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(248, 250, 252, 0.86));
border: 1px solid rgba(226, 232, 240, 0.94);
border-radius: var(--radius-panel);
box-shadow: var(--shadow-xs);
@apply px-4 py-4 text-left transition duration-200;
}
.portal-action-tile:hover {
box-shadow: var(--shadow-sm);
transform: translateY(-2px);
}
.portal-action-tile--disabled {
background: rgba(241, 245, 249, 0.86);
color: var(--color-text-tertiary);
cursor: not-allowed;
transform: none;
}
.portal-action-icon {
background: linear-gradient(135deg, rgba(0, 57, 138, 0.12), rgba(19, 168, 184, 0.12));
color: var(--color-accent-primary);
@apply flex size-10 items-center justify-center rounded-2xl;
}
.portal-action-icon--muted {
background: rgba(226, 232, 240, 0.72);
color: var(--color-text-tertiary);
}
.portal-status-card {
background: rgba(255, 255, 255, 0.92);
border: 1px solid rgba(226, 232, 240, 0.94);
border-radius: var(--radius-panel);
@apply px-4 py-4;
}
}
@keyframes page-reveal {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
+3 -2
查看文件
@@ -20,13 +20,14 @@ export function AppShellLayout() {
}, [location.pathname]);
return (
<div className="flex min-h-screen bg-[var(--color-bg-canvas)]">
<div className="relative flex min-h-screen overflow-hidden bg-[var(--color-bg-canvas)]">
<div className="pointer-events-none fixed inset-0 -z-10 bg-[radial-gradient(circle_at_12%_8%,rgba(0,57,138,0.10),transparent_28rem),radial-gradient(circle_at_90%_12%,rgba(19,168,184,0.12),transparent_32rem)]" />
<Sidebar />
<main className="flex min-w-0 flex-1 flex-col lg:ml-72">
<Topbar />
<div className={`flex-1 px-4 py-6 sm:px-8 sm:py-8 ${isMobileRoute ? "pb-24 md:pb-8" : ""}`}>
<div className={`flex-1 px-4 py-5 sm:px-7 sm:py-7 lg:px-8 ${isMobileRoute ? "pb-24 md:pb-8" : ""}`}>
<div className="layout-container max-w-none">
<ErrorBoundary>
<Outlet />
+9 -9
查看文件
@@ -3,27 +3,27 @@ import { AppBrand } from "../components/shell/AppBrand";
export function AuthLayout() {
return (
<div className="min-h-screen bg-[radial-gradient(circle_at_top_left,_rgba(17,24,39,0.08),_transparent_34%),linear-gradient(180deg,_#f8fafc_0%,_#f3f4f6_100%)] px-6 py-10 lg:px-10 lg:py-14">
<div className="min-h-screen bg-[radial-gradient(circle_at_14%_0%,rgba(0,57,138,0.12),transparent_32rem),radial-gradient(circle_at_86%_8%,rgba(19,168,184,0.12),transparent_30rem),linear-gradient(180deg,#ffffff_0%,#f5f7f8_42%,#eef2f6_100%)] px-6 py-10 lg:px-10 lg:py-14">
<div className="mx-auto grid min-h-[calc(100vh-5rem)] max-w-6xl items-center gap-8 lg:grid-cols-[minmax(0,1.05fr),minmax(420px,0.95fr)]">
<section className="hidden rounded-[2rem] border border-zinc-200 bg-white/75 p-10 shadow-sm backdrop-blur lg:block">
<section className="hidden overflow-hidden rounded-[2rem] border border-slate-200 bg-white/82 p-10 shadow-sm backdrop-blur lg:block">
<AppBrand />
<div className="mt-8 space-y-6">
<div>
<div className="text-xs font-semibold uppercase tracking-[0.18em] text-zinc-400">统一入口</div>
<h1 className="mt-2 text-4xl font-semibold tracking-tight text-zinc-900">让员工先找到今天要做什么</h1>
<p className="mt-3 max-w-xl text-base leading-7 text-zinc-600">
<div className="text-xs font-bold uppercase tracking-[0.2em] text-primary">统一入口</div>
<h1 className="mt-2 text-4xl font-semibold tracking-tight text-slate-900">让员工先找到今天要做什么</h1>
<p className="mt-3 max-w-xl text-base leading-7 text-slate-600">
企业微信负责进入和提醒,真正的审批、项目、周报与留痕都在 OA 内完成。
</p>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="surface-muted px-5 py-5">
<div className="text-sm font-semibold text-zinc-900">主路径稳定</div>
<div className="mt-2 text-sm leading-6 text-zinc-600">Portal、团队、请假、报销、资产、周报、待办、公告、项目保持稳定结构。</div>
<div className="text-sm font-semibold text-slate-900">主路径稳定</div>
<div className="mt-2 text-sm leading-6 text-slate-600">Portal、团队、请假、报销、资产、周报、待办、公告、项目保持稳定结构。</div>
</div>
<div className="surface-muted px-5 py-5">
<div className="text-sm font-semibold text-zinc-900">入口可回退</div>
<div className="mt-2 text-sm leading-6 text-zinc-600">企业微信免登失败时仍可使用账号密码,不让员工猜测下一步。</div>
<div className="text-sm font-semibold text-slate-900">入口可回退</div>
<div className="mt-2 text-sm leading-6 text-slate-600">企业微信免登失败时仍可使用账号密码,不让员工猜测下一步。</div>
</div>
</div>
</div>
+21 -21
查看文件
@@ -32,7 +32,7 @@ function noticeClasses(tone: WecomNotice["tone"]) {
if (tone === "warning") {
return "border-amber-200 bg-amber-50 text-amber-700";
}
return "border-zinc-200 bg-zinc-50 text-zinc-700";
return "border-slate-200 bg-slate-50 text-slate-700";
}
export function LoginPage() {
@@ -104,11 +104,11 @@ export function LoginPage() {
function renderPasswordPanel(testId: string, primary: boolean) {
return (
<div className="space-y-4 rounded-[1.5rem] border border-zinc-200 bg-white p-6 shadow-sm" data-testid={testId}>
<div className="space-y-4 rounded-[1.5rem] border border-slate-200 bg-white p-6 shadow-sm" data-testid={testId}>
<div>
<div className="text-xs font-semibold uppercase tracking-[0.18em] text-zinc-400">{primary ? "推荐入口" : "回退入口"}</div>
<h2 className="mt-2 text-xl font-semibold tracking-tight text-zinc-900">使用账号密码登录</h2>
<p className="mt-2 text-sm leading-6 text-zinc-600">
<div className="text-xs font-semibold uppercase tracking-[0.18em] text-slate-400">{primary ? "推荐入口" : "回退入口"}</div>
<h2 className="mt-2 text-xl font-semibold tracking-tight text-slate-900">使用账号密码登录</h2>
<p className="mt-2 text-sm leading-6 text-slate-600">
{primary
? "适合桌面浏览器直接登录;若企业微信入口暂未开放,也可正常使用。"
: "企业微信免登失败、未绑定或超时时,可改用账号密码继续进入。"}
@@ -152,14 +152,14 @@ export function LoginPage() {
}
return (
<div className="card-std overflow-hidden border-zinc-200 p-0 shadow-md">
<div className="border-b border-zinc-200 bg-gradient-to-br from-zinc-50 via-white to-zinc-100 px-6 py-6 sm:px-8 sm:py-7">
<div className="text-xs font-semibold uppercase tracking-[0.18em] text-zinc-400">统一入口</div>
<h1 className="mt-2 text-3xl font-semibold tracking-tight text-zinc-900">登录海音达工作台</h1>
<p className="mt-3 max-w-2xl text-sm leading-6 text-zinc-600">
<div className="card-std overflow-hidden border-slate-200 p-0 shadow-md">
<div className="border-b border-slate-200 bg-[radial-gradient(circle_at_8%_0%,rgba(0,57,138,0.12),transparent_16rem),linear-gradient(135deg,#ffffff,#e6ecf4)] px-6 py-6 sm:px-8 sm:py-7">
<div className="text-xs font-bold uppercase tracking-[0.2em] text-primary">统一入口</div>
<h1 className="mt-2 text-3xl font-semibold tracking-tight text-slate-900">登录海音达工作台</h1>
<p className="mt-3 max-w-2xl text-sm leading-6 text-slate-600">
企业微信负责进入与提醒,真正的审批、项目、周报和团队留痕都在海音达工作台内完成。
</p>
<div className="mt-4 rounded-2xl border border-zinc-200 bg-white/90 px-4 py-3 text-sm text-zinc-600">
<div className="mt-4 rounded-2xl border border-slate-200 bg-white/90 px-4 py-3 text-sm text-slate-600">
{isWecomContext
? "当前在企业微信中打开,推荐直接使用企业微信进入工作台;失败时可随时回退到账号密码登录。"
: "当前为浏览器场景,推荐直接使用账号密码登录;企业微信入口会作为次级入口提供。"}
@@ -176,10 +176,10 @@ export function LoginPage() {
<div className="grid gap-4">
{primaryMode === "wecom" && wecomEntry?.loginUrl ? (
<div className="rounded-[1.5rem] border border-primary-100 bg-primary-50/70 p-6" data-testid="login-wecom-primary-panel">
<div className="rounded-[1.5rem] border border-primary/20 bg-primary/5 p-6" data-testid="login-wecom-primary-panel">
<div className="text-xs font-semibold uppercase tracking-[0.18em] text-primary">推荐入口</div>
<h2 className="mt-2 text-xl font-semibold tracking-tight text-zinc-900">使用企业微信进入工作台</h2>
<p className="mt-2 text-sm leading-6 text-zinc-600">当前是企业微信场景,优先使用免登入口;若未绑定或回调失效,仍可切换为账号密码登录。</p>
<h2 className="mt-2 text-xl font-semibold tracking-tight text-slate-900">使用企业微信进入工作台</h2>
<p className="mt-2 text-sm leading-6 text-slate-600">当前是企业微信场景,优先使用免登入口;若未绑定或回调失效,仍可切换为账号密码登录。</p>
<Button className="mt-5 w-full" data-testid="wecom-entry-primary" type="button" onClick={handleWecomLogin}>
使用企业微信进入工作台
</Button>
@@ -192,10 +192,10 @@ export function LoginPage() {
)}
{((primaryMode !== "wecom" && wecomEntry?.loginUrl) || oidcProvider) ? (
<div className="rounded-[1.5rem] border border-zinc-200 bg-zinc-50/70 p-6">
<div className="text-xs font-semibold uppercase tracking-[0.18em] text-zinc-400">补充入口</div>
<h2 className="mt-2 text-lg font-semibold text-zinc-900">其他进入方式</h2>
<p className="mt-2 text-sm leading-6 text-zinc-600">入口失败时请优先选择可理解、可回退的方式继续进入。</p>
<div className="rounded-[1.5rem] border border-slate-200 bg-slate-50/70 p-6">
<div className="text-xs font-semibold uppercase tracking-[0.18em] text-slate-400">补充入口</div>
<h2 className="mt-2 text-lg font-semibold text-slate-900">其他进入方式</h2>
<p className="mt-2 text-sm leading-6 text-slate-600">入口失败时请优先选择可理解、可回退的方式继续进入。</p>
<div className="mt-4 grid gap-3">
{primaryMode !== "wecom" && wecomEntry?.loginUrl ? (
<Button className="w-full" data-testid="wecom-entry-secondary" type="button" variant="secondary" onClick={handleWecomLogin}>
@@ -220,11 +220,11 @@ export function LoginPage() {
) : null}
</div>
<div className="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">企业微信入口状态:{wecomStatus.label}</div>
<div className="rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-600">
<div className="font-medium text-slate-900">企业微信入口状态:{wecomStatus.label}</div>
<div className="mt-1 leading-6">{wecomStatus.description}</div>
{wecomStatus.reasonLabels.length ? (
<ul className="mt-2 list-disc space-y-1 pl-5 text-xs text-zinc-500">
<ul className="mt-2 list-disc space-y-1 pl-5 text-xs text-slate-500">
{wecomStatus.reasonLabels.map((label) => (
<li key={label}>{label}</li>
))}
+19 -21
查看文件
@@ -361,9 +361,7 @@ function SummaryPanel({ title, items }: { title: string; items: SummaryItem[] })
<Link
key={item.key}
to={item.href}
className={item.tone === "warning"
? "block rounded-2xl border border-amber-200 bg-amber-50 px-4 py-4 hover:bg-amber-100"
: "block rounded-2xl border border-zinc-200 bg-white px-4 py-4 hover:bg-zinc-50"}
className={item.tone === "warning" ? "portal-list-card portal-list-card--warning" : "portal-list-card"}
>
<div className="flex items-center justify-between gap-3">
<div className="font-medium text-zinc-900">{item.label}</div>
@@ -388,7 +386,7 @@ function AnnouncementsPanel({ announcements }: { announcements: PortalHomePayloa
>
<div className="space-y-3">
{safeAnnouncements.length ? safeAnnouncements.slice(0, 4).map((item) => (
<div key={item.id} className="rounded-2xl border border-zinc-200 bg-white px-4 py-4">
<div key={item.id} className="portal-list-card">
<div className="font-medium text-zinc-900">{item.title}</div>
<div className="mt-2 text-sm leading-6 text-zinc-600">{item.summary}</div>
{item.href ? (
@@ -410,7 +408,7 @@ function RecentVisitsPanel() {
<SectionPanel title="继续工作" description="保留最近访问路径,帮助快速回到上一次处理中断的位置。">
<div className="space-y-3">
{recentVisits.length ? recentVisits.map((item) => (
<Link key={item.path} to={item.path} className="block rounded-2xl border border-zinc-200 bg-white px-4 py-4 hover:bg-zinc-50">
<Link key={item.path} to={item.path} className="portal-list-card">
<div className="font-medium text-zinc-900">{item.title}</div>
<div className="mt-1 text-xs text-zinc-500">{item.path}</div>
</Link>
@@ -427,7 +425,7 @@ function QuickLinksPanel({ items }: { items: PortalHomePayload["quickLinks"] })
<SectionPanel title="常用入口" description="只保留高频入口,避免把所有模块堆成超市。">
<div className="grid gap-3 sm:grid-cols-2">
{safeItems.length ? safeItems.map((item) => (
<Link key={item.key} to={item.href} className="rounded-2xl border border-zinc-200 bg-white px-4 py-4 text-sm font-medium text-zinc-700 hover:bg-zinc-50">
<Link key={item.key} to={item.href} className="portal-list-card text-sm font-medium text-slate-700">
{item.title}
</Link>
)) : <EmptyStateCard title="暂无常用入口" description="工作区尚未配置推荐入口。" />}
@@ -474,24 +472,24 @@ function AdoptionSummaryPanel({ adoptionSummary }: { adoptionSummary: NonNullabl
<span className="text-xs uppercase tracking-[0.18em] text-zinc-400">展开 / 收起</span>
</summary>
<div className="mt-5 grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
<div className="rounded-2xl border border-zinc-200 bg-white px-4 py-4 text-sm">
<div className="portal-status-card text-sm">
<div className="text-zinc-500">近 7 天登录覆盖率</div>
<div className="mt-2 text-2xl font-semibold tracking-tight text-zinc-900">{adoptionSummary.login7dRate}%</div>
</div>
<div className="rounded-2xl border border-zinc-200 bg-white px-4 py-4 text-sm">
<div className="portal-status-card text-sm">
<div className="text-zinc-500">周报提交率</div>
<div className="mt-2 text-2xl font-semibold tracking-tight text-zinc-900">{adoptionSummary.weeklySubmissionRate}%</div>
</div>
<div className="rounded-2xl border border-zinc-200 bg-white px-4 py-4 text-sm">
<div className="portal-status-card text-sm">
<div className="text-zinc-500">团队状态覆盖率</div>
<div className="mt-2 text-2xl font-semibold tracking-tight text-zinc-900">{adoptionSummary.presenceCoverageRate}%</div>
</div>
<div className="rounded-2xl border border-zinc-200 bg-white px-4 py-4 text-sm">
<div className="portal-status-card text-sm">
<div className="text-zinc-500">当前公告数</div>
<div className="mt-2 text-2xl font-semibold tracking-tight text-zinc-900">{adoptionSummary.announcementCount}</div>
</div>
</div>
<div className="mt-4 rounded-2xl border border-zinc-200 bg-zinc-50 px-4 py-4 text-sm text-zinc-600">
<div className="mt-4 rounded-2xl border border-slate-200 bg-slate-50 px-4 py-4 text-sm text-slate-600">
<div className="font-medium text-zinc-900">本周观测提示</div>
<ul className="mt-2 list-disc space-y-1 pl-5">
{adoptionSummary.hints.map((hint) => (
@@ -549,7 +547,7 @@ function GettingStartedCard({
<Link
key={item.key}
to={item.href}
className="flex items-center gap-3 rounded-2xl border border-zinc-200 bg-white px-4 py-4 transition-colors hover:bg-zinc-50"
className="portal-list-card flex items-center gap-3"
>
<span className={item.completed ? "text-emerald-600" : "text-zinc-400"}>
<span className="material-symbols-outlined text-[20px]">
@@ -564,12 +562,12 @@ function GettingStartedCard({
</Link>
))}
<div className="rounded-2xl border border-zinc-200 bg-zinc-50 px-4 py-4">
<div className="rounded-2xl border border-slate-200 bg-slate-50 px-4 py-4">
<div className="flex items-center justify-between gap-3 text-sm">
<span className="font-medium text-zinc-900">完成进度</span>
<span className="text-zinc-500">{completedCount}/{items.length}</span>
</div>
<div className="mt-3 h-2 overflow-hidden rounded-full bg-zinc-200">
<div className="mt-3 h-2 overflow-hidden rounded-full bg-slate-200">
<div
className="h-full rounded-full bg-primary transition-all"
style={{ width: `${progressPercent}%` }}
@@ -590,15 +588,15 @@ function QuickActionTile({
disabled = false,
}: QuickActionConfig) {
const baseClassName = disabled
? "rounded-2xl border border-zinc-200 bg-zinc-50 px-4 py-4 text-left text-zinc-400"
: "rounded-2xl border border-zinc-200 bg-white px-4 py-4 text-left shadow-sm transition-colors hover:bg-zinc-50";
? "portal-action-tile portal-action-tile--disabled"
: "portal-action-tile";
const content = (
<>
<div className="flex items-center gap-3">
<div className={disabled
? "flex size-10 items-center justify-center rounded-2xl bg-zinc-100 text-zinc-400"
: "flex size-10 items-center justify-center rounded-2xl bg-primary/10 text-primary"}
? "portal-action-icon portal-action-icon--muted"
: "portal-action-icon"}
>
<span className="material-symbols-outlined text-[20px]">{icon}</span>
</div>
@@ -652,7 +650,7 @@ function TodoAggregatePanel({ items }: { items: TodoAggregateItem[] }) {
<SectionPanel title="今日待办聚合" description="合并审批、TODO 和高风险指标,只保留今天最该处理的事。">
<div className="space-y-3">
{items.length ? items.map((item) => (
<Link key={`${item.title}-${item.href}-${item.sourceLabel}`} to={item.href} className="block rounded-2xl border border-zinc-200 bg-white px-4 py-4 hover:bg-zinc-50">
<Link key={`${item.title}-${item.href}-${item.sourceLabel}`} to={item.href} className="portal-list-card">
<div className="flex items-start gap-3">
<div className={item.severity === "high"
? "mt-0.5 flex size-9 items-center justify-center rounded-2xl bg-red-50 text-red-600"
@@ -688,7 +686,7 @@ function TeamStatusBoardPanel({ items }: { items: PortalTeamStatusItem[] }) {
{safeItems.length ? safeItems.map((item) => {
const badge = resolveTeamStatusBadge(item);
return (
<div key={item.employeeId} className="rounded-2xl border border-zinc-200 bg-white px-4 py-4">
<div key={item.employeeId} className="portal-status-card">
<div className="flex items-center gap-3">
<EmployeeAvatar name={item.name} src={item.avatarUrl} size="sm" />
<div className="min-w-0 flex-1">
@@ -712,7 +710,7 @@ function RecentActivitiesPanel({ items }: { items: PortalRecentActivity[] }) {
<SectionPanel title="近期动态流" description="把最近发生的报销、请假、考勤和资产动作放在同一条时间线上。">
<div className="space-y-3">
{safeItems.length ? safeItems.map((item) => (
<Link key={item.id} to={item.href} className="block rounded-2xl border border-zinc-200 bg-white px-4 py-4 hover:bg-zinc-50">
<Link key={item.id} to={item.href} className="portal-list-card">
<div className="flex items-start gap-3">
<EmployeeAvatar name={item.actorName} size="sm" />
<div className="min-w-0 flex-1">
+9 -7
查看文件
@@ -6,19 +6,20 @@ export default {
extend: {
colors: {
primary: {
DEFAULT: "#18181b", // zinc-900
light: "#27272a", // zinc-800
accent: "#197fe6" // 蓝色用于少量强调
DEFAULT: "#00398a",
light: "#002e6e",
accent: "#13a8b8"
},
secondary: "#71717a", // zinc-500
secondary: "#5e718d",
background: {
light: "#fafafa", // zinc-50
dark: "#111921"
light: "#f5f7f8",
dark: "#0f1723"
},
border: "#e4e4e7" // zinc-200
border: "#e2e8f0"
},
fontFamily: {
sans: [
"Public Sans",
"system-ui",
"-apple-system",
"BlinkMacSystemFont",
@@ -31,6 +32,7 @@ export default {
"sans-serif"
],
display: [
"Public Sans",
"system-ui",
"-apple-system",
"BlinkMacSystemFont",
+7
查看文件
@@ -0,0 +1,7 @@
"ID","Title","Description","Acceptance","Test_Method","Tools","Dev_Status","Review1_Status","Regression_Status","Files","Dependencies","Notes"
"A1","R67 UI 改造合同建档与范围冻结","创建 R67 前端 UI 改造执行合同,冻结第一轮范围:只改前端视觉基础、App Shell、共享组件、Portal 首页与登录页;视觉方向参考公司官网 www.hyinda.com;不改后端 API、权限、workflow、状态机、Prisma 迁移和导航可见性规则。","CSV 列完整,Dependencies 为 JSON 数组字符串;A2-A6 范围与批准方案一致;Notes 明确无后端、无迁移、无权限与状态机变更,包含回滚路径。","命令:python3 - <<'PY' 校验 CSV 12 列、状态值、Dependencies JSON 数组字符串;业务:对照 AGENTS.md 确认 R67 仅覆盖前端 UI 改造,不触碰 workflow、权限、审批规则、迁移。","manual","DONE","DONE","DONE","issues/2026-04-26_r67-ui-refresh.csv","[]","回滚路径:删除本 CSV 草稿即可回到 R66 基线;无后端 API、无 Prisma 迁移、无权限/状态机/workflow 变更。验证:CSV 结构校验通过,范围已按批准方案冻结;设计参考 www.hyinda.com 的深海蓝、冷浅灰、白卡、几何 Logo 与水声科技感。"
"A2","视觉基础与设计 token 刷新","参考公司官网 www.hyinda.com,将当前灰白后台基调升级为深海蓝科技感轻量运营工作台:更新 CSS 变量、Tailwind token、页面背景、卡片层级、焦点与状态色,减少同质化白卡。","1) 全局背景、卡片、表格、badge、输入控件视觉层级更清晰;2) 不引入新 npm 依赖;3) 不破坏既有类名兼容性;4) 页面在桌面和移动端保持可读。","命令:cd client && npm run typecheck && npm run build;业务:打开 Portal、列表页、系统设置和移动入口,确认共享样式未造成文字不可读、内容溢出或点击目标缩小。","manual","DONE","DONE","TODO","client/src/design/theme.css|client/src/design/tokens.ts|client/src/index.css|client/tailwind.config.*","[""A1""]","回滚路径:恢复 R66 token 与共享 CSS;不新增依赖,不改业务逻辑。官网参考:主色 #00398a,冷浅灰背景 #f5f7f8,白色卡片,slate 文本,几何 Logo。 验证:client typecheck/build 通过;员工、管理者、管理员三类账号人工视觉复核待执行。"
"A3","App Shell 与导航视觉重构","优化侧边栏、顶部栏、移动菜单与导航项视觉层级,让用户先看主路径、再看管理入口、最后看备用模块;保留 R66 小团队导航显隐规则。","1) 主导航、管理入口、备用模块层级更明显;2) 当前路由状态、搜索、通知、用户问候区更清晰;3) 移动菜单仍可正常打开/关闭;4) 不改变导航数据来源与权限过滤。","命令:cd client && npm run typecheck && npm run build;业务:使用员工、管理者、管理员账号检查 Portal、请假、报销、系统设置、备用模块入口,确认可见性与 R66 一致。","manual","DONE","DONE","TODO","client/src/layouts/AppShellLayout.tsx|client/src/components/navigation/Sidebar.tsx|client/src/components/navigation/Topbar.tsx|client/src/components/navigation/NavLinks.tsx|client/src/components/shell/AppBrand.tsx","[""A2""]","回滚路径:恢复 Shell 与导航组件到 R66;仅改视觉和布局 class,不改权限与导航 schema。 验证:client typecheck/build 通过;员工、管理者、管理员三类账号人工视觉复核待执行。"
"A4","共享工作台组件刷新","重构 PageScaffold、WorkbenchHero、SectionPanel、MetricCard、EmptyState 等共享展示模式,让页面不再像一组相同白色卡片堆叠,同时保持现有组件 API。","1) 共享组件 API 不破坏现有页面调用;2) Hero、指标卡、内容面板、空态卡有明确视觉角色差异;3) 警告/危险状态仍可辨识;4) 大量复用页面自动受益。","命令:cd client && npm run typecheck && npm run build;业务:抽查 Portal、组织架构、公告、报销详情、系统设置页面,确认共享组件样式一致且无布局回归。","manual","DONE","DONE","TODO","client/src/components/patterns/*.tsx|client/src/components/common/EmptyStateCard.tsx|client/src/components/common/PageHeader.tsx|client/src/index.css","[""A2""]","回滚路径:恢复共享组件和 CSS class;保持组件 props 不变,降低回滚风险。 验证:client typecheck/build 通过;员工、管理者、管理员三类账号人工视觉复核待执行。"
"A5","Portal 与登录页旗舰体验刷新","将 Portal 首页和登录页作为第一轮样板页,强化今日待办、快捷动作、团队状态、近期动态和进入方式的视觉重点,减少用户觉得“丑”和“像后台模板”的感知。","1) Portal 首屏能快速识别今日要做什么;2) 快捷动作、待办、团队状态、动态流区分明显;3) 登录页保持企业微信/账号密码回退逻辑不变;4) 文案和视觉更适合小团队日常使用。","命令:cd client && npm run typecheck && npm run build;业务:员工视角检查 Portal 首屏、快捷操作、待办、公告;管理者视角检查团队状态、周报缺口、试点观测;浏览器和企业微信入口登录逻辑保持可理解。","manual","DONE","DONE","TODO","client/src/pages/PortalHomePage.tsx|client/src/layouts/AuthLayout.tsx|client/src/pages/LoginPage.tsx","[""A3"",""A4""]","回滚路径:恢复 Portal/Login 相关组件;不改 API 调用、query key、登录流程、权限判断。 验证:client typecheck/build 通过;员工、管理者、管理员三类账号人工视觉复核待执行。"
"A6","R67 验证、状态回填与提交收口","完成 UI 改造后执行前端验证、必要 smoke 检查、人工视觉复核和 CSV 状态回填,形成 R67 提交。","1) A2-A5 的 Test_Method 已执行并记录结果;2) client typecheck/build 通过;3) 受影响主路径人工复核通过或记录未执行原因;4) git commit 成功,提交信息引用 R67 A1-A6。","命令:git diff --check && cd client && npm run typecheck && npm run build;业务:按员工、管理者、管理员三类入口复核 Portal、导航、登录、通知入口、搜索入口和移动菜单。","manual","DOING","TODO","TODO","issues/2026-04-26_r67-ui-refresh.csv|client/src/**","[""A5""]","回滚路径:若验证失败,不回填 DONE,不提交;必要时整批恢复 R67 前端视觉改动。 当前进度:自动验证已完成 client typecheck/build;仍待 git diff --check、CSV 校验与三类账号人工视觉复核。"
1 ID Title Description Acceptance Test_Method Tools Dev_Status Review1_Status Regression_Status Files Dependencies Notes
2 A1 R67 UI 改造合同建档与范围冻结 创建 R67 前端 UI 改造执行合同,冻结第一轮范围:只改前端视觉基础、App Shell、共享组件、Portal 首页与登录页;视觉方向参考公司官网 www.hyinda.com;不改后端 API、权限、workflow、状态机、Prisma 迁移和导航可见性规则。 CSV 列完整,Dependencies 为 JSON 数组字符串;A2-A6 范围与批准方案一致;Notes 明确无后端、无迁移、无权限与状态机变更,包含回滚路径。 命令:python3 - <<'PY' 校验 CSV 12 列、状态值、Dependencies JSON 数组字符串;业务:对照 AGENTS.md 确认 R67 仅覆盖前端 UI 改造,不触碰 workflow、权限、审批规则、迁移。 manual DONE DONE DONE issues/2026-04-26_r67-ui-refresh.csv [] 回滚路径:删除本 CSV 草稿即可回到 R66 基线;无后端 API、无 Prisma 迁移、无权限/状态机/workflow 变更。验证:CSV 结构校验通过,范围已按批准方案冻结;设计参考 www.hyinda.com 的深海蓝、冷浅灰、白卡、几何 Logo 与水声科技感。
3 A2 视觉基础与设计 token 刷新 参考公司官网 www.hyinda.com,将当前灰白后台基调升级为深海蓝科技感轻量运营工作台:更新 CSS 变量、Tailwind token、页面背景、卡片层级、焦点与状态色,减少同质化白卡。 1) 全局背景、卡片、表格、badge、输入控件视觉层级更清晰;2) 不引入新 npm 依赖;3) 不破坏既有类名兼容性;4) 页面在桌面和移动端保持可读。 命令:cd client && npm run typecheck && npm run build;业务:打开 Portal、列表页、系统设置和移动入口,确认共享样式未造成文字不可读、内容溢出或点击目标缩小。 manual DONE DONE TODO client/src/design/theme.css|client/src/design/tokens.ts|client/src/index.css|client/tailwind.config.* ["A1"] 回滚路径:恢复 R66 token 与共享 CSS;不新增依赖,不改业务逻辑。官网参考:主色 #00398a,冷浅灰背景 #f5f7f8,白色卡片,slate 文本,几何 Logo。 验证:client typecheck/build 通过;员工、管理者、管理员三类账号人工视觉复核待执行。
4 A3 App Shell 与导航视觉重构 优化侧边栏、顶部栏、移动菜单与导航项视觉层级,让用户先看主路径、再看管理入口、最后看备用模块;保留 R66 小团队导航显隐规则。 1) 主导航、管理入口、备用模块层级更明显;2) 当前路由状态、搜索、通知、用户问候区更清晰;3) 移动菜单仍可正常打开/关闭;4) 不改变导航数据来源与权限过滤。 命令:cd client && npm run typecheck && npm run build;业务:使用员工、管理者、管理员账号检查 Portal、请假、报销、系统设置、备用模块入口,确认可见性与 R66 一致。 manual DONE DONE TODO client/src/layouts/AppShellLayout.tsx|client/src/components/navigation/Sidebar.tsx|client/src/components/navigation/Topbar.tsx|client/src/components/navigation/NavLinks.tsx|client/src/components/shell/AppBrand.tsx ["A2"] 回滚路径:恢复 Shell 与导航组件到 R66;仅改视觉和布局 class,不改权限与导航 schema。 验证:client typecheck/build 通过;员工、管理者、管理员三类账号人工视觉复核待执行。
5 A4 共享工作台组件刷新 重构 PageScaffold、WorkbenchHero、SectionPanel、MetricCard、EmptyState 等共享展示模式,让页面不再像一组相同白色卡片堆叠,同时保持现有组件 API。 1) 共享组件 API 不破坏现有页面调用;2) Hero、指标卡、内容面板、空态卡有明确视觉角色差异;3) 警告/危险状态仍可辨识;4) 大量复用页面自动受益。 命令:cd client && npm run typecheck && npm run build;业务:抽查 Portal、组织架构、公告、报销详情、系统设置页面,确认共享组件样式一致且无布局回归。 manual DONE DONE TODO client/src/components/patterns/*.tsx|client/src/components/common/EmptyStateCard.tsx|client/src/components/common/PageHeader.tsx|client/src/index.css ["A2"] 回滚路径:恢复共享组件和 CSS class;保持组件 props 不变,降低回滚风险。 验证:client typecheck/build 通过;员工、管理者、管理员三类账号人工视觉复核待执行。
6 A5 Portal 与登录页旗舰体验刷新 将 Portal 首页和登录页作为第一轮样板页,强化今日待办、快捷动作、团队状态、近期动态和进入方式的视觉重点,减少用户觉得“丑”和“像后台模板”的感知。 1) Portal 首屏能快速识别今日要做什么;2) 快捷动作、待办、团队状态、动态流区分明显;3) 登录页保持企业微信/账号密码回退逻辑不变;4) 文案和视觉更适合小团队日常使用。 命令:cd client && npm run typecheck && npm run build;业务:员工视角检查 Portal 首屏、快捷操作、待办、公告;管理者视角检查团队状态、周报缺口、试点观测;浏览器和企业微信入口登录逻辑保持可理解。 manual DONE DONE TODO client/src/pages/PortalHomePage.tsx|client/src/layouts/AuthLayout.tsx|client/src/pages/LoginPage.tsx ["A3","A4"] 回滚路径:恢复 Portal/Login 相关组件;不改 API 调用、query key、登录流程、权限判断。 验证:client typecheck/build 通过;员工、管理者、管理员三类账号人工视觉复核待执行。
7 A6 R67 验证、状态回填与提交收口 完成 UI 改造后执行前端验证、必要 smoke 检查、人工视觉复核和 CSV 状态回填,形成 R67 提交。 1) A2-A5 的 Test_Method 已执行并记录结果;2) client typecheck/build 通过;3) 受影响主路径人工复核通过或记录未执行原因;4) git commit 成功,提交信息引用 R67 A1-A6。 命令:git diff --check && cd client && npm run typecheck && npm run build;业务:按员工、管理者、管理员三类入口复核 Portal、导航、登录、通知入口、搜索入口和移动菜单。 manual DOING TODO TODO issues/2026-04-26_r67-ui-refresh.csv|client/src/** ["A5"] 回滚路径:若验证失败,不回填 DONE,不提交;必要时整批恢复 R67 前端视觉改动。 当前进度:自动验证已完成 client typecheck/build;仍待 git diff --check、CSV 校验与三类账号人工视觉复核。