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: {
|
||||
|
||||
+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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 校验与三类账号人工视觉复核。"
|
||||
|
在新工单中引用
屏蔽一个用户