Explorar el Código

✨ feat(user): 实现用户角色管理功能

- 添加RoleSelect组件,支持多角色选择
- 优化用户列表页角色展示,支持多角色标签显示
- 完善用户创建和编辑功能,支持角色分配

✨ feat(api): 增强用户API接口

- 更新用户创建接口,支持角色ID列表参数
- 更新用户更新接口,支持角色ID列表参数及角色分配
- 优化用户数据返回格式,包含完整角色信息

♻️ refactor(user): 重构用户表单数据处理

- 改进用户编辑时表单数据填充逻辑,正确处理角色ID转换
- 优化用户状态管理,初始化角色选择默认值
- 完善表单验证规则,添加角色必选验证
yourname hace 7 meses
padre
commit
a8657315bb

+ 74 - 0
src/client/admin/components/RoleSelect.tsx

@@ -0,0 +1,74 @@
+import React from 'react';
+import { Select } from 'antd';
+import type { SelectProps } from 'antd';
+import { useQuery } from '@tanstack/react-query';
+import { roleClient } from '@/client/api';
+import type { InferResponseType } from 'hono/client';
+
+const { Option } = Select;
+
+type RoleListResponse = InferResponseType<typeof roleClient.$get, 200>;
+
+interface RoleSelectProps {
+  value?: number[];
+  onChange?: (value: number[]) => void;
+  placeholder?: string;
+  mode?: 'multiple' | 'tags';
+  disabled?: boolean;
+  allowClear?: boolean;
+  showSearch?: boolean;
+}
+
+export const RoleSelect: React.FC<RoleSelectProps> = ({
+  value,
+  onChange,
+  placeholder = '请选择角色',
+  mode = 'multiple',
+  disabled = false,
+  allowClear = true,
+  showSearch = true,
+}) => {
+  const { data: rolesData, isLoading } = useQuery({
+    queryKey: ['roles', 'select'],
+    queryFn: async () => {
+      const res = await roleClient.$get({
+        query: {
+          page: 1,
+          pageSize: 100, // 获取所有角色用于选择
+        }
+      });
+      if (res.status !== 200) {
+        throw new Error('获取角色列表失败');
+      }
+      return await res.json();
+    },
+    staleTime: 5 * 60 * 1000, // 5分钟缓存
+  });
+
+  const roles = rolesData?.data || [];
+
+  return (
+    <Select
+      mode={mode as SelectProps['mode']}
+      value={value}
+      onChange={onChange}
+      placeholder={placeholder}
+      disabled={disabled || isLoading}
+      allowClear={allowClear}
+      showSearch={showSearch}
+      filterOption={(input, option) =>
+        String(option?.children).toLowerCase().includes(input.toLowerCase())
+      }
+      loading={isLoading}
+    >
+      {roles.map(role => (
+        <Option key={role.id} value={role.id}>
+          {role.name}
+        </Option>
+      ))}
+    </Select>
+  );
+};
+
+// 默认导出
+export default RoleSelect;

+ 25 - 8
src/client/admin/pages/Users.tsx

@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React, { useState, useEffect } from 'react';
 import {
 import {
   Button, Table, Space, Form, Input, Select, Modal, Card, Typography, Tag, Popconfirm,
   Button, Table, Space, Form, Input, Select, Modal, Card, Typography, Tag, Popconfirm,
   App
   App
@@ -6,6 +6,7 @@ import {
 import { useQuery } from '@tanstack/react-query';
 import { useQuery } from '@tanstack/react-query';
 import dayjs from 'dayjs';
 import dayjs from 'dayjs';
 import { roleClient, userClient } from '@/client/api';
 import { roleClient, userClient } from '@/client/api';
+import RoleSelect from '@/client/admin/components/RoleSelect';
 import type { InferResponseType, InferRequestType } from 'hono/client';
 import type { InferResponseType, InferRequestType } from 'hono/client';
 
 
 type UserListResponse = InferResponseType<typeof userClient.$get, 200>;
 type UserListResponse = InferResponseType<typeof userClient.$get, 200>;
@@ -77,6 +78,7 @@ export const UsersPage = () => {
     setModalTitle('创建用户');
     setModalTitle('创建用户');
     setEditingUser(null);
     setEditingUser(null);
     form.resetFields();
     form.resetFields();
+    form.setFieldsValue({ roleIds: [] });
     setModalVisible(true);
     setModalVisible(true);
   };
   };
 
 
@@ -84,7 +86,10 @@ export const UsersPage = () => {
   const showEditModal = (user: any) => {
   const showEditModal = (user: any) => {
     setModalTitle('编辑用户');
     setModalTitle('编辑用户');
     setEditingUser(user);
     setEditingUser(user);
-    form.setFieldsValue(user);
+    form.setFieldsValue({
+      ...user,
+      roleIds: user.roles?.map((role: any) => role.id) || []
+    });
     setModalVisible(true);
     setModalVisible(true);
   };
   };
 
 
@@ -163,12 +168,16 @@ export const UsersPage = () => {
     },
     },
     {
     {
       title: '角色',
       title: '角色',
-      dataIndex: 'role',
-      key: 'role',
-      render: (role: string) => (
-        <Tag color={role === 'admin' ? 'red' : 'blue'}>
-          {role === 'admin' ? '管理员' : '普通用户'}
-        </Tag>
+      dataIndex: 'roles',
+      key: 'roles',
+      render: (roles: any[]) => (
+        <Space wrap>
+          {roles?.map(role => (
+            <Tag key={role.id} color="blue">
+              {role.name}
+            </Tag>
+          ))}
+        </Space>
       ),
       ),
     },
     },
     {
     {
@@ -335,6 +344,14 @@ export const UsersPage = () => {
               <Select.Option value={1}>禁用</Select.Option>
               <Select.Option value={1}>禁用</Select.Option>
             </Select>
             </Select>
           </Form.Item>
           </Form.Item>
+
+          <Form.Item
+            name="roleIds"
+            label="角色"
+            rules={[{ required: true, message: '请选择用户角色' }]}
+          >
+            <RoleSelect mode="multiple" placeholder="请选择角色" />
+          </Form.Item>
         </Form>
         </Form>
       </Modal>
       </Modal>
     </div>
     </div>

+ 43 - 10
src/server/api/users/[id]/put.ts

@@ -17,11 +17,23 @@ const UpdateParams = z.object({
   })
   })
 });
 });
 
 
-const UpdateUserSchema = UserSchema.omit({ 
-  id: true,
-  createdAt: true,
-  updatedAt: true 
-}).partial();
+const UpdateUserSchema = z.object({
+  username: UserSchema.shape.username.optional(),
+  password: UserSchema.shape.password.optional(),
+  phone: UserSchema.shape.phone.optional(),
+  email: UserSchema.shape.email.optional(),
+  nickname: UserSchema.shape.nickname.optional(),
+  name: UserSchema.shape.name.optional(),
+  avatar: UserSchema.shape.avatar.optional(),
+  isDisabled: UserSchema.shape.isDisabled.optional(),
+  isDeleted: UserSchema.shape.isDeleted.optional(),
+  defaultDepartmentId: z.number().int().positive().nullable().optional(),
+  dataScopeType: z.enum(['PERSONAL', 'DEPARTMENT', 'SUB_DEPARTMENT', 'COMPANY', 'CUSTOM']).optional(),
+  roleIds: z.array(z.number().int().positive()).optional().openapi({
+    description: '角色ID列表',
+    example: [1, 2]
+  }),
+});
 
 
 const routeDef = createRoute({
 const routeDef = createRoute({
   method: 'put',
   method: 'put',
@@ -61,15 +73,36 @@ const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
   try {
   try {
     const { id } = c.req.valid('param');
     const { id } = c.req.valid('param');
     const data = c.req.valid('json');
     const data = c.req.valid('json');
-    const user = await userService.updateUser(id, data);
+    const { roleIds, ...userData } = data;
+    
+    // 转换数据类型
+    const userUpdateData: any = { ...userData };
+    if (userUpdateData.defaultDepartmentId === null) {
+      delete userUpdateData.defaultDepartmentId;
+    }
+    
+    // 更新用户基本信息
+    const user = await userService.updateUser(id, userUpdateData);
     if (!user) {
     if (!user) {
       return c.json({ code: 404, message: '用户不存在' }, 404);
       return c.json({ code: 404, message: '用户不存在' }, 404);
     }
     }
-    return c.json(user, 200);
+    
+    // 更新角色
+    if (roleIds !== undefined) {
+      await userService.assignRoles(id, roleIds);
+    }
+    
+    // 重新获取用户(包含角色信息)
+    const userWithRoles = await userService.getUserById(id);
+    if (!userWithRoles) {
+      return c.json({ code: 404, message: '用户不存在' }, 404);
+    }
+    
+    return c.json(userWithRoles, 200);
   } catch (error) {
   } catch (error) {
-    return c.json({ 
-      code: 500, 
-      message: error instanceof Error ? error.message : '更新用户失败' 
+    return c.json({
+      code: 500,
+      message: error instanceof Error ? error.message : '更新用户失败'
     }, 500);
     }, 500);
   }
   }
 });
 });

+ 30 - 3
src/server/api/users/post.ts

@@ -14,12 +14,27 @@ const CreateUserSchema = UserSchema.omit({
   id: true,
   id: true,
   createdAt: true,
   createdAt: true,
   updatedAt: true,
   updatedAt: true,
+  roles: true,
+  defaultDepartmentId: true,
+  dataScopeType: true,
 }).extend({
 }).extend({
   phone: UserSchema.shape.phone.optional(),
   phone: UserSchema.shape.phone.optional(),
   email: UserSchema.shape.email.optional(),
   email: UserSchema.shape.email.optional(),
   nickname: UserSchema.shape.nickname.optional(),
   nickname: UserSchema.shape.nickname.optional(),
   name: UserSchema.shape.name.optional(),
   name: UserSchema.shape.name.optional(),
   avatar: UserSchema.shape.avatar.optional(),
   avatar: UserSchema.shape.avatar.optional(),
+  roleIds: z.array(z.number().int().positive()).optional().openapi({
+    description: '角色ID列表',
+    example: [1, 2]
+  }),
+  defaultDepartmentId: z.number().int().positive().optional().openapi({
+    description: '默认部门ID',
+    example: 1
+  }),
+  dataScopeType: z.enum(['PERSONAL', 'DEPARTMENT', 'SUB_DEPARTMENT', 'COMPANY', 'CUSTOM']).optional().openapi({
+    description: '数据范围类型',
+    example: 'PERSONAL'
+  }),
 })
 })
 
 
 const createUserRoute = createRoute({
 const createUserRoute = createRoute({
@@ -66,10 +81,22 @@ const createUserRoute = createRoute({
 const app = new OpenAPIHono<AuthContext>().openapi(createUserRoute, async (c) => {
 const app = new OpenAPIHono<AuthContext>().openapi(createUserRoute, async (c) => {
   try {
   try {
     const data = c.req.valid('json');
     const data = c.req.valid('json');
-    const user = await userService.createUser(data);
-    return c.json(user, 201);
+    const { roleIds, ...userData } = data;
+    
+    // 创建用户
+    const user = await userService.createUser(userData);
+    
+    // 分配角色
+    if (roleIds && roleIds.length > 0) {
+      await userService.assignRoles(user.id, roleIds);
+    }
+    
+    // 重新获取用户(包含角色信息)
+    const userWithRoles = await userService.getUserById(user.id);
+    return c.json(userWithRoles, 201);
   } catch (error) {
   } catch (error) {
-    return c.json({ code: 500, message: '服务器错误' }, 500);
+    const message = error instanceof Error ? error.message : '服务器错误';
+    return c.json({ code: 500, message }, 500);
   }
   }
 });
 });
 export default app;
 export default app;