重构前端 Wave0-2 体验基线(05-28-frontend-ux-redesign)
这个提交包含在:
@@ -115,7 +115,7 @@ export function ForgotPasswordPage() {
|
||||
return (
|
||||
<AuthPanel
|
||||
title="找回密码"
|
||||
subtitle="使用账号邮箱接收验证码"
|
||||
subtitle="用企业邮箱验证码重置"
|
||||
data-testid="forgot-password-page"
|
||||
footer={(
|
||||
<Link className="font-medium text-primary hover:underline" to="/login">
|
||||
@@ -123,6 +123,17 @@ export function ForgotPasswordPage() {
|
||||
</Link>
|
||||
)}
|
||||
>
|
||||
<div className="ux-step-list mb-5">
|
||||
<div className="ux-step-item">
|
||||
<span className="ux-step-item__index">1</span>
|
||||
<span>输入账号绑定的企业邮箱,系统发送 6 位验证码。</span>
|
||||
</div>
|
||||
<div className="ux-step-item">
|
||||
<span className="ux-step-item__index">2</span>
|
||||
<span>填写验证码和新密码;验证码 10 分钟内有效。</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form className="space-y-4" onSubmit={handleRequestCode}>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="forgot-password-email">邮箱</Label>
|
||||
|
||||
@@ -366,8 +366,14 @@ export function LoginPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthPanel title="登录工作台" subtitle={showWecomPrimary ? "企业微信优先入口" : "账号、邮箱或企业微信"}>
|
||||
<AuthPanel title="海音达 OA" subtitle={showWecomPrimary ? "企业微信环境优先免登" : "选择一种方式进入工作台"}>
|
||||
<div className="space-y-4">
|
||||
{!showWecomPrimary ? (
|
||||
<div className="ux-command-note">
|
||||
员工日常建议用企业邮箱验证码;固定办公电脑可使用账号密码。企业微信免登需要从企业微信工作台打开。
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{wecomNotice ? (
|
||||
<div className={`rounded-lg border px-3.5 py-2.5 text-sm ${noticeClasses(wecomNotice.tone)}`} data-testid="login-wecom-notice">
|
||||
<div className="font-medium">{wecomNotice.title}</div>
|
||||
@@ -406,7 +412,7 @@ export function LoginPage() {
|
||||
type="button"
|
||||
onClick={() => selectLoginMode("password")}
|
||||
>
|
||||
密码登录
|
||||
账号密码
|
||||
</button>
|
||||
<button
|
||||
aria-pressed={loginMode === "email_code"}
|
||||
@@ -430,7 +436,7 @@ export function LoginPage() {
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3 text-xs text-slate-400">
|
||||
<span className="h-px flex-1 bg-slate-100" />
|
||||
<span>其他登录方式</span>
|
||||
<span>企业入口</span>
|
||||
<span className="h-px flex-1 bg-slate-100" />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,11 +4,10 @@ import { toast } from "sonner";
|
||||
import { fetchNotifications, markAllNotificationsRead, markNotificationRead } from "../api/notifications";
|
||||
import { isApiError } from "../api/types";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { PageHeader } from "../components/common/PageHeader";
|
||||
import { TableSkeleton } from "../components/common/TableSkeleton";
|
||||
import { Pagination } from "../components/common/Pagination";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { PageScaffold } from "../components/patterns/PageScaffold";
|
||||
import { FilterBar, ListWorkspace, WorkspaceHeader } from "../components/patterns/ListWorkspace";
|
||||
import { resolveNotificationTargetLink } from "../lib/notificationLinks";
|
||||
import { resolveErrorMessage } from "../lib/resolveErrorMessage";
|
||||
import { Tabs, TabsList, TabsTrigger } from "../components/ui/tabs";
|
||||
@@ -79,10 +78,11 @@ export function NotificationsPage() {
|
||||
const notificationItems = listQuery.data?.items ?? [];
|
||||
|
||||
return (
|
||||
<PageScaffold>
|
||||
<PageHeader
|
||||
title="通知"
|
||||
subtitle="事件、待办和业务直达入口。"
|
||||
<ListWorkspace>
|
||||
<WorkspaceHeader
|
||||
eyebrow="通知"
|
||||
title="通知中心"
|
||||
subtitle="这里是事件收件箱;需要处理的事项会直达业务详情或审批任务。"
|
||||
actions={
|
||||
<>
|
||||
<Button
|
||||
@@ -95,13 +95,13 @@ export function NotificationsPage() {
|
||||
全部标为已读
|
||||
</Button>
|
||||
<Button asChild variant="secondary" data-testid="notifications-back">
|
||||
<Link to="/dashboard">返回仪表盘</Link>
|
||||
<Link to="/portal">返回工作台</Link>
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="card-std space-y-4 p-4 md:p-5">
|
||||
<FilterBar className="ops-command-bar !p-3 md:!p-3.5">
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-center">
|
||||
<Tabs
|
||||
@@ -128,7 +128,7 @@ export function NotificationsPage() {
|
||||
<span>类型</span>
|
||||
<select
|
||||
data-testid="notifications-type-filter"
|
||||
className="h-10 min-w-40 rounded-lg border border-zinc-200 bg-white px-3 text-sm text-zinc-700 shadow-sm outline-none transition focus:border-zinc-300 focus:ring-2 focus:ring-zinc-100"
|
||||
className="select-sm min-w-40"
|
||||
value={selectedType}
|
||||
onChange={(event) =>
|
||||
updateFilters({
|
||||
@@ -151,20 +151,21 @@ export function NotificationsPage() {
|
||||
<span className="ml-2 text-zinc-500">/ 共 {listQuery.data?.total ?? 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
</FilterBar>
|
||||
|
||||
{listQuery.isLoading ? <TableSkeleton columns={4} rows={5} /> : null}
|
||||
{listQuery.isLoading ? <TableSkeleton columns={4} rows={5} /> : null}
|
||||
|
||||
{listQuery.isError ? (
|
||||
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
|
||||
{isApiError(listQuery.error)
|
||||
? `${listQuery.error.code}: ${mapApiErrorMessage(
|
||||
listQuery.error.code,
|
||||
listQuery.error.details,
|
||||
listQuery.error.message,
|
||||
)}`
|
||||
: "加载通知失败"}
|
||||
</p>
|
||||
) : null}
|
||||
{listQuery.isError ? (
|
||||
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
|
||||
{isApiError(listQuery.error)
|
||||
? `${listQuery.error.code}: ${mapApiErrorMessage(
|
||||
listQuery.error.code,
|
||||
listQuery.error.details,
|
||||
listQuery.error.message,
|
||||
)}`
|
||||
: "加载通知失败"}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{!listQuery.isLoading && !listQuery.isError ? (
|
||||
<>
|
||||
@@ -275,7 +276,6 @@ export function NotificationsPage() {
|
||||
}
|
||||
testIdPrefix="notifications"
|
||||
/>
|
||||
</div>
|
||||
</PageScaffold>
|
||||
</ListWorkspace>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -691,7 +691,8 @@ function QuickActionsPanel({
|
||||
}) {
|
||||
return (
|
||||
<SectionPanel
|
||||
title="常用"
|
||||
title="常用动作"
|
||||
description={layout === "rail" ? "从这里开始当天的主要操作。" : undefined}
|
||||
className={layout === "rail" ? "portal-panel-fill" : undefined}
|
||||
>
|
||||
<div className={layout === "rail" ? "portal-action-stack" : "grid gap-3 sm:grid-cols-2 xl:grid-cols-4"}>
|
||||
@@ -718,7 +719,8 @@ function TodoAggregatePanel({ items }: { items: TodoAggregateItem[] }) {
|
||||
|
||||
return (
|
||||
<SectionPanel
|
||||
title="待处理"
|
||||
title="今天先处理"
|
||||
description="审批、TODO 和异常提醒集中在这里。"
|
||||
className="portal-panel-fill"
|
||||
>
|
||||
<div className="space-y-3">
|
||||
@@ -1121,12 +1123,12 @@ export function PortalHomePage() {
|
||||
title={`你好,${payload.greeting.name}`}
|
||||
className="workbench-hero--compact"
|
||||
subtitle={roleCapabilities.canUseFinance
|
||||
? "付款、风险、审批。"
|
||||
: roleCapabilities.canManageSystem
|
||||
? "配置、审计、通知。"
|
||||
? "付款、风险、审批。"
|
||||
: roleCapabilities.canManageSystem
|
||||
? "先看待处理,再进入配置、审计和通知。"
|
||||
: isManagerView
|
||||
? "审批、团队、周报。"
|
||||
: "待办、申请、考勤。"}
|
||||
? "先处理审批,再看团队状态和周报缺口。"
|
||||
: "先看待办,再提交申请、补材料或查看状态。"}
|
||||
actions={
|
||||
!payload.workspace.persisted && canSetupWorkspace ? (
|
||||
<Button asChild variant="secondary" type="button">
|
||||
|
||||
@@ -976,7 +976,7 @@ export function ReimbursementDetailPage() {
|
||||
{downloadReasonDialog}
|
||||
<EntityOverviewHero
|
||||
title={`报销详情 ${displayCode}`}
|
||||
subtitle="状态、票据、审批和付款集中处理。"
|
||||
subtitle="先确认当前状态和下一步,再处理明细、发票、附件和审批记录。"
|
||||
meta={(
|
||||
<>
|
||||
<StatusBadge status={detail.status} />
|
||||
@@ -992,7 +992,7 @@ export function ReimbursementDetailPage() {
|
||||
<Link to={`/reimbursements/${detail.id}/print`}>打印 / PDF</Link>
|
||||
</Button>
|
||||
<Button asChild variant="secondary" data-testid="reimbursement-back">
|
||||
<Link to="/reimbursements">返回列表</Link>
|
||||
<Link to="/reimbursements">报销列表</Link>
|
||||
</Button>
|
||||
{canRemindPendingWorkflow && pendingTaskId ? (
|
||||
<Button
|
||||
@@ -1022,7 +1022,7 @@ export function ReimbursementDetailPage() {
|
||||
|
||||
<div className="detail-workspace-grid">
|
||||
<div className="space-y-5">
|
||||
<SectionPanel title="办理摘要">
|
||||
<SectionPanel title="当前状态与下一步" description="提交、付款、附件和审批依据集中在本页处理。">
|
||||
<div className="space-y-4" data-testid="reimbursement-overview">
|
||||
<StatStrip columns={4} className="ops-status-strip">
|
||||
<MetricCard
|
||||
@@ -1168,8 +1168,8 @@ export function ReimbursementDetailPage() {
|
||||
<div className="space-y-4 text-sm">
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-base font-semibold text-zinc-900">附件材料</h3>
|
||||
<p className="text-xs text-zinc-500">支付截图、合同、订单或情况说明,不参与发票识别。</p>
|
||||
<h3 className="text-base font-semibold text-zinc-900">补充附件</h3>
|
||||
<p className="text-xs text-zinc-500">合同、订单、支付截图或情况说明;敏感附件下载会要求填写用途。</p>
|
||||
</div>
|
||||
{canEditItems ? (
|
||||
<div className="flex flex-col gap-2 md:flex-row md:items-center">
|
||||
@@ -1245,14 +1245,14 @@ export function ReimbursementDetailPage() {
|
||||
</div>
|
||||
</SectionPanel>
|
||||
|
||||
<SectionPanel title="明细与票据">
|
||||
<SectionPanel title="明细与发票" description="明细决定金额,发票决定提交完整性。">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4" data-testid="reimbursement-items">
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-base font-semibold text-zinc-900">报销明细</h3>
|
||||
<p className="text-xs text-zinc-500">明细变更会自动重算总金额。</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-base font-semibold text-zinc-900">报销明细</h3>
|
||||
<p className="text-xs text-zinc-500">明细变更会自动重算总金额。</p>
|
||||
</div>
|
||||
|
||||
{canEditItems ? (
|
||||
<Button
|
||||
@@ -1399,6 +1399,7 @@ export function ReimbursementDetailPage() {
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-base font-semibold text-zinc-900">发票</h3>
|
||||
<p className="mt-1 text-xs text-zinc-500">提交前至少添加 1 张发票;重复或不可用发票会给出原因。</p>
|
||||
</div>
|
||||
{canOpenInvoiceBindDialog ? (
|
||||
<Button
|
||||
|
||||
@@ -29,7 +29,7 @@ import { Pagination } from "../components/common/Pagination";
|
||||
import { ActionConfirmDialog } from "../components/common/ActionConfirmDialog";
|
||||
import { StatusBadge } from "../components/common/StatusBadge";
|
||||
import { TableSkeleton } from "../components/common/TableSkeleton";
|
||||
import { FilterBar, ListWorkspace, PrimaryTable, StatStrip, WorkspaceHeader } from "../components/patterns/ListWorkspace";
|
||||
import { FilterBar, ListWorkspace, PrimaryTable, StatStrip, WorkspaceHeader, WorkspaceNotice } from "../components/patterns/ListWorkspace";
|
||||
import { MetricCard } from "../components/patterns/MetricCard";
|
||||
import { Button } from "../components/ui/button";
|
||||
import {
|
||||
@@ -362,8 +362,9 @@ export function ReimbursementsPage() {
|
||||
return (
|
||||
<ListWorkspace>
|
||||
<WorkspaceHeader
|
||||
title="我的报销"
|
||||
subtitle="单据、金额、状态。"
|
||||
eyebrow="报销"
|
||||
title={scope === "all" ? "报销列表" : "我的报销"}
|
||||
subtitle="先看状态和金额,再继续编辑、查看详情、导出或批量处理。"
|
||||
actions={
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
|
||||
{canCreateExpense ? (
|
||||
@@ -594,6 +595,16 @@ export function ReimbursementsPage() {
|
||||
) : null}
|
||||
</FilterBar>
|
||||
|
||||
{status === "draft" ? (
|
||||
<WorkspaceNotice title="草稿可以继续编辑或删除" tone="neutral">
|
||||
提交后的单据进入审批链;已通过、已支付单据不再提供删除入口。
|
||||
</WorkspaceNotice>
|
||||
) : status === "reviewing" ? (
|
||||
<WorkspaceNotice title="审批中单据以详情页为主" tone="neutral">
|
||||
查看当前节点、票据、附件和审批历史后再处理;有批量权限时只会选择你可处理的待办。
|
||||
</WorkspaceNotice>
|
||||
) : null}
|
||||
|
||||
{listQuery.isLoading ? <TableSkeleton columns={batchMode ? 7 : 6} rows={5} /> : null}
|
||||
|
||||
{listQuery.isError ? (
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
StatStrip,
|
||||
WorkspaceEmptyState,
|
||||
WorkspaceHeader,
|
||||
WorkspaceNotice,
|
||||
} from "../components/patterns/ListWorkspace";
|
||||
import { MetricCard } from "../components/patterns/MetricCard";
|
||||
import { Button } from "../components/ui/button";
|
||||
@@ -209,7 +210,7 @@ export function WorkflowTasksPage() {
|
||||
<WorkspaceHeader
|
||||
eyebrow="审批"
|
||||
title="审批中心"
|
||||
subtitle="任务、节点、业务状态。"
|
||||
subtitle="先看待处理,再进入详情核对依据;快速通过前仍会弹出确认。"
|
||||
/>
|
||||
|
||||
<div data-testid="workflow-task-summary">
|
||||
@@ -272,19 +273,25 @@ export function WorkflowTasksPage() {
|
||||
</div>
|
||||
</FilterBar>
|
||||
|
||||
{query.isLoading ? <TableSkeleton columns={7} rows={5} /> : null}
|
||||
{status === "pending" ? (
|
||||
<WorkspaceNotice title="处理原则" tone="neutral">
|
||||
先看业务编号、申请人、金额/天数和当前节点;需要查看附件、审批历史或驳回原因时进入详情页。
|
||||
</WorkspaceNotice>
|
||||
) : null}
|
||||
|
||||
{query.isError ? (
|
||||
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
|
||||
{isApiError(query.error)
|
||||
? `${query.error.code}: ${mapApiErrorMessage(
|
||||
query.error.code,
|
||||
query.error.details,
|
||||
query.error.message,
|
||||
)}`
|
||||
: "加载待办失败,请稍后重试"}
|
||||
</p>
|
||||
) : null}
|
||||
{query.isLoading ? <TableSkeleton columns={7} rows={5} /> : null}
|
||||
|
||||
{query.isError ? (
|
||||
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
|
||||
{isApiError(query.error)
|
||||
? `${query.error.code}: ${mapApiErrorMessage(
|
||||
query.error.code,
|
||||
query.error.details,
|
||||
query.error.message,
|
||||
)}`
|
||||
: "加载待办失败,请稍后重试"}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{!query.isLoading && !query.isError ? (
|
||||
<>
|
||||
@@ -347,7 +354,7 @@ export function WorkflowTasksPage() {
|
||||
</article>
|
||||
))
|
||||
) : (
|
||||
<WorkspaceEmptyState />
|
||||
<WorkspaceEmptyState>当前筛选下没有审批任务。</WorkspaceEmptyState>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -410,7 +417,7 @@ export function WorkflowTasksPage() {
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell className="px-6 py-8 text-center text-zinc-500" colSpan={8}>
|
||||
暂无数据
|
||||
当前筛选下没有审批任务
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
|
||||
在新工单中引用
屏蔽一个用户