TokensTable.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632
  1. import React, { useEffect, useState } from 'react';
  2. import {
  3. API,
  4. copy,
  5. showError,
  6. showSuccess,
  7. timestamp2string,
  8. } from '../helpers';
  9. import { ITEMS_PER_PAGE } from '../constants';
  10. import {renderGroup, renderQuota} from '../helpers/render';
  11. import {
  12. Button, Divider,
  13. Dropdown,
  14. Form,
  15. Modal,
  16. Popconfirm,
  17. Popover, Space,
  18. SplitButtonGroup,
  19. Table,
  20. Tag,
  21. } from '@douyinfe/semi-ui';
  22. import { IconTreeTriangleDown } from '@douyinfe/semi-icons';
  23. import EditToken from '../pages/Token/EditToken';
  24. import { useTranslation } from 'react-i18next';
  25. function renderTimestamp(timestamp) {
  26. return <>{timestamp2string(timestamp)}</>;
  27. }
  28. const TokensTable = () => {
  29. const { t } = useTranslation();
  30. const renderStatus = (status, model_limits_enabled = false) => {
  31. switch (status) {
  32. case 1:
  33. if (model_limits_enabled) {
  34. return (
  35. <Tag color='green' size='large'>
  36. {t('已启用:限制模型')}
  37. </Tag>
  38. );
  39. } else {
  40. return (
  41. <Tag color='green' size='large'>
  42. {t('已启用')}
  43. </Tag>
  44. );
  45. }
  46. case 2:
  47. return (
  48. <Tag color='red' size='large'>
  49. {t('已禁用')}
  50. </Tag>
  51. );
  52. case 3:
  53. return (
  54. <Tag color='yellow' size='large'>
  55. {t('已过期')}
  56. </Tag>
  57. );
  58. case 4:
  59. return (
  60. <Tag color='grey' size='large'>
  61. {t('已耗尽')}
  62. </Tag>
  63. );
  64. default:
  65. return (
  66. <Tag color='black' size='large'>
  67. {t('未知状态')}
  68. </Tag>
  69. );
  70. }
  71. };
  72. const columns = [
  73. {
  74. title: t('名称'),
  75. dataIndex: 'name',
  76. },
  77. {
  78. title: t('状态'),
  79. dataIndex: 'status',
  80. key: 'status',
  81. render: (text, record, index) => {
  82. return <div>
  83. <Space>
  84. {renderStatus(text, record.model_limits_enabled)}
  85. {renderGroup(record.group)}
  86. </Space>
  87. </div>;
  88. },
  89. },
  90. {
  91. title: t('已用额度'),
  92. dataIndex: 'used_quota',
  93. render: (text, record, index) => {
  94. return <div>{renderQuota(parseInt(text))}</div>;
  95. },
  96. },
  97. {
  98. title: t('剩余额度'),
  99. dataIndex: 'remain_quota',
  100. render: (text, record, index) => {
  101. return (
  102. <div>
  103. {record.unlimited_quota ? (
  104. <Tag size={'large'} color={'white'}>
  105. {t('无限制')}
  106. </Tag>
  107. ) : (
  108. <Tag size={'large'} color={'light-blue'}>
  109. {renderQuota(parseInt(text))}
  110. </Tag>
  111. )}
  112. </div>
  113. );
  114. },
  115. },
  116. {
  117. title: t('创建时间'),
  118. dataIndex: 'created_time',
  119. render: (text, record, index) => {
  120. return <div>{renderTimestamp(text)}</div>;
  121. },
  122. },
  123. {
  124. title: t('过期时间'),
  125. dataIndex: 'expired_time',
  126. render: (text, record, index) => {
  127. return (
  128. <div>
  129. {record.expired_time === -1 ? t('永不过期') : renderTimestamp(text)}
  130. </div>
  131. );
  132. },
  133. },
  134. {
  135. title: '',
  136. dataIndex: 'operate',
  137. render: (text, record, index) => {
  138. let chats = localStorage.getItem('chats');
  139. let chatsArray = []
  140. let chatLink = localStorage.getItem('chat_link');
  141. let mjLink = localStorage.getItem('chat_link2');
  142. let shouldUseCustom = true;
  143. if (chatLink) {
  144. shouldUseCustom = false;
  145. chatLink += `/#/?settings={"key":"{key}","url":"{address}"}`;
  146. chatsArray.push({
  147. node: 'item',
  148. key: 'default',
  149. name: 'ChatGPT Next Web',
  150. onClick: () => {
  151. onOpenLink('default', chatLink, record);
  152. },
  153. });
  154. }
  155. if (mjLink) {
  156. shouldUseCustom = false;
  157. mjLink += `/#/?settings={"key":"{key}","url":"{address}"}`;
  158. chatsArray.push({
  159. node: 'item',
  160. key: 'mj',
  161. name: 'ChatGPT Next Midjourney',
  162. onClick: () => {
  163. onOpenLink('mj', mjLink, record);
  164. },
  165. });
  166. }
  167. if (shouldUseCustom) {
  168. try {
  169. // console.log(chats);
  170. chats = JSON.parse(chats);
  171. // check chats is array
  172. if (Array.isArray(chats)) {
  173. for (let i = 0; i < chats.length; i++) {
  174. let chat = {}
  175. chat.node = 'item';
  176. // c is a map
  177. // chat.key = chats[i].name;
  178. // console.log(chats[i])
  179. for (let key in chats[i]) {
  180. if (chats[i].hasOwnProperty(key)) {
  181. chat.key = i;
  182. chat.name = key;
  183. chat.onClick = () => {
  184. onOpenLink(key, chats[i][key], record);
  185. }
  186. }
  187. }
  188. chatsArray.push(chat);
  189. }
  190. }
  191. } catch (e) {
  192. console.log(e);
  193. showError(t('聊天链接配置错误,请联系管理员'));
  194. }
  195. }
  196. return (
  197. <div>
  198. <Popover
  199. content={'sk-' + record.key}
  200. style={{ padding: 20 }}
  201. position='top'
  202. >
  203. <Button theme='light' type='tertiary' style={{ marginRight: 1 }}>
  204. {t('查看')}
  205. </Button>
  206. </Popover>
  207. <Button
  208. theme='light'
  209. type='secondary'
  210. style={{ marginRight: 1 }}
  211. onClick={async (text) => {
  212. await copyText('sk-' + record.key);
  213. }}
  214. >
  215. {t('复制')}
  216. </Button>
  217. <SplitButtonGroup
  218. style={{ marginRight: 1 }}
  219. aria-label={t('项目操作按钮组')}
  220. >
  221. <Button
  222. theme='light'
  223. style={{ color: 'rgba(var(--semi-teal-7), 1)' }}
  224. onClick={() => {
  225. if (chatsArray.length === 0) {
  226. showError(t('请联系管理员配置聊天链接'));
  227. } else {
  228. onOpenLink('default', chats[0][Object.keys(chats[0])[0]], record);
  229. }
  230. }}
  231. >
  232. {t('聊天')}
  233. </Button>
  234. <Dropdown
  235. trigger='click'
  236. position='bottomRight'
  237. menu={chatsArray}
  238. >
  239. <Button
  240. style={{
  241. padding: '8px 4px',
  242. color: 'rgba(var(--semi-teal-7), 1)',
  243. }}
  244. type='primary'
  245. icon={<IconTreeTriangleDown />}
  246. ></Button>
  247. </Dropdown>
  248. </SplitButtonGroup>
  249. <Popconfirm
  250. title={t('确定是否要删除此令牌?')}
  251. content={t('此修改将不可逆')}
  252. okType={'danger'}
  253. position={'left'}
  254. onConfirm={() => {
  255. manageToken(record.id, 'delete', record).then(() => {
  256. removeRecord(record.key);
  257. });
  258. }}
  259. >
  260. <Button theme='light' type='danger' style={{ marginRight: 1 }}>
  261. {t('删除')}
  262. </Button>
  263. </Popconfirm>
  264. {record.status === 1 ? (
  265. <Button
  266. theme='light'
  267. type='warning'
  268. style={{ marginRight: 1 }}
  269. onClick={async () => {
  270. manageToken(record.id, 'disable', record);
  271. }}
  272. >
  273. {t('禁用')}
  274. </Button>
  275. ) : (
  276. <Button
  277. theme='light'
  278. type='secondary'
  279. style={{ marginRight: 1 }}
  280. onClick={async () => {
  281. manageToken(record.id, 'enable', record);
  282. }}
  283. >
  284. {t('启用')}
  285. </Button>
  286. )}
  287. <Button
  288. theme='light'
  289. type='tertiary'
  290. style={{ marginRight: 1 }}
  291. onClick={() => {
  292. setEditingToken(record);
  293. setShowEdit(true);
  294. }}
  295. >
  296. {t('编辑')}
  297. </Button>
  298. </div>
  299. );
  300. },
  301. },
  302. ];
  303. const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
  304. const [showEdit, setShowEdit] = useState(false);
  305. const [tokens, setTokens] = useState([]);
  306. const [selectedKeys, setSelectedKeys] = useState([]);
  307. const [tokenCount, setTokenCount] = useState(pageSize);
  308. const [loading, setLoading] = useState(true);
  309. const [activePage, setActivePage] = useState(1);
  310. const [searchKeyword, setSearchKeyword] = useState('');
  311. const [searchToken, setSearchToken] = useState('');
  312. const [searching, setSearching] = useState(false);
  313. const [chats, setChats] = useState([]);
  314. const [editingToken, setEditingToken] = useState({
  315. id: undefined,
  316. });
  317. const closeEdit = () => {
  318. setShowEdit(false);
  319. setTimeout(() => {
  320. setEditingToken({
  321. id: undefined,
  322. });
  323. }, 500);
  324. };
  325. const setTokensFormat = (tokens) => {
  326. setTokens(tokens);
  327. if (tokens.length >= pageSize) {
  328. setTokenCount(tokens.length + pageSize);
  329. } else {
  330. setTokenCount(tokens.length);
  331. }
  332. };
  333. let pageData = tokens.slice(
  334. (activePage - 1) * pageSize,
  335. activePage * pageSize,
  336. );
  337. const loadTokens = async (startIdx) => {
  338. setLoading(true);
  339. const res = await API.get(`/api/token/?p=${startIdx}&size=${pageSize}`);
  340. const { success, message, data } = res.data;
  341. if (success) {
  342. if (startIdx === 0) {
  343. setTokensFormat(data);
  344. } else {
  345. let newTokens = [...tokens];
  346. newTokens.splice(startIdx * pageSize, data.length, ...data);
  347. setTokensFormat(newTokens);
  348. }
  349. } else {
  350. showError(message);
  351. }
  352. setLoading(false);
  353. };
  354. const refresh = async () => {
  355. await loadTokens(activePage - 1);
  356. };
  357. const copyText = async (text) => {
  358. if (await copy(text)) {
  359. showSuccess(t('已复制到剪贴板!'));
  360. } else {
  361. Modal.error({
  362. title: t('无法复制到剪贴板,请手动复制'),
  363. content: text,
  364. size: 'large',
  365. });
  366. }
  367. };
  368. const onOpenLink = async (type, url, record) => {
  369. // console.log(type, url, key);
  370. let status = localStorage.getItem('status');
  371. let serverAddress = '';
  372. if (status) {
  373. status = JSON.parse(status);
  374. serverAddress = status.server_address;
  375. }
  376. if (serverAddress === '') {
  377. serverAddress = window.location.origin;
  378. }
  379. let encodedServerAddress = encodeURIComponent(serverAddress);
  380. url = url.replaceAll('{address}', encodedServerAddress);
  381. url = url.replaceAll('{key}', 'sk-' + record.key);
  382. window.open(url, '_blank');
  383. };
  384. useEffect(() => {
  385. loadTokens(0)
  386. .then()
  387. .catch((reason) => {
  388. showError(reason);
  389. });
  390. }, [pageSize]);
  391. const removeRecord = (key) => {
  392. let newDataSource = [...tokens];
  393. if (key != null) {
  394. let idx = newDataSource.findIndex((data) => data.key === key);
  395. if (idx > -1) {
  396. newDataSource.splice(idx, 1);
  397. setTokensFormat(newDataSource);
  398. }
  399. }
  400. };
  401. const manageToken = async (id, action, record) => {
  402. setLoading(true);
  403. let data = { id };
  404. let res;
  405. switch (action) {
  406. case 'delete':
  407. res = await API.delete(`/api/token/${id}/`);
  408. break;
  409. case 'enable':
  410. data.status = 1;
  411. res = await API.put('/api/token/?status_only=true', data);
  412. break;
  413. case 'disable':
  414. data.status = 2;
  415. res = await API.put('/api/token/?status_only=true', data);
  416. break;
  417. }
  418. const { success, message } = res.data;
  419. if (success) {
  420. showSuccess('操作成功完成!');
  421. let token = res.data.data;
  422. let newTokens = [...tokens];
  423. // let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx;
  424. if (action === 'delete') {
  425. } else {
  426. record.status = token.status;
  427. // newTokens[realIdx].status = token.status;
  428. }
  429. setTokensFormat(newTokens);
  430. } else {
  431. showError(message);
  432. }
  433. setLoading(false);
  434. };
  435. const searchTokens = async () => {
  436. if (searchKeyword === '' && searchToken === '') {
  437. // if keyword is blank, load files instead.
  438. await loadTokens(0);
  439. setActivePage(1);
  440. return;
  441. }
  442. setSearching(true);
  443. const res = await API.get(
  444. `/api/token/search?keyword=${searchKeyword}&token=${searchToken}`,
  445. );
  446. const { success, message, data } = res.data;
  447. if (success) {
  448. setTokensFormat(data);
  449. setActivePage(1);
  450. } else {
  451. showError(message);
  452. }
  453. setSearching(false);
  454. };
  455. const handleKeywordChange = async (value) => {
  456. setSearchKeyword(value.trim());
  457. };
  458. const handleSearchTokenChange = async (value) => {
  459. setSearchToken(value.trim());
  460. };
  461. const sortToken = (key) => {
  462. if (tokens.length === 0) return;
  463. setLoading(true);
  464. let sortedTokens = [...tokens];
  465. sortedTokens.sort((a, b) => {
  466. return ('' + a[key]).localeCompare(b[key]);
  467. });
  468. if (sortedTokens[0].id === tokens[0].id) {
  469. sortedTokens.reverse();
  470. }
  471. setTokens(sortedTokens);
  472. setLoading(false);
  473. };
  474. const handlePageChange = (page) => {
  475. setActivePage(page);
  476. if (page === Math.ceil(tokens.length / pageSize) + 1) {
  477. // In this case we have to load more data and then append them.
  478. loadTokens(page - 1).then((r) => {});
  479. }
  480. };
  481. const rowSelection = {
  482. onSelect: (record, selected) => {},
  483. onSelectAll: (selected, selectedRows) => {},
  484. onChange: (selectedRowKeys, selectedRows) => {
  485. setSelectedKeys(selectedRows);
  486. },
  487. };
  488. const handleRow = (record, index) => {
  489. if (record.status !== 1) {
  490. return {
  491. style: {
  492. background: 'var(--semi-color-disabled-border)',
  493. },
  494. };
  495. } else {
  496. return {};
  497. }
  498. };
  499. return (
  500. <>
  501. <EditToken
  502. refresh={refresh}
  503. editingToken={editingToken}
  504. visiable={showEdit}
  505. handleClose={closeEdit}
  506. ></EditToken>
  507. <Form
  508. layout='horizontal'
  509. style={{ marginTop: 10 }}
  510. labelPosition={'left'}
  511. >
  512. <Form.Input
  513. field='keyword'
  514. label={t('搜索关键字')}
  515. placeholder={t('令牌名称')}
  516. value={searchKeyword}
  517. loading={searching}
  518. onChange={handleKeywordChange}
  519. />
  520. <Form.Input
  521. field='token'
  522. label={t('密钥')}
  523. placeholder={t('密钥')}
  524. value={searchToken}
  525. loading={searching}
  526. onChange={handleSearchTokenChange}
  527. />
  528. <Button
  529. label={t('查询')}
  530. type='primary'
  531. htmlType='submit'
  532. className='btn-margin-right'
  533. onClick={searchTokens}
  534. style={{ marginRight: 8 }}
  535. >
  536. {t('查询')}
  537. </Button>
  538. </Form>
  539. <Divider style={{margin:'15px 0'}}/>
  540. <div>
  541. <Button
  542. theme='light'
  543. type='primary'
  544. style={{ marginRight: 8 }}
  545. onClick={() => {
  546. setEditingToken({
  547. id: undefined,
  548. });
  549. setShowEdit(true);
  550. }}
  551. >
  552. {t('添加令牌')}
  553. </Button>
  554. <Button
  555. label={t('复制所选令牌')}
  556. type='warning'
  557. onClick={async () => {
  558. if (selectedKeys.length === 0) {
  559. showError(t('请至少选择一个令牌!'));
  560. return;
  561. }
  562. let keys = '';
  563. for (let i = 0; i < selectedKeys.length; i++) {
  564. keys +=
  565. selectedKeys[i].name + ' sk-' + selectedKeys[i].key + '\n';
  566. }
  567. await copyText(keys);
  568. }}
  569. >
  570. {t('复制所选令牌到剪贴板')}
  571. </Button>
  572. </div>
  573. <Table
  574. style={{ marginTop: 20 }}
  575. columns={columns}
  576. dataSource={pageData}
  577. pagination={{
  578. currentPage: activePage,
  579. pageSize: pageSize,
  580. total: tokenCount,
  581. showSizeChanger: true,
  582. pageSizeOptions: [10, 20, 50, 100],
  583. formatPageText: (page) =>
  584. t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
  585. start: page.currentStart,
  586. end: page.currentEnd,
  587. total: tokens.length
  588. }),
  589. onPageSizeChange: (size) => {
  590. setPageSize(size);
  591. setActivePage(1);
  592. },
  593. onPageChange: handlePageChange,
  594. }}
  595. loading={loading}
  596. rowSelection={rowSelection}
  597. onRow={handleRow}
  598. ></Table>
  599. </>
  600. );
  601. };
  602. export default TokensTable;