EditToken.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597
  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,
  22. TextArea,
  23. Typography,
  24. Card,
  25. Tag,
  26. } from '@douyinfe/semi-ui';
  27. import {
  28. IconClock,
  29. IconCalendar,
  30. IconCreditCard,
  31. IconLink,
  32. IconServer,
  33. IconUserGroup,
  34. IconSave,
  35. IconClose,
  36. IconPlusCircle,
  37. } from '@douyinfe/semi-icons';
  38. import { useTranslation } from 'react-i18next';
  39. const { Text, Title } = Typography;
  40. const EditToken = (props) => {
  41. const { t } = useTranslation();
  42. const [isEdit, setIsEdit] = useState(false);
  43. const [loading, setLoading] = useState(isEdit);
  44. const originInputs = {
  45. name: '',
  46. remain_quota: isEdit ? 0 : 500000,
  47. expired_time: -1,
  48. unlimited_quota: false,
  49. model_limits_enabled: false,
  50. model_limits: [],
  51. allow_ips: '',
  52. group: '',
  53. };
  54. const [inputs, setInputs] = useState(originInputs);
  55. const {
  56. name,
  57. remain_quota,
  58. expired_time,
  59. unlimited_quota,
  60. model_limits_enabled,
  61. model_limits,
  62. allow_ips,
  63. group,
  64. } = inputs;
  65. const [models, setModels] = useState([]);
  66. const [groups, setGroups] = useState([]);
  67. const navigate = useNavigate();
  68. const handleInputChange = (name, value) => {
  69. setInputs((inputs) => ({ ...inputs, [name]: value }));
  70. };
  71. const handleCancel = () => {
  72. props.handleClose();
  73. };
  74. const setExpiredTime = (month, day, hour, minute) => {
  75. let now = new Date();
  76. let timestamp = now.getTime() / 1000;
  77. let seconds = month * 30 * 24 * 60 * 60;
  78. seconds += day * 24 * 60 * 60;
  79. seconds += hour * 60 * 60;
  80. seconds += minute * 60;
  81. if (seconds !== 0) {
  82. timestamp += seconds;
  83. setInputs({ ...inputs, expired_time: timestamp2string(timestamp) });
  84. } else {
  85. setInputs({ ...inputs, expired_time: -1 });
  86. }
  87. };
  88. const setUnlimitedQuota = () => {
  89. setInputs({ ...inputs, unlimited_quota: !unlimited_quota });
  90. };
  91. const loadModels = async () => {
  92. let res = await API.get(`/api/user/models`);
  93. const { success, message, data } = res.data;
  94. if (success) {
  95. let localModelOptions = data.map((model) => ({
  96. label: model,
  97. value: model,
  98. }));
  99. setModels(localModelOptions);
  100. } else {
  101. showError(t(message));
  102. }
  103. };
  104. const loadGroups = async () => {
  105. let res = await API.get(`/api/user/self/groups`);
  106. const { success, message, data } = res.data;
  107. if (success) {
  108. let localGroupOptions = Object.entries(data).map(([group, info]) => ({
  109. label: info.desc,
  110. value: group,
  111. ratio: info.ratio,
  112. }));
  113. setGroups(localGroupOptions);
  114. } else {
  115. showError(t(message));
  116. }
  117. };
  118. const loadToken = async () => {
  119. setLoading(true);
  120. let res = await API.get(`/api/token/${props.editingToken.id}`);
  121. const { success, message, data } = res.data;
  122. if (success) {
  123. if (data.expired_time !== -1) {
  124. data.expired_time = timestamp2string(data.expired_time);
  125. }
  126. if (data.model_limits !== '') {
  127. data.model_limits = data.model_limits.split(',');
  128. } else {
  129. data.model_limits = [];
  130. }
  131. setInputs(data);
  132. } else {
  133. showError(message);
  134. }
  135. setLoading(false);
  136. };
  137. useEffect(() => {
  138. setIsEdit(props.editingToken.id !== undefined);
  139. }, [props.editingToken.id]);
  140. useEffect(() => {
  141. if (!isEdit) {
  142. setInputs(originInputs);
  143. } else {
  144. loadToken().then(() => {
  145. // console.log(inputs);
  146. });
  147. }
  148. loadModels();
  149. loadGroups();
  150. }, [isEdit]);
  151. // 新增 state 变量 tokenCount 来记录用户想要创建的令牌数量,默认为 1
  152. const [tokenCount, setTokenCount] = useState(1);
  153. // 新增处理 tokenCount 变化的函数
  154. const handleTokenCountChange = (value) => {
  155. // 确保用户输入的是正整数
  156. const count = parseInt(value, 10);
  157. if (!isNaN(count) && count > 0) {
  158. setTokenCount(count);
  159. }
  160. };
  161. // 生成一个随机的四位字母数字字符串
  162. const generateRandomSuffix = () => {
  163. const characters =
  164. 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  165. let result = '';
  166. for (let i = 0; i < 6; i++) {
  167. result += characters.charAt(
  168. Math.floor(Math.random() * characters.length),
  169. );
  170. }
  171. return result;
  172. };
  173. const submit = async () => {
  174. setLoading(true);
  175. if (isEdit) {
  176. // 编辑令牌的逻辑保持不变
  177. let localInputs = { ...inputs };
  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(t('过期时间格式错误!'));
  183. setLoading(false);
  184. return;
  185. }
  186. localInputs.expired_time = Math.ceil(time / 1000);
  187. }
  188. localInputs.model_limits = localInputs.model_limits.join(',');
  189. let res = await API.put(`/api/token/`, {
  190. ...localInputs,
  191. id: parseInt(props.editingToken.id),
  192. });
  193. const { success, message } = res.data;
  194. if (success) {
  195. showSuccess(t('令牌更新成功!'));
  196. props.refresh();
  197. props.handleClose();
  198. } else {
  199. showError(t(message));
  200. }
  201. } else {
  202. // 处理新增多个令牌的情况
  203. let successCount = 0; // 记录成功创建的令牌数量
  204. for (let i = 0; i < tokenCount; i++) {
  205. let localInputs = { ...inputs };
  206. if (i !== 0) {
  207. // 如果用户想要创建多个令牌,则给每个令牌一个序号后缀
  208. localInputs.name = `${inputs.name}-${generateRandomSuffix()}`;
  209. }
  210. localInputs.remain_quota = parseInt(localInputs.remain_quota);
  211. if (localInputs.expired_time !== -1) {
  212. let time = Date.parse(localInputs.expired_time);
  213. if (isNaN(time)) {
  214. showError(t('过期时间格式错误!'));
  215. setLoading(false);
  216. break;
  217. }
  218. localInputs.expired_time = Math.ceil(time / 1000);
  219. }
  220. localInputs.model_limits = localInputs.model_limits.join(',');
  221. let res = await API.post(`/api/token/`, localInputs);
  222. const { success, message } = res.data;
  223. if (success) {
  224. successCount++;
  225. } else {
  226. showError(t(message));
  227. break; // 如果创建失败,终止循环
  228. }
  229. }
  230. if (successCount > 0) {
  231. showSuccess(t('令牌创建成功,请在列表页面点击复制获取令牌!'));
  232. props.refresh();
  233. props.handleClose();
  234. }
  235. }
  236. setLoading(false);
  237. setInputs(originInputs); // 重置表单
  238. setTokenCount(1); // 重置数量为默认值
  239. };
  240. return (
  241. <SideSheet
  242. placement={isEdit ? 'right' : 'left'}
  243. title={
  244. <Space>
  245. {isEdit ?
  246. <Tag color="blue" shape="circle">{t('更新')}</Tag> :
  247. <Tag color="green" shape="circle">{t('新建')}</Tag>
  248. }
  249. <Title heading={4} className="m-0">
  250. {isEdit ? t('更新令牌信息') : t('创建新的令牌')}
  251. </Title>
  252. </Space>
  253. }
  254. headerStyle={{
  255. borderBottom: '1px solid var(--semi-color-border)',
  256. padding: '24px'
  257. }}
  258. bodyStyle={{
  259. backgroundColor: 'var(--semi-color-bg-0)',
  260. padding: '0'
  261. }}
  262. visible={props.visiable}
  263. width={isMobile() ? '100%' : 600}
  264. footer={
  265. <div className="flex justify-end bg-white">
  266. <Space>
  267. <Button
  268. theme="solid"
  269. size="large"
  270. className="!rounded-full"
  271. onClick={submit}
  272. icon={<IconSave />}
  273. loading={loading}
  274. >
  275. {t('提交')}
  276. </Button>
  277. <Button
  278. theme="light"
  279. size="large"
  280. className="!rounded-full"
  281. type="primary"
  282. onClick={handleCancel}
  283. icon={<IconClose />}
  284. >
  285. {t('取消')}
  286. </Button>
  287. </Space>
  288. </div>
  289. }
  290. closeIcon={null}
  291. onCancel={() => handleCancel()}
  292. >
  293. <Spin spinning={loading}>
  294. <div className="p-6">
  295. <Card className="!rounded-2xl shadow-sm border-0 mb-6">
  296. <div className="flex items-center mb-4 p-6 rounded-xl" style={{
  297. background: 'linear-gradient(135deg, #1e3a8a 0%, #2563eb 50%, #3b82f6 100%)',
  298. position: 'relative'
  299. }}>
  300. <div className="absolute inset-0 overflow-hidden">
  301. <div className="absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full"></div>
  302. <div className="absolute -bottom-8 -left-8 w-24 h-24 bg-white opacity-10 rounded-full"></div>
  303. </div>
  304. <div className="w-10 h-10 rounded-full bg-white/20 flex items-center justify-center mr-4 relative">
  305. <IconPlusCircle size="large" style={{ color: '#ffffff' }} />
  306. </div>
  307. <div className="relative">
  308. <Text style={{ color: '#ffffff' }} className="text-lg font-medium">{t('基本信息')}</Text>
  309. <div style={{ color: '#ffffff' }} className="text-sm opacity-80">{t('设置令牌的基本信息')}</div>
  310. </div>
  311. </div>
  312. <div className="space-y-4">
  313. <div>
  314. <Text strong className="block mb-2">{t('名称')}</Text>
  315. <Input
  316. placeholder={t('请输入名称')}
  317. onChange={(value) => handleInputChange('name', value)}
  318. value={name}
  319. autoComplete="new-password"
  320. size="large"
  321. className="!rounded-lg"
  322. showClear
  323. required
  324. />
  325. </div>
  326. <div>
  327. <Text strong className="block mb-2">{t('过期时间')}</Text>
  328. <div className="mb-2">
  329. <DatePicker
  330. placeholder={t('请选择过期时间')}
  331. onChange={(value) => handleInputChange('expired_time', value)}
  332. value={expired_time}
  333. autoComplete="new-password"
  334. type="dateTime"
  335. className="w-full !rounded-lg"
  336. size="large"
  337. prefix={<IconCalendar />}
  338. />
  339. </div>
  340. <div className="flex flex-wrap gap-2">
  341. <Button
  342. theme="light"
  343. type="primary"
  344. onClick={() => setExpiredTime(0, 0, 0, 0)}
  345. className="!rounded-full"
  346. >
  347. {t('永不过期')}
  348. </Button>
  349. <Button
  350. theme="light"
  351. type="tertiary"
  352. onClick={() => setExpiredTime(0, 0, 1, 0)}
  353. className="!rounded-full"
  354. icon={<IconClock />}
  355. >
  356. {t('一小时')}
  357. </Button>
  358. <Button
  359. theme="light"
  360. type="tertiary"
  361. onClick={() => setExpiredTime(0, 1, 0, 0)}
  362. className="!rounded-full"
  363. icon={<IconCalendar />}
  364. >
  365. {t('一天')}
  366. </Button>
  367. <Button
  368. theme="light"
  369. type="tertiary"
  370. onClick={() => setExpiredTime(1, 0, 0, 0)}
  371. className="!rounded-full"
  372. icon={<IconCalendar />}
  373. >
  374. {t('一个月')}
  375. </Button>
  376. </div>
  377. </div>
  378. </div>
  379. </Card>
  380. <Card className="!rounded-2xl shadow-sm border-0 mb-6">
  381. <div className="flex items-center mb-4 p-6 rounded-xl" style={{
  382. background: 'linear-gradient(135deg, #065f46 0%, #059669 50%, #10b981 100%)',
  383. position: 'relative'
  384. }}>
  385. <div className="absolute inset-0 overflow-hidden">
  386. <div className="absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full"></div>
  387. <div className="absolute -bottom-8 -left-8 w-24 h-24 bg-white opacity-10 rounded-full"></div>
  388. </div>
  389. <div className="w-10 h-10 rounded-full bg-white/20 flex items-center justify-center mr-4 relative">
  390. <IconCreditCard size="large" style={{ color: '#ffffff' }} />
  391. </div>
  392. <div className="relative">
  393. <Text style={{ color: '#ffffff' }} className="text-lg font-medium">{t('额度设置')}</Text>
  394. <div style={{ color: '#ffffff' }} className="text-sm opacity-80">{t('设置令牌可用额度和数量')}</div>
  395. </div>
  396. </div>
  397. <Banner
  398. type="warning"
  399. description={t('注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。')}
  400. className="mb-4 !rounded-lg"
  401. />
  402. <div className="space-y-4">
  403. <div>
  404. <div className="flex justify-between mb-2">
  405. <Text strong>{t('额度')}</Text>
  406. <Text type="tertiary">{renderQuotaWithPrompt(remain_quota)}</Text>
  407. </div>
  408. <AutoComplete
  409. placeholder={t('请输入额度')}
  410. onChange={(value) => handleInputChange('remain_quota', value)}
  411. value={remain_quota}
  412. autoComplete="new-password"
  413. type="number"
  414. size="large"
  415. className="w-full !rounded-lg"
  416. prefix={<IconCreditCard />}
  417. data={[
  418. { value: 500000, label: '1$' },
  419. { value: 5000000, label: '10$' },
  420. { value: 25000000, label: '50$' },
  421. { value: 50000000, label: '100$' },
  422. { value: 250000000, label: '500$' },
  423. { value: 500000000, label: '1000$' },
  424. ]}
  425. disabled={unlimited_quota}
  426. />
  427. </div>
  428. {!isEdit && (
  429. <div>
  430. <Text strong className="block mb-2">{t('新建数量')}</Text>
  431. <AutoComplete
  432. placeholder={t('请选择或输入创建令牌的数量')}
  433. onChange={(value) => handleTokenCountChange(value)}
  434. onSelect={(value) => handleTokenCountChange(value)}
  435. value={tokenCount.toString()}
  436. autoComplete="off"
  437. type="number"
  438. className="w-full !rounded-lg"
  439. size="large"
  440. prefix={<IconPlusCircle />}
  441. data={[
  442. { value: 10, label: t('10个') },
  443. { value: 20, label: t('20个') },
  444. { value: 30, label: t('30个') },
  445. { value: 100, label: t('100个') },
  446. ]}
  447. disabled={unlimited_quota}
  448. />
  449. </div>
  450. )}
  451. <div className="flex justify-end">
  452. <Button
  453. theme="light"
  454. type={unlimited_quota ? "danger" : "warning"}
  455. onClick={setUnlimitedQuota}
  456. className="!rounded-full"
  457. >
  458. {unlimited_quota ? t('取消无限额度') : t('设为无限额度')}
  459. </Button>
  460. </div>
  461. </div>
  462. </Card>
  463. <Card className="!rounded-2xl shadow-sm border-0 mb-6">
  464. <div className="flex items-center mb-4 p-6 rounded-xl" style={{
  465. background: 'linear-gradient(135deg, #4c1d95 0%, #6d28d9 50%, #7c3aed 100%)',
  466. position: 'relative'
  467. }}>
  468. <div className="absolute inset-0 overflow-hidden">
  469. <div className="absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full"></div>
  470. <div className="absolute -bottom-8 -left-8 w-24 h-24 bg-white opacity-10 rounded-full"></div>
  471. </div>
  472. <div className="w-10 h-10 rounded-full bg-white/20 flex items-center justify-center mr-4 relative">
  473. <IconLink size="large" style={{ color: '#ffffff' }} />
  474. </div>
  475. <div className="relative">
  476. <Text style={{ color: '#ffffff' }} className="text-lg font-medium">{t('访问限制')}</Text>
  477. <div style={{ color: '#ffffff' }} className="text-sm opacity-80">{t('设置令牌的访问限制')}</div>
  478. </div>
  479. </div>
  480. <div className="space-y-4">
  481. <div>
  482. <Text strong className="block mb-2">{t('IP白名单')}</Text>
  483. <TextArea
  484. placeholder={t('允许的IP,一行一个,不填写则不限制')}
  485. onChange={(value) => handleInputChange('allow_ips', value)}
  486. value={inputs.allow_ips}
  487. style={{ fontFamily: 'JetBrains Mono, Consolas' }}
  488. className="!rounded-lg"
  489. rows={4}
  490. />
  491. <Text type="tertiary" className="mt-1 block text-xs">{t('请勿过度信任此功能,IP可能被伪造')}</Text>
  492. </div>
  493. <div>
  494. <div className="flex items-center mb-2">
  495. <Checkbox
  496. checked={model_limits_enabled}
  497. onChange={(e) => handleInputChange('model_limits_enabled', e.target.checked)}
  498. >
  499. <Text strong>{t('模型限制')}</Text>
  500. </Checkbox>
  501. </div>
  502. <Select
  503. placeholder={model_limits_enabled ? t('请选择该渠道所支持的模型') : t('勾选启用模型限制后可选择')}
  504. onChange={(value) => handleInputChange('model_limits', value)}
  505. value={inputs.model_limits}
  506. multiple
  507. size="large"
  508. className="w-full !rounded-lg"
  509. prefix={<IconServer />}
  510. optionList={models}
  511. disabled={!model_limits_enabled}
  512. maxTagCount={3}
  513. />
  514. <Text type="tertiary" className="mt-1 block text-xs">{t('非必要,不建议启用模型限制')}</Text>
  515. </div>
  516. </div>
  517. </Card>
  518. <Card className="!rounded-2xl shadow-sm border-0">
  519. <div className="flex items-center mb-4 p-6 rounded-xl" style={{
  520. background: 'linear-gradient(135deg, #92400e 0%, #d97706 50%, #f59e0b 100%)',
  521. position: 'relative'
  522. }}>
  523. <div className="absolute inset-0 overflow-hidden">
  524. <div className="absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full"></div>
  525. <div className="absolute -bottom-8 -left-8 w-24 h-24 bg-white opacity-10 rounded-full"></div>
  526. </div>
  527. <div className="w-10 h-10 rounded-full bg-white/20 flex items-center justify-center mr-4 relative">
  528. <IconUserGroup size="large" style={{ color: '#ffffff' }} />
  529. </div>
  530. <div className="relative">
  531. <Text style={{ color: '#ffffff' }} className="text-lg font-medium">{t('分组信息')}</Text>
  532. <div style={{ color: '#ffffff' }} className="text-sm opacity-80">{t('设置令牌的分组')}</div>
  533. </div>
  534. </div>
  535. <div>
  536. <Text strong className="block mb-2">{t('令牌分组')}</Text>
  537. {groups.length > 0 ? (
  538. <Select
  539. placeholder={t('令牌分组,默认为用户的分组')}
  540. onChange={(value) => handleInputChange('group', value)}
  541. renderOptionItem={renderGroupOption}
  542. value={inputs.group}
  543. size="large"
  544. className="w-full !rounded-lg"
  545. prefix={<IconUserGroup />}
  546. optionList={groups}
  547. />
  548. ) : (
  549. <Select
  550. placeholder={t('管理员未设置用户可选分组')}
  551. disabled={true}
  552. size="large"
  553. className="w-full !rounded-lg"
  554. prefix={<IconUserGroup />}
  555. />
  556. )}
  557. </div>
  558. </Card>
  559. </div>
  560. </Spin>
  561. </SideSheet>
  562. );
  563. };
  564. export default EditToken;