R108/A1-A7 固化企业微信移动端视觉门禁
这个提交包含在:
@@ -33,7 +33,7 @@ export function MobileBottomNav() {
|
||||
const pendingCount = pendingCountQuery.data ?? 0;
|
||||
|
||||
return (
|
||||
<nav className="fixed inset-x-0 bottom-0 z-20 border-t border-zinc-200 bg-white/95 px-2 pb-[max(0.5rem,env(safe-area-inset-bottom))] pt-2 shadow-[0_-8px_24px_rgba(15,23,42,0.08)] backdrop-blur md:hidden">
|
||||
<nav className="z-20 shrink-0 border-t border-zinc-200 bg-white/95 px-2 pb-[max(0.5rem,env(safe-area-inset-bottom))] pt-2 shadow-[0_-8px_24px_rgba(15,23,42,0.08)] backdrop-blur md:hidden">
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{MOBILE_NAV_ITEMS.map((item) => {
|
||||
const disabled = item.anyOf ? !hasAnyPermission(permissions, [...item.anyOf]) : false;
|
||||
|
||||
@@ -28,10 +28,10 @@ export function AppShellLayout() {
|
||||
<div className="relative flex min-h-screen overflow-hidden bg-[var(--color-bg-canvas)]">
|
||||
{isMobileRoute ? null : <Sidebar />}
|
||||
|
||||
<main className={`flex min-w-0 flex-1 flex-col ${isMobileRoute ? "" : "lg:ml-72"}`}>
|
||||
<main className={`flex min-w-0 flex-1 flex-col ${isMobileRoute ? "h-[100dvh] overflow-hidden" : "lg:ml-72"}`}>
|
||||
{isMobileRoute ? null : <Topbar />}
|
||||
|
||||
<div className={`flex-1 px-4 py-5 ${isMobileRoute ? "pb-24 pt-4 sm:px-5 sm:py-5" : "sm:px-7 sm:py-7 lg:px-8"}`}>
|
||||
<div className={`flex-1 px-4 py-5 ${isMobileRoute ? "min-h-0 overflow-y-auto pb-5 pt-4 sm:px-5 sm:py-5" : "sm:px-7 sm:py-7 lg:px-8"}`}>
|
||||
<div className="layout-container max-w-none">
|
||||
<ErrorBoundary>
|
||||
<Outlet />
|
||||
|
||||
@@ -99,6 +99,17 @@ function workflowStatusLabel(status: string) {
|
||||
return status;
|
||||
}
|
||||
|
||||
function isInternalId(value: string | null | undefined) {
|
||||
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value ?? "");
|
||||
}
|
||||
|
||||
function publicReference(value: string | null | undefined) {
|
||||
if (!value || isInternalId(value)) {
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function resolveErrorMessage(error: unknown) {
|
||||
if (isApiError(error)) {
|
||||
return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
|
||||
@@ -170,18 +181,17 @@ function matchesEntityFilter(entityType: string | null, filter: EntityFilter) {
|
||||
|
||||
function toVisibleRemoteTask(task: WorkflowTaskSummary): VisibleTaskCard {
|
||||
const entityType = task.workflowInstance?.entityType ?? null;
|
||||
const entityId = task.workflowInstance?.entityId ?? null;
|
||||
return {
|
||||
id: task.id,
|
||||
title: task.stepName,
|
||||
subtitle:
|
||||
task.status === "completed"
|
||||
? `业务:${entityLabel(entityType)} · 完成于 ${formatDateTime(task.completedAt)}`
|
||||
: `业务:${entityLabel(entityType)} · 实体ID:${entityId ?? "-"}`,
|
||||
: `业务:${entityLabel(entityType)}`,
|
||||
status: task.status,
|
||||
entityType,
|
||||
entityId,
|
||||
reference: entityId ?? task.id,
|
||||
entityId: task.workflowInstance?.entityId ?? null,
|
||||
reference: publicReference(task.workflowInstance?.entityId),
|
||||
allowedActions: task.allowedActions,
|
||||
completedAt: task.completedAt,
|
||||
isCached: false,
|
||||
@@ -351,7 +361,7 @@ export function MobileApprovalsPage() {
|
||||
status: item.detail.task.status,
|
||||
entityType: item.taskSnapshot.entityType ?? item.detail.workflowInstance?.entityType ?? null,
|
||||
entityId: item.taskSnapshot.entityId,
|
||||
reference: item.taskSnapshot.reference ?? item.detail.task.id,
|
||||
reference: publicReference(item.taskSnapshot.reference),
|
||||
allowedActions: item.detail.allowedActions,
|
||||
completedAt: item.detail.task.completedAt,
|
||||
isCached: true,
|
||||
@@ -379,6 +389,7 @@ export function MobileApprovalsPage() {
|
||||
|
||||
const selectedTask = detailQuery.data ?? cachedDetailQuery.data?.detail ?? selectedCachedTaskDetail;
|
||||
const selectedReference = selectedTask ? resolveWorkflowTaskReferenceSnapshot(selectedTask) : null;
|
||||
const selectedDisplayReference = publicReference(selectedReference?.reference);
|
||||
const selectedTaskAllowsActions = Boolean(taskTab === "pending" && selectedTask?.task.status === "pending");
|
||||
const isOfflineUncachedDeepLink = Boolean(selectedTaskId) && !selectedTask && !canUseRemote;
|
||||
|
||||
@@ -738,7 +749,7 @@ export function MobileApprovalsPage() {
|
||||
<PageScaffold className="space-y-4">
|
||||
<PageHeader
|
||||
title="移动审批"
|
||||
subtitle={deepLinkTaskId ? `待办:${deepLinkTaskId}` : "处理待办。"}
|
||||
subtitle={deepLinkTaskId ? "已定位指定待办。" : "处理待办。"}
|
||||
/>
|
||||
|
||||
{!isOnline ? (
|
||||
@@ -915,7 +926,7 @@ export function MobileApprovalsPage() {
|
||||
className="mt-1 h-4 w-4 rounded border-zinc-300"
|
||||
checked={isBatchSelected}
|
||||
onChange={() => toggleBatchTask(task.id)}
|
||||
aria-label={`选择任务 ${task.id}`}
|
||||
aria-label={`选择${entityLabel(task.entityType)}待办`}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@@ -927,8 +938,8 @@ export function MobileApprovalsPage() {
|
||||
<div className="font-medium text-zinc-900">{task.title}</div>
|
||||
<StatusBadge status={task.status} label={workflowStatusLabel(task.status)} />
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-zinc-500">任务:{task.id}</div>
|
||||
<div className="mt-1 text-xs text-zinc-500">{task.subtitle}</div>
|
||||
{task.reference ? <div className="mt-1 text-xs text-zinc-500">业务单号:{task.reference}</div> : null}
|
||||
{task.isCached ? <div className="mt-1 text-xs text-zinc-500">来源:离线缓存</div> : null}
|
||||
</button>
|
||||
</div>
|
||||
@@ -949,11 +960,8 @@ export function MobileApprovalsPage() {
|
||||
<div className="font-medium text-zinc-900">{task.title}</div>
|
||||
<StatusBadge status={task.status} label={workflowStatusLabel(task.status)} />
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-zinc-500">任务:{task.id}</div>
|
||||
<div className="mt-1 text-xs text-zinc-500">{task.subtitle}</div>
|
||||
{task.reference && task.reference !== task.id ? (
|
||||
<div className="mt-1 text-xs text-zinc-500">业务参考:{task.reference}</div>
|
||||
) : null}
|
||||
{task.reference ? <div className="mt-1 text-xs text-zinc-500">业务单号:{task.reference}</div> : null}
|
||||
{task.isCached ? <div className="mt-1 text-xs text-zinc-500">来源:离线缓存</div> : null}
|
||||
</button>
|
||||
);
|
||||
@@ -978,7 +986,7 @@ export function MobileApprovalsPage() {
|
||||
|
||||
{isOfflineUncachedDeepLink ? (
|
||||
<p className="rounded-lg border border-orange-200 bg-orange-50 px-3 py-2 text-sm text-orange-700">
|
||||
离线未缓存该待办,请联网后重试。任务号:{selectedTaskId}
|
||||
离线未缓存该待办,请联网后重试。
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
@@ -988,7 +996,7 @@ export function MobileApprovalsPage() {
|
||||
<div>任务状态:{workflowStatusLabel(selectedTask.task.status)}</div>
|
||||
<div>步骤:{selectedTask.task.stepName}</div>
|
||||
<div>对象类型:{entityLabel(selectedTask.workflowInstance?.entityType)}</div>
|
||||
<div>业务参考:{selectedReference?.reference ?? "-"}</div>
|
||||
<div>业务单号:{selectedDisplayReference ?? "-"}</div>
|
||||
<div>创建时间:{formatDateTime(selectedTask.task.createdAt)}</div>
|
||||
<div>完成时间:{formatDateTime(selectedTask.task.completedAt)}</div>
|
||||
{selectedTask.reimbursement ? (
|
||||
@@ -1007,7 +1015,7 @@ export function MobileApprovalsPage() {
|
||||
) : null}
|
||||
{selectedTask.purchaseRequest ? (
|
||||
<>
|
||||
<div>采购单号:{selectedTask.purchaseRequest.requestNo ?? selectedTask.purchaseRequest.id}</div>
|
||||
<div>采购单号:{selectedTask.purchaseRequest.requestNo ?? "-"}</div>
|
||||
<div>申请人:{selectedTask.purchaseRequest.applicant?.name ?? "-"}</div>
|
||||
<div>供应商:{selectedTask.purchaseRequest.supplier?.name ?? "-"}</div>
|
||||
<div>金额:{selectedTask.purchaseRequest.totalAmount ?? "-"}</div>
|
||||
@@ -1015,7 +1023,7 @@ export function MobileApprovalsPage() {
|
||||
) : null}
|
||||
{selectedTask.travelRequest ? (
|
||||
<>
|
||||
<div>差旅单号:{selectedTask.travelRequest.requestNo ?? selectedTask.travelRequest.id}</div>
|
||||
<div>差旅单号:{selectedTask.travelRequest.requestNo ?? "-"}</div>
|
||||
<div>申请人:{selectedTask.travelRequest.applicant?.name ?? "-"}</div>
|
||||
<div>出差类型:{selectedTask.travelRequest.tripType === "international" ? "国际" : "国内"}</div>
|
||||
<div>预估金额:{selectedTask.travelRequest.estimatedAmount ?? "-"}</div>
|
||||
@@ -1154,7 +1162,7 @@ export function MobileApprovalsPage() {
|
||||
{currentQueueItems.map((item) => (
|
||||
<div key={item.id} className="rounded-lg border border-zinc-200 px-3 py-3 text-sm">
|
||||
<div className="font-medium text-zinc-900">
|
||||
{item.taskSnapshot.reference ?? item.taskId} · {item.action === "approve" ? "通过" : "驳回"}
|
||||
{publicReference(item.taskSnapshot.reference) ?? entityLabel(item.taskSnapshot.entityType)} · {item.action === "approve" ? "通过" : "驳回"}
|
||||
</div>
|
||||
<div className="text-zinc-500">业务:{entityLabel(item.taskSnapshot.entityType)} · 步骤:{item.taskSnapshot.stepName ?? "-"}</div>
|
||||
<div className="text-zinc-500">状态:{queueStatusLabel(item.status)} · 更新时间:{formatDateTime(item.updatedAt)}</div>
|
||||
|
||||
@@ -243,10 +243,13 @@ export function MobileCapturePage() {
|
||||
setError("");
|
||||
|
||||
if (!isOnline) {
|
||||
const enqueuedItems: MobileUploadQueueItem[] = [];
|
||||
for (const file of selectedFiles) {
|
||||
await enqueueInvoiceUpload(file);
|
||||
enqueuedItems.push(await enqueueInvoiceUpload(file));
|
||||
}
|
||||
await queryClient.invalidateQueries({ queryKey: ["mobile", "invoice-upload-queue"] });
|
||||
queryClient.setQueryData<MobileUploadQueueItem[]>(["mobile", "invoice-upload-queue"], (current) =>
|
||||
[...(current ?? []), ...enqueuedItems].sort((a, b) => a.createdAt.localeCompare(b.createdAt)),
|
||||
);
|
||||
setSelectedFiles([]);
|
||||
setMessage(`当前离线,已加入待上传队列 ${selectedFiles.length} 项`);
|
||||
return;
|
||||
@@ -296,7 +299,9 @@ export function MobileCapturePage() {
|
||||
data-testid={`mobile-capture-remove-${item.id}`}
|
||||
onClick={async () => {
|
||||
await removeQueuedInvoiceUpload(item.id);
|
||||
await queryClient.invalidateQueries({ queryKey: ["mobile", "invoice-upload-queue"] });
|
||||
queryClient.setQueryData<MobileUploadQueueItem[]>(["mobile", "invoice-upload-queue"], (current) =>
|
||||
(current ?? []).filter((queuedItem) => queuedItem.id !== item.id),
|
||||
);
|
||||
}}
|
||||
size="sm"
|
||||
type="button"
|
||||
|
||||
在新工单中引用
屏蔽一个用户