R89 A6-A7 移动触控与回归补齐
这个提交包含在:
@@ -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();
|
||||
|
||||
在新工单中引用
屏蔽一个用户