重构移动端轻动作页面(05-30-mobile-light-action-surfaces)

这个提交包含在:
2026-05-30 13:52:20 +08:00
父节点 fe0df2397c
当前提交 695f5c2c75
共修改 8 个文件,包含 138 行新增和 308 行删除
@@ -9,6 +9,7 @@ import {
import { PageHeader } from "../components/common/PageHeader";
import { StatusBadge } from "../components/common/StatusBadge";
import { PageScaffold } from "../components/patterns/PageScaffold";
import { Input } from "../components/ui/input";
import { Tabs, TabsList, TabsTrigger } from "../components/ui/tabs";
import { resolvePurchaseDisplayCode } from "../lib/approvalDisplay";
import { joinBusinessMeta } from "../lib/businessDisplay";
@@ -63,6 +64,29 @@ function resolveMobileEmptyState(status: PurchaseRequestStatus) {
return "暂无已取消采购";
}
function normalizeSearchValue(value: string | null | undefined) {
return String(value ?? "").trim().toLowerCase();
}
function matchesPurchaseSearch(item: PurchaseRequestSummary, keyword: string) {
const searchValue = normalizeSearchValue(keyword);
if (!searchValue) {
return true;
}
const haystack = [
resolvePurchaseDisplayCode(item),
item.title,
item.reason,
item.totalAmount,
item.applicant?.name,
item.supplier?.name,
item.project?.name,
].map(normalizeSearchValue).join(" ");
return haystack.includes(searchValue);
}
function PurchaseRequestCard({
item,
currentMobileSource,
@@ -114,6 +138,7 @@ export function MobilePurchaseRequestsPage() {
const queryStatus = searchParams.get("status");
return isPurchaseStatus(queryStatus) ? queryStatus : "draft";
});
const [searchInput, setSearchInput] = useState("");
const currentMobileSource = useMemo(() => {
const params = new URLSearchParams(location.search);
@@ -138,9 +163,13 @@ export function MobilePurchaseRequestsPage() {
}
const items = listQuery.data?.items ?? [];
const visibleItems = useMemo(
() => items.filter((item) => matchesPurchaseSearch(item, searchInput)),
[items, searchInput],
);
const totalAmount = useMemo(
() => items.reduce((sum, item) => sum + Number(item.totalAmount ?? 0), 0),
[items],
() => visibleItems.reduce((sum, item) => sum + Number(item.totalAmount ?? 0), 0),
[visibleItems],
);
return (
@@ -149,11 +178,11 @@ export function MobilePurchaseRequestsPage() {
<section className="mobile-card">
<div className="text-sm font-semibold text-zinc-900">当前采购任务</div>
<div className="mt-1 text-xs leading-5 text-zinc-500">手机端只看状态、材料摘要和下一步;台账筛选、全范围和导出留在电脑端。</div>
<div className="mt-1 text-xs leading-5 text-zinc-500">先看材料是否齐,再进详情核对采购依据。</div>
<div className="mobile-info-grid">
<div>
<div className="text-xs text-zinc-500">当前列表</div>
<div className="mt-1 text-2xl font-semibold text-zinc-900">{listQuery.data?.total ?? 0}</div>
<div className="mt-1 text-2xl font-semibold text-zinc-900">{visibleItems.length}</div>
</div>
<div>
<div className="text-xs text-zinc-500">当前金额</div>
@@ -163,6 +192,14 @@ export function MobilePurchaseRequestsPage() {
</section>
<section className="mobile-card" data-testid="mobile-purchase-requests-list">
<div className="mobile-command-surface mb-4">
<Input
aria-label="搜索采购"
value={searchInput}
onChange={(event) => setSearchInput(event.target.value)}
placeholder="搜索编号、标题、供应商"
/>
</div>
<Tabs value={status} onValueChange={handleStatusChange}>
<TabsList className="grid h-auto w-full grid-cols-3 gap-1 min-[390px]:grid-cols-5">
{STATUS_TABS.map((item) => (
@@ -180,10 +217,12 @@ export function MobilePurchaseRequestsPage() {
{resolveErrorMessage(listQuery.error)}
</div>
) : null}
{!listQuery.isLoading && !listQuery.isError && !items.length ? (
<div className="mobile-empty-state">{resolveMobileEmptyState(status)}</div>
{!listQuery.isLoading && !listQuery.isError && !visibleItems.length ? (
<div className="mobile-empty-state">
{searchInput.trim() ? "没有符合搜索的采购记录" : resolveMobileEmptyState(status)}
</div>
) : null}
{items.map((item) => (
{visibleItems.map((item) => (
<PurchaseRequestCard currentMobileSource={currentMobileSource} item={item} key={item.id} />
))}
</div>