EditToken.js 13 KB

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