From 47a7e4a4da26d16a18ea081ca549955aff56528e Mon Sep 17 00:00:00 2001 From: hailin Date: Fri, 2 Jan 2026 20:23:15 -0800 Subject: [PATCH] feat(pending-actions): enhance multi-select creation and add pre-check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Admin Web: - Redesign create modal to support multi-select action types - Add drag-and-drop ordering for execution sequence - Auto-calculate priority based on order (first = highest) - Add @dnd-kit dependencies for sortable functionality Flutter Mobile App: - Add pre-check logic before executing pending actions - Auto-complete FORCE_KYC if KYC already verified - Auto-complete BIND_PHONE if phone already bound - Skip unnecessary user interactions for completed tasks 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- frontend/admin-web/package-lock.json | 57 +++ frontend/admin-web/package.json | 3 + .../app/(dashboard)/pending-actions/page.tsx | 373 ++++++++++++++---- .../pending-actions.module.scss | 120 ++++++ .../pages/pending_actions_page.dart | 45 +++ 5 files changed, 522 insertions(+), 76 deletions(-) diff --git a/frontend/admin-web/package-lock.json b/frontend/admin-web/package-lock.json index 62b9990a..d33132ba 100644 --- a/frontend/admin-web/package-lock.json +++ b/frontend/admin-web/package-lock.json @@ -8,6 +8,9 @@ "name": "rwadurian-admin-web", "version": "1.0.0", "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@reduxjs/toolkit": "^2.3.0", "@tanstack/react-query": "^5.62.16", "axios": "^1.7.9", @@ -42,6 +45,60 @@ "node": ">=6.9.0" } }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@emnapi/core": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", diff --git a/frontend/admin-web/package.json b/frontend/admin-web/package.json index f3804074..0de95462 100644 --- a/frontend/admin-web/package.json +++ b/frontend/admin-web/package.json @@ -14,6 +14,9 @@ "type-check": "tsc --noEmit" }, "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@reduxjs/toolkit": "^2.3.0", "@tanstack/react-query": "^5.62.16", "axios": "^1.7.9", diff --git a/frontend/admin-web/src/app/(dashboard)/pending-actions/page.tsx b/frontend/admin-web/src/app/(dashboard)/pending-actions/page.tsx index 2cb6ac33..b4f245c2 100644 --- a/frontend/admin-web/src/app/(dashboard)/pending-actions/page.tsx +++ b/frontend/admin-web/src/app/(dashboard)/pending-actions/page.tsx @@ -1,6 +1,23 @@ 'use client'; import { useState, useCallback } from 'react'; +import { + DndContext, + closestCenter, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, + DragEndEvent, +} from '@dnd-kit/core'; +import { + arrayMove, + SortableContext, + sortableKeyboardCoordinates, + useSortable, + verticalListSortingStrategy, +} from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; import { Modal, toast, Button } from '@/components/common'; import { PageContainer } from '@/components/layout'; import { cn } from '@/utils/helpers'; @@ -23,6 +40,56 @@ import { } from '@/types/pending-action.types'; import styles from './pending-actions.module.scss'; +/** 可排序的操作项组件 */ +interface SortableActionItemProps { + id: string; + label: string; + index: number; + onRemove: () => void; +} + +function SortableActionItem({ id, label, index, onRemove }: SortableActionItemProps) { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1, + }; + + return ( +
+ + ☰ + + {index + 1} + {label} + +
+ ); +} + /** * 用户待办操作管理页面 */ @@ -43,15 +110,29 @@ export default function PendingActionsPage() { const [deleteConfirm, setDeleteConfirm] = useState(null); const [viewingAction, setViewingAction] = useState(null); - // 表单状态 + // 表单状态 - 支持多选操作 const [formData, setFormData] = useState({ userId: '', - actionCode: 'ADOPTION_WIZARD', + selectedActions: [] as string[], // 按顺序存储已选择的 actionCode + actionParamsMap: {} as Record, // 每个 actionCode 对应的参数 + expiresAt: '', + }); + + // 单个操作编辑时使用的表单 + const [editFormData, setEditFormData] = useState({ actionParams: '', priority: 0, expiresAt: '', }); + // 拖拽排序 sensors + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }) + ); + // 批量创建表单 const [batchFormData, setBatchFormData] = useState({ userIds: '', @@ -82,9 +163,8 @@ export default function PendingActionsPage() { setEditingAction(null); setFormData({ userId: '', - actionCode: 'ADOPTION_WIZARD', - actionParams: '', - priority: 0, + selectedActions: [], + actionParamsMap: {}, expiresAt: '', }); setShowCreateModal(true); @@ -93,9 +173,7 @@ export default function PendingActionsPage() { // 打开编辑弹窗 const handleEdit = (action: PendingAction) => { setEditingAction(action); - setFormData({ - userId: action.userId, - actionCode: action.actionCode, + setEditFormData({ actionParams: action.actionParams ? JSON.stringify(action.actionParams, null, 2) : '', priority: action.priority, expiresAt: action.expiresAt ? action.expiresAt.slice(0, 16) : '', @@ -103,6 +181,51 @@ export default function PendingActionsPage() { setShowCreateModal(true); }; + // 切换选择操作 + const handleToggleAction = (actionCode: string) => { + setFormData((prev) => { + const isSelected = prev.selectedActions.includes(actionCode); + if (isSelected) { + // 取消选择 + const newSelected = prev.selectedActions.filter((code) => code !== actionCode); + const newParamsMap = { ...prev.actionParamsMap }; + delete newParamsMap[actionCode]; + return { ...prev, selectedActions: newSelected, actionParamsMap: newParamsMap }; + } else { + // 添加选择(追加到末尾) + return { + ...prev, + selectedActions: [...prev.selectedActions, actionCode], + }; + } + }); + }; + + // 拖拽排序结束 + const handleDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + if (over && active.id !== over.id) { + setFormData((prev) => { + const oldIndex = prev.selectedActions.indexOf(active.id as string); + const newIndex = prev.selectedActions.indexOf(over.id as string); + return { + ...prev, + selectedActions: arrayMove(prev.selectedActions, oldIndex, newIndex), + }; + }); + } + }; + + // 移除已选择的操作 + const handleRemoveAction = (actionCode: string) => { + setFormData((prev) => { + const newSelected = prev.selectedActions.filter((code) => code !== actionCode); + const newParamsMap = { ...prev.actionParamsMap }; + delete newParamsMap[actionCode]; + return { ...prev, selectedActions: newSelected, actionParamsMap: newParamsMap }; + }); + }; + // 打开批量创建弹窗 const handleBatchCreate = () => { setBatchFormData({ @@ -117,45 +240,83 @@ export default function PendingActionsPage() { // 提交创建/编辑表单 const handleSubmit = async () => { - if (!editingAction && !formData.userId.trim()) { - toast.error('请输入用户ID'); - return; - } - - let actionParams: Record | undefined; - if (formData.actionParams.trim()) { - try { - actionParams = JSON.parse(formData.actionParams); - } catch { - toast.error('操作参数格式错误,请输入有效的 JSON'); - return; + // 编辑模式 + if (editingAction) { + let actionParams: Record | undefined; + if (editFormData.actionParams.trim()) { + try { + actionParams = JSON.parse(editFormData.actionParams); + } catch { + toast.error('操作参数格式错误,请输入有效的 JSON'); + return; + } } - } - try { - if (editingAction) { + try { await updateMutation.mutateAsync({ id: editingAction.id, data: { actionParams, - priority: formData.priority, - expiresAt: formData.expiresAt ? new Date(formData.expiresAt).toISOString() : undefined, + priority: editFormData.priority, + expiresAt: editFormData.expiresAt ? new Date(editFormData.expiresAt).toISOString() : undefined, }, }); toast.success('操作已更新'); - } else { + setShowCreateModal(false); + } catch (err) { + toast.error((err as Error).message || '更新失败'); + } + return; + } + + // 创建模式 - 支持多选 + if (!formData.userId.trim()) { + toast.error('请输入用户ID'); + return; + } + + if (formData.selectedActions.length === 0) { + toast.error('请至少选择一个操作类型'); + return; + } + + // 解析每个操作的参数 + const actionParamsMap: Record | undefined> = {}; + for (const actionCode of formData.selectedActions) { + const paramsStr = formData.actionParamsMap[actionCode]; + if (paramsStr?.trim()) { + try { + actionParamsMap[actionCode] = JSON.parse(paramsStr); + } catch { + toast.error(`${getActionCodeLabel(actionCode)} 的参数格式错误,请输入有效的 JSON`); + return; + } + } + } + + try { + // 按顺序创建多个操作,优先级递减(第一个最高) + const basePriority = formData.selectedActions.length * 10; // 例如 3 个操作: 30, 20, 10 + let successCount = 0; + + for (let i = 0; i < formData.selectedActions.length; i++) { + const actionCode = formData.selectedActions[i]; + const priority = basePriority - i * 10; + await createMutation.mutateAsync({ userId: formData.userId.trim(), - actionCode: formData.actionCode, - actionParams, - priority: formData.priority, + actionCode, + actionParams: actionParamsMap[actionCode], + priority, expiresAt: formData.expiresAt ? new Date(formData.expiresAt).toISOString() : undefined, }); - toast.success('操作已创建'); + successCount++; } + + toast.success(`成功创建 ${successCount} 个待办操作`); setShowCreateModal(false); } catch (err) { - toast.error((err as Error).message || '操作失败'); + toast.error((err as Error).message || '创建失败'); } }; @@ -417,7 +578,7 @@ export default function PendingActionsPage() { {/* 创建/编辑弹窗 */} setShowCreateModal(false)} footer={
@@ -429,14 +590,59 @@ export default function PendingActionsPage() { onClick={handleSubmit} loading={createMutation.isPending || updateMutation.isPending} > - {editingAction ? '保存' : '创建'} + {editingAction ? '保存' : `创建${formData.selectedActions.length > 0 ? ` (${formData.selectedActions.length})` : ''}`}
} - width={600} + width={700} > -
- {!editingAction && ( + {editingAction ? ( + /* 编辑模式 - 单个操作 */ +
+
+ + +
+ +
+ +