TokensTable.js 12 KB

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