index.tsx 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. import {
  2. MEMBER_TYPE_COLORS,
  3. MEMBER_TYPE_LABELS,
  4. STATUS_DISABLED,
  5. STATUS_ENABLED,
  6. STATUS_OPTIONS,
  7. } from '@/defines';
  8. import { BindRolesDrawer } from '@/pages/Admin/_shared/BindRolesDrawer';
  9. import { UserPermDrawer } from '@/pages/Admin/_shared/UserPermDrawer';
  10. import {
  11. useProductRolesBase,
  12. useUserProductRoles,
  13. } from '@/pages/Admin/_shared/useUserProductRoles';
  14. import { fetchDeptTree } from '@/services/dept';
  15. import {
  16. fetchResetPassword,
  17. fetchUpdateUserStatus,
  18. fetchUserCredentials,
  19. fetchUserList,
  20. } from '@/services/user';
  21. import { unixTimeFormat } from '@/utils/timeUtils';
  22. import { StopOutlined } from '@ant-design/icons';
  23. import { ActionType, PageContainer, ProColumns, ProTable } from '@ant-design/pro-components';
  24. import { useIntl } from '@umijs/max';
  25. import { Button, Popconfirm, Tag, message } from 'antd';
  26. import { useEffect, useMemo, useRef, useState } from 'react';
  27. import { UserForm } from './components/Form';
  28. import { UserCredentialModal } from './components/UserCredentialModal';
  29. const flattenDeptTree = (items: API.DeptItem[]): { label: string; value: number }[] =>
  30. items.flatMap((d) => [
  31. { label: d.name, value: d.id },
  32. ...(d.children ? flattenDeptTree(d.children) : []),
  33. ]);
  34. export default function UserPage() {
  35. const intl = useIntl();
  36. const actionRef = useRef<ActionType>();
  37. const [pageUsers, setPageUsers] = useState<API.UserItem[]>([]);
  38. const [deptOptions, setDeptOptions] = useState<{ label: string; value: number }[]>([]);
  39. const [rolesDrawer, setRolesDrawer] = useState<{ open: boolean; userId: number }>({
  40. open: false,
  41. userId: 0,
  42. });
  43. const [permDrawer, setPermDrawer] = useState<{ open: boolean; userId: number }>({
  44. open: false,
  45. userId: 0,
  46. });
  47. const [refreshKey, setRefreshKey] = useState(0);
  48. const [credData, setCredData] = useState<API.FetchUserCredentialsResp | null>(null);
  49. const [credOpen, setCredOpen] = useState(false);
  50. useEffect(() => {
  51. fetchDeptTree().then((res) => {
  52. setDeptOptions([{ label: '无部门', value: 0 }, ...flattenDeptTree(res.data ?? [])]);
  53. });
  54. }, []);
  55. const userIds = useMemo(() => pageUsers.map((u) => u.id), [pageUsers]);
  56. const productRolesBase = useProductRolesBase();
  57. const { userProductInfo } = useUserProductRoles(userIds, productRolesBase, refreshKey);
  58. const handleToggleStatus = async (r: API.UserItem) => {
  59. const res = await fetchUpdateUserStatus({
  60. id: r.id,
  61. status: r.status === STATUS_ENABLED ? STATUS_DISABLED : STATUS_ENABLED,
  62. });
  63. if (!res.success) return;
  64. message.success('操作成功');
  65. actionRef.current?.reload();
  66. };
  67. const showCredentials = async (ticket: string) => {
  68. const res = await fetchUserCredentials({ ticket });
  69. if (!res.success || !res.data) return;
  70. setCredData(res.data);
  71. setCredOpen(true);
  72. };
  73. const handleCreateSuccess = async (ticket?: string) => {
  74. actionRef.current?.reload();
  75. if (ticket) await showCredentials(ticket);
  76. };
  77. const handleResetPassword = async (r: API.UserItem) => {
  78. const res = await fetchResetPassword({ userId: r.id });
  79. if (!res.success || !res.data) return;
  80. message.success('密码已重置');
  81. await showCredentials(res.data.credentialsTicket);
  82. };
  83. const columns: ProColumns<API.UserItem>[] = [
  84. { title: '用户名', dataIndex: 'username', width: 200 },
  85. { title: '昵称', dataIndex: 'nickname', width: 200 },
  86. {
  87. title: '状态',
  88. dataIndex: 'status',
  89. width: 80,
  90. valueType: 'select',
  91. fieldProps: { options: STATUS_OPTIONS },
  92. render: (_, r) => (
  93. <Tag color={r.status === STATUS_ENABLED ? 'success' : 'error'}>
  94. {r.status === STATUS_ENABLED ? '启用' : '禁用'}
  95. </Tag>
  96. ),
  97. },
  98. {
  99. title: '部门',
  100. dataIndex: 'deptId',
  101. width: 150,
  102. valueType: 'select',
  103. fieldProps: { options: deptOptions, showSearch: true },
  104. render: (_, r) => {
  105. if (!r.deptId)
  106. return <span className="text-(--ant-color-text-quaternary) text-xs">无部门</span>;
  107. const dept = deptOptions.find((d) => d.value === r.deptId);
  108. return (
  109. dept?.label ?? <span className="text-(--ant-color-text-quaternary) text-xs">无部门</span>
  110. );
  111. },
  112. },
  113. {
  114. title: '产品角色',
  115. key: 'productRoles',
  116. search: false,
  117. render: (_, r) => {
  118. const info = userProductInfo.get(r.id) ?? [];
  119. if (info.length === 0)
  120. return <span className="text-(--ant-color-text-quaternary) text-xs">无</span>;
  121. return (
  122. <div className="flex flex-col gap-1">
  123. {info.map((p) => (
  124. <div
  125. key={p.productName}
  126. className="inline-flex flex-row gap-1 rounded border border-dashed border-(--ant-color-border) bg-(--ant-color-fill-quaternary) px-2 py-2 self-start"
  127. >
  128. <Tag color="purple" className="mr-0 self-center">
  129. {p.productName}
  130. {p.productStatus !== STATUS_ENABLED && (
  131. <StopOutlined className="ml-1 text-(--ant-color-error)!" />
  132. )}
  133. </Tag>
  134. <Tag
  135. color={MEMBER_TYPE_COLORS[p.memberType ?? ''] ?? 'default'}
  136. className="mr-0 self-center"
  137. >
  138. {MEMBER_TYPE_LABELS[p.memberType ?? ''] ?? p.memberType}
  139. </Tag>
  140. <div className="flex flex-row flex-wrap gap-1 items-center">
  141. {p.roleNames.length > 0 ? (
  142. p.roleNames.map((name) => (
  143. <Tag key={name} className="mr-0">
  144. {name}
  145. </Tag>
  146. ))
  147. ) : (
  148. <span className="text-(--ant-color-text-quaternary) text-xs whitespace-nowrap">
  149. 无角色
  150. </span>
  151. )}
  152. </div>
  153. </div>
  154. ))}
  155. </div>
  156. );
  157. },
  158. },
  159. {
  160. title: '创建时间',
  161. dataIndex: 'createTime',
  162. width: 180,
  163. search: false,
  164. render: (_, r) => unixTimeFormat(r.createTime),
  165. },
  166. {
  167. title: '操作',
  168. valueType: 'option',
  169. width: 320,
  170. render: (_, r) => [
  171. <UserForm
  172. key="edit"
  173. mode="edit"
  174. initialValues={r}
  175. trigger={<a>编辑</a>}
  176. onSuccess={() => actionRef.current?.reload()}
  177. />,
  178. <UserForm
  179. key="copy"
  180. mode="copy"
  181. initialValues={r}
  182. trigger={<a>复制</a>}
  183. onSuccess={handleCreateSuccess}
  184. />,
  185. <a key="roles" onClick={() => setRolesDrawer({ open: true, userId: r.id })}>
  186. 分配角色
  187. </a>,
  188. <a key="perms" onClick={() => setPermDrawer({ open: true, userId: r.id })}>
  189. 设置权限
  190. </a>,
  191. <Popconfirm
  192. key="reset"
  193. title={`确认重置「${r.username}」的密码?`}
  194. onConfirm={() => handleResetPassword(r)}
  195. >
  196. <a>重置密码</a>
  197. </Popconfirm>,
  198. <Popconfirm
  199. key="status"
  200. title={
  201. r.status === STATUS_ENABLED
  202. ? `确认冻结「${r.username}」?`
  203. : `确认解冻「${r.username}」?`
  204. }
  205. onConfirm={() => handleToggleStatus(r)}
  206. >
  207. <a className={r.status === STATUS_ENABLED ? 'text-(--ant-color-error)' : ''}>
  208. {r.status === STATUS_ENABLED ? '冻结' : '解冻'}
  209. </a>
  210. </Popconfirm>,
  211. ],
  212. },
  213. ];
  214. return (
  215. <PageContainer title={intl.formatMessage({ id: 'admin.user.title' })}>
  216. <ProTable<API.UserItem>
  217. actionRef={actionRef}
  218. columns={columns}
  219. rowKey="id"
  220. search={{ span: { xs: 24, sm: 12, md: 8, lg: 6, xl: 4, xxl: 4 } }}
  221. request={fetchUserList}
  222. onDataSourceChange={setPageUsers}
  223. scroll={{ x: 1280 }}
  224. tableLayout="fixed"
  225. pagination={{
  226. defaultPageSize: 20,
  227. pageSizeOptions: [10, 20, 50, 100],
  228. showSizeChanger: true,
  229. }}
  230. toolBarRender={() => [
  231. <UserForm
  232. key="create"
  233. mode="add"
  234. trigger={<Button type="primary">新建用户</Button>}
  235. onSuccess={handleCreateSuccess}
  236. />,
  237. ]}
  238. />
  239. <BindRolesDrawer
  240. {...rolesDrawer}
  241. onClose={() => setRolesDrawer((p) => ({ ...p, open: false }))}
  242. onSuccess={() => setRefreshKey((k) => k + 1)}
  243. />
  244. <UserPermDrawer
  245. {...permDrawer}
  246. onClose={() => setPermDrawer((p) => ({ ...p, open: false }))}
  247. />
  248. <UserCredentialModal open={credOpen} data={credData} onClose={() => setCredOpen(false)} />
  249. </PageContainer>
  250. );
  251. }