收口移动端入口与文案(05-30-mobile-confirmed-entry-copy-followups)

这个提交包含在:
2026-05-30 15:13:25 +08:00
父节点 1f25bb706d
当前提交 ba7391eec4
共修改 15 个文件,包含 266 行新增和 99 行删除
+2 -2
查看文件
@@ -1006,7 +1006,7 @@ export function MobileApprovalsPage() {
<div className="mobile-command-surface">
<Tabs value={taskTab} onValueChange={handleTaskTabChange}>
<TabsList className="flex w-full justify-start gap-1">
<TabsTrigger value="pending">待我审批</TabsTrigger>
<TabsTrigger value="pending">待我处理</TabsTrigger>
<TabsTrigger value="completed" disabled={!canUseRemote}>已处理</TabsTrigger>
</TabsList>
</Tabs>
@@ -1062,7 +1062,7 @@ export function MobileApprovalsPage() {
{taskTab === "completed"
? "最近 30 天暂无符合筛选条件的已处理任务。"
: !canAccessRemoteApprovals && isOnline
? "暂无待我审批;收到企业微信审批消息后可从这里查看。"
? "暂无待我处理;收到企业微信审批消息后可从这里查看。"
: canUseRemote
? "暂无符合筛选条件的审批任务。"
: "暂无符合筛选条件的已缓存审批。"}
+81 -46
查看文件
@@ -13,6 +13,7 @@ import {
type LeaveRequestItem,
type LeaveRequestStatus,
type LeaveSession,
type LeaveBalanceItem,
} from "../api/leave";
import { useAuth } from "../auth/AuthContext";
import { hasPermission } from "../auth/permissions";
@@ -156,6 +157,78 @@ function LeaveRecordCard({
);
}
function MobileLeaveBalances({
balances,
isLoading,
isError,
errorMessage,
summary,
}: {
balances: LeaveBalanceItem[];
isLoading: boolean;
isError: boolean;
errorMessage: string;
summary: { available: number; reserved: number; used: number };
}) {
return (
<section className="mobile-card mobile-page-stack" data-testid="mobile-leave-balances">
<div className="mobile-section-header">
<div>
<h3 className="mobile-section-title">假期额度</h3>
<p className="mobile-list-card__subtitle mt-1">企业微信主办,OA 只读展示同步额度</p>
</div>
<span className="badge-success">只读</span>
</div>
<div className="mobile-metric-grid text-sm" data-testid="mobile-leave-balance-summary">
<div>
<div className="text-xs text-zinc-500">可用</div>
<div className="mt-1 text-2xl font-semibold text-zinc-900">{summary.available.toFixed(1)}</div>
</div>
<div>
<div className="text-xs text-zinc-500">冻结</div>
<div className="mt-1 text-2xl font-semibold text-zinc-900">{summary.reserved.toFixed(1)}</div>
</div>
<div>
<div className="text-xs text-zinc-500">已用</div>
<div className="mt-1 text-2xl font-semibold text-zinc-900">{summary.used.toFixed(1)}</div>
</div>
</div>
{isLoading ? <div className="mobile-empty-state">额度加载中...</div> : null}
{isError ? <div className="mobile-empty-state text-red-600">{errorMessage}</div> : null}
{!isLoading && !isError && !balances.length ? <div className="mobile-empty-state">暂无可展示额度</div> : null}
{balances.length ? (
<div className="space-y-2">
{balances.slice(0, 3).map((item) => (
<div key={item.leaveType.id} className="mobile-list-card">
<div className="flex items-center justify-between gap-3">
<div className="font-medium text-zinc-900">{item.leaveType.name}</div>
<span className="badge-neutral">同步</span>
</div>
<div className="mobile-metric-grid mt-3 text-sm">
<div>
<div className="text-xs text-zinc-500">可用</div>
<div className="font-semibold text-zinc-900">{item.available}</div>
</div>
<div>
<div className="text-xs text-zinc-500">冻结</div>
<div className="font-semibold text-zinc-900">{item.reserved}</div>
</div>
<div>
<div className="text-xs text-zinc-500">已用</div>
<div className="font-semibold text-zinc-900">{item.used}</div>
</div>
</div>
</div>
))}
</div>
) : null}
</section>
);
}
export function MobileLeavePage() {
const queryClient = useQueryClient();
const location = useLocation();
@@ -412,25 +485,16 @@ export function MobileLeavePage() {
</Tabs>
</section>
<MobileLeaveBalances
balances={balances}
isLoading={balancesQuery.isLoading}
isError={balancesQuery.isError}
errorMessage={balancesQuery.isError ? resolveErrorMessage(balancesQuery.error) : ""}
summary={balanceSummary}
/>
{effectiveView === "form" ? (
<>
<section className="mobile-card" data-testid="mobile-leave-summary">
<div className="mobile-metric-grid text-sm">
<div>
<div className="text-xs text-zinc-500">可用</div>
<div className="mt-1 text-2xl font-semibold text-zinc-900">{balanceSummary.available.toFixed(1)}</div>
</div>
<div>
<div className="text-xs text-zinc-500">冻结</div>
<div className="mt-1 text-2xl font-semibold text-zinc-900">{balanceSummary.reserved.toFixed(1)}</div>
</div>
<div>
<div className="text-xs text-zinc-500">已用</div>
<div className="mt-1 text-2xl font-semibold text-zinc-900">{balanceSummary.used.toFixed(1)}</div>
</div>
</div>
</section>
<section className="mobile-card" data-testid="mobile-leave-form">
<div className="mb-4 flex items-center justify-between gap-3">
<h3 className="text-base font-semibold text-zinc-900">{editingItem ? "编辑 OA 应急请假" : "新建 OA 应急请假"}</h3>
@@ -533,35 +597,6 @@ export function MobileLeavePage() {
</div>
</section>
<section className="mobile-card" data-testid="mobile-leave-balances">
<h3 className="text-base font-semibold text-zinc-900">额度</h3>
<div className="mt-3 space-y-2">
{balancesQuery.isLoading ? <div className="text-sm text-zinc-500">加载中...</div> : null}
{!balancesQuery.isLoading && !balances.length ? <div className="text-sm text-zinc-500">暂无额度</div> : null}
{balances.slice(0, 3).map((item) => (
<div key={item.leaveType.id} className="mobile-list-card">
<div className="flex items-center justify-between gap-3">
<div className="font-medium text-zinc-900">{item.leaveType.name}</div>
<span className="badge-success">启用</span>
</div>
<div className="mobile-metric-grid mt-3 text-sm">
<div>
<div className="text-xs text-zinc-500">可用</div>
<div className="font-semibold text-zinc-900">{item.available}</div>
</div>
<div>
<div className="text-xs text-zinc-500">冻结</div>
<div className="font-semibold text-zinc-900">{item.reserved}</div>
</div>
<div>
<div className="text-xs text-zinc-500">已用</div>
<div className="font-semibold text-zinc-900">{item.used}</div>
</div>
</div>
</div>
))}
</div>
</section>
</>
) : null}
+2 -4
查看文件
@@ -6,7 +6,7 @@ import { fetchEmployeeProfile } from "../api/employees";
import { mapApiErrorMessage } from "../api/errorMessages";
import { isApiError } from "../api/types";
import { useAuth } from "../auth/AuthContext";
import { hasAnyPermission, hasBusinessApproverRole, hasFinanceRole } from "../auth/permissions";
import { hasAnyPermission, hasBusinessApproverRole } from "../auth/permissions";
import { EmployeeAvatar } from "../components/common/EmployeeAvatar";
import { PageHeader } from "../components/common/PageHeader";
import { MobileIcon } from "../components/mobile/MobileIcon";
@@ -56,7 +56,6 @@ export function MobileMePage() {
const canAccessAssets = hasAnyPermission(permissions, ["asset.view_own", "asset.view_all", "asset.manage", "asset.transfer", "asset.inventory"]);
const canAccessPurchase = hasAnyPermission(permissions, [...MOBILE_PURCHASE_PERMISSIONS]);
const canAccessApprovals = hasBusinessApproverRole(me?.roleCodes, permissions);
const canAccessFinance = hasFinanceRole(me?.roleCodes, permissions);
const profileQuery = useQuery({
enabled: Boolean(me?.id),
@@ -116,8 +115,7 @@ export function MobileMePage() {
<MobileActionLink to="/mobile/leave" label="请假记录" icon="event_available" />
<MobileActionLink to="/mobile/reimbursements" label="报销" icon="request_quote" />
{canAccessPurchase ? <MobileActionLink to="/mobile/purchase-requests" label="采购" icon="shopping_cart" /> : null}
<MobileActionLink to="/mobile/approvals" label={canAccessApprovals ? "待办" : "进度"} icon="fact_check" />
{canAccessFinance ? <MobileActionLink to="/mobile#finance" label="财务" description="审批与付款状态" icon="account_balance_wallet" /> : null}
<MobileActionLink to="/mobile/approvals" label={canAccessApprovals ? "待我处理" : "进度"} icon="fact_check" />
{canAccessAssets ? <MobileActionLink to="/mobile/assets/scan" label="资产扫码" icon="inventory_2" /> : null}
</div>
</SectionPanel>
+19 -31
查看文件
@@ -182,6 +182,10 @@ export function MobilePortalPage() {
.slice(0, 3);
const financeSummary = portalHomeQuery.data?.financeSummary ?? null;
const paymentBatchEnabled = Boolean(financeSummary?.paymentBatchEnabled);
const financePendingCount = financeSummary?.pendingBatchCount ?? 0;
const financeExceptionCount = (financeSummary?.failedItemCount ?? 0) + (financeSummary?.riskCount ?? 0);
const shouldShowFinanceCard = canUseFinance && !portalHomeQuery.isLoading && !portalHomeQuery.isError && (financePendingCount > 0 || financeExceptionCount > 0);
const financeCardBadge = paymentBatchEnabled ? "复核与付款" : financePendingCount > 0 ? "待付款" : "异常与复核";
async function openHomeNotification(item: NotificationItem) {
const targetLink = resolveMobileNotificationTargetLink(item);
@@ -261,7 +265,7 @@ export function MobilePortalPage() {
<div className="mobile-section-header mobile-section-header--top">
<div className="min-w-0">
<div className="mobile-section-title">待处理</div>
<div className="mobile-list-card__subtitle mt-1">待我审批</div>
<div className="mobile-list-card__subtitle mt-1">待我处理</div>
</div>
<div className="text-right">
<div className="text-3xl font-semibold leading-none text-zinc-900" data-testid="mobile-pending-summary-count">
@@ -305,42 +309,26 @@ export function MobilePortalPage() {
</section>
) : null}
{canUseFinance ? (
{shouldShowFinanceCard ? (
<section id="finance" tabIndex={-1} className="mobile-card mobile-page-stack" data-testid="mobile-finance-summary">
<div className="mobile-section-header">
<div className="mobile-section-title">财务</div>
<span className="badge-neutral">{paymentBatchEnabled ? "复核与付款" : "审批与台账"}</span>
<span className="badge-neutral">{financeCardBadge}</span>
</div>
{portalHomeQuery.isLoading ? <p className="text-sm text-zinc-500">读取中...</p> : null}
{portalHomeQuery.isError ? (
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
{resolveErrorMessage(portalHomeQuery.error)}
</p>
) : null}
{!portalHomeQuery.isLoading && !portalHomeQuery.isError && paymentBatchEnabled ? (
<div className="mobile-metric-grid text-sm">
<div className="rounded-lg bg-zinc-50 px-3 py-2">
<div className="text-xs text-zinc-500">待付款</div>
<div className="mt-1 text-xl font-semibold text-zinc-900">{financeSummary?.pendingBatchCount ?? 0}</div>
</div>
<div className="rounded-lg bg-zinc-50 px-3 py-2">
<div className="text-xs text-zinc-500">异常</div>
<div className="mt-1 text-xl font-semibold text-zinc-900">
{(financeSummary?.failedItemCount ?? 0) + (financeSummary?.riskCount ?? 0)}
</div>
</div>
<div className="rounded-lg bg-zinc-50 px-3 py-2">
<div className="text-xs text-zinc-500">电脑端</div>
<div className="mt-1 text-xl font-semibold text-zinc-900">处理台账</div>
</div>
<div className="mobile-metric-grid text-sm">
<div className="rounded-lg bg-zinc-50 px-3 py-2">
<div className="text-xs text-zinc-500">待付款</div>
<div className="mt-1 text-xl font-semibold text-zinc-900">{financePendingCount}</div>
</div>
) : null}
{!portalHomeQuery.isLoading && !portalHomeQuery.isError && !paymentBatchEnabled ? (
<div className="mobile-subcard text-sm text-zinc-600" data-testid="mobile-finance-disabled-state">
<div className="font-medium text-zinc-900">付款批次未启用</div>
<div className="mt-1 leading-5">可查看已流转到财务审批的报销单,待付款和异常批次暂不展示。</div>
<div className="rounded-lg bg-zinc-50 px-3 py-2">
<div className="text-xs text-zinc-500">异常</div>
<div className="mt-1 text-xl font-semibold text-zinc-900">{financeExceptionCount}</div>
</div>
) : null}
<div className="rounded-lg bg-zinc-50 px-3 py-2">
<div className="text-xs text-zinc-500">处理</div>
<div className="mt-1 text-xl font-semibold text-zinc-900">电脑端</div>
</div>
</div>
</section>
) : null}
@@ -193,8 +193,12 @@ export function MobilePurchaseRequestsPage() {
<section className="mobile-card" data-testid="mobile-purchase-requests-list">
<div className="mobile-command-surface mb-4">
<label className="text-xs font-medium text-zinc-500" htmlFor="mobile-purchase-current-search">
当前列表搜索
</label>
<Input
aria-label="搜索采购"
id="mobile-purchase-current-search"
aria-label="当前列表搜索"
value={searchInput}
onChange={(event) => setSearchInput(event.target.value)}
placeholder="搜索编号、标题、供应商"
@@ -293,8 +293,12 @@ export function MobileReimbursementsPage() {
</div>
</div>
<div className="mobile-command-surface">
<label className="text-xs font-medium text-zinc-500" htmlFor="mobile-reimbursements-current-search">
当前列表搜索
</label>
<Input
aria-label="搜索报销"
id="mobile-reimbursements-current-search"
aria-label="当前列表搜索"
value={searchInput}
onChange={(event) => setSearchInput(event.target.value)}
placeholder="搜索编号、说明、金额"
@@ -918,10 +918,14 @@ test.describe("MIF7 移动信息架构整改", () => {
await mobile.page.goto("/mobile/leave?view=records&status=approved");
await expect(mobile.page.getByTestId("mobile-leave-view-records")).toHaveAttribute("data-state", "active");
await expect(mobile.page.getByTestId("mobile-leave-record-card")).toBeVisible();
await expect(mobile.page.getByTestId("mobile-leave-balances")).toBeVisible();
await expect(mobile.page.getByTestId("mobile-leave-balances")).toContainText("企业微信主办");
await expect(mobile.page.getByTestId("mobile-leave-balance-summary")).toContainText("4.0");
await expect(mobile.page.getByTestId("mobile-leave-form")).toHaveCount(0);
await mobile.page.goto("/mobile/purchase-requests?status=approved");
await expect(mobile.page.getByTestId("mobile-purchase-requests-list")).toBeVisible();
await expect(mobile.page.getByLabel("当前列表搜索")).toBeVisible();
await expect(mobile.page.getByTestId("mobile-purchase-request-card")).toBeVisible();
await expect(mobile.page.getByTestId("mobile-purchase-tab-approved")).toHaveAttribute("data-state", "active");
} finally {
@@ -930,7 +934,7 @@ test.describe("MIF7 移动信息架构整改", () => {
}
});
test("审批人首页只保留轻待办摘要,审批页按待我审批和已处理分组", async ({ browser }) => {
test("审批人首页只保留轻待办摘要,审批页按待我处理和已处理分组", async ({ browser }) => {
const notifications = buildNotifications();
const mobile = await newMobilePage(browser, 390);
try {
@@ -986,7 +990,7 @@ test.describe("MIF7 移动信息架构整改", () => {
await mobile.page.getByTestId("mobile-approvals-detail-header").getByRole("button", { name: "返回" }).click();
await expect(mobile.page).toHaveURL(/\/mobile$/);
await mobile.page.goto("/mobile/approvals");
await expect(mobile.page.getByRole("tab", { name: "待我审批" })).toBeVisible();
await expect(mobile.page.getByRole("tab", { name: "待我处理" })).toBeVisible();
await expect(mobile.page.getByRole("tab", { name: "已处理" })).toBeVisible();
await expect(mobile.page.getByTestId("mobile-approvals-queue")).toHaveCount(0);
await mobile.page.getByRole("tab", { name: "已处理" }).click();
@@ -1061,6 +1065,7 @@ test.describe("MIF7 移动信息架构整改", () => {
await loginForMobile(mobile.page, "employee");
await mobile.page.goto("/mobile/reimbursements");
await expect(mobile.page.getByLabel("当前列表搜索")).toBeVisible();
await mobile.page.getByTestId("mobile-reimbursements-tab-approved").click();
await expect(mobile.page).toHaveURL(/\/mobile\/reimbursements\?status=approved$/);
await mobile.page.evaluate(() => {
@@ -1116,16 +1121,18 @@ test.describe("MIF7 移动信息架构整改", () => {
await loginForMobile(financeMobile.page, "finance");
await financeMobile.page.goto("/mobile");
await expect(financeMobile.page.getByTestId("mobile-finance-summary")).toBeVisible();
await expect(financeMobile.page.getByTestId("mobile-finance-summary")).toContainText("付款批次未启用");
await expect(financeMobile.page.getByTestId("mobile-finance-disabled-state")).toBeVisible();
await expect(financeMobile.page.getByTestId("mobile-finance-summary")).toContainText("待付款");
await expect(financeMobile.page.getByTestId("mobile-finance-summary")).toContainText("异常");
await expect(financeMobile.page.getByTestId("mobile-finance-summary")).toContainText("4");
await expect(financeMobile.page.getByTestId("mobile-finance-disabled-state")).toHaveCount(0);
await expectNoMobileShellRegression(financeMobile.page);
await financeMobile.page.screenshot({ path: screenshotPath("home-finance-summary"), fullPage: true });
await financeMobile.page.goto("/mobile/me");
await expect(financeMobile.page.getByRole("link", { name: /财务/ })).toBeVisible();
await financeMobile.page.getByRole("link", { name: /财务/ }).click();
await expect(financeMobile.page.getByRole("link", { name: /财务/ })).toHaveCount(0);
await financeMobile.page.goto("/mobile#finance");
await expect(financeMobile.page).toHaveURL(/\/mobile#finance$/);
await expect(financeMobile.page.getByTestId("mobile-finance-summary")).toBeInViewport();
await expect(financeMobile.page.getByTestId("mobile-finance-disabled-state")).toBeInViewport();
await expect(financeMobile.page.getByTestId("mobile-finance-disabled-state")).toHaveCount(0);
await expectNoMobileShellRegression(financeMobile.page);
} finally {
await financeMobile.context.close();
@@ -493,11 +493,13 @@ test.describe("R124 企业微信移动入口矩阵", () => {
await mobile.page.goto("/mobile");
await expect(mobile.page.getByTestId("mobile-nav-home")).toBeVisible();
await expect(mobile.page.getByTestId("mobile-finance-summary")).toHaveCount(0);
await expectMobilePageContract(mobile.page);
await mobile.page.getByTestId("mobile-nav-reimbursements").click();
await expect(mobile.page).toHaveURL(/\/mobile\/reimbursements$/);
await expect(mobile.page.getByTestId("mobile-reimbursements-list")).toBeVisible();
await expect(mobile.page.getByLabel("当前列表搜索")).toBeVisible();
await expectMobilePageContract(mobile.page);
await mobile.page.goto("/mobile/capture");
@@ -524,7 +526,7 @@ test.describe("R124 企业微信移动入口矩阵", () => {
await mobile.page.getByTestId("mobile-nav-approvals").click();
await expect(mobile.page).toHaveURL(/\/mobile\/approvals$/);
await expect(mobile.page.getByRole("heading", { name: "待办", exact: true })).toBeVisible();
await expect(mobile.page.getByRole("heading", { level: 1, name: "待我处理", exact: true })).toBeVisible();
await expect(mobile.page.locator(".mobile-status-slot--approval").first()).toContainText("待处理");
await expectMobilePageContract(mobile.page);
@@ -539,11 +541,9 @@ test.describe("R124 企业微信移动入口矩阵", () => {
await mobile.page.screenshot({ path: screenshotPath("mobile-me") });
await expectMobilePageContract(mobile.page);
await expect(mobile.page.getByRole("link", { name: /财务/ })).toBeVisible();
await mobile.page.getByRole("link", { name: /财务/ }).click();
await expect(mobile.page).toHaveURL(/\/mobile#finance$/);
await expect(mobile.page.getByTestId("mobile-finance-summary")).toBeInViewport();
await expect(mobile.page.getByTestId("mobile-finance-disabled-state")).toBeInViewport();
await expect(mobile.page.getByRole("link", { name: /财务/ })).toHaveCount(0);
await mobile.page.goto("/mobile#finance");
await expect(mobile.page.getByTestId("mobile-finance-summary")).toHaveCount(0);
await expectMobilePageContract(mobile.page);
await mobile.page.goto("/mobile/me");
@@ -572,12 +572,16 @@ test.describe("R124 企业微信移动入口矩阵", () => {
await mobile.page.getByRole("link", { name: /请假/ }).click();
await expect(mobile.page).toHaveURL(/\/mobile\/leave$/);
await expect(mobile.page.getByTestId("mobile-leave-records")).toBeVisible();
await expect(mobile.page.getByTestId("mobile-leave-balances")).toBeVisible();
await expect(mobile.page.getByTestId("mobile-leave-balances")).toContainText("企业微信主办");
await expect(mobile.page.getByTestId("mobile-leave-balance-summary")).toContainText("4.0");
await expectMobilePageContract(mobile.page);
await mobile.page.goto("/mobile/me");
await mobile.page.getByRole("link", { name: /报销/ }).click();
await expect(mobile.page).toHaveURL(/\/mobile\/reimbursements$/);
await expect(mobile.page.getByTestId("mobile-reimbursements-list")).toBeVisible();
await expect(mobile.page.getByLabel("当前列表搜索")).toBeVisible();
const draftCard = mobile.page.getByTestId("mobile-reimbursement-card").filter({ hasText: "R124 删除草稿" }).first();
await expect(draftCard).toBeVisible();
await draftCard.locator("summary").click();