| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356 |
- 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<TransferProps['dataSource']>[number];
- function firstDefinedRow(row: Record<string, any>, 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<any[]>([]);
- 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<React.Key[]>([]);
- const [transferLoading, setTransferLoading] = useState(false);
- const [transferDataSource, setTransferDataSource] = useState<TransferRecord[]>([]);
- const [targetKeys, setTargetKeys] = useState<string[]>([]);
- /** key 为穿梭框 key(与旧版一致为 userId 字符串),值为提交用 userId */
- const [userIdByKey, setUserIdByKey] = useState<Map<string, string | number>>(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<string, string | number>();
- 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<any> = [
- { 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) => (
- <div className="flex justify-center">
- <MaterialDeleteButton label={t('common.delete')} onClick={() => onDelete(row)} />
- </div>
- ),
- },
- ];
- return (
- <MaterialPageRoot>
- <MaterialToolbarCard
- filterRow={
- <>
- <div className="flex items-center gap-2">
- <label className="text-sm font-medium text-gray-700 whitespace-nowrap">工号:</label>
- <Input
- allowClear
- placeholder="请输入"
- className="w-[140px]"
- value={draftUserName}
- onChange={(e) => setDraftUserName(e.target.value)}
- />
- </div>
- <div className="flex items-center gap-2">
- <label className="text-sm font-medium text-gray-700 whitespace-nowrap">姓名:</label>
- <Input
- allowClear
- placeholder="请输入"
- className="w-[140px]"
- value={draftNickName}
- onChange={(e) => setDraftNickName(e.target.value)}
- />
- </div>
- </>
- }
- onSearch={handleQuery}
- onReset={resetQuery}
- toolbarRight={
- <>
- <Button
- type="primary"
- icon={<Plus />}
- onClick={() => {
- setTargetKeys([]);
- setModalOpen(true);
- }}
- >
- {t('common.addNew')}
- </Button>
- <Button
- danger
- icon={<Trash2 className="h-4 w-4" />}
- disabled={selectedRowKeys.length === 0}
- onClick={batchDelete}
- >
- {t('common.batchDelete')}
- </Button>
- </>
- }
- />
- <MaterialTableCard>
- <Table
- rowKey={(r) => 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' : '')}
- />
- </MaterialTableCard>
- <MaterialPaginationBar
- total={total}
- current={pageNo}
- pageSize={pageSize}
- onPageChange={setPageNo}
- loading={loading}
- listLength={list.length}
- />
- <Modal
- title="新增"
- open={modalOpen}
- onOk={submitAdd}
- onCancel={() => {
- setModalOpen(false);
- setTargetKeys([]);
- }}
- confirmLoading={submitLoading}
- {...getMaterialFormModalProps(t, 880)}
- >
- {transferLoading ? (
- <div className="flex justify-center py-16">
- <Spin />
- </div>
- ) : (
- <Transfer
- dataSource={transferDataSource}
- titles={['可选用户', '已选用户']}
- targetKeys={targetKeys}
- onChange={(next) => setTargetKeys(next as string[])}
- render={(item) => item.title ?? item.key}
- showSearch
- filterOption={transferFilter}
- listStyle={{ width: 360, height: 400 }}
- operations={['添加', '移除']}
- locale={{
- itemUnit: '项',
- itemsUnit: '项',
- searchPlaceholder: '请输入姓名或登录名',
- }}
- />
- )}
- </Modal>
- </MaterialPageRoot>
- );
- }
|