EditUser.js 11 KB

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