重构 PC 首页与报销列表降噪(frontend-simplification-refactor)
这个提交包含在:
+147
-92
@@ -67,6 +67,16 @@ type QuickActionConfig = {
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
type PortalFocusAction = {
|
||||
label: string;
|
||||
to: string;
|
||||
};
|
||||
|
||||
type PortalFocusLink = {
|
||||
label: string;
|
||||
to: string;
|
||||
};
|
||||
|
||||
type GettingStartedChecklistItem = {
|
||||
key: string;
|
||||
title: string;
|
||||
@@ -129,11 +139,6 @@ function compactCards(cards: PortalHomePayload["cards"], excludedKeys: string[])
|
||||
return cards.filter((card) => !excludedKeys.includes(card.key));
|
||||
}
|
||||
|
||||
function formatWeeklyStatus(payload: PortalHomePayload["weeklyReport"]) {
|
||||
if (!payload) return "未启用";
|
||||
return payload.status === "submitted" ? "已提交" : "待填写";
|
||||
}
|
||||
|
||||
function formatRelativeTime(value: string | null) {
|
||||
if (!value) {
|
||||
return "-";
|
||||
@@ -411,6 +416,7 @@ function SecondaryCardsPanel({ title, cards }: { title: string; cards: PortalHom
|
||||
{cards.map((card) => (
|
||||
<MetricCard
|
||||
key={card.key}
|
||||
ariaLabel={`portal-card-${card.key}`}
|
||||
title={card.title}
|
||||
value={card.value}
|
||||
href={card.href}
|
||||
@@ -481,7 +487,7 @@ function CompactMetricLinks({ title, cards }: { title: string; cards: PortalHome
|
||||
return (
|
||||
<CompactGroup title={title}>
|
||||
{cards.length ? cards.slice(0, 6).map((card) => (
|
||||
<Link key={card.key} to={card.href} className="portal-compact-row">
|
||||
<Link key={card.key} to={card.href} className="portal-compact-row" aria-label={`portal-card-${card.key}`}>
|
||||
<span className="min-w-0 truncate">{card.title}</span>
|
||||
<span className={card.severity === "high" || card.severity === "warning" ? "shrink-0 font-semibold text-amber-700" : "shrink-0 font-semibold text-slate-900"}>
|
||||
{card.value}
|
||||
@@ -713,47 +719,77 @@ function QuickActionsPanel({
|
||||
);
|
||||
}
|
||||
|
||||
function TodoAggregatePanel({ items }: { items: TodoAggregateItem[] }) {
|
||||
const visibleItems = items.slice(0, 5);
|
||||
const hiddenCount = Math.max(items.length - visibleItems.length, 0);
|
||||
|
||||
function TodayTaskPanel({
|
||||
primaryAction,
|
||||
secondaryLinks,
|
||||
focusItem,
|
||||
hiddenFocusCount,
|
||||
}: {
|
||||
primaryAction: PortalFocusAction | null;
|
||||
secondaryLinks: PortalFocusLink[];
|
||||
focusItem: TodoAggregateItem | null;
|
||||
hiddenFocusCount: number;
|
||||
}) {
|
||||
return (
|
||||
<SectionPanel
|
||||
title="今日待办焦点"
|
||||
description="审批、TODO 和异常提醒集中在这里。"
|
||||
className="portal-panel-fill"
|
||||
>
|
||||
<div className="space-y-3">
|
||||
{visibleItems.length ? visibleItems.map((item) => (
|
||||
<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-lg bg-red-50 text-red-600"
|
||||
: item.severity === "warning"
|
||||
? "mt-0.5 flex size-9 items-center justify-center rounded-lg bg-amber-50 text-amber-600"
|
||||
: "mt-0.5 flex size-9 items-center justify-center rounded-lg bg-zinc-100 text-zinc-600"}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[18px]">{item.icon}</span>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<div className="font-medium text-zinc-900">{item.title}</div>
|
||||
<span className="badge-neutral">{item.sourceLabel}</span>
|
||||
{item.severity === "high" ? <span className="badge-error">高优先</span> : null}
|
||||
{item.severity === "warning" ? <span className="badge-warning">关注</span> : null}
|
||||
<SectionPanel title="今日任务" className="portal-panel-fill">
|
||||
<div className="grid gap-4 xl:grid-cols-[minmax(0,1.45fr),minmax(280px,0.72fr)] xl:items-stretch">
|
||||
<div className="rounded-lg border border-slate-200 bg-white/80 px-3.5 py-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="text-sm font-semibold text-zinc-900">当前焦点</div>
|
||||
{hiddenFocusCount > 0 ? <div className="text-xs text-zinc-500">另有 {hiddenFocusCount} 项</div> : null}
|
||||
</div>
|
||||
|
||||
{focusItem ? (
|
||||
<Link
|
||||
to={focusItem.href}
|
||||
className="mt-3 block rounded-lg border border-slate-200 bg-slate-50/70 px-3 py-2.5 transition-colors hover:border-slate-300 hover:bg-white"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={focusItem.severity === "high"
|
||||
? "flex size-9 shrink-0 items-center justify-center rounded-lg bg-red-50 text-red-600"
|
||||
: focusItem.severity === "warning"
|
||||
? "flex size-9 shrink-0 items-center justify-center rounded-lg bg-amber-50 text-amber-600"
|
||||
: "flex size-9 shrink-0 items-center justify-center rounded-lg bg-zinc-100 text-zinc-600"}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[18px]">{focusItem.icon}</span>
|
||||
</div>
|
||||
<div className="mt-1 text-sm text-primary">进入</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate font-medium text-zinc-900">{focusItem.title}</div>
|
||||
<div className="mt-1 text-xs text-zinc-500">{focusItem.sourceLabel}</div>
|
||||
</div>
|
||||
<span className="material-symbols-outlined text-zinc-400">arrow_forward</span>
|
||||
</div>
|
||||
</Link>
|
||||
) : (
|
||||
<div className="mt-3 rounded-lg border border-dashed border-slate-300 px-3 py-4 text-sm text-zinc-500">
|
||||
暂无必须立即处理的事项。
|
||||
</div>
|
||||
</Link>
|
||||
)) : <EmptyStateCard title="暂无待办" description="没有需要立刻处理的事。" />}
|
||||
</div>
|
||||
{hiddenCount > 0 ? (
|
||||
<div className="portal-panel-footer">
|
||||
<span>另有 {hiddenCount} 项。</span>
|
||||
<Link to="/todos" className="font-medium text-primary hover:underline">查看全部待办</Link>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="rounded-lg border border-slate-200 bg-white/90 px-3.5 py-3">
|
||||
<div className="text-sm font-semibold text-zinc-900">下一步</div>
|
||||
{primaryAction ? (
|
||||
<Button asChild className="mt-3 w-full" type="button">
|
||||
<Link to={primaryAction.to}>{primaryAction.label}</Link>
|
||||
</Button>
|
||||
) : (
|
||||
<Button className="mt-3 w-full" disabled type="button">
|
||||
暂无主动作
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{secondaryLinks.length ? (
|
||||
<div className="mt-3 grid gap-2 sm:grid-cols-2 xl:grid-cols-1">
|
||||
{secondaryLinks.map((item) => (
|
||||
<Button key={`${item.label}-${item.to}`} asChild variant="secondary" type="button">
|
||||
<Link to={item.to}>{item.label}</Link>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</SectionPanel>
|
||||
);
|
||||
}
|
||||
@@ -1112,6 +1148,33 @@ export function PortalHomePage() {
|
||||
});
|
||||
}
|
||||
|
||||
const approvalAction = quickActions.find((item) => item.key === "approval");
|
||||
const expenseAction = quickActions.find((item) => item.key === "expense");
|
||||
const fallbackAction = quickActions.find((item): item is QuickActionConfig & { to: string } => Boolean(item.to));
|
||||
const primaryAction: PortalFocusAction | null =
|
||||
payload.pendingTasks.count > 0 && approvalAction?.to
|
||||
? { label: "处理待办", to: approvalAction.to }
|
||||
: expenseAction?.to
|
||||
? { label: "新建报销", to: expenseAction.to }
|
||||
: fallbackAction
|
||||
? { label: fallbackAction.label, to: fallbackAction.to }
|
||||
: null;
|
||||
const secondaryLinkCandidates: Array<PortalFocusLink | null> = [
|
||||
hasAnyPermission(permissions, EXPENSE_PORTAL_PERMISSIONS)
|
||||
? { label: "报销进度", to: "/reimbursements" }
|
||||
: null,
|
||||
payload.pendingTasks.count > 0 && approvalAction?.to
|
||||
? { label: "审批中心", to: approvalAction.to }
|
||||
: null,
|
||||
todoAggregateItems.length > 1
|
||||
? { label: "全部待办", to: "/todos" }
|
||||
: null,
|
||||
];
|
||||
const secondaryLinks = secondaryLinkCandidates
|
||||
.filter((item): item is PortalFocusLink => item !== null)
|
||||
.filter((item) => item.to !== primaryAction?.to)
|
||||
.slice(0, 2);
|
||||
|
||||
return (
|
||||
<PageScaffold>
|
||||
<ErrorBoundary fallback={null}>
|
||||
@@ -1123,12 +1186,12 @@ export function PortalHomePage() {
|
||||
title={`你好,${payload.greeting.name}`}
|
||||
className="workbench-hero--compact"
|
||||
subtitle={roleCapabilities.canUseFinance
|
||||
? "付款、风险、审批。"
|
||||
? "先处理今天的付款、风险和审批。"
|
||||
: roleCapabilities.canManageSystem
|
||||
? "先看待处理,再进入配置、审计和通知。"
|
||||
? "先处理待办,再进入管理入口。"
|
||||
: isManagerView
|
||||
? "审批待办、团队状态和周报缺口集中查看。"
|
||||
: "先看待办,再提交申请、补材料或查看状态。"}
|
||||
? "先处理待办,再看团队状态。"
|
||||
: "先处理待办,再发起申请。"}
|
||||
actions={
|
||||
!payload.workspace.persisted && canSetupWorkspace ? (
|
||||
<Button asChild variant="secondary" type="button">
|
||||
@@ -1138,25 +1201,12 @@ export function PortalHomePage() {
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="portal-metric-strip ops-status-strip">
|
||||
{primaryCards.map((card) => (
|
||||
<MetricCard
|
||||
key={card.key}
|
||||
ariaLabel={`portal-card-${card.key}`}
|
||||
href={card.href}
|
||||
note={card.key === "my_weekly_report"
|
||||
? formatWeeklyStatus(payload.weeklyReport)
|
||||
: card.key === "today_presence"
|
||||
? payload.todayPresence?.note ?? "更新今日状态"
|
||||
: card.key === "team_presence"
|
||||
? payload.teamPresence ? `请假 ${payload.teamPresence.leave},未更新 ${payload.teamPresence.unspecified}` : "团队状态板待启用"
|
||||
: undefined}
|
||||
title={card.title}
|
||||
tone={severityTone(card.severity)}
|
||||
value={card.value}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<TodayTaskPanel
|
||||
primaryAction={primaryAction}
|
||||
secondaryLinks={secondaryLinks}
|
||||
focusItem={todoAggregateItems[0] ?? null}
|
||||
hiddenFocusCount={Math.max(todoAggregateItems.length - 1, 0)}
|
||||
/>
|
||||
|
||||
{showGettingStarted ? (
|
||||
<ErrorBoundary
|
||||
@@ -1175,33 +1225,33 @@ export function PortalHomePage() {
|
||||
</ErrorBoundary>
|
||||
) : null}
|
||||
|
||||
<div className="portal-home-grid">
|
||||
<TodoAggregatePanel items={todoAggregateItems} />
|
||||
<QuickActionsPanel items={quickActions} layout="rail" />
|
||||
</div>
|
||||
|
||||
<div className="portal-home-grid portal-home-grid--collaboration">
|
||||
<ErrorBoundary
|
||||
fallback={(
|
||||
<PortalSectionFallback
|
||||
title="团队全员状态"
|
||||
description="今日团队状态。"
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<TeamStatusBoardPanel items={teamStatusBoard} />
|
||||
</ErrorBoundary>
|
||||
<ErrorBoundary
|
||||
fallback={(
|
||||
<PortalSectionFallback
|
||||
title="近期动态流"
|
||||
description="最近业务动作。"
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<ActivityAnnouncementsPanel activities={recentActivities} announcements={announcementItems} />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
<SecondaryWorkspacePanel
|
||||
title="团队与通知"
|
||||
description="团队状态、动态和公告"
|
||||
>
|
||||
<div className="portal-home-grid portal-home-grid--collaboration">
|
||||
<ErrorBoundary
|
||||
fallback={(
|
||||
<PortalSectionFallback
|
||||
title="团队全员状态"
|
||||
description="今日团队状态。"
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<TeamStatusBoardPanel items={teamStatusBoard} />
|
||||
</ErrorBoundary>
|
||||
<ErrorBoundary
|
||||
fallback={(
|
||||
<PortalSectionFallback
|
||||
title="近期动态流"
|
||||
description="最近业务动作。"
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<ActivityAnnouncementsPanel activities={recentActivities} announcements={announcementItems} />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</SecondaryWorkspacePanel>
|
||||
|
||||
{isManagerView ? (
|
||||
<>
|
||||
@@ -1210,6 +1260,10 @@ export function PortalHomePage() {
|
||||
description="入口"
|
||||
>
|
||||
<div className="grid gap-4 xl:grid-cols-[minmax(0,1fr),minmax(0,1fr)] 2xl:grid-cols-[minmax(0,1fr),minmax(0,1fr),340px]">
|
||||
{primaryCards.length ? (
|
||||
<SecondaryCardsPanel title="状态摘要" cards={primaryCards} />
|
||||
) : null}
|
||||
<QuickActionsPanel items={quickActions} />
|
||||
{roleCapabilities.canApproveTasks ? (
|
||||
<SummaryPanel title="审批处理" items={approvalSummaryItems} />
|
||||
) : null}
|
||||
@@ -1248,6 +1302,7 @@ export function PortalHomePage() {
|
||||
description="申请、资产、最近访问"
|
||||
>
|
||||
<div className="portal-secondary-compact-grid">
|
||||
<CompactMetricLinks title="状态摘要" cards={primaryCards} />
|
||||
<CompactSummaryLinks title="我的申请与资产" items={employeeSummaryItems} />
|
||||
<CompactRecentVisits />
|
||||
<CompactQuickLinks items={quickLinkItems} />
|
||||
|
||||
+212
-171
@@ -29,8 +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, WorkspaceNotice } from "../components/patterns/ListWorkspace";
|
||||
import { MetricCard } from "../components/patterns/MetricCard";
|
||||
import { FilterBar, ListWorkspace, PrimaryTable, WorkspaceHeader } from "../components/patterns/ListWorkspace";
|
||||
import { Button } from "../components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -66,6 +65,15 @@ type BatchResultState = {
|
||||
>;
|
||||
};
|
||||
|
||||
const REIMBURSEMENT_STATUS_OPTIONS: Array<{ value: ReimbursementStatus; label: string }> = [
|
||||
{ value: "draft", label: "草稿" },
|
||||
{ value: "rejected", label: "被驳回" },
|
||||
{ value: "reviewing", label: "审批中" },
|
||||
{ value: "approved", label: "已通过" },
|
||||
{ value: "paid", label: "已支付" },
|
||||
{ value: "cancelled", label: "已取消" },
|
||||
];
|
||||
|
||||
function formatCurrency(value: number) {
|
||||
return new Intl.NumberFormat("zh-CN", { style: "currency", currency: "CNY" }).format(value);
|
||||
}
|
||||
@@ -108,6 +116,7 @@ export function ReimbursementsPage() {
|
||||
const [batchResult, setBatchResult] = useState<BatchResultState | null>(null);
|
||||
const [deleteTarget, setDeleteTarget] = useState<ReimbursementListItem | null>(null);
|
||||
const [exportConfirmFormat, setExportConfirmFormat] = useState<ExportFormat | null>(null);
|
||||
const [showLedgerTools, setShowLedgerTools] = useState(false);
|
||||
|
||||
const status = (searchParams.get("status") as ReimbursementStatus | null) ?? "draft";
|
||||
const page = Number(searchParams.get("page") ?? "1") || 1;
|
||||
@@ -178,6 +187,7 @@ export function ReimbursementsPage() {
|
||||
);
|
||||
const canShowBatchEntry =
|
||||
status === "reviewing" && scope === "all" && canApproveExpense && actionableItems.length > 0;
|
||||
const showAdvancedTools = showLedgerTools || scope === "all" || batchMode || showProxyOwnerSelector;
|
||||
const selectedItems = actionableItems.filter((item) => selectedReimbursementIds.includes(item.id));
|
||||
const allActionableSelected =
|
||||
actionableItems.length > 0 && actionableItems.every((item) => selectedReimbursementIds.includes(item.id));
|
||||
@@ -363,67 +373,18 @@ export function ReimbursementsPage() {
|
||||
<ListWorkspace>
|
||||
<WorkspaceHeader
|
||||
eyebrow="报销"
|
||||
title={scope === "all" ? "报销列表" : "我的报销"}
|
||||
subtitle="先看状态和金额,再继续编辑、查看详情、导出或批量处理。"
|
||||
title={scope === "all" ? "报销台账" : "我的报销"}
|
||||
actions={
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
|
||||
{canCreateExpense ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
data-testid="reimbursements-create"
|
||||
disabled={createMutation.isPending}
|
||||
type="button"
|
||||
>
|
||||
{createMutation.isPending ? "创建中..." : "新建报销"}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-80">
|
||||
<DropdownMenuItem onSelect={() => startCreateReimbursement()}>
|
||||
空白报销
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<div className="px-2 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-zinc-400">
|
||||
从模板创建
|
||||
</div>
|
||||
{templateOptionsQuery.isLoading ? (
|
||||
<DropdownMenuItem disabled>正在加载模板...</DropdownMenuItem>
|
||||
) : templateOptions.length ? (
|
||||
templateOptions.map((template) => (
|
||||
<DropdownMenuItem
|
||||
key={template.id}
|
||||
onSelect={() => startCreateReimbursement({ templateId: template.id })}
|
||||
>
|
||||
{template.name}
|
||||
</DropdownMenuItem>
|
||||
))
|
||||
) : (
|
||||
<DropdownMenuItem disabled>暂无可用模板</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
<div className="px-2 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-zinc-400">
|
||||
关联差旅申请
|
||||
</div>
|
||||
{linkableTravelRequestsQuery.isLoading ? (
|
||||
<DropdownMenuItem disabled>正在加载差旅申请...</DropdownMenuItem>
|
||||
) : linkableTravelRequests.length ? (
|
||||
linkableTravelRequests.map((travelRequest) => (
|
||||
<DropdownMenuItem
|
||||
key={travelRequest.id}
|
||||
onSelect={() => startCreateReimbursement({ linkedTravelRequestId: travelRequest.id })}
|
||||
>
|
||||
{travelRequest.requestNo ?? travelRequest.id} · {travelRequest.purpose}
|
||||
</DropdownMenuItem>
|
||||
))
|
||||
) : (
|
||||
<DropdownMenuItem disabled>暂无可关联的差旅申请</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : null}
|
||||
</div>
|
||||
canCreateExpense ? (
|
||||
<Button
|
||||
data-testid="reimbursements-create"
|
||||
disabled={createMutation.isPending}
|
||||
onClick={() => startCreateReimbursement()}
|
||||
type="button"
|
||||
>
|
||||
{createMutation.isPending ? "创建中..." : "新建报销"}
|
||||
</Button>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -462,13 +423,6 @@ export function ReimbursementsPage() {
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<StatStrip columns={4} className="ops-status-strip">
|
||||
<MetricCard title="当前列表" value={stats.currentPageCount} note={`共 ${listQuery.data?.total ?? 0} 条`} />
|
||||
<MetricCard title="当前金额" value={formatCurrency(stats.currentPageAmount)} note="当前页" />
|
||||
<MetricCard title="审批中" value={stats.reviewing} note="待推进" tone={stats.reviewing > 0 ? "warning" : "neutral"} />
|
||||
<MetricCard title="已支付" value={stats.paid} note={scope === "all" ? "全范围" : "仅自己"} />
|
||||
</StatStrip>
|
||||
|
||||
<FilterBar className="ops-command-bar space-y-3 !p-3 md:!p-3.5">
|
||||
<div className="ops-command-bar__row">
|
||||
<Tabs
|
||||
@@ -477,14 +431,7 @@ export function ReimbursementsPage() {
|
||||
onValueChange={(value) => setSearchParams({ status: value, page: "1", scope })}
|
||||
>
|
||||
<TabsList className="ops-command-bar__tabs !h-auto !justify-start !bg-slate-50">
|
||||
{[
|
||||
{ value: "draft", label: "草稿" },
|
||||
{ value: "reviewing", label: "审批中" },
|
||||
{ value: "approved", label: "已通过" },
|
||||
{ value: "paid", label: "已支付" },
|
||||
{ value: "rejected", label: "已驳回" },
|
||||
{ value: "cancelled", label: "已取消" },
|
||||
].map((item) => (
|
||||
{REIMBURSEMENT_STATUS_OPTIONS.map((item) => (
|
||||
<TabsTrigger
|
||||
data-testid={`reimbursements-tab-${item.value}`}
|
||||
key={item.value}
|
||||
@@ -497,52 +444,144 @@ export function ReimbursementsPage() {
|
||||
</Tabs>
|
||||
|
||||
<div className="ops-command-bar__actions">
|
||||
{showProxyOwnerSelector ? (
|
||||
<select
|
||||
className="select-sm"
|
||||
data-testid="reimbursements-owner-select"
|
||||
onChange={(event) => setProxyOwnerEmployeeId(event.target.value)}
|
||||
value={proxyOwnerEmployeeId}
|
||||
{canApproveExpense ? (
|
||||
<Button
|
||||
data-testid="reimbursements-actionable"
|
||||
size="sm"
|
||||
variant={status === "reviewing" && scope === "all" ? "default" : "secondary"}
|
||||
onClick={() => setSearchParams({ status: "reviewing", page: "1", scope: "all" })}
|
||||
type="button"
|
||||
>
|
||||
<option value="">自己</option>
|
||||
{(proxyDelegationsQuery.data?.received ?? [])
|
||||
.filter((item) => item.status === "active")
|
||||
.map((item) => (
|
||||
<option key={item.id} value={item.principalEmployeeId}>
|
||||
{item.principal?.name ?? item.principalEmployeeId}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
待我处理
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
size="sm"
|
||||
variant={showAdvancedTools ? "default" : "secondary"}
|
||||
onClick={() => setShowLedgerTools((current) => !current)}
|
||||
type="button"
|
||||
>
|
||||
高级 / 台账
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{canViewAll ? (
|
||||
<>
|
||||
<Button
|
||||
data-testid="reimbursements-scope-own"
|
||||
size="sm"
|
||||
variant={scope === "own" ? "default" : "secondary"}
|
||||
onClick={() => setSearchParams({ status, page: "1", scope: "own" })}
|
||||
type="button"
|
||||
>
|
||||
仅自己
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="reimbursements-scope-all"
|
||||
size="sm"
|
||||
variant={scope === "all" ? "default" : "secondary"}
|
||||
onClick={() => setSearchParams({ status, page: "1", scope: "all" })}
|
||||
type="button"
|
||||
>
|
||||
全范围
|
||||
</Button>
|
||||
<Button asChild size="sm" variant="secondary" type="button">
|
||||
<Link to="/reimbursements/dashboard">统计分析</Link>
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
{showAdvancedTools ? (
|
||||
<div className="rounded-lg border border-slate-200 bg-slate-50/70 p-3">
|
||||
<div className="grid gap-3 text-sm md:grid-cols-4">
|
||||
<div>
|
||||
<div className="text-xs text-zinc-500">当前列表</div>
|
||||
<div className="mt-1 font-semibold text-zinc-900">{stats.currentPageCount} 条 / 共 {listQuery.data?.total ?? 0} 条</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-zinc-500">当前金额</div>
|
||||
<div className="mt-1 font-semibold text-zinc-900">{formatCurrency(stats.currentPageAmount)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-zinc-500">审批中</div>
|
||||
<div className={stats.reviewing > 0 ? "mt-1 font-semibold text-amber-700" : "mt-1 font-semibold text-zinc-900"}>{stats.reviewing} 条</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-zinc-500">已支付</div>
|
||||
<div className="mt-1 font-semibold text-zinc-900">{stats.paid} 条</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div className="mt-3 flex flex-wrap items-center gap-2">
|
||||
{showProxyOwnerSelector ? (
|
||||
<select
|
||||
className="select-sm"
|
||||
data-testid="reimbursements-owner-select"
|
||||
onChange={(event) => setProxyOwnerEmployeeId(event.target.value)}
|
||||
value={proxyOwnerEmployeeId}
|
||||
>
|
||||
<option value="">自己</option>
|
||||
{(proxyDelegationsQuery.data?.received ?? [])
|
||||
.filter((item) => item.status === "active")
|
||||
.map((item) => (
|
||||
<option key={item.id} value={item.principalEmployeeId}>
|
||||
{item.principal?.name ?? item.principalEmployeeId}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : null}
|
||||
|
||||
{canCreateExpense ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size="sm" variant="secondary" type="button">
|
||||
更多创建方式
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-80">
|
||||
<div className="px-2 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-zinc-400">
|
||||
从模板创建
|
||||
</div>
|
||||
{templateOptionsQuery.isLoading ? (
|
||||
<DropdownMenuItem disabled>正在加载模板...</DropdownMenuItem>
|
||||
) : templateOptions.length ? (
|
||||
templateOptions.map((template) => (
|
||||
<DropdownMenuItem
|
||||
key={template.id}
|
||||
onSelect={() => startCreateReimbursement({ templateId: template.id })}
|
||||
>
|
||||
{template.name}
|
||||
</DropdownMenuItem>
|
||||
))
|
||||
) : (
|
||||
<DropdownMenuItem disabled>暂无可用模板</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
<div className="px-2 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-zinc-400">
|
||||
关联差旅申请
|
||||
</div>
|
||||
{linkableTravelRequestsQuery.isLoading ? (
|
||||
<DropdownMenuItem disabled>正在加载差旅申请...</DropdownMenuItem>
|
||||
) : linkableTravelRequests.length ? (
|
||||
linkableTravelRequests.map((travelRequest) => (
|
||||
<DropdownMenuItem
|
||||
key={travelRequest.id}
|
||||
onSelect={() => startCreateReimbursement({ linkedTravelRequestId: travelRequest.id })}
|
||||
>
|
||||
{travelRequest.requestNo ?? travelRequest.id} · {travelRequest.purpose}
|
||||
</DropdownMenuItem>
|
||||
))
|
||||
) : (
|
||||
<DropdownMenuItem disabled>暂无可关联的差旅申请</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : null}
|
||||
|
||||
{canViewAll ? (
|
||||
<>
|
||||
<Button
|
||||
data-testid="reimbursements-scope-own"
|
||||
size="sm"
|
||||
variant={scope === "own" ? "default" : "secondary"}
|
||||
onClick={() => setSearchParams({ status, page: "1", scope: "own" })}
|
||||
type="button"
|
||||
>
|
||||
仅自己
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="reimbursements-scope-all"
|
||||
size="sm"
|
||||
variant={scope === "all" ? "default" : "secondary"}
|
||||
onClick={() => setSearchParams({ status, page: "1", scope: "all" })}
|
||||
type="button"
|
||||
>
|
||||
全范围
|
||||
</Button>
|
||||
<Button asChild size="sm" variant="secondary" type="button">
|
||||
<Link to="/reimbursements/dashboard">统计分析</Link>
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
data-testid="reimbursements-export"
|
||||
size="sm"
|
||||
@@ -552,41 +591,42 @@ export function ReimbursementsPage() {
|
||||
>
|
||||
{exportMutation.isPending ? "导出中..." : "导出"}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
data-testid="reimbursements-export-csv"
|
||||
onSelect={() => setExportConfirmFormat("csv")}
|
||||
>
|
||||
导出 CSV
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
data-testid="reimbursements-export-xlsx"
|
||||
onSelect={() => setExportConfirmFormat("xlsx")}
|
||||
>
|
||||
导出 Excel
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
data-testid="reimbursements-export-csv"
|
||||
onSelect={() => setExportConfirmFormat("csv")}
|
||||
>
|
||||
导出 CSV
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
data-testid="reimbursements-export-xlsx"
|
||||
onSelect={() => setExportConfirmFormat("xlsx")}
|
||||
>
|
||||
导出 Excel
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{canShowBatchEntry ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant={batchMode ? "default" : "secondary"}
|
||||
onClick={() => {
|
||||
setBatchMode((current) => !current);
|
||||
setSelectedReimbursementIds([]);
|
||||
setBatchDialogAction(null);
|
||||
setBatchComment("");
|
||||
setBatchError("");
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{batchMode ? "退出批量模式" : "批量操作"}
|
||||
</Button>
|
||||
) : null}
|
||||
{canShowBatchEntry ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant={batchMode ? "default" : "secondary"}
|
||||
onClick={() => {
|
||||
setBatchMode((current) => !current);
|
||||
setSelectedReimbursementIds([]);
|
||||
setBatchDialogAction(null);
|
||||
setBatchComment("");
|
||||
setBatchError("");
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{batchMode ? "退出批量模式" : "批量操作"}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{batchMode ? (
|
||||
<div className="ops-inline-alert border-amber-200 bg-amber-50 text-amber-800">
|
||||
@@ -595,16 +635,6 @@ 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 ? (
|
||||
@@ -810,15 +840,26 @@ export function ReimbursementsPage() {
|
||||
</Link>
|
||||
</Button>
|
||||
{canDeleteDraft ? (
|
||||
<Button
|
||||
disabled={deleteMutation.isPending}
|
||||
onClick={() => handleDeleteDraft(item)}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="destructive"
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
disabled={deleteMutation.isPending}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="secondary"
|
||||
>
|
||||
更多
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
className="text-red-600"
|
||||
onSelect={() => handleDeleteDraft(item)}
|
||||
>
|
||||
删除草稿
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : null}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@@ -36,8 +36,10 @@ test("E2E 公告与团队状态:公告可见 + 今日状态更新 + Portal 汇
|
||||
await page.getByRole("button", { name: "保存状态" }).click();
|
||||
await expect(page.getByText("今日状态已更新")).toBeVisible({ timeout: 30_000 });
|
||||
await page.goto("/portal");
|
||||
await page.locator("summary").filter({ hasText: "团队与通知" }).click();
|
||||
await page.getByRole("tab", { name: "公告" }).click();
|
||||
await expect(page.getByText(announcementTitle)).toBeVisible();
|
||||
await page.locator("summary").filter({ hasText: "更多入口" }).click();
|
||||
await expect(page.locator('[aria-label="portal-card-today_presence"]')).toBeVisible();
|
||||
|
||||
await logout(page, EMPLOYEE.name);
|
||||
|
||||
@@ -24,7 +24,7 @@ type Envelope<T> = {
|
||||
export async function assertPortalLoaded(page: Page) {
|
||||
await expect(page).toHaveURL(/\/portal/);
|
||||
await expect(page.getByRole("link", { name: "Portal" }).first()).toBeVisible();
|
||||
await expect(page.locator('[aria-label^="portal-card-"]').first()).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "今日任务" })).toBeVisible();
|
||||
}
|
||||
|
||||
export async function assertForbiddenPage(page: Page) {
|
||||
|
||||
@@ -512,25 +512,26 @@ test("Portal 员工视角优先显示今日动作顺序", async ({ page }) => {
|
||||
|
||||
await login(page, EMPLOYEE);
|
||||
|
||||
await expect(page.locator('[aria-label^="portal-card-"]').first()).toBeVisible();
|
||||
|
||||
await expect(page.locator('[aria-label^="portal-card-"]').first()).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "今日任务" })).toBeVisible();
|
||||
await expect(page.getByText("当前焦点")).toBeVisible();
|
||||
await expect(page.getByText("下一步")).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "新建报销" })).toBeVisible();
|
||||
|
||||
await page.locator("summary").filter({ hasText: "更多入口" }).click();
|
||||
const labels = await page.locator('[aria-label^="portal-card-"]').evaluateAll((nodes) =>
|
||||
nodes.map((node) => node.getAttribute("aria-label")),
|
||||
);
|
||||
expect(labels).toEqual([
|
||||
expect(labels.slice(0, 4)).toEqual([
|
||||
"portal-card-my_todos",
|
||||
"portal-card-today_presence",
|
||||
"portal-card-my_weekly_report",
|
||||
"portal-card-my_projects",
|
||||
]);
|
||||
|
||||
await expect(page.locator("summary").filter({ hasText: "更多入口" })).toBeVisible();
|
||||
await expect(page.locator("summary").filter({ hasText: "团队与通知" })).toBeVisible();
|
||||
await page.locator("summary").filter({ hasText: "团队与通知" }).click();
|
||||
await expect(page.getByRole("tab", { name: "公告" })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "常用" })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "团队状态" })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "今天先处理" })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "通知" })).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -544,19 +545,21 @@ test("Portal 管理视角重排正确,更多入口默认折叠", async ({ page
|
||||
|
||||
await login(page, MANAGER);
|
||||
|
||||
await expect(page.locator('[aria-label^="portal-card-"]').first()).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "今日任务" })).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "处理待办" })).toBeVisible();
|
||||
|
||||
await page.locator("summary").filter({ hasText: "角色重点" }).click();
|
||||
const labels = await page.locator('[aria-label^="portal-card-"]').evaluateAll((nodes) =>
|
||||
nodes.map((node) => node.getAttribute("aria-label")),
|
||||
);
|
||||
expect(labels).toEqual([
|
||||
expect(labels.slice(0, 4)).toEqual([
|
||||
"portal-card-pending_approvals",
|
||||
"portal-card-team_presence",
|
||||
"portal-card-weekly_missing",
|
||||
"portal-card-weekly_help",
|
||||
]);
|
||||
|
||||
await expect(page.locator("summary").filter({ hasText: "角色重点" })).toBeVisible();
|
||||
await expect(page.locator("summary").filter({ hasText: "团队与通知" })).toBeVisible();
|
||||
await expect(page.getByText("试点观测摘要")).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "采购申请" })).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "差旅申请" })).not.toBeVisible();
|
||||
@@ -636,7 +639,7 @@ test("Portal bootstrap refresh 首次瞬时失败后重试成功仍保留登录
|
||||
await page.goto("/portal");
|
||||
|
||||
await expect(page).toHaveURL(/\/portal/);
|
||||
await expect(page.locator('[aria-label^="portal-card-"]').first()).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "今日任务" })).toBeVisible();
|
||||
expect(refreshAttempts).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
@@ -650,7 +653,9 @@ test("Portal 域管理角色也进入经理视图", async ({ page }) => {
|
||||
|
||||
await login(page, EMPLOYEE);
|
||||
|
||||
await expect(page.getByRole("heading", { name: "今日任务" })).toBeVisible();
|
||||
await expect(page.locator("summary").filter({ hasText: "角色重点" })).toBeVisible();
|
||||
await page.locator("summary").filter({ hasText: "角色重点" }).click();
|
||||
await expect(page.locator('[aria-label="portal-card-pending_approvals"]')).toBeVisible();
|
||||
await expect(page.locator('[aria-label="portal-card-team_presence"]')).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -194,8 +194,10 @@ test("R70 深层自动辅助试运行:周报待办、公告团队状态、真
|
||||
await expect(page.getByText("今日状态已更新")).toBeVisible({ timeout: 30_000 });
|
||||
|
||||
await page.goto("/portal");
|
||||
await page.locator("summary").filter({ hasText: "团队与通知" }).click();
|
||||
await page.getByRole("tab", { name: "公告" }).click();
|
||||
await expect(page.getByText(announcementTitle)).toBeVisible({ timeout: 30_000 });
|
||||
await page.locator("summary").filter({ hasText: "更多入口" }).click();
|
||||
await expect(page.locator('[aria-label="portal-card-today_presence"]')).toBeVisible();
|
||||
return announcementTitle;
|
||||
});
|
||||
|
||||
@@ -316,7 +316,8 @@ test("PC 核心工作流 Wave2 视觉截图", async ({ page }) => {
|
||||
await page.getByTestId("login-password").fill(visualUser.password);
|
||||
await page.getByTestId("login-submit").click();
|
||||
await expect(page).toHaveURL(/\/portal/);
|
||||
await expect(page.locator('[aria-label="portal-card-pending_approvals"]')).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "今日任务" })).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "处理待办" })).toBeVisible();
|
||||
await capture(page, "02-portal");
|
||||
|
||||
await page.goto("/reimbursements?status=reviewing&scope=all");
|
||||
|
||||
@@ -323,7 +323,8 @@ test.describe("R102 reimbursement main flow", () => {
|
||||
await expect(page.getByTestId("reimbursement-row").getByText(formNumber)).toBeVisible();
|
||||
await expect(page.getByText(reimbursementId)).toHaveCount(0);
|
||||
|
||||
await page.getByRole("button", { name: "删除" }).first().click();
|
||||
await page.getByRole("button", { name: "更多" }).first().click();
|
||||
await page.getByRole("menuitem", { name: "删除草稿" }).click();
|
||||
await expect(page.getByTestId("action-confirm-dialog")).toContainText(formNumber);
|
||||
await page.getByTestId("action-confirm-submit").click();
|
||||
await expect(page.getByTestId("reimbursement-row")).toHaveCount(0);
|
||||
|
||||
@@ -120,7 +120,7 @@ test.describe("R105 pretrial visual acceptance", () => {
|
||||
await loginLiveUser(page, "employee");
|
||||
|
||||
await page.goto("/portal");
|
||||
await expect(page.locator('[aria-label^="portal-card-"]').first()).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "今日任务" })).toBeVisible();
|
||||
await expectNoLegacyDesignCopy(page);
|
||||
await page.screenshot({ path: screenshotPath("desktop-portal.png"), fullPage: true });
|
||||
|
||||
|
||||
@@ -277,17 +277,20 @@ test.describe("R7E 全站高风险操作二次确认", () => {
|
||||
await page.goto("/reimbursements");
|
||||
await expect(page.getByTestId("reimbursements-table")).toBeVisible();
|
||||
|
||||
await page.getByTestId("reimbursement-row").getByRole("button", { name: "删除" }).click();
|
||||
await page.getByTestId("reimbursement-row").getByRole("button", { name: "更多" }).click();
|
||||
await page.getByRole("menuitem", { name: "删除草稿" }).click();
|
||||
await expect(page.getByTestId("action-confirm-dialog")).toContainText("删除后无法恢复");
|
||||
expect(calls.deleteReimbursement).toBe(0);
|
||||
|
||||
await page.getByTestId("action-confirm-cancel").click();
|
||||
expect(calls.deleteReimbursement).toBe(0);
|
||||
|
||||
await page.getByTestId("reimbursement-row").getByRole("button", { name: "删除" }).click();
|
||||
await page.getByTestId("reimbursement-row").getByRole("button", { name: "更多" }).click();
|
||||
await page.getByRole("menuitem", { name: "删除草稿" }).click();
|
||||
await page.getByTestId("action-confirm-submit").click();
|
||||
await expect.poll(() => calls.deleteReimbursement).toBe(1);
|
||||
|
||||
await page.getByRole("button", { name: "高级 / 台账" }).click();
|
||||
await page.getByTestId("reimbursements-export").click();
|
||||
await page.getByTestId("reimbursements-export-csv").click();
|
||||
await expect(page.getByTestId("action-confirm-dialog")).toContainText("导出文件可能包含业务或个人信息");
|
||||
|
||||
@@ -20,12 +20,11 @@ test.describe("visual portal", () => {
|
||||
await page.setViewportSize({ width: 1440, height: 1200 });
|
||||
await login(page, EMPLOYEE);
|
||||
|
||||
await expect(page.locator('[aria-label="portal-card-my_todos"]')).toBeVisible();
|
||||
await expect(page.locator('[aria-label="portal-card-today_presence"]')).toBeVisible();
|
||||
await expect(page.locator('[aria-label="portal-card-my_weekly_report"]')).toBeVisible();
|
||||
await expect(page.locator('[aria-label="portal-card-my_projects"]')).toBeVisible();
|
||||
await expect(page.locator("summary").filter({ hasText: "我的申请与资产 / 继续工作" })).toBeVisible();
|
||||
await expect(page.getByRole("tab", { name: "公告" })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "今日任务" })).toBeVisible();
|
||||
await expect(page.getByText("当前焦点")).toBeVisible();
|
||||
await expect(page.getByText("下一步")).toBeVisible();
|
||||
await expect(page.locator("summary").filter({ hasText: "团队与通知" })).toBeVisible();
|
||||
await expect(page.locator("summary").filter({ hasText: "更多入口" })).toBeVisible();
|
||||
|
||||
await attachScreenshot(page, testInfo, "portal-employee.png");
|
||||
});
|
||||
@@ -35,12 +34,11 @@ test.describe("visual portal", () => {
|
||||
await login(page, ADMIN);
|
||||
|
||||
await expect(page.locator('[aria-label="portal-card-pending_approvals"]')).toHaveCount(0);
|
||||
await expect(page.locator('[aria-label="portal-card-team_presence"]')).toBeVisible();
|
||||
await expect(page.locator('[aria-label="portal-card-weekly_missing"]')).toBeVisible();
|
||||
await expect(page.locator('[aria-label="portal-card-weekly_help"]')).toBeVisible();
|
||||
await expect(page.locator("summary").filter({ hasText: "角色重点 / 常用入口" })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "今日任务" })).toBeVisible();
|
||||
await expect(page.getByText("下一步")).toBeVisible();
|
||||
await expect(page.locator("summary").filter({ hasText: "团队与通知" })).toBeVisible();
|
||||
await expect(page.locator("summary").filter({ hasText: "角色重点" })).toBeVisible();
|
||||
await expect(page.getByText("试点观测摘要")).toBeVisible();
|
||||
await expect(page.getByRole("tab", { name: "公告" })).toBeVisible();
|
||||
|
||||
await attachScreenshot(page, testInfo, "portal-admin.png");
|
||||
await logout(page, ADMIN.name);
|
||||
@@ -51,8 +49,8 @@ test.describe("visual portal", () => {
|
||||
await login(page, FINANCE);
|
||||
|
||||
await expect(page.getByText("财务工作台")).toBeVisible();
|
||||
await expect(page.getByText("财务处理")).toBeVisible();
|
||||
await expect(page.locator("summary").filter({ hasText: "角色重点 / 常用入口" })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "今日任务" })).toBeVisible();
|
||||
await expect(page.locator("summary").filter({ hasText: "角色重点" })).toBeVisible();
|
||||
|
||||
await attachScreenshot(page, testInfo, "portal-finance.png");
|
||||
await logout(page, FINANCE.name);
|
||||
|
||||
@@ -334,15 +334,17 @@ test.describe("visual shell", () => {
|
||||
await page.setViewportSize({ width: 1440, height: 1024 });
|
||||
await loginVisualUser(page);
|
||||
|
||||
await expect(page.getByRole("heading", { name: "今天先处理" })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "常用动作" })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "今日任务" })).toBeVisible();
|
||||
await expect(page.getByText("下一步")).toBeVisible();
|
||||
await expect(page.locator("summary").filter({ hasText: "团队与通知" })).toBeVisible();
|
||||
await expect(page.locator("summary").filter({ hasText: /角色重点|更多入口/ })).toBeVisible();
|
||||
await attachScreenshot(page, testInfo, "pc-portal-wave1.png");
|
||||
|
||||
await page.goto("/reimbursements");
|
||||
await expect(page.getByRole("heading", { name: "我的报销" })).toBeVisible();
|
||||
await expect(page.getByTestId("reimbursements-create")).toBeVisible();
|
||||
await expect(page.getByTestId("reimbursements-export")).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "高级 / 台账" })).toBeVisible();
|
||||
await expect(page.getByTestId("reimbursements-export")).not.toBeVisible();
|
||||
await expect(page.getByTestId("reimbursements-status-tabs")).toBeVisible();
|
||||
await expect(page.getByTestId("reimbursements-table")).toBeVisible();
|
||||
await attachScreenshot(page, testInfo, "pc-reimbursements-wave1.png");
|
||||
|
||||
在新工单中引用
屏蔽一个用户