Przeglądaj źródła

feat: 页面优化

BaiLuoYan 2 dni temu
rodzic
commit
8503e00bf0

+ 61 - 7
src/pages/Admin/Dept/components/UserTable.tsx

@@ -2,17 +2,29 @@ import { MEMBER_TYPE_COLORS, MEMBER_TYPE_LABELS, STATUS_ENABLED } from '@/define
 import { BindRolesDrawer } from '@/pages/Admin/_shared/BindRolesDrawer';
 import { UserPermDrawer } from '@/pages/Admin/_shared/UserPermDrawer';
 import { ProductRolesBase, useUserProductRoles } from '@/pages/Admin/_shared/useUserProductRoles';
+import { fetchUpdateUser } from '@/services/user';
 import { StopOutlined } from '@ant-design/icons';
-import { Spin, Table, Tag } from 'antd';
-import type { ColumnsType } from 'antd/es/table';
+import { DrawerForm, ProColumns, ProFormSelect, ProTable } from '@ant-design/pro-components';
+import { Button, message, Popconfirm, Spin, Tag } from 'antd';
 import { useMemo, useState } from 'react';
 
 interface DeptUserTableProps {
   users: API.UserItem[];
   productRolesBase: ProductRolesBase;
+  deptId: number;
+  deptName: string;
+  noDeptUsers: API.UserItem[];
+  onMemberChange: () => void;
 }
 
-export const DeptUserTable = ({ users, productRolesBase }: DeptUserTableProps) => {
+export const DeptUserTable = ({
+  users,
+  productRolesBase,
+  deptId,
+  deptName,
+  noDeptUsers,
+  onMemberChange,
+}: DeptUserTableProps) => {
   const [rolesDrawer, setRolesDrawer] = useState<{ open: boolean; userId: number }>({
     open: false,
     userId: 0,
@@ -26,7 +38,12 @@ export const DeptUserTable = ({ users, productRolesBase }: DeptUserTableProps) =
   const userIds = useMemo(() => users.map((u) => u.id), [users]);
   const { userProductInfo } = useUserProductRoles(userIds, productRolesBase, refreshKey);
 
-  const columns: ColumnsType<API.UserItem> = [
+  const userOptions = useMemo(
+    () => noDeptUsers.map((u) => ({ label: `${u.username}(${u.nickname})`, value: u.id })),
+    [noDeptUsers],
+  );
+
+  const columns: ProColumns<API.UserItem>[] = [
     { title: '用户名', dataIndex: 'username', width: 200 },
     { title: '昵称', dataIndex: 'nickname', width: 200 },
     {
@@ -77,11 +94,22 @@ export const DeptUserTable = ({ users, productRolesBase }: DeptUserTableProps) =
     {
       title: '操作',
       key: 'action',
-      width: 160,
+      width: 220,
       render: (_, r) => (
         <span className="flex gap-3">
           <a onClick={() => setRolesDrawer({ open: true, userId: r.id })}>分配角色</a>
           <a onClick={() => setPermDrawer({ open: true, userId: r.id })}>设置权限</a>
+          <Popconfirm
+            title={`确认将「${r.username}」移出「${deptName}」?`}
+            onConfirm={async () => {
+              const res = await fetchUpdateUser({ id: r.id, deptId: 0 });
+              if (!res.success) return;
+              message.success('已移出');
+              onMemberChange();
+            }}
+          >
+            <a className="text-(--ant-color-error)">移出部门</a>
+          </Popconfirm>
         </span>
       ),
     },
@@ -89,19 +117,45 @@ export const DeptUserTable = ({ users, productRolesBase }: DeptUserTableProps) =
 
   return (
     <Spin spinning={productRolesBase.loading}>
-      <Table<API.UserItem>
+      <ProTable<API.UserItem>
         columns={columns}
         dataSource={users}
         rowKey="id"
+        search={false}
+        options={false}
         pagination={{
           defaultPageSize: 20,
           pageSizeOptions: [10, 20, 50, 100],
           showSizeChanger: true,
-          showTotal: (total, range) => `第 ${range[0]}-${range[1]} 条/总共 ${total} 条`,
         }}
         size="small"
         scroll={{ x: 900 }}
         tableLayout="fixed"
+        toolBarRender={() => [
+          <DrawerForm
+            key="add"
+            title="添加成员"
+            trigger={<Button type="primary">添加成员</Button>}
+            onFinish={async (values) => {
+              const res = await fetchUpdateUser({ id: values.userId, deptId });
+              if (!res.success) return false;
+              message.success('添加成功');
+              onMemberChange();
+              return true;
+            }}
+            drawerProps={{ destroyOnClose: true }}
+          >
+            <ProFormSelect
+              name="userId"
+              label="选择用户"
+              options={userOptions}
+              showSearch
+              rules={[{ required: true, message: '请选择用户' }]}
+              placeholder="仅显示未分配部门的用户"
+              fieldProps={{ id: 'dept-member-select' }}
+            />
+          </DrawerForm>,
+        ]}
       />
       <BindRolesDrawer
         {...rolesDrawer}

+ 13 - 4
src/pages/Admin/Dept/hooks/useDeptPage.ts

@@ -29,25 +29,34 @@ export const useDeptPage = () => {
     loadData();
   }, []);
 
-  const selectedDeptIds = useMemo(() => {
+  const selectedDeptNode = useMemo(() => {
     if (!selectedDeptId) return undefined;
-    const node = findDeptNode(treeData, selectedDeptId);
-    if (!node) return undefined;
-    return new Set(collectDeptIds(node));
+    return findDeptNode(treeData, selectedDeptId);
   }, [treeData, selectedDeptId]);
 
+  const selectedDeptIds = useMemo(() => {
+    if (!selectedDeptNode) return undefined;
+    return new Set(collectDeptIds(selectedDeptNode));
+  }, [selectedDeptNode]);
+
   const filteredUsers = useMemo(() => {
     if (!selectedDeptIds) return [];
     return allUsers.filter((u) => selectedDeptIds.has(u.deptId));
   }, [allUsers, selectedDeptIds]);
 
+  const noDeptUsers = useMemo(() => {
+    return allUsers.filter((u) => !u.deptId || u.deptId === 0);
+  }, [allUsers]);
+
   return {
     loading,
     treeData,
     selectedDeptId,
     setSelectedDeptId,
     selectedDeptIds,
+    selectedDeptName: selectedDeptNode?.name,
     filteredUsers,
+    noDeptUsers,
     loadData,
     productRolesBase,
   };

+ 45 - 32
src/pages/Admin/Dept/index.tsx

@@ -16,7 +16,9 @@ export default function DeptPage() {
     selectedDeptId,
     setSelectedDeptId,
     selectedDeptIds,
+    selectedDeptName,
     filteredUsers,
+    noDeptUsers,
     loadData,
     productRolesBase,
   } = useDeptPage();
@@ -62,42 +64,53 @@ export default function DeptPage() {
   return (
     <PageContainer title={intl.formatMessage({ id: 'admin.dept.title' })}>
       <ProCard>
-        <div className="flex items-center justify-between mb-3">
-          <DeptForm
-            mode="add"
-            initialValues={{ parentId: 0 }}
-            onSuccess={loadData}
-            trigger={
-              <Button type="primary">{intl.formatMessage({ id: 'admin.dept.createRoot' })}</Button>
-            }
-          />
-          <Tooltip title="刷新">
-            <span
-              className="cursor-pointer text-(--ant-color-text) hover:text-(--ant-color-primary) transition-colors"
-              onClick={loadData}
-            >
-              <ReloadOutlined style={{ fontSize: 16 }} />
-            </span>
-          </Tooltip>
-        </div>
         <div className="flex gap-4">
-          <div className="w-80 shrink-0 bg-(--ant-color-fill-quaternary) rounded p-2">
-            <Spin spinning={loading}>
-              {treeData.length > 0 && (
-                <Tree
-                  className="bg-transparent!"
-                  treeData={renderNodes(treeData)}
-                  defaultExpandAll
-                  showLine
-                  selectedKeys={selectedDeptId ? [selectedDeptId] : []}
-                  onSelect={(keys) => setSelectedDeptId(keys[0] as number | undefined)}
-                />
-              )}
-            </Spin>
+          <div className="flex flex-col">
+            <div className="flex gap-2 items-center py-4">
+              <DeptForm
+                mode="add"
+                initialValues={{ parentId: 0 }}
+                onSuccess={loadData}
+                trigger={
+                  <Button type="primary">
+                    {intl.formatMessage({ id: 'admin.dept.createRoot' })}
+                  </Button>
+                }
+              />
+              <Tooltip title="刷新">
+                <span
+                  className="cursor-pointer text-(--ant-color-text) hover:text-(--ant-color-primary) transition-colors"
+                  onClick={loadData}
+                >
+                  <ReloadOutlined style={{ fontSize: 16 }} />
+                </span>
+              </Tooltip>
+            </div>
+            <div className="w-80 shrink-0 bg-(--ant-color-fill-quaternary) rounded-lg p-2">
+              <Spin spinning={loading}>
+                {treeData.length > 0 && (
+                  <Tree
+                    className="bg-transparent!"
+                    treeData={renderNodes(treeData)}
+                    defaultExpandAll
+                    showLine
+                    selectedKeys={selectedDeptId ? [selectedDeptId] : []}
+                    onSelect={(keys) => setSelectedDeptId(keys[0] as number | undefined)}
+                  />
+                )}
+              </Spin>
+            </div>
           </div>
           <div className="flex-1 min-w-0">
             {selectedDeptIds ? (
-              <DeptUserTable users={filteredUsers} productRolesBase={productRolesBase} />
+              <DeptUserTable
+                users={filteredUsers}
+                productRolesBase={productRolesBase}
+                deptId={selectedDeptId!}
+                deptName={selectedDeptName ?? ''}
+                noDeptUsers={noDeptUsers}
+                onMemberChange={loadData}
+              />
             ) : (
               <Empty description="请在左侧选择部门" className="py-16" />
             )}

+ 9 - 3
src/pages/Admin/User/components/Form.tsx

@@ -31,9 +31,15 @@ export const UserForm = ({ mode, initialValues, trigger, onSuccess }: UserFormPr
 
   const formValues =
     mode === 'copy'
-      ? { ...initialValues, id: undefined, username: undefined, password: undefined }
+      ? {
+          ...initialValues,
+          id: undefined,
+          username: undefined,
+          password: undefined,
+          deptId: initialValues?.deptId || undefined,
+        }
       : mode === 'edit'
-        ? initialValues
+        ? { ...initialValues, deptId: initialValues?.deptId || undefined }
         : undefined;
 
   const handleFinish = async (values: any) => {
@@ -44,7 +50,7 @@ export const UserForm = ({ mode, initialValues, trigger, onSuccess }: UserFormPr
         email: values.email,
         phone: values.phone,
         remark: values.remark,
-        deptId: values.deptId,
+        deptId: values.deptId ?? 0,
       });
       if (!res.success) return false;
     } else {