EditUserModal.jsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. import React, { useEffect, useState, useRef } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import {
  4. API,
  5. showError,
  6. showSuccess,
  7. renderQuota,
  8. renderQuotaWithPrompt,
  9. } from '../../../../helpers';
  10. import { useIsMobile } from '../../../../hooks/common/useIsMobile.js';
  11. import {
  12. Button,
  13. Modal,
  14. SideSheet,
  15. Space,
  16. Spin,
  17. Typography,
  18. Card,
  19. Tag,
  20. Form,
  21. Avatar,
  22. Row,
  23. Col,
  24. Input,
  25. InputNumber,
  26. } from '@douyinfe/semi-ui';
  27. import {
  28. IconUser,
  29. IconSave,
  30. IconClose,
  31. IconLink,
  32. IconUserGroup,
  33. IconPlus,
  34. } from '@douyinfe/semi-icons';
  35. const { Text, Title } = Typography;
  36. const EditUserModal = (props) => {
  37. const { t } = useTranslation();
  38. const userId = props.editingUser.id;
  39. const [loading, setLoading] = useState(true);
  40. const [addQuotaModalOpen, setIsModalOpen] = useState(false);
  41. const [addQuotaLocal, setAddQuotaLocal] = useState('');
  42. const isMobile = useIsMobile();
  43. const [groupOptions, setGroupOptions] = useState([]);
  44. const formApiRef = useRef(null);
  45. const isEdit = Boolean(userId);
  46. const getInitValues = () => ({
  47. username: '',
  48. display_name: '',
  49. password: '',
  50. github_id: '',
  51. oidc_id: '',
  52. wechat_id: '',
  53. telegram_id: '',
  54. email: '',
  55. quota: 0,
  56. group: 'default',
  57. remark: '',
  58. });
  59. const fetchGroups = async () => {
  60. try {
  61. let res = await API.get(`/api/group/`);
  62. setGroupOptions(
  63. res.data.data.map((g) => ({ label: g, value: g }))
  64. );
  65. } catch (e) {
  66. showError(e.message);
  67. }
  68. };
  69. const handleCancel = () => props.handleClose();
  70. const loadUser = async () => {
  71. setLoading(true);
  72. const url = userId ? `/api/user/${userId}` : `/api/user/self`;
  73. const res = await API.get(url);
  74. const { success, message, data } = res.data;
  75. if (success) {
  76. data.password = '';
  77. formApiRef.current?.setValues({ ...getInitValues(), ...data });
  78. } else {
  79. showError(message);
  80. }
  81. setLoading(false);
  82. };
  83. useEffect(() => {
  84. loadUser();
  85. if (userId) fetchGroups();
  86. }, [props.editingUser.id]);
  87. /* ----------------------- submit ----------------------- */
  88. const submit = async (values) => {
  89. setLoading(true);
  90. let payload = { ...values };
  91. if (typeof payload.quota === 'string') payload.quota = parseInt(payload.quota) || 0;
  92. if (userId) {
  93. payload.id = parseInt(userId);
  94. }
  95. const url = userId ? `/api/user/` : `/api/user/self`;
  96. const res = await API.put(url, payload);
  97. const { success, message } = res.data;
  98. if (success) {
  99. showSuccess(t('用户信息更新成功!'));
  100. props.refresh();
  101. props.handleClose();
  102. } else {
  103. showError(message);
  104. }
  105. setLoading(false);
  106. };
  107. /* --------------------- quota helper -------------------- */
  108. const addLocalQuota = () => {
  109. const current = parseInt(formApiRef.current?.getValue('quota') || 0);
  110. const delta = parseInt(addQuotaLocal) || 0;
  111. formApiRef.current?.setValue('quota', current + delta);
  112. };
  113. /* --------------------------- UI --------------------------- */
  114. return (
  115. <>
  116. <SideSheet
  117. placement='right'
  118. title={
  119. <Space>
  120. <Tag color='blue' shape='circle'>
  121. {t(isEdit ? '编辑' : '新建')}
  122. </Tag>
  123. <Title heading={4} className='m-0'>
  124. {isEdit ? t('编辑用户') : t('创建用户')}
  125. </Title>
  126. </Space>
  127. }
  128. bodyStyle={{ padding: 0 }}
  129. visible={props.visible}
  130. width={isMobile ? '100%' : 600}
  131. footer={
  132. <div className='flex justify-end bg-white'>
  133. <Space>
  134. <Button
  135. theme='solid'
  136. onClick={() => formApiRef.current?.submitForm()}
  137. icon={<IconSave />}
  138. loading={loading}
  139. >
  140. {t('提交')}
  141. </Button>
  142. <Button
  143. theme='light'
  144. type='primary'
  145. onClick={handleCancel}
  146. icon={<IconClose />}
  147. >
  148. {t('取消')}
  149. </Button>
  150. </Space>
  151. </div>
  152. }
  153. closeIcon={null}
  154. onCancel={handleCancel}
  155. >
  156. <Spin spinning={loading}>
  157. <Form
  158. initValues={getInitValues()}
  159. getFormApi={(api) => (formApiRef.current = api)}
  160. onSubmit={submit}
  161. >
  162. {({ values }) => (
  163. <div className='p-2'>
  164. {/* 基本信息 */}
  165. <Card className='!rounded-2xl shadow-sm border-0'>
  166. <div className='flex items-center mb-2'>
  167. <Avatar size='small' color='blue' className='mr-2 shadow-md'>
  168. <IconUser size={16} />
  169. </Avatar>
  170. <div>
  171. <Text className='text-lg font-medium'>{t('基本信息')}</Text>
  172. <div className='text-xs text-gray-600'>{t('用户的基本账户信息')}</div>
  173. </div>
  174. </div>
  175. <Row gutter={12}>
  176. <Col span={24}>
  177. <Form.Input
  178. field='username'
  179. label={t('用户名')}
  180. placeholder={t('请输入新的用户名')}
  181. rules={[{ required: true, message: t('请输入用户名') }]}
  182. showClear
  183. />
  184. </Col>
  185. <Col span={24}>
  186. <Form.Input
  187. field='password'
  188. label={t('密码')}
  189. placeholder={t('请输入新的密码,最短 8 位')}
  190. mode='password'
  191. showClear
  192. />
  193. </Col>
  194. <Col span={24}>
  195. <Form.Input
  196. field='display_name'
  197. label={t('显示名称')}
  198. placeholder={t('请输入新的显示名称')}
  199. showClear
  200. />
  201. </Col>
  202. <Col span={24}>
  203. <Form.Input
  204. field='remark'
  205. label={t('备注')}
  206. placeholder={t('请输入备注(仅管理员可见)')}
  207. showClear
  208. />
  209. </Col>
  210. </Row>
  211. </Card>
  212. {/* 权限设置 */}
  213. {userId && (
  214. <Card className='!rounded-2xl shadow-sm border-0'>
  215. <div className='flex items-center mb-2'>
  216. <Avatar size='small' color='green' className='mr-2 shadow-md'>
  217. <IconUserGroup size={16} />
  218. </Avatar>
  219. <div>
  220. <Text className='text-lg font-medium'>{t('权限设置')}</Text>
  221. <div className='text-xs text-gray-600'>{t('用户分组和额度管理')}</div>
  222. </div>
  223. </div>
  224. <Row gutter={12}>
  225. <Col span={24}>
  226. <Form.Select
  227. field='group'
  228. label={t('分组')}
  229. placeholder={t('请选择分组')}
  230. optionList={groupOptions}
  231. allowAdditions
  232. search
  233. rules={[{ required: true, message: t('请选择分组') }]}
  234. />
  235. </Col>
  236. <Col span={10}>
  237. <Form.InputNumber
  238. field='quota'
  239. label={t('剩余额度')}
  240. placeholder={t('请输入新的剩余额度')}
  241. step={500000}
  242. extraText={renderQuotaWithPrompt(values.quota || 0)}
  243. rules={[{ required: true, message: t('请输入额度') }]}
  244. style={{ width: '100%' }}
  245. />
  246. </Col>
  247. <Col span={14}>
  248. <Form.Slot label={t('添加额度')}>
  249. <Button
  250. icon={<IconPlus />}
  251. onClick={() => setIsModalOpen(true)}
  252. />
  253. </Form.Slot>
  254. </Col>
  255. </Row>
  256. </Card>
  257. )}
  258. {/* 绑定信息 */}
  259. <Card className='!rounded-2xl shadow-sm border-0'>
  260. <div className='flex items-center mb-2'>
  261. <Avatar size='small' color='purple' className='mr-2 shadow-md'>
  262. <IconLink size={16} />
  263. </Avatar>
  264. <div>
  265. <Text className='text-lg font-medium'>{t('绑定信息')}</Text>
  266. <div className='text-xs text-gray-600'>{t('第三方账户绑定状态(只读)')}</div>
  267. </div>
  268. </div>
  269. <Row gutter={12}>
  270. {['github_id', 'oidc_id', 'wechat_id', 'email', 'telegram_id'].map((field) => (
  271. <Col span={24} key={field}>
  272. <Form.Input
  273. field={field}
  274. label={t(`已绑定的 ${field.replace('_id', '').toUpperCase()} 账户`)}
  275. readonly
  276. placeholder={t('此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改')}
  277. />
  278. </Col>
  279. ))}
  280. </Row>
  281. </Card>
  282. </div>
  283. )}
  284. </Form>
  285. </Spin>
  286. </SideSheet>
  287. {/* 添加额度模态框 */}
  288. <Modal
  289. centered
  290. visible={addQuotaModalOpen}
  291. onOk={() => {
  292. addLocalQuota();
  293. setIsModalOpen(false);
  294. }}
  295. onCancel={() => setIsModalOpen(false)}
  296. closable={null}
  297. title={
  298. <div className='flex items-center'>
  299. <IconPlus className='mr-2' />
  300. {t('添加额度')}
  301. </div>
  302. }
  303. >
  304. <div className='mb-4'>
  305. {
  306. (() => {
  307. const current = formApiRef.current?.getValue('quota') || 0;
  308. return (
  309. <Text type='secondary' className='block mb-2'>
  310. {`${t('新额度:')}${renderQuota(current)} + ${renderQuota(addQuotaLocal)} = ${renderQuota(current + parseInt(addQuotaLocal || 0))}`}
  311. </Text>
  312. );
  313. })()
  314. }
  315. </div>
  316. <InputNumber
  317. placeholder={t('需要添加的额度(支持负数)')}
  318. value={addQuotaLocal}
  319. onChange={setAddQuotaLocal}
  320. style={{ width: '100%' }}
  321. showClear
  322. step={500000}
  323. />
  324. </Modal>
  325. </>
  326. );
  327. };
  328. export default EditUserModal;