EditToken.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. import React, { useEffect, useState } from 'react';
  2. import { useNavigate } from 'react-router-dom';
  3. import {
  4. API,
  5. isMobile,
  6. showError,
  7. showSuccess,
  8. timestamp2string,
  9. } from '../../helpers';
  10. import { renderGroupOption, renderQuotaWithPrompt } from '../../helpers/render';
  11. import {
  12. AutoComplete,
  13. Banner,
  14. Button,
  15. Checkbox,
  16. DatePicker,
  17. Input,
  18. Select,
  19. SideSheet,
  20. Space,
  21. Spin, TextArea,
  22. Typography
  23. } from '@douyinfe/semi-ui';
  24. import Title from '@douyinfe/semi-ui/lib/es/typography/title';
  25. import { Divider } from 'semantic-ui-react';
  26. import { useTranslation } from 'react-i18next';
  27. const EditToken = (props) => {
  28. const [isEdit, setIsEdit] = useState(false);
  29. const [loading, setLoading] = useState(isEdit);
  30. const originInputs = {
  31. name: '',
  32. remain_quota: isEdit ? 0 : 500000,
  33. expired_time: -1,
  34. unlimited_quota: false,
  35. model_limits_enabled: false,
  36. model_limits: [],
  37. allow_ips: '',
  38. group: '',
  39. };
  40. const [inputs, setInputs] = useState(originInputs);
  41. const {
  42. name,
  43. remain_quota,
  44. expired_time,
  45. unlimited_quota,
  46. model_limits_enabled,
  47. model_limits,
  48. allow_ips,
  49. group
  50. } = inputs;
  51. // const [visible, setVisible] = useState(false);
  52. const [models, setModels] = useState([]);
  53. const [groups, setGroups] = useState([]);
  54. const navigate = useNavigate();
  55. const { t } = useTranslation();
  56. const handleInputChange = (name, value) => {
  57. setInputs((inputs) => ({ ...inputs, [name]: value }));
  58. };
  59. const handleCancel = () => {
  60. props.handleClose();
  61. };
  62. const setExpiredTime = (month, day, hour, minute) => {
  63. let now = new Date();
  64. let timestamp = now.getTime() / 1000;
  65. let seconds = month * 30 * 24 * 60 * 60;
  66. seconds += day * 24 * 60 * 60;
  67. seconds += hour * 60 * 60;
  68. seconds += minute * 60;
  69. if (seconds !== 0) {
  70. timestamp += seconds;
  71. setInputs({ ...inputs, expired_time: timestamp2string(timestamp) });
  72. } else {
  73. setInputs({ ...inputs, expired_time: -1 });
  74. }
  75. };
  76. const setUnlimitedQuota = () => {
  77. setInputs({ ...inputs, unlimited_quota: !unlimited_quota });
  78. };
  79. const loadModels = async () => {
  80. let res = await API.get(`/api/user/models`);
  81. const { success, message, data } = res.data;
  82. if (success) {
  83. let localModelOptions = data.map((model) => ({
  84. label: model,
  85. value: model,
  86. }));
  87. setModels(localModelOptions);
  88. } else {
  89. showError(t(message));
  90. }
  91. };
  92. const loadGroups = async () => {
  93. let res = await API.get(`/api/user/self/groups`);
  94. const { success, message, data } = res.data;
  95. if (success) {
  96. let localGroupOptions = Object.entries(data).map(([group, info]) => ({
  97. label: info.desc,
  98. value: group,
  99. ratio: info.ratio
  100. }));
  101. setGroups(localGroupOptions);
  102. } else {
  103. showError(t(message));
  104. }
  105. };
  106. const loadToken = async () => {
  107. setLoading(true);
  108. let res = await API.get(`/api/token/${props.editingToken.id}`);
  109. const { success, message, data } = res.data;
  110. if (success) {
  111. if (data.expired_time !== -1) {
  112. data.expired_time = timestamp2string(data.expired_time);
  113. }
  114. if (data.model_limits !== '') {
  115. data.model_limits = data.model_limits.split(',');
  116. } else {
  117. data.model_limits = [];
  118. }
  119. setInputs(data);
  120. } else {
  121. showError(message);
  122. }
  123. setLoading(false);
  124. };
  125. useEffect(() => {
  126. setIsEdit(props.editingToken.id !== undefined);
  127. }, [props.editingToken.id]);
  128. useEffect(() => {
  129. if (!isEdit) {
  130. setInputs(originInputs);
  131. } else {
  132. loadToken().then(() => {
  133. // console.log(inputs);
  134. });
  135. }
  136. loadModels();
  137. loadGroups();
  138. }, [isEdit]);
  139. // 新增 state 变量 tokenCount 来记录用户想要创建的令牌数量,默认为 1
  140. const [tokenCount, setTokenCount] = useState(1);
  141. // 新增处理 tokenCount 变化的函数
  142. const handleTokenCountChange = (value) => {
  143. // 确保用户输入的是正整数
  144. const count = parseInt(value, 10);
  145. if (!isNaN(count) && count > 0) {
  146. setTokenCount(count);
  147. }
  148. };
  149. // 生成一个随机的四位字母数字字符串
  150. const generateRandomSuffix = () => {
  151. const characters =
  152. 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  153. let result = '';
  154. for (let i = 0; i < 6; i++) {
  155. result += characters.charAt(
  156. Math.floor(Math.random() * characters.length),
  157. );
  158. }
  159. return result;
  160. };
  161. const submit = async () => {
  162. setLoading(true);
  163. if (isEdit) {
  164. // 编辑令牌的逻辑保持不变
  165. let localInputs = { ...inputs };
  166. localInputs.remain_quota = parseInt(localInputs.remain_quota);
  167. if (localInputs.expired_time !== -1) {
  168. let time = Date.parse(localInputs.expired_time);
  169. if (isNaN(time)) {
  170. showError(t('过期时间格式错误!'));
  171. setLoading(false);
  172. return;
  173. }
  174. localInputs.expired_time = Math.ceil(time / 1000);
  175. }
  176. localInputs.model_limits = localInputs.model_limits.join(',');
  177. let res = await API.put(`/api/token/`, {
  178. ...localInputs,
  179. id: parseInt(props.editingToken.id),
  180. });
  181. const { success, message } = res.data;
  182. if (success) {
  183. showSuccess(t('令牌更新成功!'));
  184. props.refresh();
  185. props.handleClose();
  186. } else {
  187. showError(t(message));
  188. }
  189. } else {
  190. // 处理新增多个令牌的情况
  191. let successCount = 0; // 记录成功创建的令牌数量
  192. for (let i = 0; i < tokenCount; i++) {
  193. let localInputs = { ...inputs };
  194. if (i !== 0) {
  195. // 如果用户想要创建多个令牌,则给每个令牌一个序号后缀
  196. localInputs.name = `${inputs.name}-${generateRandomSuffix()}`;
  197. }
  198. localInputs.remain_quota = parseInt(localInputs.remain_quota);
  199. if (localInputs.expired_time !== -1) {
  200. let time = Date.parse(localInputs.expired_time);
  201. if (isNaN(time)) {
  202. showError(t('过期时间格式错误!'));
  203. setLoading(false);
  204. break;
  205. }
  206. localInputs.expired_time = Math.ceil(time / 1000);
  207. }
  208. localInputs.model_limits = localInputs.model_limits.join(',');
  209. let res = await API.post(`/api/token/`, localInputs);
  210. const { success, message } = res.data;
  211. if (success) {
  212. successCount++;
  213. } else {
  214. showError(t(message));
  215. break; // 如果创建失败,终止循环
  216. }
  217. }
  218. if (successCount > 0) {
  219. showSuccess(
  220. t('令牌创建成功,请在列表页面点击复制获取令牌!')
  221. );
  222. props.refresh();
  223. props.handleClose();
  224. }
  225. }
  226. setLoading(false);
  227. setInputs(originInputs); // 重置表单
  228. setTokenCount(1); // 重置数量为默认值
  229. };
  230. return (
  231. <>
  232. <SideSheet
  233. placement={isEdit ? 'right' : 'left'}
  234. title={
  235. <Title level={3}>{isEdit ? t('更新令牌信息') : t('创建新的令牌')}</Title>
  236. }
  237. headerStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
  238. bodyStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
  239. visible={props.visiable}
  240. footer={
  241. <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
  242. <Space>
  243. <Button theme='solid' size={'large'} onClick={submit}>
  244. {t('提交')}
  245. </Button>
  246. <Button
  247. theme='solid'
  248. size={'large'}
  249. type={'tertiary'}
  250. onClick={handleCancel}
  251. >
  252. {t('取消')}
  253. </Button>
  254. </Space>
  255. </div>
  256. }
  257. closeIcon={null}
  258. onCancel={() => handleCancel()}
  259. width={isMobile() ? '100%' : 600}
  260. >
  261. <Spin spinning={loading}>
  262. <Input
  263. style={{ marginTop: 20 }}
  264. label={t('名称')}
  265. name='name'
  266. placeholder={t('请输入名称')}
  267. onChange={(value) => handleInputChange('name', value)}
  268. value={name}
  269. autoComplete='new-password'
  270. required={!isEdit}
  271. />
  272. <Divider />
  273. <DatePicker
  274. label={t('过期时间')}
  275. name='expired_time'
  276. placeholder={t('请选择过期时间')}
  277. onChange={(value) => handleInputChange('expired_time', value)}
  278. value={expired_time}
  279. autoComplete='new-password'
  280. type='dateTime'
  281. />
  282. <div style={{ marginTop: 20 }}>
  283. <Space>
  284. <Button
  285. type={'tertiary'}
  286. onClick={() => {
  287. setExpiredTime(0, 0, 0, 0);
  288. }}
  289. >
  290. {t('永不过期')}
  291. </Button>
  292. <Button
  293. type={'tertiary'}
  294. onClick={() => {
  295. setExpiredTime(0, 0, 1, 0);
  296. }}
  297. >
  298. {t('一小时')}
  299. </Button>
  300. <Button
  301. type={'tertiary'}
  302. onClick={() => {
  303. setExpiredTime(1, 0, 0, 0);
  304. }}
  305. >
  306. {t('一个月')}
  307. </Button>
  308. <Button
  309. type={'tertiary'}
  310. onClick={() => {
  311. setExpiredTime(0, 1, 0, 0);
  312. }}
  313. >
  314. {t('一天')}
  315. </Button>
  316. </Space>
  317. </div>
  318. <Divider />
  319. <Banner
  320. type={'warning'}
  321. description={t('注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。')}
  322. ></Banner>
  323. <div style={{ marginTop: 20 }}>
  324. <Typography.Text>{`${t('额度')}${renderQuotaWithPrompt(remain_quota)}`}</Typography.Text>
  325. </div>
  326. <AutoComplete
  327. style={{ marginTop: 8 }}
  328. name='remain_quota'
  329. placeholder={t('请输入额度')}
  330. onChange={(value) => handleInputChange('remain_quota', value)}
  331. value={remain_quota}
  332. autoComplete='new-password'
  333. type='number'
  334. // position={'top'}
  335. data={[
  336. { value: 500000, label: '1$' },
  337. { value: 5000000, label: '10$' },
  338. { value: 25000000, label: '50$' },
  339. { value: 50000000, label: '100$' },
  340. { value: 250000000, label: '500$' },
  341. { value: 500000000, label: '1000$' },
  342. ]}
  343. disabled={unlimited_quota}
  344. />
  345. {!isEdit && (
  346. <>
  347. <div style={{ marginTop: 20 }}>
  348. <Typography.Text>{t('新建数量')}</Typography.Text>
  349. </div>
  350. <AutoComplete
  351. style={{ marginTop: 8 }}
  352. label={t('数量')}
  353. placeholder={t('请选择或输入创建令牌的数量')}
  354. onChange={(value) => handleTokenCountChange(value)}
  355. onSelect={(value) => handleTokenCountChange(value)}
  356. value={tokenCount.toString()}
  357. autoComplete='off'
  358. type='number'
  359. data={[
  360. { value: 10, label: t('10个') },
  361. { value: 20, label: t('20个') },
  362. { value: 30, label: t('30个') },
  363. { value: 100, label: t('100个') },
  364. ]}
  365. disabled={unlimited_quota}
  366. />
  367. </>
  368. )}
  369. <div>
  370. <Button
  371. style={{ marginTop: 8 }}
  372. type={'warning'}
  373. onClick={() => {
  374. setUnlimitedQuota();
  375. }}
  376. >
  377. {unlimited_quota ? t('取消无限额度') : t('设为无限额度')}
  378. </Button>
  379. </div>
  380. <Divider />
  381. <div style={{ marginTop: 10 }}>
  382. <Typography.Text>{t('IP白名单(请勿过度信任此功能)')}</Typography.Text>
  383. </div>
  384. <TextArea
  385. label={t('IP白名单')}
  386. name='allow_ips'
  387. placeholder={t('允许的IP,一行一个,不填写则不限制')}
  388. onChange={(value) => {
  389. handleInputChange('allow_ips', value);
  390. }}
  391. value={inputs.allow_ips}
  392. style={{ fontFamily: 'JetBrains Mono, Consolas' }}
  393. />
  394. <div style={{ marginTop: 10, display: 'flex' }}>
  395. <Space>
  396. <Checkbox
  397. name='model_limits_enabled'
  398. checked={model_limits_enabled}
  399. onChange={(e) =>
  400. handleInputChange('model_limits_enabled', e.target.checked)
  401. }
  402. >
  403. {t('启用模型限制(非必要,不建议启用)')}
  404. </Checkbox>
  405. </Space>
  406. </div>
  407. <Select
  408. style={{ marginTop: 8 }}
  409. placeholder={t('请选择该渠道所支持的模型')}
  410. name='models'
  411. required
  412. multiple
  413. selection
  414. onChange={(value) => {
  415. handleInputChange('model_limits', value);
  416. }}
  417. value={inputs.model_limits}
  418. autoComplete='new-password'
  419. optionList={models}
  420. disabled={!model_limits_enabled}
  421. />
  422. <div style={{ marginTop: 10 }}>
  423. <Typography.Text>{t('令牌分组,默认为用户的分组')}</Typography.Text>
  424. </div>
  425. {groups.length > 0 ?
  426. <Select
  427. style={{ marginTop: 8 }}
  428. placeholder={t('令牌分组,默认为用户的分组')}
  429. name='gruop'
  430. required
  431. selection
  432. onChange={(value) => {
  433. handleInputChange('group', value);
  434. }}
  435. position={'topLeft'}
  436. renderOptionItem={renderGroupOption}
  437. value={inputs.group}
  438. autoComplete='new-password'
  439. optionList={groups}
  440. />:
  441. <Select
  442. style={{ marginTop: 8 }}
  443. placeholder={t('管理员未设置用户可选分组')}
  444. name='gruop'
  445. disabled={true}
  446. />
  447. }
  448. </Spin>
  449. </SideSheet>
  450. </>
  451. );
  452. };
  453. export default EditToken;