MUD6 推广移动统一设计
这个提交包含在:
@@ -40,9 +40,10 @@ Default behavior:
|
|||||||
5. Prefer existing project patterns, official SDKs, and principles from documented domestic OA/approval/mobile references; do not copy competitor flows or text directly.
|
5. Prefer existing project patterns, official SDKs, and principles from documented domestic OA/approval/mobile references; do not copy competitor flows or text directly.
|
||||||
6. Verify with the narrowest relevant automated tests, then expand by risk.
|
6. Verify with the narrowest relevant automated tests, then expand by risk.
|
||||||
7. Use Playwright screenshots or visual metrics for PC/mobile/WeCom UI changes. New or changed mobile pages must join the unified responsive system and the 320/360/375/390/393/414/430px 统一视觉矩阵 rather than adding page-specific layout fixes.
|
7. Use Playwright screenshots or visual metrics for PC/mobile/WeCom UI changes. New or changed mobile pages must join the unified responsive system and the 320/360/375/390/393/414/430px 统一视觉矩阵 rather than adding page-specific layout fixes.
|
||||||
8. Update Issue CSV statuses only after the stated `Test_Method` passes.
|
8. Dedicated `/mobile` pages must use the shared mobile slot/card/list/sticky-action system from `docs/product-design-rules.md`: fixed icon/avatar/status slots, `minmax(0, 1fr)` copy slots, `mobile-card`/mobile list cards, mobile tokens, and no desktop `card-std p-4` as a mobile main container.
|
||||||
9. Commit in Chinese batches with Issue IDs.
|
9. Update Issue CSV statuses only after the stated `Test_Method` passes.
|
||||||
10. 面向用户可测试的改动,最终必须明确“是否已部署”。只有部署与公网检查通过后,才能告知用户可以去企业微信或公网试运行环境测试。
|
10. Commit in Chinese batches with Issue IDs.
|
||||||
|
11. 面向用户可测试的改动,最终必须明确“是否已部署”。只有部署与公网检查通过后,才能告知用户可以去企业微信或公网试运行环境测试。
|
||||||
|
|
||||||
## Default Verification Matrix
|
## Default Verification Matrix
|
||||||
|
|
||||||
@@ -104,3 +105,4 @@ Final responses should include:
|
|||||||
- Approval-chain correctness can be proven by PC or automation, while Enterprise WeChat validates entry, binding,免登录, notification, deeplink, and mobile layout.
|
- Approval-chain correctness can be proven by PC or automation, while Enterprise WeChat validates entry, binding,免登录, notification, deeplink, and mobile layout.
|
||||||
- If real-device mobile validation is paused after automation passes, record the gate as `PAUSED`; do not close RS6/UI6/R6 mobile blockers until representative real-device confirmation is recorded.
|
- If real-device mobile validation is paused after automation passes, record the gate as `PAUSED`; do not close RS6/UI6/R6 mobile blockers until representative real-device confirmation is recorded.
|
||||||
- Role-specific pages should use one shared OA/mobile framework and tailor summaries, entries, actions, and sensitive fields by role. Do not create unrelated UI systems for employee, approver, finance, admin, or CEO views.
|
- Role-specific pages should use one shared OA/mobile framework and tailor summaries, entries, actions, and sensitive fields by role. Do not create unrelated UI systems for employee, approver, finance, admin, or CEO views.
|
||||||
|
- After MUD6, `R6-ISSUE-001` is treated as resolved based on representative real-device confirmation; future mobile regressions should be recorded as new Issues rather than reopening the old WCM6/MME6 gate unless the same production behavior returns.
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ export function BarcodeScannerPanel({ disabled = false, onDetected }: BarcodeSca
|
|||||||
}, [startWithBarcodeDetector, startWithZxing, stopScanner]);
|
}, [startWithBarcodeDetector, startWithZxing, stopScanner]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3 rounded-xl border border-zinc-200 p-4" data-testid="barcode-scanner-panel">
|
<div className="mobile-card space-y-3" data-testid="barcode-scanner-panel">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-base font-semibold text-zinc-900">相机扫码</h3>
|
<h3 className="text-base font-semibold text-zinc-900">相机扫码</h3>
|
||||||
|
|||||||
@@ -811,7 +811,7 @@ export function MobileApprovalsPage() {
|
|||||||
<p className="rounded-lg border border-emerald-200 bg-emerald-50 px-3 py-2 text-sm text-emerald-700">{message}</p>
|
<p className="rounded-lg border border-emerald-200 bg-emerald-50 px-3 py-2 text-sm text-emerald-700">{message}</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<section className="card-std space-y-4 p-4" data-testid="mobile-approvals-list">
|
<section className="mobile-card space-y-4" data-testid="mobile-approvals-list">
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-base font-semibold text-zinc-900">{listTitle}</h3>
|
<h3 className="text-base font-semibold text-zinc-900">{listTitle}</h3>
|
||||||
@@ -835,7 +835,7 @@ export function MobileApprovalsPage() {
|
|||||||
placeholder="搜索待办"
|
placeholder="搜索待办"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<details className="rounded-xl border border-zinc-200 bg-zinc-50/70 px-3 py-2 text-sm">
|
<details className="mobile-subcard bg-zinc-50/70 text-sm">
|
||||||
<summary className="cursor-pointer select-none font-medium text-zinc-700">
|
<summary className="cursor-pointer select-none font-medium text-zinc-700">
|
||||||
筛选与批量
|
筛选与批量
|
||||||
</summary>
|
</summary>
|
||||||
@@ -891,7 +891,7 @@ export function MobileApprovalsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{batchMode ? (
|
{batchMode ? (
|
||||||
<div className="rounded-2xl border border-zinc-200 bg-zinc-50 p-3 text-sm">
|
<div className="mobile-subcard bg-zinc-50 text-sm">
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||||
<div className="text-zinc-700">已选 {selectedBatchTaskIds.length} 条</div>
|
<div className="text-zinc-700">已选 {selectedBatchTaskIds.length} 条</div>
|
||||||
<div className="grid w-full gap-2 sm:flex sm:w-auto sm:flex-wrap">
|
<div className="grid w-full gap-2 sm:flex sm:w-auto sm:flex-wrap">
|
||||||
@@ -964,7 +964,7 @@ export function MobileApprovalsPage() {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={task.id}
|
key={task.id}
|
||||||
className={`flex gap-3 rounded-xl border px-3 py-3 ${
|
className={`mobile-list-card flex gap-3 ${
|
||||||
isBatchSelected ? "border-primary bg-primary-50" : "border-zinc-200 bg-white"
|
isBatchSelected ? "border-primary bg-primary-50" : "border-zinc-200 bg-white"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -999,7 +999,7 @@ export function MobileApprovalsPage() {
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={task.id}
|
key={task.id}
|
||||||
className={`min-h-20 w-full rounded-xl border px-3 py-3 text-left transition-colors ${
|
className={`mobile-list-card min-h-20 w-full text-left transition-colors ${
|
||||||
isSelected ? "border-primary bg-primary-50" : "border-zinc-200 bg-white"
|
isSelected ? "border-primary bg-primary-50" : "border-zinc-200 bg-white"
|
||||||
}`}
|
}`}
|
||||||
data-testid={`mobile-approvals-task-${task.id}`}
|
data-testid={`mobile-approvals-task-${task.id}`}
|
||||||
@@ -1022,7 +1022,7 @@ export function MobileApprovalsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section ref={detailSectionRef} className="card-std scroll-mt-4 space-y-3 p-4" data-testid="mobile-approvals-detail">
|
<section ref={detailSectionRef} className="mobile-card scroll-mt-4 space-y-3" data-testid="mobile-approvals-detail">
|
||||||
<h3 className="text-base font-semibold text-zinc-900">处理</h3>
|
<h3 className="text-base font-semibold text-zinc-900">处理</h3>
|
||||||
|
|
||||||
{!selectedTaskId ? <p className="text-sm text-zinc-500">请先选择任务。</p> : null}
|
{!selectedTaskId ? <p className="text-sm text-zinc-500">请先选择任务。</p> : null}
|
||||||
@@ -1045,7 +1045,7 @@ export function MobileApprovalsPage() {
|
|||||||
|
|
||||||
{selectedTask ? (
|
{selectedTask ? (
|
||||||
<>
|
<>
|
||||||
<div className="rounded-lg border border-zinc-200 p-3 text-sm">
|
<div className="mobile-list-card text-sm">
|
||||||
<div>任务状态:{workflowStatusLabel(selectedTask.task.status)}</div>
|
<div>任务状态:{workflowStatusLabel(selectedTask.task.status)}</div>
|
||||||
<div>步骤:{selectedTask.task.stepName}</div>
|
<div>步骤:{selectedTask.task.stepName}</div>
|
||||||
<div>对象类型:{entityLabel(selectedTask.workflowInstance?.entityType)}</div>
|
<div>对象类型:{entityLabel(selectedTask.workflowInstance?.entityType)}</div>
|
||||||
@@ -1105,7 +1105,7 @@ export function MobileApprovalsPage() {
|
|||||||
<p className="text-xs text-zinc-500">离线仅保存通过 / 驳回草稿,联网同步成功后才会正式提交。</p>
|
<p className="text-xs text-zinc-500">离线仅保存通过 / 驳回草稿,联网同步成功后才会正式提交。</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="grid gap-2 sm:grid-cols-3">
|
<div className="grid gap-2 min-[390px]:grid-cols-3">
|
||||||
<Button
|
<Button
|
||||||
className="min-h-12 w-full"
|
className="min-h-12 w-full"
|
||||||
data-testid="mobile-approvals-approve"
|
data-testid="mobile-approvals-approve"
|
||||||
@@ -1150,7 +1150,7 @@ export function MobileApprovalsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{canTransfer ? (
|
{canTransfer ? (
|
||||||
<div className="space-y-3 rounded-lg border border-zinc-200 bg-white p-3 text-sm">
|
<div className="mobile-list-card space-y-3 text-sm">
|
||||||
<p className="font-medium text-zinc-800">转审</p>
|
<p className="font-medium text-zinc-800">转审</p>
|
||||||
<select
|
<select
|
||||||
className="h-10 w-full rounded-lg border border-zinc-300 bg-white px-3 py-2 text-sm"
|
className="h-10 w-full rounded-lg border border-zinc-300 bg-white px-3 py-2 text-sm"
|
||||||
@@ -1192,7 +1192,7 @@ export function MobileApprovalsPage() {
|
|||||||
) : null}
|
) : null}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="card-std space-y-3 p-4" data-testid="mobile-approvals-queue">
|
<section className="mobile-card space-y-3" data-testid="mobile-approvals-queue">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<h3 className="text-base font-semibold text-zinc-900">离线草稿</h3>
|
<h3 className="text-base font-semibold text-zinc-900">离线草稿</h3>
|
||||||
<span className="text-sm text-zinc-500">{currentQueueItems.length ? `待同步 ${currentQueueItems.length}` : "无草稿"}</span>
|
<span className="text-sm text-zinc-500">{currentQueueItems.length ? `待同步 ${currentQueueItems.length}` : "无草稿"}</span>
|
||||||
@@ -1216,7 +1216,7 @@ export function MobileApprovalsPage() {
|
|||||||
{!currentQueueItems.length ? <p className="text-sm text-zinc-500">暂无待同步草稿。</p> : null}
|
{!currentQueueItems.length ? <p className="text-sm text-zinc-500">暂无待同步草稿。</p> : null}
|
||||||
|
|
||||||
{currentQueueItems.map((item) => (
|
{currentQueueItems.map((item) => (
|
||||||
<div key={item.id} className="rounded-lg border border-zinc-200 px-3 py-3 text-sm">
|
<div key={item.id} className="mobile-list-card text-sm">
|
||||||
<div className="font-medium text-zinc-900">
|
<div className="font-medium text-zinc-900">
|
||||||
{publicReference(item.taskSnapshot.reference) ?? entityLabel(item.taskSnapshot.entityType)} · {item.action === "approve" ? "通过" : "驳回"}
|
{publicReference(item.taskSnapshot.reference) ?? entityLabel(item.taskSnapshot.entityType)} · {item.action === "approve" ? "通过" : "驳回"}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { useAuth } from "../auth/AuthContext";
|
|||||||
import { hasPermission } from "../auth/permissions";
|
import { hasPermission } from "../auth/permissions";
|
||||||
import { PageHeader } from "../components/common/PageHeader";
|
import { PageHeader } from "../components/common/PageHeader";
|
||||||
import { BarcodeScannerPanel } from "../components/mobile/BarcodeScannerPanel";
|
import { BarcodeScannerPanel } from "../components/mobile/BarcodeScannerPanel";
|
||||||
|
import { PageScaffold } from "../components/patterns/PageScaffold";
|
||||||
import { Button } from "../components/ui/button";
|
import { Button } from "../components/ui/button";
|
||||||
import { Input } from "../components/ui/input";
|
import { Input } from "../components/ui/input";
|
||||||
import {
|
import {
|
||||||
@@ -394,7 +395,7 @@ export function MobileAssetScanPage() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<PageScaffold className="space-y-4">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={isInventoryMode ? "移动扫码盘点" : "移动资产扫码"}
|
title={isInventoryMode ? "移动扫码盘点" : "移动资产扫码"}
|
||||||
subtitle={isInventoryMode ? "在盘点任务上下文中扫码定位资产并快捷录入结果。" : "扫码或手工输入资产编号,快速查看资产卡片。"}
|
subtitle={isInventoryMode ? "在盘点任务上下文中扫码定位资产并快捷录入结果。" : "扫码或手工输入资产编号,快速查看资产卡片。"}
|
||||||
@@ -410,7 +411,7 @@ export function MobileAssetScanPage() {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{isInventoryMode ? (
|
{isInventoryMode ? (
|
||||||
<div className="card-std space-y-3 p-4" data-testid="mobile-asset-scan-inventory-context">
|
<div className="mobile-card space-y-3" data-testid="mobile-asset-scan-inventory-context">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-base font-semibold text-zinc-900">盘点任务上下文</h3>
|
<h3 className="text-base font-semibold text-zinc-900">盘点任务上下文</h3>
|
||||||
@@ -436,7 +437,7 @@ export function MobileAssetScanPage() {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{selectedInventoryCheck ? (
|
{selectedInventoryCheck ? (
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-zinc-200 px-3 py-3 text-sm">
|
<div className="mobile-list-card flex flex-wrap items-center justify-between gap-3 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium text-zinc-900">{selectedInventoryCheck.checkNo}</div>
|
<div className="font-medium text-zinc-900">{selectedInventoryCheck.checkNo}</div>
|
||||||
<div className="text-zinc-500">状态:{inventoryStatusLabel(selectedInventoryCheck.status)} · 范围:{selectedInventoryCheck.scopeType} / {selectedInventoryCheck.scopeRefId ?? "全量"}</div>
|
<div className="text-zinc-500">状态:{inventoryStatusLabel(selectedInventoryCheck.status)} · 范围:{selectedInventoryCheck.scopeType} / {selectedInventoryCheck.scopeRefId ?? "全量"}</div>
|
||||||
@@ -459,13 +460,13 @@ export function MobileAssetScanPage() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="card-std space-y-3 p-4" data-testid="mobile-asset-scan-search">
|
<div className="mobile-card space-y-3" data-testid="mobile-asset-scan-search">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<h3 className="text-base font-semibold text-zinc-900">手工输入 / 搜索</h3>
|
<h3 className="text-base font-semibold text-zinc-900">手工输入 / 搜索</h3>
|
||||||
<span className={`text-sm ${isOnline ? "text-emerald-600" : "text-orange-600"}`}>{isOnline ? "在线" : "离线"}</span>
|
<span className={`text-sm ${isOnline ? "text-emerald-600" : "text-orange-600"}`}>{isOnline ? "在线" : "离线"}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2 sm:flex-row">
|
<div className="grid gap-2 min-[390px]:grid-cols-[minmax(0,1fr)_auto]">
|
||||||
<Input
|
<Input
|
||||||
data-testid="mobile-asset-scan-input"
|
data-testid="mobile-asset-scan-input"
|
||||||
value={searchInput}
|
value={searchInput}
|
||||||
@@ -483,14 +484,14 @@ export function MobileAssetScanPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{filteredSearchResults.length > 1 ? (
|
{filteredSearchResults.length > 1 ? (
|
||||||
<div className="card-std space-y-3 p-4" data-testid="mobile-asset-scan-candidates">
|
<div className="mobile-card space-y-3" data-testid="mobile-asset-scan-candidates">
|
||||||
<h3 className="text-base font-semibold text-zinc-900">候选资产</h3>
|
<h3 className="text-base font-semibold text-zinc-900">候选资产</h3>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{filteredSearchResults.map((item) => (
|
{filteredSearchResults.map((item) => (
|
||||||
<button
|
<button
|
||||||
key={item.id}
|
key={item.id}
|
||||||
type="button"
|
type="button"
|
||||||
className={`w-full rounded-lg border px-3 py-3 text-left text-sm ${selectedAssetId === item.id ? "border-primary bg-primary-50 text-primary" : "border-zinc-200 bg-white text-zinc-800"}`}
|
className={`mobile-list-card w-full text-left text-sm ${selectedAssetId === item.id ? "border-primary bg-primary-50 text-primary" : "border-zinc-200 bg-white text-zinc-800"}`}
|
||||||
onClick={() => setSelectedAssetId(item.id)}
|
onClick={() => setSelectedAssetId(item.id)}
|
||||||
data-testid={`mobile-asset-scan-candidate-${item.id}`}
|
data-testid={`mobile-asset-scan-candidate-${item.id}`}
|
||||||
>
|
>
|
||||||
@@ -503,7 +504,7 @@ export function MobileAssetScanPage() {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{selectedAsset ? (
|
{selectedAsset ? (
|
||||||
<div className="card-std space-y-4 p-4" data-testid="mobile-asset-scan-detail">
|
<div className="mobile-card space-y-4" data-testid="mobile-asset-scan-detail">
|
||||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-semibold text-zinc-900">{selectedAsset.assetNo}</h3>
|
<h3 className="text-lg font-semibold text-zinc-900">{selectedAsset.assetNo}</h3>
|
||||||
@@ -514,22 +515,22 @@ export function MobileAssetScanPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-3 md:grid-cols-2">
|
<div className="mobile-info-grid">
|
||||||
<div className="rounded-lg border border-zinc-200 px-3 py-2 text-sm">状态:{statusLabel(selectedAsset.status)}</div>
|
<div className="mobile-subcard text-sm">状态:{statusLabel(selectedAsset.status)}</div>
|
||||||
<div className="rounded-lg border border-zinc-200 px-3 py-2 text-sm">法体:{selectedAsset.legalEntity?.name ?? "-"}</div>
|
<div className="mobile-subcard text-sm">法体:{selectedAsset.legalEntity?.name ?? "-"}</div>
|
||||||
<div className="rounded-lg border border-zinc-200 px-3 py-2 text-sm">成本中心:{selectedAsset.costCenter?.name ?? "-"}</div>
|
<div className="mobile-subcard text-sm">成本中心:{selectedAsset.costCenter?.name ?? "-"}</div>
|
||||||
<div className="rounded-lg border border-zinc-200 px-3 py-2 text-sm">位置:{selectedAsset.location ?? "-"}</div>
|
<div className="mobile-subcard text-sm">位置:{selectedAsset.location ?? "-"}</div>
|
||||||
<div className="rounded-lg border border-zinc-200 px-3 py-2 text-sm">归属人:{selectedAsset.assignee?.name ?? "未分配"}</div>
|
<div className="mobile-subcard text-sm">归属人:{selectedAsset.assignee?.name ?? "未分配"}</div>
|
||||||
<div className="rounded-lg border border-zinc-200 px-3 py-2 text-sm">序列号:{selectedAsset.serialNo ?? "-"}</div>
|
<div className="mobile-subcard text-sm">序列号:{selectedAsset.serialNo ?? "-"}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isInventoryMode && canRecordInventory && selectedInventoryCheck ? (
|
{isInventoryMode && canRecordInventory && selectedInventoryCheck ? (
|
||||||
<div className="space-y-3 rounded-xl border border-zinc-200 bg-zinc-50 p-4">
|
<div className="mobile-subcard space-y-3 bg-zinc-50">
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-sm font-semibold text-zinc-900">盘点录入</h4>
|
<h4 className="text-sm font-semibold text-zinc-900">盘点录入</h4>
|
||||||
<p className="text-xs text-zinc-500">仅在盘点中的任务可提交或离线缓存结果。</p>
|
<p className="text-xs text-zinc-500">仅在盘点中的任务可提交或离线缓存结果。</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-3 md:grid-cols-2">
|
<div className="mobile-info-grid">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium text-zinc-700">盘点结果</label>
|
<label className="text-sm font-medium text-zinc-700">盘点结果</label>
|
||||||
<select className="select-std" value={recordResult} onChange={(event) => setRecordResult(event.target.value as InventoryCheckResult)}>
|
<select className="select-std" value={recordResult} onChange={(event) => setRecordResult(event.target.value as InventoryCheckResult)}>
|
||||||
@@ -564,7 +565,7 @@ export function MobileAssetScanPage() {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{isInventoryMode && canRecordInventory ? (
|
{isInventoryMode && canRecordInventory ? (
|
||||||
<div className="card-std space-y-3 p-4" data-testid="mobile-asset-scan-queue">
|
<div className="mobile-card space-y-3" data-testid="mobile-asset-scan-queue">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<h3 className="text-base font-semibold text-zinc-900">离线盘点队列</h3>
|
<h3 className="text-base font-semibold text-zinc-900">离线盘点队列</h3>
|
||||||
<span className="text-sm text-zinc-500">{currentQueueItems.length ? `待同步 ${currentQueueItems.length} 项` : "无待同步项"}</span>
|
<span className="text-sm text-zinc-500">{currentQueueItems.length ? `待同步 ${currentQueueItems.length} 项` : "无待同步项"}</span>
|
||||||
@@ -575,7 +576,7 @@ export function MobileAssetScanPage() {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{currentQueueItems.map((item) => (
|
{currentQueueItems.map((item) => (
|
||||||
<div key={item.id} className="rounded-lg border border-zinc-200 px-3 py-3 text-sm">
|
<div key={item.id} className="mobile-list-card text-sm">
|
||||||
<div className="font-medium text-zinc-900">{item.assetNo}</div>
|
<div className="font-medium text-zinc-900">{item.assetNo}</div>
|
||||||
<div className="text-zinc-500">结果:{inventoryResultLabel(item.result)} · 创建时间:{formatDateTime(item.createdAt)}</div>
|
<div className="text-zinc-500">结果:{inventoryResultLabel(item.result)} · 创建时间:{formatDateTime(item.createdAt)}</div>
|
||||||
<div className="text-zinc-500">备注:{item.note ?? "-"}</div>
|
<div className="text-zinc-500">备注:{item.note ?? "-"}</div>
|
||||||
@@ -597,6 +598,6 @@ export function MobileAssetScanPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</PageScaffold>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ export function MobileCapturePage() {
|
|||||||
<p className="rounded-lg border border-emerald-200 bg-emerald-50 px-3 py-2 text-sm text-emerald-700">{message}</p>
|
<p className="rounded-lg border border-emerald-200 bg-emerald-50 px-3 py-2 text-sm text-emerald-700">{message}</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="card-std space-y-4 p-4">
|
<div className="mobile-card space-y-4">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-base font-semibold text-zinc-900">选择票据</h3>
|
<h3 className="text-base font-semibold text-zinc-900">选择票据</h3>
|
||||||
@@ -200,10 +200,10 @@ export function MobileCapturePage() {
|
|||||||
<div className="grid gap-2" data-testid="mobile-capture-file">
|
<div className="grid gap-2" data-testid="mobile-capture-file">
|
||||||
<label
|
<label
|
||||||
htmlFor="mobile-capture-pdf-file"
|
htmlFor="mobile-capture-pdf-file"
|
||||||
className="flex min-h-16 cursor-pointer items-center gap-3 rounded-xl border border-zinc-200 bg-white px-4 py-3 text-left shadow-sm transition-colors hover:bg-zinc-50"
|
className="mobile-action-link min-h-16 cursor-pointer"
|
||||||
>
|
>
|
||||||
<MobileIcon name="picture_as_pdf" className="size-10 rounded-lg bg-emerald-50 p-2 text-emerald-700" />
|
<MobileIcon name="picture_as_pdf" className="mobile-action-icon bg-emerald-50 text-emerald-700" />
|
||||||
<span className="min-w-0">
|
<span className="mobile-action-copy">
|
||||||
<span className="block text-sm font-semibold text-zinc-900">选择 PDF 发票</span>
|
<span className="block text-sm font-semibold text-zinc-900">选择 PDF 发票</span>
|
||||||
<span className="mt-0.5 block text-xs leading-5 text-zinc-500">数电票或扫描件 PDF</span>
|
<span className="mt-0.5 block text-xs leading-5 text-zinc-500">数电票或扫描件 PDF</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -221,7 +221,7 @@ export function MobileCapturePage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-xl border border-zinc-200 bg-white px-3 py-3 text-sm text-zinc-600">
|
<div className="mobile-subcard text-sm text-zinc-600">
|
||||||
<span className="font-medium text-zinc-800">当前文件:</span>
|
<span className="font-medium text-zinc-800">当前文件:</span>
|
||||||
{selectedFiles.length ? `已选择 ${selectedFiles.length} 个` : "未选择"}
|
{selectedFiles.length ? `已选择 ${selectedFiles.length} 个` : "未选择"}
|
||||||
{selectedFiles.length ? (
|
{selectedFiles.length ? (
|
||||||
@@ -282,7 +282,7 @@ export function MobileCapturePage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="card-std space-y-3 p-4" data-testid="mobile-capture-queue">
|
<div className="mobile-card space-y-3" data-testid="mobile-capture-queue">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<h3 className="text-base font-semibold text-zinc-900">离线上传队列</h3>
|
<h3 className="text-base font-semibold text-zinc-900">离线上传队列</h3>
|
||||||
<span className="shrink-0 text-sm text-zinc-500">{queuedCountLabel}</span>
|
<span className="shrink-0 text-sm text-zinc-500">{queuedCountLabel}</span>
|
||||||
@@ -295,7 +295,7 @@ export function MobileCapturePage() {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{queuedItems.map((item) => (
|
{queuedItems.map((item) => (
|
||||||
<div key={item.id} className="rounded-xl border border-zinc-200 bg-white px-3 py-3 text-sm">
|
<div key={item.id} className="mobile-list-card text-sm">
|
||||||
<div className="font-medium text-zinc-800">{item.fileName}</div>
|
<div className="font-medium text-zinc-800">{item.fileName}</div>
|
||||||
<div className="text-zinc-500">创建时间:{formatTime(item.createdAt)}</div>
|
<div className="text-zinc-500">创建时间:{formatTime(item.createdAt)}</div>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
|
|||||||
@@ -376,7 +376,7 @@ export function MobileLeavePage() {
|
|||||||
{balancesQuery.isLoading ? <div className="text-sm text-zinc-500">加载中...</div> : null}
|
{balancesQuery.isLoading ? <div className="text-sm text-zinc-500">加载中...</div> : null}
|
||||||
{!balancesQuery.isLoading && !balances.length ? <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) => (
|
{balances.slice(0, 3).map((item) => (
|
||||||
<div key={item.leaveType.id} className="rounded-lg border border-zinc-200 p-3">
|
<div key={item.leaveType.id} className="mobile-list-card">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div className="font-medium text-zinc-900">{item.leaveType.name}</div>
|
<div className="font-medium text-zinc-900">{item.leaveType.name}</div>
|
||||||
<span className="badge-success">启用</span>
|
<span className="badge-success">启用</span>
|
||||||
@@ -413,14 +413,14 @@ export function MobileLeavePage() {
|
|||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
<div className="mt-4 space-y-3">
|
<div className="mt-4 space-y-3">
|
||||||
{requestsQuery.isLoading ? <div className="rounded-lg border border-dashed border-zinc-200 p-6 text-center text-sm text-zinc-500">加载中...</div> : null}
|
{requestsQuery.isLoading ? <div className="mobile-empty-state">加载中...</div> : null}
|
||||||
{requestsQuery.isError ? (
|
{requestsQuery.isError ? (
|
||||||
<div className="rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-600">
|
<div className="rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-600">
|
||||||
{resolveErrorMessage(requestsQuery.error)}
|
{resolveErrorMessage(requestsQuery.error)}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{!requestsQuery.isLoading && !requestsQuery.isError && !records.length ? (
|
{!requestsQuery.isLoading && !requestsQuery.isError && !records.length ? (
|
||||||
<div className="rounded-lg border border-dashed border-zinc-200 p-6 text-center text-sm text-zinc-500">暂无请假记录</div>
|
<div className="mobile-empty-state">暂无请假记录</div>
|
||||||
) : null}
|
) : null}
|
||||||
{records.map((item) => (
|
{records.map((item) => (
|
||||||
<LeaveRecordCard
|
<LeaveRecordCard
|
||||||
|
|||||||
@@ -38,11 +38,11 @@ function MobileActionLink({
|
|||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
to={to}
|
to={to}
|
||||||
className="mobile-me-action-link"
|
className="mobile-action-link mobile-me-action-link"
|
||||||
data-testid="mobile-me-action-link"
|
data-testid="mobile-me-action-link"
|
||||||
>
|
>
|
||||||
<MobileIcon name={icon} className="mobile-me-action-icon" />
|
<MobileIcon name={icon} className="mobile-action-icon mobile-me-action-icon" />
|
||||||
<span className="mobile-me-action-copy">
|
<span className="mobile-action-copy mobile-me-action-copy">
|
||||||
<span className="block text-sm font-semibold text-zinc-900">{label}</span>
|
<span className="block text-sm font-semibold text-zinc-900">{label}</span>
|
||||||
{description ? <span className="mt-0.5 block text-xs leading-5 text-zinc-500">{description}</span> : null}
|
{description ? <span className="mt-0.5 block text-xs leading-5 text-zinc-500">{description}</span> : null}
|
||||||
</span>
|
</span>
|
||||||
@@ -82,11 +82,11 @@ export function MobileMePage() {
|
|||||||
<PageHeader title="我的" />
|
<PageHeader title="我的" />
|
||||||
|
|
||||||
<SectionPanel title="个人信息">
|
<SectionPanel title="个人信息">
|
||||||
<div className="mobile-me-profile-row" data-testid="mobile-me-profile-row">
|
<div className="mobile-slot-row mobile-me-profile-row" data-testid="mobile-me-profile-row">
|
||||||
<div className="mobile-me-avatar-slot" data-testid="mobile-me-avatar-slot">
|
<div className="mobile-slot-avatar mobile-me-avatar-slot" data-testid="mobile-me-avatar-slot">
|
||||||
<EmployeeAvatar name={me?.name ?? "员工"} src={profile?.avatarUrl} size="md" />
|
<EmployeeAvatar name={me?.name ?? "员工"} src={profile?.avatarUrl} size="md" />
|
||||||
</div>
|
</div>
|
||||||
<div className="mobile-me-profile-copy" data-testid="mobile-me-profile-copy">
|
<div className="mobile-slot-copy mobile-me-profile-copy" data-testid="mobile-me-profile-copy">
|
||||||
<div className="text-lg font-semibold leading-7 text-zinc-900" data-testid="mobile-me-profile-name">
|
<div className="text-lg font-semibold leading-7 text-zinc-900" data-testid="mobile-me-profile-name">
|
||||||
{me?.name ?? "-"}
|
{me?.name ?? "-"}
|
||||||
</div>
|
</div>
|
||||||
@@ -113,7 +113,7 @@ export function MobileMePage() {
|
|||||||
</SectionPanel>
|
</SectionPanel>
|
||||||
|
|
||||||
<SectionPanel title="更多入口">
|
<SectionPanel title="更多入口">
|
||||||
<div className="mobile-me-entry-grid" data-testid="mobile-more-links">
|
<div className="mobile-entry-grid mobile-me-entry-grid" data-testid="mobile-more-links">
|
||||||
<MobileActionLink to="/mobile/notifications" label="通知中心" icon="notifications" />
|
<MobileActionLink to="/mobile/notifications" label="通知中心" icon="notifications" />
|
||||||
<MobileActionLink to="/mobile/leave" label="请假" icon="event_available" />
|
<MobileActionLink to="/mobile/leave" label="请假" icon="event_available" />
|
||||||
<MobileActionLink to="/mobile/reimbursements" label="报销" icon="request_quote" />
|
<MobileActionLink to="/mobile/reimbursements" label="报销" icon="request_quote" />
|
||||||
@@ -125,7 +125,7 @@ export function MobileMePage() {
|
|||||||
|
|
||||||
{canAccessAdmin ? (
|
{canAccessAdmin ? (
|
||||||
<SectionPanel title="管理入口">
|
<SectionPanel title="管理入口">
|
||||||
<div className="mobile-me-entry-grid">
|
<div className="mobile-entry-grid mobile-me-entry-grid">
|
||||||
<MobileActionLink to="/mobile/me" label="人员管理" icon="group" />
|
<MobileActionLink to="/mobile/me" label="人员管理" icon="group" />
|
||||||
<MobileActionLink to="/mobile/me" label="组织架构" icon="account_tree" />
|
<MobileActionLink to="/mobile/me" label="组织架构" icon="account_tree" />
|
||||||
<MobileActionLink to="/mobile/me" label="系统设置" icon="settings" />
|
<MobileActionLink to="/mobile/me" label="系统设置" icon="settings" />
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ export function MobileNotificationsPage() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<section className="card-std space-y-4 p-4" data-testid="mobile-notifications-list">
|
<section className="mobile-card space-y-4" data-testid="mobile-notifications-list">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<Tabs
|
<Tabs
|
||||||
data-testid="mobile-notifications-filter-tabs"
|
data-testid="mobile-notifications-filter-tabs"
|
||||||
@@ -160,7 +160,7 @@ export function MobileNotificationsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{listQuery.isLoading ? (
|
{listQuery.isLoading ? (
|
||||||
<div className="rounded-lg border border-dashed border-zinc-200 p-6 text-center text-sm text-zinc-500">加载中...</div>
|
<div className="mobile-empty-state">加载中...</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{listQuery.isError ? (
|
{listQuery.isError ? (
|
||||||
@@ -170,13 +170,13 @@ export function MobileNotificationsPage() {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{!listQuery.isLoading && !listQuery.isError && !notificationItems.length ? (
|
{!listQuery.isLoading && !listQuery.isError && !notificationItems.length ? (
|
||||||
<div className="rounded-lg border border-dashed border-zinc-200 p-6 text-center text-sm text-zinc-500">暂无通知</div>
|
<div className="mobile-empty-state">暂无通知</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{notificationItems.map((item) => {
|
{notificationItems.map((item) => {
|
||||||
const targetLink = resolveMobileNotificationTarget(item);
|
const targetLink = resolveMobileNotificationTarget(item);
|
||||||
return (
|
return (
|
||||||
<article data-testid="mobile-notification-card" key={item.id} className="rounded-lg border border-zinc-200 bg-white p-4">
|
<article data-testid="mobile-notification-card" key={item.id} className="mobile-list-card">
|
||||||
<div className="flex items-start justify-between gap-3">
|
<div className="flex items-start justify-between gap-3">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="text-sm font-semibold text-zinc-900">{item.title}</div>
|
<div className="text-sm font-semibold text-zinc-900">{item.title}</div>
|
||||||
|
|||||||
@@ -124,20 +124,20 @@ export function MobileProfilePage() {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<SectionPanel title="基本信息">
|
<SectionPanel title="基本信息">
|
||||||
<dl className="grid gap-3 text-sm">
|
<dl className="mobile-info-grid text-sm">
|
||||||
<div className="rounded-lg border border-zinc-200 bg-white px-3 py-3">
|
<div className="mobile-subcard">
|
||||||
<dt className="text-xs text-zinc-500">姓名</dt>
|
<dt className="text-xs text-zinc-500">姓名</dt>
|
||||||
<dd className="mt-1 font-medium text-zinc-900">{me?.name ?? "-"}</dd>
|
<dd className="mt-1 font-medium text-zinc-900">{me?.name ?? "-"}</dd>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-lg border border-zinc-200 bg-white px-3 py-3">
|
<div className="mobile-subcard">
|
||||||
<dt className="text-xs text-zinc-500">员工号</dt>
|
<dt className="text-xs text-zinc-500">员工号</dt>
|
||||||
<dd className="mt-1 font-medium text-zinc-900">{me?.employeeNo ?? "-"}</dd>
|
<dd className="mt-1 font-medium text-zinc-900">{me?.employeeNo ?? "-"}</dd>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-lg border border-zinc-200 bg-white px-3 py-3">
|
<div className="mobile-subcard">
|
||||||
<dt className="text-xs text-zinc-500">邮箱</dt>
|
<dt className="text-xs text-zinc-500">邮箱</dt>
|
||||||
<dd className="mt-1 break-words font-medium text-zinc-900">{me?.email ?? "-"}</dd>
|
<dd className="mt-1 break-words font-medium text-zinc-900">{me?.email ?? "-"}</dd>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-lg border border-zinc-200 bg-white px-3 py-3">
|
<div className="mobile-subcard">
|
||||||
<dt className="text-xs text-zinc-500">手机号</dt>
|
<dt className="text-xs text-zinc-500">手机号</dt>
|
||||||
<dd className="mt-1 font-medium text-zinc-900">{me?.phone ?? "-"}</dd>
|
<dd className="mt-1 font-medium text-zinc-900">{me?.phone ?? "-"}</dd>
|
||||||
</div>
|
</div>
|
||||||
@@ -145,7 +145,7 @@ export function MobileProfilePage() {
|
|||||||
</SectionPanel>
|
</SectionPanel>
|
||||||
|
|
||||||
<SectionPanel title="外部通知推送">
|
<SectionPanel title="外部通知推送">
|
||||||
<label className="flex min-h-12 items-center justify-between gap-3 rounded-lg border border-zinc-200 bg-white px-3 py-3 text-sm text-zinc-800">
|
<label className="mobile-list-card flex min-h-12 items-center justify-between gap-3 text-sm text-zinc-800">
|
||||||
<span>接收外部渠道推送</span>
|
<span>接收外部渠道推送</span>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -159,13 +159,13 @@ export function MobileProfilePage() {
|
|||||||
|
|
||||||
<SectionPanel title="企业微信绑定">
|
<SectionPanel title="企业微信绑定">
|
||||||
<div className="space-y-3" data-testid="mobile-profile-wecom-card">
|
<div className="space-y-3" data-testid="mobile-profile-wecom-card">
|
||||||
<div className="rounded-lg border border-zinc-200 bg-white px-3 py-3">
|
<div className="mobile-subcard">
|
||||||
<div className="text-xs text-zinc-500">接入状态</div>
|
<div className="text-xs text-zinc-500">接入状态</div>
|
||||||
<div className="mt-1 font-semibold text-zinc-900">{wecomStatus.label}</div>
|
<div className="mt-1 font-semibold text-zinc-900">{wecomStatus.label}</div>
|
||||||
<div className="mt-1 text-sm leading-5 text-zinc-500">{wecomStatus.description}</div>
|
<div className="mt-1 text-sm leading-5 text-zinc-500">{wecomStatus.description}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-lg border border-zinc-200 bg-white px-3 py-3">
|
<div className="mobile-subcard">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs text-zinc-500">当前绑定</div>
|
<div className="text-xs text-zinc-500">当前绑定</div>
|
||||||
@@ -181,7 +181,7 @@ export function MobileProfilePage() {
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="mobile-two-action-grid">
|
||||||
<Button
|
<Button
|
||||||
className="w-full"
|
className="w-full"
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -880,7 +880,7 @@ export function MobileReimbursementDetailPage() {
|
|||||||
<div className="mt-3 space-y-2">
|
<div className="mt-3 space-y-2">
|
||||||
{detail.items.length ? (
|
{detail.items.length ? (
|
||||||
detail.items.map((item) => (
|
detail.items.map((item) => (
|
||||||
<article key={item.id} className="rounded-lg border border-zinc-200 px-3 py-3">
|
<article key={item.id} className="mobile-list-card">
|
||||||
<div className="flex items-start justify-between gap-3">
|
<div className="flex items-start justify-between gap-3">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="truncate text-sm font-semibold text-zinc-900">{item.expenseType?.name ?? "费用"}</div>
|
<div className="truncate text-sm font-semibold text-zinc-900">{item.expenseType?.name ?? "费用"}</div>
|
||||||
@@ -934,7 +934,7 @@ export function MobileReimbursementDetailPage() {
|
|||||||
{detail.invoices.length ? (
|
{detail.invoices.length ? (
|
||||||
<div className="mt-3 space-y-2">
|
<div className="mt-3 space-y-2">
|
||||||
{detail.invoices.map((invoice) => (
|
{detail.invoices.map((invoice) => (
|
||||||
<article key={invoice.id} className="rounded-lg border border-zinc-200 px-3 py-3">
|
<article key={invoice.id} className="mobile-list-card">
|
||||||
<div className="flex items-start justify-between gap-3">
|
<div className="flex items-start justify-between gap-3">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="truncate text-sm font-semibold text-zinc-900">{formatInvoiceTitle(invoice)}</div>
|
<div className="truncate text-sm font-semibold text-zinc-900">{formatInvoiceTitle(invoice)}</div>
|
||||||
@@ -1025,7 +1025,7 @@ export function MobileReimbursementDetailPage() {
|
|||||||
{(detail.attachments ?? []).length ? (
|
{(detail.attachments ?? []).length ? (
|
||||||
<div className="mt-3 space-y-2">
|
<div className="mt-3 space-y-2">
|
||||||
{(detail.attachments ?? []).map((attachment) => (
|
{(detail.attachments ?? []).map((attachment) => (
|
||||||
<article key={attachment.id} className="rounded-lg border border-zinc-200 px-3 py-3">
|
<article key={attachment.id} className="mobile-list-card">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="truncate text-sm font-semibold text-zinc-900">{attachment.originalName}</div>
|
<div className="truncate text-sm font-semibold text-zinc-900">{attachment.originalName}</div>
|
||||||
<div className="mt-1 text-xs text-zinc-500">
|
<div className="mt-1 text-xs text-zinc-500">
|
||||||
|
|||||||
@@ -199,14 +199,14 @@ export function MobileReimbursementsPage() {
|
|||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
<div className="mt-4 space-y-3">
|
<div className="mt-4 space-y-3">
|
||||||
{listQuery.isLoading ? <div className="rounded-lg border border-dashed border-zinc-200 p-6 text-center text-sm text-zinc-500">加载中...</div> : null}
|
{listQuery.isLoading ? <div className="mobile-empty-state">加载中...</div> : null}
|
||||||
{listQuery.isError ? (
|
{listQuery.isError ? (
|
||||||
<div className="rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-600">
|
<div className="rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-600">
|
||||||
{resolveErrorMessage(listQuery.error)}
|
{resolveErrorMessage(listQuery.error)}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{!listQuery.isLoading && !listQuery.isError && !items.length ? (
|
{!listQuery.isLoading && !listQuery.isError && !items.length ? (
|
||||||
<div className="rounded-lg border border-dashed border-zinc-200 p-6 text-center text-sm text-zinc-500">暂无报销记录</div>
|
<div className="mobile-empty-state">暂无报销记录</div>
|
||||||
) : null}
|
) : null}
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<ReimbursementCard
|
<ReimbursementCard
|
||||||
|
|||||||
@@ -16,8 +16,10 @@
|
|||||||
--mobile-bottom-nav-height: 68px;
|
--mobile-bottom-nav-height: 68px;
|
||||||
--mobile-sticky-action-height: 78px;
|
--mobile-sticky-action-height: 78px;
|
||||||
--mobile-icon-action-min-height: 72px;
|
--mobile-icon-action-min-height: 72px;
|
||||||
--mobile-me-slot-gap: clamp(12px, 3.7vw, 16px);
|
--mobile-slot-gap: clamp(12px, 3.7vw, 16px);
|
||||||
--mobile-me-action-icon-slot: 36px;
|
--mobile-action-icon-slot: 36px;
|
||||||
|
--mobile-me-slot-gap: var(--mobile-slot-gap);
|
||||||
|
--mobile-me-action-icon-slot: var(--mobile-action-icon-slot);
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-scaffold.mobile-fixed-action-page {
|
.page-scaffold.mobile-fixed-action-page {
|
||||||
@@ -119,6 +121,7 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mobile-entry-grid,
|
||||||
.mobile-me-entry-grid {
|
.mobile-me-entry-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
@@ -126,14 +129,16 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mobile-slot-row,
|
||||||
.mobile-me-profile-row {
|
.mobile-me-profile-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 44px minmax(0, 1fr);
|
grid-template-columns: 44px minmax(0, 1fr);
|
||||||
column-gap: var(--mobile-me-slot-gap);
|
column-gap: var(--mobile-slot-gap);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mobile-slot-avatar,
|
||||||
.mobile-me-avatar-slot {
|
.mobile-me-avatar-slot {
|
||||||
width: 44px;
|
width: 44px;
|
||||||
min-width: 44px;
|
min-width: 44px;
|
||||||
@@ -142,10 +147,12 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mobile-slot-copy,
|
||||||
.mobile-me-profile-copy {
|
.mobile-me-profile-copy {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mobile-slot-copy > *,
|
||||||
.mobile-me-profile-copy > * {
|
.mobile-me-profile-copy > * {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -153,10 +160,11 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mobile-action-link,
|
||||||
.mobile-me-action-link {
|
.mobile-me-action-link {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: var(--mobile-me-action-icon-slot) minmax(0, 1fr);
|
grid-template-columns: var(--mobile-action-icon-slot) minmax(0, 1fr);
|
||||||
column-gap: var(--mobile-me-slot-gap);
|
column-gap: var(--mobile-slot-gap);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 48px;
|
min-height: 48px;
|
||||||
@@ -169,30 +177,35 @@
|
|||||||
transition: background-color 150ms ease, border-color 150ms ease;
|
transition: background-color 150ms ease, border-color 150ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mobile-action-link:hover,
|
||||||
.mobile-me-action-link:hover {
|
.mobile-me-action-link:hover {
|
||||||
background: rgb(250 250 250);
|
background: rgb(250 250 250);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mobile-action-icon,
|
||||||
.mobile-me-action-icon {
|
.mobile-me-action-icon {
|
||||||
width: var(--mobile-me-action-icon-slot);
|
width: var(--mobile-action-icon-slot);
|
||||||
height: var(--mobile-me-action-icon-slot);
|
height: var(--mobile-action-icon-slot);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
background: rgb(244 244 245);
|
background: rgb(244 244 245);
|
||||||
color: rgb(63 63 70);
|
color: rgb(63 63 70);
|
||||||
padding: 7px;
|
padding: 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mobile-action-icon svg,
|
||||||
.mobile-me-action-icon svg {
|
.mobile-me-action-icon svg {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mobile-action-copy,
|
||||||
.mobile-me-action-copy {
|
.mobile-me-action-copy {
|
||||||
display: block;
|
display: block;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mobile-action-copy > *,
|
||||||
.mobile-me-action-copy > * {
|
.mobile-me-action-copy > * {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -200,6 +213,34 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mobile-list-card {
|
||||||
|
min-width: 0;
|
||||||
|
border: 1px solid rgb(228 228 231);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgb(255 255 255);
|
||||||
|
box-shadow: 0 1px 2px rgb(15 23 42 / 0.05);
|
||||||
|
padding: var(--mobile-card-padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-subcard {
|
||||||
|
min-width: 0;
|
||||||
|
border: 1px solid rgb(228 228 231);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgb(255 255 255);
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-empty-state {
|
||||||
|
min-width: 0;
|
||||||
|
border: 1px dashed rgb(228 228 231);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px 12px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.25rem;
|
||||||
|
color: rgb(113 113 122);
|
||||||
|
}
|
||||||
|
|
||||||
.mobile-info-grid {
|
.mobile-info-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 7.5rem), 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(min(100%, 7.5rem), 1fr));
|
||||||
@@ -238,8 +279,8 @@
|
|||||||
--mobile-card-gap: 6px;
|
--mobile-card-gap: 6px;
|
||||||
--mobile-card-padding: 10px;
|
--mobile-card-padding: 10px;
|
||||||
--mobile-icon-action-min-height: 68px;
|
--mobile-icon-action-min-height: 68px;
|
||||||
--mobile-me-slot-gap: 12px;
|
--mobile-slot-gap: 12px;
|
||||||
--mobile-me-action-icon-slot: 34px;
|
--mobile-action-icon-slot: 34px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-icon-action__icon {
|
.mobile-icon-action__icon {
|
||||||
|
|||||||
@@ -0,0 +1,529 @@
|
|||||||
|
import { mkdirSync } from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { expect, test, type Page, type Route } from "@playwright/test";
|
||||||
|
|
||||||
|
const ROOT_DIR = path.resolve(process.cwd(), "..");
|
||||||
|
const SCREENSHOT_DIR = path.join(ROOT_DIR, "output/playwright/r6-mobile-unified-design-rollout");
|
||||||
|
|
||||||
|
const IOS_WECOM_UA =
|
||||||
|
"Mozilla/5.0 (iPhone; CPU iPhone OS 14_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 wxwork/4.1.20 MicroMessenger/8.0.1 Language/zh_CN";
|
||||||
|
const ANDROID_WECOM_UA =
|
||||||
|
"Mozilla/5.0 (Linux; Android 14; Redmi K60 Build/UKQ1.230804.001) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/124.0.0.0 Mobile Safari/537.36 wxwork/4.1.20 MicroMessenger/8.0.1 Language/zh_CN";
|
||||||
|
const MOBILE_BROWSER_UA =
|
||||||
|
"Mozilla/5.0 (iPhone; CPU iPhone OS 16_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.2 Mobile/15E148 Safari/604.1";
|
||||||
|
|
||||||
|
const VIEWPORT_CASES = [
|
||||||
|
{ key: "w320-ios-wecom", label: "320 iOS 企业微信", viewport: { width: 320, height: 568 }, userAgent: IOS_WECOM_UA },
|
||||||
|
{ key: "w360-android-wecom", label: "360 Android 企业微信", viewport: { width: 360, height: 740 }, userAgent: ANDROID_WECOM_UA },
|
||||||
|
{ key: "w375-browser", label: "375 普通移动浏览器", viewport: { width: 375, height: 667 }, userAgent: MOBILE_BROWSER_UA },
|
||||||
|
{ key: "w390-ios-wecom", label: "390 iOS 企业微信", viewport: { width: 390, height: 844 }, userAgent: IOS_WECOM_UA },
|
||||||
|
{ key: "w393-android-wecom", label: "393 Android 企业微信", viewport: { width: 393, height: 873 }, userAgent: ANDROID_WECOM_UA },
|
||||||
|
{ key: "w414-ios-wecom", label: "414 iOS 企业微信", viewport: { width: 414, height: 896 }, userAgent: IOS_WECOM_UA },
|
||||||
|
{ key: "w430-browser", label: "430 普通移动浏览器", viewport: { width: 430, height: 932 }, userAgent: MOBILE_BROWSER_UA },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const MOBILE_PAGES = [
|
||||||
|
{ key: "home", path: "/mobile", marker: "mobile-action-grid", deep: false },
|
||||||
|
{ key: "approvals", path: "/mobile/approvals", marker: "mobile-approvals-list", deep: false },
|
||||||
|
{ key: "reimbursements", path: "/mobile/reimbursements", marker: "mobile-reimbursements-list", deep: false },
|
||||||
|
{ key: "reimbursement-detail", path: "/mobile/reimbursements/mud6-reimbursement", marker: "mobile-reimbursement-detail", deep: true },
|
||||||
|
{ key: "me", path: "/mobile/me", marker: "mobile-more-links", deep: false },
|
||||||
|
{ key: "profile", path: "/mobile/profile", marker: "mobile-profile-page", deep: false },
|
||||||
|
{ key: "notifications", path: "/mobile/notifications", marker: "mobile-notifications-page", deep: false },
|
||||||
|
{ key: "capture", path: "/mobile/capture", marker: "mobile-capture-file", deep: false },
|
||||||
|
{ key: "leave", path: "/mobile/leave", marker: "mobile-leave-form", deep: false },
|
||||||
|
{ key: "asset-scan", path: "/mobile/assets/scan", marker: "barcode-scanner-panel", deep: false },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const employee = {
|
||||||
|
id: "mud6-employee",
|
||||||
|
employeeNo: "SMOKE002",
|
||||||
|
name: "OA试运行测试01",
|
||||||
|
phone: "13800000000",
|
||||||
|
email: "[email protected]",
|
||||||
|
departmentId: "mud6-dept",
|
||||||
|
directManagerId: "mud6-manager",
|
||||||
|
status: "active",
|
||||||
|
notificationPushEnabled: true,
|
||||||
|
roleCodes: ["manager", "finance_mgr", "employee"],
|
||||||
|
profileCompleteness: {
|
||||||
|
hasEmergencyContact: true,
|
||||||
|
hasBankAccount: true,
|
||||||
|
hasAvatar: false,
|
||||||
|
hasIdNumber: false,
|
||||||
|
score: 80,
|
||||||
|
},
|
||||||
|
wecomBinding: {
|
||||||
|
externalUserId: "oa_smoke_01",
|
||||||
|
createdAt: "2026-05-11T08:00:00.000Z",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function envelope(data: unknown) {
|
||||||
|
return { requestId: "mock-r6-mobile-unified", data };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fulfillJson(route: Route, data: unknown, status = 200) {
|
||||||
|
await route.fulfill({
|
||||||
|
status,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify(envelope(data)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function screenshotPath(fileName: string) {
|
||||||
|
mkdirSync(SCREENSHOT_DIR, { recursive: true });
|
||||||
|
return path.join(SCREENSHOT_DIR, `${fileName}.png`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function reimbursementListItem() {
|
||||||
|
return {
|
||||||
|
id: "mud6-reimbursement",
|
||||||
|
formNumber: "BX20260511007",
|
||||||
|
status: "draft",
|
||||||
|
reimbursementDate: "2026-05-11",
|
||||||
|
totalAmount: "168.30",
|
||||||
|
description: "MUD6 移动统一设计验证",
|
||||||
|
payment: {},
|
||||||
|
owner: { id: employee.id, employeeNo: employee.employeeNo, name: employee.name },
|
||||||
|
submitter: { id: employee.id, employeeNo: employee.employeeNo, name: employee.name },
|
||||||
|
project: null,
|
||||||
|
pendingWorkflow: null,
|
||||||
|
createdAt: "2026-05-11T08:00:00.000Z",
|
||||||
|
updatedAt: "2026-05-11T08:00:00.000Z",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function mockApis(page: Page) {
|
||||||
|
await page.route((url) => url.pathname.startsWith("/api/"), async (route) => {
|
||||||
|
const request = route.request();
|
||||||
|
const url = new URL(request.url());
|
||||||
|
const pathName = url.pathname;
|
||||||
|
|
||||||
|
if (pathName === "/api/auth/login" || pathName === "/api/auth/refresh") {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
headers: { "set-cookie": "has_session=1; Path=/; SameSite=Lax" },
|
||||||
|
body: JSON.stringify(envelope({ accessToken: "mock-mud6-token", expiresIn: 900 })),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathName === "/api/auth/sso/providers") {
|
||||||
|
await fulfillJson(route, []);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathName === "/api/auth/wecom/config") {
|
||||||
|
await fulfillJson(route, {
|
||||||
|
enabled: true,
|
||||||
|
entryEnabled: true,
|
||||||
|
oauthEnabled: true,
|
||||||
|
corpId: "ww-mud6",
|
||||||
|
agentId: "1000002",
|
||||||
|
loginUrl: "/api/auth/wecom/login?returnTo=%2Fmobile",
|
||||||
|
bindEnabled: true,
|
||||||
|
defaultReturnTo: "/mobile",
|
||||||
|
status: "ready",
|
||||||
|
statusReasons: [],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathName === "/api/me") {
|
||||||
|
await fulfillJson(route, employee);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathName === `/api/employees/${employee.id}/profile`) {
|
||||||
|
await fulfillJson(route, {
|
||||||
|
employee: {
|
||||||
|
...employee,
|
||||||
|
title: "测试员工",
|
||||||
|
avatarUrl: null,
|
||||||
|
department: { id: "mud6-dept", name: "试运行部" },
|
||||||
|
directManager: { id: "mud6-manager", name: "OA试运行测试02" },
|
||||||
|
},
|
||||||
|
lifecycle: { events: [], checklistTasks: [], blockers: { openAssetCount: 0, pendingWorkflowTaskCount: 0, canCompleteOffboarding: true } },
|
||||||
|
assignedAssets: [],
|
||||||
|
organization: { reportingChain: [], directReports: [] },
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathName === "/api/attendance/me/today") {
|
||||||
|
await fulfillJson(route, { workDate: "2026-05-11", needClockIn: false, exemptionReason: null, item: null });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathName === "/api/notifications" && request.method() === "GET") {
|
||||||
|
await fulfillJson(route, {
|
||||||
|
page: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
total: 1,
|
||||||
|
unreadOnly: false,
|
||||||
|
unreadCount: 1,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "mud6-notification",
|
||||||
|
type: "workflow.task_assigned",
|
||||||
|
title: "报销待审批",
|
||||||
|
body: "MUD6 移动统一设计待办",
|
||||||
|
targetType: "workflow_task",
|
||||||
|
targetId: "mud6-task",
|
||||||
|
readAt: null,
|
||||||
|
createdAt: "2026-05-11T08:00:00.000Z",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathName === "/api/workflow/tasks" && request.method() === "GET") {
|
||||||
|
await fulfillJson(route, {
|
||||||
|
page: 1,
|
||||||
|
pageSize: 100,
|
||||||
|
total: 1,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "mud6-task",
|
||||||
|
instanceId: "mud6-instance",
|
||||||
|
stepId: "mud6-step",
|
||||||
|
stepName: "主管审批",
|
||||||
|
assigneeId: employee.id,
|
||||||
|
assigneeRole: null,
|
||||||
|
status: "pending",
|
||||||
|
outcome: null,
|
||||||
|
comments: null,
|
||||||
|
createdAt: "2026-05-11T08:00:00.000Z",
|
||||||
|
completedAt: null,
|
||||||
|
workflowInstance: {
|
||||||
|
id: "mud6-instance",
|
||||||
|
entityType: "reimbursement",
|
||||||
|
entityId: "mud6-reimbursement",
|
||||||
|
currentStatus: "reviewing",
|
||||||
|
startedAt: "2026-05-11T08:00:00.000Z",
|
||||||
|
completedAt: null,
|
||||||
|
},
|
||||||
|
businessSummary: {
|
||||||
|
reference: "BX20260511007",
|
||||||
|
title: "报销待审批",
|
||||||
|
applicantName: employee.name,
|
||||||
|
amount: "168.30",
|
||||||
|
duration: null,
|
||||||
|
businessStatus: "reviewing",
|
||||||
|
},
|
||||||
|
allowedActions: ["approve", "reject", "return"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathName === "/api/workflow/tasks/mud6-task" && request.method() === "GET") {
|
||||||
|
await fulfillJson(route, {
|
||||||
|
task: {
|
||||||
|
id: "mud6-task",
|
||||||
|
instanceId: "mud6-instance",
|
||||||
|
stepId: "mud6-step",
|
||||||
|
stepName: "主管审批",
|
||||||
|
assigneeId: employee.id,
|
||||||
|
assigneeRole: null,
|
||||||
|
status: "pending",
|
||||||
|
outcome: null,
|
||||||
|
comment: null,
|
||||||
|
createdAt: "2026-05-11T08:00:00.000Z",
|
||||||
|
completedAt: null,
|
||||||
|
},
|
||||||
|
workflowInstance: {
|
||||||
|
id: "mud6-instance",
|
||||||
|
entityType: "reimbursement",
|
||||||
|
currentStatus: "reviewing",
|
||||||
|
currentStepId: "mud6-step",
|
||||||
|
contextData: { standardViolations: [] },
|
||||||
|
startedAt: "2026-05-11T08:00:00.000Z",
|
||||||
|
completedAt: null,
|
||||||
|
},
|
||||||
|
reimbursement: reimbursementListItem(),
|
||||||
|
leaveRequest: null,
|
||||||
|
purchaseRequest: null,
|
||||||
|
travelRequest: null,
|
||||||
|
allowedActions: ["approve", "reject", "return"],
|
||||||
|
history: [],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathName === "/api/workflow/approvers" && request.method() === "GET") {
|
||||||
|
await fulfillJson(route, { page: 1, pageSize: 20, total: 0, items: [] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathName === "/api/reimbursements" && request.method() === "GET") {
|
||||||
|
await fulfillJson(route, { page: 1, pageSize: 20, total: 1, scope: "own", items: [reimbursementListItem()] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathName === "/api/reimbursements/mud6-reimbursement") {
|
||||||
|
await fulfillJson(route, {
|
||||||
|
...reimbursementListItem(),
|
||||||
|
submitterId: employee.id,
|
||||||
|
ownerId: employee.id,
|
||||||
|
departmentId: "mud6-dept",
|
||||||
|
department: { id: "mud6-dept", name: "试运行部" },
|
||||||
|
submittedAt: null,
|
||||||
|
projectId: null,
|
||||||
|
linkedTravelRequestId: null,
|
||||||
|
linkedTravelRequest: null,
|
||||||
|
project: null,
|
||||||
|
owner: { id: employee.id, employee_no: employee.employeeNo, name: employee.name },
|
||||||
|
submitter: { id: employee.id, employee_no: employee.employeeNo, name: employee.name },
|
||||||
|
invoices: [],
|
||||||
|
attachments: [],
|
||||||
|
budgetHint: null,
|
||||||
|
pendingWorkflow: null,
|
||||||
|
workflowProgress: null,
|
||||||
|
approvalHistory: [],
|
||||||
|
riskFlags: [],
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "mud6-item",
|
||||||
|
expenseTypeId: "mud6-expense-type",
|
||||||
|
expenseDate: "2026-05-11",
|
||||||
|
amount: "168.30",
|
||||||
|
description: "统一设计验证",
|
||||||
|
expenseType: { id: "mud6-expense-type", code: "office", name: "办公费", category: "office", enabled: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathName === "/api/expense-types") {
|
||||||
|
await fulfillJson(route, {
|
||||||
|
page: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
total: 1,
|
||||||
|
items: [{ id: "mud6-expense-type", code: "office", name: "办公费", category: "office", enabled: true }],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathName === "/api/leave-types") {
|
||||||
|
await fulfillJson(route, {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "mud6-leave-type",
|
||||||
|
code: "annual",
|
||||||
|
name: "年假",
|
||||||
|
description: null,
|
||||||
|
enabled: true,
|
||||||
|
requiresAttachment: false,
|
||||||
|
attachmentMimeTypes: [],
|
||||||
|
sortOrder: 1,
|
||||||
|
policy: { id: "mud6-leave-policy", annualQuotaDays: 5, enabled: true, description: null },
|
||||||
|
createdAt: null,
|
||||||
|
updatedAt: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathName === "/api/leave-balances") {
|
||||||
|
await fulfillJson(route, {
|
||||||
|
employeeId: employee.id,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
leaveType: { id: "mud6-leave-type", code: "annual", name: "年假", requiresAttachment: false, attachmentMimeTypes: [] },
|
||||||
|
policy: { id: "mud6-leave-policy", annualQuotaDays: 5, enabled: true, description: null },
|
||||||
|
granted: 5,
|
||||||
|
adjusted: 0,
|
||||||
|
reserved: 0,
|
||||||
|
used: 1,
|
||||||
|
available: 4,
|
||||||
|
periodStart: "2026-01-01",
|
||||||
|
periodEnd: "2026-12-31",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathName === "/api/leave-requests") {
|
||||||
|
await fulfillJson(route, {
|
||||||
|
page: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
total: 1,
|
||||||
|
scope: "own",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "mud6-leave",
|
||||||
|
ownerId: employee.id,
|
||||||
|
departmentId: "mud6-dept",
|
||||||
|
status: "draft",
|
||||||
|
startDate: "2026-05-12",
|
||||||
|
endDate: "2026-05-12",
|
||||||
|
startSession: "am",
|
||||||
|
endSession: "pm",
|
||||||
|
requestedDays: 1,
|
||||||
|
reason: "统一设计验证",
|
||||||
|
attachment: null,
|
||||||
|
leaveType: { id: "mud6-leave-type", code: "annual", name: "年假", requiresAttachment: false, attachmentMimeTypes: [] },
|
||||||
|
owner: { id: employee.id, employeeNo: employee.employeeNo, name: employee.name },
|
||||||
|
createdAt: "2026-05-11T08:00:00.000Z",
|
||||||
|
updatedAt: "2026-05-11T08:00:00.000Z",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathName === "/api/inventory-checks") {
|
||||||
|
await fulfillJson(route, { page: 1, pageSize: 20, total: 0, items: [] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathName === "/api/assets") {
|
||||||
|
await fulfillJson(route, { page: 1, pageSize: 20, total: 0, items: [] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await fulfillJson(route, {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function login(page: Page) {
|
||||||
|
await page.goto("/login");
|
||||||
|
const accountLogin = page.getByRole("button", { name: "使用账号密码登录" });
|
||||||
|
if (await accountLogin.isVisible().catch(() => false)) {
|
||||||
|
await accountLogin.click();
|
||||||
|
}
|
||||||
|
await page.getByTestId("login-identifier").fill(employee.email);
|
||||||
|
await page.getByTestId("login-password").fill("Password1234");
|
||||||
|
await page.getByTestId("login-submit").click();
|
||||||
|
await page.waitForURL(/\/(portal|mobile)/, { timeout: 30_000 });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function collectMetrics(page: Page) {
|
||||||
|
return page.evaluate(() => {
|
||||||
|
const viewportWidth = window.innerWidth;
|
||||||
|
const visibleElements = (selector: string) =>
|
||||||
|
Array.from(document.querySelectorAll(selector)).filter((element) => {
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
const style = window.getComputedStyle(element);
|
||||||
|
return rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none";
|
||||||
|
});
|
||||||
|
const bottomNav = document.querySelector("[data-testid='mobile-bottom-nav']");
|
||||||
|
const navItems = ["home", "approvals", "reimbursements", "me"].map((key) => {
|
||||||
|
const element = document.querySelector(`[data-testid='mobile-nav-${key}']`);
|
||||||
|
const rect = element?.getBoundingClientRect();
|
||||||
|
const centerX = rect ? rect.left + rect.width / 2 : 0;
|
||||||
|
const centerY = rect ? rect.top + rect.height / 2 : 0;
|
||||||
|
const hit = rect ? document.elementFromPoint(centerX, centerY) : null;
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
visible: Boolean(rect && rect.width > 0 && rect.height > 0),
|
||||||
|
width: rect?.width ?? 0,
|
||||||
|
height: rect?.height ?? 0,
|
||||||
|
hitWithin: Boolean(element && hit instanceof Element && hit.closest(`[data-testid='mobile-nav-${key}']`) === element),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const actionSlotGaps = visibleElements(".mobile-action-link").map((element) => {
|
||||||
|
const icon = element.querySelector(".mobile-action-icon");
|
||||||
|
const copy = element.querySelector(".mobile-action-copy");
|
||||||
|
const iconRect = icon?.getBoundingClientRect();
|
||||||
|
const copyRect = copy?.getBoundingClientRect();
|
||||||
|
return iconRect && copyRect ? copyRect.left - iconRect.right : null;
|
||||||
|
}).filter((item): item is number => typeof item === "number");
|
||||||
|
const mobileSectionHasDecorativeTopLine = visibleElements(".hyb-mobile-shell .section-panel").some((element) => {
|
||||||
|
const before = window.getComputedStyle(element, "::before");
|
||||||
|
const opacity = Number.parseFloat(before.opacity || "1");
|
||||||
|
const height = Number.parseFloat(before.height || "0");
|
||||||
|
return before.display !== "none" && before.visibility !== "hidden" && opacity > 0.01 && height > 0;
|
||||||
|
});
|
||||||
|
const stickyButtons = [
|
||||||
|
document.querySelector("[data-testid='mobile-reimbursement-sticky-invoice-add']"),
|
||||||
|
document.querySelector("[data-testid='reimbursement-submit']"),
|
||||||
|
].filter(Boolean).map((element) => element!.getBoundingClientRect());
|
||||||
|
return {
|
||||||
|
viewportWidth,
|
||||||
|
scrollWidth: Math.max(document.documentElement.scrollWidth, document.body?.scrollWidth ?? 0),
|
||||||
|
shellScoped: Boolean(document.querySelector(".hyb-mobile-shell")),
|
||||||
|
desktopSidebarCount: visibleElements("aside, [data-testid='sidebar']").length,
|
||||||
|
tableCount: visibleElements("table").length,
|
||||||
|
desktopCardMainCount: visibleElements(".hyb-mobile-shell .card-std:not(.section-panel)").length,
|
||||||
|
mobileCardCount: visibleElements(".mobile-card, .section-panel").length,
|
||||||
|
mobileListCardCount: visibleElements(".mobile-list-card").length,
|
||||||
|
hasDecorativeTopLine: mobileSectionHasDecorativeTopLine,
|
||||||
|
bottomNavVisible: Boolean(bottomNav?.getBoundingClientRect().height),
|
||||||
|
navItems,
|
||||||
|
actionGapMin: actionSlotGaps.length ? Math.min(...actionSlotGaps) : null,
|
||||||
|
actionGapMax: actionSlotGaps.length ? Math.max(...actionSlotGaps) : null,
|
||||||
|
stickyButtonsSameRow:
|
||||||
|
stickyButtons.length === 2 ? Math.abs(stickyButtons[0].top - stickyButtons[1].top) <= 2 : stickyButtons.length <= 1,
|
||||||
|
stickyButtonMinHeight: stickyButtons.length ? Math.min(...stickyButtons.map((rect) => rect.height)) : 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function expectUnifiedMobilePage(page: Page, label: string, screenshotName: string, options: { deep: boolean }) {
|
||||||
|
await page.screenshot({ path: screenshotPath(screenshotName), fullPage: true });
|
||||||
|
const metrics = await collectMetrics(page);
|
||||||
|
expect(metrics.shellScoped, `${label} mobile shell scope`).toBe(true);
|
||||||
|
expect(metrics.scrollWidth, `${label} no horizontal overflow`).toBeLessThanOrEqual(metrics.viewportWidth + 2);
|
||||||
|
expect(metrics.desktopSidebarCount, `${label} no desktop sidebar`).toBe(0);
|
||||||
|
expect(metrics.tableCount, `${label} no visible desktop table`).toBe(0);
|
||||||
|
expect(metrics.desktopCardMainCount, `${label} no desktop card main container`).toBe(0);
|
||||||
|
expect(metrics.mobileCardCount, `${label} has mobile card system`).toBeGreaterThan(0);
|
||||||
|
expect(metrics.hasDecorativeTopLine, `${label} mobile card top line hidden`).toBe(false);
|
||||||
|
|
||||||
|
if (metrics.actionGapMin !== null) {
|
||||||
|
const minSlotGap = metrics.viewportWidth >= 390 ? 14 : 12;
|
||||||
|
expect(metrics.actionGapMin, `${label} action slot gap min`).toBeGreaterThanOrEqual(minSlotGap);
|
||||||
|
expect(metrics.actionGapMax ?? 0, `${label} action slot gap max`).toBeLessThanOrEqual(18);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.deep) {
|
||||||
|
expect(metrics.bottomNavVisible, `${label} deep page hides bottom nav`).toBe(false);
|
||||||
|
expect(metrics.stickyButtonsSameRow, `${label} sticky buttons same row`).toBe(true);
|
||||||
|
expect(metrics.stickyButtonMinHeight, `${label} sticky buttons touch height`).toBeGreaterThanOrEqual(44);
|
||||||
|
} else {
|
||||||
|
expect(metrics.bottomNavVisible, `${label} bottom nav visible`).toBe(true);
|
||||||
|
for (const item of metrics.navItems) {
|
||||||
|
expect(item.visible, `${label} ${item.key} nav visible`).toBe(true);
|
||||||
|
expect(item.width, `${label} ${item.key} nav width`).toBeGreaterThanOrEqual(44);
|
||||||
|
expect(item.height, `${label} ${item.key} nav height`).toBeGreaterThanOrEqual(44);
|
||||||
|
expect(item.hitWithin, `${label} ${item.key} nav hit`).toBe(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test.describe("MUD6 专用移动页面统一设计", () => {
|
||||||
|
for (const device of VIEWPORT_CASES) {
|
||||||
|
test.describe(device.label, () => {
|
||||||
|
test.use({
|
||||||
|
viewport: device.viewport,
|
||||||
|
isMobile: true,
|
||||||
|
hasTouch: true,
|
||||||
|
userAgent: device.userAgent,
|
||||||
|
locale: "zh-CN",
|
||||||
|
serviceWorkers: "block",
|
||||||
|
});
|
||||||
|
|
||||||
|
test("所有专用 /mobile 页面复用统一槽位、卡片和底部导航体系", async ({ page }) => {
|
||||||
|
await mockApis(page);
|
||||||
|
await login(page);
|
||||||
|
|
||||||
|
for (const mobilePage of MOBILE_PAGES) {
|
||||||
|
await page.goto(mobilePage.path);
|
||||||
|
await expect(page.getByTestId(mobilePage.marker)).toBeVisible();
|
||||||
|
await expectUnifiedMobilePage(page, `${device.label} ${mobilePage.key}`, `${device.key}-${mobilePage.key}`, {
|
||||||
|
deep: mobilePage.deep,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -86,6 +86,7 @@ AGENTS.md 只放硬约束和入口,不复制长规则。
|
|||||||
|
|
||||||
- 改移动页面前先读 `docs/product-design-rules.md`,确认是否属于底部一级 Tab、首页图标宫格、卡片列表、深层 Push 页、顶部返回、吸底主操作或安全区问题。
|
- 改移动页面前先读 `docs/product-design-rules.md`,确认是否属于底部一级 Tab、首页图标宫格、卡片列表、深层 Push 页、顶部返回、吸底主操作或安全区问题。
|
||||||
- 新移动页默认纳入统一响应式 token 和跨视口视觉矩阵,覆盖 320、360、375、390、393、414、430px;不得只按某一台手机打补丁。
|
- 新移动页默认纳入统一响应式 token 和跨视口视觉矩阵,覆盖 320、360、375、390、393、414、430px;不得只按某一台手机打补丁。
|
||||||
|
- 专用 `/mobile` 路由默认使用统一移动槽位、`mobile-card`、移动列表卡片和吸底操作;不得把桌面 `card-std p-4` 作为手机主容器,也不得为单个页面自造另一套间距、卡片或图标布局。
|
||||||
- 角色差异采用“同框架、按角色裁剪内容和动作”:员工、审批人、财务、管理员、负责人看到不同入口和动作,但不创建互不一致的角色专属移动 UI。
|
- 角色差异采用“同框架、按角色裁剪内容和动作”:员工、审批人、财务、管理员、负责人看到不同入口和动作,但不创建互不一致的角色专属移动 UI。
|
||||||
- 真机验证暂停时记录为 `PAUSED`;自动化通过不能替代代表性真机确认,也不能关闭仍待人工确认的移动阻断。
|
- 真机验证暂停时记录为 `PAUSED`;自动化通过不能替代代表性真机确认,也不能关闭仍待人工确认的移动阻断。
|
||||||
|
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ R6 之后的 PC、移动和审批体验参考国内主流协同/OA 产品的共
|
|||||||
## 移动统一页面范式
|
## 移动统一页面范式
|
||||||
|
|
||||||
- 所有 `/mobile` 页面默认复用同一套移动 Shell、响应式 token、底部导航、安全区和视觉矩阵;不允许单页自造布局体系解决局部拥挤。
|
- 所有 `/mobile` 页面默认复用同一套移动 Shell、响应式 token、底部导航、安全区和视觉矩阵;不允许单页自造布局体系解决局部拥挤。
|
||||||
|
- 专用 `/mobile` 页面包括移动首页、审批、报销列表、报销详情、我的、个人设置、通知、票据上传、请假和资产扫码;这些页面必须先使用统一移动组件和样式,再按业务差异裁剪内容。
|
||||||
- 手机端默认隐藏 PC 侧边栏和顶栏,使用移动首页、底部导航和轻量卡片。
|
- 手机端默认隐藏 PC 侧边栏和顶栏,使用移动首页、底部导航和轻量卡片。
|
||||||
- 菜单不应占用大量空间;常用入口放底部导航,更多入口放“我的”。
|
- 菜单不应占用大量空间;常用入口放底部导航,更多入口放“我的”。
|
||||||
- 移动底部导航固定为“首页、审批、报销、我的”;票据上传作为首页快捷入口和报销页内操作,不作为底部一级 Tab。
|
- 移动底部导航固定为“首页、审批、报销、我的”;票据上传作为首页快捷入口和报销页内操作,不作为底部一级 Tab。
|
||||||
@@ -117,6 +118,8 @@ R6 之后的 PC、移动和审批体验参考国内主流协同/OA 产品的共
|
|||||||
- 企业微信入口应尽量免登录;绑定失败或权限不足时显示明确下一步,而不是让用户反复登录。
|
- 企业微信入口应尽量免登录;绑定失败或权限不足时显示明确下一步,而不是让用户反复登录。
|
||||||
- 头像 fallback 遵循国内中文习惯:中文/CJK 姓名默认取首个可见字符,英文或拼音姓名保留两段首字母。
|
- 头像 fallback 遵循国内中文习惯:中文/CJK 姓名默认取首个可见字符,英文或拼音姓名保留两段首字母。
|
||||||
- “我的”页和更多入口使用组件级自适应槽位:头像/图标槽固定,文字槽使用 `minmax(0, 1fr)` 可收缩,间距使用移动 token 或 `clamp()`;不按具体 iPhone/Android 机型写补丁,不通过放大字号、放大卡片或改成单列来规避图文拥挤。
|
- “我的”页和更多入口使用组件级自适应槽位:头像/图标槽固定,文字槽使用 `minmax(0, 1fr)` 可收缩,间距使用移动 token 或 `clamp()`;不按具体 iPhone/Android 机型写补丁,不通过放大字号、放大卡片或改成单列来规避图文拥挤。
|
||||||
|
- 所有专用 `/mobile` 页面统一使用移动槽位和移动卡片:图标/头像/状态槽固定,文字槽可收缩,列表项使用移动列表卡片,提示、空状态和表单分组使用移动 token;不得把桌面 `card-std p-4` 当作移动主容器重新引入。
|
||||||
|
- 移动卡片保持白底、浅边框、轻阴影,不显示 PC 端 `SectionPanel` 的蓝青色顶边装饰线。
|
||||||
- 移动角色差异采用“同框架、按角色裁剪内容和动作”:员工、审批人、财务、管理员、负责人看到不同入口、摘要、动作和敏感信息,但不做多套角色专属移动 UI。
|
- 移动角色差异采用“同框架、按角色裁剪内容和动作”:员工、审批人、财务、管理员、负责人看到不同入口、摘要、动作和敏感信息,但不做多套角色专属移动 UI。
|
||||||
- 移动验收必须包含跨视口自动化和代表性真机确认;若真机验证暂缓,只能记为 `PAUSED`,不能把相关移动阻断标为完成。
|
- 移动验收必须包含跨视口自动化和代表性真机确认;若真机验证暂缓,只能记为 `PAUSED`,不能把相关移动阻断标为完成。
|
||||||
|
|
||||||
|
|||||||
@@ -60,10 +60,12 @@
|
|||||||
|
|
||||||
| ID | 级别 | 角色 | 入口/设备 | 流程 | 复现步骤 | 当前处理 | 负责人 | 状态 | 关联 Issue |
|
| ID | 级别 | 角色 | 入口/设备 | 流程 | 复现步骤 | 当前处理 | 负责人 | 状态 | 关联 Issue |
|
||||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||||
| R6-ISSUE-001 | 阻断 | 审批人 / 员工 | iPhone 12 / iOS 14.1 / 企业微信 WebView | 移动底部导航、审批入口、首页快捷卡 | 旧记录误写为 iPhone 11;用户更正为 iPhone 12 / iOS 14.1。真机截图显示首页快捷卡和底部 Tab 的字体图标漂移、重影、挤压,底部“审批”体验不稳定;自动化 Chromium UA 截图与真实 iOS 企业微信 WebView 存在差异 | `PAUSED`:WCM6 已部署公网并完成自动复检,移动核心图标改 SVG,补企业微信 WebView viewport/字号/safe-area,底部角标独立定位;公网 WCM6 视觉专项 8 passed,live smoke readiness PASS,截图 `output/playwright/r6-wecom-mobile-rendering-hardening/`;待 iPhone 12 / iOS 14.1 / 企业微信和 Redmi K60 真机确认后关闭或降级 | Codex / 待指派 | open | `issues/2026-05-11_20-38-33-r6-wecom-mobile-rendering-hardening.csv` |
|
| R6-ISSUE-001 | 阻断 | 审批人 / 员工 | iPhone 12 / iOS 14.1 / 企业微信 WebView | 移动底部导航、审批入口、首页快捷卡 | 旧记录误写为 iPhone 11;用户更正为 iPhone 12 / iOS 14.1。真机截图显示首页快捷卡和底部 Tab 的字体图标漂移、重影、挤压,底部“审批”体验不稳定;自动化 Chromium UA 截图与真实 iOS 企业微信 WebView 存在差异 | 已关闭:WCM6/MME6 已部署公网并完成自动复检,移动核心图标改 SVG,补企业微信 WebView viewport/字号/safe-area,底部角标独立定位,`/mobile/me` 图文槽位和卡片顶边完成自适应收口;用户确认“看过了,可以没问题”。后续若出现移动回归,另建新 Issue 处理。 | Codex | closed | `issues/2026-05-11_20-38-33-r6-wecom-mobile-rendering-hardening.csv`; `issues/2026-05-11_22-42-11-r6-mobile-me-adaptive-visual-polish.csv`; `issues/2026-05-11_23-26-00-r6-mobile-unified-design-rollout.csv` |
|
||||||
| R6-ISSUE-002 | 一般 | 自动化 / 员工与财务测试账号 | 本地隔离 E2E / PC | 报销审批进度、报销支付批次 | `BUSINESS_FLOW_GROUPS=reimbursement bash scripts/qa/run-business-flow-gates.sh local` 中 2 条用例曾失败,失败点为 Playwright 查找旧导航入口 `票据中心`;当前正式员工路径应从报销详情添加 PDF 发票 | 已关闭:测试已改为报销详情页“添加发票”路径,不恢复普通员工“票据中心”入口;支付批次用例同步按业务编号定位待办,并按金额选择财务或财务主管二级审批;报销业务流 gate 10 passed、1 skipped | Codex | closed | `issues/2026-05-11_15-50-00-r6-reimbursement-e2e-nav-alignment.csv` |
|
| R6-ISSUE-002 | 一般 | 自动化 / 员工与财务测试账号 | 本地隔离 E2E / PC | 报销审批进度、报销支付批次 | `BUSINESS_FLOW_GROUPS=reimbursement bash scripts/qa/run-business-flow-gates.sh local` 中 2 条用例曾失败,失败点为 Playwright 查找旧导航入口 `票据中心`;当前正式员工路径应从报销详情添加 PDF 发票 | 已关闭:测试已改为报销详情页“添加发票”路径,不恢复普通员工“票据中心”入口;支付批次用例同步按业务编号定位待办,并按金额选择财务或财务主管二级审批;报销业务流 gate 10 passed、1 skipped | Codex | closed | `issues/2026-05-11_15-50-00-r6-reimbursement-e2e-nav-alignment.csv` |
|
||||||
|
|
||||||
MME6 补充(2026-05-11):针对用户补充的 iPhone 12 / iOS 14.1 图文贴近与 iPhone 14 / iOS 16.2 卡片顶边观感,已把 `/mobile/me` 改为固定头像/图标槽 + `minmax(0, 1fr)` 文字槽,移动卡片顶边装饰线仅在 `/mobile` 作用域禁用,并将该规则写入产品设计规约。本地 `client typecheck`、`client build`、WCM6 视觉专项 8 passed、R5/R6/R124 移动相关回归 10 passed;部署后 `/ready` 正常、public bundle `findings=0`、live smoke readiness PASS、真实 OCR 样票返回重复发票可解释终态且测试通过,公网 WCM6 视觉专项 8 passed,报告 `artifacts/qa-flow/live-smoke-readiness-20260511-230918`。三台代表性真机确认前,`R6-ISSUE-001` 继续 `open/PAUSED`。
|
MME6 补充(2026-05-11):针对用户补充的 iPhone 12 / iOS 14.1 图文贴近与 iPhone 14 / iOS 16.2 卡片顶边观感,已把 `/mobile/me` 改为固定头像/图标槽 + `minmax(0, 1fr)` 文字槽,移动卡片顶边装饰线仅在 `/mobile` 作用域禁用,并将该规则写入产品设计规约。本地 `client typecheck`、`client build`、WCM6 视觉专项 8 passed、R5/R6/R124 移动相关回归 10 passed;部署后 `/ready` 正常、public bundle `findings=0`、live smoke readiness PASS、真实 OCR 样票返回重复发票可解释终态且测试通过,公网 WCM6 视觉专项 8 passed,报告 `artifacts/qa-flow/live-smoke-readiness-20260511-230918`。用户已确认当前设计可接受,`R6-ISSUE-001` 关闭;MUD6 继续把该设计推广到所有专用 `/mobile` 页面。
|
||||||
|
|
||||||
|
MUD6 收口(2026-05-11):已将 MME6 槽位自适应方案推广到专用 `/mobile` 路由:移动首页、审批、报销列表、报销详情、我的、个人设置、通知、票据上传、请假和资产扫码。通用移动样式固定图标/头像槽、`minmax(0, 1fr)` 文本槽、`clamp()` 间距、`mobile-card`、移动列表卡片、空状态和深层页吸底操作;业务角色差异继续按权限裁剪入口和动作,不拆多套 UI。本地 `client typecheck`、`client build`、MUD6 全移动页视觉专项 7 passed、WCM6/RS6/R124 回归 16 passed;已部署到 `https://oa.hyinda.com`,`/ready` 正常、public bundle `findings=0`、live smoke readiness PASS、真实 OCR 样票为重复发票可解释终态且通过,公网 MUD6 视觉专项 7 passed,报告 `artifacts/qa-flow/live-smoke-readiness-20260511-235918`。R6-3 可恢复执行试运行前检查,建议仍抽查 iPhone 12 / iOS 14、iPhone 14 / iOS 16.2、Redmi K60 的企业微信首页、审批、报销详情、我的和通知。
|
||||||
|
|
||||||
级别定义:
|
级别定义:
|
||||||
|
|
||||||
|
|||||||
@@ -98,7 +98,7 @@
|
|||||||
| 工作台入口与免登录 | `oa_smoke_01` | `SMOKE002` | 从企业微信工作台进入 OA,已绑定后免登录 | 通过 | 用户确认;截图/录屏不提交仓库 |
|
| 工作台入口与免登录 | `oa_smoke_01` | `SMOKE002` | 从企业微信工作台进入 OA,已绑定后免登录 | 通过 | 用户确认;截图/录屏不提交仓库 |
|
||||||
| 提交人移动报销体验 | `oa_smoke_01` | `SMOKE002` | 移动首页、报销页、详情页、PDF 发票入口可用 | 公网自动视觉通过;真机核心链路通过 | `output/playwright/r5-live-mobile-wecom-visual/` |
|
| 提交人移动报销体验 | `oa_smoke_01` | `SMOKE002` | 移动首页、报销页、详情页、PDF 发票入口可用 | 公网自动视觉通过;真机核心链路通过 | `output/playwright/r5-live-mobile-wecom-visual/` |
|
||||||
| 审批人移动待办体验 | `oa_smoke_02` | `SMOKE101` | 移动审批待办、审批详情、结果反馈可用 | 公网自动视觉通过;真机审批与消息回流通过 | `output/playwright/r5-live-mobile-wecom-visual/` |
|
| 审批人移动待办体验 | `oa_smoke_02` | `SMOKE101` | 移动审批待办、审批详情、结果反馈可用 | 公网自动视觉通过;真机审批与消息回流通过 | `output/playwright/r5-live-mobile-wecom-visual/` |
|
||||||
| 移动底部导航与深层页 | `oa_smoke_01` / `oa_smoke_02` | `SMOKE002` / `SMOKE101` | 首页、审批、报销、我的固定可见;报销详情隐藏底部 Tab,吸底双按钮不遮挡 | `PAUSED`:WCM6 已部署公网且自动视觉通过;代表性真机确认待补 | `output/playwright/r6-wecom-mobile-rendering-hardening/` |
|
| 移动底部导航与深层页 | `oa_smoke_01` / `oa_smoke_02` | `SMOKE002` / `SMOKE101` | 首页、审批、报销、我的固定可见;报销详情隐藏底部 Tab,吸底双按钮不遮挡 | 通过:WCM6/MME6 已部署公网且自动视觉通过;用户确认当前设计可接受,MUD6 继续推广统一设计 | `output/playwright/r6-wecom-mobile-rendering-hardening/` |
|
||||||
| 通知与 deeplink | `oa_smoke_01` / `oa_smoke_02` | `SMOKE002` / `SMOKE101` | 通知能回到移动待办或详情页 | 真机通过:收到个人应用消息,点击后首次需点进入工作台,后续可直达移动审批 | `output/playwright/r5-wecom-app-message-live/` |
|
| 通知与 deeplink | `oa_smoke_01` / `oa_smoke_02` | `SMOKE002` / `SMOKE101` | 通知能回到移动待办或详情页 | 真机通过:收到个人应用消息,点击后首次需点进入工作台,后续可直达移动审批 | `output/playwright/r5-wecom-app-message-live/` |
|
||||||
|
|
||||||
自动化补充证据(用于审批链业务验收):
|
自动化补充证据(用于审批链业务验收):
|
||||||
@@ -113,8 +113,9 @@
|
|||||||
- `2026-05-11` 公网自动视觉补充:部署 `795e976` 后,Playwright 企业微信 UA 覆盖 iPhone 近似视口与 Redmi K60 视口;`SMOKE002` 移动首页、报销列表、报销详情、PDF-only 上传入口通过,`SMOKE101` 移动审批列表通过;截图目录 `output/playwright/r5-live-mobile-wecom-visual/`。用户已确认真实企业微信工作台入口和免登录通过。
|
- `2026-05-11` 公网自动视觉补充:部署 `795e976` 后,Playwright 企业微信 UA 覆盖 iPhone 近似视口与 Redmi K60 视口;`SMOKE002` 移动首页、报销列表、报销详情、PDF-only 上传入口通过,`SMOKE101` 移动审批列表通过;截图目录 `output/playwright/r5-live-mobile-wecom-visual/`。用户已确认真实企业微信工作台入口和免登录通过。
|
||||||
- `2026-05-11` 通知与 deeplink 自动视觉补充:`SMOKE101` 现有两条未读报销待办站内通知,iPhone 近似视口与 Redmi K60 近似视口、企业微信 UA 下通知页无横向溢出,通知卡片和“查看”按钮可见;点击后进入 `/mobile/approvals?taskId=177b3a9e-67fe-447d-92f4-96a652a06841` 移动审批详情,无 PC 壳混入,截图目录 `output/playwright/r5-notification-deeplink-live/`。该历史待办已处理,因此本证据只证明站内通知和移动 deeplink,不证明新待办审批按钮;通知投递审计显示渠道 `R50-MOCK-007334` 为 `mock_sent`,真实企业微信外发应用消息仍待通道确认或真机记录。
|
- `2026-05-11` 通知与 deeplink 自动视觉补充:`SMOKE101` 现有两条未读报销待办站内通知,iPhone 近似视口与 Redmi K60 近似视口、企业微信 UA 下通知页无横向溢出,通知卡片和“查看”按钮可见;点击后进入 `/mobile/approvals?taskId=177b3a9e-67fe-447d-92f4-96a652a06841` 移动审批详情,无 PC 壳混入,截图目录 `output/playwright/r5-notification-deeplink-live/`。该历史待办已处理,因此本证据只证明站内通知和移动 deeplink,不证明新待办审批按钮;通知投递审计显示渠道 `R50-MOCK-007334` 为 `mock_sent`,真实企业微信外发应用消息仍待通道确认或真机记录。
|
||||||
- `2026-05-11` 个人企微应用消息补充:部署 `b1ae41f` 后启用 `R5-WECOM-APP-PERSONAL / wecom_app_message`,停用旧 mock 渠道;`SMOKE002` 创建请假测试单据并生成 `SMOKE101` 待办 `936362d8-c4e0-454f-a589-9c9653bcbfda`,通知 `9ee0602e-66a5-4a6e-81e5-04d41908c782` 投递审计为 `wecom_app_sent`、`errcode=0`、无 `invaliduser`。iPhone 近似视口与 Redmi K60 企业微信 UA 自动视觉验证通知列表和审批详情均无横向溢出,截图目录 `output/playwright/r5-wecom-app-message-live/`。用户确认手机企业微信 `oa_smoke_02` 收到“海音达工作台”应用消息,标题正确;点击后首次进入会提示进入工作台,进入后可看到移动审批,后续再次进入可直接看到移动审批;存在消息通知但未观察到系统弹窗。
|
- `2026-05-11` 个人企微应用消息补充:部署 `b1ae41f` 后启用 `R5-WECOM-APP-PERSONAL / wecom_app_message`,停用旧 mock 渠道;`SMOKE002` 创建请假测试单据并生成 `SMOKE101` 待办 `936362d8-c4e0-454f-a589-9c9653bcbfda`,通知 `9ee0602e-66a5-4a6e-81e5-04d41908c782` 投递审计为 `wecom_app_sent`、`errcode=0`、无 `invaliduser`。iPhone 近似视口与 Redmi K60 企业微信 UA 自动视觉验证通知列表和审批详情均无横向溢出,截图目录 `output/playwright/r5-wecom-app-message-live/`。用户确认手机企业微信 `oa_smoke_02` 收到“海音达工作台”应用消息,标题正确;点击后首次进入会提示进入工作台,进入后可看到移动审批,后续再次进入可直接看到移动审批;存在消息通知但未观察到系统弹窗。
|
||||||
- `2026-05-11` R6 移动体验补充:RS6 响应式体系修复已完成公网自动化,320/360/375/390/393/414/430px 视觉矩阵通过;用户补充真实设备为 iPhone 12 / iOS 14.1 / 企业微信,截图显示字体图标漂移、重影和首页/底部导航视觉拥挤。当前记录为 `PAUSED`,不得将 `RS6-7`、`UI6-6` 或 `R6-ISSUE-001` 置为完成;WCM6 已部署公网并完成自动复检,移动核心 SVG 图标、企业微信 WebView 基线、多手机视口和字体失败场景均已覆盖,公网 WCM6 视觉专项 8 passed,截图目录 `output/playwright/r6-wecom-mobile-rendering-hardening/`;R6-3 继续等待代表性真机确认。
|
- `2026-05-11` R6 移动体验补充:RS6 响应式体系修复已完成公网自动化,320/360/375/390/393/414/430px 视觉矩阵通过;用户补充真实设备为 iPhone 12 / iOS 14.1 / 企业微信,截图显示字体图标漂移、重影和首页/底部导航视觉拥挤。该阶段曾记录为 `PAUSED`,不得仅凭自动化关闭;随后 WCM6/MME6 已部署公网并完成自动复检,移动核心 SVG 图标、企业微信 WebView 基线、多手机视口、字体失败场景和 `/mobile/me` 自适应均已覆盖,公网 WCM6 视觉专项 8 passed,截图目录 `output/playwright/r6-wecom-mobile-rendering-hardening/`。用户已确认当前设计可接受,`R6-ISSUE-001` 关闭;后续移动回归另建新 Issue。
|
||||||
- `2026-05-11` MME6 我的页自适应补充:针对 iPhone 12 / iOS 14.1 企业微信中图文贴近和 iPhone 14 / iOS 16.2 企业微信中卡片顶边观感,`/mobile/me` 已改为固定头像/图标槽 + 可收缩文字槽,移动卡片顶边装饰线仅在 `/mobile` 作用域禁用。本地 WCM6 视觉专项 8 passed,R5/R6/R124 移动相关回归 10 passed;部署后 `/ready` 正常、public bundle `findings=0`、live smoke readiness PASS、真实 OCR 样票返回重复发票可解释终态且测试通过,公网 WCM6 视觉专项 8 passed,报告 `artifacts/qa-flow/live-smoke-readiness-20260511-230918`。iPhone 12 / iPhone 14 / Redmi K60 真机确认前,`R6-ISSUE-001` 继续 `PAUSED`。
|
- `2026-05-11` MME6 我的页自适应补充:针对 iPhone 12 / iOS 14.1 企业微信中图文贴近和 iPhone 14 / iOS 16.2 企业微信中卡片顶边观感,`/mobile/me` 已改为固定头像/图标槽 + 可收缩文字槽,移动卡片顶边装饰线仅在 `/mobile` 作用域禁用。本地 WCM6 视觉专项 8 passed,R5/R6/R124 移动相关回归 10 passed;部署后 `/ready` 正常、public bundle `findings=0`、live smoke readiness PASS、真实 OCR 样票返回重复发票可解释终态且测试通过,公网 WCM6 视觉专项 8 passed,报告 `artifacts/qa-flow/live-smoke-readiness-20260511-230918`。用户已确认当前设计可接受,`R6-ISSUE-001` 关闭;MUD6 继续把该设计推广到所有专用 `/mobile` 页面,后续若有移动回归另建新 Issue。
|
||||||
|
- `2026-05-11` MUD6 全移动页统一补充:专用 `/mobile` 路由已统一使用固定图标/头像槽、可收缩文字槽、`mobile-card`、移动列表卡片、空状态、安全区和吸底操作。覆盖 `/mobile`、审批、报销列表、报销详情、我的、个人设置、通知、票据上传、请假和资产扫码;不改后端、审批链、权限模型、PDF-only 规则或企业微信个人应用消息通道。本地 MUD6 视觉专项 7 passed,WCM6/RS6/R124 回归 16 passed;部署到 `https://oa.hyinda.com` 后 `/ready` 正常、public bundle `findings=0`、live smoke readiness PASS、真实 OCR 样票为重复发票可解释终态且通过,公网 MUD6 视觉专项 7 passed,报告 `artifacts/qa-flow/live-smoke-readiness-20260511-235918`。R6-3 可恢复试运行前检查,仍建议抽查 iPhone 12 / iOS 14、iPhone 14 / iOS 16.2、Redmi K60 企业微信。
|
||||||
|
|
||||||
- 经理账号可在移动端看到审批待办。
|
- 经理账号可在移动端看到审批待办。
|
||||||
- 审批详情可查看申请摘要、附件、发票、审批记录。
|
- 审批详情可查看申请摘要、附件、发票、审批记录。
|
||||||
|
|||||||
@@ -4,4 +4,4 @@ UI6-2,补 iPhone 11 企业微信命中测试,补强 Playwright 视觉和命中
|
|||||||
UI6-3,修复底部 Tab 固定与点击命中,修复移动 Shell 在 iPhone 11 企业微信 WebView 下底部导航不在真实可点区域或被内容层遮挡的问题。,"1) 底部 Tab 固定在可见视口底部并预留 safe-area;2) 四个 Tab 等宽且触控高度不低于 44px;3) 第二项审批 Tab 可点击进入审批列表;4) 不影响 Redmi K60 和普通手机浏览器。","流程级验证:在 iPhone 11 企业微信 UA 下从 /mobile 点击底部审批,进入 /mobile/approvals 并显示 mobile-approvals-list;重复点击首页、报销、我的均可命中;无横向滚动。",playwright|manual,DONE,DONE,DONE,client/src/*,"[""UI6-2""]",移动审批一级 Tab 已取消前端权限禁用和路由 403;底部导航提升层级、触控区域和 safe-area 预留;iPhone 11 / Redmi K60 E2E 通过。
|
UI6-3,修复底部 Tab 固定与点击命中,修复移动 Shell 在 iPhone 11 企业微信 WebView 下底部导航不在真实可点区域或被内容层遮挡的问题。,"1) 底部 Tab 固定在可见视口底部并预留 safe-area;2) 四个 Tab 等宽且触控高度不低于 44px;3) 第二项审批 Tab 可点击进入审批列表;4) 不影响 Redmi K60 和普通手机浏览器。","流程级验证:在 iPhone 11 企业微信 UA 下从 /mobile 点击底部审批,进入 /mobile/approvals 并显示 mobile-approvals-list;重复点击首页、报销、我的均可命中;无横向滚动。",playwright|manual,DONE,DONE,DONE,client/src/*,"[""UI6-2""]",移动审批一级 Tab 已取消前端权限禁用和路由 403;底部导航提升层级、触控区域和 safe-area 预留;iPhone 11 / Redmi K60 E2E 通过。
|
||||||
UI6-4,修复 iPhone 11 页面拥挤和控件挤压,调整移动首页、审批、报销和我的页面的小屏密度,避免文字和控件在 iPhone 11 企业微信 WebView 内相互挤压。,"1) iPhone 11 下核心卡片和按钮不重叠;2) 文字不挤出容器且不遮挡相邻控件;3) 保留报销详情吸底双按钮同行,不降级为单列;4) 移动端不出现 PC 表格或 PC 壳。","流程级验证:Playwright 截图检查 /mobile、/mobile/approvals、/mobile/reimbursements、/mobile/me、/mobile/reimbursements/:id;断言 scrollWidth <= viewportWidth + 2;人工视觉确认控件间距可读可点。",playwright|manual,DONE,DONE,DONE,client/src/*,"[""UI6-2""]",移动 Shell 改为可视视口高度、紧凑小屏 padding 和 overscroll 约束;E2E 断言无横向滚动、无 PC 表格、报销详情吸底双按钮仍同行。
|
UI6-4,修复 iPhone 11 页面拥挤和控件挤压,调整移动首页、审批、报销和我的页面的小屏密度,避免文字和控件在 iPhone 11 企业微信 WebView 内相互挤压。,"1) iPhone 11 下核心卡片和按钮不重叠;2) 文字不挤出容器且不遮挡相邻控件;3) 保留报销详情吸底双按钮同行,不降级为单列;4) 移动端不出现 PC 表格或 PC 壳。","流程级验证:Playwright 截图检查 /mobile、/mobile/approvals、/mobile/reimbursements、/mobile/me、/mobile/reimbursements/:id;断言 scrollWidth <= viewportWidth + 2;人工视觉确认控件间距可读可点。",playwright|manual,DONE,DONE,DONE,client/src/*,"[""UI6-2""]",移动 Shell 改为可视视口高度、紧凑小屏 padding 和 overscroll 约束;E2E 断言无横向滚动、无 PC 表格、报销详情吸底双按钮仍同行。
|
||||||
UI6-5,本地构建与移动视觉回归,完成前端类型检查、构建和移动专项 E2E,覆盖 iPhone 11、Redmi K60、企业微信 UA、底部导航、审批入口、报销详情和 PDF 上传入口。,"1) cd client && npm run typecheck 通过;2) cd client && npm run build 通过;3) 移动视觉专项通过;4) 截图证据保存在 output/playwright/r6-iphone11-wecom-mobile-layout-hotfix。","流程级验证:cd client && npm run typecheck;cd client && npm run build;cd client && npm run test:e2e -- r5-real-device-feedback-mobile-ux.spec.ts r124-mobile-entry-matrix.spec.ts --workers=1 --reporter=list。",npm|playwright,DONE,DONE,DONE,client/tests/e2e/*|output/playwright/r6-iphone11-wecom-mobile-layout-hotfix,"[""UI6-3"",""UI6-4""]",已通过 npm run typecheck、npm run build、r5-real-device-feedback-mobile-ux.spec.ts、r124-mobile-entry-matrix.spec.ts;UI6-6 部署与真机复测仍待执行。
|
UI6-5,本地构建与移动视觉回归,完成前端类型检查、构建和移动专项 E2E,覆盖 iPhone 11、Redmi K60、企业微信 UA、底部导航、审批入口、报销详情和 PDF 上传入口。,"1) cd client && npm run typecheck 通过;2) cd client && npm run build 通过;3) 移动视觉专项通过;4) 截图证据保存在 output/playwright/r6-iphone11-wecom-mobile-layout-hotfix。","流程级验证:cd client && npm run typecheck;cd client && npm run build;cd client && npm run test:e2e -- r5-real-device-feedback-mobile-ux.spec.ts r124-mobile-entry-matrix.spec.ts --workers=1 --reporter=list。",npm|playwright,DONE,DONE,DONE,client/tests/e2e/*|output/playwright/r6-iphone11-wecom-mobile-layout-hotfix,"[""UI6-3"",""UI6-4""]",已通过 npm run typecheck、npm run build、r5-real-device-feedback-mobile-ux.spec.ts、r124-mobile-entry-matrix.spec.ts;UI6-6 部署与真机复测仍待执行。
|
||||||
UI6-6,部署复测并恢复 R6 Gate,部署修复后做 R4 复检和企业微信真机复测,只有 iPhone 11 底部导航与拥挤问题关闭后才允许继续 R6-3。,"1) 部署后 /ready 正常;2) 公网 bundle findings=0;3) iPhone 11 企业微信底部第二项可点;4) Redmi K60 不回归;5) R6-ISSUE-001 状态关闭或降级为非阻断。","流程级验证:bash deploy/trialrun/deploy.sh;LIVE_SMOKE_BASE_URL=https://oa.hyinda.com bash scripts/qa/run-live-smoke-readiness.sh;人工真机复测 iPhone 11 企业微信打开 OA 并点击底部审批;回填 docs/trialrun/manual-trialrun-report.md。",bash|playwright|manual,DONE,DONE,TODO,deploy/trialrun/*|docs/trialrun/manual-trialrun-report.md,"[""UI6-5""]",PAUSED:已升级到 RS6 响应式体系修复并部署到 https://oa.hyinda.com;/ready 正常;公网 bundle findings=0;live smoke readiness PASS,报告 artifacts/qa-flow/live-smoke-readiness-20260511-124651;公网移动视觉矩阵 320/360/375/390/393/414/430 全部通过。用户暂时暂停真机验证;仍待 iPhone 11 企业微信底部审批可点、首页宫格不拥挤和 Redmi K60 不回归,确认后再置 Regression DONE。
|
UI6-6,部署复测并恢复 R6 Gate,部署修复后做 R4 复检和企业微信真机复测,只有 iPhone 11 底部导航与拥挤问题关闭后才允许继续 R6-3。,"1) 部署后 /ready 正常;2) 公网 bundle findings=0;3) iPhone 11 企业微信底部第二项可点;4) Redmi K60 不回归;5) R6-ISSUE-001 状态关闭或降级为非阻断。","流程级验证:bash deploy/trialrun/deploy.sh;LIVE_SMOKE_BASE_URL=https://oa.hyinda.com bash scripts/qa/run-live-smoke-readiness.sh;人工真机复测 iPhone 11 企业微信打开 OA 并点击底部审批;回填 docs/trialrun/manual-trialrun-report.md。",bash|playwright|manual,DONE,DONE,DONE,deploy/trialrun/*|docs/trialrun/manual-trialrun-report.md,"[""UI6-5""]",已由 RS6/WCM6/MME6/MUD6 升级收口:用户更正代表设备为 iPhone 12 / iOS 14.1 和 iPhone 14 / iOS 16.2,并确认当前设计可以;R6-ISSUE-001 已关闭,后续移动回归另建新 Issue。
|
||||||
|
|||||||
|
@@ -5,4 +5,4 @@ RS6-3,重构移动首页快捷入口为图标宫格,将移动首页审批、传
|
|||||||
RS6-4,统一底部导航滚动容器和吸底操作,统一移动 Shell、底部 Tab、内容滚动区和报销详情吸底操作的 safe-area 与高度预留策略。,"1) 底部导航固定为首页/审批/报销/我的;2) 四个 Tab 等宽且中心点 elementFromPoint 命中自身;3) 深层报销详情隐藏底部 Tab;4) 报销详情吸底双按钮保持同行且内容不被遮挡。","流程级验证:Playwright 访问 /mobile、/mobile/approvals、/mobile/reimbursements、/mobile/reimbursements/:id;断言 scrollWidth <= viewportWidth + 2;断言底部 Tab 中心点可点;断言吸底双按钮同行且高度 >=44px。",playwright,DONE,DONE,DONE,client/src/layouts/AppShellLayout.tsx|client/src/components/mobile/MobileBottomNav.tsx|client/src/pages/MobileReimbursementDetailPage.tsx,["RS6-2"],已统一移动 Shell、底部导航和报销详情吸底操作 safe-area;报销详情主按钮仍保持双按钮同行。
|
RS6-4,统一底部导航滚动容器和吸底操作,统一移动 Shell、底部 Tab、内容滚动区和报销详情吸底操作的 safe-area 与高度预留策略。,"1) 底部导航固定为首页/审批/报销/我的;2) 四个 Tab 等宽且中心点 elementFromPoint 命中自身;3) 深层报销详情隐藏底部 Tab;4) 报销详情吸底双按钮保持同行且内容不被遮挡。","流程级验证:Playwright 访问 /mobile、/mobile/approvals、/mobile/reimbursements、/mobile/reimbursements/:id;断言 scrollWidth <= viewportWidth + 2;断言底部 Tab 中心点可点;断言吸底双按钮同行且高度 >=44px。",playwright,DONE,DONE,DONE,client/src/layouts/AppShellLayout.tsx|client/src/components/mobile/MobileBottomNav.tsx|client/src/pages/MobileReimbursementDetailPage.tsx,["RS6-2"],已统一移动 Shell、底部导航和报销详情吸底操作 safe-area;报销详情主按钮仍保持双按钮同行。
|
||||||
RS6-5,巡检 R6 相关移动页刚性布局,巡检移动审批、报销、通知、票据上传、请假、我的等页面,清理小屏刚性两列和文字挤压风险。,"1) R6 相关移动页在窄屏无 PC 壳、无桌面表格、无横向溢出;2) 信息字段可根据最小宽度从两列变一列;3) 核心操作按钮保持可点且无遮挡;4) 不扩大业务功能范围。","流程级验证:Playwright 覆盖 /mobile/approvals、/mobile/reimbursements、/mobile/reimbursements/:id、/mobile/me、/mobile/notifications、/mobile/capture、/mobile/leave;人工检查截图中无挤压和遮挡。",playwright|manual,DONE,DONE,DONE,client/src/pages/Mobile*.tsx,"[""RS6-3"",""RS6-4""]",已巡检并清理 R6 关键移动页刚性布局;只修布局体系,不改审批链、组织关系、通知通道或上传业务规则。
|
RS6-5,巡检 R6 相关移动页刚性布局,巡检移动审批、报销、通知、票据上传、请假、我的等页面,清理小屏刚性两列和文字挤压风险。,"1) R6 相关移动页在窄屏无 PC 壳、无桌面表格、无横向溢出;2) 信息字段可根据最小宽度从两列变一列;3) 核心操作按钮保持可点且无遮挡;4) 不扩大业务功能范围。","流程级验证:Playwright 覆盖 /mobile/approvals、/mobile/reimbursements、/mobile/reimbursements/:id、/mobile/me、/mobile/notifications、/mobile/capture、/mobile/leave;人工检查截图中无挤压和遮挡。",playwright|manual,DONE,DONE,DONE,client/src/pages/Mobile*.tsx,"[""RS6-3"",""RS6-4""]",已巡检并清理 R6 关键移动页刚性布局;只修布局体系,不改审批链、组织关系、通知通道或上传业务规则。
|
||||||
RS6-6,补跨视口视觉自动化矩阵,新增或补强移动视觉 E2E,覆盖 320x568、360x740、375x667、390x844、393x873、414x896、430x932 和企业微信/普通移动 UA。,"1) 新视觉矩阵截图输出到 output/playwright/r6-mobile-responsive-system-hotfix;2) 断言无横向滚动;3) 断言底部 Tab 命中;4) 断言首页图标宫格不重叠;5) 断言报销详情吸底双按钮同行。","流程级验证:cd client && npm run test:e2e -- r6-mobile-responsive-system-hotfix.spec.ts r5-real-device-feedback-mobile-ux.spec.ts r124-mobile-entry-matrix.spec.ts --workers=1 --reporter=list。",playwright,DONE,DONE,DONE,client/tests/e2e/r6-mobile-responsive-system-hotfix.spec.ts|client/tests/e2e/r5-real-device-feedback-mobile-ux.spec.ts,"[""RS6-3"",""RS6-4"",""RS6-5""]",本地 typecheck/build 通过;移动回归组合 10 passed,截图输出到 output/playwright/r6-mobile-responsive-system-hotfix。
|
RS6-6,补跨视口视觉自动化矩阵,新增或补强移动视觉 E2E,覆盖 320x568、360x740、375x667、390x844、393x873、414x896、430x932 和企业微信/普通移动 UA。,"1) 新视觉矩阵截图输出到 output/playwright/r6-mobile-responsive-system-hotfix;2) 断言无横向滚动;3) 断言底部 Tab 命中;4) 断言首页图标宫格不重叠;5) 断言报销详情吸底双按钮同行。","流程级验证:cd client && npm run test:e2e -- r6-mobile-responsive-system-hotfix.spec.ts r5-real-device-feedback-mobile-ux.spec.ts r124-mobile-entry-matrix.spec.ts --workers=1 --reporter=list。",playwright,DONE,DONE,DONE,client/tests/e2e/r6-mobile-responsive-system-hotfix.spec.ts|client/tests/e2e/r5-real-device-feedback-mobile-ux.spec.ts,"[""RS6-3"",""RS6-4"",""RS6-5""]",本地 typecheck/build 通过;移动回归组合 10 passed,截图输出到 output/playwright/r6-mobile-responsive-system-hotfix。
|
||||||
RS6-7,部署公网复检并回填 R6 移动阻断,本地验证通过后部署公网,复检 /ready、bundle、live smoke、PDF OCR 和企微个人应用消息,等待真机确认后关闭或降级 R6-ISSUE-001。,"1) 部署后 /ready 正常;2) 公网 bundle findings=0;3) live smoke readiness PASS;4) 公网移动视觉专项通过;5) 用户真机确认 iPhone 11 企业微信不拥挤、审批可点、Redmi K60 不回归后回填 UI6-6 和 R6 问题状态。","流程级验证:bash deploy/trialrun/deploy.sh;LIVE_SMOKE_BASE_URL=https://oa.hyinda.com bash scripts/qa/run-live-smoke-readiness.sh;E2E_MODE=live 或 BASE_URL=https://oa.hyinda.com 运行移动视觉专项;人工真机复测并回填 docs/trialrun/manual-trialrun-report.md。",bash|playwright|manual,DONE,DONE,TODO,deploy/trialrun/*|docs/trialrun/manual-trialrun-report.md,["RS6-6"],PAUSED:已部署到 https://oa.hyinda.com;/ready 正常;公网 bundle findings=0;live smoke readiness PASS,报告 artifacts/qa-flow/live-smoke-readiness-20260511-124651;公网移动视觉专项 7 passed。用户暂时暂停真机验证;仍待 iPhone 11 企业微信不拥挤、审批可点、Redmi K60 不回归后再置 Regression DONE。
|
RS6-7,部署公网复检并回填 R6 移动阻断,本地验证通过后部署公网,复检 /ready、bundle、live smoke、PDF OCR 和企微个人应用消息,等待真机确认后关闭或降级 R6-ISSUE-001。,"1) 部署后 /ready 正常;2) 公网 bundle findings=0;3) live smoke readiness PASS;4) 公网移动视觉专项通过;5) 用户真机确认 iPhone 11 企业微信不拥挤、审批可点、Redmi K60 不回归后回填 UI6-6 和 R6 问题状态。","流程级验证:bash deploy/trialrun/deploy.sh;LIVE_SMOKE_BASE_URL=https://oa.hyinda.com bash scripts/qa/run-live-smoke-readiness.sh;E2E_MODE=live 或 BASE_URL=https://oa.hyinda.com 运行移动视觉专项;人工真机复测并回填 docs/trialrun/manual-trialrun-report.md。",bash|playwright|manual,DONE,DONE,DONE,deploy/trialrun/*|docs/trialrun/manual-trialrun-report.md,["RS6-6"],已由 WCM6/MME6/MUD6 升级收口:多手机企业微信视觉自动化、公网复检和用户代表性真机确认完成;R6-ISSUE-001 已关闭,R6-3 不再因此阻塞。
|
||||||
|
|||||||
|
无法渲染此文件,因为它包含了意外字符,其位于第 3 行和第 756 列。
|
@@ -5,4 +5,4 @@ WCM6-3,企业微信 WebView 视口字号安全区基线,"补充 企业微信 iPh
|
|||||||
WCM6-4,底部 Tab 与首页宫格视觉密度重整,"底部导航保持四项等宽但减轻 active 大块深蓝态,badge 独立定位;首页宫格保持四项同行并固定图标槽和标题槽,避免 iPhone 企业微信真机上图标和文字互相挤压。","1) 底部首页/审批/报销/我的四项等宽、中心点命中自身;2) active 态不挤占相邻图标;3) badge 不参与图标文字排版;4) 首页宫格 4 项同一行,标题单行省略,触控高度不低于 44px。","流程级验证:Playwright 320/360/375/390/393/414/430 宽度截图;断言 nav/action 图标、文字、badge 不重叠;点击审批进入 /mobile/approvals;截图输出 output/playwright/r6-wecom-mobile-rendering-hardening/。",playwright,DONE,DONE,DONE,client/src/components/mobile/MobileBottomNav.tsx|client/src/pages/MobilePortalPage.tsx|client/src/styles/mobile-touch.css,"[""WCM6-2"",""WCM6-3""]",WCM6-4 completed:底部 active 态改为轻量选中态,待办 badge 固定为按钮级角标,不参与图标/文字排版;320/360/375/390/393/414/430px 企业微信多手机视觉矩阵通过。回滚路径:恢复 RS6 前一版移动导航样式。
|
WCM6-4,底部 Tab 与首页宫格视觉密度重整,"底部导航保持四项等宽但减轻 active 大块深蓝态,badge 独立定位;首页宫格保持四项同行并固定图标槽和标题槽,避免 iPhone 企业微信真机上图标和文字互相挤压。","1) 底部首页/审批/报销/我的四项等宽、中心点命中自身;2) active 态不挤占相邻图标;3) badge 不参与图标文字排版;4) 首页宫格 4 项同一行,标题单行省略,触控高度不低于 44px。","流程级验证:Playwright 320/360/375/390/393/414/430 宽度截图;断言 nav/action 图标、文字、badge 不重叠;点击审批进入 /mobile/approvals;截图输出 output/playwright/r6-wecom-mobile-rendering-hardening/。",playwright,DONE,DONE,DONE,client/src/components/mobile/MobileBottomNav.tsx|client/src/pages/MobilePortalPage.tsx|client/src/styles/mobile-touch.css,"[""WCM6-2"",""WCM6-3""]",WCM6-4 completed:底部 active 态改为轻量选中态,待办 badge 固定为按钮级角标,不参与图标/文字排版;320/360/375/390/393/414/430px 企业微信多手机视觉矩阵通过。回滚路径:恢复 RS6 前一版移动导航样式。
|
||||||
WCM6-5,R6 核心移动页巡检收口,"巡检审批、报销、通知、我的、票据上传和报销详情等 R6 核心移动页,必要时把桌面通用密度替换为移动 token,但不扩展新功能。","1) R6 核心移动页无 PC 壳、无桌面表格、无横向溢出;2) 图标渲染不依赖字体连字;3) 核心按钮不遮挡内容;4) 报销详情深层页继续隐藏底部 Tab。","流程级验证:访问 /mobile、/mobile/approvals、/mobile/reimbursements、/mobile/reimbursements/:id、/mobile/me、/mobile/notifications、/mobile/capture;断言无横向溢出、无 visible table、底部或吸底操作命中正常。",playwright|manual,DONE,DONE,DONE,client/src/pages/Mobile*.tsx,["WCM6-4"],WCM6-5 completed:/mobile、审批、报销、通知、我的、票据上传、报销详情核心路径通过 WCM6 专项与 RS6 响应式回归;无横向溢出、无 PC 表格、深层详情继续隐藏底部 Tab。回滚路径:按文件恢复本轮样式与组件改动。
|
WCM6-5,R6 核心移动页巡检收口,"巡检审批、报销、通知、我的、票据上传和报销详情等 R6 核心移动页,必要时把桌面通用密度替换为移动 token,但不扩展新功能。","1) R6 核心移动页无 PC 壳、无桌面表格、无横向溢出;2) 图标渲染不依赖字体连字;3) 核心按钮不遮挡内容;4) 报销详情深层页继续隐藏底部 Tab。","流程级验证:访问 /mobile、/mobile/approvals、/mobile/reimbursements、/mobile/reimbursements/:id、/mobile/me、/mobile/notifications、/mobile/capture;断言无横向溢出、无 visible table、底部或吸底操作命中正常。",playwright|manual,DONE,DONE,DONE,client/src/pages/Mobile*.tsx,["WCM6-4"],WCM6-5 completed:/mobile、审批、报销、通知、我的、票据上传、报销详情核心路径通过 WCM6 专项与 RS6 响应式回归;无横向溢出、无 PC 表格、深层详情继续隐藏底部 Tab。回滚路径:按文件恢复本轮样式与组件改动。
|
||||||
WCM6-6,补强企业微信多手机与字体失败视觉自动化,"新增或补强移动视觉 E2E,覆盖 iPhone/Android 企业微信 UA、多手机视口和 Material Symbols 字体失败场景,弥补此前只用单一模拟视口导致的真实企业微信 WebView 差异。","1) 新视觉用例覆盖 iPhone 12 企业微信近似视口 390x844;2) 拦截 Material Symbols 字体后移动核心图标仍正常;3) 自动截图写入 output/playwright/r6-wecom-mobile-rendering-hardening;4) 相关旧移动回归不下降。","流程级验证:cd client && npm run test:e2e -- r6-wecom-mobile-rendering-hardening.spec.ts r5-real-device-feedback-mobile-ux.spec.ts r6-mobile-responsive-system-hotfix.spec.ts r124-mobile-entry-matrix.spec.ts --workers=1 --reporter=list。",playwright,DONE,DONE,DONE,client/tests/e2e/r6-wecom-mobile-rendering-hardening.spec.ts|client/tests/e2e/*mobile*.spec.ts,["WCM6-5"],WCM6-6 completed:WCM6 专项 8 passed;R6 响应式矩阵拆分补跑 320/360/375/390/393/414/430px 全部通过;R124 入口矩阵 1 passed;R5 真机反馈模拟 2 passed;截图输出 output/playwright/r6-wecom-mobile-rendering-hardening/。自动化不能替代真机;回滚路径:移除新增视觉 spec 和截图产物。
|
WCM6-6,补强企业微信多手机与字体失败视觉自动化,"新增或补强移动视觉 E2E,覆盖 iPhone/Android 企业微信 UA、多手机视口和 Material Symbols 字体失败场景,弥补此前只用单一模拟视口导致的真实企业微信 WebView 差异。","1) 新视觉用例覆盖 iPhone 12 企业微信近似视口 390x844;2) 拦截 Material Symbols 字体后移动核心图标仍正常;3) 自动截图写入 output/playwright/r6-wecom-mobile-rendering-hardening;4) 相关旧移动回归不下降。","流程级验证:cd client && npm run test:e2e -- r6-wecom-mobile-rendering-hardening.spec.ts r5-real-device-feedback-mobile-ux.spec.ts r6-mobile-responsive-system-hotfix.spec.ts r124-mobile-entry-matrix.spec.ts --workers=1 --reporter=list。",playwright,DONE,DONE,DONE,client/tests/e2e/r6-wecom-mobile-rendering-hardening.spec.ts|client/tests/e2e/*mobile*.spec.ts,["WCM6-5"],WCM6-6 completed:WCM6 专项 8 passed;R6 响应式矩阵拆分补跑 320/360/375/390/393/414/430px 全部通过;R124 入口矩阵 1 passed;R5 真机反馈模拟 2 passed;截图输出 output/playwright/r6-wecom-mobile-rendering-hardening/。自动化不能替代真机;回滚路径:移除新增视觉 spec 和截图产物。
|
||||||
WCM6-7,部署公网复检并等待真机门禁,"本地验证通过后受控部署,复检 /ready、bundle、live smoke、PDF OCR 和企微个人应用消息;等待 iPhone 12 / iOS 14.1 / 企业微信与 Redmi K60 真机确认后再关闭或降级 R6-ISSUE-001。","1) 部署后 /ready 正常;2) 公网 bundle findings=0;3) live smoke readiness PASS;4) 公网移动视觉专项通过;5) 真机确认前 R6-ISSUE-001 仍 open/PAUSED。","流程级验证:bash deploy/trialrun/deploy.sh;LIVE_SMOKE_BASE_URL=https://oa.hyinda.com bash scripts/qa/run-live-smoke-readiness.sh;E2E_MODE=live 运行移动视觉专项;人工真机确认 iPhone 12 / iOS 14.1 / 企业微信和 Redmi K60 企业微信。",bash|playwright|manual,DONE,DONE,TODO,deploy/trialrun/*|docs/trialrun/*.md,["WCM6-6"],WCM6-7 automated completed:已部署公网,/ready 正常,public bundle findings=0,live smoke readiness PASS,真实 OCR 样票返回“重复发票已用于报销”的可解释终态且测试通过,公网 WCM6 移动视觉专项 8 passed;真机确认前 R6-ISSUE-001 保持 open/PAUSED,R6-3 不启动。默认不覆盖远端 .env。
|
WCM6-7,部署公网复检并等待真机门禁,"本地验证通过后受控部署,复检 /ready、bundle、live smoke、PDF OCR 和企微个人应用消息;等待 iPhone 12 / iOS 14.1 / 企业微信与 Redmi K60 真机确认后再关闭或降级 R6-ISSUE-001。","1) 部署后 /ready 正常;2) 公网 bundle findings=0;3) live smoke readiness PASS;4) 公网移动视觉专项通过;5) 真机确认前 R6-ISSUE-001 仍 open/PAUSED。","流程级验证:bash deploy/trialrun/deploy.sh;LIVE_SMOKE_BASE_URL=https://oa.hyinda.com bash scripts/qa/run-live-smoke-readiness.sh;E2E_MODE=live 运行移动视觉专项;人工真机确认 iPhone 12 / iOS 14.1 / 企业微信和 Redmi K60 企业微信。",bash|playwright|manual,DONE,DONE,DONE,deploy/trialrun/*|docs/trialrun/*.md,["WCM6-6"],WCM6/MME6 真机门禁已关闭:用户确认当前企业微信移动设计可以;MUD6 继续推广到全部专用 /mobile 页面。此前公网 /ready、bundle、live smoke、真实 OCR 和 WCM6 公网视觉专项均已通过;默认不覆盖远端 .env。
|
||||||
|
|||||||
|
无法渲染此文件,因为它包含了意外字符,其位于第 3 行和第 836 列。
|
@@ -5,4 +5,4 @@ MME6-3,我的页头像与入口槽位自适应,"将 /mobile/me 的个人信息
|
|||||||
MME6-4,移动卡片顶边视觉收口,"仅在 /mobile 页面禁用 SectionPanel 顶部蓝青色渐变线,保留白卡、浅边框和轻阴影,解决 iOS16.2 企业微信卡片顶部漏边观感。","1) /mobile/me 的 SectionPanel 顶部无蓝青色渐变线;2) PC SectionPanel 装饰线不受影响;3) 移动卡片边框、圆角、阴影仍清晰。","流程级验证:WCM6 Playwright 390x844 iPhone 企业微信近似视口打开 /mobile/me;断言移动 SectionPanel ::before 不可见或透明;截图输出 output/playwright/r6-wecom-mobile-rendering-hardening/。",css|playwright,DONE,DONE,DONE,client/src/styles/mobile-touch.css|client/src/layouts/AppShellLayout.tsx,["MME6-2"],MME6-4 completed:仅在 hyb-mobile-shell 下禁用 SectionPanel 顶部蓝青色渐变线;PC SectionPanel 不在本轮改动范围;WCM6 断言移动卡片顶边不可见。回滚路径:恢复移动 SectionPanel 伪元素覆盖。
|
MME6-4,移动卡片顶边视觉收口,"仅在 /mobile 页面禁用 SectionPanel 顶部蓝青色渐变线,保留白卡、浅边框和轻阴影,解决 iOS16.2 企业微信卡片顶部漏边观感。","1) /mobile/me 的 SectionPanel 顶部无蓝青色渐变线;2) PC SectionPanel 装饰线不受影响;3) 移动卡片边框、圆角、阴影仍清晰。","流程级验证:WCM6 Playwright 390x844 iPhone 企业微信近似视口打开 /mobile/me;断言移动 SectionPanel ::before 不可见或透明;截图输出 output/playwright/r6-wecom-mobile-rendering-hardening/。",css|playwright,DONE,DONE,DONE,client/src/styles/mobile-touch.css|client/src/layouts/AppShellLayout.tsx,["MME6-2"],MME6-4 completed:仅在 hyb-mobile-shell 下禁用 SectionPanel 顶部蓝青色渐变线;PC SectionPanel 不在本轮改动范围;WCM6 断言移动卡片顶边不可见。回滚路径:恢复移动 SectionPanel 伪元素覆盖。
|
||||||
MME6-5,补强我的页自适应视觉断言,"在 WCM6 企业微信多手机视觉专项中补 /mobile/me 槽位间距、两列入口、卡片顶边和底部导航稳定断言。","1) 覆盖 320/360/375/390/393/414/430px;2) 覆盖 Material Symbols 字体失败场景;3) 截图输出到既有 WCM6 目录;4) 不把真实企业微信真机验收替换为自动化。","流程级验证:cd client && npm run test:e2e -- r6-wecom-mobile-rendering-hardening.spec.ts --workers=1 --reporter=list。",playwright,DONE,DONE,DONE,client/tests/e2e/r6-wecom-mobile-rendering-hardening.spec.ts,"[""MME6-3"",""MME6-4""]",MME6-5 completed:WCM6 专项已补 /mobile/me 槽位间距、两列入口、卡片顶边和底部导航断言;8 passed,截图输出 output/playwright/r6-wecom-mobile-rendering-hardening/。回滚路径:移除新增断言;自动化不能替代真机。
|
MME6-5,补强我的页自适应视觉断言,"在 WCM6 企业微信多手机视觉专项中补 /mobile/me 槽位间距、两列入口、卡片顶边和底部导航稳定断言。","1) 覆盖 320/360/375/390/393/414/430px;2) 覆盖 Material Symbols 字体失败场景;3) 截图输出到既有 WCM6 目录;4) 不把真实企业微信真机验收替换为自动化。","流程级验证:cd client && npm run test:e2e -- r6-wecom-mobile-rendering-hardening.spec.ts --workers=1 --reporter=list。",playwright,DONE,DONE,DONE,client/tests/e2e/r6-wecom-mobile-rendering-hardening.spec.ts,"[""MME6-3"",""MME6-4""]",MME6-5 completed:WCM6 专项已补 /mobile/me 槽位间距、两列入口、卡片顶边和底部导航断言;8 passed,截图输出 output/playwright/r6-wecom-mobile-rendering-hardening/。回滚路径:移除新增断言;自动化不能替代真机。
|
||||||
MME6-6,本地验证与状态回填,"执行前端 typecheck/build、WCM6 专项和必要移动回归;通过后回填 MME6-1 到 MME6-6。","1) typecheck 通过;2) build 通过;3) WCM6 专项通过;4) git diff --check 通过;5) R6-ISSUE-001 仍 open/PAUSED。","流程级验证:cd client && npm run typecheck;cd client && npm run build;cd client && npm run test:e2e -- r6-wecom-mobile-rendering-hardening.spec.ts --workers=1 --reporter=list;git diff --check。",npm|playwright|git,DONE,DONE,DONE,client|issues|docs,["MME6-5"],MME6-6 completed:client typecheck 通过;client build 通过;WCM6 专项 8 passed;R5/R6/R124 移动相关回归 10 passed;R6-ISSUE-001 仍 open/PAUSED。回滚路径:撤销本轮前端、测试与文档变更。
|
MME6-6,本地验证与状态回填,"执行前端 typecheck/build、WCM6 专项和必要移动回归;通过后回填 MME6-1 到 MME6-6。","1) typecheck 通过;2) build 通过;3) WCM6 专项通过;4) git diff --check 通过;5) R6-ISSUE-001 仍 open/PAUSED。","流程级验证:cd client && npm run typecheck;cd client && npm run build;cd client && npm run test:e2e -- r6-wecom-mobile-rendering-hardening.spec.ts --workers=1 --reporter=list;git diff --check。",npm|playwright|git,DONE,DONE,DONE,client|issues|docs,["MME6-5"],MME6-6 completed:client typecheck 通过;client build 通过;WCM6 专项 8 passed;R5/R6/R124 移动相关回归 10 passed;R6-ISSUE-001 仍 open/PAUSED。回滚路径:撤销本轮前端、测试与文档变更。
|
||||||
MME6-7,部署公网复检并等待真机门禁,"本地验证通过后受控部署,复检 /ready、bundle、live smoke 和公网 WCM6 视觉专项;等待 iPhone12/iOS14、iPhone14/iOS16.2、Redmi K60 真机确认。","1) 部署后 /ready 正常;2) bundle findings=0;3) live smoke PASS;4) 公网 WCM6 视觉专项通过;5) 真机确认前 R6-ISSUE-001 不关闭。","流程级验证:bash deploy/trialrun/deploy.sh;node scripts/qa/check-public-bundle-copy.mjs https://oa.hyinda.com;LIVE_SMOKE_BASE_URL=https://oa.hyinda.com bash scripts/qa/run-live-smoke-readiness.sh;E2E_MODE=live 运行 WCM6 专项;人工真机确认。",bash|playwright|manual,DONE,DONE,TODO,deploy/trialrun/*|docs/trialrun/*.md,["MME6-6"],MME6-7 automated completed:已部署公网,/ready 正常,public bundle findings=0,live smoke readiness PASS,真实 OCR 样票返回重复发票可解释终态且测试通过,公网 WCM6 视觉专项 8 passed,报告 artifacts/qa-flow/live-smoke-readiness-20260511-230918;真机通过前 R6-ISSUE-001 保持 open/PAUSED,R6-3 不启动。默认不覆盖远端 .env。
|
MME6-7,部署公网复检并等待真机门禁,"本地验证通过后受控部署,复检 /ready、bundle、live smoke 和公网 WCM6 视觉专项;等待 iPhone12/iOS14、iPhone14/iOS16.2、Redmi K60 真机确认。","1) 部署后 /ready 正常;2) bundle findings=0;3) live smoke PASS;4) 公网 WCM6 视觉专项通过;5) 真机确认前 R6-ISSUE-001 不关闭。","流程级验证:bash deploy/trialrun/deploy.sh;node scripts/qa/check-public-bundle-copy.mjs https://oa.hyinda.com;LIVE_SMOKE_BASE_URL=https://oa.hyinda.com bash scripts/qa/run-live-smoke-readiness.sh;E2E_MODE=live 运行 WCM6 专项;人工真机确认。",bash|playwright|manual,DONE,DONE,DONE,deploy/trialrun/*|docs/trialrun/*.md,["MME6-6"],MME6-7 completed:已部署公网,/ready 正常,public bundle findings=0,live smoke readiness PASS,真实 OCR 样票返回重复发票可解释终态且测试通过,公网 WCM6 视觉专项 8 passed;用户确认当前设计可以,R6-ISSUE-001 已关闭,后续移动回归另建新 Issue。默认不覆盖远端 .env。
|
||||||
|
|||||||
|
无法渲染此文件,因为它包含了意外字符,其位于第 3 行和第 575 列。
|
@@ -0,0 +1,8 @@
|
|||||||
|
ID,Title,Description,Acceptance,Test_Method,Tools,Dev_Status,Review1_Status,Regression_Status,Files,Dependencies,Notes
|
||||||
|
MUD6-1,建档并关闭 R6 移动阻断,"基于用户确认 MME6 真机效果可以,将 R6-ISSUE-001 从 open/PAUSED 关闭或降级,并记录本轮范围仅限专用 /mobile 路由。","1) 本 CSV 存在且校验通过;2) R6-ISSUE-001 记录为已处理,不再阻塞 R6-3;3) 本轮不改后端、权限、审批链、PDF-only 或企微通知通道。","流程级验证:python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-05-11_23-26-00-r6-mobile-unified-design-rollout.csv;人工核对 docs/trialrun/manual-trialrun-report.md 与 docs/trialrun/real-device-public-checklist.md 中 R6-ISSUE-001 状态。",python3|manual,DONE,DONE,DONE,issues/2026-05-11_23-26-00-r6-mobile-unified-design-rollout.csv|docs/trialrun/*.md,[],MUD6-1 completed:CSV 校验通过;R6-ISSUE-001 已按用户真机确认关闭,UI6/RS6/WCM6/MME6 收口项同步 DONE;本轮不改后端、权限、审批链、PDF-only 或企微通知通道。
|
||||||
|
MUD6-2,规约与交付 Skill 稳定,"把 MME6 槽位自适应、移动卡片无蓝青色顶边、专用 /mobile 路由统一矩阵写入产品规约、交付实践和 hyb-oa delivery Skill。","1) docs/product-design-rules.md 明确所有专用 /mobile 页面共享槽位、卡片、列表、底部导航和吸底操作;2) docs/agent-development-practices.md 与 Skill 明确新移动页必须复用该体系;3) 不复制竞品文案或交互细节。","流程级验证:python3 /home/shunli/.codex/skills/.system/skill-creator/scripts/quick_validate.py .agents/skills/hyb-oa-delivery;rg -n \"移动槽位|专用 /mobile|mobile-card|R6-ISSUE-001|MUD6\" docs .agents/skills/hyb-oa-delivery issues。",rg|python3,DONE,DONE,DONE,docs/product-design-rules.md|docs/agent-development-practices.md|.agents/skills/hyb-oa-delivery/SKILL.md,["MUD6-1"],MUD6-2 completed:产品规约、交付实践和 Skill 已写入专用 /mobile 统一槽位、卡片、列表、吸底操作和跨视口矩阵;Skill quick_validate 通过。
|
||||||
|
MUD6-3,共享移动样式抽象,"将 MME6 的 mobile-me 专用槽位样式抽象为通用移动样式,保留兼容别名,供所有 /mobile 页面复用。","1) 新增通用移动槽位、列表卡、表单网格、空状态和提示样式;2) mobile-me 旧类继续兼容或已替换,不破坏既有测试;3) 移动卡片顶边禁用仍只作用于 /mobile。","流程级验证:cd client && npm run typecheck;WCM6 专项打开 /mobile/me 仍通过槽位间距、两列入口和卡片顶边断言。",typescript|css|playwright,DONE,DONE,DONE,client/src/styles/mobile-touch.css|client/src/pages/MobileMePage.tsx,["MUD6-2"],MUD6-3 completed:已抽象 mobile-slot/mobile-action/mobile-list-card/mobile-empty-state 等通用样式并保留 mobile-me 兼容别名;client typecheck/build 通过。
|
||||||
|
MUD6-4,统一高频移动页,"统一 /mobile、/mobile/me、/mobile/profile、/mobile/notifications 的卡片、入口项、列表卡和操作区,不改变角色入口裁剪。","1) 高频页全部使用 mobile-card 或共享移动槽位;2) 无 card-std p-4 主容器;3) 无横向滚动、图文不重叠、底部 Tab 可点;4) 角色差异按权限裁剪入口不变。","流程级验证:移动统一视觉专项覆盖 /mobile、/mobile/me、/mobile/profile、/mobile/notifications;断言 scrollWidth <= viewportWidth + 2、无 PC 壳、底部 Tab 中心点命中。",typescript|playwright,DONE,DONE,DONE,client/src/pages/MobilePortalPage.tsx|client/src/pages/MobileMePage.tsx|client/src/pages/MobileProfilePage.tsx|client/src/pages/MobileNotificationsPage.tsx,["MUD6-3"],MUD6-4 completed:高频移动页已纳入 mobile-card、共享槽位和移动列表卡;本地与公网 MUD6 视觉专项均 7 passed。
|
||||||
|
MUD6-5,统一业务与工具移动页,"统一 /mobile/approvals、/mobile/reimbursements、/mobile/reimbursements/:id、/mobile/capture、/mobile/leave、/mobile/assets/scan 和扫码组件的移动卡片与槽位布局。","1) 业务页不再使用桌面 card-std 作为主容器;2) 报销详情继续隐藏底部 Tab,吸底双按钮同行;3) PDF-only 发票上传入口不回归;4) 资产扫码业务逻辑不变。","流程级验证:移动统一视觉专项覆盖全部专用 /mobile 路由;业务回归跑 r6-wecom-mobile-rendering-hardening、r6-mobile-responsive-system-hotfix、r124-mobile-entry-matrix;必要时补审批、报销、请假相关 E2E。",typescript|playwright,DONE,DONE,DONE,client/src/pages/MobileApprovalsPage.tsx|client/src/pages/MobileReimbursementsPage.tsx|client/src/pages/MobileReimbursementDetailPage.tsx|client/src/pages/MobileCapturePage.tsx|client/src/pages/MobileLeavePage.tsx|client/src/pages/MobileAssetScanPage.tsx|client/src/components/mobile/BarcodeScannerPanel.tsx,["MUD6-3"],MUD6-5 completed:审批、报销、报销详情、票据上传、请假、资产扫码和扫码组件已统一移动卡片/槽位;WCM6/RS6/R124 回归 16 passed。
|
||||||
|
MUD6-6,补强全移动页视觉矩阵,"新增或扩展 Playwright 专项,遍历所有专用 /mobile 路由,覆盖企业微信 iOS UA、企业微信 Android UA、普通移动浏览器 UA 与 320/360/375/390/393/414/430px 宽度。","1) 全 /mobile 路由无横向滚动;2) 无 PC 侧边栏、桌面表格和桌面卡片主容器;3) 底部 Tab 可点;4) 图文槽位不重叠;5) 报销详情深层页规则不回归。","流程级验证:cd client && npm run test:e2e -- r6-mobile-unified-design-rollout.spec.ts --workers=1 --reporter=list;截图输出 output/playwright/r6-mobile-unified-design-rollout/。",playwright,DONE,DONE,DONE,client/tests/e2e/r6-mobile-unified-design-rollout.spec.ts,"[""MUD6-4"",""MUD6-5""]",MUD6-6 completed:新增全 /mobile 视觉专项覆盖 7 个宽度、3 类 UA 和全部专用 /mobile 路由;本地 7 passed,截图输出 output/playwright/r6-mobile-unified-design-rollout/。
|
||||||
|
MUD6-7,部署公网复检并恢复 R6-3 条件,"本地验证通过后受控部署,复检 /ready、bundle、live smoke、真实 PDF OCR、企业微信个人应用消息和公网移动统一视觉专项;确认 R6-3 可恢复。","1) client typecheck/build 通过;2) 本地与公网移动统一视觉通过;3) /ready 正常、bundle findings=0、live smoke readiness PASS;4) R6-3 恢复条件写入试运行文档;5) 用户仅需抽查三台代表性真机。","流程级验证:cd client && npm run typecheck;cd client && npm run build;bash deploy/trialrun/deploy.sh;node scripts/qa/check-public-bundle-copy.mjs https://oa.hyinda.com;LIVE_SMOKE_BASE_URL=https://oa.hyinda.com bash scripts/qa/run-live-smoke-readiness.sh;E2E_MODE=live 运行移动统一视觉专项。",npm|bash|playwright|manual,DONE,DONE,DONE,deploy/trialrun/*|docs/trialrun/*.md,["MUD6-6"],MUD6-7 completed:已部署到 https://oa.hyinda.com;/ready ok,public bundle findings=0,live smoke readiness PASS,真实 OCR 样票为重复发票可解释终态且通过;公网 MUD6 视觉专项 7 passed;报告 artifacts/qa-flow/live-smoke-readiness-20260511-235918。默认不覆盖远端 .env;建议用户抽查 iPhone12/iOS14、iPhone14/iOS16.2、Redmi K60。
|
||||||
|
无法渲染此文件,因为它包含了意外字符,其位于第 3 行和第 600 列。
|
在新工单中引用
屏蔽一个用户