Ver código fonte

合并冲突

wyn 5 meses atrás
pai
commit
d882a5c842

+ 34 - 46
src/components/DepartmentManagement.tsx

@@ -3,8 +3,7 @@ import { Plus, Search, RefreshCw, ChevronRight, ChevronDown, Edit2, Trash2 } fro
 import { Modal } from 'antd';
 import { ExclamationCircleOutlined } from '@ant-design/icons';
 import { Button } from './ui/button';
-import { Input } from './ui/input';
-import { Label } from './ui/label';
+import { Button as AntButton, Input, Space } from 'antd';
 import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
 import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './ui/table';
 import PermissionWrapper from './PermissionWrapper';
@@ -239,67 +238,56 @@ export default function DepartmentManagement() {
     <div className="space-y-4">
       {/* 搜索栏 */}
       <div className="bg-white rounded-xl border border-gray-200/50 shadow-sm p-5">
-        <div className="flex items-center justify-between gap-4">
+        <div className="flex items-center justify-between gap-4 flex-wrap">
           {/* 搜索输入框 */}
-          <div className="flex items-center gap-3 flex-1 max-w-md">
-            <Label htmlFor="dept-name-search" className="text-sm font-medium text-gray-700 whitespace-nowrap">
-              部门名称
-            </Label>
-            <Input
-              id="dept-name-search"
-              placeholder="请输入部门名称"
-              value={queryParams.name || ''}
-              onChange={(e) => setQueryParams(prev => ({ ...prev, name: e.target.value }))}
-              onKeyDown={(e) => e.key === 'Enter' && handleQuery()}
-              className="h-10 bg-gray-50 border-gray-200 focus:bg-white focus:border-blue-400 focus:ring-2 focus:ring-blue-100 transition-all"
-            />
+          <div className="flex items-center gap-3 flex-wrap flex-1">
+            <div className="flex items-center gap-3">
+              <label className="text-sm font-medium text-gray-700 whitespace-nowrap">部门名称:</label>
+              <Input
+                value={queryParams.name || ''}
+                onChange={(e) => setQueryParams(prev => ({ ...prev, name: e.target.value }))}
+                onPressEnter={handleQuery}
+                placeholder="请输入部门名称"
+                style={{ width: 192 }}
+                allowClear
+              />
+            </div>
           </div>
 
           {/* 操作按钮组 */}
-          <div className="flex items-center gap-3">
-            <button
+          <Space>
+            <AntButton
+              type="primary"
+              icon={<Search className="w-4 h-4" />}
               onClick={handleQuery}
-              className="flex items-center gap-2 px-4 py-2.5 bg-gradient-to-r from-blue-500 to-blue-600 text-white rounded-xl hover:shadow-lg hover:shadow-blue-400/40 transition-all duration-300"
             >
-              <Search className="w-4 h-4" strokeWidth={2.5} />
-              <span className="text-sm">搜索</span>
-            </button>
+              搜索
+            </AntButton>
             
-            <button
+            <AntButton
+              icon={<RefreshCw className="w-4 h-4" />}
               onClick={resetQuery}
-              className="flex items-center gap-2 px-4 py-2.5 bg-white border border-gray-300 text-gray-700 rounded-xl hover:bg-gray-50 hover:border-gray-400 transition-all duration-300"
             >
-              <RefreshCw className="w-4 h-4" strokeWidth={2.5} />
-              <span className="text-sm">重置</span>
-            </button>
+              重置
+            </AntButton>
             
             <PermissionWrapper permission="system:dept:create">
-              <button
+              <AntButton
+                type="primary"
+                icon={<Plus className="w-4 h-4" />}
                 onClick={() => openForm('create')}
-                className="flex items-center gap-2 px-4 py-2.5 bg-gradient-to-r from-blue-500 to-blue-600 text-white rounded-xl hover:shadow-lg hover:shadow-blue-400/40 transition-all duration-300"
               >
-                <Plus className="w-4 h-4" strokeWidth={2.5} />
-                <span className="text-sm">新增</span>
-              </button>
+                新增
+              </AntButton>
             </PermissionWrapper>
             
-            <button
+            <AntButton
+              icon={isExpandAll ? <ChevronDown className="w-4 h-4" /> : <ChevronRight className="w-4 h-4" />}
               onClick={toggleExpandAll}
-              className="flex items-center gap-2 px-4 py-2.5 bg-white border border-gray-300 text-gray-700 rounded-xl hover:bg-gray-50 hover:border-gray-400 transition-all duration-300"
             >
-              {isExpandAll ? (
-                <>
-                  <ChevronDown className="w-4 h-4" strokeWidth={2.5} />
-                  <span className="text-sm">折叠</span>
-                </>
-              ) : (
-                <>
-                  <ChevronRight className="w-4 h-4" strokeWidth={2.5} />
-                  <span className="text-sm">展开</span>
-                </>
-              )}
-            </button>
-          </div>
+              {isExpandAll ? '折叠' : '展开'}
+            </AntButton>
+          </Space>
         </div>
       </div>
 

+ 236 - 0
src/components/DictDataForm.tsx

@@ -0,0 +1,236 @@
+import React, { useState, useImperativeHandle, forwardRef } from 'react';
+import { dictDataApi, DictDataVO } from '../api/DictData';
+import { toast } from 'sonner';
+import { Modal, Form, Input, Button, InputNumber, Radio, Select, Spin } from 'antd';
+import { CommonStatusEnum } from '../utils/constants';
+import { getIntDictOptions, DICT_TYPE } from '../utils/dict';
+
+interface DictDataFormProps {
+  onSuccess?: () => void;
+}
+
+export interface DictDataFormRef {
+  open: (type: string, id?: number, dictType?: string) => void;
+}
+
+const DictDataForm = forwardRef<DictDataFormRef, DictDataFormProps>(({ onSuccess }, ref) => {
+  const [dialogVisible, setDialogVisible] = useState(false);
+  const [dialogTitle, setDialogTitle] = useState('');
+  const [formType, setFormType] = useState<'create' | 'update'>('create');
+  const [formLoading, setFormLoading] = useState(false);
+  const [currentId, setCurrentId] = useState<number | undefined>();
+  const [currentDictType, setCurrentDictType] = useState<string>('');
+  const [form] = Form.useForm();
+
+  // 颜色类型选项
+  const colorTypeOptions = [
+    { label: '成功', value: 'success' },
+    { label: '信息', value: 'info' },
+    { label: '警告', value: 'warning' },
+    { label: '危险', value: 'danger' },
+    { label: '主要', value: 'primary' },
+    { label: '默认', value: 'default' },
+  ];
+
+  // 暴露方法给父组件
+  useImperativeHandle(ref, () => ({
+    open: (type: string, id?: number, dictType?: string) => {
+      setDialogTitle(type === 'create' ? '新增字典数据' : '修改字典数据');
+      setFormType(type as 'create' | 'update');
+      setCurrentId(id);
+      setCurrentDictType(dictType || '');
+      form.resetFields();
+
+      if (id) {
+        loadDictDataData(id);
+      } else {
+        form.setFieldsValue({
+          dictType: dictType || '',
+          label: '',
+          value: '',
+          sort: 0,
+          status: CommonStatusEnum.ENABLE,
+          colorType: 'default',
+          cssClass: '',
+          remark: '',
+        });
+        setFormLoading(false);
+      }
+
+      setDialogVisible(true);
+    },
+  }));
+
+  // 加载字典数据
+  const loadDictDataData = async (id: number) => {
+    setFormLoading(true);
+    try {
+      const res = await dictDataApi.getDictData(id);
+      const data = (res as any)?.data || res;
+      setCurrentDictType(data.dictType || '');
+      form.setFieldsValue({
+        dictType: data.dictType || '',
+        label: data.label || '',
+        value: data.value || '',
+        sort: data.sort ?? 0,
+        status: typeof data.status === 'string' ? Number(data.status) : (data.status ?? CommonStatusEnum.ENABLE),
+        colorType: data.colorType || 'default',
+        cssClass: data.cssClass || '',
+        remark: data.remark || '',
+      });
+    } catch (error: any) {
+      toast.error(error.message || '获取字典数据详情失败');
+    } finally {
+      setFormLoading(false);
+    }
+  };
+
+  // 提交表单
+  const submitForm = async () => {
+    try {
+      const values = await form.validateFields();
+      setFormLoading(true);
+      
+      const submitData: DictDataVO = {
+        ...values,
+        sort: Number(values.sort) || 0,
+        status: Number(values.status),
+        colorType: values.colorType || 'default',
+      };
+
+      if (formType === 'create') {
+        await dictDataApi.createDictData(submitData);
+        toast.success('创建成功');
+      } else {
+        if (currentId) {
+          submitData.id = currentId;
+        }
+        await dictDataApi.updateDictData(submitData);
+        toast.success('更新成功');
+      }
+      setDialogVisible(false);
+      onSuccess?.();
+    } catch (error: any) {
+      if (error.errorFields) {
+        // 表单验证错误
+        return;
+      }
+      toast.error(error.message || '操作失败');
+    } finally {
+      setFormLoading(false);
+    }
+  };
+
+  const statusOptions = getIntDictOptions(DICT_TYPE.COMMON_STATUS);
+
+  return (
+    <Modal
+      title={dialogTitle}
+      open={dialogVisible}
+      onCancel={() => setDialogVisible(false)}
+      onOk={submitForm}
+      confirmLoading={formLoading}
+      width={700}
+      destroyOnClose
+    >
+      <Spin spinning={formLoading && formType === 'update'}>
+        <Form
+          form={form}
+          layout="horizontal"
+          labelCol={{ span: 5 }}
+          wrapperCol={{ span: 19 }}
+          initialValues={{
+            sort: 0,
+            status: CommonStatusEnum.ENABLE,
+            colorType: 'default',
+            remark: '',
+          }}
+        >
+          <Form.Item
+            label="字典类型"
+            name="dictType"
+            rules={[{ required: true, message: '字典类型不能为空' }]}
+          >
+            <Input 
+              placeholder="请输入字典类型" 
+              disabled={formType === 'update'}
+            />
+          </Form.Item>
+
+          <Form.Item
+            label="数据标签"
+            name="label"
+            rules={[
+              { required: true, message: '数据标签不能为空' },
+              { max: 50, message: '数据标签不能超过50个字符' },
+            ]}
+          >
+            <Input placeholder="请输入数据标签" />
+          </Form.Item>
+
+          <Form.Item
+            label="数据键值"
+            name="value"
+            rules={[
+              { required: true, message: '数据键值不能为空' },
+              { max: 50, message: '数据键值不能超过50个字符' },
+            ]}
+          >
+            <Input placeholder="请输入数据键值" />
+          </Form.Item>
+
+          <Form.Item
+            label="显示排序"
+            name="sort"
+            rules={[{ required: true, message: '显示排序不能为空' }]}
+          >
+            <InputNumber 
+              placeholder="请输入显示排序" 
+              min={0} 
+              style={{ width: '100%' }}
+            />
+          </Form.Item>
+
+          <Form.Item
+            label="状态"
+            name="status"
+            rules={[{ required: true, message: '状态不能为空' }]}
+          >
+            <Radio.Group>
+              {statusOptions.map((option) => (
+                <Radio key={option.value} value={option.value}>
+                  {option.label}
+                </Radio>
+              ))}
+            </Radio.Group>
+          </Form.Item>
+
+          <Form.Item
+            label="颜色类型"
+            name="colorType"
+          >
+            <Select placeholder="请选择颜色类型">
+              {colorTypeOptions.map((option) => (
+                <Select.Option key={option.value} value={option.value}>
+                  {option.label} ({option.value})
+                </Select.Option>
+              ))}
+            </Select>
+          </Form.Item>
+
+          <Form.Item
+            label="备注"
+            name="remark"
+          >
+            <Input.TextArea placeholder="请输入备注" rows={4} />
+          </Form.Item>
+        </Form>
+      </Spin>
+    </Modal>
+  );
+});
+
+DictDataForm.displayName = 'DictDataForm';
+
+export default DictDataForm;
+

+ 208 - 0
src/components/DictDataManagement.tsx

@@ -0,0 +1,208 @@
+import React, { useState, useRef } from 'react';
+import { Plus, Edit2, Trash2 } from 'lucide-react';
+import { dictDataApi, DictDataVO } from '../api/DictData';
+import { toast } from 'sonner';
+import { formatDateTimeFull } from '../utils/formatTime';
+import { getIntDictOptions, DICT_TYPE, getDictLabel } from '../utils/dict';
+import { Modal, Card, Tag, Space, Table, Button as AntButton } from 'antd';
+import { ExclamationCircleOutlined } from '@ant-design/icons';
+import { Button } from './ui/button';
+import DictDataForm, { DictDataFormRef } from './DictDataForm';
+import type { ColumnsType } from 'antd/es/table';
+
+interface DictDataManagementProps {
+  dictType: string;
+  dictDataList: DictDataVO[];
+  onRefresh: () => void;
+}
+
+export default function DictDataManagement({ dictType, dictDataList, onRefresh }: DictDataManagementProps) {
+  const [list, setList] = useState<DictDataVO[]>(dictDataList);
+  const formRef = useRef<DictDataFormRef>(null);
+
+  // 当 dictDataList 变化时更新 list
+  React.useEffect(() => {
+    setList(dictDataList);
+  }, [dictDataList]);
+
+  // 打开表单
+  const openForm = (type: string, id?: number) => {
+    if (formRef.current) {
+      formRef.current.open(type, id, dictType);
+    }
+  };
+
+  // 删除字典数据
+  const handleDelete = async (id: number, label: string) => {
+    Modal.confirm({
+      title: '确认删除',
+      icon: <ExclamationCircleOutlined />,
+      content: (
+        <div>
+          <p>确定要删除字典数据 <strong>"{label}"</strong> 吗?</p>
+          <p style={{ color: '#ff4d4f', marginTop: '8px' }}>删除后无法恢复,请谨慎操作!</p>
+        </div>
+      ),
+      okText: '确定删除',
+      okType: 'danger',
+      cancelText: '取消',
+      onOk: async () => {
+        try {
+          await dictDataApi.deleteDictData(id);
+          toast.success('删除成功');
+          onRefresh();
+        } catch (error: any) {
+          toast.error(error.message || '删除失败');
+        }
+      },
+    });
+  };
+
+  // 获取状态标签
+  const getStatusLabel = (status: number) => {
+    return getDictLabel(DICT_TYPE.COMMON_STATUS, status) || (status === 0 ? '启用' : '禁用');
+  };
+
+  // 获取状态颜色
+  const getStatusColor = (status: number) => {
+    return status === 0 ? 'success' : 'default';
+  };
+
+  // 获取颜色类型标签
+  const getColorTypeTag = (colorType: string) => {
+    if (!colorType) return null;
+    const colorMap: Record<string, string> = {
+      'success': 'green',
+      'info': 'blue',
+      'warning': 'orange',
+      'danger': 'red',
+      'primary': 'blue',
+      'default': 'default',
+    };
+    return <Tag color={colorMap[colorType] || 'default'}>{colorType}</Tag>;
+  };
+
+  const columns: ColumnsType<DictDataVO> = [
+    {
+      title: '字典编码',
+      dataIndex: 'id',
+      key: 'id',
+      align: 'center',
+      width: 100,
+    },
+    {
+      title: '字典标签',
+      dataIndex: 'label',
+      key: 'label',
+      align: 'center',
+    },
+    {
+      title: '字典键值',
+      dataIndex: 'value',
+      key: 'value',
+      align: 'center',
+    },
+    {
+      title: '字典排序',
+      dataIndex: 'sort',
+      key: 'sort',
+      align: 'center',
+      width: 100,
+    },
+    {
+      title: '状态',
+      dataIndex: 'status',
+      key: 'status',
+      align: 'center',
+      width: 100,
+      render: (status: number) => (
+        <Tag color={getStatusColor(status)}>{getStatusLabel(status)}</Tag>
+      ),
+    },
+    {
+      title: '颜色类型',
+      dataIndex: 'colorType',
+      key: 'colorType',
+      align: 'center',
+      width: 120,
+      render: (colorType: string) => getColorTypeTag(colorType),
+    },
+    {
+      title: 'CSS Class',
+      dataIndex: 'cssClass',
+      key: 'cssClass',
+      align: 'center',
+      width: 120,
+    },
+    {
+      title: '备注',
+      dataIndex: 'remark',
+      key: 'remark',
+      align: 'center',
+      ellipsis: true,
+    },
+    {
+      title: '创建时间',
+      dataIndex: 'createTime',
+      key: 'createTime',
+      align: 'center',
+      width: 180,
+      render: (time: Date) => formatDateTimeFull(time),
+    },
+    {
+      title: '操作',
+      key: 'action',
+      align: 'center',
+      width: 150,
+      fixed: 'right',
+      render: (_: any, record: DictDataVO) => (
+        <Space>
+          <AntButton
+            type="link"
+            size="small"
+            onClick={() => openForm('update', record.id)}
+          >
+            修改
+          </AntButton>
+          <AntButton
+            type="link"
+            size="small"
+            danger
+            onClick={() => handleDelete(record.id!, record.label)}
+          >
+            删除
+          </AntButton>
+        </Space>
+      ),
+    },
+  ];
+
+  return (
+    <div className="space-y-4">
+      {/* 工具栏 */}
+      <div className="flex justify-end">
+        <Button
+          onClick={() => openForm('create')}
+          className="flex items-center gap-2"
+        >
+          <Plus className="w-4 h-4" />
+          新增字典数据
+        </Button>
+      </div>
+
+      {/* 表格 */}
+      <Table
+        columns={columns}
+        dataSource={list}
+        rowKey="id"
+        pagination={false}
+        size="small"
+        scroll={{ x: 1200 }}
+      />
+
+      {/* 表单弹窗 */}
+      <DictDataForm ref={formRef} onSuccess={onRefresh} />
+    </div>
+  );
+}
+

+ 178 - 0
src/components/DictTypeForm.tsx

@@ -0,0 +1,178 @@
+import React, { useState, useImperativeHandle, forwardRef } from 'react';
+import { dictTypeApi, DictTypeVO } from '../api/DictType';
+import { toast } from 'sonner';
+import { Modal, Form, Input, Button, Radio, Spin } from 'antd';
+import { CommonStatusEnum } from '../utils/constants';
+import { getIntDictOptions, DICT_TYPE } from '../utils/dict';
+
+interface DictTypeFormProps {
+  onSuccess?: () => void;
+}
+
+export interface DictTypeFormRef {
+  open: (type: string, id?: number) => void;
+}
+
+const DictTypeForm = forwardRef<DictTypeFormRef, DictTypeFormProps>(({ onSuccess }, ref) => {
+  const [dialogVisible, setDialogVisible] = useState(false);
+  const [dialogTitle, setDialogTitle] = useState('');
+  const [formType, setFormType] = useState<'create' | 'update'>('create');
+  const [formLoading, setFormLoading] = useState(false);
+  const [currentId, setCurrentId] = useState<number | undefined>();
+  const [form] = Form.useForm();
+
+  // 暴露方法给父组件
+  useImperativeHandle(ref, () => ({
+    open: (type: string, id?: number) => {
+      setDialogTitle(type === 'create' ? '新增字典类型' : '修改字典类型');
+      setFormType(type as 'create' | 'update');
+      setCurrentId(id);
+      form.resetFields();
+
+      if (id) {
+        loadDictTypeData(id);
+      } else {
+        form.setFieldsValue({
+          name: '',
+          type: '',
+          status: CommonStatusEnum.ENABLE,
+          remark: '',
+        });
+        setFormLoading(false);
+      }
+
+      setDialogVisible(true);
+    },
+  }));
+
+  // 加载字典类型数据
+  const loadDictTypeData = async (id: number) => {
+    setFormLoading(true);
+    try {
+      const res = await dictTypeApi.getDictType(id);
+      const data = (res as any)?.data || res;
+      form.setFieldsValue({
+        name: data.name,
+        type: data.type,
+        status: typeof data.status === 'string' ? Number(data.status) : (data.status ?? CommonStatusEnum.ENABLE),
+        remark: data.remark || '',
+      });
+    } catch (error: any) {
+      toast.error(error.message || '获取字典类型详情失败');
+    } finally {
+      setFormLoading(false);
+    }
+  };
+
+  // 提交表单
+  const submitForm = async () => {
+    try {
+      const values = await form.validateFields();
+      setFormLoading(true);
+      
+      const submitData: DictTypeVO = {
+        ...values,
+        status: Number(values.status),
+      };
+
+      if (formType === 'create') {
+        await dictTypeApi.createDictType(submitData);
+        toast.success('创建成功');
+      } else {
+        if (currentId) {
+          submitData.id = currentId;
+        }
+        await dictTypeApi.updateDictType(submitData);
+        toast.success('更新成功');
+      }
+      setDialogVisible(false);
+      onSuccess?.();
+    } catch (error: any) {
+      if (error.errorFields) {
+        // 表单验证错误
+        return;
+      }
+      toast.error(error.message || '操作失败');
+    } finally {
+      setFormLoading(false);
+    }
+  };
+
+  const statusOptions = getIntDictOptions(DICT_TYPE.COMMON_STATUS);
+
+  return (
+    <Modal
+      title={dialogTitle}
+      open={dialogVisible}
+      onCancel={() => setDialogVisible(false)}
+      onOk={submitForm}
+      confirmLoading={formLoading}
+      width={600}
+      destroyOnClose
+    >
+      <Spin spinning={formLoading && formType === 'update'}>
+        <Form
+          form={form}
+          layout="horizontal"
+          labelCol={{ span: 4 }}
+          wrapperCol={{ span: 20 }}
+          initialValues={{
+            status: CommonStatusEnum.ENABLE,
+            remark: '',
+          }}
+        >
+          <Form.Item
+            label="字典名称"
+            name="name"
+            rules={[
+              { required: true, message: '字典名称不能为空' },
+              { max: 50, message: '字典名称不能超过50个字符' },
+            ]}
+          >
+            <Input placeholder="请输入字典名称" />
+          </Form.Item>
+
+          <Form.Item
+            label="字典类型"
+            name="type"
+            rules={[
+              { required: true, message: '字典类型不能为空' },
+              { max: 50, message: '字典类型不能超过50个字符' },
+            ]}
+          >
+            <Input 
+              placeholder="请输入字典类型" 
+              disabled={formType === 'update'}
+            />
+          </Form.Item>
+
+          <Form.Item
+            label="状态"
+            name="status"
+            rules={[{ required: true, message: '状态不能为空' }]}
+          >
+            <Radio.Group>
+              {statusOptions.map((option) => (
+                <Radio key={option.value} value={option.value}>
+                  {option.label}
+                </Radio>
+              ))}
+            </Radio.Group>
+          </Form.Item>
+
+          <Form.Item
+            label="备注"
+            name="remark"
+          >
+            <Input.TextArea placeholder="请输入备注" rows={4} />
+          </Form.Item>
+        </Form>
+      </Spin>
+    </Modal>
+  );
+});
+
+DictTypeForm.displayName = 'DictTypeForm';
+
+export default DictTypeForm;
+

+ 563 - 0
src/components/DictTypeManagement.tsx

@@ -0,0 +1,563 @@
+import React, { useState, useEffect, useRef } from 'react';
+import { Search, Plus, RefreshCw, Edit2, Trash2, Database, Eye } from 'lucide-react';
+import { dictTypeApi, DictTypeVO, PageParam } from '../api/DictType';
+import { dictDataApi, DictDataVO, PageParam as DictDataPageParam } from '../api/DictData';
+import { toast } from 'sonner';
+import { formatDateTimeFull } from '../utils/formatTime';
+import { getIntDictOptions, DICT_TYPE, getDictLabel } from '../utils/dict';
+import { Modal, Tag, Space, Table, Button, Input, Pagination } from 'antd';
+import { ExclamationCircleOutlined } from '@ant-design/icons';
+import { Button as UIButton } from './ui/button';
+import { Table as UITable, TableBody, TableCell, TableHead, TableHeader, TableRow } from './ui/table';
+import DictTypeForm, { DictTypeFormRef } from './DictTypeForm';
+import DictDataForm, { DictDataFormRef } from './DictDataForm';
+import type { ColumnsType } from 'antd/es/table';
+
+const { Search: AntSearch } = Input;
+
+export default function DictTypeManagement() {
+  const [loading, setLoading] = useState(true);
+  const [list, setList] = useState<DictTypeVO[]>([]);
+  const [total, setTotal] = useState(0);
+  const [queryParams, setQueryParams] = useState<PageParam>({
+    pageNo: 1,
+    pageSize: 20,
+    name: undefined,
+    type: undefined,
+  });
+  
+  // 弹框相关状态
+  const [modalVisible, setModalVisible] = useState(false);
+  const [currentDictType, setCurrentDictType] = useState<DictTypeVO | null>(null);
+  const [dictDataList, setDictDataList] = useState<DictDataVO[]>([]);
+  const [dictDataTotal, setDictDataTotal] = useState(0);
+  const [dictDataLoading, setDictDataLoading] = useState(false);
+  const [dictDataSearchTerm, setDictDataSearchTerm] = useState('');
+  const [dictDataPageParams, setDictDataPageParams] = useState<DictDataPageParam>({
+    pageNo: 1,
+    pageSize: 10,
+  });
+  
+  const formRef = useRef<DictTypeFormRef>(null);
+  const dictDataFormRef = useRef<DictDataFormRef>(null);
+
+  // 获取字典类型列表
+  const getList = async (params?: PageParam) => {
+    const currentParams = params || queryParams;
+    setLoading(true);
+    try {
+      const response = await dictTypeApi.getDictTypePage(currentParams);
+      const data = (response as any)?.data || response;
+      
+      if (!data || typeof data !== 'object') {
+        setList([]);
+        setTotal(0);
+        return;
+      }
+      
+      const typeList = data.list || [];
+      setList(typeList);
+      setTotal(data.total || 0);
+    } catch (error: any) {
+      console.error('获取字典类型列表失败', error);
+      setList([]);
+      setTotal(0);
+      const errorMessage = error?.response?.data?.message || error?.message || '获取字典类型列表失败';
+      toast.error(errorMessage);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  // 打开查看全部弹框
+  const openDictDataModal = async (dictType: DictTypeVO) => {
+    setCurrentDictType(dictType);
+    setModalVisible(true);
+    setDictDataSearchTerm('');
+    setDictDataPageParams({ pageNo: 1, pageSize: 10 });
+    await loadDictDataFull(dictType.type, { pageNo: 1, pageSize: 10 });
+  };
+
+  // 加载完整的字典数据列表(弹框内使用)
+  const loadDictDataFull = async (dictType: string, params?: DictDataPageParam) => {
+    setDictDataLoading(true);
+    try {
+      const searchParams: DictDataPageParam = {
+        ...params,
+        dictType,
+        label: dictDataSearchTerm || undefined,
+      };
+      const response = await dictDataApi.getDictDataPage(searchParams);
+      const data = (response as any)?.data || response;
+      setDictDataList(data?.list || []);
+      setDictDataTotal(data?.total || 0);
+    } catch (error: any) {
+      console.error('获取字典数据失败', error);
+      setDictDataList([]);
+      setDictDataTotal(0);
+      toast.error('获取字典数据失败');
+    } finally {
+      setDictDataLoading(false);
+    }
+  };
+
+  // 搜索字典数据
+  const handleDictDataSearch = () => {
+    if (currentDictType) {
+      setDictDataPageParams(prev => ({ ...prev, pageNo: 1 }));
+      loadDictDataFull(currentDictType.type, { ...dictDataPageParams, pageNo: 1 });
+    }
+  };
+
+  // 分页变化
+  const handleDictDataPageChange = (page: number, pageSize: number) => {
+    setDictDataPageParams({ pageNo: page, pageSize });
+    if (currentDictType) {
+      loadDictDataFull(currentDictType.type, { pageNo: page, pageSize });
+    }
+  };
+
+  // 组件挂载和分页参数变化时获取数据
+  useEffect(() => {
+    getList();
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [queryParams.pageNo, queryParams.pageSize]);
+
+  // 搜索
+  const handleQuery = () => {
+    const newParams = { ...queryParams, pageNo: 1 };
+    setQueryParams(newParams);
+    getList(newParams);
+  };
+
+  // 重置搜索
+  const resetQuery = () => {
+    const resetParams: PageParam = {
+      pageNo: 1,
+      pageSize: 20,
+      name: undefined,
+      type: undefined,
+    };
+    setQueryParams(resetParams);
+    getList(resetParams);
+  };
+
+  // 打开表单
+  const openForm = (type: string, id?: number) => {
+    if (formRef.current) {
+      formRef.current.open(type, id);
+    }
+  };
+
+  // 删除字典类型
+  const handleDelete = async (id: number, name: string) => {
+    Modal.confirm({
+      title: '确认删除',
+      icon: <ExclamationCircleOutlined />,
+      content: (
+        <div>
+          <p>确定要删除字典类型 <strong>"{name}"</strong> 吗?</p>
+          <p style={{ color: '#ff4d4f', marginTop: '8px' }}>删除后无法恢复,请谨慎操作!</p>
+        </div>
+      ),
+      okText: '确定删除',
+      okType: 'danger',
+      cancelText: '取消',
+      onOk: async () => {
+        try {
+          await dictTypeApi.deleteDictType(id);
+          toast.success('删除成功');
+          await getList();
+        } catch (error: any) {
+          toast.error(error.message || '删除失败');
+        }
+      },
+    });
+  };
+
+  // 打开字典数据表单
+  const openDictDataForm = (type: string, id?: number) => {
+    if (dictDataFormRef.current && currentDictType) {
+      dictDataFormRef.current.open(type, id, currentDictType.type);
+    }
+  };
+
+  // 删除字典数据
+  const handleDeleteDictData = async (id: number, label: string) => {
+    Modal.confirm({
+      title: '确认删除',
+      icon: <ExclamationCircleOutlined />,
+      content: (
+        <div>
+          <p>确定要删除字典数据 <strong>"{label}"</strong> 吗?</p>
+          <p style={{ color: '#ff4d4f', marginTop: '8px' }}>删除后无法恢复,请谨慎操作!</p>
+        </div>
+      ),
+      okText: '确定删除',
+      okType: 'danger',
+      cancelText: '取消',
+      onOk: async () => {
+        try {
+          await dictDataApi.deleteDictData(id);
+          toast.success('删除成功');
+          if (currentDictType) {
+            await loadDictDataFull(currentDictType.type, dictDataPageParams);
+          }
+        } catch (error: any) {
+          toast.error(error.message || '删除失败');
+        }
+      },
+    });
+  };
+
+  // 获取状态标签
+  const getStatusLabel = (status: number) => {
+    return getDictLabel(DICT_TYPE.COMMON_STATUS, status) || (status === 0 ? '启用' : '禁用');
+  };
+
+  // 获取状态颜色
+  const getStatusColor = (status: number) => {
+    return status === 0 ? 'success' : 'default';
+  };
+
+  // 获取颜色类型标签
+  const getColorTypeTag = (colorType: string) => {
+    if (!colorType) return null;
+    const colorMap: Record<string, string> = {
+      'success': 'green',
+      'info': 'blue',
+      'warning': 'orange',
+      'danger': 'red',
+      'primary': 'blue',
+      'default': 'default',
+    };
+    return <Tag color={colorMap[colorType] || 'default'}>{colorType}</Tag>;
+  };
+
+  // 字典数据表格列定义
+  const dictDataColumns: ColumnsType<DictDataVO> = [
+    {
+      title: '字典编码',
+      dataIndex: 'id',
+      key: 'id',
+      align: 'center',
+      width: 100,
+    },
+    {
+      title: '字典标签',
+      dataIndex: 'label',
+      key: 'label',
+      align: 'center',
+    },
+    {
+      title: '字典键值',
+      dataIndex: 'value',
+      key: 'value',
+      align: 'center',
+    },
+    {
+      title: '字典排序',
+      dataIndex: 'sort',
+      key: 'sort',
+      align: 'center',
+      width: 100,
+    },
+    {
+      title: '状态',
+      dataIndex: 'status',
+      key: 'status',
+      align: 'center',
+      width: 100,
+      render: (status: number) => (
+        <Tag color={getStatusColor(status)}>{getStatusLabel(status)}</Tag>
+      ),
+    },
+    {
+      title: '颜色类型',
+      dataIndex: 'colorType',
+      key: 'colorType',
+      align: 'center',
+      width: 120,
+      render: (colorType: string) => getColorTypeTag(colorType),
+    },
+    {
+      title: '备注',
+      dataIndex: 'remark',
+      key: 'remark',
+      align: 'center',
+      ellipsis: true,
+    },
+    {
+      title: '操作',
+      key: 'action',
+      align: 'center',
+      width: 150,
+      fixed: 'right',
+      render: (_: any, record: DictDataVO) => (
+        <Space>
+          <AntButton
+            type="link"
+            size="small"
+            onClick={() => openDictDataForm('update', record.id)}
+          >
+            修改
+          </AntButton>
+          <AntButton
+            type="link"
+            size="small"
+            danger
+            onClick={() => handleDeleteDictData(record.id!, record.label)}
+          >
+            删除
+          </AntButton>
+        </Space>
+      ),
+    },
+  ];
+
+  return (
+    <div className="space-y-4">
+      {/* 搜索栏 */}
+      <div className="bg-white rounded-xl border border-gray-200/50 shadow-sm p-5">
+        <div className="flex items-center justify-between gap-4 flex-wrap">
+          {/* 搜索输入框 */}
+          <div className="flex items-center gap-3 flex-wrap flex-1">
+            <div className="flex items-center gap-3">
+              <label className="text-sm font-medium text-gray-700 whitespace-nowrap">字典名称:</label>
+              <Input
+                value={queryParams.name || ''}
+                onChange={(e) => setQueryParams(prev => ({ ...prev, name: e.target.value }))}
+                onPressEnter={handleQuery}
+                placeholder="请输入字典名称"
+                style={{ width: 192 }}
+                allowClear
+              />
+            </div>
+            <div className="flex items-center gap-3">
+              <label className="text-sm font-medium text-gray-700 whitespace-nowrap">字典类型:</label>
+              <Input
+                value={queryParams.type || ''}
+                onChange={(e) => setQueryParams(prev => ({ ...prev, type: e.target.value }))}
+                onPressEnter={handleQuery}
+                placeholder="请输入字典类型"
+                style={{ width: 192 }}
+                allowClear
+              />
+            </div>
+          </div>
+
+          {/* 操作按钮组 */}
+          <Space>
+            <Button
+              type="primary"
+              icon={<Search className="w-4 h-4" />}
+              onClick={handleQuery}
+            >
+              搜索
+            </Button>
+            
+            <Button
+              icon={<RefreshCw className="w-4 h-4" />}
+              onClick={resetQuery}
+            >
+              重置
+            </Button>
+            
+            <Button
+              type="primary"
+              icon={<Plus className="w-4 h-4" />}
+              onClick={() => openForm('create')}
+            >
+              新增
+            </Button>
+          </Space>
+        </div>
+      </div>
+
+      {/* 表格列表 */}
+      <div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
+        <UITable>
+          <TableHeader>
+            <TableRow>
+              <TableHead className="w-[100px] text-center">字典编号</TableHead>
+              <TableHead className="w-[150px]">字典名称</TableHead>
+              <TableHead className="w-[200px]">字典类型</TableHead>
+              <TableHead className="w-[100px] text-center">状态</TableHead>
+              <TableHead>备注</TableHead>
+              <TableHead className="w-[180px] text-center">创建时间</TableHead>
+              <TableHead className="w-[280px] text-center">操作</TableHead>
+            </TableRow>
+          </TableHeader>
+          <TableBody>
+            {loading ? (
+              <TableRow>
+                <TableCell colSpan={7} className="text-center py-8 text-gray-500">
+                  加载中...
+                </TableCell>
+              </TableRow>
+            ) : list.length === 0 ? (
+              <TableRow>
+                <TableCell colSpan={7} className="text-center py-8 text-gray-500">
+                  暂无数据
+                </TableCell>
+              </TableRow>
+            ) : (
+              list.map((row) => (
+                <TableRow key={row.id} className="hover:bg-gray-50">
+                  <TableCell className="text-center">{row.id}</TableCell>
+                  <TableCell className="font-medium">{row.name}</TableCell>
+                  <TableCell>
+                    <div className="flex items-center gap-2">
+                      <Database className="w-4 h-4 text-blue-500" />
+                      <span className="font-mono text-sm">{row.type}</span>
+                    </div>
+                  </TableCell>
+                  <TableCell className="text-center">
+                    <Tag color={getStatusColor(row.status)}>{getStatusLabel(row.status)}</Tag>
+                  </TableCell>
+                  <TableCell className="text-sm text-gray-600">{row.remark || '-'}</TableCell>
+                  <TableCell className="text-center text-sm text-gray-600">
+                    {formatDateTimeFull(row.createTime)}
+                  </TableCell>
+                  <TableCell>
+                    <div className="flex items-center gap-2 justify-center">
+                      <UIButton
+                        variant="ghost"
+                        size="sm"
+                        onClick={() => openForm('update', row.id)}
+                        className="h-8 px-2"
+                      >
+                        <Edit2 className="w-4 h-4 mr-1" />
+                        修改
+                      </UIButton>
+                      <UIButton
+                        variant="ghost"
+                        size="sm"
+                        onClick={() => openDictDataModal(row)}
+                        className="h-8 px-2"
+                      >
+                        <Eye className="w-4 h-4 mr-1" />
+                        数据
+                      </UIButton>
+                      <UIButton
+                        variant="ghost"
+                        size="sm"
+                        onClick={() => handleDelete(row.id!, row.name)}
+                        className="h-8 px-2 text-red-600 hover:text-red-700"
+                      >
+                        <Trash2 className="w-4 h-4 mr-1" />
+                        删除
+                      </UIButton>
+                    </div>
+                  </TableCell>
+                </TableRow>
+              ))
+            )}
+          </TableBody>
+        </UITable>
+      </div>
+
+      {/* 分页 */}
+      {!loading && list.length > 0 && (
+        <div className="bg-white rounded-lg border border-gray-200 px-6 py-4">
+          <div className="flex items-center justify-between">
+            <div className="text-sm text-gray-600">
+              共 <span className="text-blue-600 font-medium">{total}</span> 条记录
+            </div>
+            <div className="flex gap-2">
+              <Button
+                onClick={() => setQueryParams({ ...queryParams, pageNo: queryParams.pageNo! - 1 })}
+                disabled={queryParams.pageNo! <= 1}
+              >
+                上一页
+              </Button>
+              <span className="px-4 py-2 text-sm text-gray-600 flex items-center">
+                {queryParams.pageNo} / {Math.ceil(total / queryParams.pageSize!) || 1}
+              </span>
+              <Button
+                onClick={() => setQueryParams({ ...queryParams, pageNo: queryParams.pageNo! + 1 })}
+                disabled={queryParams.pageNo! >= Math.ceil(total / queryParams.pageSize!)}
+              >
+                下一页
+              </Button>
+            </div>
+          </div>
+        </div>
+      )}
+
+      {/* 字典数据查看全部弹框 */}
+      <Modal
+        title={
+          <div className="flex items-center gap-2">
+            <Database className="w-5 h-5 text-blue-500" />
+            <span>字典数据 - {currentDictType?.name}</span>
+            <Tag className="ml-2">{currentDictType?.type}</Tag>
+          </div>
+        }
+        open={modalVisible}
+        onCancel={() => setModalVisible(false)}
+        footer={null}
+        width={1200}
+        destroyOnClose
+      >
+        <div className="space-y-4">
+          {/* 工具栏 */}
+          <div className="flex items-center justify-between gap-4">
+            <AntSearch
+              placeholder="搜索字典标签"
+              value={dictDataSearchTerm}
+              onChange={(e) => setDictDataSearchTerm(e.target.value)}
+              onSearch={handleDictDataSearch}
+              style={{ width: 300 }}
+              enterButton
+            />
+            <UIButton
+              onClick={() => openDictDataForm('create')}
+              className="flex items-center gap-2"
+            >
+              <Plus className="w-4 h-4" />
+              新增字典项
+            </UIButton>
+          </div>
+
+          {/* 表格 */}
+          <Table
+            columns={dictDataColumns}
+            dataSource={dictDataList}
+            rowKey="id"
+            loading={dictDataLoading}
+            pagination={false}
+            size="small"
+            scroll={{ x: 1200 }}
+          />
+
+          {/* 分页 */}
+          {dictDataTotal > 0 && (
+            <div className="flex justify-end">
+              <Pagination
+                current={dictDataPageParams.pageNo}
+                pageSize={dictDataPageParams.pageSize}
+                total={dictDataTotal}
+                onChange={handleDictDataPageChange}
+                showSizeChanger
+                showQuickJumper
+                showTotal={(total) => `共 ${total} 条记录`}
+              />
+            </div>
+          )}
+        </div>
+      </Modal>
+
+      {/* 表单弹窗 */}
+      <DictTypeForm ref={formRef} onSuccess={getList} />
+      <DictDataForm 
+        ref={dictDataFormRef} 
+        onSuccess={async () => {
+          if (currentDictType) {
+            await loadDictDataFull(currentDictType.type, dictDataPageParams);
+          }
+        }} 
+      />
+    </div>
+  );
+}

+ 45 - 59
src/components/MenuManagement.tsx

@@ -9,14 +9,12 @@ import {
   ShoppingBag, Package, Database, PieChart, BarChart3, 
   Grid3x3, List, Link2, Key, Wrench, Monitor, Server
 } from 'lucide-react';
-import { Modal } from 'antd';
+import { Modal, Button, Input, Space } from 'antd';
 import { ExclamationCircleOutlined } from '@ant-design/icons';
 import { menuApi, MenuVO, MenuQueryParams, MenuType, MenuStatus } from '../api/Menu';
 import { handleTree, TreeNode } from '../utils/tree';
 import { toast } from 'sonner';
-import { Button } from './ui/button';
-import { Input } from './ui/input';
-import { Label } from './ui/label';
+import { Button as UIButton } from './ui/button';
 import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './ui/table';
 import { Switch } from './ui/switch';
 import MenuForm, { MenuFormRef } from './MenuForm';
@@ -373,30 +371,30 @@ export default function MenuManagement() {
           </TableCell>
           <TableCell>
             <div className="flex items-center gap-2 justify-center">
-              <Button
+              <UIButton
                 variant="ghost"
                 size="sm"
                 onClick={() => openForm('update', node.id)}
                 className="h-8 px-2"
               >
                 <Edit2 className="w-4 h-4" />
-              </Button>
-              <Button
+              </UIButton>
+              <UIButton
                 variant="ghost"
                 size="sm"
                 onClick={() => openForm('create', undefined, node.id)}
                 className="h-8 px-2"
               >
                 <Plus className="w-4 h-4" />
-              </Button>
-              <Button
+              </UIButton>
+              <UIButton
                 variant="ghost"
                 size="sm"
                 onClick={() => handleDelete(node.id!, node.name)}
                 className="h-8 px-2 text-red-600 hover:text-red-700"
               >
                 <Trash2 className="w-4 h-4" />
-              </Button>
+              </UIButton>
             </div>
           </TableCell>
         </TableRow>
@@ -413,73 +411,61 @@ export default function MenuManagement() {
     <div className="space-y-4">
       {/* 搜索栏 */}
       <div className="bg-white rounded-xl border border-gray-200/50 shadow-sm p-5">
-        <div className="flex items-center justify-between gap-4">
+        <div className="flex items-center justify-between gap-4 flex-wrap">
           {/* 搜索输入框 */}
-          <div className="flex items-center gap-3 flex-1 max-w-md">
-            <Label htmlFor="menu-name-search" className="text-sm font-medium text-gray-700 whitespace-nowrap">
-              菜单名称
-            </Label>
-            <Input
-              id="menu-name-search"
-              placeholder="请输入菜单名称"
-              value={queryParams.name || ''}
-              onChange={(e) => setQueryParams(prev => ({ ...prev, name: e.target.value }))}
-              onKeyDown={(e) => e.key === 'Enter' && handleQuery()}
-              className="h-10 bg-gray-50 border-gray-200 focus:bg-white focus:border-blue-400 focus:ring-2 focus:ring-blue-100 transition-all"
-            />
+          <div className="flex items-center gap-3 flex-wrap flex-1">
+            <div className="flex items-center gap-3">
+              <label className="text-sm font-medium text-gray-700 whitespace-nowrap">菜单名称:</label>
+              <Input
+                value={queryParams.name || ''}
+                onChange={(e) => setQueryParams(prev => ({ ...prev, name: e.target.value }))}
+                onPressEnter={handleQuery}
+                placeholder="请输入菜单名称"
+                style={{ width: 192 }}
+                allowClear
+              />
+            </div>
           </div>
 
           {/* 操作按钮组 */}
-          <div className="flex items-center gap-3">
-            <button
+          <Space>
+            <Button
+              type="primary"
+              icon={<Search className="w-4 h-4" />}
               onClick={handleQuery}
-              className="flex items-center gap-2 px-4 py-2.5 bg-gradient-to-r from-blue-500 to-blue-600 text-white rounded-xl hover:shadow-lg hover:shadow-blue-400/40 transition-all duration-300"
             >
-              <Search className="w-4 h-4" strokeWidth={2.5} />
-              <span className="text-sm">搜索</span>
-            </button>
+              搜索
+            </Button>
             
-            <button
+            <Button
+              icon={<RefreshCw className="w-4 h-4" />}
               onClick={resetQuery}
-              className="flex items-center gap-2 px-4 py-2.5 bg-white border border-gray-300 text-gray-700 rounded-xl hover:bg-gray-50 hover:border-gray-400 transition-all duration-300"
             >
-              <RefreshCw className="w-4 h-4" strokeWidth={2.5} />
-              <span className="text-sm">重置</span>
-            </button>
+              重置
+            </Button>
             
-            <button
+            <Button
+              type="primary"
+              icon={<Plus className="w-4 h-4" />}
               onClick={() => openForm('create')}
-              className="flex items-center gap-2 px-4 py-2.5 bg-gradient-to-r from-blue-500 to-blue-600 text-white rounded-xl hover:shadow-lg hover:shadow-blue-400/40 transition-all duration-300"
             >
-              <Plus className="w-4 h-4" strokeWidth={2.5} />
-              <span className="text-sm">新增</span>
-            </button>
+              新增
+            </Button>
             
-            <button
+            <Button
+              icon={isExpandAll ? <ChevronDown className="w-4 h-4" /> : <ChevronRight className="w-4 h-4" />}
               onClick={toggleExpandAll}
-              className="flex items-center gap-2 px-4 py-2.5 bg-white border border-gray-300 text-gray-700 rounded-xl hover:bg-gray-50 hover:border-gray-400 transition-all duration-300"
             >
-              {isExpandAll ? (
-                <>
-                  <ChevronDown className="w-4 h-4" strokeWidth={2.5} />
-                  <span className="text-sm">折叠</span>
-                </>
-              ) : (
-                <>
-                  <ChevronRight className="w-4 h-4" strokeWidth={2.5} />
-                  <span className="text-sm">展开</span>
-                </>
-              )}
-            </button>
+              {isExpandAll ? '折叠' : '展开'}
+            </Button>
             
-            <button
+            <Button
+              icon={<RefreshCw className="w-4 h-4" />}
               onClick={refreshMenu}
-              className="flex items-center gap-2 px-4 py-2.5 bg-white border border-gray-300 text-gray-700 rounded-xl hover:bg-gray-50 hover:border-gray-400 transition-all duration-300"
             >
-              <RefreshCw className="w-4 h-4" strokeWidth={2.5} />
-              <span className="text-sm">刷新缓存</span>
-            </button>
-          </div>
+              刷新缓存
+            </Button>
+          </Space>
         </div>
       </div>
 

+ 48 - 50
src/components/PostManagement.tsx

@@ -3,11 +3,9 @@ import { Search, Plus, RefreshCw, Edit2, Trash2, Download } from 'lucide-react';
 import { postApi, PostVO, PostStatus, PageParam } from '../api/Post';
 import { toast } from 'sonner';
 import { formatDateTimeFull } from '../utils/formatTime';
-import { Modal } from 'antd';
+import { Modal, Button, Input, Space } from 'antd';
 import { ExclamationCircleOutlined } from '@ant-design/icons';
-import { Button } from './ui/button';
-import { Input } from './ui/input';
-import { Label } from './ui/label';
+import { Button as UIButton } from './ui/button';
 import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './ui/table';
 import PostForm, { PostFormRef } from './PostForm';
 
@@ -20,10 +18,21 @@ export default function PostManagement() {
     pageSize: 10,
     name: undefined,
     code: undefined,
+    userId: undefined, // 添加用户ID参数
   });
   const [exportLoading, setExportLoading] = useState(false);
   const formRef = useRef<PostFormRef>(null);
 
+  // 从 sessionStorage 读取用户ID(如果存在)
+  useEffect(() => {
+    const userId = sessionStorage.getItem('selectedUserId');
+    if (userId) {
+      setQueryParams(prev => ({ ...prev, userId: Number(userId) }));
+      // 读取后清除,避免下次进入时还使用旧的用户ID
+      sessionStorage.removeItem('selectedUserId');
+    }
+  }, []);
+
   // 获取岗位列表
   const getList = async (params?: PageParam) => {
     const currentParams = params || queryParams;
@@ -184,69 +193,62 @@ export default function PostManagement() {
           {/* 搜索输入框 */}
           <div className="flex items-center gap-3 flex-wrap flex-1">
             <div className="flex items-center gap-3">
-              <Label htmlFor="post-name-search" className="text-sm font-medium text-gray-700 whitespace-nowrap">
-                岗位名称
-              </Label>
+              <label className="text-sm font-medium text-gray-700 whitespace-nowrap">岗位名称:</label>
               <Input
-                id="post-name-search"
-                placeholder="请输入岗位名称"
                 value={queryParams.name || ''}
                 onChange={(e) => setQueryParams(prev => ({ ...prev, name: e.target.value }))}
-                onKeyDown={(e) => e.key === 'Enter' && handleQuery()}
-                className="h-10 bg-gray-50 border-gray-200 focus:bg-white focus:border-blue-400 focus:ring-2 focus:ring-blue-100 transition-all w-60"
+                onPressEnter={handleQuery}
+                placeholder="请输入岗位名称"
+                style={{ width: 192 }}
+                allowClear
               />
             </div>
-
             <div className="flex items-center gap-3">
-              <Label htmlFor="post-code-search" className="text-sm font-medium text-gray-700 whitespace-nowrap">
-                岗位编码
-              </Label>
+              <label className="text-sm font-medium text-gray-700 whitespace-nowrap">岗位编码:</label>
               <Input
-                id="post-code-search"
-                placeholder="请输入岗位编码"
                 value={queryParams.code || ''}
                 onChange={(e) => setQueryParams(prev => ({ ...prev, code: e.target.value }))}
-                onKeyDown={(e) => e.key === 'Enter' && handleQuery()}
-                className="h-10 bg-gray-50 border-gray-200 focus:bg-white focus:border-blue-400 focus:ring-2 focus:ring-blue-100 transition-all w-60"
+                onPressEnter={handleQuery}
+                placeholder="请输入岗位编码"
+                style={{ width: 192 }}
+                allowClear
               />
             </div>
           </div>
 
           {/* 操作按钮组 */}
-          <div className="flex items-center gap-3">
-            <button
+          <Space>
+            <Button
+              type="primary"
+              icon={<Search className="w-4 h-4" />}
               onClick={handleQuery}
-              className="flex items-center gap-2 px-4 py-2.5 bg-gradient-to-r from-blue-500 to-blue-600 text-white rounded-xl hover:shadow-lg hover:shadow-blue-400/40 transition-all duration-300"
             >
-              <Search className="w-4 h-4" strokeWidth={2.5} />
-              <span className="text-sm">搜索</span>
-            </button>
+              搜索
+            </Button>
             
-            <button
+            <Button
+              icon={<RefreshCw className="w-4 h-4" />}
               onClick={resetQuery}
-              className="flex items-center gap-2 px-4 py-2.5 bg-white border border-gray-300 text-gray-700 rounded-xl hover:bg-gray-50 hover:border-gray-400 transition-all duration-300"
             >
-              <RefreshCw className="w-4 h-4" strokeWidth={2.5} />
-              <span className="text-sm">重置</span>
-            </button>
+              重置
+            </Button>
             
-            <button
+            <Button
+              type="primary"
+              icon={<Plus className="w-4 h-4" />}
               onClick={() => openForm('create')}
-              className="flex items-center gap-2 px-4 py-2.5 bg-gradient-to-r from-blue-500 to-blue-600 text-white rounded-xl hover:shadow-lg hover:shadow-blue-400/40 transition-all duration-300"
             >
-              <Plus className="w-4 h-4" strokeWidth={2.5} />
-              <span className="text-sm">新增</span>
-            </button>
+              新增
+            </Button>
             
-            <button
+            <Button
+              icon={<Download className="w-4 h-4" />}
               onClick={handleExport}
-              disabled={exportLoading}
-              className="flex items-center gap-2 px-4 py-2.5 bg-white border border-gray-300 text-gray-700 rounded-xl hover:bg-gray-50 hover:border-gray-400 transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed"
+              loading={exportLoading}
             >
-              <Download className="w-4 h-4" strokeWidth={2.5} />
-              <span className="text-sm">{exportLoading ? '导出中...' : '导出'}</span>
-            </button>
-          </div>
+              导出
+            </Button>
+          </Space>
         </div>
       </div>
 
@@ -296,22 +298,22 @@ export default function PostManagement() {
                   </TableCell>
                   <TableCell>
                     <div className="flex items-center gap-2 justify-center">
-                      <Button
+                      <UIButton
                         variant="ghost"
                         size="sm"
                         onClick={() => openForm('update', row.id)}
                         className="h-8 px-2"
                       >
                         <Edit2 className="w-4 h-4" />
-                      </Button>
-                      <Button
+                      </UIButton>
+                      <UIButton
                         variant="ghost"
                         size="sm"
                         onClick={() => handleDelete(row.id!, row.name)}
                         className="h-8 px-2 text-red-600 hover:text-red-700"
                       >
                         <Trash2 className="w-4 h-4" />
-                      </Button>
+                      </UIButton>
                     </div>
                   </TableCell>
                 </TableRow>
@@ -330,8 +332,6 @@ export default function PostManagement() {
             </div>
             <div className="flex gap-2">
               <Button
-                variant="outline"
-                size="sm"
                 onClick={() => setQueryParams({ ...queryParams, pageNo: queryParams.pageNo! - 1 })}
                 disabled={queryParams.pageNo! <= 1}
               >
@@ -341,8 +341,6 @@ export default function PostManagement() {
                 {queryParams.pageNo} / {Math.ceil(total / queryParams.pageSize!) || 1}
               </span>
               <Button
-                variant="outline"
-                size="sm"
                 onClick={() => setQueryParams({ ...queryParams, pageNo: queryParams.pageNo! + 1 })}
                 disabled={queryParams.pageNo! >= Math.ceil(total / queryParams.pageSize!)}
               >

+ 41 - 55
src/components/RoleManagement.tsx

@@ -4,11 +4,9 @@ import { roleApi, RoleVO, PageParam } from '../api/Role';
 import { toast } from 'sonner';
 import { formatDateTimeFull } from '../utils/formatTime';
 import { getIntDictOptions, DICT_TYPE, getDictLabel } from '../utils/dict';
-import { Modal } from 'antd';
+import { Modal, Button, Input, Space } from 'antd';
 import { ExclamationCircleOutlined } from '@ant-design/icons';
-import { Button } from './ui/button';
-import { Input } from './ui/input';
-import { Label } from './ui/label';
+import { Button as UIButton } from './ui/button';
 import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './ui/table';
 import RoleForm, { RoleFormRef } from './RoleForm';
 import RoleAssignMenuForm, { RoleAssignMenuFormRef } from './RoleAssignMenuForm';
@@ -185,70 +183,62 @@ export default function RoleManagement() {
           {/* 搜索输入框 */}
           <div className="flex items-center gap-3 flex-wrap flex-1">
             <div className="flex items-center gap-3">
-              <Label htmlFor="role-name-search" className="text-sm font-medium text-gray-700 whitespace-nowrap">
-                角色名称
-              </Label>
+              <label className="text-sm font-medium text-gray-700 whitespace-nowrap">角色名称:</label>
               <Input
-                id="role-name-search"
-                placeholder="请输入角色名称"
                 value={queryParams.name || ''}
                 onChange={(e) => setQueryParams(prev => ({ ...prev, name: e.target.value }))}
-                onKeyDown={(e) => e.key === 'Enter' && handleQuery()}
-                className="h-10 bg-gray-50 border-gray-200 focus:bg-white focus:border-blue-400 focus:ring-2 focus:ring-blue-100 transition-all w-60"
+                onPressEnter={handleQuery}
+                placeholder="请输入角色名称"
+                style={{ width: 192 }}
+                allowClear
               />
             </div>
-
             <div className="flex items-center gap-3">
-              <Label htmlFor="role-code-search" className="text-sm font-medium text-gray-700 whitespace-nowrap">
-                角色标识
-              </Label>
+              <label className="text-sm font-medium text-gray-700 whitespace-nowrap">角色标识:</label>
               <Input
-                id="role-code-search"
-                placeholder="请输入角色标识"
                 value={queryParams.code || ''}
                 onChange={(e) => setQueryParams(prev => ({ ...prev, code: e.target.value }))}
-                onKeyDown={(e) => e.key === 'Enter' && handleQuery()}
-                className="h-10 bg-gray-50 border-gray-200 focus:bg-white focus:border-blue-400 focus:ring-2 focus:ring-blue-100 transition-all w-60"
+                onPressEnter={handleQuery}
+                placeholder="请输入角色标识"
+                style={{ width: 192 }}
+                allowClear
               />
             </div>
-
           </div>
 
           {/* 操作按钮组 */}
-          <div className="flex items-center gap-3">
-            <button
+          <Space>
+            <Button
+              type="primary"
+              icon={<Search className="w-4 h-4" />}
               onClick={handleQuery}
-              className="flex items-center gap-2 px-4 py-2.5 bg-gradient-to-r from-blue-500 to-blue-600 text-white rounded-xl hover:shadow-lg hover:shadow-blue-400/40 transition-all duration-300"
             >
-              <Search className="w-4 h-4" strokeWidth={2.5} />
-              <span className="text-sm">搜索</span>
-            </button>
+              搜索
+            </Button>
             
-            <button
+            <Button
+              icon={<RefreshCw className="w-4 h-4" />}
               onClick={resetQuery}
-              className="flex items-center gap-2 px-4 py-2.5 bg-white border border-gray-300 text-gray-700 rounded-xl hover:bg-gray-50 hover:border-gray-400 transition-all duration-300"
             >
-              <RefreshCw className="w-4 h-4" strokeWidth={2.5} />
-              <span className="text-sm">重置</span>
-            </button>
+              重置
+            </Button>
             
-            <button
+            <Button
+              type="primary"
+              icon={<Plus className="w-4 h-4" />}
               onClick={() => openForm('create')}
-              className="flex items-center gap-2 px-4 py-2.5 bg-gradient-to-r from-blue-500 to-blue-600 text-white rounded-xl hover:shadow-lg hover:shadow-blue-400/40 transition-all duration-300"
             >
-              <Plus className="w-4 h-4" strokeWidth={2.5} />
-              <span className="text-sm">新增</span>
-            </button>
+              新增
+            </Button>
             
-            <button
+            <Button
+              icon={<Download className="w-4 h-4" />}
               onClick={handleExport}
-              disabled={exportLoading}
-              className="flex items-center gap-2 px-4 py-2.5 bg-white border border-gray-300 text-gray-700 rounded-xl hover:bg-gray-50 hover:border-gray-400 transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed"
+              loading={exportLoading}
             >
-              <Download className="w-4 h-4" strokeWidth={2.5} />
-              <span className="text-sm">{exportLoading ? '导出中...' : '导出'}</span>
-            </button>
-          </div>
+              导出
+            </Button>
+          </Space>
         </div>
       </div>
 
@@ -302,7 +292,7 @@ export default function RoleManagement() {
                   </TableCell>
                   <TableCell>
                     <div className="flex items-center gap-2 justify-center">
-                      <Button
+                      <UIButton
                         variant="ghost"
                         size="sm"
                         onClick={() => openForm('update', row.id)}
@@ -310,8 +300,8 @@ export default function RoleManagement() {
                       >
                         <Edit2 className="w-4 h-4" />
                         <span className="ml-1">编辑</span>
-                      </Button>
-                      <Button
+                      </UIButton>
+                      <UIButton
                         variant="ghost"
                         size="sm"
                         onClick={() => openAssignMenuForm(row)}
@@ -319,8 +309,8 @@ export default function RoleManagement() {
                       >
                         <Settings className="w-4 h-4" />
                         <span className="ml-1">菜单权限</span>
-                      </Button>
-                      <Button
+                      </UIButton>
+                      <UIButton
                         variant="ghost"
                         size="sm"
                         onClick={() => openDataPermissionForm(row)}
@@ -328,8 +318,8 @@ export default function RoleManagement() {
                       >
                         <Shield className="w-4 h-4" />
                         <span className="ml-1">数据权限</span>
-                      </Button>
-                      <Button
+                      </UIButton>
+                      <UIButton
                         variant="ghost"
                         size="sm"
                         onClick={() => handleDelete(row.id!, row.name)}
@@ -337,7 +327,7 @@ export default function RoleManagement() {
                       >
                         <Trash2 className="w-4 h-4" />
                         <span className="ml-1">删除</span>
-                      </Button>
+                      </UIButton>
                     </div>
                   </TableCell>
                 </TableRow>
@@ -356,8 +346,6 @@ export default function RoleManagement() {
             </div>
             <div className="flex gap-2">
               <Button
-                variant="outline"
-                size="sm"
                 onClick={() => setQueryParams({ ...queryParams, pageNo: queryParams.pageNo! - 1 })}
                 disabled={queryParams.pageNo! <= 1}
               >
@@ -367,8 +355,6 @@ export default function RoleManagement() {
                 {queryParams.pageNo} / {Math.ceil(total / queryParams.pageSize!) || 1}
               </span>
               <Button
-                variant="outline"
-                size="sm"
                 onClick={() => setQueryParams({ ...queryParams, pageNo: queryParams.pageNo! + 1 })}
                 disabled={queryParams.pageNo! >= Math.ceil(total / queryParams.pageSize!)}
               >

+ 383 - 0
src/components/SegregationPointForm.tsx

@@ -0,0 +1,383 @@
+import React, { useState, useImperativeHandle, forwardRef, useEffect } from 'react';
+import { Modal, Form, Input, Select, TreeSelect, Upload, Image, Row, Col, message } from 'antd';
+import { PlusOutlined, UploadOutlined } from '@ant-design/icons';
+import type { UploadFile } from 'antd/es/upload/interface';
+import { segregationPointApi, SegregationPointVO } from '../api/spm/index';
+import { marsDeptApi } from '../api/marsdept/index';
+import { lotoStationApi } from '../api/lotoStation/index';
+import { rfidApi } from '../api/rfid/index';
+import { loginApi } from '../api/Login';
+import { handleTree } from '../utils/tree';
+import { getStrDictOptions, DICT_TYPE } from '../utils/dict';
+import { toast } from 'sonner';
+
+interface SegregationPointFormProps {
+  onSuccess?: () => void;
+}
+
+export interface SegregationPointFormRef {
+  open: (type: string, id?: number) => void;
+}
+
+const SegregationPointForm = forwardRef<SegregationPointFormRef, SegregationPointFormProps>(
+  ({ onSuccess }, ref) => {
+    const [dialogVisible, setDialogVisible] = useState(false);
+    const [dialogTitle, setDialogTitle] = useState('');
+    const [formLoading, setFormLoading] = useState(false);
+    const [formType, setFormType] = useState<'create' | 'update'>('create');
+    const [form] = Form.useForm();
+    const [selectedImageIndex, setSelectedImageIndex] = useState(-1);
+    const [imageMap, setImageMap] = useState<Record<number, string>>({});
+
+    // 下拉选项数据
+    const [deptOptions, setDeptOptions] = useState<any[]>([]);
+    const [lotoOptions, setLotoOptions] = useState<Array<{ label: string; value: number }>>([]);
+    const [rfidTokenData, setRfidTokenData] = useState<Array<{ label: string; value: number }>>([]);
+    const powerTypeOptions = getStrDictOptions(DICT_TYPE.POWER_TYPE);
+
+    // 打开弹窗
+    const open = async (type: string, id?: number) => {
+      setDialogVisible(true);
+      setDialogTitle(type === 'create' ? '新增隔离点' : '编辑隔离点');
+      setFormType(type as 'create' | 'update');
+      form.resetFields();
+      setSelectedImageIndex(-1);
+      setImageMap({});
+
+      // 加载下拉选项数据
+      await loadOptionsData();
+
+      // 修改时,设置数据
+      if (id) {
+        setFormLoading(true);
+        try {
+          const response = await segregationPointApi.selectIsIsolationPointById(id);
+          const data = response?.data || response;
+          form.setFieldsValue({
+            pointId: data.pointId,
+            pointName: data.pointName,
+            pointIcon: data.pointIcon,
+            pointPicture: data.pointPicture,
+            rfidId: data.rfidId,
+            workstationId: data.workstationId,
+            lotoId: data.lotoId,
+            powerType: data.powerType,
+            pointSerialNumber: data.pointSerialNumber,
+            remark: data.remark,
+          });
+
+          // 获取隔离点图标
+          await loadIsolationIcons(data.pointIcon);
+        } catch (error: any) {
+          toast.error(error.message || '获取隔离点详情失败');
+        } finally {
+          setFormLoading(false);
+        }
+      } else {
+        // 新增时也加载图标
+        await loadIsolationIcons();
+      }
+    };
+
+    // 加载下拉选项数据
+    const loadOptionsData = async () => {
+      try {
+        // 获取岗位数据
+        const deptRes = await marsDeptApi.listMarsDept({ pageNo: 1, pageSize: -1 });
+        const deptTreeData = handleTree(deptRes.list, 'id', 'parentId');
+        setDeptOptions(convertToTreeSelectData(deptTreeData, 'workstationName'));
+
+        // 获取锁定站数据
+        const lotoRes = await lotoStationApi.listLoto({ pageNo: 1, pageSize: -1 });
+        setLotoOptions(lotoRes.list.map(item => ({
+          value: item.id!,
+          label: item.lotoName,
+        })));
+
+        // 获取RFID Token数据
+        const rfidRes = await rfidApi.getIsRfidTokenPage({ pageNo: 1, pageSize: -1 });
+        setRfidTokenData(rfidRes.list.map(record => ({
+          value: record.id!,
+          label: record.rfid,
+        })));
+      } catch (error) {
+        console.error('加载选项数据失败:', error);
+      }
+    };
+
+    // 加载隔离点图标
+    const loadIsolationIcons = async (selectedIcon?: string) => {
+      try {
+        const sysAttrKey1 = 'sys.icon_set.isolation';
+        const iconRes = await loginApi.getSystemAttributeByKey(sysAttrKey1);
+        const values = (iconRes as any).sysAttrValue?.split(',').map((v: string) => v.trim()) || [];
+        
+        const iconPromises = values.map((value: string) => 
+          loginApi.getSystemAttributeByKey(value)
+        );
+        const iconResponses = await Promise.all(iconPromises);
+        
+        const newImageMap: Record<number, string> = {};
+        iconResponses.forEach((response: any, index: number) => {
+          newImageMap[index] = response.sysAttrValue || '';
+        });
+        setImageMap(newImageMap);
+
+        // 确定选中的图片索引
+        if (selectedIcon) {
+          const index = getImageIndexByIcon(selectedIcon, newImageMap);
+          setSelectedImageIndex(index);
+        }
+      } catch (error) {
+        console.error('加载隔离点图标失败:', error);
+      }
+    };
+
+    // 获取图片索引
+    const getImageIndexByIcon = (iconUrl: string, imageMap: Record<number, string>): number => {
+      for (const [index, imageUrl] of Object.entries(imageMap)) {
+        if (imageUrl === iconUrl) {
+          return Number(index);
+        }
+      }
+      return -1;
+    };
+
+    // 转换树形数据为 TreeSelect 格式
+    const convertToTreeSelectData = (treeData: any[], labelKey: string): any[] => {
+      return treeData.map(item => ({
+        title: item[labelKey],
+        value: item.id,
+        key: item.id,
+        children: item.children ? convertToTreeSelectData(item.children, labelKey) : undefined,
+      }));
+    };
+
+    // 选择隔离点图标
+    const selectIcon = (imageUrl: string, index: number) => {
+      form.setFieldsValue({ pointIcon: imageUrl });
+      setSelectedImageIndex(index);
+    };
+
+    useImperativeHandle(ref, () => ({
+      open,
+    }));
+
+    // 提交表单
+    const submitForm = async () => {
+      try {
+        const values = await form.validateFields();
+        
+        setFormLoading(true);
+        try {
+          const data: SegregationPointVO = {
+            pointId: values.pointId,
+            pointName: values.pointName,
+            pointIcon: values.pointIcon,
+            pointPicture: values.pointPicture,
+            rfidId: values.rfidId,
+            workstationId: values.workstationId,
+            lotoId: values.lotoId,
+            powerType: values.powerType,
+            pointSerialNumber: values.pointSerialNumber,
+            remark: values.remark,
+          };
+
+          if (formType === 'create') {
+            await segregationPointApi.addinsertIsIsolationPoint(data);
+            toast.success('新增成功');
+          } else {
+            await segregationPointApi.updateIsIsolationPoint(data);
+            toast.success('修改成功');
+          }
+          setDialogVisible(false);
+          onSuccess?.();
+        } catch (error: any) {
+          toast.error(error.message || '操作失败');
+        } finally {
+          setFormLoading(false);
+        }
+      } catch (error) {
+        // 表单验证失败
+      }
+    };
+
+    // 图片上传配置
+    const uploadProps = {
+      name: 'file',
+      action: '/api/upload', // 需要根据实际接口调整
+      listType: 'picture-card' as const,
+      maxCount: 1,
+      beforeUpload: (file: File) => {
+        const isImage = file.type.startsWith('image/');
+        if (!isImage) {
+          message.error('只能上传图片文件!');
+          return false;
+        }
+        const isLt2M = file.size / 1024 / 1024 < 2;
+        if (!isLt2M) {
+          message.error('图片大小不能超过 2MB!');
+          return false;
+        }
+        return false; // 阻止自动上传,需要手动处理
+      },
+      onChange: (info: any) => {
+        if (info.file.status === 'done') {
+          // 上传成功,设置图片URL
+          form.setFieldsValue({ pointPicture: info.file.response?.url || info.file.url });
+        }
+      },
+    };
+
+    return (
+      <Modal
+        title={dialogTitle}
+        open={dialogVisible}
+        onCancel={() => setDialogVisible(false)}
+        onOk={submitForm}
+        confirmLoading={formLoading}
+        width={960}
+        destroyOnClose
+      >
+        <Form
+          form={form}
+          labelCol={{ span: 6 }}
+          wrapperCol={{ span: 18 }}
+          layout="horizontal"
+        >
+          <Row gutter={16}>
+            <Col span={12}>
+              <Form.Item
+                label="隔离点名称"
+                name="pointName"
+                rules={[{ required: true, message: '隔离点名称不能为空' }]}
+              >
+                <Input placeholder="请输入隔离点名称" />
+              </Form.Item>
+            </Col>
+            <Col span={12}>
+              <Form.Item
+                label="隔离点NFC"
+                name="rfidId"
+                rules={[{ required: true, message: '隔离点NFC不能为空' }]}
+              >
+                <Select placeholder="请选择隔离点NFC" options={rfidTokenData} />
+              </Form.Item>
+            </Col>
+          </Row>
+
+          <Row gutter={16}>
+            <Col span={12}>
+              <Form.Item
+                label="锁定站"
+                name="lotoId"
+                rules={[{ required: true, message: '锁定站不能为空' }]}
+              >
+                <Select placeholder="请选择锁定站" options={lotoOptions} />
+              </Form.Item>
+            </Col>
+            <Col span={12}>
+              <Form.Item
+                label="岗位"
+                name="workstationId"
+                rules={[{ required: true, message: '岗位不能为空' }]}
+              >
+                <TreeSelect
+                  treeData={deptOptions}
+                  placeholder="选择岗位"
+                  style={{ width: '100%' }}
+                />
+              </Form.Item>
+            </Col>
+          </Row>
+
+          <Row gutter={16}>
+            <Col span={12}>
+              <Form.Item
+                label="能量源"
+                name="powerType"
+              >
+                <Select placeholder="请选择能量源" options={powerTypeOptions} />
+              </Form.Item>
+            </Col>
+            <Col span={12}>
+              <Form.Item
+                label="隔离点序列号"
+                name="pointSerialNumber"
+              >
+                <Input placeholder="请输入隔离点序列号" />
+              </Form.Item>
+            </Col>
+          </Row>
+
+          <Row gutter={16}>
+            <Col span={12}>
+              <Form.Item
+                label="作用"
+                name="remark"
+              >
+                <Input placeholder="请输入作用" />
+              </Form.Item>
+            </Col>
+          </Row>
+
+          <Row gutter={16}>
+            <Col span={12}>
+              <Form.Item
+                label="隔离点图标"
+                name="pointIcon"
+              >
+                <div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
+                  {Object.entries(imageMap).map(([index, imageUrl]) => (
+                    <div
+                      key={index}
+                      onClick={() => selectIcon(imageUrl, Number(index))}
+                      style={{
+                        width: '55px',
+                        height: '55px',
+                        cursor: 'pointer',
+                        border: selectedImageIndex === Number(index) ? '2px solid rgb(2, 86, 255)' : '2px solid transparent',
+                        padding: '2px',
+                        transition: 'border-color 0.3s',
+                      }}
+                    >
+                      <img src={imageUrl} alt="Isolation Icon" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
+                    </div>
+                  ))}
+                </div>
+              </Form.Item>
+            </Col>
+            <Col span={12}>
+              <Form.Item
+                label="隔离点图片"
+                name="pointPicture"
+              >
+                <Upload {...uploadProps}>
+                  {form.getFieldValue('pointPicture') ? (
+                    <Image
+                      src={form.getFieldValue('pointPicture')}
+                      width={100}
+                      height={100}
+                      style={{ objectFit: 'cover' }}
+                      preview={false}
+                    />
+                  ) : (
+                    <div>
+                      <PlusOutlined />
+                      <div style={{ marginTop: 8 }}>上传</div>
+                    </div>
+                  )}
+                </Upload>
+              </Form.Item>
+            </Col>
+          </Row>
+        </Form>
+      </Modal>
+    );
+  }
+);
+
+SegregationPointForm.displayName = 'SegregationPointForm';
+
+export default SegregationPointForm;
+

+ 439 - 0
src/components/SegregationPointManagement.tsx

@@ -0,0 +1,439 @@
+import React, { useState, useEffect, useRef } from 'react';
+import { Plus, Search, RefreshCw } from 'lucide-react';
+import { segregationPointApi, SegregationPointVO, PageParam } from '../api/spm/index';
+import { marsDeptApi, MarsDeptVO } from '../api/marsdept/index';
+import { lotoStationApi, LotoStationVO } from '../api/lotoStation/index';
+import { technologyApi, TechnologyVO } from '../api/technology/index';
+import { toast } from 'sonner';
+import { Modal, Table, Input, Button, Select, TreeSelect, Space, Image, Switch } from 'antd';
+import { ExclamationCircleOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
+import type { ColumnsType } from 'antd/es/table';
+import { handleTree } from '../utils/tree';
+import { getStrDictOptions, DICT_TYPE } from '../utils/dict';
+import SegregationPointForm, { SegregationPointFormRef } from './SegregationPointForm';
+
+interface SegregationPointManagementProps {
+  subMenu?: string;
+}
+
+export default function SegregationPointManagement({ subMenu }: SegregationPointManagementProps) {
+  const [loading, setLoading] = useState(true);
+  const [list, setList] = useState<SegregationPointVO[]>([]);
+  const [total, setTotal] = useState(0);
+  const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
+  const [queryParams, setQueryParams] = useState<PageParam>({
+    current: 1,
+    size: 10,
+    pointName: undefined,
+    workstationId: undefined,
+    machineryId: undefined,
+    lotoId: undefined,
+    powerType: undefined,
+  });
+
+  // 下拉选项数据
+  const [deptOptions, setDeptOptions] = useState<any[]>([]);
+  const [machineryOptions, setMachineryOptions] = useState<any[]>([]);
+  const [lotoOptions, setLotoOptions] = useState<Array<{ label: string; value: number }>>([]);
+  const powerTypeOptions = getStrDictOptions(DICT_TYPE.POWER_TYPE);
+
+  const formRef = useRef<SegregationPointFormRef>(null);
+
+  // 获取隔离点列表
+  const getList = async (params?: PageParam) => {
+    const currentParams = params || queryParams;
+    setLoading(true);
+    try {
+      console.log('SegregationPointManagement: 开始获取隔离点列表', currentParams);
+      const response = await segregationPointApi.getIsIsolationPointPage(currentParams);
+      console.log('SegregationPointManagement: API 响应', response);
+      
+      // 处理响应数据
+      let data;
+      if (response && typeof response === 'object') {
+        // 如果响应有 data 属性,使用 data;否则直接使用 response
+        if ('data' in response && response.data) {
+          data = response.data;
+        } else if ('list' in response || 'total' in response) {
+          data = response;
+        } else {
+          data = response;
+        }
+      } else {
+        data = response;
+      }
+      
+      setList(data?.list || []);
+      setTotal(data?.total || 0);
+      console.log('SegregationPointManagement: 设置列表数据', { list: data?.list || [], total: data?.total || 0 });
+    } catch (error: any) {
+      console.error('SegregationPointManagement: 获取列表失败', error);
+      toast.error(error.message || '获取隔离点列表失败');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  useEffect(() => {
+    getList();
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [queryParams.current, queryParams.size]);
+
+  // 初始化数据
+  useEffect(() => {
+    const initData = async () => {
+      try {
+        // 获取岗位数据
+        const deptRes = await marsDeptApi.listMarsDept({ pageNo: 1, pageSize: -1 });
+        const deptTreeData = handleTree(deptRes.list, 'id', 'parentId');
+        setDeptOptions(convertToTreeSelectData(deptTreeData, 'workstationName'));
+
+        // 获取锁定站数据
+        const lotoRes = await lotoStationApi.listLoto({ pageNo: 1, pageSize: -1 });
+        setLotoOptions(lotoRes.list.map(item => ({
+          value: item.id!,
+          label: item.lotoName,
+        })));
+
+        // 获取设备/工艺数据
+        const techRes = await technologyApi.listTechnology({ pageNo: 1, pageSize: -1 });
+        const techData = techRes.list.filter(item => item.machineryType === '工艺');
+        const techTreeData = handleTree(techData, 'id', 'parentId');
+        setMachineryOptions(convertToTreeSelectData(techTreeData, 'machineryName'));
+      } catch (error) {
+        console.error('初始化数据失败:', error);
+      }
+    };
+    initData();
+  }, []);
+
+  // 转换树形数据为 TreeSelect 格式
+  const convertToTreeSelectData = (treeData: any[], labelKey: string): any[] => {
+    return treeData.map(item => ({
+      title: item[labelKey],
+      value: item.id,
+      key: item.id,
+      children: item.children ? convertToTreeSelectData(item.children, labelKey) : undefined,
+    }));
+  };
+
+  // 搜索
+  const handleQuery = () => {
+    setQueryParams({ ...queryParams, current: 1 });
+    getList({ ...queryParams, current: 1 });
+  };
+
+  // 重置搜索
+  const resetQuery = () => {
+    const resetParams: PageParam = {
+      current: 1,
+      size: 10,
+      pointName: undefined,
+      workstationId: undefined,
+      machineryId: undefined,
+      lotoId: undefined,
+      powerType: undefined,
+    };
+    setQueryParams(resetParams);
+    getList(resetParams);
+  };
+
+  // 打开表单
+  const openForm = (type: string, id?: number) => {
+    formRef.current?.open(type, id);
+  };
+
+  // 删除隔离点
+  const handleDelete = async (id?: number) => {
+    const pointIds = id ? [id] : selectedRowKeys.map(key => Number(key));
+    
+    Modal.confirm({
+      title: '确认删除',
+      icon: <ExclamationCircleOutlined />,
+      content: `确定要删除选中的 ${pointIds.length} 条数据吗?`,
+      okText: '确定删除',
+      okType: 'danger',
+      cancelText: '取消',
+      onOk: async () => {
+        try {
+          await segregationPointApi.deleteIsIsolationPointByPointIds(pointIds);
+          toast.success('删除成功');
+          setSelectedRowKeys([]);
+          await getList();
+        } catch (error: any) {
+          toast.error(error.message || '删除失败');
+        }
+      },
+    });
+  };
+
+  // 表格列配置
+  const columns: ColumnsType<SegregationPointVO> = [
+    {
+      title: '隔离点编号',
+      dataIndex: 'id',
+      width: 100,
+      align: 'center',
+    },
+    {
+      title: '隔离点名称',
+      dataIndex: 'pointName',
+      align: 'center',
+    },
+    {
+      title: '隔离点图标',
+      dataIndex: 'pointIcon',
+      width: 100,
+      align: 'center',
+      render: (text: string) => {
+        if (text) {
+          return <Image src={text} width={50} height={50} style={{ objectFit: 'cover' }} />;
+        }
+        return '-';
+      },
+    },
+    {
+      title: '开关状态',
+      dataIndex: 'switchStatus',
+      width: 100,
+      align: 'center',
+      render: (status: number | string | null) => {
+        if (status === null || status === undefined) {
+          return '-';
+        }
+        const isChecked = String(status) === '1';
+        return (
+          <Switch
+            checked={isChecked}
+            checkedChildren="ON"
+            unCheckedChildren="OFF"
+            disabled
+            style={{ pointerEvents: 'none' }}
+          />
+        );
+      },
+    },
+    {
+      title: '隔离点NFC',
+      dataIndex: 'pointNfc',
+      align: 'center',
+    },
+    {
+      title: '岗位',
+      dataIndex: 'workstationName',
+      align: 'center',
+    },
+    {
+      title: '设备/工艺',
+      dataIndex: 'machineryName',
+      align: 'center',
+    },
+    {
+      title: '锁定站',
+      dataIndex: 'lotoName',
+      align: 'center',
+    },
+    {
+      title: '隔离点序列号',
+      dataIndex: 'pointSerialNumber',
+      align: 'center',
+    },
+    {
+      title: '作用',
+      dataIndex: 'remark',
+      align: 'center',
+    },
+    {
+      title: '隔离点图片',
+      dataIndex: 'pointPicture',
+      width: 100,
+      align: 'center',
+      render: (text: string) => {
+        if (text) {
+          return <Image src={text} width={50} height={50} style={{ objectFit: 'cover' }} />;
+        }
+        return '-';
+      },
+    },
+    {
+      title: '能量源',
+      dataIndex: 'powerType',
+      align: 'center',
+      render: (value: string) => {
+        const option = powerTypeOptions.find(opt => opt.value === value);
+        return option ? option.label : value || '-';
+      },
+    },
+    {
+      title: '操作',
+      width: 120,
+      align: 'center',
+      fixed: 'right',
+      render: (_: any, record: SegregationPointVO) => (
+        <Space>
+          <Button
+            type="link"
+            icon={<EditOutlined />}
+            onClick={() => openForm('update', record.pointId)}
+            title="编辑"
+          />
+          <Button
+            type="link"
+            danger
+            icon={<DeleteOutlined />}
+            onClick={() => handleDelete(record.pointId)}
+            title="删除"
+          />
+        </Space>
+      ),
+    },
+  ];
+
+  return (
+    <div className="p-6 space-y-4">
+      {/* 搜索栏 */}
+      <div className="bg-white rounded-xl border border-gray-200/50 shadow-sm p-5">
+        <div className="flex items-center justify-between gap-4 flex-wrap">
+          {/* 搜索输入框 */}
+          <div className="flex items-center gap-3 flex-wrap flex-1">
+            <div className="flex items-center gap-3">
+              <label className="text-sm font-medium text-gray-700 whitespace-nowrap">隔离点名称:</label>
+              <Input
+                value={queryParams.pointName}
+                onChange={(e) => setQueryParams({ ...queryParams, pointName: e.target.value })}
+                onPressEnter={handleQuery}
+                placeholder="请输入隔离点名称"
+                style={{ width: 192 }}
+                allowClear
+              />
+            </div>
+            <div className="flex items-center gap-3">
+              <label className="text-sm font-medium text-gray-700 whitespace-nowrap">岗位:</label>
+              <TreeSelect
+                value={queryParams.workstationId}
+                onChange={(value) => setQueryParams({ ...queryParams, workstationId: value })}
+                treeData={deptOptions}
+                placeholder="选择岗位"
+                allowClear
+                style={{ width: 192 }}
+              />
+            </div>
+            <div className="flex items-center gap-3">
+              <label className="text-sm font-medium text-gray-700 whitespace-nowrap">设备/工艺:</label>
+              <TreeSelect
+                value={queryParams.machineryId}
+                onChange={(value) => setQueryParams({ ...queryParams, machineryId: value })}
+                treeData={machineryOptions}
+                placeholder="选择设备/工艺"
+                allowClear
+                style={{ width: 192 }}
+              />
+            </div>
+            <div className="flex items-center gap-3">
+              <label className="text-sm font-medium text-gray-700 whitespace-nowrap">锁定站:</label>
+              <Select
+                value={queryParams.lotoId}
+                onChange={(value) => setQueryParams({ ...queryParams, lotoId: value })}
+                placeholder="请选择锁定站"
+                allowClear
+                style={{ width: 192 }}
+                options={lotoOptions}
+              />
+            </div>
+            <div className="flex items-center gap-3">
+              <label className="text-sm font-medium text-gray-700 whitespace-nowrap">能量源:</label>
+              <Select
+                value={queryParams.powerType}
+                onChange={(value) => setQueryParams({ ...queryParams, powerType: value })}
+                placeholder="请选择能量源"
+                allowClear
+                style={{ width: 192 }}
+                options={powerTypeOptions}
+              />
+            </div>
+          </div>
+
+          {/* 操作按钮组 */}
+          <Space>
+            <Button
+              type="primary"
+              icon={<Search />}
+              onClick={handleQuery}
+            >
+              搜索
+            </Button>
+            <Button
+              icon={<RefreshCw />}
+              onClick={resetQuery}
+            >
+              重置
+            </Button>
+            <Button
+              type="primary"
+              icon={<Plus />}
+              onClick={() => openForm('create')}
+            >
+              新增
+            </Button>
+            <Button
+              danger
+              icon={<DeleteOutlined />}
+              disabled={selectedRowKeys.length === 0}
+              onClick={() => handleDelete()}
+            >
+              批量删除
+            </Button>
+          </Space>
+        </div>
+      </div>
+
+      {/* 表格 */}
+      <div className="bg-white rounded-lg border border-gray-200">
+        <Table
+          columns={columns}
+          dataSource={list}
+          rowKey="pointId"
+          loading={loading}
+          pagination={false}
+          scroll={{ x: 'max-content' }}
+          rowSelection={{
+            selectedRowKeys,
+            onChange: (keys) => setSelectedRowKeys(keys),
+          }}
+        />
+
+        {/* 分页 */}
+        {!loading && list.length > 0 && (
+          <div className="bg-white rounded-lg border border-gray-200 px-6 py-4">
+            <div className="flex items-center justify-between">
+              <div className="text-sm text-gray-600">
+                共 <span className="text-blue-600 font-medium">{total}</span> 条记录
+              </div>
+              <div className="flex gap-2">
+                <Button
+                  onClick={() => setQueryParams({ ...queryParams, current: (queryParams.current || 1) - 1 })}
+                  disabled={(queryParams.current || 1) <= 1}
+                >
+                  上一页
+                </Button>
+                <span className="px-4 py-2 text-sm text-gray-600 flex items-center">
+                  {queryParams.current || 1} / {Math.ceil(total / (queryParams.size || 10)) || 1}
+                </span>
+                <Button
+                  onClick={() => setQueryParams({ ...queryParams, current: (queryParams.current || 1) + 1 })}
+                  disabled={(queryParams.current || 1) >= Math.ceil(total / (queryParams.size || 10))}
+                >
+                  下一页
+                </Button>
+              </div>
+            </div>
+          </div>
+        )}
+      </div>
+
+      {/* 表单弹窗 */}
+      <SegregationPointForm ref={formRef} onSuccess={getList} />
+    </div>
+  );
+}
+

+ 14 - 2
src/components/SystemConfig.tsx

@@ -5,6 +5,7 @@ import MenuManagement from './MenuManagement';
 import PostManagement from './PostManagement';
 import RoleManagement from './RoleManagement';
 import SystemLockCabinetManagement from './lockCabinet/SystemLockCabinetManagement';
+import DictTypeManagement from './DictTypeManagement';
 
 interface TableRow {
   id: number;
@@ -437,8 +438,19 @@ export default function SystemConfig({ subMenu }: SystemConfigProps) {
     console.log('SystemConfig: ❌ 不匹配,继续执行其他逻辑');
   }
 
-  // 字典管理使用卡片展示
-  if (subMenu === '字典管理') {
+  // 字典管理使用专门的组件
+  // 支持多种可能的 subMenu 值:'字典管理'、'dictManagement'、'dict'
+  const isDictManagement = subMenu === '字典管理' || 
+                           subMenu === 'dictManagement' || 
+                           subMenu === 'dict';
+  console.log('SystemConfig: 检查字典管理条件,subMenu =', subMenu, '是否匹配:', isDictManagement);
+  if (isDictManagement) {
+    console.log('SystemConfig: ✅ 匹配成功,渲染字典管理组件');
+    return <DictTypeManagement />;
+  }
+
+  // 旧的字典管理代码(已废弃,保留作为备份)
+  if (false && subMenu === '字典管理') {
     return (
       <>
         <div className="space-y-6">

+ 383 - 313
src/components/UserManagement.tsx

@@ -1,22 +1,18 @@
 import React, { useState, useEffect, useRef } from 'react';
-import { Plus, Search, Edit2, Trash2, MoreVertical, Upload, Download, Key, UserCog, Eye, RefreshCw } from 'lucide-react';
+import { Plus, Search, Upload, Download, RefreshCw } from 'lucide-react';
 import { userApi } from '../api/user';
 import { userImportApi } from '../api/user/import';
+import { postApi, PostVO } from '../api/Post';
 import { UserVO, WorkstationNode } from '../types';
 import { toast } from 'sonner';
+import { Modal, Table, Input, Button, Switch, Dropdown, Space, Tooltip, Form } from 'antd';
+import { ExclamationCircleOutlined, EditOutlined, DeleteOutlined, KeyOutlined, UserSwitchOutlined, MoreOutlined } from '@ant-design/icons';
+import type { ColumnsType } from 'antd/es/table';
 import DeptTree from './user/DeptTree';
 import UserForm, { UserFormRef } from './user/UserForm';
 import UserImportForm, { UserImportFormRef } from './user/UserImportForm';
 import UserAssignRoleForm, { UserAssignRoleFormRef } from './user/UserAssignRoleForm';
 import FaceOrFingerForm, { FaceOrFingerFormRef } from './user/FaceOrFingerForm';
-import { Switch } from './ui/switch';
-import { Button } from './ui/button';
-import {
-  DropdownMenu,
-  DropdownMenuContent,
-  DropdownMenuItem,
-  DropdownMenuTrigger,
-} from './ui/dropdown-menu';
 
 interface UserManagementProps {
   subMenu: string;
@@ -26,6 +22,7 @@ export default function UserManagement({ subMenu }: UserManagementProps) {
   const [loading, setLoading] = useState(true);
   const [list, setList] = useState<UserVO[]>([]);
   const [total, setTotal] = useState(0);
+  const [postList, setPostList] = useState<PostVO[]>([]);
   const [queryParams, setQueryParams] = useState({
     pageNo: 1,
     pageSize: 10,
@@ -36,6 +33,10 @@ export default function UserManagement({ subMenu }: UserManagementProps) {
     createTime: [] as string[],
   });
   const [exportLoading, setExportLoading] = useState(false);
+  const [resetPwdModalVisible, setResetPwdModalVisible] = useState(false);
+  const [resetPwdUser, setResetPwdUser] = useState<UserVO | null>(null);
+  const [resetPwdPassword, setResetPwdPassword] = useState('');
+  const [resetPwdLoading, setResetPwdLoading] = useState(false);
 
   // 子组件引用
   const formRef = useRef<UserFormRef>(null);
@@ -58,6 +59,22 @@ export default function UserManagement({ subMenu }: UserManagementProps) {
     }
   };
 
+  // 加载岗位列表
+  useEffect(() => {
+    const loadPostList = async () => {
+      try {
+        const response = await postApi.getSimplePostList();
+        // 处理响应数据,可能是直接返回数组,也可能包装在 data 中
+        const posts = (response as any)?.data || response;
+        setPostList(Array.isArray(posts) ? posts : []);
+      } catch (error) {
+        console.error('加载岗位列表失败:', error);
+        setPostList([]);
+      }
+    };
+    loadPostList();
+  }, []);
+
   useEffect(() => {
     getList();
     // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -107,80 +124,121 @@ export default function UserManagement({ subMenu }: UserManagementProps) {
 
   // 导出用户
   const handleExport = async () => {
-    if (!confirm('确定要导出用户数据吗?')) {
-      return;
-    }
-
-    setExportLoading(true);
-    try {
-      const blob = await userImportApi.exportUser(queryParams);
-      const url = window.URL.createObjectURL(blob);
-      const link = document.createElement('a');
-      link.href = url;
-      link.download = '用户数据.xls';
-      document.body.appendChild(link);
-      link.click();
-      document.body.removeChild(link);
-      window.URL.revokeObjectURL(url);
-      toast.success('导出成功');
-    } catch (error: any) {
-      toast.error(error.message || '导出失败');
-    } finally {
-      setExportLoading(false);
-    }
+    Modal.confirm({
+      title: '确认导出',
+      icon: <ExclamationCircleOutlined />,
+      content: '确定要导出用户数据吗?',
+      okText: '确定导出',
+      cancelText: '取消',
+      onOk: async () => {
+        setExportLoading(true);
+        try {
+          const blob = await userImportApi.exportUser(queryParams);
+          const url = window.URL.createObjectURL(blob);
+          const link = document.createElement('a');
+          link.href = url;
+          link.download = '用户数据.xls';
+          document.body.appendChild(link);
+          link.click();
+          document.body.removeChild(link);
+          window.URL.revokeObjectURL(url);
+          toast.success('导出成功');
+        } catch (error: any) {
+          toast.error(error.message || '导出失败');
+        } finally {
+          setExportLoading(false);
+        }
+      },
+    });
   };
 
   // 修改用户状态
   const handleStatusChange = async (row: UserVO, newChecked: boolean) => {
-    const newStatus = newChecked ? 0 : 1;
+    const newStatus = newChecked ? 0 : 1; // 0是开启,1是关闭
     const text = newStatus === 0 ? '启用' : '停用';
     
-    // 使用更友好的确认框
-    const confirmed = window.confirm(`确认要${text}"${row.username}"用户吗?`);
-    if (!confirmed) {
-      // 用户取消,不更新状态,需要刷新列表以恢复Switch状态
-      await getList();
-      return;
-    }
-
-    try {
-      await userApi.updateUserStatus(row.id, newStatus);
-      toast.success('状态更新成功');
-      await getList();
-    } catch (error: any) {
-      toast.error(error.message || '状态更新失败');
-      // 刷新列表以恢复原状态
-      await getList();
-    }
+    // 使用 antd 的确认弹框
+    Modal.confirm({
+      title: '确认操作',
+      icon: <ExclamationCircleOutlined />,
+      content: `确定要${text}用户"${row.username}"吗?`,
+      okText: '确定',
+      cancelText: '取消',
+      onOk: async () => {
+        try {
+          await userApi.updateUserStatus(row.id, newStatus);
+          toast.success(`用户${text}成功`);
+          // 刷新列表以更新状态
+          await getList();
+        } catch (error: any) {
+          toast.error(error.message || `用户${text}失败`);
+          // 接口调用失败,刷新列表以恢复Switch状态
+          await getList();
+        }
+      },
+      onCancel: async () => {
+        // 用户取消操作,立即刷新列表以恢复Switch状态
+        // 由于Switch是受控组件,刷新列表后会自动恢复到原始状态(基于row.status)
+        await getList();
+      },
+    });
   };
 
   // 删除用户
-  const handleDelete = async (id: number) => {
-    if (!confirm('确定要删除这条数据吗?')) {
-      return;
-    }
-
-    try {
-      await userApi.deleteUser(id);
-      toast.success('删除成功');
-      await getList();
-    } catch (error: any) {
-      toast.error(error.message || '删除失败');
-    }
+  const handleDelete = async (id: number, username?: string) => {
+    Modal.confirm({
+      title: '确认删除',
+      icon: <ExclamationCircleOutlined />,
+      content: (
+        <div>
+          <p>确定要删除用户 <strong>"{username || '该用户'}"</strong> 吗?</p>
+          <p style={{ color: '#ff4d4f', marginTop: '8px' }}>删除后无法恢复,请谨慎操作!</p>
+        </div>
+      ),
+      okText: '确定删除',
+      okType: 'danger',
+      cancelText: '取消',
+      onOk: async () => {
+        try {
+          await userApi.deleteUser(id);
+          toast.success('删除成功');
+          await getList();
+        } catch (error: any) {
+          toast.error(error.message || '删除失败');
+        }
+      },
+    });
   };
 
   // 重置密码
-  const handleResetPwd = async (row: UserVO) => {
-    const password = prompt(`请输入"${row.username}"的新密码`, '');
-    if (!password) {
+  const handleResetPwd = (row: UserVO) => {
+    setResetPwdUser(row);
+    setResetPwdPassword('');
+    setResetPwdModalVisible(true);
+  };
+
+  // 提交重置密码
+  const submitResetPassword = async () => {
+    if (!resetPwdPassword.trim()) {
+      toast.error('请输入新密码');
       return;
     }
 
+    if (!resetPwdUser) {
+      return;
+    }
+
+    setResetPwdLoading(true);
     try {
-      await userApi.resetUserPassword(row.id, password);
-      toast.success(`修改成功,新密码是:${password}`);
+      await userApi.resetUserPassword(resetPwdUser.id, resetPwdPassword);
+      toast.success(`修改成功,新密码是:${resetPwdPassword}`);
+      setResetPwdModalVisible(false);
+      setResetPwdPassword('');
+      setResetPwdUser(null);
     } catch (error: any) {
       toast.error(error.message || '重置密码失败');
+    } finally {
+      setResetPwdLoading(false);
     }
   };
 
@@ -194,18 +252,158 @@ export default function UserManagement({ subMenu }: UserManagementProps) {
     faceOrFingerFormRef.current?.open(type, row.id, row);
   };
 
-  // 查看岗位
-  const handleLookWorkStation = (row: UserVO) => {
-    // 可以跳转到岗位管理页面,这里暂时用提示
-    toast.info('查看岗位功能,可跳转到岗位管理页面');
-  };
 
-  // 格式化日期
-  const formatDate = (dateStr?: string | Date) => {
-    if (!dateStr) return '';
-    const date = typeof dateStr === 'string' ? new Date(dateStr) : dateStr;
-    return date.toLocaleString('zh-CN');
-  };
+  // 表格列配置
+  const columns: ColumnsType<UserVO> = [
+    {
+      title: '序号',
+      width: '5%',
+      render: (_: any, __: UserVO, index: number) => {
+        return (queryParams.pageNo - 1) * queryParams.pageSize + index + 1;
+      },
+    },
+    {
+      title: '用户编号',
+      dataIndex: 'id',
+      width: '8%',
+    },
+    {
+      title: '账号',
+      dataIndex: 'username',
+      width: '10%',
+    },
+    {
+      title: '用户昵称',
+      dataIndex: 'nickname',
+      width: '10%',
+    },
+    {
+      title: '手机号码',
+      dataIndex: 'mobile',
+      width: '12%',
+      render: (text: string) => text || '-',
+    },
+    {
+      title: '部门',
+      width: '10%',
+      render: (_: any, record: UserVO) => {
+        return (record as any).deptName || '-';
+      },
+    },
+    {
+      title: '岗位',
+      width: '10%',
+      render: (_: any, record: UserVO) => {
+        // 根据 postIds 匹配岗位名称
+        if (record.postIds && Array.isArray(record.postIds) && record.postIds.length > 0) {
+          const postNames = record.postIds
+            .map((postId: string | number) => {
+              // 将 postId 转换为数字进行比较
+              const id = typeof postId === 'string' ? Number(postId) : postId;
+              const post = postList.find(p => p.id === id);
+              return post ? post.name : null;
+            })
+            .filter((name: string | null) => name !== null);
+          
+          const displayText = postNames.length > 0 ? postNames.join(',') : '-';
+          
+          return (
+            <Tooltip 
+              title={
+                <div style={{ maxHeight: '200px', overflowY: 'auto', maxWidth: '300px' }}>
+                  {displayText}
+                </div>
+              } 
+              placement="topLeft"
+            >
+              <div
+                style={{
+                  overflow: 'hidden',
+                  textOverflow: 'ellipsis',
+                  whiteSpace: 'nowrap',
+                  cursor: 'help',
+                }}
+              >
+                {displayText}
+              </div>
+            </Tooltip>
+          );
+        }
+        return '-';
+      },
+    },
+    {
+      title: '人脸',
+      width: '8%',
+      render: (_: any, record: UserVO) => (
+        <Button
+          type="link"
+          onClick={() => openFaceOrFingerForm('face', record)}
+          style={{ padding: 0 }}
+        >
+          查看
+        </Button>
+      ),
+    },
+    {
+      title: '状态',
+      width: '8%',
+      render: (_: any, record: UserVO) => (
+        <Switch
+          checked={record.status === 0}
+          onChange={(checked) => handleStatusChange(record, checked)}
+        />
+      ),
+    },
+    {
+      title: '操作',
+      width: '10%',
+      align: 'center',
+      render: (_: any, record: UserVO) => {
+        const menuItems = [
+          {
+            key: 'delete',
+            label: '删除',
+            icon: <DeleteOutlined />,
+            onClick: () => handleDelete(record.id, record.username),
+          },
+          {
+            key: 'resetPwd',
+            label: '重置密码',
+            icon: <KeyOutlined />,
+            onClick: () => handleResetPwd(record),
+          },
+          {
+            key: 'assignRole',
+            label: '分配角色',
+            icon: <UserSwitchOutlined />,
+            onClick: () => handleRole(record),
+          },
+        ];
+
+        return (
+          <Space>
+            <Button
+              type="link"
+              icon={<EditOutlined />}
+              onClick={() => openForm('update', record.id)}
+              title="编辑"
+            />
+            <Dropdown
+              menu={{ items: menuItems }}
+              trigger={['click']}
+            >
+              <Button
+                type="link"
+                icon={<MoreOutlined />}
+                title="更多"
+              />
+            </Dropdown>
+          </Space>
+        );
+      },
+    },
+  ];
 
   return (
     <div className="flex gap-6 h-full">
@@ -220,253 +418,98 @@ export default function UserManagement({ subMenu }: UserManagementProps) {
       <div className="flex-1 min-w-0">
         <div className="space-y-6">
           {/* 搜索栏 */}
-          <div className="bg-white rounded-2xl border border-gray-200/50 shadow-sm p-4">
-            {/* 一行:检索条件和按钮 */}
-            <div className="flex items-center gap-4">
-              <div className="flex items-center gap-2">
-                <label className="text-sm text-gray-700 whitespace-nowrap">工号:</label>
-                <input
-                  type="text"
-                  value={queryParams.username}
-                  onChange={(e) => setQueryParams({ ...queryParams, username: e.target.value })}
-                  onKeyUp={(e) => e.key === 'Enter' && handleQuery()}
-                  placeholder="请输入工号"
-                  className="w-48 h-9 px-3 bg-gray-50 border border-gray-200 rounded-lg outline-none focus:border-blue-400 focus:ring-2 focus:ring-blue-100 transition-all text-sm"
-                />
-              </div>
-              <div className="flex items-center gap-2">
-                <label className="text-sm text-gray-700 whitespace-nowrap">手机号码:</label>
-                <input
-                  type="text"
-                  value={queryParams.mobile}
-                  onChange={(e) => setQueryParams({ ...queryParams, mobile: e.target.value })}
-                  onKeyUp={(e) => e.key === 'Enter' && handleQuery()}
-                  placeholder="请输入手机号码"
-                  className="w-48 h-9 px-3 bg-gray-50 border border-gray-200 rounded-lg outline-none focus:border-blue-400 focus:ring-2 focus:ring-blue-100 transition-all text-sm"
-                />
+          <div className="bg-white rounded-xl border border-gray-200/50 shadow-sm p-5">
+            <div className="flex items-center justify-between gap-4 flex-wrap">
+              {/* 搜索输入框 */}
+              <div className="flex items-center gap-3 flex-wrap flex-1">
+                <div className="flex items-center gap-3">
+                  <label className="text-sm font-medium text-gray-700 whitespace-nowrap">账号:</label>
+                  <Input
+                    value={queryParams.username}
+                    onChange={(e) => setQueryParams({ ...queryParams, username: e.target.value })}
+                    onPressEnter={handleQuery}
+                    placeholder="请输入账号"
+                    style={{ width: 192 }}
+                    allowClear
+                  />
+                </div>
+                <div className="flex items-center gap-3">
+                  <label className="text-sm font-medium text-gray-700 whitespace-nowrap">手机号码:</label>
+                  <Input
+                    value={queryParams.mobile}
+                    onChange={(e) => setQueryParams({ ...queryParams, mobile: e.target.value })}
+                    onPressEnter={handleQuery}
+                    placeholder="请输入手机号码"
+                    style={{ width: 192 }}
+                    allowClear
+                  />
+                </div>
               </div>
-              <Button 
-                onClick={handleQuery}
-                className="bg-blue-500 hover:bg-blue-600 text-white border-0"
-              >
-                <Search className="w-4 h-4 mr-2" />
-                搜索
-              </Button>
-              <Button 
-                variant="outline" 
-                onClick={resetQuery}
-                className="bg-gray-100 hover:bg-gray-200 text-gray-700 border-gray-300"
-              >
-                <RefreshCw className="w-4 h-4 mr-2" />
-                重置
-              </Button>
-              <Button
-                onClick={() => openForm('create')}
-                className="bg-green-500 hover:bg-green-600 text-white border-0"
-              >
-                <Plus className="w-4 h-4 mr-2" />
-                新增
-              </Button>
-              <Button
-                onClick={handleImport}
-                className="bg-orange-500 hover:bg-orange-600 text-white border-0"
-              >
-                <Upload className="w-4 h-4 mr-2" />
-                导入
-              </Button>
-              <Button
+
+              {/* 操作按钮组 */}
+              <Space>
+                <Button
+                  type="primary"
+                  icon={<Search className="w-4 h-4" />}
+                  onClick={handleQuery}
+                >
+                  搜索
+                </Button>
+                
+                <Button
+                  icon={<RefreshCw className="w-4 h-4" />}
+                  onClick={resetQuery}
+                >
+                  重置
+                </Button>
+                
+                <Button
+                  type="primary"
+                  icon={<Plus className="w-4 h-4" />}
+                  onClick={() => openForm('create')}
+                >
+                  新增
+                </Button>
+                
+                <Button
+                  icon={<Upload className="w-4 h-4" />}
+                  onClick={handleImport}
+                >
+                  导入
+                </Button>
+                
+                <Button
+                  icon={<Download className="w-4 h-4" />}
                   onClick={handleExport}
-                  className="bg-green-500 hover:bg-indigo-700 text-white border-1 disabled:bg-gray-400 disabled:hover:bg-gray-400"
-              >
-                <Download className="w-4 h-4 mr-2" />
-                导出
-              </Button>
-              {/*<Button*/}
-              {/*  onClick={handleExport}*/}
-              {/*  disabled={exportLoading}*/}
-              {/*  className="bg-indigo-600 hover:bg-indigo-700 text-white border-0 disabled:bg-gray-400 disabled:hover:bg-gray-400"*/}
-              {/*>*/}
-              {/*  <Download className="w-4 h-4 mr-2" />*/}
-              {/*  {exportLoading ? '导出中...' : '导出'}*/}
-              {/*</Button>*/}
+                  loading={exportLoading}
+                >
+                  导出
+                </Button>
+              </Space>
             </div>
           </div>
 
           {/* 表格容器 */}
           <div className="bg-white rounded-2xl border border-gray-200/50 shadow-sm overflow-hidden">
-            {/* 工具栏 - 显示已筛选岗位 */}
-            {queryParams.workstationId && (
-            <div className="p-4 border-b border-gray-200/50">
-                <div className="flex items-center gap-3">
-                    <div className="flex items-center gap-2 px-3 py-2 bg-blue-50 text-blue-700 rounded-lg text-sm">
-                    <span>已筛选岗位</span>
-                      <button
-                      onClick={() => setQueryParams({ ...queryParams, workstationId: undefined, pageNo: 1 })}
-                        className="ml-1 hover:bg-blue-100 rounded p-0.5"
-                      >
-                        <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
-                          <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
-                        </svg>
-                      </button>
-                    </div>
-                </div>
-              </div>
-            )}
-
-            {/* 表格 */}
-            <div className="overflow-x-auto">
-              <table className="w-full">
-                <thead>
-                  <tr className="bg-gradient-to-r from-gray-50 to-gray-100/50 border-b border-gray-200">
-                    <th className="px-6 py-4 text-left text-xs text-gray-600 uppercase tracking-wider" style={{ width: '5%' }}>
-                      序号
-                    </th>
-                    <th className="px-6 py-4 text-left text-xs text-gray-600 uppercase tracking-wider" style={{ width: '8%' }}>
-                      用户编号
-                    </th>
-                    <th className="px-6 py-4 text-left text-xs text-gray-600 uppercase tracking-wider" style={{ width: '10%' }}>
-                      工号
-                    </th>
-                    <th className="px-6 py-4 text-left text-xs text-gray-600 uppercase tracking-wider" style={{ width: '10%' }}>
-                      用户昵称
-                    </th>
-                    <th className="px-6 py-4 text-left text-xs text-gray-600 uppercase tracking-wider" style={{ width: '12%' }}>
-                      手机号码
-                    </th>
-                    <th className="px-6 py-4 text-left text-xs text-gray-600 uppercase tracking-wider" style={{ width: '10%' }}>
-                      岗位
-                    </th>
-                    <th className="px-6 py-4 text-left text-xs text-gray-600 uppercase tracking-wider" style={{ width: '8%' }}>
-                      指纹
-                    </th>
-                    <th className="px-6 py-4 text-left text-xs text-gray-600 uppercase tracking-wider" style={{ width: '8%' }}>
-                      人脸
-                    </th>
-                    <th className="px-6 py-4 text-left text-xs text-gray-600 uppercase tracking-wider" style={{ width: '8%' }}>
-                      状态
-                    </th>
-                    <th className="px-6 py-4 text-left text-xs text-gray-600 uppercase tracking-wider" style={{ width: '12%' }}>
-                      创建时间
-                      </th>
-                    <th className="px-6 py-4 text-center text-xs text-gray-600 uppercase tracking-wider" style={{ width: '10%' }}>
-                      操作
-                    </th>
-                  </tr>
-                </thead>
-                <tbody className="divide-y divide-gray-100">
-                  {loading ? (
-                    <tr>
-                      <td colSpan={11} className="px-6 py-8 text-center">
-                        <div className="flex items-center justify-center">
-                          <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
-                        </div>
-                      </td>
-                    </tr>
-                  ) : list.length === 0 ? (
-                    <tr>
-                      <td colSpan={11} className="px-6 py-8 text-center text-gray-500">
-                        暂无数据
-                      </td>
-                    </tr>
-                  ) : (
-                    list.map((row, index) => (
-                    <tr
-                      key={row.id}
-                      className="hover:bg-blue-50/30 transition-colors"
-                    >
-                      <td className="px-6 py-4 text-sm text-gray-900">
-                          {(queryParams.pageNo - 1) * queryParams.pageSize + index + 1}
-                        </td>
-                        <td className="px-6 py-4 text-sm text-gray-900">{row.id}</td>
-                        <td className="px-6 py-4 text-sm text-gray-900">{row.username}</td>
-                        <td className="px-6 py-4 text-sm text-gray-900">{row.nickname}</td>
-                        <td className="px-6 py-4 text-sm text-gray-900">{row.mobile || '-'}</td>
-                        <td className="px-6 py-4 text-sm text-gray-900">
-                          <button
-                            onClick={() => handleLookWorkStation(row)}
-                            className="text-blue-600 hover:text-blue-800 hover:underline"
-                          >
-                            {row.workstationName || '查看'}
-                          </button>
-                        </td>
-                        <td className="px-6 py-4 text-sm text-gray-900">
-                          <button
-                            onClick={() => openFaceOrFingerForm('finger', row)}
-                            className="text-blue-600 hover:text-blue-800 hover:underline"
-                          >
-                            查看
-                          </button>
-                        </td>
-                        <td className="px-6 py-4 text-sm text-gray-900">
-                          <button
-                            onClick={() => openFaceOrFingerForm('face', row)}
-                            className="text-blue-600 hover:text-blue-800 hover:underline"
-                          >
-                            查看
-                          </button>
-                        </td>
-                        <td className="px-6 py-4 text-sm">
-                          <Switch
-                            checked={row.status === 0}
-                            onCheckedChange={(checked) => handleStatusChange(row, checked)}
-                            className="data-[state=checked]:!bg-blue-500 data-[state=unchecked]:!bg-gray-400"
-                          />
-                        </td>
-                        <td className="px-6 py-4 text-sm text-gray-900">
-                          {formatDate(row.createTime)}
-                        </td>
-                        <td className="px-6 py-4">
-                          <div className="flex items-center justify-center gap-2">
-                            <button
-                              onClick={() => openForm('update', row.id)}
-                              className="p-2 text-blue-600 hover:bg-blue-100 rounded-lg transition-colors"
-                              title="编辑"
-                            >
-                              <Edit2 className="w-4 h-4" />
-                            </button>
-                            <DropdownMenu>
-                              <DropdownMenuTrigger asChild>
-                                <button
-                                  className="p-2 text-gray-600 hover:bg-gray-100 rounded-lg transition-colors relative z-10"
-                                  title="更多"
-                                >
-                                  <MoreVertical className="w-4 h-4" />
-                                </button>
-                              </DropdownMenuTrigger>
-                              <DropdownMenuContent align="end" className="!z-[9999]">
-                                <DropdownMenuItem onClick={() => handleDelete(row.id)}>
-                                  <Trash2 className="w-4 h-4 mr-2" />
-                                  删除
-                                </DropdownMenuItem>
-                                <DropdownMenuItem onClick={() => handleResetPwd(row)}>
-                                  <Key className="w-4 h-4 mr-2" />
-                                  重置密码
-                                </DropdownMenuItem>
-                                <DropdownMenuItem onClick={() => handleRole(row)}>
-                                  <UserCog className="w-4 h-4 mr-2" />
-                                  分配角色
-                                </DropdownMenuItem>
-                              </DropdownMenuContent>
-                            </DropdownMenu>
-                        </div>
-                      </td>
-                    </tr>
-                    ))
-                  )}
-                </tbody>
-              </table>
-            </div>
+            <Table
+              columns={columns}
+              dataSource={list}
+              rowKey="id"
+              loading={loading}
+              pagination={false}
+              scroll={{ x: 'max-content' }}
+            />
+          </div>
 
-            {/* 分页 */}
-            <div className="px-6 py-4 bg-gray-50/50 border-t border-gray-200">
+          {/* 分页 */}
+          {!loading && list.length > 0 && (
+            <div className="bg-white rounded-lg border border-gray-200 px-6 py-4">
               <div className="flex items-center justify-between">
                 <div className="text-sm text-gray-600">
-                  共 <span className="text-blue-600">{total}</span> 条记录
+                  共 <span className="text-blue-600 font-medium">{total}</span> 条记录
                 </div>
                 <div className="flex gap-2">
                   <Button
-                    variant="outline"
-                    size="sm"
                     onClick={() => setQueryParams({ ...queryParams, pageNo: queryParams.pageNo - 1 })}
                     disabled={queryParams.pageNo <= 1}
                   >
@@ -476,8 +519,6 @@ export default function UserManagement({ subMenu }: UserManagementProps) {
                     {queryParams.pageNo} / {Math.ceil(total / queryParams.pageSize) || 1}
                   </span>
                   <Button
-                    variant="outline"
-                    size="sm"
                     onClick={() => setQueryParams({ ...queryParams, pageNo: queryParams.pageNo + 1 })}
                     disabled={queryParams.pageNo >= Math.ceil(total / queryParams.pageSize)}
                   >
@@ -486,7 +527,7 @@ export default function UserManagement({ subMenu }: UserManagementProps) {
                 </div>
               </div>
             </div>
-          </div>
+          )}
         </div>
       </div>
 
@@ -495,6 +536,35 @@ export default function UserManagement({ subMenu }: UserManagementProps) {
       <UserImportForm ref={importFormRef} onSuccess={getList} />
       <UserAssignRoleForm ref={assignRoleFormRef} onSuccess={getList} />
       <FaceOrFingerForm ref={faceOrFingerFormRef} onSuccess={getList} />
+
+      {/* 重置密码弹框 */}
+      <Modal
+        title="重置密码"
+        open={resetPwdModalVisible}
+        onOk={submitResetPassword}
+        onCancel={() => {
+          setResetPwdModalVisible(false);
+          setResetPwdPassword('');
+          setResetPwdUser(null);
+        }}
+        confirmLoading={resetPwdLoading}
+        okText="确定"
+        cancelText="取消"
+        width={500}
+      >
+        <div style={{ marginTop: 16 }}>
+          <p style={{ marginBottom: 12 }}>
+            请输入用户 <strong>"{resetPwdUser?.username}"</strong> 的新密码:
+          </p>
+          <Input.Password
+            placeholder="请输入新密码"
+            value={resetPwdPassword}
+            onChange={(e) => setResetPwdPassword(e.target.value)}
+            onPressEnter={submitResetPassword}
+            autoFocus
+          />
+        </div>
+      </Modal>
     </div>
   );
 }

+ 69 - 56
src/components/user/DeptTree.tsx

@@ -1,7 +1,9 @@
-import React, { useState, useEffect, useRef } from 'react';
+import React, { useState, useEffect } from 'react';
 import { Search, ChevronRight, ChevronDown, Building2 } from 'lucide-react';
-import { systemApi } from '../../api/System';
+import { deptApi } from '../../api/dept';
 import { WorkstationNode } from '../../types';
+import { handleTree } from '../../utils/tree';
+import { toast } from 'sonner';
 
 interface DeptTreeProps {
   onNodeClick: (node: WorkstationNode) => void;
@@ -12,64 +14,49 @@ export default function DeptTree({ onNodeClick }: DeptTreeProps) {
   const [deptList, setDeptList] = useState<WorkstationNode[]>([]);
   const [expandedIds, setExpandedIds] = useState<number[]>([]);
   const [selectedId, setSelectedId] = useState<number | null>(null);
+  const [loading, setLoading] = useState(false);
 
-  // 将扁平数据转换为树形结构
-  const handleTree = (list: any[], idKey = 'id', parentKey = 'parentId', childrenKey = 'children'): WorkstationNode[] => {
-    const map = new Map<number, any>();
-    const roots: WorkstationNode[] = [];
-
-    // 创建映射
-    list.forEach(item => {
-      map.set(item[idKey], { ...item, [childrenKey]: [] });
-    });
-
-    // 构建树
-    list.forEach(item => {
-      const node = map.get(item[idKey]);
-      if (item[parentKey] && map.has(item[parentKey])) {
-        const parent = map.get(item[parentKey]);
-        if (!parent[childrenKey]) {
-          parent[childrenKey] = [];
-        }
-        parent[childrenKey].push(node);
+  // 获取部门树
+  const getTree = async (searchName?: string) => {
+    setLoading(true);
+    try {
+      // 如果有搜索关键词,调用带参数的接口;否则调用精简列表接口
+      let deptList;
+      if (searchName && searchName.trim()) {
+        // 调用带搜索参数的接口
+        deptList = await deptApi.getDeptPage({ name: searchName.trim() });
       } else {
-        roots.push(node);
+        // 调用精简列表接口
+        deptList = await deptApi.getSimpleDeptList();
       }
-    });
-
-    return roots;
-  };
-
-  // 获取岗位树
-  const getTree = async () => {
-    try {
-      // TODO: 暂时注释,等待接口实现
-      // const res = await systemApi.listMarsDept({ pageNo: 1, pageSize: -1 });
-      // const treeData = handleTree(res.list || [], 'id', 'parentId', 'children');
-      // setDeptList(treeData);
-      // 默认展开第一层
-      // if (treeData.length > 0) {
-      //   setExpandedIds([treeData[0].id]);
-      // }
       
-      // 暂时使用空数据,避免报错
+      // 将部门数据转换为 WorkstationNode 格式
+      const workstationList = deptList.map(dept => ({
+        id: dept.id!,
+        workstationName: dept.name,
+        parentId: dept.parentId,
+      }));
+      
+      // 转换为树形结构
+      const treeData = handleTree(workstationList, 'id', 'parentId', 'children') as WorkstationNode[];
+      setDeptList(treeData);
+      
+      // 默认展开第一层
+      if (treeData.length > 0) {
+        setExpandedIds([treeData[0].id]);
+      }
+    } catch (error: any) {
+      console.error('获取部门树失败:', error);
+      toast.error(error.message || '获取部门树失败');
       setDeptList([]);
-    } catch (error) {
-      console.error('获取岗位树失败:', error);
+    } finally {
+      setLoading(false);
     }
   };
 
-  // 过滤节点
-  const filterNode = (name: string, data: WorkstationNode): boolean => {
-    if (!name) return true;
-    return data.workstationName.includes(name);
-  };
-
   // 递归渲染树节点
-  const renderTreeNode = (nodes: WorkstationNode[], level: number = 0, searchTerm: string = ''): React.ReactNode[] => {
-    return nodes
-      .filter(node => filterNode(searchTerm, node))
-      .map(node => {
+  const renderTreeNode = (nodes: WorkstationNode[], level: number = 0): React.ReactNode[] => {
+    return nodes.map(node => {
         const isExpanded = expandedIds.includes(node.id);
         const isSelected = selectedId === node.id;
         const hasChildren = node.children && node.children.length > 0;
@@ -116,7 +103,7 @@ export default function DeptTree({ onNodeClick }: DeptTreeProps) {
             
             {hasChildren && isExpanded && (
               <div className="mt-1">
-                {renderTreeNode(node.children!, level + 1, searchTerm)}
+                {renderTreeNode(node.children!, level + 1)}
               </div>
             )}
           </div>
@@ -124,10 +111,26 @@ export default function DeptTree({ onNodeClick }: DeptTreeProps) {
       });
   };
 
+  // 初始化加载
   useEffect(() => {
     getTree();
+    // eslint-disable-next-line react-hooks/exhaustive-deps
   }, []);
 
+  // 处理搜索输入变化(防抖)
+  useEffect(() => {
+    const timer = setTimeout(() => {
+      if (deptName.trim()) {
+        getTree(deptName);
+      } else {
+        getTree();
+      }
+    }, 300);
+
+    return () => clearTimeout(timer);
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [deptName]);
+
   return (
     <div className="h-full flex flex-col">
       <div className="px-4 py-4 border-b border-gray-200/50 flex-shrink-0">
@@ -137,16 +140,26 @@ export default function DeptTree({ onNodeClick }: DeptTreeProps) {
             type="text"
             value={deptName}
             onChange={(e) => setDeptName(e.target.value)}
-            placeholder="请输入岗位名称"
+            placeholder="请输入部门名称"
             className="w-full h-9 pl-9 pr-3 bg-gray-50 border border-gray-200 rounded-lg outline-none focus:border-blue-400 focus:ring-2 focus:ring-blue-100 transition-all text-sm"
           />
         </div>
       </div>
       
       <div className="flex-1 overflow-y-auto p-3">
-        <div className="space-y-1">
-          {renderTreeNode(deptList, 0, deptName)}
-        </div>
+        {loading ? (
+          <div className="flex items-center justify-center py-8">
+            <div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-500"></div>
+          </div>
+        ) : (
+          <div className="space-y-1">
+            {deptList.length === 0 ? (
+              <div className="text-center py-8 text-gray-400 text-sm">暂无数据</div>
+            ) : (
+              renderTreeNode(deptList, 0)
+            )}
+          </div>
+        )}
       </div>
     </div>
   );

+ 143 - 176
src/components/user/FaceOrFingerForm.tsx

@@ -1,10 +1,10 @@
 import React, { useState, useEffect, useImperativeHandle, forwardRef } from 'react';
-import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '../ui/dialog';
-import { Button } from '../ui/button';
-import { Trash2, ZoomIn } from 'lucide-react';
+import { Modal, Table, Button, Image, message, Space } from 'antd';
+import { DeleteOutlined, ZoomInOutlined } from '@ant-design/icons';
 import { userCharacteristicApi } from '../../api/user/characteristic';
 import { UserCharacteristic } from '../../types';
-import { toast } from 'sonner';
+import { formatDateTimeFull } from '../../utils/formatTime';
+import type { ColumnsType } from 'antd/es/table';
 
 interface FaceOrFingerFormProps {
   onSuccess?: () => void;
@@ -19,7 +19,7 @@ const FaceOrFingerForm = forwardRef<FaceOrFingerFormRef, FaceOrFingerFormProps>(
   const [dialogTitle, setDialogTitle] = useState('');
   const [formLoading, setFormLoading] = useState(false);
   const [tableData, setTableData] = useState<UserCharacteristic[]>([]);
-  const [selectedRows, setSelectedRows] = useState<number[]>([]);
+  const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
   const [total, setTotal] = useState(0);
   const [queryParams, setQueryParams] = useState({
     pageNo: 1,
@@ -36,7 +36,7 @@ const FaceOrFingerForm = forwardRef<FaceOrFingerFormRef, FaceOrFingerFormProps>(
     setUserType(type === 'finger' ? '1' : '2');
     setQueryParams({ pageNo: 1, pageSize: 10 });
     setTotal(0);
-    setSelectedRows([]);
+    setSelectedRowKeys([]);
     await getFaceOrFingerList();
   };
 
@@ -59,7 +59,7 @@ const FaceOrFingerForm = forwardRef<FaceOrFingerFormRef, FaceOrFingerFormProps>(
       setTableData(response.list || []);
       setTotal(response.total || 0);
     } catch (error: any) {
-      toast.error(error.message || '获取数据失败');
+      message.error(error.message || '获取数据失败');
     } finally {
       setFormLoading(false);
     }
@@ -73,192 +73,159 @@ const FaceOrFingerForm = forwardRef<FaceOrFingerFormRef, FaceOrFingerFormProps>(
 
   // 删除指纹或人脸
   const handleDelete = async (id?: number) => {
-    const idsToDelete = id ? [id] : selectedRows;
+    const idsToDelete = id ? [id] : selectedRowKeys.map(key => Number(key));
     
     if (idsToDelete.length === 0) {
-      toast.warning('请选择要删除的数据');
+      message.warning('请选择要删除的数据');
       return;
     }
 
-    if (!confirm(`确定要删除选中的 ${idsToDelete.length} 条数据吗?`)) {
-      return;
-    }
-
-    try {
-      // 逐个删除
-      for (const deleteId of idsToDelete) {
-        await userCharacteristicApi.deleteUserFaceOrFinger(deleteId);
-      }
-      toast.success('删除成功');
-      await getFaceOrFingerList();
-      setSelectedRows([]);
-      onSuccess?.();
-    } catch (error: any) {
-      toast.error(error.message || '删除失败');
-    }
+    Modal.confirm({
+      title: '确认删除',
+      content: `确定要删除选中的 ${idsToDelete.length} 条数据吗?`,
+      okText: '确定',
+      cancelText: '取消',
+      onOk: async () => {
+        try {
+          // 逐个删除
+          for (const deleteId of idsToDelete) {
+            await userCharacteristicApi.deleteUserFaceOrFinger(deleteId);
+          }
+          message.success('删除成功');
+          await getFaceOrFingerList();
+          setSelectedRowKeys([]);
+          onSuccess?.();
+        } catch (error: any) {
+          message.error(error.message || '删除失败');
+        }
+      },
+    });
   };
 
-  // 格式化日期
-  const formatDate = (dateStr?: string) => {
-    if (!dateStr) return '';
-    const date = new Date(dateStr);
-    return date.toLocaleString('zh-CN');
+  // 表格列配置
+  const columns: ColumnsType<UserCharacteristic> = [
+    {
+      title: '序号',
+      width: 80,
+      align: 'center',
+      render: (_: any, __: UserCharacteristic, index: number) => {
+        return (queryParams.pageNo - 1) * queryParams.pageSize + index + 1;
+      },
+    },
+    {
+      title: dialogTitle === '人员指纹数据' ? '指纹' : '人脸',
+      align: 'center',
+      render: (_: any, record: UserCharacteristic) => (
+        <div style={{ position: 'relative', display: 'inline-block' }}>
+          <Image
+            width={60}
+            height={60}
+            src={record.imageUrl}
+            alt={dialogTitle === '人员指纹数据' ? '指纹' : '人脸'}
+            preview={{
+              mask: <ZoomInOutlined />,
+            }}
+            style={{ borderRadius: 4, border: '1px solid #dcdfe6' }}
+          />
+        </div>
+      ),
+    },
+    {
+      title: '创建时间',
+      align: 'center',
+      width: 180,
+      render: (_: any, record: UserCharacteristic) => formatDateTimeFull(record.createTime),
+    },
+    {
+      title: '操作',
+      align: 'center',
+      width: 120,
+      render: (_: any, record: UserCharacteristic) => (
+        <Button
+          type="link"
+          danger
+          icon={<DeleteOutlined />}
+          onClick={() => handleDelete(record.id)}
+        >
+          删除
+        </Button>
+      ),
+    },
+  ];
+
+  // 行选择配置
+  const rowSelection = {
+    selectedRowKeys,
+    onChange: (selectedKeys: React.Key[]) => {
+      setSelectedRowKeys(selectedKeys);
+    },
   };
 
   return (
-    <Dialog open={dialogVisible} onOpenChange={setDialogVisible}>
-      <DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto">
-        <DialogHeader>
-          <DialogTitle>{dialogTitle}</DialogTitle>
-        </DialogHeader>
-        
-        <div className="py-4">
-          <div className="flex justify-end mb-4">
+    <Modal
+      title={dialogTitle}
+      open={dialogVisible}
+      onCancel={() => setDialogVisible(false)}
+      footer={[
+        <Button key="close" onClick={() => setDialogVisible(false)}>
+          关闭
+        </Button>,
+      ]}
+      width={800}
+      destroyOnClose
+    >
+      <div style={{ marginBottom: 16, display: 'flex', justifyContent: 'flex-end' }}>
+        <Button
+          type="primary"
+          danger
+          icon={<DeleteOutlined />}
+          onClick={() => handleDelete()}
+          disabled={selectedRowKeys.length === 0 || formLoading}
+        >
+          删除
+        </Button>
+      </div>
+
+      <Table
+        columns={columns}
+        dataSource={tableData}
+        rowKey="id"
+        loading={formLoading}
+        pagination={false}
+        rowSelection={rowSelection}
+        scroll={{ y: 450 }}
+        bordered
+      />
+
+      {/* 分页 */}
+      {total > 0 && (
+        <div style={{ marginTop: 16, paddingTop: 16, borderTop: '1px solid #f0f0f0', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
+          <div style={{ fontSize: 14, color: '#666' }}>
+            共 <span style={{ color: '#1890ff', fontWeight: 500 }}>{total}</span> 条记录
+          </div>
+          <Space>
             <Button
-              variant="destructive"
-              onClick={() => handleDelete()}
-              disabled={selectedRows.length === 0 || formLoading}
+              onClick={() => setQueryParams({ ...queryParams, pageNo: queryParams.pageNo - 1 })}
+              disabled={queryParams.pageNo <= 1}
             >
-              <Trash2 className="w-4 h-4 mr-2" />
-              删除
+              上一页
             </Button>
-          </div>
-
-          {formLoading && (
-            <div className="flex items-center justify-center py-8">
-              <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
-            </div>
-          )}
-
-          {!formLoading && (
-            <div className="overflow-x-auto">
-              <table className="w-full border-collapse">
-                <thead>
-                  <tr className="bg-gray-50 border-b">
-                    <th className="px-4 py-2 text-left text-xs font-medium text-gray-600">
-                      <input
-                        type="checkbox"
-                        checked={selectedRows.length === tableData.length && tableData.length > 0}
-                        onChange={(e) => {
-                          if (e.target.checked) {
-                            setSelectedRows(tableData.map(item => item.id));
-                          } else {
-                            setSelectedRows([]);
-                          }
-                        }}
-                      />
-                    </th>
-                    <th className="px-4 py-2 text-left text-xs font-medium text-gray-600">序号</th>
-                    <th className="px-4 py-2 text-left text-xs font-medium text-gray-600">
-                      {dialogTitle === '人员指纹数据' ? '指纹' : '人脸'}
-                    </th>
-                    <th className="px-4 py-2 text-left text-xs font-medium text-gray-600">创建时间</th>
-                    <th className="px-4 py-2 text-left text-xs font-medium text-gray-600">操作</th>
-                  </tr>
-                </thead>
-                <tbody>
-                  {tableData.map((row, index) => (
-                    <tr key={row.id} className="border-b hover:bg-gray-50">
-                      <td className="px-4 py-2">
-                        <input
-                          type="checkbox"
-                          checked={selectedRows.includes(row.id)}
-                          onChange={(e) => {
-                            if (e.target.checked) {
-                              setSelectedRows([...selectedRows, row.id]);
-                            } else {
-                              setSelectedRows(selectedRows.filter(id => id !== row.id));
-                            }
-                          }}
-                        />
-                      </td>
-                      <td className="px-4 py-2 text-sm text-gray-900">{index + 1}</td>
-                      <td className="px-4 py-2">
-                        <div className="relative inline-block group">
-                          <img
-                            src={row.imageUrl}
-                            alt={dialogTitle === '人员指纹数据' ? '指纹' : '人脸'}
-                            className="w-16 h-16 object-cover rounded border border-gray-200 cursor-pointer"
-                            onClick={() => {
-                              // 打开图片预览
-                              window.open(row.imageUrl, '_blank');
-                            }}
-                          />
-                          <div className="absolute inset-0 flex items-center justify-center bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity rounded cursor-pointer">
-                            <ZoomIn className="w-6 h-6 text-white" />
-                          </div>
-                        </div>
-                      </td>
-                      <td className="px-4 py-2 text-sm text-gray-900">{formatDate(row.createTime)}</td>
-                      <td className="px-4 py-2">
-                        <Button
-                          variant="ghost"
-                          size="sm"
-                          onClick={() => handleDelete(row.id)}
-                        >
-                          <Trash2 className="w-4 h-4 mr-1" />
-                          删除
-                        </Button>
-                      </td>
-                    </tr>
-                  ))}
-                  {tableData.length === 0 && (
-                    <tr>
-                      <td colSpan={5} className="px-4 py-8 text-center text-gray-500">
-                        暂无数据
-                      </td>
-                    </tr>
-                  )}
-                </tbody>
-              </table>
-            </div>
-          )}
-
-          {/* 分页 */}
-          {total > 0 && (
-            <div className="flex items-center justify-between mt-4 pt-4 border-t">
-              <div className="text-sm text-gray-600">
-                共 <span className="text-blue-600">{total}</span> 条记录
-              </div>
-              <div className="flex gap-2">
-                <Button
-                  variant="outline"
-                  size="sm"
-                  onClick={() => setQueryParams({ ...queryParams, pageNo: queryParams.pageNo - 1 })}
-                  disabled={queryParams.pageNo <= 1}
-                >
-                  上一页
-                </Button>
-                <span className="px-4 py-2 text-sm text-gray-600">
-                  {queryParams.pageNo} / {Math.ceil(total / queryParams.pageSize)}
-                </span>
-                <Button
-                  variant="outline"
-                  size="sm"
-                  onClick={() => setQueryParams({ ...queryParams, pageNo: queryParams.pageNo + 1 })}
-                  disabled={queryParams.pageNo >= Math.ceil(total / queryParams.pageSize)}
-                >
-                  下一页
-                </Button>
-              </div>
-            </div>
-          )}
+            <span style={{ padding: '0 16px', fontSize: 14, color: '#666' }}>
+              {queryParams.pageNo} / {Math.ceil(total / queryParams.pageSize)}
+            </span>
+            <Button
+              onClick={() => setQueryParams({ ...queryParams, pageNo: queryParams.pageNo + 1 })}
+              disabled={queryParams.pageNo >= Math.ceil(total / queryParams.pageSize)}
+            >
+              下一页
+            </Button>
+          </Space>
         </div>
-
-        <DialogFooter>
-          <Button
-            variant="outline"
-            onClick={() => setDialogVisible(false)}
-          >
-            关闭
-          </Button>
-        </DialogFooter>
-      </DialogContent>
-    </Dialog>
+      )}
+    </Modal>
   );
 });
 
 FaceOrFingerForm.displayName = 'FaceOrFingerForm';
 
 export default FaceOrFingerForm;
-

+ 67 - 109
src/components/user/UserAssignRoleForm.tsx

@@ -1,12 +1,8 @@
-import React, { useState, useEffect, useImperativeHandle, forwardRef } from 'react';
-import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '../ui/dialog';
-import { Button } from '../ui/button';
-import { Input } from '../ui/input';
-import { Label } from '../ui/label';
+import React, { useState, useImperativeHandle, forwardRef } from 'react';
+import { Modal, Form, Input, Select, message } from 'antd';
 import { userPermissionApi } from '../../api/user/permission';
 import { systemApi } from '../../api/System';
 import { UserVO, RoleVO } from '../../types';
-import { toast } from 'sonner';
 
 interface UserAssignRoleFormProps {
   onSuccess?: () => void;
@@ -19,26 +15,19 @@ export interface UserAssignRoleFormRef {
 const UserAssignRoleForm = forwardRef<UserAssignRoleFormRef, UserAssignRoleFormProps>(({ onSuccess }, ref) => {
   const [dialogVisible, setDialogVisible] = useState(false);
   const [formLoading, setFormLoading] = useState(false);
-  const [formData, setFormData] = useState<{
-    id: number;
-    nickname: string;
-    username: string;
-    roleIds: number[];
-  }>({
-    id: -1,
-    nickname: '',
-    username: '',
-    roleIds: [],
-  });
+  const [form] = Form.useForm();
   const [roleList, setRoleList] = useState<RoleVO[]>([]);
 
   // 打开弹窗
   const open = async (row: UserVO) => {
     setDialogVisible(true);
-    setFormData({
+    form.resetFields();
+
+    // 设置用户信息
+    form.setFieldsValue({
       id: row.id,
-      nickname: row.nickname,
       username: row.username,
+      nickname: row.nickname,
       roleIds: [],
     });
 
@@ -48,15 +37,19 @@ const UserAssignRoleForm = forwardRef<UserAssignRoleFormRef, UserAssignRoleFormP
       setRoleList(roles || []);
     } catch (error) {
       console.error('加载角色列表失败:', error);
+      message.error('加载角色列表失败');
     }
 
     // 获取用户角色列表
     setFormLoading(true);
     try {
       const roleIds = await userPermissionApi.getUserRoleList(row.id);
-      setFormData(prev => ({ ...prev, roleIds: roleIds || [] }));
-    } catch (error) {
+      form.setFieldsValue({
+        roleIds: roleIds || [],
+      });
+    } catch (error: any) {
       console.error('获取用户角色失败:', error);
+      message.error(error.message || '获取用户角色失败');
     } finally {
       setFormLoading(false);
     }
@@ -68,102 +61,67 @@ const UserAssignRoleForm = forwardRef<UserAssignRoleFormRef, UserAssignRoleFormP
 
   // 提交表单
   const submitForm = async () => {
-    setFormLoading(true);
     try {
-      await userPermissionApi.assignUserRole({
-        userId: formData.id,
-        roleIds: formData.roleIds,
-      });
-      toast.success('分配成功');
-      setDialogVisible(false);
-      onSuccess?.();
-    } catch (error: any) {
-      toast.error(error.message || '分配失败');
-    } finally {
-      setFormLoading(false);
+      const values = await form.validateFields();
+      
+      setFormLoading(true);
+      try {
+        await userPermissionApi.assignUserRole({
+          userId: values.id,
+          roleIds: values.roleIds,
+        });
+        message.success('分配成功');
+        setDialogVisible(false);
+        onSuccess?.();
+      } catch (error: any) {
+        message.error(error.message || '分配失败');
+      } finally {
+        setFormLoading(false);
+      }
+    } catch (error) {
+      // 表单验证失败
     }
   };
 
   return (
-    <Dialog open={dialogVisible} onOpenChange={setDialogVisible}>
-      <DialogContent className="max-w-md">
-        <DialogHeader>
-          <DialogTitle>分配角色</DialogTitle>
-        </DialogHeader>
-        
-        {formLoading && (
-          <div className="flex items-center justify-center py-8">
-            <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
-          </div>
-        )}
-
-        {!formLoading && (
-          <div className="space-y-4 py-4">
-            <div>
-              <Label htmlFor="username">用户名称</Label>
-              <Input
-                id="username"
-                value={formData.username}
-                disabled
-                className="bg-gray-50"
-              />
-            </div>
-
-            <div>
-              <Label htmlFor="nickname">用户昵称</Label>
-              <Input
-                id="nickname"
-                value={formData.nickname}
-                disabled
-                className="bg-gray-50"
-              />
-            </div>
-
-            <div>
-              <Label htmlFor="roleIds">角色</Label>
-              <select
-                id="roleIds"
-                multiple
-                value={formData.roleIds.map(String)}
-                onChange={(e) => {
-                  const selected = Array.from(e.target.selectedOptions, option => Number(option.value));
-                  setFormData({ ...formData, roleIds: selected });
-                }}
-                className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm"
-                size={5}
-              >
-                {roleList.map(role => (
-                  <option key={role.id} value={role.id}>
-                    {role.name}
-                  </option>
-                ))}
-              </select>
-              <p className="text-xs text-gray-500 mt-1">按住 Ctrl/Cmd 键可多选</p>
-            </div>
-          </div>
-        )}
-
-        <DialogFooter>
-          <Button
-            variant="outline"
-            onClick={() => setDialogVisible(false)}
-            disabled={formLoading}
-          >
-            取消
-          </Button>
-          <Button
-            onClick={submitForm}
-            disabled={formLoading}
-          >
-            {formLoading ? '提交中...' : '确定'}
-          </Button>
-        </DialogFooter>
-      </DialogContent>
-    </Dialog>
+    <Modal
+      title="分配角色"
+      open={dialogVisible}
+      onCancel={() => setDialogVisible(false)}
+      onOk={submitForm}
+      confirmLoading={formLoading}
+      destroyOnClose
+    >
+      <Form
+        form={form}
+        layout="vertical"
+        initialValues={{
+          roleIds: [],
+        }}
+      >
+        <Form.Item label="用户名称" name="username">
+          <Input disabled />
+        </Form.Item>
+
+        <Form.Item label="用户昵称" name="nickname">
+          <Input disabled />
+        </Form.Item>
+
+        <Form.Item label="角色" name="roleIds">
+          <Select
+            mode="multiple"
+            placeholder="请选择角色"
+            options={roleList.map(role => ({
+              label: role.name,
+              value: role.id,
+            }))}
+          />
+        </Form.Item>
+      </Form>
+    </Modal>
   );
 });
 
 UserAssignRoleForm.displayName = 'UserAssignRoleForm';
 
 export default UserAssignRoleForm;
-

+ 207 - 265
src/components/user/UserForm.tsx

@@ -1,13 +1,13 @@
-import React, { useState, useEffect, useImperativeHandle, forwardRef } from 'react';
-import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '../ui/dialog';
-import { Button } from '../ui/button';
-import { Input } from '../ui/input';
-import { Label } from '../ui/label';
-import { Textarea } from '../ui/textarea';
+import React, { useState, useImperativeHandle, forwardRef } from 'react';
+import { Modal, Form, Input, Select, TreeSelect, Row, Col, message } from 'antd';
 import { userApi } from '../../api/user';
-import { systemApi } from '../../api/System';
+import { deptApi } from '../../api/dept';
+import { postApi, PostVO } from '../../api/Post';
 import { UserVO, WorkstationNode } from '../../types';
-import { toast } from 'sonner';
+import { getIntDictOptions, DICT_TYPE } from '../../utils/dict';
+import { handleTree } from '../../utils/tree';
+
+const { TextArea } = Input;
 
 interface UserFormProps {
   onSuccess?: () => void;
@@ -22,62 +22,53 @@ const UserForm = forwardRef<UserFormRef, UserFormProps>(({ onSuccess }, ref) =>
   const [dialogTitle, setDialogTitle] = useState('');
   const [formLoading, setFormLoading] = useState(false);
   const [formType, setFormType] = useState<'create' | 'update'>('create');
-  const [formData, setFormData] = useState<Partial<UserVO>>({
-    nickname: '',
-    mobile: '',
-    email: '',
-    username: '',
-    password: '',
-    sex: undefined,
-    remark: '',
-    workstationIds: [],
-    status: 0, // 0-启用
-  });
+  const [form] = Form.useForm();
   const [workstationList, setWorkstationList] = useState<WorkstationNode[]>([]);
-
-  // 将扁平数据转换为树形结构
-  const handleTree = (list: any[], idKey = 'id', parentKey = 'parentId', childrenKey = 'children'): WorkstationNode[] => {
-    const map = new Map<number, any>();
-    const roots: WorkstationNode[] = [];
-
-    list.forEach(item => {
-      map.set(item[idKey], { ...item, [childrenKey]: [] });
-    });
-
-    list.forEach(item => {
-      const node = map.get(item[idKey]);
-      if (item[parentKey] && map.has(item[parentKey])) {
-        const parent = map.get(item[parentKey]);
-        if (!parent[childrenKey]) {
-          parent[childrenKey] = [];
-        }
-        parent[childrenKey].push(node);
-      } else {
-        roots.push(node);
-      }
-    });
-
-    return roots;
-  };
+  const [deptList, setDeptList] = useState<any[]>([]);
+  const [postList, setPostList] = useState<PostVO[]>([]);
+  const [sexOptions, setSexOptions] = useState<Array<{ label: string; value: number }>>([]);
 
   // 打开弹窗
   const open = async (type: string, id?: number) => {
     setDialogVisible(true);
     setDialogTitle(type === 'create' ? '新增用户' : '编辑用户');
     setFormType(type as 'create' | 'update');
-    resetForm();
+    form.resetFields();
 
-    // 加载岗位列表
+    // 加载字典数据
     try {
-      // TODO: 暂时注释,等待接口实现
-      // const data = await systemApi.listMarsDept({ pageNo: 1, pageSize: -1 });
-      // const treeData = handleTree(data.list || [], 'id', 'parentId', 'children');
-      // setWorkstationList(treeData);
-      
-      // 暂时使用空数据,避免报错
-      setWorkstationList([]);
+      const sexDict = getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX);
+      setSexOptions(sexDict.map(item => ({ label: item.label, value: item.value })));
+    } catch (error) {
+      console.error('加载性别字典失败:', error);
+      setSexOptions([
+        { label: '男', value: 1 },
+        { label: '女', value: 2 },
+      ]);
+    }
+
+    // 加载部门列表
+    try {
+      const deptData = await deptApi.getSimpleDeptList();
+      const deptTreeData = deptData.map(dept => ({
+        id: dept.id!,
+        name: dept.name,
+        parentId: dept.parentId,
+      }));
+      const treeData = handleTree(deptTreeData, 'id', 'parentId', 'children');
+      setDeptList(treeData);
+    } catch (error) {
+      console.error('加载部门列表失败:', error);
+      setDeptList([]);
+    }
+
+    // 加载岗位列表(使用真正的岗位接口)
+    try {
+      const posts = await postApi.getSimplePostList();
+      setPostList(posts || []);
     } catch (error) {
       console.error('加载岗位列表失败:', error);
+      setPostList([]);
     }
 
     // 修改时,设置数据
@@ -85,12 +76,22 @@ const UserForm = forwardRef<UserFormRef, UserFormProps>(({ onSuccess }, ref) =>
       setFormLoading(true);
       try {
         const userData = await userApi.getUser(id);
-        setFormData(userData);
-      } catch (error) {
-        toast.error('获取用户信息失败');
+        form.setFieldsValue({
+          ...userData,
+          workstationIds: userData.workstationIds || [],
+          deptId: userData.deptId,
+        });
+      } catch (error: any) {
+        message.error(error.message || '获取用户信息失败');
       } finally {
         setFormLoading(false);
       }
+    } else {
+      // 新增时设置默认值
+      form.setFieldsValue({
+        status: 0,
+        workstationIds: [],
+      });
     }
   };
 
@@ -98,235 +99,176 @@ const UserForm = forwardRef<UserFormRef, UserFormProps>(({ onSuccess }, ref) =>
     open,
   }));
 
-  // 重置表单
-  const resetForm = () => {
-    setFormData({
-      nickname: '',
-      mobile: '',
-      email: '',
-      username: '',
-      password: '',
-      sex: undefined,
-      remark: '',
-      workstationIds: [],
-      status: 0,
-    });
-  };
-
   // 提交表单
   const submitForm = async () => {
-    // 基本验证
-    if (!formData.nickname) {
-      toast.error('用户昵称不能为空');
-      return;
-    }
-    if (formType === 'create') {
-      if (!formData.username) {
-        toast.error('工号不能为空');
-        return;
-      }
-      if (!formData.password) {
-        toast.error('用户密码不能为空');
-        return;
-      }
-    }
-    if (formData.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
-      toast.error('请输入正确的邮箱地址');
-      return;
-    }
-    if (formData.mobile && !/^(?:(?:\+|00)86)?1(?:3[\d]|4[5-79]|5[0-35-9]|6[5-7]|7[0-8]|8[\d]|9[189])\d{8}$/.test(formData.mobile)) {
-      toast.error('请输入正确的手机号码');
-      return;
-    }
-
-    setFormLoading(true);
     try {
-      if (formType === 'create') {
-        await userApi.createUser(formData);
-        toast.success('创建成功');
-      } else {
-        await userApi.updateUser(formData);
-        toast.success('更新成功');
+      const values = await form.validateFields();
+      
+      setFormLoading(true);
+      try {
+        if (formType === 'create') {
+          await userApi.createUser(values as UserVO);
+          message.success('创建成功');
+        } else {
+          await userApi.updateUser(values as UserVO);
+          message.success('更新成功');
+        }
+        setDialogVisible(false);
+        onSuccess?.();
+      } catch (error: any) {
+        message.error(error.message || '操作失败');
+      } finally {
+        setFormLoading(false);
       }
-      setDialogVisible(false);
-      onSuccess?.();
-    } catch (error: any) {
-      toast.error(error.message || '操作失败');
-    } finally {
-      setFormLoading(false);
+    } catch (error) {
+      // 表单验证失败
     }
   };
 
-  // 递归渲染岗位树选择(简化版,使用多选)
-  const renderWorkstationOptions = (nodes: WorkstationNode[], level: number = 0): React.ReactNode[] => {
-    return nodes.map(node => (
-      <option key={node.id} value={node.id}>
-        {'  '.repeat(level)}{node.workstationName}
-        {node.children && node.children.length > 0 && renderWorkstationOptions(node.children, level + 1)}
-      </option>
-    ));
-  };
-
-  // 扁平化岗位列表用于选择
-  const flattenWorkstations = (nodes: WorkstationNode[]): WorkstationNode[] => {
-    const result: WorkstationNode[] = [];
-    const traverse = (items: WorkstationNode[]) => {
-      items.forEach(item => {
-        result.push(item);
-        if (item.children) {
-          traverse(item.children);
-        }
-      });
-    };
-    traverse(nodes);
-    return result;
+  // 将部门树形数据转换为 TreeSelect 需要的格式
+  const convertDeptToTreeSelectData = (nodes: any[]): any[] => {
+    return nodes.map(node => ({
+      title: node.name,
+      value: node.id,
+      key: node.id,
+      children: node.children ? convertDeptToTreeSelectData(node.children) : undefined,
+    }));
   };
 
-  const flatWorkstations = flattenWorkstations(workstationList);
-
   return (
-    <Dialog open={dialogVisible} onOpenChange={setDialogVisible}>
-      <DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
-        <DialogHeader>
-          <DialogTitle>{dialogTitle}</DialogTitle>
-        </DialogHeader>
-        
-        {formLoading && (
-          <div className="flex items-center justify-center py-8">
-            <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
-          </div>
+    <Modal
+      title={dialogTitle}
+      open={dialogVisible}
+      onCancel={() => setDialogVisible(false)}
+      onOk={submitForm}
+      confirmLoading={formLoading}
+      width={800}
+      destroyOnClose
+    >
+      <Form
+        form={form}
+        labelCol={{ span: 6 }}
+        wrapperCol={{ span: 18 }}
+        initialValues={{
+          status: 0,
+          workstationIds: [],
+        }}
+      >
+        {formType === 'create' && (
+          <>
+            <Row gutter={16}>
+              <Col span={12}>
+                <Form.Item
+                  label="账号"
+                  name="username"
+                  rules={[{ required: true, message: '账号不能为空' }]}
+                >
+                  <Input placeholder="请输入账号" />
+                </Form.Item>
+              </Col>
+              <Col span={12}>
+                <Form.Item
+                  label="用户密码"
+                  name="password"
+                  rules={[{ required: true, message: '用户密码不能为空' }]}
+                >
+                  <Input.Password placeholder="请输入用户密码" />
+                </Form.Item>
+              </Col>
+            </Row>
+          </>
         )}
 
-        {!formLoading && (
-          <div className="grid grid-cols-2 gap-4 py-4">
-            <div>
-              <Label htmlFor="nickname">用户昵称 *</Label>
-              <Input
-                id="nickname"
-                value={formData.nickname || ''}
-                onChange={(e) => setFormData({ ...formData, nickname: e.target.value })}
-                placeholder="请输入用户昵称"
-              />
-            </div>
-            
-            <div>
-              <Label htmlFor="workstationIds">岗位</Label>
-              <select
-                id="workstationIds"
-                multiple
-                value={formData.workstationIds?.map(String) || []}
-                onChange={(e) => {
-                  const selected = Array.from(e.target.selectedOptions, option => Number(option.value));
-                  setFormData({ ...formData, workstationIds: selected });
-                }}
-                className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm"
-                size={3}
-              >
-                {flatWorkstations.map(ws => (
-                  <option key={ws.id} value={ws.id}>
-                    {ws.workstationName}
-                  </option>
-                ))}
-              </select>
-              <p className="text-xs text-gray-500 mt-1">按住 Ctrl/Cmd 键可多选</p>
-            </div>
-
-            <div>
-              <Label htmlFor="mobile">手机号码</Label>
-              <Input
-                id="mobile"
-                value={formData.mobile || ''}
-                onChange={(e) => setFormData({ ...formData, mobile: e.target.value })}
-                placeholder="请输入手机号码"
-                maxLength={11}
+        <Row gutter={16}>
+          <Col span={12}>
+            <Form.Item
+              label="用户昵称"
+              name="nickname"
+              rules={[{ required: true, message: '用户昵称不能为空' }]}
+            >
+              <Input placeholder="请输入用户昵称" />
+            </Form.Item>
+          </Col>
+          <Col span={12}>
+            <Form.Item label="部门" name="deptId">
+              <TreeSelect
+                treeData={convertDeptToTreeSelectData(deptList)}
+                placeholder="请选择归属部门"
+                style={{ width: '100%' }}
               />
-            </div>
+            </Form.Item>
+          </Col>
+        </Row>
 
-            <div>
-              <Label htmlFor="email">邮箱</Label>
-              <Input
-                id="email"
-                type="email"
-                value={formData.email || ''}
-                onChange={(e) => setFormData({ ...formData, email: e.target.value })}
-                placeholder="请输入邮箱"
-                maxLength={50}
+        <Row gutter={16}>
+          <Col span={12}>
+            <Form.Item label="岗位" name="workstationIds">
+              <Select
+                mode="multiple"
+                placeholder="选择岗位"
+                options={postList.map(post => ({
+                  label: post.name,
+                  value: post.id,
+                }))}
               />
-            </div>
-
-            {formType === 'create' && (
-              <>
-                <div>
-                  <Label htmlFor="username">工号 *</Label>
-                  <Input
-                    id="username"
-                    value={formData.username || ''}
-                    onChange={(e) => setFormData({ ...formData, username: e.target.value })}
-                    placeholder="请输入工号"
-                  />
-                </div>
+            </Form.Item>
+          </Col>
+          <Col span={12}>
+            <Form.Item
+              label="手机号码"
+              name="mobile"
+              rules={[
+                {
+                  pattern: /^(?:(?:\+|00)86)?1(?:3[\d]|4[5-79]|5[0-35-9]|6[5-7]|7[0-8]|8[\d]|9[189])\d{8}$/,
+                  message: '请输入正确的手机号码',
+                },
+              ]}
+            >
+              <Input maxLength={11} placeholder="请输入手机号码" />
+            </Form.Item>
+          </Col>
+        </Row>
 
-                <div>
-                  <Label htmlFor="password">用户密码 *</Label>
-                  <Input
-                    id="password"
-                    type="password"
-                    value={formData.password || ''}
-                    onChange={(e) => setFormData({ ...formData, password: e.target.value })}
-                    placeholder="请输入用户密码"
-                  />
-                </div>
-              </>
-            )}
-
-            <div>
-              <Label htmlFor="sex">用户性别</Label>
-              <select
-                id="sex"
-                value={formData.sex ?? ''}
-                onChange={(e) => setFormData({ ...formData, sex: e.target.value ? Number(e.target.value) : undefined })}
-                className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm"
-              >
-                <option value="">请选择</option>
-                <option value="1">男</option>
-                <option value="2">女</option>
-              </select>
-            </div>
-
-            <div>
-              <Label htmlFor="remark">备注</Label>
-              <Textarea
-                id="remark"
-                value={formData.remark || ''}
-                onChange={(e) => setFormData({ ...formData, remark: e.target.value })}
-                placeholder="请输入内容"
-                rows={3}
-              />
-            </div>
-          </div>
-        )}
+        <Row gutter={16}>
+          <Col span={12}>
+            <Form.Item
+              label="邮箱"
+              name="email"
+              rules={[
+                {
+                  type: 'email',
+                  message: '请输入正确的邮箱地址',
+                },
+              ]}
+            >
+              <Input maxLength={50} placeholder="请输入邮箱" />
+            </Form.Item>
+          </Col>
+          <Col span={12}>
+            <Form.Item label="用户性别" name="sex">
+              <Select placeholder="请选择">
+                {sexOptions.map(option => (
+                  <Select.Option key={option.value} value={option.value}>
+                    {option.label}
+                  </Select.Option>
+                ))}
+              </Select>
+            </Form.Item>
+          </Col>
+        </Row>
 
-        <DialogFooter>
-          <Button
-            variant="outline"
-            onClick={() => setDialogVisible(false)}
-            disabled={formLoading}
-          >
-            取消
-          </Button>
-          <Button
-            onClick={submitForm}
-            disabled={formLoading}
-          >
-            {formLoading ? '提交中...' : '确定'}
-          </Button>
-        </DialogFooter>
-      </DialogContent>
-    </Dialog>
+        <Row gutter={16}>
+          <Col span={12}>
+            <Form.Item label="备注" name="remark">
+              <TextArea rows={3} placeholder="请输入内容" />
+            </Form.Item>
+          </Col>
+        </Row>
+      </Form>
+    </Modal>
   );
 });
 
 UserForm.displayName = 'UserForm';
 
 export default UserForm;
-

+ 85 - 104
src/components/user/UserImportForm.tsx

@@ -1,9 +1,11 @@
 import React, { useState, useImperativeHandle, forwardRef } from 'react';
-import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '../ui/dialog';
-import { Button } from '../ui/button';
-import { Upload, Download, FileSpreadsheet } from 'lucide-react';
+import { Modal, Upload, Button, Checkbox, message } from 'antd';
+import { InboxOutlined, DownloadOutlined } from '@ant-design/icons';
+import type { UploadFile, UploadProps } from 'antd';
 import { userImportApi } from '../../api/user/import';
-import { toast } from 'sonner';
+import { getAccessToken, getTenantId } from '../../utils/auth';
+
+const { Dragger } = Upload;
 
 interface UserImportFormProps {
   onSuccess?: () => void;
@@ -16,13 +18,13 @@ export interface UserImportFormRef {
 const UserImportForm = forwardRef<UserImportFormRef, UserImportFormProps>(({ onSuccess }, ref) => {
   const [dialogVisible, setDialogVisible] = useState(false);
   const [formLoading, setFormLoading] = useState(false);
-  const [file, setFile] = useState<File | null>(null);
+  const [fileList, setFileList] = useState<UploadFile[]>([]);
   const [updateSupport, setUpdateSupport] = useState(false);
 
   // 打开弹窗
   const open = () => {
     setDialogVisible(true);
-    setFile(null);
+    setFileList([]);
     setUpdateSupport(false);
   };
 
@@ -42,16 +44,46 @@ const UserImportForm = forwardRef<UserImportFormRef, UserImportFormProps>(({ onS
       link.click();
       document.body.removeChild(link);
       window.URL.revokeObjectURL(url);
-      toast.success('模板下载成功');
+      message.success('模板下载成功');
     } catch (error: any) {
-      toast.error(error.message || '下载模板失败');
+      message.error(error.message || '下载模板失败');
     }
   };
 
+  // 上传配置
+  const uploadProps: UploadProps = {
+    name: 'file',
+    multiple: false,
+    fileList,
+    accept: '.xlsx,.xls',
+    beforeUpload: (file) => {
+      const validTypes = ['.xlsx', '.xls'];
+      const fileExtension = file.name.substring(file.name.lastIndexOf('.')).toLowerCase();
+      if (!validTypes.includes(fileExtension)) {
+        message.error('仅允许导入 xls、xlsx 格式文件');
+        return false;
+      }
+      setFileList([file]);
+      return false; // 阻止自动上传
+    },
+    onRemove: () => {
+      setFileList([]);
+    },
+    onChange: (info) => {
+      setFileList(info.fileList);
+    },
+  };
+
   // 提交表单
   const submitForm = async () => {
+    if (fileList.length === 0) {
+      message.error('请上传文件');
+      return;
+    }
+
+    const file = fileList[0].originFileObj;
     if (!file) {
-      toast.error('请上传文件');
+      message.error('请上传文件');
       return;
     }
 
@@ -76,119 +108,68 @@ const UserImportForm = forwardRef<UserImportFormRef, UserImportFormProps>(({ onS
           .join(' ');
       }
       
-      toast.success(text, { duration: 5000 });
+      Modal.info({
+        title: '导入结果',
+        content: <div dangerouslySetInnerHTML={{ __html: text }} />,
+        width: 600,
+      });
+      
       setDialogVisible(false);
       onSuccess?.();
     } catch (error: any) {
-      toast.error(error.message || '上传失败,请您重新上传!');
+      message.error(error.message || '上传失败,请您重新上传!');
     } finally {
       setFormLoading(false);
     }
   };
 
-  // 处理文件选择
-  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
-    const selectedFile = e.target.files?.[0];
-    if (selectedFile) {
-      const validTypes = ['.xlsx', '.xls'];
-      const fileExtension = selectedFile.name.substring(selectedFile.name.lastIndexOf('.')).toLowerCase();
-      if (!validTypes.includes(fileExtension)) {
-        toast.error('仅允许导入 xls、xlsx 格式文件');
-        return;
-      }
-      setFile(selectedFile);
-    }
-  };
-
   return (
-    <Dialog open={dialogVisible} onOpenChange={setDialogVisible}>
-      <DialogContent className="max-w-md">
-        <DialogHeader>
-          <DialogTitle>用户导入</DialogTitle>
-        </DialogHeader>
-        
-        <div className="py-4 space-y-4">
-          <div
-            className={`border-2 border-dashed rounded-lg p-8 text-center transition-colors ${
-              file
-                ? 'border-blue-500 bg-blue-50'
-                : 'border-gray-300 hover:border-gray-400'
-            }`}
+    <Modal
+      title="用户导入"
+      open={dialogVisible}
+      onCancel={() => setDialogVisible(false)}
+      onOk={submitForm}
+      confirmLoading={formLoading}
+      width={400}
+      destroyOnClose
+    >
+      <div style={{ padding: '16px 0' }}>
+        <Dragger
+          {...uploadProps}
+          disabled={formLoading}
+        >
+          <p className="ant-upload-drag-icon">
+            <InboxOutlined />
+          </p>
+          <p className="ant-upload-text">将文件拖到此处,或点击上传</p>
+        </Dragger>
+
+        <div style={{ marginTop: 16, marginBottom: 16 }}>
+          <Checkbox
+            checked={updateSupport}
+            onChange={(e) => setUpdateSupport(e.target.checked)}
           >
-            <input
-              type="file"
-              id="file-upload"
-              accept=".xlsx,.xls"
-              onChange={handleFileChange}
-              className="hidden"
-              disabled={formLoading}
-            />
-            <label
-              htmlFor="file-upload"
-              className="cursor-pointer flex flex-col items-center gap-2"
-            >
-              <Upload className="w-12 h-12 text-gray-400" />
-              <div className="text-sm text-gray-600">
-                {file ? (
-                  <div className="flex items-center gap-2">
-                    <FileSpreadsheet className="w-5 h-5 text-blue-500" />
-                    <span className="text-blue-600">{file.name}</span>
-                  </div>
-                ) : (
-                  <>
-                    将文件拖到此处,或<em className="text-blue-500">点击上传</em>
-                  </>
-                )}
-              </div>
-            </label>
-          </div>
-
-          <div className="flex items-center gap-2">
-            <input
-              type="checkbox"
-              id="updateSupport"
-              checked={updateSupport}
-              onChange={(e) => setUpdateSupport(e.target.checked)}
-              className="w-4 h-4"
-            />
-            <label htmlFor="updateSupport" className="text-sm text-gray-700">
-              是否更新已经存在的用户数据
-            </label>
-          </div>
-
-          <div className="text-xs text-gray-500 text-center">
-            <span>仅允许导入 xls、xlsx 格式文件。</span>
-            <button
-              type="button"
-              onClick={importTemplate}
-              className="text-blue-500 hover:text-blue-600 ml-1 underline"
-            >
-              下载模板
-            </button>
-          </div>
+            是否更新已经存在的用户数据
+          </Checkbox>
         </div>
 
-        <DialogFooter>
+        <div style={{ fontSize: 12, color: '#999', textAlign: 'center' }}>
+          <span>仅允许导入 xls、xlsx 格式文件。</span>
           <Button
-            variant="outline"
-            onClick={() => setDialogVisible(false)}
-            disabled={formLoading}
+            type="link"
+            size="small"
+            icon={<DownloadOutlined />}
+            onClick={importTemplate}
+            style={{ padding: 0, fontSize: 12, height: 'auto' }}
           >
-            取消
+            下载模板
           </Button>
-          <Button
-            onClick={submitForm}
-            disabled={formLoading || !file}
-          >
-            {formLoading ? '上传中...' : '确定'}
-          </Button>
-        </DialogFooter>
-      </DialogContent>
-    </Dialog>
+        </div>
+      </div>
+    </Modal>
   );
 });
 
 UserImportForm.displayName = 'UserImportForm';
 
 export default UserImportForm;
-