EditToken.js 11 KB

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