Explorar o código

新增岗位管理页面修复部分页面axios等

pm hai 5 meses
pai
achega
83d41dbd8b

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 909 - 0
package-lock.json


+ 1 - 0
package.json

@@ -29,6 +29,7 @@
             "@radix-ui/react-toggle": "^1.1.2",
             "@radix-ui/react-toggle-group": "^1.1.2",
             "@radix-ui/react-tooltip": "^1.1.8",
+            "antd": "^6.0.1",
             "axios": "^1.13.2",
             "class-variance-authority": "^0.7.1",
             "clsx": "*",

+ 9 - 9
src/Dashboard.tsx

@@ -127,11 +127,11 @@ export default function Dashboard() {
     const backendMenus = getMenus();
     
     // 调试:打印后端菜单数据
-    console.log('后端菜单数据:', backendMenus);
+    // console.log('后端菜单数据:', backendMenus);
     
     // 如果没有后端菜单数据,使用默认菜单(兼容性处理)
     if (!backendMenus || backendMenus.length === 0) {
-      console.log('没有后端菜单数据,使用默认菜单');
+      // console.log('没有后端菜单数据,使用默认菜单');
       return {
         mainMenus: [
           { key: 'dashboard', icon: Gauge, path: '/dashboard', name: '驾驶舱' },
@@ -334,23 +334,23 @@ export default function Dashboard() {
           if (!subMenuMap[frontendKey]) {
             subMenuMap[frontendKey] = [];
           }
-          console.log(`处理父菜单 "${menu.name}" 的子菜单,共 ${menu.children.length} 个`);
+          // console.log(`处理父菜单 "${menu.name}" 的子菜单,共 ${menu.children.length} 个`);
           menu.children.forEach((child: any) => {
             // 子菜单也需要检查是否包含"客户端",或者父菜单是客户端菜单
             const isChildClientMenu = child.name && child.name.includes('客户端');
             const isChildShouldProcess = isChildClientMenu || isClientMenu;
             
-            console.log(`  子菜单: ${child.name}, visible: ${child.visible}, isChildShouldProcess: ${isChildShouldProcess}`);
+            // console.log(`  子菜单: ${child.name}, visible: ${child.visible}, isChildShouldProcess: ${isChildShouldProcess}`);
             
             // 对于客户端菜单的子菜单,即使 visible 为 false 也显示
             if (!isChildShouldProcess && child.visible === false) {
-              console.log(`    跳过: 不是客户端菜单且不可见`);
+              // console.log(`    跳过: 不是客户端菜单且不可见`);
               return;
             }
             
             if (!isChildShouldProcess) {
               // 如果子菜单不是客户端菜单,且父菜单也不是客户端菜单,跳过
-              console.log(`    跳过: 不是客户端菜单的子菜单`);
+              // console.log(`    跳过: 不是客户端菜单的子菜单`);
               return;
             }
             let childKey: string = mapBackendPathToFrontendKey(child.path) || '';
@@ -362,7 +362,7 @@ export default function Dashboard() {
             // 检查是否已经添加过(去重)
             const subMenuKey = `${frontendKey}_${childKey}`;
             if (processedSubMenuKeys.has(subMenuKey)) {
-              console.log(`    跳过: 子菜单 ${childKey} 已存在`);
+              // console.log(`    跳过: 子菜单 ${childKey} 已存在`);
               return;
             }
             processedSubMenuKeys.add(subMenuKey);
@@ -378,10 +378,10 @@ export default function Dashboard() {
               path: child.path,
               name: childDisplayName
             };
-            console.log(`    添加子菜单到 ${frontendKey}:`, subMenuItem);
+            // console.log(`    添加子菜单到 ${frontendKey}:`, subMenuItem);
             subMenuMap[frontendKey].push(subMenuItem);
           });
-          console.log(`父菜单 "${menu.name}" 的子菜单数量: ${subMenuMap[frontendKey].length}`);
+          // console.log(`父菜单 "${menu.name}" 的子菜单数量: ${subMenuMap[frontendKey].length}`);
         }
       }
       // 注意:不再在这里处理子菜单,避免重复添加

+ 5 - 0
src/api/Login.ts

@@ -55,6 +55,11 @@ export const loginApi = {
     return axiosInstance.get<string>('/system/auth/social-auth-redirect?type=' + type + '&redirectUri=' + encodeURIComponent(redirectUri));
   },
 
+  // 刷新Token
+  refreshToken: (refreshToken: string) => {
+    return axiosInstance.post<LoginResponse>('/system/auth/refresh-token', { refreshToken });
+  },
+
   // 登出
   logout: () => {
     return axiosInstance.post('/system/auth/logout');

+ 76 - 0
src/api/Post.ts

@@ -0,0 +1,76 @@
+import axiosInstance from '../utils/axios';
+
+// 岗位状态枚举
+export enum PostStatus {
+  DISABLE = 0, // 禁用
+  ENABLE = 1   // 启用
+}
+
+// 岗位 VO
+export interface PostVO {
+  id?: number;
+  name: string;
+  code: string;
+  sort: number;
+  status: number;
+  remark: string;
+  createTime?: Date;
+}
+
+// 分页参数类型
+export interface PageParam {
+  pageNo?: number;
+  pageSize?: number;
+  name?: string;
+  code?: string;
+  status?: number;
+  [key: string]: any;
+}
+
+// 分页响应类型
+export interface PageResponse<T> {
+  list: T[];
+  total: number;
+}
+
+// 岗位管理 API
+export const postApi = {
+  // 查询岗位列表(分页)
+  getPostPage: (params?: PageParam) => {
+    return axiosInstance.get<PageResponse<PostVO>>('/system/post/page', { params });
+  },
+
+  // 获取岗位精简信息列表
+  getSimplePostList: () => {
+    return axiosInstance.get<PostVO[]>('/system/post/simple-list');
+  },
+
+  // 查询岗位详情
+  getPost: (id: number) => {
+    return axiosInstance.get<PostVO>(`/system/post/get?id=${id}`);
+  },
+
+  // 新增岗位
+  createPost: (data: PostVO) => {
+    return axiosInstance.post('/system/post/create', data);
+  },
+
+  // 修改岗位
+  updatePost: (data: PostVO) => {
+    return axiosInstance.put('/system/post/update', data);
+  },
+
+  // 删除岗位
+  deletePost: (id: number) => {
+    return axiosInstance.delete(`/system/post/delete?id=${id}`);
+  },
+
+  // 导出岗位
+  exportPost: (params?: PageParam) => {
+    return axiosInstance.get('/system/post/export', { 
+      params,
+      responseType: 'blob'
+    });
+  },
+};
+

+ 3 - 0
src/api/index.ts

@@ -1,5 +1,6 @@
 import { loginApi } from './Login';
 import { menuApi } from './Menu';
+import { postApi } from './Post';
 import { userApi } from './user';
 import { userPermissionApi } from './user/permission';
 import { userImportApi } from './user/import';
@@ -17,6 +18,7 @@ export interface ApiResponse<T = any> {
 // 导出所有 API 模块
 export { loginApi } from './Login';
 export { menuApi } from './Menu';
+export { postApi } from './Post';
 export { userApi } from './user';
 export { userPermissionApi } from './user/permission';
 export { userImportApi } from './user/import';
@@ -32,6 +34,7 @@ export default {
   login: loginApi,
   auth: loginApi, // 兼容旧代码
   menu: menuApi,
+  post: postApi,
   user: userApi,
   userPermission: userPermissionApi,
   userImport: userImportApi,

+ 3 - 3
src/components/MenuManagement.tsx

@@ -118,8 +118,8 @@ export default function MenuManagement() {
   };
 
   // 删除菜单
-  const handleDelete = async (id: number) => {
-    if (!confirm('确定要删除这个菜单吗?删除后无法恢复!')) {
+  const handleDelete = async (id: number, name: string) => {
+    if (!window.confirm(`确定要删除菜单 "${name}" 吗?\n删除后无法恢复,请谨慎操作!`)) {
       return;
     }
 
@@ -379,7 +379,7 @@ export default function MenuManagement() {
               <Button
                 variant="ghost"
                 size="sm"
-                onClick={() => handleDelete(node.id!)}
+                onClick={() => handleDelete(node.id!, node.name)}
                 className="h-8 px-2 text-red-600 hover:text-red-700"
               >
                 <Trash2 className="w-4 h-4" />

+ 244 - 0
src/components/PostForm.tsx

@@ -0,0 +1,244 @@
+import React, { useState, useImperativeHandle, forwardRef } from 'react';
+import { postApi, PostVO, PostStatus } from '../api/Post';
+import { toast } from 'sonner';
+import { Input } from './ui/input';
+import { Label } from './ui/label';
+import { Textarea } from './ui/textarea';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
+import { X } from 'lucide-react';
+
+interface PostFormProps {
+  onSuccess?: () => void;
+}
+
+export interface PostFormRef {
+  open: (type: string, id?: number) => void;
+}
+
+const PostForm = forwardRef<PostFormRef, PostFormProps>(({ onSuccess }, ref) => {
+  const [dialogVisible, setDialogVisible] = useState(false);
+  const [dialogTitle, setDialogTitle] = useState('');
+  const [formType, setFormType] = useState<'create' | 'update'>('create');
+  const [formLoading, setFormLoading] = useState(false);
+  const [formData, setFormData] = useState<PostVO>({
+    name: '',
+    code: '',
+    sort: 0,
+    status: PostStatus.ENABLE,
+    remark: '',
+  });
+
+  // 暴露方法给父组件
+  useImperativeHandle(ref, () => ({
+    open: (type: string, id?: number) => {
+      setDialogTitle(type === 'create' ? '新增岗位' : '修改岗位');
+      setFormType(type as 'create' | 'update');
+      resetForm();
+
+      if (id) {
+        loadPostData(id);
+      } else {
+        setFormLoading(false);
+      }
+
+      setDialogVisible(true);
+    },
+  }));
+
+  // 加载岗位数据
+  const loadPostData = async (id: number) => {
+    setFormLoading(true);
+    try {
+      const res = await postApi.getPost(id);
+      const data = (res as any)?.data || res;
+      setFormData({
+        ...data,
+        status: typeof data.status === 'string' ? Number(data.status) : (data.status ?? PostStatus.ENABLE),
+      } as PostVO);
+    } catch (error: any) {
+      toast.error(error.message || '获取岗位详情失败');
+    } finally {
+      setFormLoading(false);
+    }
+  };
+
+  // 重置表单
+  const resetForm = () => {
+    setFormData({
+      name: '',
+      code: '',
+      sort: 0,
+      status: PostStatus.ENABLE,
+      remark: '',
+    });
+  };
+
+  // 提交表单
+  const submitForm = async () => {
+    // 验证必填字段
+    if (!formData.name || formData.name.trim() === '') {
+      toast.error('岗位名称不能为空');
+      return;
+    }
+    if (!formData.code || formData.code.trim() === '') {
+      toast.error('岗位编码不能为空');
+      return;
+    }
+    if (formData.sort === undefined || formData.sort === null) {
+      toast.error('岗位顺序不能为空');
+      return;
+    }
+    if (formData.status === undefined || formData.status === null) {
+      toast.error('岗位状态不能为空');
+      return;
+    }
+
+    setFormLoading(true);
+    try {
+      const submitData: PostVO = {
+        ...formData,
+        sort: Number(formData.sort) || 0,
+        status: Number(formData.status),
+      };
+
+      if (formType === 'create') {
+        await postApi.createPost(submitData);
+        toast.success('创建成功');
+      } else {
+        await postApi.updatePost(submitData);
+        toast.success('更新成功');
+      }
+      setDialogVisible(false);
+      onSuccess?.();
+    } catch (error: any) {
+      toast.error(error.message || '操作失败');
+    } finally {
+      setFormLoading(false);
+    }
+  };
+
+  if (!dialogVisible) return null;
+
+  return (
+    <div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-[9999] animate-in fade-in duration-200 p-4">
+      <div className="bg-white rounded-xl shadow-2xl w-full max-w-2xl animate-in zoom-in duration-200 relative z-[10000] max-h-[85vh] flex flex-col overflow-hidden">
+        {/* 弹窗标题 */}
+        <div className="px-5 py-3 border-b border-gray-200 flex items-center justify-between shrink-0">
+          <h3 className="text-base text-gray-900">{dialogTitle}</h3>
+          <button
+            onClick={() => setDialogVisible(false)}
+            className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
+          >
+            <X className="w-5 h-5 text-gray-600" />
+          </button>
+        </div>
+
+        {/* 弹窗内容 */}
+        <div className="px-5 py-3 overflow-y-auto overflow-x-hidden flex-1 min-h-0">
+          {formLoading && formType === 'update' ? (
+            <div className="py-6 text-center text-gray-500">加载中...</div>
+          ) : (
+            <div className="space-y-4">
+              <div className="grid grid-cols-[100px_1fr] items-center gap-3">
+                <label className="text-sm text-gray-700">
+                  <span className="text-red-500 mr-1">*</span>
+                  岗位名称
+                </label>
+                <Input
+                  placeholder="请输入岗位名称"
+                  value={formData.name}
+                  onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
+                  disabled={formLoading}
+                  className="bg-white border-gray-200 focus:border-blue-400 focus:ring-2 focus:ring-blue-100"
+                />
+              </div>
+
+              <div className="grid grid-cols-[100px_1fr] items-center gap-3">
+                <label className="text-sm text-gray-700">
+                  <span className="text-red-500 mr-1">*</span>
+                  岗位编码
+                </label>
+                <Input
+                  placeholder="请输入岗位编码"
+                  value={formData.code}
+                  onChange={(e) => setFormData(prev => ({ ...prev, code: e.target.value }))}
+                  disabled={formLoading}
+                  className="bg-white border-gray-200 focus:border-blue-400 focus:ring-2 focus:ring-blue-100"
+                />
+              </div>
+
+              <div className="grid grid-cols-[100px_1fr] items-center gap-3">
+                <label className="text-sm text-gray-700">
+                  <span className="text-red-500 mr-1">*</span>
+                  岗位顺序
+                </label>
+                <Input
+                  type="number"
+                  placeholder="请输入岗位顺序"
+                  value={formData.sort}
+                  onChange={(e) => setFormData(prev => ({ ...prev, sort: Number(e.target.value) || 0 }))}
+                  disabled={formLoading}
+                  className="bg-white border-gray-200 focus:border-blue-400 focus:ring-2 focus:ring-blue-100"
+                />
+              </div>
+
+              <div className="grid grid-cols-[100px_1fr] items-center gap-3">
+                <label className="text-sm text-gray-700">
+                  <span className="text-red-500 mr-1">*</span>
+                  状态
+                </label>
+                <Select
+                  value={formData.status?.toString()}
+                  onValueChange={(value) => setFormData(prev => ({ ...prev, status: Number(value) }))}
+                  disabled={formLoading}
+                >
+                  <SelectTrigger className="bg-white border-gray-200 focus:border-blue-400 focus:ring-2 focus:ring-blue-100">
+                    <SelectValue placeholder="请选择状态" />
+                  </SelectTrigger>
+                  <SelectContent>
+                    <SelectItem value={PostStatus.ENABLE.toString()}>启用</SelectItem>
+                    <SelectItem value={PostStatus.DISABLE.toString()}>禁用</SelectItem>
+                  </SelectContent>
+                </Select>
+              </div>
+
+              <div className="grid grid-cols-[100px_1fr] items-start gap-3">
+                <label className="text-sm text-gray-700 pt-2">备注</label>
+                <Textarea
+                  placeholder="请输入备注"
+                  value={formData.remark}
+                  onChange={(e) => setFormData(prev => ({ ...prev, remark: e.target.value }))}
+                  disabled={formLoading}
+                  rows={4}
+                  className="bg-white border-gray-200 focus:border-blue-400 focus:ring-2 focus:ring-blue-100"
+                />
+              </div>
+            </div>
+          )}
+        </div>
+
+        {/* 弹窗底部 */}
+        <div className="px-5 py-3 bg-gray-50/50 border-t border-gray-200 flex justify-end gap-2 rounded-b-xl shrink-0">
+          <button
+            onClick={() => setDialogVisible(false)}
+            disabled={formLoading}
+            className="px-4 py-2 text-sm text-gray-600 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
+          >
+            取消
+          </button>
+          <button
+            onClick={submitForm}
+            disabled={formLoading}
+            className="px-4 py-2 text-sm text-white bg-gradient-to-r from-blue-500 to-blue-600 rounded-lg hover:shadow-lg hover:shadow-blue-400/40 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
+          >
+            {formLoading ? '提交中...' : '确定'}
+          </button>
+        </div>
+      </div>
+    </div>
+  );
+});
+
+PostForm.displayName = 'PostForm';
+
+export default PostForm;

+ 342 - 0
src/components/PostManagement.tsx

@@ -0,0 +1,342 @@
+import React, { useState, useEffect, useRef } from 'react';
+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 { Button } from './ui/button';
+import { Input } from './ui/input';
+import { Label } from './ui/label';
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './ui/table';
+import PostForm, { PostFormRef } from './PostForm';
+
+export default function PostManagement() {
+  const [loading, setLoading] = useState(true);
+  const [list, setList] = useState<PostVO[]>([]);
+  const [total, setTotal] = useState(0);
+  const [queryParams, setQueryParams] = useState<PageParam>({
+    pageNo: 1,
+    pageSize: 10,
+    name: undefined,
+    code: undefined,
+  });
+  const [exportLoading, setExportLoading] = useState(false);
+  const formRef = useRef<PostFormRef>(null);
+
+  // 获取岗位列表
+  const getList = async (params?: PageParam) => {
+    const currentParams = params || queryParams;
+    setLoading(true);
+    try {
+      console.log('PostManagement: 开始获取岗位列表', currentParams);
+      const response = await postApi.getPostPage(currentParams);
+      console.log('PostManagement: API 响应', response);
+      
+      // 处理响应数据
+      let data;
+      if (response && typeof response === 'object') {
+        // 如果响应有 data 属性,使用 data;否则直接使用 response
+        data = (response as any).data !== undefined ? (response as any).data : response;
+      } else {
+        data = response;
+      }
+      
+      // 确保 data 是对象
+      if (!data || typeof data !== 'object') {
+        console.warn('PostManagement: 响应数据格式异常', data);
+        setList([]);
+        setTotal(0);
+        return;
+      }
+      
+      setList(data.list || []);
+      setTotal(data.total || 0);
+      console.log('PostManagement: 设置列表数据', data.list, '总数', data.total);
+    } catch (error: any) {
+      console.error('PostManagement: 获取岗位列表失败', error);
+      // 即使请求失败,也设置空列表,避免一直显示加载中
+      setList([]);
+      setTotal(0);
+      
+      // 显示错误信息
+      const errorMessage = error?.response?.data?.message || 
+                          error?.message || 
+                          '获取岗位列表失败,请检查网络连接或联系管理员';
+      toast.error(errorMessage);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  // 组件挂载和分页参数变化时获取数据
+  useEffect(() => {
+    console.log('PostManagement: useEffect 触发,queryParams =', queryParams);
+    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: 10,
+      name: undefined,
+      code: undefined,
+    };
+    setQueryParams(resetParams);
+    getList(resetParams);
+  };
+
+  // 打开表单
+  const openForm = (type: string, id?: number) => {
+    console.log('PostManagement: 打开表单', type, id, 'formRef.current =', formRef.current);
+    if (formRef.current) {
+      formRef.current.open(type, id);
+    } else {
+      console.error('PostManagement: formRef.current 为 null');
+      toast.error('表单组件未初始化,请刷新页面重试');
+    }
+  };
+
+  // 删除岗位
+  const handleDelete = async (id: number) => {
+    if (!confirm('确定要删除这个岗位吗?删除后无法恢复!')) {
+      return;
+    }
+
+    try {
+      await postApi.deletePost(id);
+      toast.success('删除成功');
+      await getList();
+    } catch (error: any) {
+      toast.error(error.message || '删除失败');
+    }
+  };
+
+  // 导出岗位
+  const handleExport = async () => {
+    if (!confirm('确定要导出岗位数据吗?')) {
+      return;
+    }
+
+    setExportLoading(true);
+    try {
+      const blob = await postApi.exportPost(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 getStatusLabel = (status: number) => {
+    return status === PostStatus.ENABLE ? '启用' : '禁用';
+  };
+
+  // 获取状态样式
+  const getStatusStyle = (status: number) => {
+    return status === PostStatus.ENABLE
+      ? 'bg-green-100 text-green-700'
+      : 'bg-gray-100 text-gray-700';
+  };
+
+  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 htmlFor="post-name-search" 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"
+              />
+            </div>
+
+            <div className="flex items-center gap-3">
+              <Label htmlFor="post-code-search" 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"
+              />
+            </div>
+          </div>
+
+          {/* 操作按钮组 */}
+          <div className="flex items-center gap-3">
+            <button
+              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
+              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
+              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
+              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"
+            >
+              <Download className="w-4 h-4" strokeWidth={2.5} />
+              <span className="text-sm">{exportLoading ? '导出中...' : '导出'}</span>
+            </button>
+          </div>
+        </div>
+      </div>
+
+      {/* 表格 */}
+      <div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
+        <Table>
+          <TableHeader>
+            <TableRow>
+              <TableHead className="w-[80px] text-center">岗位编号</TableHead>
+              <TableHead className="w-[150px]">岗位名称</TableHead>
+              <TableHead className="w-[150px]">岗位编码</TableHead>
+              <TableHead className="w-[100px] text-center">岗位顺序</TableHead>
+              <TableHead>岗位备注</TableHead>
+              <TableHead className="w-[100px] text-center">状态</TableHead>
+              <TableHead className="w-[180px] text-center">创建时间</TableHead>
+              <TableHead className="w-[160px] text-center">操作</TableHead>
+            </TableRow>
+          </TableHeader>
+          <TableBody>
+            {loading ? (
+              <TableRow>
+                <TableCell colSpan={8} className="text-center py-8 text-gray-500">
+                  加载中...
+                </TableCell>
+              </TableRow>
+            ) : list.length === 0 ? (
+              <TableRow>
+                <TableCell colSpan={8} className="text-center py-8 text-gray-500">
+                  暂无数据
+                </TableCell>
+              </TableRow>
+            ) : (
+              list.map((row, index) => (
+                <TableRow key={row.id} className="hover:bg-gray-50">
+                  <TableCell className="text-center">{row.id}</TableCell>
+                  <TableCell className="font-medium">{row.name}</TableCell>
+                  <TableCell>{row.code}</TableCell>
+                  <TableCell className="text-center">{row.sort}</TableCell>
+                  <TableCell className="text-sm text-gray-600">{row.remark || '-'}</TableCell>
+                  <TableCell className="text-center">
+                    <span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getStatusStyle(row.status)}`}>
+                      {getStatusLabel(row.status)}
+                    </span>
+                  </TableCell>
+                  <TableCell className="text-center text-sm text-gray-600">
+                    {formatDateTimeFull(row.createTime)}
+                  </TableCell>
+                  <TableCell>
+                    <div className="flex items-center gap-2 justify-center">
+                      <Button
+                        variant="ghost"
+                        size="sm"
+                        onClick={() => openForm('update', row.id)}
+                        className="h-8 px-2"
+                      >
+                        <Edit2 className="w-4 h-4" />
+                      </Button>
+                      <Button
+                        variant="ghost"
+                        size="sm"
+                        onClick={() => handleDelete(row.id!)}
+                        className="h-8 px-2 text-red-600 hover:text-red-700"
+                      >
+                        <Trash2 className="w-4 h-4" />
+                      </Button>
+                    </div>
+                  </TableCell>
+                </TableRow>
+              ))
+            )}
+          </TableBody>
+        </Table>
+      </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
+                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 flex items-center">
+                {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!)}
+              >
+                下一页
+              </Button>
+            </div>
+          </div>
+        </div>
+      )}
+
+      {/* 表单弹窗 */}
+      <PostForm ref={formRef} onSuccess={getList} />
+    </div>
+  );
+}
+

+ 16 - 0
src/components/SystemConfig.tsx

@@ -2,6 +2,7 @@ import React, { useState } from 'react';
 import { Plus, Search, Edit2, Trash2, MoreVertical, ChevronRight, ChevronDown } from 'lucide-react';
 import DepartmentManagement from './DepartmentManagement';
 import MenuManagement from './MenuManagement';
+import PostManagement from './PostManagement';
 
 interface TableRow {
   id: number;
@@ -13,6 +14,7 @@ interface SystemConfigProps {
 }
 
 export default function SystemConfig({ subMenu }: SystemConfigProps) {
+  console.log('SystemConfig: 组件渲染,subMenu =', subMenu, '类型:', typeof subMenu);
   const [searchTerm, setSearchTerm] = useState('');
   const [showAddModal, setShowAddModal] = useState(false);
   const [editingItem, setEditingItem] = useState<TableRow | null>(null);
@@ -48,6 +50,7 @@ export default function SystemConfig({ subMenu }: SystemConfigProps) {
       children: [
         { id: 21, name: '菜单管理', path: '/system/menu', icon: '菜单', order: 1, status: '启用', createTime: '2025-01-02', parentId: 2 },
         { id: 22, name: '部门管理', path: '/system/dept', icon: '部门', order: 2, status: '启用', createTime: '2025-01-02', parentId: 2 },
+        { id: 22, name: '岗位管理', path: '/system/post', icon: '岗位', order: 2, status: '启用', createTime: '2025-01-02', parentId: 2 },
         { id: 23, name: '角色管理', path: '/system/role', icon: '角色', order: 3, status: '启用', createTime: '2025-01-02', parentId: 2 },
         { id: 24, name: '字典管理', path: '/system/dict', icon: '字典', order: 4, status: '启用', createTime: '2025-01-02', parentId: 2 },
         { id: 25, name: '机柜管理', path: '/system/cabinet', icon: '机柜', order: 5, status: '启用', createTime: '2025-01-02', parentId: 2 },
@@ -406,6 +409,19 @@ export default function SystemConfig({ subMenu }: SystemConfigProps) {
     return <MenuManagement />;
   }
 
+  // 岗位管理使用专门的组件
+  // 支持多种可能的 subMenu 值:'岗位管理'、'positionManagement'、'post'
+  const isPostManagement = subMenu === '岗位管理' || 
+                           subMenu === 'positionManagement' || 
+                           subMenu === 'post';
+  console.log('SystemConfig: 检查岗位管理条件,subMenu =', subMenu, '是否匹配:', isPostManagement);
+  if (isPostManagement) {
+    console.log('SystemConfig: ✅ 匹配成功,渲染岗位管理组件');
+    return <PostManagement />;
+  } else {
+    console.log('SystemConfig: ❌ 不匹配,继续执行其他逻辑');
+  }
+
   // 字典管理使用卡片展示
   if (subMenu === '字典管理') {
     return (

+ 103 - 119
src/components/ui/alert-dialog.tsx

@@ -1,146 +1,130 @@
 "use client";
 
 import * as React from "react";
-import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog@1.1.6";
+import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
 
 import { cn } from "./utils";
 import { buttonVariants } from "./button";
 
-function AlertDialog({
-  ...props
-}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
-  return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
-}
+const AlertDialog = AlertDialogPrimitive.Root;
 
-function AlertDialogTrigger({
-  ...props
-}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
-  return (
-    <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
-  );
-}
+const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
 
-function AlertDialogPortal({
-  ...props
-}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
-  return (
-    <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
-  );
-}
+const AlertDialogPortal = AlertDialogPrimitive.Portal;
 
-function AlertDialogOverlay({
-  className,
-  ...props
-}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
-  return (
-    <AlertDialogPrimitive.Overlay
-      data-slot="alert-dialog-overlay"
+const AlertDialogOverlay = React.forwardRef<
+  React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
+  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
+>(({ className, ...props }, ref) => (
+  <AlertDialogPrimitive.Overlay
+    className={cn(
+      "fixed inset-0 z-[9999] bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
+      className
+    )}
+    {...props}
+    ref={ref}
+  />
+));
+AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
+
+const AlertDialogContent = React.forwardRef<
+  React.ElementRef<typeof AlertDialogPrimitive.Content>,
+  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
+>(({ className, ...props }, ref) => (
+  <AlertDialogPortal>
+    <AlertDialogOverlay />
+    <AlertDialogPrimitive.Content
+      ref={ref}
       className={cn(
-        "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
-        className,
+        "fixed left-[50%] top-[50%] z-[10000] grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-white p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 sm:rounded-lg",
+        className
       )}
       {...props}
     />
-  );
-}
+  </AlertDialogPortal>
+));
+AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
 
-function AlertDialogContent({
+const AlertDialogHeader = ({
   className,
   ...props
-}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
-  return (
-    <AlertDialogPortal>
-      <AlertDialogOverlay />
-      <AlertDialogPrimitive.Content
-        data-slot="alert-dialog-content"
-        className={cn(
-          "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
-          className,
-        )}
-        {...props}
-      />
-    </AlertDialogPortal>
-  );
-}
+}: React.HTMLAttributes<HTMLDivElement>) => (
+  <div
+    className={cn(
+      "flex flex-col space-y-2 text-center sm:text-left",
+      className
+    )}
+    {...props}
+  />
+);
+AlertDialogHeader.displayName = "AlertDialogHeader";
 
-function AlertDialogHeader({
+const AlertDialogFooter = ({
   className,
   ...props
-}: React.ComponentProps<"div">) {
-  return (
-    <div
-      data-slot="alert-dialog-header"
-      className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
-      {...props}
-    />
-  );
-}
+}: React.HTMLAttributes<HTMLDivElement>) => (
+  <div
+    className={cn(
+      "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
+      className
+    )}
+    {...props}
+  />
+);
+AlertDialogFooter.displayName = "AlertDialogFooter";
 
-function AlertDialogFooter({
-  className,
-  ...props
-}: React.ComponentProps<"div">) {
-  return (
-    <div
-      data-slot="alert-dialog-footer"
-      className={cn(
-        "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
-        className,
-      )}
-      {...props}
-    />
-  );
-}
+const AlertDialogTitle = React.forwardRef<
+  React.ElementRef<typeof AlertDialogPrimitive.Title>,
+  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
+>(({ className, ...props }, ref) => (
+  <AlertDialogPrimitive.Title
+    ref={ref}
+    className={cn("text-lg font-semibold", className)}
+    {...props}
+  />
+));
+AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
 
-function AlertDialogTitle({
-  className,
-  ...props
-}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
-  return (
-    <AlertDialogPrimitive.Title
-      data-slot="alert-dialog-title"
-      className={cn("text-lg font-semibold", className)}
-      {...props}
-    />
-  );
-}
+const AlertDialogDescription = React.forwardRef<
+  React.ElementRef<typeof AlertDialogPrimitive.Description>,
+  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
+>(({ className, ...props }, ref) => (
+  <AlertDialogPrimitive.Description
+    ref={ref}
+    className={cn("text-sm text-muted-foreground", className)}
+    {...props}
+  />
+));
+AlertDialogDescription.displayName =
+  AlertDialogPrimitive.Description.displayName;
 
-function AlertDialogDescription({
-  className,
-  ...props
-}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
-  return (
-    <AlertDialogPrimitive.Description
-      data-slot="alert-dialog-description"
-      className={cn("text-muted-foreground text-sm", className)}
-      {...props}
-    />
-  );
-}
+const AlertDialogAction = React.forwardRef<
+  React.ElementRef<typeof AlertDialogPrimitive.Action>,
+  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
+>(({ className, ...props }, ref) => (
+  <AlertDialogPrimitive.Action
+    ref={ref}
+    className={cn(buttonVariants(), className)}
+    {...props}
+  />
+));
+AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
 
-function AlertDialogAction({
-  className,
-  ...props
-}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
-  return (
-    <AlertDialogPrimitive.Action
-      className={cn(buttonVariants(), className)}
-      {...props}
-    />
-  );
-}
-
-function AlertDialogCancel({
-  className,
-  ...props
-}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
-  return (
-    <AlertDialogPrimitive.Cancel
-      className={cn(buttonVariants({ variant: "outline" }), className)}
-      {...props}
-    />
-  );
-}
+const AlertDialogCancel = React.forwardRef<
+  React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
+  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
+>(({ className, ...props }, ref) => (
+  <AlertDialogPrimitive.Cancel
+    ref={ref}
+    className={cn(
+      buttonVariants({ variant: "outline" }),
+      "mt-2 sm:mt-0",
+      className
+    )}
+    {...props}
+  />
+));
+AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
 
 export {
   AlertDialog,

+ 2 - 2
src/components/ui/dialog.tsx

@@ -39,7 +39,7 @@ const DialogOverlay = React.forwardRef<
       ref={ref}
       data-slot="dialog-overlay"
       className={cn(
-        "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-[100] bg-black/50",
+        "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-[9999] bg-black/50",
         className,
       )}
       {...props}
@@ -59,7 +59,7 @@ const DialogContent = React.forwardRef<
         ref={ref}
         data-slot="dialog-content"
         className={cn(
-          "bg-white data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-[101] grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
+          "bg-white data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-[10000] grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
           className,
         )}
         {...props}

+ 10 - 6
src/components/user/DeptTree.tsx

@@ -43,13 +43,17 @@ export default function DeptTree({ onNodeClick }: DeptTreeProps) {
   // 获取岗位树
   const getTree = async () => {
     try {
-      const res = await systemApi.listMarsDept({ pageNo: 1, pageSize: -1 });
-      const treeData = handleTree(res.list || [], 'id', 'parentId', 'children');
-      setDeptList(treeData);
+      // 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]);
-      }
+      // if (treeData.length > 0) {
+      //   setExpandedIds([treeData[0].id]);
+      // }
+      
+      // 暂时使用空数据,避免报错
+      setDeptList([]);
     } catch (error) {
       console.error('获取岗位树失败:', error);
     }

+ 7 - 3
src/components/user/UserForm.tsx

@@ -69,9 +69,13 @@ const UserForm = forwardRef<UserFormRef, UserFormProps>(({ onSuccess }, ref) =>
 
     // 加载岗位列表
     try {
-      const data = await systemApi.listMarsDept({ pageNo: 1, pageSize: -1 });
-      const treeData = handleTree(data.list || [], 'id', 'parentId', 'children');
-      setWorkstationList(treeData);
+      // TODO: 暂时注释,等待接口实现
+      // const data = await systemApi.listMarsDept({ pageNo: 1, pageSize: -1 });
+      // const treeData = handleTree(data.list || [], 'id', 'parentId', 'children');
+      // setWorkstationList(treeData);
+      
+      // 暂时使用空数据,避免报错
+      setWorkstationList([]);
     } catch (error) {
       console.error('加载岗位列表失败:', error);
     }

+ 101 - 7
src/utils/axios.ts

@@ -1,6 +1,8 @@
-import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios';
+import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError, InternalAxiosRequestConfig } from 'axios';
 import { toast } from 'sonner';
 import { env } from '../config/env';
+import { getRefreshToken, setToken, setAccessToken, setRefreshToken, clearAuth } from './auth';
+import { loginApi } from '../api/Login';
 
 // 创建 axios 实例
 const axiosInstance: AxiosInstance = axios.create({
@@ -11,6 +13,26 @@ const axiosInstance: AxiosInstance = axios.create({
   },
 });
 
+// 是否正在刷新token
+let isRefreshing = false;
+// 待重试的请求队列
+let failedQueue: Array<{
+  resolve: (value?: any) => void;
+  reject: (reason?: any) => void;
+}> = [];
+
+// 处理队列中的请求
+const processQueue = (error: any, token: string | null = null) => {
+  failedQueue.forEach(prom => {
+    if (error) {
+      prom.reject(error);
+    } else {
+      prom.resolve(token);
+    }
+  });
+  failedQueue = [];
+};
+
 // 请求拦截器
 axiosInstance.interceptors.request.use(
   (config) => {
@@ -70,12 +92,84 @@ axiosInstance.interceptors.response.use(
 
       switch (status) {
         case 401:
-          errorMessage = '未授权,请重新登录';
-          // 清除 token 并跳转到登录页
-          localStorage.removeItem('token');
-          localStorage.removeItem('tenantId');
-          localStorage.removeItem('userId');
-          window.location.href = '/login';
+          // 获取原始请求配置
+          const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean };
+          
+          // 如果是刷新token的请求失败,直接跳转登录
+          if (originalRequest.url?.includes('/refresh-token') || originalRequest._retry) {
+            errorMessage = '登录已过期,请重新登录';
+            clearAuth();
+            window.location.href = '/login';
+            return Promise.reject(new Error(errorMessage));
+          }
+
+          // 如果正在刷新token,将请求加入队列
+          if (isRefreshing) {
+            return new Promise((resolve, reject) => {
+              failedQueue.push({ resolve, reject });
+            })
+              .then((token) => {
+                if (originalRequest.headers) {
+                  originalRequest.headers.Authorization = `Bearer ${token}`;
+                }
+                return axiosInstance(originalRequest);
+              })
+              .catch((err) => {
+                return Promise.reject(err);
+              });
+          }
+
+          // 尝试刷新token
+          const refreshToken = getRefreshToken();
+          if (!refreshToken) {
+            errorMessage = '登录已过期,请重新登录';
+            clearAuth();
+            window.location.href = '/login';
+            return Promise.reject(new Error(errorMessage));
+          }
+
+          isRefreshing = true;
+          originalRequest._retry = true;
+
+          return loginApi.refreshToken(refreshToken)
+            .then((res: any) => {
+              const tokenData = res?.data || res;
+              const newAccessToken = tokenData?.accessToken || tokenData?.token;
+              const newRefreshToken = tokenData?.refreshToken;
+
+              if (newAccessToken) {
+                // 更新token
+                setToken({ accessToken: newAccessToken, token: newAccessToken });
+                setAccessToken(newAccessToken);
+                if (newRefreshToken) {
+                  setRefreshToken(newRefreshToken);
+                }
+
+                // 更新请求头
+                if (originalRequest.headers) {
+                  originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
+                }
+
+                // 处理队列中的请求
+                processQueue(null, newAccessToken);
+
+                // 重试原始请求
+                return axiosInstance(originalRequest);
+              } else {
+                throw new Error('刷新token失败:未返回新token');
+              }
+            })
+            .catch((refreshError: any) => {
+              // 刷新失败,清除所有认证信息并跳转登录
+              errorMessage = '登录已过期,请重新登录';
+              processQueue(refreshError, null);
+              clearAuth();
+              window.location.href = '/login';
+              return Promise.reject(new Error(errorMessage));
+            })
+            .finally(() => {
+              isRefreshing = false;
+            });
           break;
         case 403:
           errorMessage = '拒绝访问';

+ 94 - 0
src/utils/formatTime.ts

@@ -0,0 +1,94 @@
+/**
+ * 格式化时间工具函数
+ * 支持时间戳(毫秒)、Date 对象、字符串格式
+ */
+
+/**
+ * 格式化日期时间
+ * @param date 可以是时间戳(毫秒)、Date 对象、字符串格式的日期
+ * @param format 格式化类型:'date' | 'datetime' | 'time' | 'full'
+ * @returns 格式化后的日期字符串
+ */
+export function formatDate(
+  date?: number | string | Date | null,
+  format: 'date' | 'datetime' | 'time' | 'full' = 'datetime'
+): string {
+  if (!date) return '-';
+
+  let dateObj: Date;
+
+  // 处理时间戳(毫秒)
+  if (typeof date === 'number') {
+    // 判断是秒级还是毫秒级时间戳
+    // 如果小于 13 位数字,认为是秒级,需要乘以 1000
+    if (date.toString().length <= 10) {
+      dateObj = new Date(date * 1000);
+    } else {
+      dateObj = new Date(date);
+    }
+  } else if (typeof date === 'string') {
+    dateObj = new Date(date);
+  } else {
+    dateObj = date;
+  }
+
+  // 检查日期是否有效
+  if (isNaN(dateObj.getTime())) {
+    return '-';
+  }
+
+  const options: Intl.DateTimeFormatOptions = {};
+
+  switch (format) {
+    case 'date':
+      options.year = 'numeric';
+      options.month = '2-digit';
+      options.day = '2-digit';
+      break;
+    case 'time':
+      options.hour = '2-digit';
+      options.minute = '2-digit';
+      options.second = '2-digit';
+      break;
+    case 'full':
+      options.year = 'numeric';
+      options.month = '2-digit';
+      options.day = '2-digit';
+      options.hour = '2-digit';
+      options.minute = '2-digit';
+      options.second = '2-digit';
+      break;
+    case 'datetime':
+    default:
+      options.year = 'numeric';
+      options.month = '2-digit';
+      options.day = '2-digit';
+      options.hour = '2-digit';
+      options.minute = '2-digit';
+      break;
+  }
+
+  return dateObj.toLocaleString('zh-CN', options);
+}
+
+/**
+ * 格式化日期(仅日期部分)
+ */
+export function formatDateOnly(date?: number | string | Date | null): string {
+  return formatDate(date, 'date');
+}
+
+/**
+ * 格式化时间(仅时间部分)
+ */
+export function formatTimeOnly(date?: number | string | Date | null): string {
+  return formatDate(date, 'time');
+}
+
+/**
+ * 格式化完整日期时间(包含秒)
+ */
+export function formatDateTimeFull(date?: number | string | Date | null): string {
+  return formatDate(date, 'full');
+}
+

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio