TokensTable.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. import React, { useEffect, useState } from 'react';
  2. import { Button, Form, Label, Modal, Pagination, Popup, Table } from 'semantic-ui-react';
  3. import { Link } from 'react-router-dom';
  4. import { API, copy, showError, showSuccess, showWarning, timestamp2string } from '../helpers';
  5. import { ITEMS_PER_PAGE } from '../constants';
  6. function renderTimestamp(timestamp) {
  7. return (
  8. <>
  9. {timestamp2string(timestamp)}
  10. </>
  11. );
  12. }
  13. function renderStatus(status) {
  14. switch (status) {
  15. case 1:
  16. return <Label basic color='green'>已启用</Label>;
  17. case 2:
  18. return <Label basic color='red'> 已禁用 </Label>;
  19. case 3:
  20. return <Label basic color='yellow'> 已过期 </Label>;
  21. case 4:
  22. return <Label basic color='grey'> 已耗尽 </Label>;
  23. default:
  24. return <Label basic color='black'> 未知状态 </Label>;
  25. }
  26. }
  27. const TokensTable = () => {
  28. const [tokens, setTokens] = useState([]);
  29. const [loading, setLoading] = useState(true);
  30. const [activePage, setActivePage] = useState(1);
  31. const [searchKeyword, setSearchKeyword] = useState('');
  32. const [searching, setSearching] = useState(false);
  33. const [showTopUpModal, setShowTopUpModal] = useState(false);
  34. const [targetTokenIdx, setTargetTokenIdx] = useState(0);
  35. const [redemptionCode, setRedemptionCode] = useState('');
  36. const [topUpLink, setTopUpLink] = useState('');
  37. const loadTokens = async (startIdx) => {
  38. const res = await API.get(`/api/token/?p=${startIdx}`);
  39. const { success, message, data } = res.data;
  40. if (success) {
  41. if (startIdx === 0) {
  42. setTokens(data);
  43. } else {
  44. let newTokens = tokens;
  45. newTokens.push(...data);
  46. setTokens(newTokens);
  47. }
  48. } else {
  49. showError(message);
  50. }
  51. setLoading(false);
  52. };
  53. const onPaginationChange = (e, { activePage }) => {
  54. (async () => {
  55. if (activePage === Math.ceil(tokens.length / ITEMS_PER_PAGE) + 1) {
  56. // In this case we have to load more data and then append them.
  57. await loadTokens(activePage - 1);
  58. }
  59. setActivePage(activePage);
  60. })();
  61. };
  62. useEffect(() => {
  63. loadTokens(0)
  64. .then()
  65. .catch((reason) => {
  66. showError(reason);
  67. });
  68. let status = localStorage.getItem('status');
  69. if (status) {
  70. status = JSON.parse(status);
  71. if (status.top_up_link) {
  72. setTopUpLink(status.top_up_link);
  73. }
  74. }
  75. }, []);
  76. const manageToken = async (id, action, idx) => {
  77. let data = { id };
  78. let res;
  79. switch (action) {
  80. case 'delete':
  81. res = await API.delete(`/api/token/${id}/`);
  82. break;
  83. case 'enable':
  84. data.status = 1;
  85. res = await API.put('/api/token/?status_only=true', data);
  86. break;
  87. case 'disable':
  88. data.status = 2;
  89. res = await API.put('/api/token/?status_only=true', data);
  90. break;
  91. }
  92. const { success, message } = res.data;
  93. if (success) {
  94. showSuccess('操作成功完成!');
  95. let token = res.data.data;
  96. let newTokens = [...tokens];
  97. let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx;
  98. if (action === 'delete') {
  99. newTokens[realIdx].deleted = true;
  100. } else {
  101. newTokens[realIdx].status = token.status;
  102. }
  103. setTokens(newTokens);
  104. } else {
  105. showError(message);
  106. }
  107. };
  108. const searchTokens = async () => {
  109. if (searchKeyword === '') {
  110. // if keyword is blank, load files instead.
  111. await loadTokens(0);
  112. setActivePage(1);
  113. return;
  114. }
  115. setSearching(true);
  116. const res = await API.get(`/api/token/search?keyword=${searchKeyword}`);
  117. const { success, message, data } = res.data;
  118. if (success) {
  119. setTokens(data);
  120. setActivePage(1);
  121. } else {
  122. showError(message);
  123. }
  124. setSearching(false);
  125. };
  126. const handleKeywordChange = async (e, { value }) => {
  127. setSearchKeyword(value.trim());
  128. };
  129. const sortToken = (key) => {
  130. if (tokens.length === 0) return;
  131. setLoading(true);
  132. let sortedTokens = [...tokens];
  133. sortedTokens.sort((a, b) => {
  134. return ('' + a[key]).localeCompare(b[key]);
  135. });
  136. if (sortedTokens[0].id === tokens[0].id) {
  137. sortedTokens.reverse();
  138. }
  139. setTokens(sortedTokens);
  140. setLoading(false);
  141. };
  142. const topUp = async () => {
  143. if (redemptionCode === '') {
  144. return;
  145. }
  146. const res = await API.post('/api/token/topup/', {
  147. id: tokens[targetTokenIdx].id,
  148. key: redemptionCode
  149. });
  150. const { success, message, data } = res.data;
  151. if (success) {
  152. showSuccess('充值成功!');
  153. let newTokens = [...tokens];
  154. let realIdx = (activePage - 1) * ITEMS_PER_PAGE + targetTokenIdx;
  155. newTokens[realIdx].remain_quota += data;
  156. setTokens(newTokens);
  157. setRedemptionCode('');
  158. setShowTopUpModal(false);
  159. } else {
  160. showError(message);
  161. }
  162. }
  163. return (
  164. <>
  165. <Form onSubmit={searchTokens}>
  166. <Form.Input
  167. icon='search'
  168. fluid
  169. iconPosition='left'
  170. placeholder='搜索令牌的 ID 和名称 ...'
  171. value={searchKeyword}
  172. loading={searching}
  173. onChange={handleKeywordChange}
  174. />
  175. </Form>
  176. <Table basic>
  177. <Table.Header>
  178. <Table.Row>
  179. <Table.HeaderCell
  180. style={{ cursor: 'pointer' }}
  181. onClick={() => {
  182. sortToken('id');
  183. }}
  184. >
  185. ID
  186. </Table.HeaderCell>
  187. <Table.HeaderCell
  188. style={{ cursor: 'pointer' }}
  189. onClick={() => {
  190. sortToken('name');
  191. }}
  192. >
  193. 名称
  194. </Table.HeaderCell>
  195. <Table.HeaderCell
  196. style={{ cursor: 'pointer' }}
  197. onClick={() => {
  198. sortToken('status');
  199. }}
  200. >
  201. 状态
  202. </Table.HeaderCell>
  203. <Table.HeaderCell
  204. style={{ cursor: 'pointer' }}
  205. onClick={() => {
  206. sortToken('remain_quota');
  207. }}
  208. >
  209. 额度
  210. </Table.HeaderCell>
  211. <Table.HeaderCell
  212. style={{ cursor: 'pointer' }}
  213. onClick={() => {
  214. sortToken('created_time');
  215. }}
  216. >
  217. 创建时间
  218. </Table.HeaderCell>
  219. <Table.HeaderCell
  220. style={{ cursor: 'pointer' }}
  221. onClick={() => {
  222. sortToken('expired_time');
  223. }}
  224. >
  225. 过期时间
  226. </Table.HeaderCell>
  227. <Table.HeaderCell>操作</Table.HeaderCell>
  228. </Table.Row>
  229. </Table.Header>
  230. <Table.Body>
  231. {tokens
  232. .slice(
  233. (activePage - 1) * ITEMS_PER_PAGE,
  234. activePage * ITEMS_PER_PAGE
  235. )
  236. .map((token, idx) => {
  237. if (token.deleted) return <></>;
  238. return (
  239. <Table.Row key={token.id}>
  240. <Table.Cell>{token.id}</Table.Cell>
  241. <Table.Cell>{token.name ? token.name : '无'}</Table.Cell>
  242. <Table.Cell>{renderStatus(token.status)}</Table.Cell>
  243. <Table.Cell>{token.unlimited_quota ? '无限制' : token.remain_quota}</Table.Cell>
  244. <Table.Cell>{renderTimestamp(token.created_time)}</Table.Cell>
  245. <Table.Cell>{token.expired_time === -1 ? '永不过期' : renderTimestamp(token.expired_time)}</Table.Cell>
  246. <Table.Cell>
  247. <div>
  248. <Button
  249. size={'small'}
  250. positive
  251. onClick={async () => {
  252. if (await copy(token.key)) {
  253. showSuccess('已复制到剪贴板!');
  254. } else {
  255. showWarning('无法复制到剪贴板,请手动复制,已将令牌填入搜索框。');
  256. setSearchKeyword(token.key);
  257. }
  258. }}
  259. >
  260. 复制
  261. </Button>
  262. <Button
  263. size={'small'}
  264. color={'yellow'}
  265. onClick={() => {
  266. setTargetTokenIdx(idx);
  267. setShowTopUpModal(true);
  268. }}>
  269. 充值
  270. </Button>
  271. <Popup
  272. trigger={
  273. <Button size='small' negative>
  274. 删除
  275. </Button>
  276. }
  277. on='click'
  278. flowing
  279. hoverable
  280. >
  281. <Button
  282. negative
  283. onClick={() => {
  284. manageToken(token.id, 'delete', idx);
  285. }}
  286. >
  287. 删除令牌 {token.name}
  288. </Button>
  289. </Popup>
  290. <Button
  291. size={'small'}
  292. onClick={() => {
  293. manageToken(
  294. token.id,
  295. token.status === 1 ? 'disable' : 'enable',
  296. idx
  297. );
  298. }}
  299. >
  300. {token.status === 1 ? '禁用' : '启用'}
  301. </Button>
  302. <Button
  303. size={'small'}
  304. as={Link}
  305. to={'/token/edit/' + token.id}
  306. >
  307. 编辑
  308. </Button>
  309. </div>
  310. </Table.Cell>
  311. </Table.Row>
  312. );
  313. })}
  314. </Table.Body>
  315. <Table.Footer>
  316. <Table.Row>
  317. <Table.HeaderCell colSpan='8'>
  318. <Button size='small' as={Link} to='/token/add' loading={loading}>
  319. 添加新的令牌
  320. </Button>
  321. <Pagination
  322. floated='right'
  323. activePage={activePage}
  324. onPageChange={onPaginationChange}
  325. size='small'
  326. siblingRange={1}
  327. totalPages={
  328. Math.ceil(tokens.length / ITEMS_PER_PAGE) +
  329. (tokens.length % ITEMS_PER_PAGE === 0 ? 1 : 0)
  330. }
  331. />
  332. </Table.HeaderCell>
  333. </Table.Row>
  334. </Table.Footer>
  335. </Table>
  336. <Modal
  337. onClose={() => setShowTopUpModal(false)}
  338. onOpen={() => setShowTopUpModal(true)}
  339. open={showTopUpModal}
  340. size={'mini'}
  341. >
  342. <Modal.Header>通过兑换码为令牌「{tokens[targetTokenIdx]?.name}」充值</Modal.Header>
  343. <Modal.Content>
  344. <Modal.Description>
  345. {/*<Image src={status.wechat_qrcode} fluid />*/}
  346. {
  347. topUpLink && <p>
  348. <a target='_blank' href={topUpLink}>点击此处获取兑换码</a>
  349. </p>
  350. }
  351. <Form size='large'>
  352. <Form.Input
  353. fluid
  354. placeholder='兑换码'
  355. name='redemptionCode'
  356. value={redemptionCode}
  357. onChange={(e) => {
  358. setRedemptionCode(e.target.value);
  359. }}
  360. />
  361. <Button color='' fluid size='large' onClick={topUp}>
  362. 充值
  363. </Button>
  364. </Form>
  365. </Modal.Description>
  366. </Modal.Content>
  367. </Modal>
  368. </>
  369. );
  370. };
  371. export default TokensTable;