ChannelsTable.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  1. import React, { useEffect, useState } from 'react';
  2. import { Button, Form, Label, Pagination, Popup, Table } from 'semantic-ui-react';
  3. import { Link } from 'react-router-dom';
  4. import { API, showError, showInfo, showSuccess, timestamp2string } from '../helpers';
  5. import { CHANNEL_OPTIONS, ITEMS_PER_PAGE } from '../constants';
  6. function renderTimestamp(timestamp) {
  7. return (
  8. <>
  9. {timestamp2string(timestamp)}
  10. </>
  11. );
  12. }
  13. let type2label = undefined;
  14. function renderType(type) {
  15. if (!type2label) {
  16. type2label = new Map;
  17. for (let i = 0; i < CHANNEL_OPTIONS.length; i++) {
  18. type2label[CHANNEL_OPTIONS[i].value] = CHANNEL_OPTIONS[i];
  19. }
  20. type2label[0] = { value: 0, text: '未知类型', color: 'grey' };
  21. }
  22. return <Label basic color={type2label[type].color}>{type2label[type].text}</Label>;
  23. }
  24. const ChannelsTable = () => {
  25. const [channels, setChannels] = useState([]);
  26. const [loading, setLoading] = useState(true);
  27. const [activePage, setActivePage] = useState(1);
  28. const [searchKeyword, setSearchKeyword] = useState('');
  29. const [searching, setSearching] = useState(false);
  30. const [updatingBalance, setUpdatingBalance] = useState(false);
  31. const loadChannels = async (startIdx) => {
  32. const res = await API.get(`/api/channel/?p=${startIdx}`);
  33. const { success, message, data } = res.data;
  34. if (success) {
  35. if (startIdx === 0) {
  36. setChannels(data);
  37. } else {
  38. let newChannels = channels;
  39. newChannels.push(...data);
  40. setChannels(newChannels);
  41. }
  42. } else {
  43. showError(message);
  44. }
  45. setLoading(false);
  46. };
  47. const onPaginationChange = (e, { activePage }) => {
  48. (async () => {
  49. if (activePage === Math.ceil(channels.length / ITEMS_PER_PAGE) + 1) {
  50. // In this case we have to load more data and then append them.
  51. await loadChannels(activePage - 1);
  52. }
  53. setActivePage(activePage);
  54. })();
  55. };
  56. const refresh = async () => {
  57. setLoading(true);
  58. await loadChannels(0);
  59. };
  60. useEffect(() => {
  61. loadChannels(0)
  62. .then()
  63. .catch((reason) => {
  64. showError(reason);
  65. });
  66. }, []);
  67. const manageChannel = async (id, action, idx) => {
  68. let data = { id };
  69. let res;
  70. switch (action) {
  71. case 'delete':
  72. res = await API.delete(`/api/channel/${id}/`);
  73. break;
  74. case 'enable':
  75. data.status = 1;
  76. res = await API.put('/api/channel/', data);
  77. break;
  78. case 'disable':
  79. data.status = 2;
  80. res = await API.put('/api/channel/', data);
  81. break;
  82. }
  83. const { success, message } = res.data;
  84. if (success) {
  85. showSuccess('操作成功完成!');
  86. let channel = res.data.data;
  87. let newChannels = [...channels];
  88. let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx;
  89. if (action === 'delete') {
  90. newChannels[realIdx].deleted = true;
  91. } else {
  92. newChannels[realIdx].status = channel.status;
  93. }
  94. setChannels(newChannels);
  95. } else {
  96. showError(message);
  97. }
  98. };
  99. const renderStatus = (status) => {
  100. switch (status) {
  101. case 1:
  102. return <Label basic color='green'>已启用</Label>;
  103. case 2:
  104. return (
  105. <Label basic color='red'>
  106. 已禁用
  107. </Label>
  108. );
  109. default:
  110. return (
  111. <Label basic color='grey'>
  112. 未知状态
  113. </Label>
  114. );
  115. }
  116. };
  117. const renderResponseTime = (responseTime) => {
  118. let time = responseTime / 1000;
  119. time = time.toFixed(2) + ' 秒';
  120. if (responseTime === 0) {
  121. return <Label basic color='grey'>未测试</Label>;
  122. } else if (responseTime <= 1000) {
  123. return <Label basic color='green'>{time}</Label>;
  124. } else if (responseTime <= 3000) {
  125. return <Label basic color='olive'>{time}</Label>;
  126. } else if (responseTime <= 5000) {
  127. return <Label basic color='yellow'>{time}</Label>;
  128. } else {
  129. return <Label basic color='red'>{time}</Label>;
  130. }
  131. };
  132. const searchChannels = async () => {
  133. if (searchKeyword === '') {
  134. // if keyword is blank, load files instead.
  135. await loadChannels(0);
  136. setActivePage(1);
  137. return;
  138. }
  139. setSearching(true);
  140. const res = await API.get(`/api/channel/search?keyword=${searchKeyword}`);
  141. const { success, message, data } = res.data;
  142. if (success) {
  143. setChannels(data);
  144. setActivePage(1);
  145. } else {
  146. showError(message);
  147. }
  148. setSearching(false);
  149. };
  150. const testChannel = async (id, name, idx) => {
  151. const res = await API.get(`/api/channel/test/${id}/`);
  152. const { success, message, time } = res.data;
  153. if (success) {
  154. let newChannels = [...channels];
  155. let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx;
  156. newChannels[realIdx].response_time = time * 1000;
  157. newChannels[realIdx].test_time = Date.now() / 1000;
  158. setChannels(newChannels);
  159. showInfo(`通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。`);
  160. } else {
  161. showError(message);
  162. }
  163. };
  164. const testAllChannels = async () => {
  165. const res = await API.get(`/api/channel/test`);
  166. const { success, message } = res.data;
  167. if (success) {
  168. showInfo('已成功开始测试所有已启用通道,请刷新页面查看结果。');
  169. } else {
  170. showError(message);
  171. }
  172. };
  173. const updateChannelBalance = async (id, name, idx) => {
  174. const res = await API.get(`/api/channel/update_balance/${id}/`);
  175. const { success, message, balance } = res.data;
  176. if (success) {
  177. let newChannels = [...channels];
  178. let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx;
  179. newChannels[realIdx].balance = balance;
  180. newChannels[realIdx].balance_updated_time = Date.now() / 1000;
  181. setChannels(newChannels);
  182. showInfo(`通道 ${name} 余额更新成功!`);
  183. } else {
  184. showError(message);
  185. }
  186. };
  187. const updateAllChannelsBalance = async () => {
  188. setUpdatingBalance(true);
  189. const res = await API.get(`/api/channel/update_balance`);
  190. const { success, message } = res.data;
  191. if (success) {
  192. showInfo('已更新完毕所有已启用通道余额!');
  193. } else {
  194. showError(message);
  195. }
  196. setUpdatingBalance(false);
  197. };
  198. const handleKeywordChange = async (e, { value }) => {
  199. setSearchKeyword(value.trim());
  200. };
  201. const sortChannel = (key) => {
  202. if (channels.length === 0) return;
  203. setLoading(true);
  204. let sortedChannels = [...channels];
  205. sortedChannels.sort((a, b) => {
  206. return ('' + a[key]).localeCompare(b[key]);
  207. });
  208. if (sortedChannels[0].id === channels[0].id) {
  209. sortedChannels.reverse();
  210. }
  211. setChannels(sortedChannels);
  212. setLoading(false);
  213. };
  214. return (
  215. <>
  216. <Form onSubmit={searchChannels}>
  217. <Form.Input
  218. icon='search'
  219. fluid
  220. iconPosition='left'
  221. placeholder='搜索渠道的 ID 和名称 ...'
  222. value={searchKeyword}
  223. loading={searching}
  224. onChange={handleKeywordChange}
  225. />
  226. </Form>
  227. <Table basic>
  228. <Table.Header>
  229. <Table.Row>
  230. <Table.HeaderCell
  231. style={{ cursor: 'pointer' }}
  232. onClick={() => {
  233. sortChannel('id');
  234. }}
  235. >
  236. ID
  237. </Table.HeaderCell>
  238. <Table.HeaderCell
  239. style={{ cursor: 'pointer' }}
  240. onClick={() => {
  241. sortChannel('name');
  242. }}
  243. >
  244. 名称
  245. </Table.HeaderCell>
  246. <Table.HeaderCell
  247. style={{ cursor: 'pointer' }}
  248. onClick={() => {
  249. sortChannel('type');
  250. }}
  251. >
  252. 类型
  253. </Table.HeaderCell>
  254. <Table.HeaderCell
  255. style={{ cursor: 'pointer' }}
  256. onClick={() => {
  257. sortChannel('status');
  258. }}
  259. >
  260. 状态
  261. </Table.HeaderCell>
  262. <Table.HeaderCell
  263. style={{ cursor: 'pointer' }}
  264. onClick={() => {
  265. sortChannel('response_time');
  266. }}
  267. >
  268. 响应时间
  269. </Table.HeaderCell>
  270. <Table.HeaderCell
  271. style={{ cursor: 'pointer' }}
  272. onClick={() => {
  273. sortChannel('balance');
  274. }}
  275. >
  276. 余额
  277. </Table.HeaderCell>
  278. <Table.HeaderCell>操作</Table.HeaderCell>
  279. </Table.Row>
  280. </Table.Header>
  281. <Table.Body>
  282. {channels
  283. .slice(
  284. (activePage - 1) * ITEMS_PER_PAGE,
  285. activePage * ITEMS_PER_PAGE
  286. )
  287. .map((channel, idx) => {
  288. if (channel.deleted) return <></>;
  289. return (
  290. <Table.Row key={channel.id}>
  291. <Table.Cell>{channel.id}</Table.Cell>
  292. <Table.Cell>{channel.name ? channel.name : '无'}</Table.Cell>
  293. <Table.Cell>{renderType(channel.type)}</Table.Cell>
  294. <Table.Cell>{renderStatus(channel.status)}</Table.Cell>
  295. <Table.Cell>
  296. <Popup
  297. content={channel.test_time ? renderTimestamp(channel.test_time) : '未测试'}
  298. key={channel.id}
  299. trigger={renderResponseTime(channel.response_time)}
  300. basic
  301. />
  302. </Table.Cell>
  303. <Table.Cell>
  304. <Popup
  305. content={channel.balance_updated_time ? renderTimestamp(channel.balance_updated_time) : '未更新'}
  306. key={channel.id}
  307. trigger={<span>${channel.balance.toFixed(2)}</span>}
  308. basic
  309. />
  310. </Table.Cell>
  311. <Table.Cell>
  312. <div>
  313. <Button
  314. size={'small'}
  315. positive
  316. onClick={() => {
  317. testChannel(channel.id, channel.name, idx);
  318. }}
  319. >
  320. 测试
  321. </Button>
  322. <Button
  323. size={'small'}
  324. positive
  325. loading={updatingBalance}
  326. onClick={() => {
  327. updateChannelBalance(channel.id, channel.name, idx);
  328. }}
  329. >
  330. 更新余额
  331. </Button>
  332. <Popup
  333. trigger={
  334. <Button size='small' negative>
  335. 删除
  336. </Button>
  337. }
  338. on='click'
  339. flowing
  340. hoverable
  341. >
  342. <Button
  343. negative
  344. onClick={() => {
  345. manageChannel(channel.id, 'delete', idx);
  346. }}
  347. >
  348. 删除渠道 {channel.name}
  349. </Button>
  350. </Popup>
  351. <Button
  352. size={'small'}
  353. onClick={() => {
  354. manageChannel(
  355. channel.id,
  356. channel.status === 1 ? 'disable' : 'enable',
  357. idx
  358. );
  359. }}
  360. >
  361. {channel.status === 1 ? '禁用' : '启用'}
  362. </Button>
  363. <Button
  364. size={'small'}
  365. as={Link}
  366. to={'/channel/edit/' + channel.id}
  367. >
  368. 编辑
  369. </Button>
  370. </div>
  371. </Table.Cell>
  372. </Table.Row>
  373. );
  374. })}
  375. </Table.Body>
  376. <Table.Footer>
  377. <Table.Row>
  378. <Table.HeaderCell colSpan='7'>
  379. <Button size='small' as={Link} to='/channel/add' loading={loading}>
  380. 添加新的渠道
  381. </Button>
  382. <Button size='small' loading={loading} onClick={testAllChannels}>
  383. 测试所有已启用通道
  384. </Button>
  385. <Button size='small' onClick={updateAllChannelsBalance} loading={loading || updatingBalance}>更新所有已启用通道余额</Button>
  386. <Pagination
  387. floated='right'
  388. activePage={activePage}
  389. onPageChange={onPaginationChange}
  390. size='small'
  391. siblingRange={1}
  392. totalPages={
  393. Math.ceil(channels.length / ITEMS_PER_PAGE) +
  394. (channels.length % ITEMS_PER_PAGE === 0 ? 1 : 0)
  395. }
  396. />
  397. <Button size='small' onClick={refresh} loading={loading}>刷新</Button>
  398. </Table.HeaderCell>
  399. </Table.Row>
  400. </Table.Footer>
  401. </Table>
  402. </>
  403. );
  404. };
  405. export default ChannelsTable;