فهرست منبع

fix: 修复操作失败时也弹出成功提示的bug

BaiLuoYan 2 روز پیش
والد
کامیت
c3b0598d47

+ 6 - 0
src/defines/admin.ts

@@ -19,3 +19,9 @@ export const MEMBER_TYPE_LABELS: Record<string, string> = {
   DEVELOPER: '开发者',
   MEMBER: '成员',
 };
+
+export const MEMBER_TYPE_COLORS: Record<string, string> = {
+  ADMIN: 'red',
+  DEVELOPER: 'green',
+  MEMBER: 'blue',
+};

+ 4 - 2
src/pages/Admin/Dept/components/Form.tsx

@@ -14,9 +14,11 @@ export const DeptForm = ({ mode, initialValues, onSuccess, trigger }: DeptFormPr
 
   const handleFinish = async (values: API.CreateDeptReq) => {
     if (mode === 'edit' && initialValues?.id) {
-      await fetchUpdateDept({ ...values, id: initialValues.id } as API.UpdateDeptReq);
+      const res = await fetchUpdateDept({ ...values, id: initialValues.id } as API.UpdateDeptReq);
+      if (!res.success) return false;
     } else {
-      await fetchCreateDept(values);
+      const res = await fetchCreateDept(values);
+      if (!res.success) return false;
     }
     onSuccess();
     return true;

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

@@ -1,15 +1,17 @@
+import { MEMBER_TYPE_COLORS, MEMBER_TYPE_LABELS } from '@/defines';
 import { BindRolesDrawer } from '@/pages/Admin/_shared/BindRolesDrawer';
 import { UserPermDrawer } from '@/pages/Admin/_shared/UserPermDrawer';
-import { useUserProductRoles } from '@/pages/Admin/_shared/useUserProductRoles';
+import { ProductRolesBase, useUserProductRoles } from '@/pages/Admin/_shared/useUserProductRoles';
 import { Spin, Table, Tag } from 'antd';
 import type { ColumnsType } from 'antd/es/table';
 import { useMemo, useState } from 'react';
 
 interface DeptUserTableProps {
   users: API.UserItem[];
+  productRolesBase: ProductRolesBase;
 }
 
-export const DeptUserTable = ({ users }: DeptUserTableProps) => {
+export const DeptUserTable = ({ users, productRolesBase }: DeptUserTableProps) => {
   const [rolesDrawer, setRolesDrawer] = useState<{ open: boolean; userId: number }>({
     open: false,
     userId: 0,
@@ -21,7 +23,7 @@ export const DeptUserTable = ({ users }: DeptUserTableProps) => {
   const [refreshKey, setRefreshKey] = useState(0);
 
   const userIds = useMemo(() => users.map((u) => u.id), [users]);
-  const { userProductInfo, loading: infoLoading } = useUserProductRoles(userIds, refreshKey);
+  const { userProductInfo } = useUserProductRoles(userIds, productRolesBase, refreshKey);
 
   const columns: ColumnsType<API.UserItem> = [
     { title: '用户名', dataIndex: 'username', width: 200 },
@@ -39,14 +41,14 @@ export const DeptUserTable = ({ users }: DeptUserTableProps) => {
                 key={p.productName}
                 className="flex flex-row gap-1 rounded border border-dashed border-gray-400 bg-gray-50 px-2 py-2"
               >
-                <Tag color="blue" className="mr-0 self-center">
+                <Tag color="purple" className="mr-0 self-center">
                   {p.productName}
                 </Tag>
                 <Tag
-                  color={p.memberType === 'ADMIN' ? 'red' : 'green'}
+                  color={MEMBER_TYPE_COLORS[p.memberType ?? ''] ?? 'default'}
                   className="mr-0 self-center"
                 >
-                  {p.memberType === 'ADMIN' ? '管理员' : '开发者'}
+                  {MEMBER_TYPE_LABELS[p.memberType ?? ''] ?? p.memberType}
                 </Tag>
                 <div className="flex flex-row flex-wrap gap-1 items-center">
                   {p.roleNames.length > 0 ? (
@@ -79,7 +81,7 @@ export const DeptUserTable = ({ users }: DeptUserTableProps) => {
   ];
 
   return (
-    <Spin spinning={infoLoading}>
+    <Spin spinning={productRolesBase.loading}>
       <Table<API.UserItem>
         columns={columns}
         dataSource={users}

+ 3 - 0
src/pages/Admin/Dept/hooks/useDeptPage.ts

@@ -1,3 +1,4 @@
+import { useProductRolesBase } from '@/pages/Admin/_shared/useUserProductRoles';
 import { fetchDeptTree } from '@/services/dept';
 import { fetchUserList } from '@/services/user';
 import { useEffect, useMemo, useState } from 'react';
@@ -8,6 +9,7 @@ export const useDeptPage = () => {
   const [treeData, setTreeData] = useState<API.DeptItem[]>([]);
   const [allUsers, setAllUsers] = useState<API.UserItem[]>([]);
   const [selectedDeptId, setSelectedDeptId] = useState<number | undefined>();
+  const productRolesBase = useProductRolesBase();
 
   const loadData = async () => {
     setLoading(true);
@@ -47,5 +49,6 @@ export const useDeptPage = () => {
     selectedDeptIds,
     filteredUsers,
     loadData,
+    productRolesBase,
   };
 };

+ 6 - 6
src/pages/Admin/Dept/index.tsx

@@ -17,15 +17,15 @@ export default function DeptPage() {
     selectedDeptIds,
     filteredUsers,
     loadData,
+    productRolesBase,
   } = useDeptPage();
 
   const handleDelete = async (id: number) => {
     const res = await fetchDeleteDept({ id });
-    if (res.success) {
-      message.success('删除成功');
-      if (selectedDeptId === id) setSelectedDeptId(undefined);
-      loadData();
-    }
+    if (!res.success) return;
+    message.success('删除成功');
+    if (selectedDeptId === id) setSelectedDeptId(undefined);
+    loadData();
   };
 
   const renderTitle = (node: API.DeptItem) => (
@@ -84,7 +84,7 @@ export default function DeptPage() {
         </div>
         <div className="flex-1 min-w-0">
           {selectedDeptIds ? (
-            <DeptUserTable users={filteredUsers} />
+            <DeptUserTable users={filteredUsers} productRolesBase={productRolesBase} />
           ) : (
             <Empty description="请在左侧选择部门" className="py-16" />
           )}

+ 143 - 56
src/pages/Admin/Product/Detail/tabs/MemberTab.tsx

@@ -1,11 +1,19 @@
-import { MEMBER_TYPE_LABELS, MEMBER_TYPE_OPTIONS, STATUS_OPTIONS } from '@/defines';
+import {
+  MEMBER_TYPE_COLORS,
+  MEMBER_TYPE_LABELS,
+  MEMBER_TYPE_OPTIONS,
+  STATUS_OPTIONS,
+} from '@/defines';
+import { BindRolesDrawer } from '@/pages/Admin/_shared/BindRolesDrawer';
+import { UserPermDrawer } from '@/pages/Admin/_shared/UserPermDrawer';
 import {
   fetchAddMember,
   fetchMemberList,
   fetchRemoveMember,
   fetchUpdateMember,
 } from '@/services/member';
-import { fetchUserList } from '@/services/user';
+import { fetchRoleList } from '@/services/role';
+import { fetchUserDetail, fetchUserList } from '@/services/user';
 import {
   ActionType,
   DrawerForm,
@@ -23,6 +31,18 @@ interface MemberTabProps {
 export const MemberTab = ({ productCode }: MemberTabProps) => {
   const actionRef = useRef<ActionType>();
   const [userOptions, setUserOptions] = useState<{ label: string; value: number }[]>([]);
+  const [roleMap, setRoleMap] = useState<Map<number, string>>(new Map());
+  const [userRoleIds, setUserRoleIds] = useState<Map<number, number[]>>(new Map());
+  const [pageMembers, setPageMembers] = useState<API.MemberItem[]>([]);
+  const [rolesDrawer, setRolesDrawer] = useState<{ open: boolean; userId: number }>({
+    open: false,
+    userId: 0,
+  });
+  const [permDrawer, setPermDrawer] = useState<{ open: boolean; userId: number }>({
+    open: false,
+    userId: 0,
+  });
+  const [refreshKey, setRefreshKey] = useState(0);
 
   const loadUsers = async () => {
     const res = await fetchUserList({ pageSize: 9999 });
@@ -35,8 +55,30 @@ export const MemberTab = ({ productCode }: MemberTabProps) => {
   };
 
   useEffect(() => {
-    loadUsers();
-  }, []);
+    fetchRoleList({ productCode, pageSize: 9999 }).then((res) => {
+      const map = new Map<number, string>();
+      (res.data ?? []).forEach((r: API.RoleItem) => map.set(r.id, r.name));
+      setRoleMap(map);
+    });
+  }, [productCode]);
+
+  useEffect(() => {
+    if (pageMembers.length === 0) {
+      setUserRoleIds(new Map());
+      return;
+    }
+    const load = async () => {
+      const details = await Promise.all(
+        pageMembers.map((m) => fetchUserDetail({ id: m.userId, productCode })),
+      );
+      const map = new Map<number, number[]>();
+      details.forEach((res, i) => {
+        map.set(pageMembers[i].userId, res.data?.roleIds ?? []);
+      });
+      setUserRoleIds(map);
+    };
+    load();
+  }, [pageMembers, refreshKey]);
 
   const columns: ProColumns<API.MemberItem>[] = [
     { title: '用户名', dataIndex: 'username', width: 200 },
@@ -45,7 +87,29 @@ export const MemberTab = ({ productCode }: MemberTabProps) => {
       title: '成员类型',
       dataIndex: 'memberType',
       width: 100,
-      render: (_, r) => <Tag>{MEMBER_TYPE_LABELS[r.memberType] ?? r.memberType}</Tag>,
+      render: (_, r) => (
+        <Tag color={MEMBER_TYPE_COLORS[r.memberType] ?? 'default'}>
+          {MEMBER_TYPE_LABELS[r.memberType] ?? r.memberType}
+        </Tag>
+      ),
+    },
+    {
+      title: '角色',
+      key: 'roles',
+      render: (_, r) => {
+        const roleIds = userRoleIds.get(r.userId) ?? [];
+        const names = roleIds.map((id) => roleMap.get(id)).filter(Boolean) as string[];
+        if (names.length === 0) return <span className="text-gray-400 text-xs">无</span>;
+        return (
+          <div className="flex flex-row flex-wrap gap-1">
+            {names.map((name) => (
+              <Tag key={name} className="mr-0">
+                {name}
+              </Tag>
+            ))}
+          </div>
+        );
+      },
     },
     {
       title: '状态',
@@ -58,7 +122,7 @@ export const MemberTab = ({ productCode }: MemberTabProps) => {
     {
       title: '操作',
       valueType: 'option',
-      width: 120,
+      width: 220,
       render: (_, r) => [
         <DrawerForm
           key="edit"
@@ -66,11 +130,12 @@ export const MemberTab = ({ productCode }: MemberTabProps) => {
           trigger={<a>编辑</a>}
           initialValues={{ memberType: r.memberType, status: r.status }}
           onFinish={async (values) => {
-            await fetchUpdateMember({
+            const res = await fetchUpdateMember({
               id: r.id,
               memberType: values.memberType,
               status: values.status,
             });
+            if (!res.success) return false;
             actionRef.current?.reload();
             return true;
           }}
@@ -89,11 +154,18 @@ export const MemberTab = ({ productCode }: MemberTabProps) => {
             rules={[{ required: true }]}
           />
         </DrawerForm>,
+        <a key="roles" onClick={() => setRolesDrawer({ open: true, userId: r.userId })}>
+          分配角色
+        </a>,
+        <a key="perms" onClick={() => setPermDrawer({ open: true, userId: r.userId })}>
+          设置权限
+        </a>,
         <Popconfirm
           key="remove"
           title="确认移除该成员?"
           onConfirm={async () => {
-            await fetchRemoveMember({ id: r.id });
+            const res = await fetchRemoveMember({ id: r.id });
+            if (!res.success) return;
             message.success('已移除');
             actionRef.current?.reload();
           }}
@@ -105,53 +177,68 @@ export const MemberTab = ({ productCode }: MemberTabProps) => {
   ];
 
   return (
-    <ProTable<API.MemberItem>
-      actionRef={actionRef}
-      columns={columns}
-      rowKey="id"
-      search={false}
-      request={async (params, sorter, filter) => {
-        return fetchMemberList({ ...params, productCode }, sorter, filter);
-      }}
-      scroll={{ x: 700 }}
-      tableLayout="fixed"
-      pagination={{
-        defaultPageSize: 20,
-        pageSizeOptions: [10, 20, 50, 100],
-        showSizeChanger: 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>,
-      ]}
-    />
+    <>
+      <ProTable<API.MemberItem>
+        actionRef={actionRef}
+        columns={columns}
+        rowKey="id"
+        search={false}
+        request={async (params, sorter, filter) => {
+          return fetchMemberList({ ...params, productCode }, sorter, filter);
+        }}
+        onDataSourceChange={setPageMembers}
+        scroll={{ x: 1000 }}
+        tableLayout="fixed"
+        pagination={{
+          defaultPageSize: 20,
+          pageSizeOptions: [10, 20, 50, 100],
+          showSizeChanger: true,
+        }}
+        toolBarRender={() => [
+          <DrawerForm
+            key="add"
+            title="添加成员"
+            trigger={<Button type="primary">添加成员</Button>}
+            onFinish={async (values) => {
+              const res = await fetchAddMember({
+                productCode,
+                userId: values.userId,
+                memberType: values.memberType,
+              });
+              if (!res.success) return false;
+              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>,
+        ]}
+      />
+      <BindRolesDrawer
+        {...rolesDrawer}
+        onClose={() => setRolesDrawer((p) => ({ ...p, open: false }))}
+        onSuccess={() => setRefreshKey((k) => k + 1)}
+        defaultProductCode={productCode}
+      />
+      <UserPermDrawer
+        {...permDrawer}
+        onClose={() => setPermDrawer((p) => ({ ...p, open: false }))}
+        defaultProductCode={productCode}
+      />
+    </>
   );
 };

+ 9 - 5
src/pages/Admin/Product/Detail/tabs/RolePermDrawer.tsx

@@ -31,7 +31,8 @@ export const RolePermDrawer = ({ roleId, productCode, open, onClose }: RolePermD
   const handleSave = async () => {
     setSaving(true);
     try {
-      await fetchBindRolePerms({ roleId, permIds: checkedIds });
+      const res = await fetchBindRolePerms({ roleId, permIds: checkedIds });
+      if (!res.success) return;
       message.success('保存成功');
       onClose();
     } finally {
@@ -45,10 +46,13 @@ export const RolePermDrawer = ({ roleId, productCode, open, onClose }: RolePermD
       open={open}
       onClose={onClose}
       width={560}
-      extra={
-        <Button type="primary" loading={saving} onClick={handleSave}>
-          保存
-        </Button>
+      footer={
+        <div className="flex justify-end gap-2">
+          <Button onClick={onClose}>取消</Button>
+          <Button type="primary" loading={saving} onClick={handleSave}>
+            确定
+          </Button>
+        </div>
       }
     >
       <Spin spinning={loading}>

+ 8 - 4
src/pages/Admin/Product/Detail/tabs/RoleTab.tsx

@@ -28,19 +28,21 @@ const RoleForm = ({ mode, record, trigger, productCode, onSuccess }: RoleFormPro
     }
     onFinish={async (values) => {
       if (mode === 'edit' && record?.id) {
-        await fetchUpdateRole({
+        const res = await fetchUpdateRole({
           id: record.id,
           name: values.name,
           permsLevel: values.permsLevel,
           remark: values.remark,
         });
+        if (!res.success) return false;
       } else {
-        await fetchCreateRole({
+        const res = await fetchCreateRole({
           productCode,
           name: values.name,
           permsLevel: values.permsLevel,
           remark: values.remark,
         });
+        if (!res.success) return false;
       }
       onSuccess();
       return true;
@@ -77,6 +79,7 @@ export const RoleTab = ({ productCode }: RoleTabProps) => {
   const columns: ProColumns<API.RoleItem>[] = [
     { title: '名称', dataIndex: 'name', width: 160 },
     { title: '权限级别', dataIndex: 'permsLevel', width: 100 },
+    { title: '备注', dataIndex: 'remark' },
     {
       title: '状态',
       dataIndex: 'status',
@@ -88,7 +91,7 @@ export const RoleTab = ({ productCode }: RoleTabProps) => {
     {
       title: '操作',
       valueType: 'option',
-      width: 160,
+      width: 190,
       render: (_, r) => [
         <RoleForm
           key="edit"
@@ -113,7 +116,8 @@ export const RoleTab = ({ productCode }: RoleTabProps) => {
           key="del"
           title="确认删除?"
           onConfirm={async () => {
-            await fetchDeleteRole({ id: r.id });
+            const res = await fetchDeleteRole({ id: r.id });
+            if (!res.success) return;
             message.success('删除成功');
             actionRef.current?.reload();
           }}

+ 11 - 2
src/pages/Admin/Product/components/Form.tsx

@@ -20,18 +20,26 @@ const buildDeptTreeData = (items: API.DeptItem[]): any[] =>
 
 export const ProductForm = ({ mode, initialValues, trigger, onSuccess }: ProductFormProps) => {
   const [deptTree, setDeptTree] = useState<any[]>([]);
+  const [open, setOpen] = useState(false);
   const title = mode === 'edit' ? '编辑产品' : '新建产品';
 
   useEffect(() => {
+    if (!open || mode === 'edit' || deptTree.length > 0) return;
     fetchDeptTree().then((res) => setDeptTree(buildDeptTreeData(res.data ?? [])));
-  }, []);
+  }, [open]);
 
   const handleFinish = async (values: any) => {
     if (mode === 'edit' && initialValues?.id) {
-      await fetchUpdateProduct({ id: initialValues.id, name: values.name, remark: values.remark });
+      const res = await fetchUpdateProduct({
+        id: initialValues.id,
+        name: values.name,
+        remark: values.remark,
+      });
+      if (!res.success) return false;
       onSuccess();
     } else {
       const res = await fetchCreateProduct(values);
+      if (!res.success) return false;
       onSuccess(res.data?.credentialsTicket);
     }
     return true;
@@ -43,6 +51,7 @@ export const ProductForm = ({ mode, initialValues, trigger, onSuccess }: Product
       trigger={trigger}
       initialValues={mode === 'edit' ? initialValues : undefined}
       onFinish={handleFinish}
+      onOpenChange={setOpen}
       drawerProps={{ destroyOnClose: true }}
     >
       <ProFormText name="name" label="产品名称" rules={[{ required: true }]} />

+ 4 - 2
src/pages/Admin/Product/index.tsx

@@ -22,11 +22,12 @@ export default function ProductPage() {
   };
 
   const handleToggleStatus = async (record: API.ProductItem) => {
-    await fetchUpdateProduct({
+    const res = await fetchUpdateProduct({
       id: record.id,
       name: record.name,
       status: record.status === 1 ? 0 : 1,
     });
+    if (!res.success) return;
     message.success('操作成功');
     actionRef.current?.reload();
   };
@@ -39,7 +40,8 @@ export default function ProductPage() {
       render: (_, r) => <a onClick={() => navigate(`/admin/products/${r.id}`)}>{r.name}</a>,
     },
     { title: 'Code', dataIndex: 'code', width: 160, copyable: true },
-    { title: 'App Key', dataIndex: 'appKey', copyable: true },
+    { title: 'App Key', dataIndex: 'appKey', width: 300, copyable: true },
+    { title: '备注', dataIndex: 'remark' },
     {
       title: '状态',
       dataIndex: 'status',

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

@@ -20,10 +20,12 @@ const buildDeptOptions = (items: API.DeptItem[]): any[] =>
 
 export const UserForm = ({ mode, initialValues, trigger, onSuccess }: UserFormProps) => {
   const [deptTree, setDeptTree] = useState<any[]>([]);
+  const [open, setOpen] = useState(false);
 
   useEffect(() => {
+    if (!open || deptTree.length > 0) return;
     fetchDeptTree().then((res) => setDeptTree(buildDeptOptions(res.data ?? [])));
-  }, []);
+  }, [open]);
 
   const title = { add: '新建用户', edit: '编辑用户', copy: '复制用户' }[mode];
 
@@ -36,7 +38,7 @@ export const UserForm = ({ mode, initialValues, trigger, onSuccess }: UserFormPr
 
   const handleFinish = async (values: any) => {
     if (mode === 'edit' && initialValues?.id) {
-      await fetchUpdateUser({
+      const res = await fetchUpdateUser({
         id: initialValues.id,
         nickname: values.nickname,
         email: values.email,
@@ -44,8 +46,10 @@ export const UserForm = ({ mode, initialValues, trigger, onSuccess }: UserFormPr
         remark: values.remark,
         deptId: values.deptId,
       });
+      if (!res.success) return false;
     } else {
-      await fetchCreateUser(values);
+      const res = await fetchCreateUser(values);
+      if (!res.success) return false;
     }
     onSuccess();
     return true;
@@ -57,6 +61,7 @@ export const UserForm = ({ mode, initialValues, trigger, onSuccess }: UserFormPr
       trigger={trigger}
       initialValues={formValues}
       onFinish={handleFinish}
+      onOpenChange={setOpen}
       drawerProps={{ destroyOnClose: true }}
     >
       <ProFormText

+ 12 - 6
src/pages/Admin/User/index.tsx

@@ -1,6 +1,10 @@
+import { MEMBER_TYPE_COLORS, MEMBER_TYPE_LABELS } from '@/defines';
 import { BindRolesDrawer } from '@/pages/Admin/_shared/BindRolesDrawer';
 import { UserPermDrawer } from '@/pages/Admin/_shared/UserPermDrawer';
-import { useUserProductRoles } from '@/pages/Admin/_shared/useUserProductRoles';
+import {
+  useProductRolesBase,
+  useUserProductRoles,
+} from '@/pages/Admin/_shared/useUserProductRoles';
 import { fetchUpdateUserStatus, fetchUserList } from '@/services/user';
 import { ActionType, PageContainer, ProColumns, ProTable } from '@ant-design/pro-components';
 import { useIntl } from '@umijs/max';
@@ -23,10 +27,12 @@ export default function UserPage() {
   const [refreshKey, setRefreshKey] = useState(0);
 
   const userIds = useMemo(() => pageUsers.map((u) => u.id), [pageUsers]);
-  const { userProductInfo } = useUserProductRoles(userIds, refreshKey);
+  const productRolesBase = useProductRolesBase();
+  const { userProductInfo } = useUserProductRoles(userIds, productRolesBase, refreshKey);
 
   const handleToggleStatus = async (r: API.UserItem) => {
-    await fetchUpdateUserStatus({ id: r.id, status: r.status === 1 ? 0 : 1 });
+    const res = await fetchUpdateUserStatus({ id: r.id, status: r.status === 1 ? 0 : 1 });
+    if (!res.success) return;
     message.success('操作成功');
     actionRef.current?.reload();
   };
@@ -55,14 +61,14 @@ export default function UserPage() {
                 key={p.productName}
                 className="flex flex-row gap-1 rounded border border-dashed border-gray-400 bg-gray-50 px-2 py-2"
               >
-                <Tag color="blue" className="mr-0 self-center">
+                <Tag color="purple" className="mr-0 self-center">
                   {p.productName}
                 </Tag>
                 <Tag
-                  color={p.memberType === 'ADMIN' ? 'red' : 'green'}
+                  color={MEMBER_TYPE_COLORS[p.memberType ?? ''] ?? 'default'}
                   className="mr-0 self-center"
                 >
-                  {p.memberType === 'ADMIN' ? '管理员' : '开发者'}
+                  {MEMBER_TYPE_LABELS[p.memberType ?? ''] ?? p.memberType}
                 </Tag>
                 <div className="flex flex-row flex-wrap gap-1 items-center">
                   {p.roleNames.length > 0 ? (

+ 25 - 12
src/pages/Admin/_shared/BindRolesDrawer.tsx

@@ -9,9 +9,16 @@ interface BindRolesDrawerProps {
   open: boolean;
   onClose: () => void;
   onSuccess?: () => void;
+  defaultProductCode?: string;
 }
 
-export const BindRolesDrawer = ({ userId, open, onClose, onSuccess }: BindRolesDrawerProps) => {
+export const BindRolesDrawer = ({
+  userId,
+  open,
+  onClose,
+  onSuccess,
+  defaultProductCode,
+}: BindRolesDrawerProps) => {
   const [products, setProducts] = useState<API.UserProductItem[]>([]);
   const [productCode, setProductCode] = useState<string | undefined>();
   const [loading, setLoading] = useState(false);
@@ -22,10 +29,14 @@ export const BindRolesDrawer = ({ userId, open, onClose, onSuccess }: BindRolesD
 
   useEffect(() => {
     if (!open) return;
-    setProductCode(undefined);
     setRoles([]);
     setCheckedIds([]);
     setInitialCheckedIds([]);
+    if (defaultProductCode) {
+      setProductCode(defaultProductCode);
+      return;
+    }
+    setProductCode(undefined);
     fetchMemberUserProducts({ userId }).then((res) => {
       const list = res.data?.list ?? [];
       setProducts(list);
@@ -103,19 +114,21 @@ export const BindRolesDrawer = ({ userId, open, onClose, onSuccess }: BindRolesD
         </div>
       }
     >
-      {products.length === 0 ? (
+      {products.length === 0 && !defaultProductCode ? (
         <span className="text-gray-400 text-sm">用户不属于任何产品,无法分配角色</span>
       ) : (
         <>
-          <div className="mb-4">
-            <Select
-              placeholder="请选择产品"
-              options={products.map((p) => ({ label: p.productName, value: p.productCode }))}
-              value={productCode}
-              onChange={handleProductChange}
-              className="w-full"
-            />
-          </div>
+          {!defaultProductCode && (
+            <div className="mb-4">
+              <Select
+                placeholder="请选择产品"
+                options={products.map((p) => ({ label: p.productName, value: p.productCode }))}
+                value={productCode}
+                onChange={handleProductChange}
+                className="w-full"
+              />
+            </div>
+          )}
           {productCode && (
             <Spin spinning={loading}>
               <div className="flex flex-col gap-2">

+ 26 - 13
src/pages/Admin/_shared/UserPermDrawer.tsx

@@ -11,9 +11,15 @@ interface UserPermDrawerProps {
   userId: number;
   open: boolean;
   onClose: () => void;
+  defaultProductCode?: string;
 }
 
-export const UserPermDrawer = ({ userId, open, onClose }: UserPermDrawerProps) => {
+export const UserPermDrawer = ({
+  userId,
+  open,
+  onClose,
+  defaultProductCode,
+}: UserPermDrawerProps) => {
   const [products, setProducts] = useState<API.UserProductItem[]>([]);
   const [productCode, setProductCode] = useState<string | undefined>();
   const [loading, setLoading] = useState(false);
@@ -25,11 +31,15 @@ export const UserPermDrawer = ({ userId, open, onClose }: UserPermDrawerProps) =
 
   useEffect(() => {
     if (!open) return;
-    setProductCode(undefined);
     setAllPerms([]);
     setCheckedIds([]);
     setInitialCheckedIds([]);
     setRoleInheritedIds([]);
+    if (defaultProductCode) {
+      setProductCode(defaultProductCode);
+      return;
+    }
+    setProductCode(undefined);
     fetchMemberUserProducts({ userId }).then((res) => {
       const list = res.data?.list ?? [];
       setProducts(list);
@@ -96,7 +106,8 @@ export const UserPermDrawer = ({ userId, open, onClose }: UserPermDrawerProps) =
     setSaving(true);
     try {
       const perms = calcUserPermDelta(checkedIds, roleInheritedIds);
-      await fetchSetUserPerms({ userId, perms });
+      const res = await fetchSetUserPerms({ userId, perms });
+      if (!res.success) return;
       message.success('保存成功');
       onClose();
     } finally {
@@ -124,19 +135,21 @@ export const UserPermDrawer = ({ userId, open, onClose }: UserPermDrawerProps) =
         </div>
       }
     >
-      {products.length === 0 ? (
+      {products.length === 0 && !defaultProductCode ? (
         <span className="text-gray-400 text-sm">用户不属于任何产品,无法设置权限</span>
       ) : (
         <>
-          <div className="mb-4">
-            <Select
-              placeholder="请选择产品"
-              options={products.map((p) => ({ label: p.productName, value: p.productCode }))}
-              value={productCode}
-              onChange={handleProductChange}
-              className="w-full"
-            />
-          </div>
+          {!defaultProductCode && (
+            <div className="mb-4">
+              <Select
+                placeholder="请选择产品"
+                options={products.map((p) => ({ label: p.productName, value: p.productCode }))}
+                value={productCode}
+                onChange={handleProductChange}
+                className="w-full"
+              />
+            </div>
+          )}
           {productCode && (
             <Spin spinning={loading}>
               <PermTree

+ 19 - 3
src/pages/Admin/_shared/useUserProductRoles.ts

@@ -11,14 +11,19 @@ export interface ProductRoleInfo {
   roleNames: string[];
 }
 
-export const useUserProductRoles = (userIds: number[], refreshKey?: number) => {
+export interface ProductRolesBase {
+  roleMap: Map<number, { productName: string; roleName: string }>;
+  memberMap: Map<number, { productName: string; memberType: string }[]>;
+  loading: boolean;
+}
+
+export const useProductRolesBase = (): ProductRolesBase => {
   const [roleMap, setRoleMap] = useState<Map<number, { productName: string; roleName: string }>>(
     new Map(),
   );
   const [memberMap, setMemberMap] = useState<
     Map<number, { productName: string; memberType: string }[]>
   >(new Map());
-  const [userRoleIds, setUserRoleIds] = useState<Map<number, number[]>>(new Map());
   const [loading, setLoading] = useState(false);
 
   useEffect(() => {
@@ -59,6 +64,17 @@ export const useUserProductRoles = (userIds: number[], refreshKey?: number) => {
     load();
   }, []);
 
+  return { roleMap, memberMap, loading };
+};
+
+export const useUserProductRoles = (
+  userIds: number[],
+  base: ProductRolesBase,
+  refreshKey?: number,
+) => {
+  const { roleMap, memberMap } = base;
+  const [userRoleIds, setUserRoleIds] = useState<Map<number, number[]>>(new Map());
+
   useEffect(() => {
     if (userIds.length === 0) {
       setUserRoleIds(new Map());
@@ -113,5 +129,5 @@ export const useUserProductRoles = (userIds: number[], refreshKey?: number) => {
     return map;
   }, [userIds, userRoleIds, roleMap, memberMap]);
 
-  return { userProductInfo, loading };
+  return { userProductInfo };
 };