| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561 |
- import React, { useCallback, useEffect, useMemo, useState } from 'react';
- import { Table, Button, Form, Input, Modal, Image, Row, Col, Tree, Select, Tag } from 'antd';
- import type { ColumnsType } from 'antd/es/table';
- import type { DataNode } from 'antd/es/tree';
- import { Plus } from 'lucide-react';
- import { toast } from 'sonner';
- import { useTranslation } from 'react-i18next';
- import { materialLockerApi, CabinetVO } from '../../../api/material/lockers';
- import { workstationApi } from '../../../api/workstation';
- import { handleTree } from '../../../utils/tree';
- import UploadImg from '../../lockCabinet/UploadImg';
- import MaterialLockerDetailPage from './MaterialLockerDetailPage';
- import { pickRecords, pickTotal, buildPageParams, fetchAllWorkstations } from '../materialListUtils';
- import {
- MaterialPageRoot,
- MaterialToolbarCard,
- MaterialTableCard,
- MaterialPaginationBar,
- MaterialEditButton,
- MaterialDeleteButton,
- MaterialViewButton,
- materialConfirmDelete,
- getMaterialFormModalProps,
- materialFormLayout,
- } from '../MaterialPageLayout';
- /** 物资柜状态:0 正常 1 借出 2 异常 */
- const CABINET_STATUS_OPTIONS = [
- { value: 0, label: '正常' },
- { value: 1, label: '借出' },
- { value: 2, label: '异常' },
- ];
- const CABINET_STATUS_LABEL: Record<number, string> = {
- 0: '正常',
- 1: '借出',
- 2: '异常',
- };
- /** 异常类型:0 正常 1 物资异常 2 物资柜异常 3 错还柜子 4 物资借出 5 超时未关门 */
- const EXCEPTION_TYPE_LABEL: Record<number, string> = {
- 0: '正常',
- 1: '物资异常',
- 2: '物资柜异常',
- 3: '错还柜子',
- 4: '物资借出',
- 5: '超时未关门',
- };
- function firstDefined(row: Record<string, unknown>, keys: string[]) {
- for (const k of keys) {
- const v = row[k];
- if (v !== undefined && v !== null && v !== '') return v;
- }
- return undefined;
- }
- function toFiniteInt(v: unknown): number | undefined {
- if (v === undefined || v === null || v === '') return undefined;
- if (typeof v === 'string' && v.trim() === '') return undefined;
- const n = Number(v);
- return Number.isFinite(n) ? Math.trunc(n) : undefined;
- }
- function getCabinetStatusValue(row: Record<string, unknown>): number | undefined {
- const raw = firstDefined(row, ['status', 'cabinetStatus', 'runStatus', 'cabinetRunStatus']);
- if (raw === '正常') return 0;
- if (raw === '借出') return 1;
- if (raw === '异常') return 2;
- return toFiniteInt(raw);
- }
- function renderCabinetStatusTag(row: Record<string, unknown>) {
- const raw = firstDefined(row, ['status', 'cabinetStatus', 'runStatus', 'cabinetRunStatus']);
- const code = getCabinetStatusValue(row);
- if (code !== undefined && CABINET_STATUS_LABEL[code] !== undefined) {
- const color = code === 0 ? 'blue' : code === 1 ? 'processing' : 'error';
- return <Tag color={color}>{CABINET_STATUS_LABEL[code]}</Tag>;
- }
- if (typeof raw === 'string' && raw.trim() !== '') {
- return <Tag color="default">{raw}</Tag>;
- }
- return <Tag color="default">—</Tag>;
- }
- function getExceptionTypeCode(row: Record<string, unknown>): number | undefined {
- return toFiniteInt(
- firstDefined(row, [
- 'exReason',
- 'exceptionType',
- 'abnormalType',
- 'exceptionTypeCode',
- 'abnormalTypeCode',
- 'doorExceptionType',
- 'exceptionKind',
- ]),
- );
- }
- function exceptionTypeTagColor(code: number): string {
- if (code === 0) return 'blue';
- if (code === 1 || code === 2) return 'error';
- if (code === 3 || code === 5) return 'warning';
- if (code === 4) return 'processing';
- return 'default';
- }
- function renderExceptionTypeTag(row: Record<string, unknown>) {
- const code = getExceptionTypeCode(row);
- if (code !== undefined && EXCEPTION_TYPE_LABEL[code] !== undefined) {
- return (
- <Tag color={exceptionTypeTagColor(code)}>{EXCEPTION_TYPE_LABEL[code]}</Tag>
- );
- }
- const ex = firstDefined(row, [
- 'exceptionTypeName',
- 'exceptionName',
- 'exceptionDesc',
- 'abnormalTypeName',
- 'exceptionMsg',
- 'errorMessage',
- 'abnormalReason',
- ]);
- if (ex !== undefined && ex !== null && ex !== '') {
- return <Tag color="default">{String(ex)}</Tag>;
- }
- return <Tag color="default">—</Tag>;
- }
- function filterWorkstationTree(nodes: any[], kw: string): any[] {
- const q = kw.trim().toLowerCase();
- if (!q) return nodes;
- const match = (n: any) => {
- const code = String(n.workstationCode ?? '').toLowerCase();
- const name = String(n.workstationName ?? '').toLowerCase();
- return code.includes(q) || name.includes(q);
- };
- return nodes
- .map((n) => {
- const children = n.children?.length ? filterWorkstationTree(n.children, kw) : [];
- if (match(n) || children.length) return { ...n, children: children.length ? children : undefined };
- return null;
- })
- .filter(Boolean) as any[];
- }
- function toAntdTreeData(nodes: any[]): DataNode[] {
- return nodes.map((n) => ({
- key: String(n.id),
- title: (n.workstationCode || n.workstationName || String(n.id)) as React.ReactNode,
- children: n.children?.length ? toAntdTreeData(n.children) : undefined,
- }));
- }
- /** 区域树打平为下拉选项(value 为岗位/区域 id) */
- function flattenWorkstationOptions(nodes: any[]): { label: string; value: number }[] {
- const out: { label: string; value: number }[] = [];
- const walk = (arr: any[]) => {
- for (const n of arr) {
- const id = Number(n.id ?? n.workstationId);
- if (Number.isFinite(id) && id > 0) {
- const code = n.workstationCode ? String(n.workstationCode) : '';
- const name = n.workstationName ? String(n.workstationName) : '';
- const label = [code, name].filter(Boolean).join(' / ') || String(id);
- out.push({ value: id, label });
- }
- if (n.children?.length) walk(n.children);
- }
- };
- walk(nodes);
- return out;
- }
- export default function MaterialLockersPage() {
- 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 = 10;
- const [draftQuery, setDraftQuery] = useState({ cabinetName: '', status: undefined as number | undefined });
- const [appliedQuery, setAppliedQuery] = useState({ cabinetName: '', status: undefined as number | undefined });
- const [modalOpen, setModalOpen] = useState(false);
- const [editing, setEditing] = useState<CabinetVO | null>(null);
- const [form] = Form.useForm();
- const [areaSearch, setAreaSearch] = useState('');
- const [areaTreeRaw, setAreaTreeRaw] = useState<any[]>([]);
- const [selectedAreaKey, setSelectedAreaKey] = useState<string>('all');
- /** 非空时展示物资柜详情页(由列表「查看」进入) */
- const [lockerDetail, setLockerDetail] = useState<{ id: number; name: string } | null>(null);
- const workstationIdFilter = selectedAreaKey === 'all' ? undefined : Number(selectedAreaKey);
- const filteredAreaTree = useMemo(() => filterWorkstationTree(areaTreeRaw, areaSearch), [areaTreeRaw, areaSearch]);
- const areaTreeData: DataNode[] = useMemo(
- () => [{ key: 'all', title: '全部区域' }, ...toAntdTreeData(filteredAreaTree)],
- [filteredAreaTree],
- );
- const workstationOptions = useMemo(() => {
- const base = flattenWorkstationOptions(areaTreeRaw);
- if (!editing) return base;
- const wid = Number(editing.workstationId);
- if (Number.isFinite(wid) && wid > 0 && !base.some((o) => o.value === wid)) {
- const label =
- firstDefined(editing as unknown as Record<string, unknown>, ['workstationCode', 'workstationName']) ??
- `区域 #${wid}`;
- return [{ value: wid, label: String(label) }, ...base];
- }
- return base;
- }, [areaTreeRaw, editing]);
- const loadAreaTree = useCallback(async () => {
- try {
- const flat = await fetchAllWorkstations(workstationApi.listMarsDept);
- const normalized = flat.map((w: any) => {
- const id = Number(w.workstationId ?? w.id);
- return {
- ...w,
- id: Number.isFinite(id) ? id : 0,
- parentId: w.parentId != null && w.parentId !== '' ? Number(w.parentId) : 0,
- };
- });
- const tree = handleTree(normalized, 'id', 'parentId', 'children');
- setAreaTreeRaw(tree);
- } catch (e: any) {
- toast.error(e?.message || '加载区域失败');
- setAreaTreeRaw([]);
- }
- }, []);
- useEffect(() => {
- loadAreaTree();
- }, [loadAreaTree]);
- const fetchList = useCallback(async () => {
- setLoading(true);
- try {
- const res = await materialLockerApi.listMaterialsCabinet({
- ...buildPageParams(pageNo, pageSize),
- cabinetName: appliedQuery.cabinetName || undefined,
- status: appliedQuery.status,
- workstationId: workstationIdFilter,
- });
- setList(pickRecords(res));
- setTotal(pickTotal(res));
- } catch (e: any) {
- toast.error(e?.message || '加载物资柜失败');
- setList([]);
- } finally {
- setLoading(false);
- }
- }, [pageNo, appliedQuery.cabinetName, appliedQuery.status, workstationIdFilter]);
- useEffect(() => {
- fetchList();
- }, [fetchList]);
- const handleQuery = () => {
- setAppliedQuery({ ...draftQuery });
- setPageNo(1);
- };
- const resetQuery = () => {
- const empty = { cabinetName: '', status: undefined as number | undefined };
- setDraftQuery(empty);
- setAppliedQuery(empty);
- setPageNo(1);
- };
- const openCreate = () => {
- setEditing(null);
- form.resetFields();
- form.setFieldsValue({
- cabinetName: '',
- remark: '',
- cabinetPicture: '',
- workstationId:
- workstationIdFilter != null && Number.isFinite(workstationIdFilter) && workstationIdFilter > 0
- ? workstationIdFilter
- : undefined,
- });
- setModalOpen(true);
- };
- const openEdit = (row: any) => {
- setEditing(row);
- form.setFieldsValue({
- cabinetName: row.cabinetName,
- remark: row.remark ?? '',
- cabinetPicture: row.cabinetPicture ?? '',
- workstationId: row.workstationId != null && row.workstationId !== '' ? Number(row.workstationId) : undefined,
- });
- setModalOpen(true);
- };
- const openLockerDetail = (row: any) => {
- const id = row.cabinetId ?? row.id;
- if (id == null) return;
- setLockerDetail({
- id: Number(id),
- name: String(row.cabinetName ?? ''),
- });
- };
- const handleSubmit = async () => {
- try {
- const v = await form.validateFields();
- setLoading(true);
- const editId = editing ? (editing.cabinetId ?? editing.id) : undefined;
- if (editId != null && editing) {
- await materialLockerApi.updateMaterialsCabinet({
- ...editing,
- cabinetId: editId,
- cabinetName: v.cabinetName,
- cabinetCode: editing.cabinetCode || v.cabinetName,
- cabinetType: editing.cabinetType || 'default',
- remark: v.remark,
- cabinetPicture: v.cabinetPicture || '',
- workstationId: v.workstationId,
- } as CabinetVO);
- toast.success(t('common.updateSuccess'));
- } else {
- await materialLockerApi.addMaterialsCabinet({
- cabinetName: v.cabinetName,
- cabinetCode: v.cabinetName,
- cabinetType: 'default',
- status: 0,
- remark: v.remark,
- cabinetPicture: v.cabinetPicture || undefined,
- workstationId: v.workstationId,
- } as CabinetVO);
- toast.success(t('common.addSuccess'));
- }
- setModalOpen(false);
- fetchList();
- } catch (e: any) {
- if (e?.errorFields) return;
- toast.error(e?.message || t('common.operationFailed'));
- } finally {
- setLoading(false);
- }
- };
- const handleDelete = (row: any) => {
- const id = row.cabinetId ?? row.id;
- if (!id) return;
- materialConfirmDelete(t, {
- count: 1,
- onOk: async () => {
- await materialLockerApi.deleteMaterialsCabinet(id);
- toast.success(t('common.deleteSuccess'));
- fetchList();
- },
- });
- };
- const areaCol = (row: any) =>
- firstDefined(row, ['workstationCode', 'workstationName', 'areaCode', 'areaName']) ?? '—';
- const columns: ColumnsType<any> = [
- { title: '物资柜编号', dataIndex: 'cabinetId', width: 110, align: 'center', render: (v, row) => v ?? row.id ?? '—' },
- { title: '物资柜名称', dataIndex: 'cabinetName', ellipsis: true, align: 'center' },
- {
- title: '所属区域',
- key: 'area',
- ellipsis: true,
- align: 'center',
- render: (_, row) => areaCol(row),
- },
- {
- title: '物资柜图片',
- dataIndex: 'cabinetPicture',
- width: 100,
- align: 'center',
- render: (url: string) =>
- url ? <Image src={url} width={50} height={50} style={{ objectFit: 'cover' }} /> : '—',
- },
- {
- title: '物资柜状态',
- key: 'cabinetStatus',
- width: 100,
- align: 'center',
- render: (_, row) => renderCabinetStatusTag(row),
- },
- {
- title: '异常类型',
- key: 'exceptionType',
- width: 140,
- align: 'center',
- render: (_, row) => renderExceptionTypeTag(row),
- },
- {
- title: '查看',
- key: 'view',
- width: 100,
- align: 'center',
- fixed: 'right',
- render: (_, row) => <MaterialViewButton label="查看" onClick={() => openLockerDetail(row)} />,
- },
- {
- title: t('table.operation'),
- width: 160,
- align: 'center',
- fixed: 'right',
- render: (_, row) => (
- <div className="flex items-center gap-2 justify-center">
- <MaterialEditButton label={t('common.edit')} onClick={() => openEdit(row)} />
- <MaterialDeleteButton label={t('common.delete')} onClick={() => handleDelete(row)} />
- </div>
- ),
- },
- ];
- const listBody = (
- <div className="flex gap-4 items-start">
- <div className="w-[260px] shrink-0 bg-white rounded-xl border border-gray-200/50 shadow-sm p-4 flex flex-col gap-3 min-h-[420px]">
- <Input.Search allowClear placeholder="请输入区域名称" value={areaSearch} onChange={(e) => setAreaSearch(e.target.value)} />
- <div className="flex-1 overflow-auto border border-gray-100 rounded-lg p-2">
- <Tree
- blockNode
- showLine
- selectedKeys={[selectedAreaKey]}
- treeData={areaTreeData}
- onSelect={(keys) => {
- if (keys.length && typeof keys[0] === 'string') {
- setSelectedAreaKey(keys[0]);
- setPageNo(1);
- }
- }}
- />
- </div>
- </div>
- <div className="flex-1 min-w-0 space-y-4">
- <MaterialToolbarCard
- filterRow={
- <div className="flex items-center gap-3 flex-wrap">
- <div className="flex items-center gap-2">
- <span className="text-sm font-medium text-gray-700 whitespace-nowrap">物资柜名称:</span>
- <Input
- allowClear
- placeholder="请输入物资柜名称"
- className="w-[180px]"
- value={draftQuery.cabinetName}
- onChange={(e) => setDraftQuery((q) => ({ ...q, cabinetName: e.target.value }))}
- onPressEnter={handleQuery}
- />
- </div>
- <div className="flex items-center gap-2">
- <span className="text-sm font-medium text-gray-700 whitespace-nowrap">物资柜状态:</span>
- <Select
- allowClear
- placeholder="请选择物资柜状态"
- className="w-[180px]"
- options={CABINET_STATUS_OPTIONS}
- value={draftQuery.status}
- onChange={(v) => setDraftQuery((q) => ({ ...q, status: v ?? undefined }))}
- />
- </div>
- </div>
- }
- onSearch={handleQuery}
- onReset={resetQuery}
- toolbarRight={
- <Button type="primary" icon={<Plus />} onClick={openCreate}>
- {t('common.addNew')}
- </Button>
- }
- />
- <MaterialTableCard>
- <Table
- rowKey={(r) => r.cabinetId ?? r.id}
- loading={loading}
- columns={columns}
- dataSource={list}
- pagination={false}
- scroll={{ x: 'max-content' }}
- 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}
- />
- </div>
- </div>
- );
- if (lockerDetail) {
- return (
- <MaterialPageRoot>
- <MaterialLockerDetailPage
- cabinetId={lockerDetail.id}
- cabinetName={lockerDetail.name}
- onBack={() => setLockerDetail(null)}
- />
- </MaterialPageRoot>
- );
- }
- return (
- <MaterialPageRoot>
- {listBody}
- <Modal
- title={editing ? '编辑物资柜信息' : '新增物资柜信息'}
- open={modalOpen}
- onOk={handleSubmit}
- onCancel={() => setModalOpen(false)}
- confirmLoading={loading}
- {...getMaterialFormModalProps(t, 560)}
- >
- <Form form={form} {...materialFormLayout}>
- <Row gutter={16}>
- <Col span={24}>
- <Form.Item
- name="cabinetName"
- label="物资柜名称"
- rules={[{ required: true, message: '请输入物资柜名称' }]}
- >
- <Input allowClear placeholder="请输入物资柜名称" />
- </Form.Item>
- </Col>
- <Col span={24}>
- <Form.Item
- name="workstationId"
- label="所属区域"
- rules={[{ required: true, message: '请选择所属区域' }]}
- >
- <Select
- allowClear
- showSearch
- optionFilterProp="label"
- placeholder="选择所属区域"
- options={workstationOptions}
- />
- </Form.Item>
- </Col>
- <Col span={24}>
- <Form.Item name="cabinetPicture" label="物资柜图片">
- <UploadImg width="120px" height="120px" />
- </Form.Item>
- </Col>
- <Col span={24}>
- <Form.Item name="remark" label="备注">
- <Input.TextArea allowClear rows={4} placeholder="请输入内容" />
- </Form.Item>
- </Col>
- </Row>
- </Form>
- </Modal>
- </MaterialPageRoot>
- );
- }
|