import React, { useCallback, useEffect, useState } from 'react'; import dayjs from 'dayjs'; import { Button, Table, Modal, Input, Transfer, Spin } from 'antd'; import type { TransferProps } from 'antd/es/transfer'; import type { ColumnsType } from 'antd/es/table'; import { Plus, Trash2 } from 'lucide-react'; import { toast } from 'sonner'; import { useTranslation } from 'react-i18next'; import { materialBlacklistApi } from '../../../api/material/blacklist'; import { pickRecords, pickTotal, buildPageParams } from '../materialListUtils'; import { MaterialPageRoot, MaterialToolbarCard, MaterialTableCard, MaterialPaginationBar, MaterialDeleteButton, materialConfirmDelete, getMaterialFormModalProps, } from '../MaterialPageLayout'; type TransferRecord = NonNullable[number]; function firstDefinedRow(row: Record, keys: string[]) { for (const k of keys) { const v = row[k]; if (v !== undefined && v !== null && v !== '') return v; } return undefined; } function formatDateTimeCell(v: unknown): string { if (v === undefined || v === null || v === '') return '—'; const n = typeof v === 'number' ? v : /^\d+$/.test(String(v).trim()) ? Number(String(v).trim()) : NaN; const d = Number.isFinite(n) && n > 1e12 ? dayjs(n) : dayjs(v as string | number | Date); return d.isValid() ? d.format('YYYY-MM-DD HH:mm:ss') : '—'; } /** 与旧版 BlacklistForm.vue 一致:module 固定为物资柜等业务维度 */ const BLACKLIST_MODULE = '2'; export default function MaterialBlacklistPage() { const { t } = useTranslation(); const [loading, setLoading] = useState(false); const [list, setList] = useState([]); const [total, setTotal] = useState(0); const [pageNo, setPageNo] = useState(1); const [pageSize] = useState(10); const [draftUserName, setDraftUserName] = useState(''); const [draftNickName, setDraftNickName] = useState(''); const [appliedUserName, setAppliedUserName] = useState(''); const [appliedNickName, setAppliedNickName] = useState(''); const [modalOpen, setModalOpen] = useState(false); const [submitLoading, setSubmitLoading] = useState(false); const [selectedRowKeys, setSelectedRowKeys] = useState([]); const [transferLoading, setTransferLoading] = useState(false); const [transferDataSource, setTransferDataSource] = useState([]); const [targetKeys, setTargetKeys] = useState([]); /** key 为穿梭框 key(与旧版一致为 userId 字符串),值为提交用 userId */ const [userIdByKey, setUserIdByKey] = useState>(new Map()); const fetchList = useCallback(async () => { setLoading(true); try { const res = await materialBlacklistApi.listBlacklist({ ...buildPageParams(pageNo, pageSize), userName: appliedUserName || undefined, nickName: appliedNickName || undefined, }); setList(pickRecords(res)); setTotal(pickTotal(res)); } catch (e: any) { toast.error(e?.message || t('common.operationFailed')); } finally { setLoading(false); } }, [pageNo, pageSize, appliedUserName, appliedNickName, t]); useEffect(() => { fetchList(); }, [fetchList]); /** * 与旧版 BlacklistForm getLeftList 一致:白名单分页接口拉可选用户, * 不再用「全量用户 − 黑名单」自行过滤,避免 userId 形态与后端不一致。 */ const loadTransferUsers = useCallback(async () => { setTransferLoading(true); try { const res = await materialBlacklistApi.listWhitelist({ ...buildPageParams(1, -1), nickName: undefined, }); const rows = pickRecords(res); const map = new Map(); const items: TransferRecord[] = []; for (const item of rows as any[]) { const userId = firstDefinedRow(item, ['userId', 'user_id', 'id']); if (userId === undefined || userId === null || userId === '') continue; const sid = String(userId); map.set(sid, userId as string | number); items.push({ key: sid, title: `${item.nickName ?? item.nickname ?? '—'} (${item.userName ?? item.username ?? sid})`, }); } setUserIdByKey(map); setTransferDataSource(items); } catch (e: any) { toast.error(e?.message || '加载用户列表失败'); setTransferDataSource([]); setUserIdByKey(new Map()); } finally { setTransferLoading(false); } }, []); useEffect(() => { if (!modalOpen) return; setTargetKeys([]); void loadTransferUsers(); }, [modalOpen, loadTransferUsers]); const handleQuery = () => { setAppliedUserName(draftUserName); setAppliedNickName(draftNickName); setPageNo(1); setSelectedRowKeys([]); }; const resetQuery = () => { setDraftUserName(''); setDraftNickName(''); setAppliedUserName(''); setAppliedNickName(''); setPageNo(1); setSelectedRowKeys([]); }; /** 与旧版 submitForm 一致:一次 POST 数组 [{ userId, module: '2' }, ...] */ const submitAdd = async () => { if (targetKeys.length === 0) { toast.error('请至少选择一名用户加入黑名单'); return; } setSubmitLoading(true); try { const payload = targetKeys.map((key) => ({ userId: userIdByKey.get(key) ?? key, module: BLACKLIST_MODULE, })); await materialBlacklistApi.addBlacklist(payload); toast.success(t('common.addSuccess')); setModalOpen(false); setTargetKeys([]); fetchList(); } catch (e: any) { toast.error(e?.message || t('common.operationFailed')); } finally { setSubmitLoading(false); } }; const onDelete = (row: any) => { const id = row.recordId ?? row.id; materialConfirmDelete(t, { count: 1, onOk: async () => { await materialBlacklistApi.delBlacklist(id); toast.success(t('common.deleteSuccess')); setSelectedRowKeys((prev) => prev.filter((k) => String(k) !== String(id))); fetchList(); }, }); }; const batchDelete = () => { if (selectedRowKeys.length === 0) { toast.error(t('common.pleaseSelectDataToDelete')); return; } const ids = selectedRowKeys .map((k) => Number(k)) .filter((n) => Number.isFinite(n) && !Number.isNaN(n)); if (ids.length === 0) { toast.error(t('common.pleaseSelectDataToDelete')); return; } materialConfirmDelete(t, { count: ids.length, onOk: async () => { await materialBlacklistApi.delBlacklist(ids.join(',')); toast.success(t('common.deleteSuccess')); setSelectedRowKeys([]); fetchList(); }, }); }; /** Ant Design Transfer:operations[0]=左→右(添加),operations[1]=右→左(移除) */ const transferFilter: TransferProps['filterOption'] = (inputValue, item) => { const title = (item.title ?? '').toLowerCase(); const q = inputValue.trim().toLowerCase(); return !q || title.includes(q); }; const columns: ColumnsType = [ { title: '用户编号', dataIndex: 'userId', align: 'center' }, { title: '工号', dataIndex: 'userName', align: 'center' }, { title: '姓名', dataIndex: 'nickName', align: 'center' }, { title: '创建时间', key: 'createTime', width: 170, align: 'center', render: (_, row) => formatDateTimeCell( firstDefinedRow(row, ['createTime', 'create_time', 'gmtCreate', 'gmt_create', 'createDate']), ), }, { title: t('table.operation'), width: 120, align: 'center', fixed: 'right', render: (_, row) => (
onDelete(row)} />
), }, ]; return (
setDraftUserName(e.target.value)} />
setDraftNickName(e.target.value)} />
} onSearch={handleQuery} onReset={resetQuery} toolbarRight={ <> } /> r.recordId ?? r.id} loading={loading} columns={columns} dataSource={list} pagination={false} scroll={{ x: 'max-content' }} rowSelection={{ selectedRowKeys, onChange: setSelectedRowKeys, preserveSelectedRowKeys: true, getCheckboxProps: (record) => ({ name: record.userName }), }} rowClassName={(_, index) => (index !== undefined && index % 2 === 1 ? 'isolation-work-list-row-alt' : '')} /> { setModalOpen(false); setTargetKeys([]); }} confirmLoading={submitLoading} {...getMaterialFormModalProps(t, 880)} > {transferLoading ? (
) : ( setTargetKeys(next as string[])} render={(item) => item.title ?? item.key} showSearch filterOption={transferFilter} listStyle={{ width: 360, height: 400 }} operations={['添加', '移除']} locale={{ itemUnit: '项', itemsUnit: '项', searchPlaceholder: '请输入姓名或登录名', }} /> )}
); }