Forráskód Böngészése

feat: add user management page with role/perm drawers

BaiLuoYan 3 napja
szülő
commit
36ffb8bd16

+ 71 - 0
src/pages/Admin/User/components/BindRolesDrawer.tsx

@@ -0,0 +1,71 @@
+import { fetchRoleList } from '@/services/role';
+import { fetchBindRoles, fetchUserDetail } from '@/services/user';
+import { Button, Checkbox, Drawer, Spin, message } from 'antd';
+import { useEffect, useState } from 'react';
+
+interface BindRolesDrawerProps {
+  userId: number;
+  productCode: string;
+  open: boolean;
+  onClose: () => void;
+}
+
+export const BindRolesDrawer = ({ userId, productCode, open, onClose }: BindRolesDrawerProps) => {
+  const [loading, setLoading] = useState(false);
+  const [saving, setSaving] = useState(false);
+  const [roles, setRoles] = useState<API.RoleItem[]>([]);
+  const [checkedIds, setCheckedIds] = useState<number[]>([]);
+
+  useEffect(() => {
+    if (!open) return;
+    setLoading(true);
+    Promise.all([fetchUserDetail({ id: userId }), fetchRoleList({ productCode, pageSize: 999 })])
+      .then(([userRes, roleRes]) => {
+        setRoles(roleRes.data?.list ?? []);
+        setCheckedIds(userRes.data?.roleIds ?? []);
+      })
+      .finally(() => setLoading(false));
+  }, [open, userId, productCode]);
+
+  const handleSave = async () => {
+    setSaving(true);
+    try {
+      await fetchBindRoles({ userId, roleIds: checkedIds });
+      message.success('保存成功');
+      onClose();
+    } finally {
+      setSaving(false);
+    }
+  };
+
+  const toggle = (id: number) =>
+    setCheckedIds((prev) => (prev.includes(id) ? prev.filter((i) => i !== id) : [...prev, id]));
+
+  return (
+    <Drawer
+      title="分配角色"
+      open={open}
+      onClose={onClose}
+      width={400}
+      extra={
+        <Button type="primary" loading={saving} onClick={handleSave}>
+          保存
+        </Button>
+      }
+    >
+      <Spin spinning={loading}>
+        <div className="flex flex-col gap-2">
+          {roles.map((r) => (
+            <Checkbox key={r.id} checked={checkedIds.includes(r.id)} onChange={() => toggle(r.id)}>
+              <span>{r.name}</span>
+              <span className="text-xs text-gray-400 ml-2">级别 {r.permsLevel}</span>
+            </Checkbox>
+          ))}
+          {roles.length === 0 && !loading && (
+            <span className="text-gray-400 text-sm">该产品暂无角色</span>
+          )}
+        </div>
+      </Spin>
+    </Drawer>
+  );
+};

+ 86 - 0
src/pages/Admin/User/components/Form.tsx

@@ -0,0 +1,86 @@
+import { fetchDeptTree } from '@/services/dept';
+import { fetchCreateUser, fetchUpdateUser } from '@/services/user';
+import { DrawerForm, ProFormItem, ProFormText } from '@ant-design/pro-components';
+import { TreeSelect } from 'antd';
+import { useEffect, useState } from 'react';
+
+interface UserFormProps {
+  mode: EditorFormMode;
+  initialValues?: Partial<API.UserItem>;
+  trigger: React.ReactElement;
+  onSuccess: () => void;
+}
+
+const buildDeptOptions = (items: API.DeptItem[]): any[] =>
+  items.map((d) => ({
+    value: d.id,
+    title: d.name,
+    children: d.children ? buildDeptOptions(d.children) : undefined,
+  }));
+
+export const UserForm = ({ mode, initialValues, trigger, onSuccess }: UserFormProps) => {
+  const [deptTree, setDeptTree] = useState<any[]>([]);
+
+  useEffect(() => {
+    fetchDeptTree().then((res) => setDeptTree(buildDeptOptions(res.data ?? [])));
+  }, []);
+
+  const title = { add: '新建用户', edit: '编辑用户', copy: '复制用户' }[mode];
+
+  const formValues =
+    mode === 'copy'
+      ? { ...initialValues, id: undefined, username: undefined, password: undefined }
+      : mode === 'edit'
+        ? initialValues
+        : undefined;
+
+  const handleFinish = async (values: any) => {
+    if (mode === 'edit' && initialValues?.id) {
+      await fetchUpdateUser({
+        id: initialValues.id,
+        nickname: values.nickname,
+        email: values.email,
+        phone: values.phone,
+        remark: values.remark,
+        deptId: values.deptId,
+      });
+    } else {
+      await fetchCreateUser(values);
+    }
+    onSuccess();
+    return true;
+  };
+
+  return (
+    <DrawerForm
+      title={title}
+      trigger={trigger}
+      initialValues={formValues}
+      onFinish={handleFinish}
+      drawerProps={{ destroyOnClose: true }}
+    >
+      <ProFormText
+        name="username"
+        label="用户名"
+        rules={[{ required: true }]}
+        disabled={mode === 'edit'}
+        readonly={mode === 'edit'}
+      />
+      {mode !== 'edit' && (
+        <ProFormText
+          name="password"
+          label="密码"
+          rules={[{ required: true }]}
+          fieldProps={{ type: 'password' }}
+        />
+      )}
+      <ProFormText name="nickname" label="昵称" />
+      <ProFormText name="email" label="邮箱" rules={[{ type: 'email' }]} />
+      <ProFormText name="phone" label="手机号" />
+      <ProFormText name="remark" label="备注" />
+      <ProFormItem name="deptId" label="所属部门">
+        <TreeSelect treeData={deptTree} allowClear placeholder="请选择部门" />
+      </ProFormItem>
+    </DrawerForm>
+  );
+};

+ 88 - 0
src/pages/Admin/User/components/UserPermDrawer.tsx

@@ -0,0 +1,88 @@
+import { PermTree } from '@/pages/Admin/_shared/PermTree';
+import { calcUserPermDelta } from '@/pages/Admin/_shared/PermTree/lib/permUtils';
+import { fetchPermList } from '@/services/perm';
+import { fetchRoleDetail } from '@/services/role';
+import { fetchSetUserPerms, fetchUserDetail } from '@/services/user';
+import { Button, Drawer, Spin, message } from 'antd';
+import { useEffect, useState } from 'react';
+
+interface UserPermDrawerProps {
+  userId: number;
+  productCode: string;
+  open: boolean;
+  onClose: () => void;
+}
+
+export const UserPermDrawer = ({ userId, productCode, open, onClose }: UserPermDrawerProps) => {
+  const [loading, setLoading] = useState(false);
+  const [saving, setSaving] = useState(false);
+  const [allPerms, setAllPerms] = useState<API.PermItem[]>([]);
+  const [checkedIds, setCheckedIds] = useState<number[]>([]);
+  const [roleInheritedIds, setRoleInheritedIds] = useState<number[]>([]);
+
+  useEffect(() => {
+    if (!open) return;
+    setLoading(true);
+
+    const load = async () => {
+      const [userRes, permRes] = await Promise.all([
+        fetchUserDetail({ id: userId }),
+        fetchPermList({ productCode, pageSize: 9999 }),
+      ]);
+      const perms = permRes.data?.list ?? [];
+      setAllPerms(perms);
+
+      const codeToId = new Map(perms.map((p) => [p.code, p.id]));
+
+      const userPermCodes = userRes.data?.perms ?? [];
+      const userPermIds = userPermCodes.flatMap((code) => {
+        const id = codeToId.get(code);
+        return id !== undefined ? [id] : [];
+      });
+      setCheckedIds(userPermIds);
+
+      const roleIds = userRes.data?.roleIds ?? [];
+      const roleDetails = await Promise.all(roleIds.map((id) => fetchRoleDetail({ id })));
+      const inherited = new Set<number>();
+      roleDetails.forEach((r) => (r.data?.permIds ?? []).forEach((id) => inherited.add(id)));
+      setRoleInheritedIds([...inherited]);
+    };
+
+    load().finally(() => setLoading(false));
+  }, [open, userId, productCode]);
+
+  const handleSave = async () => {
+    setSaving(true);
+    try {
+      const perms = calcUserPermDelta(checkedIds, roleInheritedIds);
+      await fetchSetUserPerms({ userId, perms });
+      message.success('保存成功');
+      onClose();
+    } finally {
+      setSaving(false);
+    }
+  };
+
+  return (
+    <Drawer
+      title="设置权限"
+      open={open}
+      onClose={onClose}
+      width={560}
+      extra={
+        <Button type="primary" loading={saving} onClick={handleSave}>
+          保存
+        </Button>
+      }
+    >
+      <Spin spinning={loading}>
+        <PermTree
+          mode="edit"
+          allPerms={allPerms}
+          checkedPermIds={checkedIds}
+          onChange={setCheckedIds}
+        />
+      </Spin>
+    </Drawer>
+  );
+};

+ 132 - 1
src/pages/Admin/User/index.tsx

@@ -1,3 +1,134 @@
+import { fetchProductList } from '@/services/product';
+import { fetchUpdateUserStatus, fetchUserList } from '@/services/user';
+import { ActionType, PageContainer, ProColumns, ProTable } from '@ant-design/pro-components';
+import { useIntl } from '@umijs/max';
+import { Button, Popconfirm, Select, Tag, message } from 'antd';
+import { useEffect, useRef, useState } from 'react';
+import { BindRolesDrawer } from './components/BindRolesDrawer';
+import { UserForm } from './components/Form';
+import { UserPermDrawer } from './components/UserPermDrawer';
+
 export default function UserPage() {
-  return null;
+  const intl = useIntl();
+  const actionRef = useRef<ActionType>();
+  const [products, setProducts] = useState<API.ProductItem[]>([]);
+  const [productCode, setProductCode] = useState<string | undefined>();
+  const [rolesDrawer, setRolesDrawer] = useState<{ open: boolean; userId: number }>({
+    open: false,
+    userId: 0,
+  });
+  const [permDrawer, setPermDrawer] = useState<{ open: boolean; userId: number }>({
+    open: false,
+    userId: 0,
+  });
+
+  useEffect(() => {
+    fetchProductList({ pageSize: 999 }).then((res) => setProducts(res.data?.list ?? []));
+  }, []);
+
+  const handleToggleStatus = async (r: API.UserItem) => {
+    await fetchUpdateUserStatus({ id: r.id, status: r.status === 1 ? 0 : 1 });
+    message.success('操作成功');
+    actionRef.current?.reload();
+  };
+
+  const columns: ProColumns<API.UserItem>[] = [
+    { title: '用户名', dataIndex: 'username' },
+    { title: '昵称', dataIndex: 'nickname' },
+    {
+      title: '状态',
+      dataIndex: 'status',
+      render: (_, r) => (
+        <Tag color={r.status === 1 ? 'success' : 'default'}>{r.status === 1 ? '启用' : '禁用'}</Tag>
+      ),
+    },
+    { title: '创建时间', dataIndex: 'createTime', valueType: 'dateTime' },
+    {
+      title: '操作',
+      valueType: 'option',
+      render: (_, r) =>
+        [
+          <UserForm
+            key="edit"
+            mode="edit"
+            initialValues={r}
+            trigger={<a>编辑</a>}
+            onSuccess={() => actionRef.current?.reload()}
+          />,
+          <UserForm
+            key="copy"
+            mode="copy"
+            initialValues={r}
+            trigger={<a>复制</a>}
+            onSuccess={() => actionRef.current?.reload()}
+          />,
+          productCode ? (
+            <a key="roles" onClick={() => setRolesDrawer({ open: true, userId: r.id })}>
+              分配角色
+            </a>
+          ) : null,
+          productCode ? (
+            <a key="perms" onClick={() => setPermDrawer({ open: true, userId: r.id })}>
+              设置权限
+            </a>
+          ) : null,
+          <Popconfirm
+            key="status"
+            title={r.status === 1 ? '确认冻结?' : '确认解冻?'}
+            onConfirm={() => handleToggleStatus(r)}
+          >
+            <a className={r.status === 1 ? 'text-red-500' : ''}>
+              {r.status === 1 ? '冻结' : '解冻'}
+            </a>
+          </Popconfirm>,
+        ].filter(Boolean),
+    },
+  ];
+
+  return (
+    <PageContainer title={intl.formatMessage({ id: 'admin.user.title' })}>
+      <div className="mb-3 flex items-center gap-2">
+        <span className="text-sm text-gray-500">当前产品:</span>
+        <Select
+          allowClear
+          placeholder="选择产品(用于角色/权限操作)"
+          options={products.map((p) => ({ label: p.name, value: p.code }))}
+          value={productCode}
+          onChange={setProductCode}
+          className="w-64"
+        />
+      </div>
+      <ProTable<API.UserItem>
+        actionRef={actionRef}
+        columns={columns}
+        rowKey="id"
+        request={async ({ current, pageSize }) => {
+          const res = await fetchUserList({ page: current, pageSize });
+          return { data: res.data?.list ?? [], total: res.data?.total ?? 0, success: true };
+        }}
+        toolBarRender={() => [
+          <UserForm
+            key="create"
+            mode="add"
+            trigger={<Button type="primary">新建用户</Button>}
+            onSuccess={() => actionRef.current?.reload()}
+          />,
+        ]}
+      />
+      {productCode && (
+        <>
+          <BindRolesDrawer
+            {...rolesDrawer}
+            productCode={productCode}
+            onClose={() => setRolesDrawer((p) => ({ ...p, open: false }))}
+          />
+          <UserPermDrawer
+            {...permDrawer}
+            productCode={productCode}
+            onClose={() => setPermDrawer((p) => ({ ...p, open: false }))}
+          />
+        </>
+      )}
+    </PageContainer>
+  );
 }