TokensTable.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. import React, {useEffect, useState} from 'react';
  2. import {Link} from 'react-router-dom';
  3. import {API, copy, isAdmin, showError, showSuccess, showWarning, timestamp2string} from '../helpers';
  4. import {ITEMS_PER_PAGE} from '../constants';
  5. import {renderQuota, stringToColor} from '../helpers/render';
  6. import {Avatar, Tag, Table, Button, Popover, Form, Modal, Popconfirm} from "@douyinfe/semi-ui";
  7. import EditToken from "../pages/Token/EditToken";
  8. const {Column} = Table;
  9. const COPY_OPTIONS = [
  10. {key: 'next', text: 'ChatGPT Next Web', value: 'next'},
  11. {key: 'ama', text: 'AMA 问天', value: 'ama'},
  12. {key: 'opencat', text: 'OpenCat', value: 'opencat'},
  13. ];
  14. const OPEN_LINK_OPTIONS = [
  15. {key: 'ama', text: 'AMA 问天', value: 'ama'},
  16. {key: 'opencat', text: 'OpenCat', value: 'opencat'},
  17. ];
  18. function renderTimestamp(timestamp) {
  19. return (
  20. <>
  21. {timestamp2string(timestamp)}
  22. </>
  23. );
  24. }
  25. function renderStatus(status) {
  26. switch (status) {
  27. case 1:
  28. return <Tag color='green' size='large'>已启用</Tag>;
  29. case 2:
  30. return <Tag color='red' size='large'> 已禁用 </Tag>;
  31. case 3:
  32. return <Tag color='yellow' size='large'> 已过期 </Tag>;
  33. case 4:
  34. return <Tag color='grey' size='large'> 已耗尽 </Tag>;
  35. default:
  36. return <Tag color='black' size='large'> 未知状态 </Tag>;
  37. }
  38. }
  39. const TokensTable = () => {
  40. const columns = [
  41. {
  42. title: '名称',
  43. dataIndex: 'name',
  44. },
  45. {
  46. title: '状态',
  47. dataIndex: 'status',
  48. key: 'status',
  49. render: (text, record, index) => {
  50. return (
  51. <div>
  52. {renderStatus(text)}
  53. </div>
  54. );
  55. },
  56. },
  57. {
  58. title: '已用额度',
  59. dataIndex: 'used_quota',
  60. render: (text, record, index) => {
  61. return (
  62. <div>
  63. {renderQuota(parseInt(text))}
  64. </div>
  65. );
  66. },
  67. },
  68. {
  69. title: '剩余额度',
  70. dataIndex: 'remain_quota',
  71. render: (text, record, index) => {
  72. return (
  73. <div>
  74. {record.unlimited_quota ? <Tag size={'large'} color={'white'}>无限制</Tag> : <Tag size={'large'} color={'light-blue'}>{renderQuota(parseInt(text))}</Tag>}
  75. </div>
  76. );
  77. },
  78. },
  79. {
  80. title: '创建时间',
  81. dataIndex: 'created_time',
  82. render: (text, record, index) => {
  83. return (
  84. <div>
  85. {renderTimestamp(text)}
  86. </div>
  87. );
  88. },
  89. },
  90. {
  91. title: '过期时间',
  92. dataIndex: 'expired_time',
  93. render: (text, record, index) => {
  94. return (
  95. <div>
  96. {record.expired_time === -1 ? "永不过期" : renderTimestamp(text)}
  97. </div>
  98. );
  99. },
  100. },
  101. {
  102. title: '',
  103. dataIndex: 'operate',
  104. render: (text, record, index) => (
  105. <div>
  106. <Popover
  107. content={
  108. 'sk-' + record.key
  109. }
  110. style={{padding: 20}}
  111. position="top"
  112. >
  113. <Button theme='light' type='tertiary' style={{marginRight: 1}}>查看</Button>
  114. </Popover>
  115. <Button theme='light' type='secondary' style={{marginRight: 1}}
  116. onClick={async (text) => {
  117. await copyText('sk-' + record.key)
  118. }}
  119. >复制</Button>
  120. <Popconfirm
  121. title="确定是否要删除此令牌?"
  122. content="此修改将不可逆"
  123. okType={'danger'}
  124. position={'left'}
  125. onConfirm={() => {
  126. manageToken(record.id, 'delete', record).then(
  127. () => {
  128. removeRecord(record.key);
  129. }
  130. )
  131. }}
  132. >
  133. <Button theme='light' type='danger' style={{marginRight: 1}}>删除</Button>
  134. </Popconfirm>
  135. {
  136. record.status === 1 ?
  137. <Button theme='light' type='warning' style={{marginRight: 1}} onClick={
  138. async () => {
  139. manageToken(
  140. record.id,
  141. 'disable',
  142. record
  143. )
  144. }
  145. }>禁用</Button> :
  146. <Button theme='light' type='secondary' style={{marginRight: 1}} onClick={
  147. async () => {
  148. manageToken(
  149. record.id,
  150. 'enable',
  151. record
  152. );
  153. }
  154. }>启用</Button>
  155. }
  156. <Button theme='light' type='tertiary' style={{marginRight: 1}} onClick={
  157. () => {
  158. setEditingToken(record);
  159. setShowEdit(true);
  160. }
  161. }>编辑</Button>
  162. </div>
  163. ),
  164. },
  165. ];
  166. const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
  167. const [showEdit, setShowEdit] = useState(false);
  168. const [tokens, setTokens] = useState([]);
  169. const [selectedKeys, setSelectedKeys] = useState([]);
  170. const [tokenCount, setTokenCount] = useState(pageSize);
  171. const [loading, setLoading] = useState(true);
  172. const [activePage, setActivePage] = useState(1);
  173. const [searchKeyword, setSearchKeyword] = useState('');
  174. const [searchToken, setSearchToken] = useState('');
  175. const [searching, setSearching] = useState(false);
  176. const [showTopUpModal, setShowTopUpModal] = useState(false);
  177. const [targetTokenIdx, setTargetTokenIdx] = useState(0);
  178. const [editingToken, setEditingToken] = useState({
  179. id: undefined,
  180. });
  181. const closeEdit = () => {
  182. setShowEdit(false);
  183. }
  184. const setTokensFormat = (tokens) => {
  185. setTokens(tokens);
  186. if (tokens.length >= pageSize) {
  187. setTokenCount(tokens.length + pageSize);
  188. } else {
  189. setTokenCount(tokens.length);
  190. }
  191. }
  192. let pageData = tokens.slice((activePage - 1) * pageSize, activePage * pageSize);
  193. const loadTokens = async (startIdx) => {
  194. setLoading(true);
  195. const res = await API.get(`/api/token/?p=${startIdx}&size=${pageSize}`);
  196. const {success, message, data} = res.data;
  197. if (success) {
  198. if (startIdx === 0) {
  199. setTokensFormat(data);
  200. } else {
  201. let newTokens = [...tokens];
  202. newTokens.splice(startIdx * pageSize, data.length, ...data);
  203. setTokensFormat(newTokens);
  204. }
  205. } else {
  206. showError(message);
  207. }
  208. setLoading(false);
  209. };
  210. const onPaginationChange = (e, {activePage}) => {
  211. (async () => {
  212. if (activePage === Math.ceil(tokens.length / pageSize) + 1) {
  213. // In this case we have to load more data and then append them.
  214. await loadTokens(activePage - 1);
  215. }
  216. setActivePage(activePage);
  217. })();
  218. };
  219. const refresh = async () => {
  220. await loadTokens(activePage - 1);
  221. };
  222. const onCopy = async (type, key) => {
  223. let status = localStorage.getItem('status');
  224. let serverAddress = '';
  225. if (status) {
  226. status = JSON.parse(status);
  227. serverAddress = status.server_address;
  228. }
  229. if (serverAddress === '') {
  230. serverAddress = window.location.origin;
  231. }
  232. let encodedServerAddress = encodeURIComponent(serverAddress);
  233. const nextLink = localStorage.getItem('chat_link');
  234. let nextUrl;
  235. if (nextLink) {
  236. nextUrl = nextLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
  237. } else {
  238. nextUrl = `https://chat.oneapi.pro/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
  239. }
  240. let url;
  241. switch (type) {
  242. case 'ama':
  243. url = `ama://set-api-key?server=${encodedServerAddress}&key=sk-${key}`;
  244. break;
  245. case 'opencat':
  246. url = `opencat://team/join?domain=${encodedServerAddress}&token=sk-${key}`;
  247. break;
  248. case 'next':
  249. url = nextUrl;
  250. break;
  251. default:
  252. url = `sk-${key}`;
  253. }
  254. // if (await copy(url)) {
  255. // showSuccess('已复制到剪贴板!');
  256. // } else {
  257. // showWarning('无法复制到剪贴板,请手动复制,已将令牌填入搜索框。');
  258. // setSearchKeyword(url);
  259. // }
  260. };
  261. const copyText = async (text) => {
  262. if (await copy(text)) {
  263. showSuccess('已复制到剪贴板!');
  264. } else {
  265. // setSearchKeyword(text);
  266. Modal.error({ title: '无法复制到剪贴板,请手动复制', content: text });
  267. }
  268. }
  269. const onOpenLink = async (type, key) => {
  270. let status = localStorage.getItem('status');
  271. let serverAddress = '';
  272. if (status) {
  273. status = JSON.parse(status);
  274. serverAddress = status.server_address;
  275. }
  276. if (serverAddress === '') {
  277. serverAddress = window.location.origin;
  278. }
  279. let encodedServerAddress = encodeURIComponent(serverAddress);
  280. const chatLink = localStorage.getItem('chat_link');
  281. let defaultUrl;
  282. if (chatLink) {
  283. defaultUrl = chatLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
  284. } else {
  285. defaultUrl = `https://chat.oneapi.pro/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
  286. }
  287. let url;
  288. switch (type) {
  289. case 'ama':
  290. url = `ama://set-api-key?server=${encodedServerAddress}&key=sk-${key}`;
  291. break;
  292. case 'opencat':
  293. url = `opencat://team/join?domain=${encodedServerAddress}&token=sk-${key}`;
  294. break;
  295. default:
  296. url = defaultUrl;
  297. }
  298. window.open(url, '_blank');
  299. }
  300. useEffect(() => {
  301. loadTokens(0)
  302. .then()
  303. .catch((reason) => {
  304. showError(reason);
  305. });
  306. }, [pageSize]);
  307. const removeRecord = key => {
  308. let newDataSource = [...tokens];
  309. if (key != null) {
  310. let idx = newDataSource.findIndex(data => data.key === key);
  311. if (idx > -1) {
  312. newDataSource.splice(idx, 1);
  313. setTokensFormat(newDataSource);
  314. }
  315. }
  316. };
  317. const manageToken = async (id, action, record) => {
  318. setLoading(true);
  319. let data = {id};
  320. let res;
  321. switch (action) {
  322. case 'delete':
  323. res = await API.delete(`/api/token/${id}/`);
  324. break;
  325. case 'enable':
  326. data.status = 1;
  327. res = await API.put('/api/token/?status_only=true', data);
  328. break;
  329. case 'disable':
  330. data.status = 2;
  331. res = await API.put('/api/token/?status_only=true', data);
  332. break;
  333. }
  334. const {success, message} = res.data;
  335. if (success) {
  336. showSuccess('操作成功完成!');
  337. let token = res.data.data;
  338. let newTokens = [...tokens];
  339. // let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx;
  340. if (action === 'delete') {
  341. } else {
  342. record.status = token.status;
  343. // newTokens[realIdx].status = token.status;
  344. }
  345. setTokensFormat(newTokens);
  346. } else {
  347. showError(message);
  348. }
  349. setLoading(false);
  350. };
  351. const searchTokens = async () => {
  352. if (searchKeyword === '' && searchToken === '') {
  353. // if keyword is blank, load files instead.
  354. await loadTokens(0);
  355. setActivePage(1);
  356. return;
  357. }
  358. setSearching(true);
  359. const res = await API.get(`/api/token/search?keyword=${searchKeyword}&token=${searchToken}`);
  360. const {success, message, data} = res.data;
  361. if (success) {
  362. setTokensFormat(data);
  363. setActivePage(1);
  364. } else {
  365. showError(message);
  366. }
  367. setSearching(false);
  368. };
  369. const handleKeywordChange = async (value) => {
  370. setSearchKeyword(value.trim());
  371. };
  372. const handleSearchTokenChange = async (value) => {
  373. setSearchToken(value.trim());
  374. };
  375. const sortToken = (key) => {
  376. if (tokens.length === 0) return;
  377. setLoading(true);
  378. let sortedTokens = [...tokens];
  379. sortedTokens.sort((a, b) => {
  380. return ('' + a[key]).localeCompare(b[key]);
  381. });
  382. if (sortedTokens[0].id === tokens[0].id) {
  383. sortedTokens.reverse();
  384. }
  385. setTokens(sortedTokens);
  386. setLoading(false);
  387. };
  388. const handlePageChange = page => {
  389. setActivePage(page);
  390. if (page === Math.ceil(tokens.length / pageSize) + 1) {
  391. // In this case we have to load more data and then append them.
  392. loadTokens(page - 1).then(r => {
  393. });
  394. }
  395. };
  396. const rowSelection = {
  397. onSelect: (record, selected) => {
  398. },
  399. onSelectAll: (selected, selectedRows) => {
  400. },
  401. onChange: (selectedRowKeys, selectedRows) => {
  402. setSelectedKeys(selectedRows);
  403. },
  404. };
  405. return (
  406. <>
  407. <EditToken refresh={refresh} editingToken={editingToken} visiable={showEdit} handleClose={closeEdit}></EditToken>
  408. <Form layout='horizontal' style={{marginTop: 10}} labelPosition={'left'}>
  409. <Form.Input
  410. field="keyword"
  411. label='搜索关键字'
  412. placeholder='令牌名称'
  413. value={searchKeyword}
  414. loading={searching}
  415. onChange={handleKeywordChange}
  416. />
  417. <Form.Input
  418. field="token"
  419. label='Key'
  420. placeholder='密钥'
  421. value={searchToken}
  422. loading={searching}
  423. onChange={handleSearchTokenChange}
  424. />
  425. <Button label='查询' type="primary" htmlType="submit" className="btn-margin-right"
  426. onClick={searchTokens} style={{marginRight: 8}}>查询</Button>
  427. </Form>
  428. <Table style={{marginTop: 20}} columns={columns} dataSource={pageData} pagination={{
  429. currentPage: activePage,
  430. pageSize: pageSize,
  431. total: tokenCount,
  432. showSizeChanger: true,
  433. pageSizeOptions: [10, 20, 50, 100],
  434. formatPageText: (page) => `第 ${page.currentStart} - ${page.currentEnd} 条,共 ${tokens.length} 条`,
  435. onPageSizeChange: (size) => {
  436. setPageSize(size);
  437. setActivePage(1);
  438. },
  439. onPageChange: handlePageChange,
  440. }} loading={loading} rowSelection={rowSelection}>
  441. </Table>
  442. <Button theme='light' type='primary' style={{marginRight: 8}} onClick={
  443. () => {
  444. setEditingToken({
  445. id: undefined,
  446. });
  447. setShowEdit(true);
  448. }
  449. }>添加令牌</Button>
  450. <Button label='复制所选令牌' type="warning" onClick={
  451. async () => {
  452. if (selectedKeys.length === 0) {
  453. showError('请至少选择一个令牌!');
  454. return;
  455. }
  456. let keys = "";
  457. for (let i = 0; i < selectedKeys.length; i++) {
  458. keys += selectedKeys[i].name + " sk-" + selectedKeys[i].key + "\n";
  459. }
  460. await copyText(keys);
  461. }
  462. }>复制所选令牌到剪贴板</Button>
  463. </>
  464. );
  465. };
  466. export default TokensTable;