Просмотр исходного кода

feat: implement product detail tabs (role/perm/member)

BaiLuoYan 3 дней назад
Родитель
Сommit
2bc4c54460

+ 3 - 5
src/pages/Admin/Product/Detail/index.tsx

@@ -4,11 +4,9 @@ import { useNavigate, useParams } from '@umijs/max';
 import { Spin, Tabs, Tag } from 'antd';
 import { useEffect, useState } from 'react';
 import { ProductForm } from '../components/Form';
-
-// Tab 内容由计划六实现,此处先用占位
-const RoleTab = ({ productCode }: { productCode: string }) => <div>角色 Tab - {productCode}</div>;
-const PermTab = ({ productCode }: { productCode: string }) => <div>权限 Tab - {productCode}</div>;
-const MemberTab = ({ productCode }: { productCode: string }) => <div>成员 Tab - {productCode}</div>;
+import { MemberTab } from './tabs/MemberTab';
+import { PermTab } from './tabs/PermTab';
+import { RoleTab } from './tabs/RoleTab';
 
 export default function ProductDetailPage() {
   const { id } = useParams<{ id: string }>();

+ 144 - 0
src/pages/Admin/Product/Detail/tabs/MemberTab.tsx

@@ -0,0 +1,144 @@
+import { MEMBER_TYPE_LABELS, MEMBER_TYPE_OPTIONS, STATUS_OPTIONS } from '@/defines';
+import {
+  fetchAddMember,
+  fetchMemberList,
+  fetchRemoveMember,
+  fetchUpdateMember,
+} from '@/services/member';
+import { fetchUserList } from '@/services/user';
+import {
+  ActionType,
+  DrawerForm,
+  ProColumns,
+  ProFormSelect,
+  ProTable,
+} from '@ant-design/pro-components';
+import { Button, Popconfirm, Tag, message } from 'antd';
+import { useEffect, useRef, useState } from 'react';
+
+interface MemberTabProps {
+  productCode: string;
+}
+
+export const MemberTab = ({ productCode }: MemberTabProps) => {
+  const actionRef = useRef<ActionType>();
+  const [userOptions, setUserOptions] = useState<{ label: string; value: number }[]>([]);
+
+  const loadUsers = async () => {
+    const res = await fetchUserList({ pageSize: 50 });
+    setUserOptions(
+      (res.data?.list ?? []).map((u) => ({ label: `${u.username}(${u.nickname})`, value: u.id })),
+    );
+  };
+
+  useEffect(() => {
+    loadUsers();
+  }, []);
+
+  const columns: ProColumns<API.MemberItem>[] = [
+    { title: '用户名', dataIndex: 'username' },
+    { title: '昵称', dataIndex: 'nickname' },
+    {
+      title: '成员类型',
+      dataIndex: 'memberType',
+      render: (_, r) => <Tag>{MEMBER_TYPE_LABELS[r.memberType] ?? r.memberType}</Tag>,
+    },
+    {
+      title: '状态',
+      dataIndex: 'status',
+      render: (_, r) => (
+        <Tag color={r.status === 1 ? 'success' : 'default'}>{r.status === 1 ? '启用' : '禁用'}</Tag>
+      ),
+    },
+    {
+      title: '操作',
+      valueType: 'option',
+      render: (_, r) => [
+        <DrawerForm
+          key="edit"
+          title="编辑成员"
+          trigger={<a>编辑</a>}
+          initialValues={{ memberType: r.memberType, status: r.status }}
+          onFinish={async (values) => {
+            await fetchUpdateMember({
+              id: r.id,
+              memberType: values.memberType,
+              status: values.status,
+            });
+            actionRef.current?.reload();
+            return true;
+          }}
+          drawerProps={{ destroyOnClose: true }}
+        >
+          <ProFormSelect
+            name="memberType"
+            label="成员类型"
+            options={MEMBER_TYPE_OPTIONS}
+            rules={[{ required: true }]}
+          />
+          <ProFormSelect
+            name="status"
+            label="状态"
+            options={STATUS_OPTIONS}
+            rules={[{ required: true }]}
+          />
+        </DrawerForm>,
+        <Popconfirm
+          key="remove"
+          title="确认移除该成员?"
+          onConfirm={async () => {
+            await fetchRemoveMember({ id: r.id });
+            message.success('已移除');
+            actionRef.current?.reload();
+          }}
+        >
+          <a className="text-red-500">移除</a>
+        </Popconfirm>,
+      ],
+    },
+  ];
+
+  return (
+    <ProTable<API.MemberItem>
+      actionRef={actionRef}
+      columns={columns}
+      rowKey="id"
+      request={async ({ current, pageSize }) => {
+        const res = await fetchMemberList({ productCode, page: current, pageSize });
+        return { data: res.data?.list ?? [], total: res.data?.total ?? 0, success: true };
+      }}
+      toolBarRender={() => [
+        <DrawerForm
+          key="add"
+          title="添加成员"
+          trigger={<Button type="primary">添加成员</Button>}
+          onFinish={async (values) => {
+            await fetchAddMember({
+              productCode,
+              userId: values.userId,
+              memberType: values.memberType,
+            });
+            actionRef.current?.reload();
+            return true;
+          }}
+          drawerProps={{ destroyOnClose: true }}
+        >
+          <ProFormSelect
+            name="userId"
+            label="选择用户"
+            options={userOptions}
+            showSearch
+            fieldProps={{ onSearch: loadUsers, filterOption: false }}
+            rules={[{ required: true }]}
+          />
+          <ProFormSelect
+            name="memberType"
+            label="成员类型"
+            options={MEMBER_TYPE_OPTIONS}
+            rules={[{ required: true }]}
+          />
+        </DrawerForm>,
+      ]}
+    />
+  );
+};

+ 29 - 0
src/pages/Admin/Product/Detail/tabs/PermTab.tsx

@@ -0,0 +1,29 @@
+import { PermTree } from '@/pages/Admin/_shared/PermTree';
+import { fetchPermList } from '@/services/perm';
+import { Spin, Typography } from 'antd';
+import { useEffect, useState } from 'react';
+
+interface PermTabProps {
+  productCode: string;
+}
+
+export const PermTab = ({ productCode }: PermTabProps) => {
+  const [loading, setLoading] = useState(false);
+  const [perms, setPerms] = useState<API.PermItem[]>([]);
+
+  useEffect(() => {
+    setLoading(true);
+    fetchPermList({ productCode, pageSize: 9999 })
+      .then((res) => setPerms(res.data?.list ?? []))
+      .finally(() => setLoading(false));
+  }, [productCode]);
+
+  return (
+    <Spin spinning={loading}>
+      <Typography.Text type="secondary" className="block mb-3">
+        共 {perms.length} 条权限(由产品服务端同步,只读)
+      </Typography.Text>
+      <PermTree mode="view" allPerms={perms} checkedPermIds={perms.map((p) => p.id)} />
+    </Spin>
+  );
+};

+ 64 - 0
src/pages/Admin/Product/Detail/tabs/RolePermDrawer.tsx

@@ -0,0 +1,64 @@
+import { PermTree } from '@/pages/Admin/_shared/PermTree';
+import { fetchPermList } from '@/services/perm';
+import { fetchBindRolePerms, fetchRoleDetail } from '@/services/role';
+import { Button, Drawer, Spin, message } from 'antd';
+import { useEffect, useState } from 'react';
+
+interface RolePermDrawerProps {
+  roleId: number;
+  productCode: string;
+  open: boolean;
+  onClose: () => void;
+}
+
+export const RolePermDrawer = ({ roleId, productCode, open, onClose }: RolePermDrawerProps) => {
+  const [loading, setLoading] = useState(false);
+  const [saving, setSaving] = useState(false);
+  const [allPerms, setAllPerms] = useState<API.PermItem[]>([]);
+  const [checkedIds, setCheckedIds] = useState<number[]>([]);
+
+  useEffect(() => {
+    if (!open) return;
+    setLoading(true);
+    Promise.all([fetchRoleDetail({ id: roleId }), fetchPermList({ productCode, pageSize: 9999 })])
+      .then(([roleRes, permRes]) => {
+        setAllPerms(permRes.data?.list ?? []);
+        setCheckedIds(roleRes.data?.permIds ?? []);
+      })
+      .finally(() => setLoading(false));
+  }, [open, roleId, productCode]);
+
+  const handleSave = async () => {
+    setSaving(true);
+    try {
+      await fetchBindRolePerms({ roleId, permIds: checkedIds });
+      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>
+  );
+};

+ 127 - 0
src/pages/Admin/Product/Detail/tabs/RoleTab.tsx

@@ -0,0 +1,127 @@
+import { fetchCreateRole, fetchDeleteRole, fetchRoleList, fetchUpdateRole } from '@/services/role';
+import {
+  ActionType,
+  DrawerForm,
+  ProColumns,
+  ProFormDigit,
+  ProFormText,
+  ProTable,
+} from '@ant-design/pro-components';
+import { Button, Popconfirm, Tag, message } from 'antd';
+import { useRef, useState } from 'react';
+import { RolePermDrawer } from './RolePermDrawer';
+
+interface RoleTabProps {
+  productCode: string;
+}
+
+export const RoleTab = ({ productCode }: RoleTabProps) => {
+  const actionRef = useRef<ActionType>();
+  const [permDrawer, setPermDrawer] = useState<{ open: boolean; roleId: number }>({
+    open: false,
+    roleId: 0,
+  });
+
+  const RoleForm = ({
+    mode,
+    record,
+    trigger,
+  }: {
+    mode: EditorFormMode;
+    record?: API.RoleItem;
+    trigger: React.ReactElement;
+  }) => (
+    <DrawerForm
+      title={mode === 'edit' ? '编辑角色' : mode === 'copy' ? '复制角色' : '新建角色'}
+      trigger={trigger}
+      initialValues={
+        mode === 'copy' ? { ...record, id: undefined } : mode === 'edit' ? record : undefined
+      }
+      onFinish={async (values) => {
+        if (mode === 'edit' && record?.id) {
+          await fetchUpdateRole({
+            id: record.id,
+            name: values.name,
+            permsLevel: values.permsLevel,
+            remark: values.remark,
+          });
+        } else {
+          await fetchCreateRole({
+            productCode,
+            name: values.name,
+            permsLevel: values.permsLevel,
+            remark: values.remark,
+          });
+        }
+        actionRef.current?.reload();
+        return true;
+      }}
+      drawerProps={{ destroyOnClose: true }}
+    >
+      <ProFormText name="name" label="角色名称" rules={[{ required: true }]} />
+      <ProFormDigit
+        name="permsLevel"
+        label="权限级别"
+        rules={[{ required: true }]}
+        fieldProps={{ precision: 0 }}
+      />
+      <ProFormText name="remark" label="备注" />
+    </DrawerForm>
+  );
+
+  const columns: ProColumns<API.RoleItem>[] = [
+    { title: '名称', dataIndex: 'name' },
+    { title: '权限级别', dataIndex: 'permsLevel' },
+    {
+      title: '状态',
+      dataIndex: 'status',
+      render: (_, r) => (
+        <Tag color={r.status === 1 ? 'success' : 'default'}>{r.status === 1 ? '启用' : '禁用'}</Tag>
+      ),
+    },
+    {
+      title: '操作',
+      valueType: 'option',
+      render: (_, r) => [
+        <RoleForm key="edit" mode="edit" record={r} trigger={<a>编辑</a>} />,
+        <RoleForm key="copy" mode="copy" record={r} trigger={<a>复制</a>} />,
+        <a key="perm" onClick={() => setPermDrawer({ open: true, roleId: r.id })}>
+          绑定权限
+        </a>,
+        <Popconfirm
+          key="del"
+          title="确认删除?"
+          onConfirm={async () => {
+            await fetchDeleteRole({ id: r.id });
+            message.success('删除成功');
+            actionRef.current?.reload();
+          }}
+        >
+          <a className="text-red-500">删除</a>
+        </Popconfirm>,
+      ],
+    },
+  ];
+
+  return (
+    <>
+      <ProTable<API.RoleItem>
+        actionRef={actionRef}
+        columns={columns}
+        rowKey="id"
+        request={async ({ current, pageSize }) => {
+          const res = await fetchRoleList({ productCode, page: current, pageSize });
+          return { data: res.data?.list ?? [], total: res.data?.total ?? 0, success: true };
+        }}
+        toolBarRender={() => [
+          <RoleForm key="create" mode="add" trigger={<Button type="primary">新建角色</Button>} />,
+        ]}
+      />
+      <RolePermDrawer
+        {...permDrawer}
+        productCode={productCode}
+        onClose={() => setPermDrawer((p) => ({ ...p, open: false }))}
+      />
+    </>
+  );
+};