EditToken.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480
  1. import React, { useEffect, useState, useContext, useRef } from 'react';
  2. import {
  3. API,
  4. isMobile,
  5. showError,
  6. showSuccess,
  7. timestamp2string,
  8. renderGroupOption,
  9. renderQuotaWithPrompt,
  10. } from '../../helpers';
  11. import {
  12. Banner,
  13. Button,
  14. SideSheet,
  15. Space,
  16. Spin,
  17. Typography,
  18. Card,
  19. Tag,
  20. Avatar,
  21. Form,
  22. Col,
  23. Row,
  24. } from '@douyinfe/semi-ui';
  25. import {
  26. IconCreditCard,
  27. IconLink,
  28. IconSave,
  29. IconClose,
  30. IconPlusCircle,
  31. } from '@douyinfe/semi-icons';
  32. import { useTranslation } from 'react-i18next';
  33. import { StatusContext } from '../../context/Status';
  34. const { Text, Title } = Typography;
  35. const EditToken = (props) => {
  36. const { t } = useTranslation();
  37. const [statusState, statusDispatch] = useContext(StatusContext);
  38. const [isEdit, setIsEdit] = useState(false);
  39. const [loading, setLoading] = useState(false);
  40. const formApiRef = useRef(null);
  41. const [models, setModels] = useState([]);
  42. const [groups, setGroups] = useState([]);
  43. const getInitValues = () => ({
  44. name: '',
  45. remain_quota: 500000,
  46. expired_time: -1,
  47. unlimited_quota: false,
  48. model_limits_enabled: false,
  49. model_limits: [],
  50. allow_ips: '',
  51. group: '',
  52. tokenCount: 1,
  53. });
  54. const handleCancel = () => {
  55. props.handleClose();
  56. };
  57. const setExpiredTime = (month, day, hour, minute) => {
  58. let now = new Date();
  59. let timestamp = now.getTime() / 1000;
  60. let seconds = month * 30 * 24 * 60 * 60;
  61. seconds += day * 24 * 60 * 60;
  62. seconds += hour * 60 * 60;
  63. seconds += minute * 60;
  64. if (!formApiRef.current) return;
  65. if (seconds !== 0) {
  66. timestamp += seconds;
  67. formApiRef.current.setValue('expired_time', timestamp2string(timestamp));
  68. } else {
  69. formApiRef.current.setValue('expired_time', -1);
  70. }
  71. };
  72. const loadModels = async () => {
  73. let res = await API.get(`/api/user/models`);
  74. const { success, message, data } = res.data;
  75. if (success) {
  76. let localModelOptions = data.map((model) => ({
  77. label: model,
  78. value: model,
  79. }));
  80. setModels(localModelOptions);
  81. } else {
  82. showError(t(message));
  83. }
  84. };
  85. const loadGroups = async () => {
  86. let res = await API.get(`/api/user/self/groups`);
  87. const { success, message, data } = res.data;
  88. if (success) {
  89. let localGroupOptions = Object.entries(data).map(([group, info]) => ({
  90. label: info.desc,
  91. value: group,
  92. ratio: info.ratio,
  93. }));
  94. if (statusState?.status?.default_use_auto_group) {
  95. if (localGroupOptions.some((group) => group.value === 'auto')) {
  96. localGroupOptions.sort((a, b) => (a.value === 'auto' ? -1 : 1));
  97. } else {
  98. localGroupOptions.unshift({ label: t('自动选择'), value: 'auto' });
  99. }
  100. }
  101. setGroups(localGroupOptions);
  102. if (statusState?.status?.default_use_auto_group && formApiRef.current) {
  103. formApiRef.current.setValue('group', 'auto');
  104. }
  105. } else {
  106. showError(t(message));
  107. }
  108. };
  109. const loadToken = async () => {
  110. setLoading(true);
  111. let res = await API.get(`/api/token/${props.editingToken.id}`);
  112. const { success, message, data } = res.data;
  113. if (success) {
  114. if (data.expired_time !== -1) {
  115. data.expired_time = timestamp2string(data.expired_time);
  116. }
  117. if (data.model_limits !== '') {
  118. data.model_limits = data.model_limits.split(',');
  119. } else {
  120. data.model_limits = [];
  121. }
  122. if (formApiRef.current) {
  123. formApiRef.current.setValues({ ...getInitValues(), ...data });
  124. }
  125. } else {
  126. showError(message);
  127. }
  128. setLoading(false);
  129. };
  130. useEffect(() => {
  131. setIsEdit(props.editingToken.id !== undefined);
  132. }, [props.editingToken.id]);
  133. useEffect(() => {
  134. if (formApiRef.current) {
  135. if (!isEdit) {
  136. formApiRef.current.setValues(getInitValues());
  137. } else {
  138. loadToken();
  139. }
  140. }
  141. loadModels();
  142. loadGroups();
  143. }, [isEdit]);
  144. const generateRandomSuffix = () => {
  145. const characters =
  146. 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  147. let result = '';
  148. for (let i = 0; i < 6; i++) {
  149. result += characters.charAt(
  150. Math.floor(Math.random() * characters.length),
  151. );
  152. }
  153. return result;
  154. };
  155. const submit = async (values) => {
  156. setLoading(true);
  157. if (isEdit) {
  158. let { tokenCount: _tc, ...localInputs } = values;
  159. localInputs.remain_quota = parseInt(localInputs.remain_quota);
  160. if (localInputs.expired_time !== -1) {
  161. let time = Date.parse(localInputs.expired_time);
  162. if (isNaN(time)) {
  163. showError(t('过期时间格式错误!'));
  164. setLoading(false);
  165. return;
  166. }
  167. localInputs.expired_time = Math.ceil(time / 1000);
  168. }
  169. localInputs.model_limits = localInputs.model_limits.join(',');
  170. localInputs.model_limits_enabled = localInputs.model_limits.length > 0;
  171. let res = await API.put(`/api/token/`, {
  172. ...localInputs,
  173. id: parseInt(props.editingToken.id),
  174. });
  175. const { success, message } = res.data;
  176. if (success) {
  177. showSuccess(t('令牌更新成功!'));
  178. props.refresh();
  179. props.handleClose();
  180. } else {
  181. showError(t(message));
  182. }
  183. } else {
  184. const count = parseInt(values.tokenCount, 10) || 1;
  185. let successCount = 0;
  186. for (let i = 0; i < count; i++) {
  187. let { tokenCount: _tc, ...localInputs } = values;
  188. const baseName = values.name.trim() === '' ? 'default' : values.name.trim();
  189. if (i !== 0 || values.name.trim() === '') {
  190. localInputs.name = `${baseName}-${generateRandomSuffix()}`;
  191. } else {
  192. localInputs.name = baseName;
  193. }
  194. localInputs.remain_quota = parseInt(localInputs.remain_quota);
  195. if (localInputs.expired_time !== -1) {
  196. let time = Date.parse(localInputs.expired_time);
  197. if (isNaN(time)) {
  198. showError(t('过期时间格式错误!'));
  199. setLoading(false);
  200. break;
  201. }
  202. localInputs.expired_time = Math.ceil(time / 1000);
  203. }
  204. localInputs.model_limits = localInputs.model_limits.join(',');
  205. localInputs.model_limits_enabled = localInputs.model_limits.length > 0;
  206. let res = await API.post(`/api/token/`, localInputs);
  207. const { success, message } = res.data;
  208. if (success) {
  209. successCount++;
  210. } else {
  211. showError(t(message));
  212. break;
  213. }
  214. }
  215. if (successCount > 0) {
  216. showSuccess(t('令牌创建成功,请在列表页面点击复制获取令牌!'));
  217. props.refresh();
  218. props.handleClose();
  219. }
  220. }
  221. setLoading(false);
  222. formApiRef.current?.setValues(getInitValues());
  223. };
  224. return (
  225. <SideSheet
  226. placement={isEdit ? 'right' : 'left'}
  227. title={
  228. <Space>
  229. {isEdit ? (
  230. <Tag color='blue' shape='circle'>
  231. {t('更新')}
  232. </Tag>
  233. ) : (
  234. <Tag color='green' shape='circle'>
  235. {t('新建')}
  236. </Tag>
  237. )}
  238. <Title heading={4} className='m-0'>
  239. {isEdit ? t('更新令牌信息') : t('创建新的令牌')}
  240. </Title>
  241. </Space>
  242. }
  243. bodyStyle={{ padding: '0' }}
  244. visible={props.visiable}
  245. width={isMobile() ? '100%' : 600}
  246. footer={
  247. <div className='flex justify-end bg-white'>
  248. <Space>
  249. <Button
  250. theme='solid'
  251. className='!rounded-full'
  252. onClick={() => formApiRef.current?.submitForm()}
  253. icon={<IconSave />}
  254. loading={loading}
  255. >
  256. {t('提交')}
  257. </Button>
  258. <Button
  259. theme='light'
  260. className='!rounded-full'
  261. type='primary'
  262. onClick={handleCancel}
  263. icon={<IconClose />}
  264. >
  265. {t('取消')}
  266. </Button>
  267. </Space>
  268. </div>
  269. }
  270. closeIcon={null}
  271. onCancel={() => handleCancel()}
  272. >
  273. <Spin spinning={loading}>
  274. <Form
  275. key={isEdit ? 'edit' : 'new'}
  276. initValues={getInitValues()}
  277. getFormApi={(api) => (formApiRef.current = api)}
  278. onSubmit={submit}
  279. >
  280. {({ values }) => (
  281. <div className='p-6 space-y-6'>
  282. {/* 基本信息 */}
  283. <Card className='!rounded-2xl shadow-sm border-0'>
  284. <div className='flex items-center mb-2'>
  285. <Avatar size='small' color='blue' className='mr-2 shadow-md'>
  286. <IconPlusCircle size={16} />
  287. </Avatar>
  288. <div>
  289. <Text className='text-lg font-medium'>{t('基本信息')}</Text>
  290. <div className='text-xs text-gray-600'>{t('设置令牌的基本信息')}</div>
  291. </div>
  292. </div>
  293. <Row gutter={12}>
  294. <Col span={24}>
  295. <Form.Input
  296. field='name'
  297. label={t('名称')}
  298. placeholder={t('请输入名称')}
  299. rules={[{ required: true, message: t('请输入名称') }]}
  300. showClear
  301. />
  302. </Col>
  303. <Col span={24}>
  304. {groups.length > 0 ? (
  305. <Form.Select
  306. field='group'
  307. label={t('令牌分组')}
  308. placeholder={t('令牌分组,默认为用户的分组')}
  309. optionList={groups}
  310. renderOptionItem={renderGroupOption}
  311. />
  312. ) : (
  313. <Form.Select
  314. placeholder={t('管理员未设置用户可选分组')}
  315. disabled
  316. label={t('令牌分组')}
  317. />
  318. )}
  319. </Col>
  320. <Col span={10}>
  321. <Form.DatePicker
  322. field='expired_time'
  323. label={t('过期时间')}
  324. type='dateTime'
  325. placeholder={t('请选择过期时间')}
  326. style={{ width: '100%' }}
  327. rules={[{ required: true, message: t('请选择过期时间') }]}
  328. />
  329. </Col>
  330. <Col span={14} className='flex flex-col justify-end'>
  331. <Form.Slot label={t('快捷设置')}>
  332. <Space wrap>
  333. <Button
  334. theme='light'
  335. type='primary'
  336. onClick={() => setExpiredTime(0, 0, 0, 0)}
  337. className='!rounded-full'
  338. >
  339. {t('永不过期')}
  340. </Button>
  341. <Button
  342. theme='light'
  343. type='tertiary'
  344. onClick={() => setExpiredTime(1, 0, 0, 0)}
  345. className='!rounded-full'
  346. >
  347. {t('一个月')}
  348. </Button>
  349. <Button
  350. theme='light'
  351. type='tertiary'
  352. onClick={() => setExpiredTime(0, 1, 0, 0)}
  353. className='!rounded-full'
  354. >
  355. {t('一天')}
  356. </Button>
  357. <Button
  358. theme='light'
  359. type='tertiary'
  360. onClick={() => setExpiredTime(0, 0, 1, 0)}
  361. className='!rounded-full'
  362. >
  363. {t('一小时')}
  364. </Button>
  365. </Space>
  366. </Form.Slot>
  367. </Col>
  368. {!isEdit && (
  369. <Col span={24}>
  370. <Form.InputNumber
  371. field='tokenCount'
  372. label={t('新建数量')}
  373. min={1}
  374. extraText={t('批量创建时会在名称后自动添加随机后缀')}
  375. rules={[{ required: true, message: t('请输入新建数量') }]}
  376. />
  377. </Col>
  378. )}
  379. </Row>
  380. </Card>
  381. {/* 额度设置 */}
  382. <Card className='!rounded-2xl shadow-sm border-0'>
  383. <div className='flex items-center mb-2'>
  384. <Avatar size='small' color='green' className='mr-2 shadow-md'>
  385. <IconCreditCard size={16} />
  386. </Avatar>
  387. <div>
  388. <Text className='text-lg font-medium'>{t('额度设置')}</Text>
  389. <div className='text-xs text-gray-600'>{t('设置令牌可用额度和数量')}</div>
  390. </div>
  391. </div>
  392. <Row gutter={12}>
  393. <Col span={10}>
  394. <Form.AutoComplete
  395. field='remain_quota'
  396. label={t('额度')}
  397. placeholder={t('请输入额度')}
  398. type='number'
  399. disabled={values.unlimited_quota}
  400. extraText={renderQuotaWithPrompt(values.remain_quota)}
  401. rules={values.unlimited_quota ? [] : [{ required: true, message: t('请输入额度') }]}
  402. data={[
  403. { value: 500000, label: '1$' },
  404. { value: 5000000, label: '10$' },
  405. { value: 25000000, label: '50$' },
  406. { value: 50000000, label: '100$' },
  407. { value: 250000000, label: '500$' },
  408. { value: 500000000, label: '1000$' },
  409. ]}
  410. />
  411. </Col>
  412. <Col span={14} className='flex justify-end'>
  413. <Form.Switch field='unlimited_quota' label={t('无限额度')} size='large' />
  414. </Col>
  415. </Row>
  416. <Banner
  417. type='warning'
  418. description={t('注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。')}
  419. className='mb-4 !rounded-lg'
  420. />
  421. </Card>
  422. {/* 访问限制 */}
  423. <Card className='!rounded-2xl shadow-sm border-0'>
  424. <div className='flex items-center mb-2'>
  425. <Avatar size='small' color='purple' className='mr-2 shadow-md'>
  426. <IconLink size={16} />
  427. </Avatar>
  428. <div>
  429. <Text className='text-lg font-medium'>{t('访问限制')}</Text>
  430. <div className='text-xs text-gray-600'>{t('设置令牌的访问限制')}</div>
  431. </div>
  432. </div>
  433. <Row gutter={12}>
  434. <Col span={24}>
  435. <Form.TextArea
  436. field='allow_ips'
  437. label={t('IP白名单')}
  438. placeholder={t('允许的IP,一行一个,不填写则不限制')}
  439. rows={4}
  440. extraText={t('请勿过度信任此功能,IP可能被伪造')}
  441. />
  442. </Col>
  443. <Col span={24}>
  444. <Form.Select
  445. field='model_limits'
  446. label={t('模型限制列表')}
  447. placeholder={t('请选择该令牌支持的模型,留空支持所有模型')}
  448. multiple
  449. optionList={models}
  450. maxTagCount={3}
  451. extraText={t('非必要,不建议启用模型限制')}
  452. />
  453. </Col>
  454. </Row>
  455. </Card>
  456. </div>
  457. )}
  458. </Form>
  459. </Spin>
  460. </SideSheet>
  461. );
  462. };
  463. export default EditToken;