R89 A6-A7 移动触控与回归补齐

这个提交包含在:
2026-05-05 19:52:04 +08:00
父节点 ee64909811
当前提交 99ad4c6aa6
共修改 6 个文件,包含 546 行新增和 31 行删除
+40 -12
查看文件
@@ -230,6 +230,7 @@ export function MobileApprovalsPage() {
const [offlineDraftSubmitting, setOfflineDraftSubmitting] = useState(false);
const [isOnline, setIsOnline] = useState(true);
const autoSyncRequestedRef = useRef(false);
const detailSectionRef = useRef<HTMLElement | null>(null);
const ownerEmployeeId = me?.id ?? null;
const canUseRemote = isOnline && Boolean(accessToken);
@@ -258,6 +259,21 @@ export function MobileApprovalsPage() {
setSearchParams(nextParams, { replace: true });
}
function scrollDetailIntoViewOnMobile() {
if (typeof window === "undefined" || !window.matchMedia("(max-width: 767px)").matches) {
return;
}
window.requestAnimationFrame(() => {
detailSectionRef.current?.scrollIntoView({ behavior: "smooth", block: "start" });
});
}
function handleTaskSelection(taskId: string) {
updateTaskSelection(taskId);
scrollDetailIntoViewOnMobile();
}
async function invalidateWorkflowQueries(taskId: string | null = selectedTaskId) {
await Promise.all([
queryClient.invalidateQueries({ queryKey: ["mobile", "workflow", "tasks"] }),
@@ -781,11 +797,12 @@ export function MobileApprovalsPage() {
/>
{taskTab === "pending" ? (
<div className="flex flex-wrap items-center gap-2">
<div className="grid gap-2 sm:flex sm:flex-wrap sm:items-center">
<Button
type="button"
size="sm"
variant={batchMode ? "default" : "secondary"}
className="min-h-11 w-full sm:w-auto"
disabled={!filteredTaskCards.length}
onClick={() => {
setBatchMode((current) => !current);
@@ -800,13 +817,14 @@ export function MobileApprovalsPage() {
type="button"
size="sm"
variant="secondary"
className="min-h-11 w-full sm:w-auto"
disabled={!filteredTaskCards.length}
onClick={toggleSelectAllVisibleTasks}
>
{allVisibleSelected ? "清空选择" : "全选当前筛选结果"}
</Button>
) : null}
<span className="text-sm text-zinc-500">
<span className="text-sm text-zinc-500 sm:ml-1">
当前结果 {filteredTaskCards.length} 条
{batchMode ? `,已选 ${selectedBatchTaskIds.length} 条` : ""}
</span>
@@ -820,10 +838,11 @@ export function MobileApprovalsPage() {
<div className="rounded-2xl border border-zinc-200 bg-zinc-50 p-3 text-sm">
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="text-zinc-700">已选择 {selectedBatchTaskIds.length} 条待办,可在线串行处理或离线逐条入队。</div>
<div className="flex flex-wrap gap-2">
<div className="grid w-full gap-2 sm:flex sm:w-auto sm:flex-wrap">
<Button
type="button"
size="sm"
className="min-h-11 w-full sm:w-auto"
disabled={!selectedBatchTaskIds.length || batchActionMutation.isPending}
onClick={() => handleBatchAction("approve")}
>
@@ -833,6 +852,7 @@ export function MobileApprovalsPage() {
type="button"
size="sm"
variant="secondary"
className="min-h-11 w-full sm:w-auto"
disabled={!selectedBatchTaskIds.length || batchActionMutation.isPending || !batchRejectComment.trim()}
onClick={() => handleBatchAction("reject")}
>
@@ -899,7 +919,7 @@ export function MobileApprovalsPage() {
/>
<button
type="button"
className="min-w-0 flex-1 text-left"
className="min-h-16 min-w-0 flex-1 text-left"
data-testid={`mobile-approvals-task-${task.id}`}
onClick={() => toggleBatchTask(task.id)}
>
@@ -918,11 +938,11 @@ export function MobileApprovalsPage() {
return (
<button
key={task.id}
className={`w-full rounded-xl border px-3 py-3 text-left ${
className={`min-h-20 w-full rounded-xl border px-3 py-3 text-left transition-colors ${
isSelected ? "border-primary bg-primary-50" : "border-zinc-200 bg-white"
}`}
data-testid={`mobile-approvals-task-${task.id}`}
onClick={() => updateTaskSelection(task.id)}
onClick={() => handleTaskSelection(task.id)}
type="button"
>
<div className="flex items-center justify-between gap-3">
@@ -941,7 +961,7 @@ export function MobileApprovalsPage() {
</div>
</section>
<section className="card-std space-y-3 p-4" data-testid="mobile-approvals-detail">
<section ref={detailSectionRef} className="card-std scroll-mt-4 space-y-3 p-4" data-testid="mobile-approvals-detail">
<h3 className="text-base font-semibold text-zinc-900">任务详情与动作</h3>
{!selectedTaskId ? <p className="text-sm text-zinc-500">请先选择任务。</p> : null}
@@ -1021,8 +1041,9 @@ export function MobileApprovalsPage() {
<p className="text-xs text-zinc-500">离线状态下仅支持通过 / 驳回草稿,退回与转审需联网执行。</p>
) : null}
<div className="flex flex-wrap gap-2">
<div className="grid gap-2 sm:grid-cols-3">
<Button
className="min-h-12 w-full"
data-testid="mobile-approvals-approve"
disabled={isActionSubmitting || !selectedTask.allowedActions.includes("approve")}
onClick={() => {
@@ -1037,6 +1058,7 @@ export function MobileApprovalsPage() {
{canUseRemote ? "通过" : "离线加入通过草稿"}
</Button>
<Button
className="min-h-12 w-full"
data-testid="mobile-approvals-reject"
disabled={isActionSubmitting || !selectedTask.allowedActions.includes("reject")}
onClick={() => {
@@ -1052,6 +1074,7 @@ export function MobileApprovalsPage() {
{canUseRemote ? "驳回" : "离线加入驳回草稿"}
</Button>
<Button
className="min-h-12 w-full"
data-testid="mobile-approvals-return"
disabled={isActionSubmitting || !canUseRemote || !selectedTask.allowedActions.includes("return")}
onClick={() => actionMutation.mutate("return")}
@@ -1063,7 +1086,7 @@ export function MobileApprovalsPage() {
</div>
{canTransfer ? (
<div className="space-y-2 rounded-lg border border-zinc-200 p-3 text-sm">
<div className="space-y-3 rounded-lg border border-zinc-200 bg-white p-3 text-sm">
<p className="font-medium text-zinc-800">转审</p>
<select
className="h-10 w-full rounded-lg border border-zinc-300 bg-white px-3 py-2 text-sm"
@@ -1088,6 +1111,7 @@ export function MobileApprovalsPage() {
/>
<Button
className="min-h-12 w-full"
data-testid="mobile-approvals-transfer"
disabled={isActionSubmitting || !selectedTask.allowedActions.includes("transfer")}
onClick={() => actionMutation.mutate("transfer")}
@@ -1111,8 +1135,9 @@ export function MobileApprovalsPage() {
</div>
{currentQueueItems.length ? (
<div className="flex flex-wrap gap-2">
<div className="grid gap-2 sm:flex sm:flex-wrap">
<Button
className="min-h-11 w-full sm:w-auto"
data-testid="mobile-approvals-sync-queue"
disabled={!canUseRemote || syncMutation.isPending || !retryableQueueItems.length}
onClick={() => syncMutation.mutate(retryableQueueItems)}
@@ -1135,12 +1160,13 @@ export function MobileApprovalsPage() {
<div className="text-zinc-500">状态:{queueStatusLabel(item.status)} · 更新时间:{formatDateTime(item.updatedAt)}</div>
<div className="text-zinc-500">意见:{item.comment ?? "-"}</div>
<div className="text-zinc-500">最近错误:{item.lastError ?? "-"}</div>
<div className="mt-2 flex flex-wrap gap-2">
<div className="mt-2 grid gap-2 sm:flex sm:flex-wrap">
<Button
type="button"
size="sm"
variant="secondary"
onClick={() => updateTaskSelection(item.taskId)}
className="min-h-11 w-full sm:w-auto"
onClick={() => handleTaskSelection(item.taskId)}
>
查看任务
</Button>
@@ -1149,6 +1175,7 @@ export function MobileApprovalsPage() {
type="button"
size="sm"
variant="secondary"
className="min-h-11 w-full sm:w-auto"
disabled={syncMutation.isPending}
onClick={async () => {
await updateWorkflowActionDraftStatus({
@@ -1170,6 +1197,7 @@ export function MobileApprovalsPage() {
type="button"
size="sm"
variant="secondary"
className="min-h-11 w-full sm:w-auto"
onClick={async () => {
await removeWorkflowActionDraft(item.taskId, ownerEmployeeId);
await refreshDraftQueueCache();