// aislop-ignore-file: duplicate-block -- 表格/表单声明结构相似且参数不同,渲染逻辑已共享组件化 import React, { useMemo, useState } from 'react'; import { useQuery } from '@tanstack/react-query'; import { App, Alert, Button, Form, Input, Modal, Popconfirm, Select, Space, Table, Tag } from 'antd'; import { BankOutlined, InboxOutlined, PlusOutlined, UndoOutlined } from '@ant-design/icons'; import api from '../../api'; import PermissionButton from '../../components/PermissionButton'; import EditableCell from '../../components/EditableCell'; import { message } from '../../ui/app-message'; import { RefreshButton } from '../../components/RefreshButton'; import { usePermission } from '../../hooks/usePermission'; import { useApiMutation } from '../../hooks/useApiMutation'; import { validateResponse } from '../../utils/validate'; import { organizationsSchema } from '../../api/schemas'; import { useDirtyGuard } from '../../hooks/useDirtyGuard'; import { QueryEmpty, QueryErrorState } from '../../components/QueryState'; const PRESET_COLORS = [ '#ff7875', '#ffa940', '#ffc53d', '#73d13d', '#36cfc9', '#40a9ff', '#597ef7', '#9254de', '#f759ab', '#8c8c8c', ]; interface OrganizationItem { id: number; code: string; name: string; isHost: boolean; contactName?: string; phone?: string; color?: string; notes?: string; status: 'active' | 'archived'; } const ORGANIZATION_FIELDS = { name: 'name', code: 'code', contactName: 'contactName', phone: 'phone', notes: 'notes', } as const; const OrganizationsPage: React.FC = () => { const { modal } = App.useApp(); const { hasPermission } = usePermission(); const canPurgeOrganization = hasPermission('organization:purge'); const [modalOpen, setModalOpen] = useState(false); const [editing, setEditing] = useState(null); const [form] = Form.useForm(); const orgGuard = useDirtyGuard(form); const [saving, setSaving] = useState(false); const [searchText, setSearchText] = useState(''); const [filterStatus, setFilterStatus] = useState(); const { data = [], isLoading, isFetching, isError, refetch } = useQuery({ queryKey: ['organizations'], queryFn: async () => validateResponse( organizationsSchema, await api.get('/organizations', { params: { includeArchived: true }, }), ), }); const loading = isLoading || isFetching; const saveMutation = useApiMutation( async (values: { name: string; code: string; color?: string; notes?: string }) => editing ? api.put(`/organizations/${editing.id}`, values) : api.post('/organizations', values), { invalidate: [['organizations']] }, ); const saveCellMutation = useApiMutation( async ({ record, field, value }: { record: OrganizationItem; field: string; value: unknown }) => api.put(`/organizations/${record.id}`, { [field]: value }), { invalidate: [['organizations']] }, ); const statusMutation = useApiMutation( async ({ id, status }: { id: number; status: 'active' | 'archived' }) => status === 'active' ? api.put(`/organizations/${id}`, { status: 'active' }) : api.delete(`/organizations/${id}`), { invalidate: [['organizations']] }, ); const purgeMutation = useApiMutation( async (id: number) => api.delete(`/organizations/${id}/permanent`), { invalidate: [['organizations']] }, ); const filteredData = useMemo(() => { const keyword = searchText.trim().toLowerCase(); return data.filter((item) => { const matchesKeyword = !keyword || item.name.toLowerCase().includes(keyword) || item.code.toLowerCase().includes(keyword) || item.contactName?.toLowerCase().includes(keyword); return matchesKeyword && (!filterStatus || item.status === filterStatus); }); }, [data, searchText, filterStatus]); const handlePurge = (record: OrganizationItem) => { modal.confirm({ title: `永久删除机构「${record.name}」?`, content: '删除后不可恢复,存在学生归属、入住或租赁关联时将无法删除。确定继续?', okText: '永久删除', okButtonProps: { danger: true }, cancelText: '取消', onOk: async () => { try { await purgeMutation.mutateAsync(record.id); message.success('已永久删除(不可恢复)'); } catch { // 错误提示由 useApiMutation 统一处理 } }, }); }; const openEditor = (record?: OrganizationItem) => { setEditing(record ?? null); form.resetFields(); if (record) form.setFieldsValue(record); else form.setFieldsValue({ color: PRESET_COLORS[data.length % PRESET_COLORS.length] }); orgGuard.snapshot(); setModalOpen(true); }; const handleSave = async () => { const values = await form.validateFields(); setSaving(true); try { await saveMutation.mutateAsync(values); message.success(editing ? '机构已更新' : '机构已创建'); setModalOpen(false); } catch { // 错误提示由 useApiMutation 统一处理 } finally { setSaving(false); } }; const saveCell = async (record: OrganizationItem, field: string, value: unknown) => { try { await saveCellMutation.mutateAsync({ record, field, value }); message.success('已保存'); } catch { // 错误提示由 useApiMutation 统一处理 } }; const EditableOrganizationCell = ({ value, field, record, editor, required, onSave, children, }: { value: unknown; field: string; record: R; editor?: React.ComponentProps['editor']; required?: boolean; onSave: (record: R, field: string, value: unknown) => Promise | void; children?: React.ReactNode; }) => ( { await onSave(record, field, next); }} > {children ?? String(value ?? '-')} ); const columns = [ { title: '机构', dataIndex: 'name', width: 220, render: (name: string, record: OrganizationItem) => ( {name} {record.isHost ? ( }> 本机构 ) : ( 外部机构 )} ), }, { title: '机构编码', dataIndex: 'code', width: 130, render: (value: string, record: OrganizationItem) => ( {value} ), }, { title: '联系人', dataIndex: 'contactName', width: 120, render: (value: string | undefined, record: OrganizationItem) => ( {value || '-'} ), }, { title: '电话', dataIndex: 'phone', width: 140, render: (value: string | undefined, record: OrganizationItem) => ( {value || '-'} ), }, { title: '备注', dataIndex: 'notes', ellipsis: true, render: (value: string | undefined, record: OrganizationItem) => ( {value || '-'} ), }, { title: '状态', dataIndex: 'status', width: 90, render: (status: string) => ( {status === 'active' ? '正常' : '已归档'} ), }, { title: '操作', width: 160, render: (_: unknown, record: OrganizationItem) => ( {record.status === 'archived' ? ( <> { try { await statusMutation.mutateAsync({ id: record.id, status: 'active' }); message.success('机构已恢复'); } catch { // 错误提示由 useApiMutation 统一处理 } }} > } > 恢复 {canPurgeOrganization && !record.isHost ? ( ) : null} ) : ( <> openEditor(record)} > 编辑 {!record.isHost ? ( { try { await statusMutation.mutateAsync({ id: record.id, status: 'archived' }); message.success('机构已归档'); } catch { // 错误提示由 useApiMutation 统一处理 } }} > } > 归档 ) : null} )} ), }, ]; return (
setSearchText(event.target.value)} /> form.setFieldValue('code', event.target.value.toUpperCase())} /> {PRESET_COLORS.map((color) => (
); }; export default OrganizationsPage;