MjLogsTable.js 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975
  1. import React, { useEffect, useState } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import {
  4. Palette,
  5. ZoomIn,
  6. Shuffle,
  7. Move,
  8. FileText,
  9. Blend,
  10. Upload,
  11. Minimize2,
  12. RotateCcw,
  13. PaintBucket,
  14. Focus,
  15. Move3D,
  16. Monitor,
  17. UserCheck,
  18. HelpCircle,
  19. CheckCircle,
  20. Clock,
  21. Copy,
  22. FileX,
  23. Pause,
  24. XCircle,
  25. Loader,
  26. AlertCircle,
  27. Hash
  28. } from 'lucide-react';
  29. import {
  30. API,
  31. copy,
  32. isAdmin,
  33. showError,
  34. showSuccess,
  35. timestamp2string
  36. } from '../../helpers';
  37. import {
  38. Button,
  39. Card,
  40. Checkbox,
  41. Divider,
  42. Empty,
  43. Form,
  44. ImagePreview,
  45. Layout,
  46. Modal,
  47. Progress,
  48. Skeleton,
  49. Table,
  50. Tag,
  51. Typography
  52. } from '@douyinfe/semi-ui';
  53. import {
  54. IllustrationNoResult,
  55. IllustrationNoResultDark
  56. } from '@douyinfe/semi-illustrations';
  57. import { ITEMS_PER_PAGE } from '../../constants';
  58. import {
  59. IconEyeOpened,
  60. IconSearch,
  61. IconSetting
  62. } from '@douyinfe/semi-icons';
  63. const { Text } = Typography;
  64. const colors = [
  65. 'amber',
  66. 'blue',
  67. 'cyan',
  68. 'green',
  69. 'grey',
  70. 'indigo',
  71. 'light-blue',
  72. 'lime',
  73. 'orange',
  74. 'pink',
  75. 'purple',
  76. 'red',
  77. 'teal',
  78. 'violet',
  79. 'yellow',
  80. ];
  81. // 定义列键值常量
  82. const COLUMN_KEYS = {
  83. SUBMIT_TIME: 'submit_time',
  84. DURATION: 'duration',
  85. CHANNEL: 'channel',
  86. TYPE: 'type',
  87. TASK_ID: 'task_id',
  88. SUBMIT_RESULT: 'submit_result',
  89. TASK_STATUS: 'task_status',
  90. PROGRESS: 'progress',
  91. IMAGE: 'image',
  92. PROMPT: 'prompt',
  93. PROMPT_EN: 'prompt_en',
  94. FAIL_REASON: 'fail_reason',
  95. };
  96. const LogsTable = () => {
  97. const { t } = useTranslation();
  98. const [isModalOpen, setIsModalOpen] = useState(false);
  99. const [modalContent, setModalContent] = useState('');
  100. // 列可见性状态
  101. const [visibleColumns, setVisibleColumns] = useState({});
  102. const [showColumnSelector, setShowColumnSelector] = useState(false);
  103. const isAdminUser = isAdmin();
  104. // 加载保存的列偏好设置
  105. useEffect(() => {
  106. const savedColumns = localStorage.getItem('mj-logs-table-columns');
  107. if (savedColumns) {
  108. try {
  109. const parsed = JSON.parse(savedColumns);
  110. const defaults = getDefaultColumnVisibility();
  111. const merged = { ...defaults, ...parsed };
  112. setVisibleColumns(merged);
  113. } catch (e) {
  114. console.error('Failed to parse saved column preferences', e);
  115. initDefaultColumns();
  116. }
  117. } else {
  118. initDefaultColumns();
  119. }
  120. }, []);
  121. // 获取默认列可见性
  122. const getDefaultColumnVisibility = () => {
  123. return {
  124. [COLUMN_KEYS.SUBMIT_TIME]: true,
  125. [COLUMN_KEYS.DURATION]: true,
  126. [COLUMN_KEYS.CHANNEL]: isAdminUser,
  127. [COLUMN_KEYS.TYPE]: true,
  128. [COLUMN_KEYS.TASK_ID]: true,
  129. [COLUMN_KEYS.SUBMIT_RESULT]: isAdminUser,
  130. [COLUMN_KEYS.TASK_STATUS]: true,
  131. [COLUMN_KEYS.PROGRESS]: true,
  132. [COLUMN_KEYS.IMAGE]: true,
  133. [COLUMN_KEYS.PROMPT]: true,
  134. [COLUMN_KEYS.PROMPT_EN]: true,
  135. [COLUMN_KEYS.FAIL_REASON]: true,
  136. };
  137. };
  138. // 初始化默认列可见性
  139. const initDefaultColumns = () => {
  140. const defaults = getDefaultColumnVisibility();
  141. setVisibleColumns(defaults);
  142. localStorage.setItem('mj-logs-table-columns', JSON.stringify(defaults));
  143. };
  144. // 处理列可见性变化
  145. const handleColumnVisibilityChange = (columnKey, checked) => {
  146. const updatedColumns = { ...visibleColumns, [columnKey]: checked };
  147. setVisibleColumns(updatedColumns);
  148. };
  149. // 处理全选
  150. const handleSelectAll = (checked) => {
  151. const allKeys = Object.keys(COLUMN_KEYS).map((key) => COLUMN_KEYS[key]);
  152. const updatedColumns = {};
  153. allKeys.forEach((key) => {
  154. if ((key === COLUMN_KEYS.CHANNEL || key === COLUMN_KEYS.SUBMIT_RESULT) && !isAdminUser) {
  155. updatedColumns[key] = false;
  156. } else {
  157. updatedColumns[key] = checked;
  158. }
  159. });
  160. setVisibleColumns(updatedColumns);
  161. };
  162. // 更新表格时保存列可见性
  163. useEffect(() => {
  164. if (Object.keys(visibleColumns).length > 0) {
  165. localStorage.setItem('mj-logs-table-columns', JSON.stringify(visibleColumns));
  166. }
  167. }, [visibleColumns]);
  168. function renderType(type) {
  169. switch (type) {
  170. case 'IMAGINE':
  171. return (
  172. <Tag color='blue' size='large' shape='circle' prefixIcon={<Palette size={14} />}>
  173. {t('绘图')}
  174. </Tag>
  175. );
  176. case 'UPSCALE':
  177. return (
  178. <Tag color='orange' size='large' shape='circle' prefixIcon={<ZoomIn size={14} />}>
  179. {t('放大')}
  180. </Tag>
  181. );
  182. case 'VARIATION':
  183. return (
  184. <Tag color='purple' size='large' shape='circle' prefixIcon={<Shuffle size={14} />}>
  185. {t('变换')}
  186. </Tag>
  187. );
  188. case 'HIGH_VARIATION':
  189. return (
  190. <Tag color='purple' size='large' shape='circle' prefixIcon={<Shuffle size={14} />}>
  191. {t('强变换')}
  192. </Tag>
  193. );
  194. case 'LOW_VARIATION':
  195. return (
  196. <Tag color='purple' size='large' shape='circle' prefixIcon={<Shuffle size={14} />}>
  197. {t('弱变换')}
  198. </Tag>
  199. );
  200. case 'PAN':
  201. return (
  202. <Tag color='cyan' size='large' shape='circle' prefixIcon={<Move size={14} />}>
  203. {t('平移')}
  204. </Tag>
  205. );
  206. case 'DESCRIBE':
  207. return (
  208. <Tag color='yellow' size='large' shape='circle' prefixIcon={<FileText size={14} />}>
  209. {t('图生文')}
  210. </Tag>
  211. );
  212. case 'BLEND':
  213. return (
  214. <Tag color='lime' size='large' shape='circle' prefixIcon={<Blend size={14} />}>
  215. {t('图混合')}
  216. </Tag>
  217. );
  218. case 'UPLOAD':
  219. return (
  220. <Tag color='blue' size='large' shape='circle' prefixIcon={<Upload size={14} />}>
  221. 上传文件
  222. </Tag>
  223. );
  224. case 'SHORTEN':
  225. return (
  226. <Tag color='pink' size='large' shape='circle' prefixIcon={<Minimize2 size={14} />}>
  227. {t('缩词')}
  228. </Tag>
  229. );
  230. case 'REROLL':
  231. return (
  232. <Tag color='indigo' size='large' shape='circle' prefixIcon={<RotateCcw size={14} />}>
  233. {t('重绘')}
  234. </Tag>
  235. );
  236. case 'INPAINT':
  237. return (
  238. <Tag color='violet' size='large' shape='circle' prefixIcon={<PaintBucket size={14} />}>
  239. {t('局部重绘-提交')}
  240. </Tag>
  241. );
  242. case 'ZOOM':
  243. return (
  244. <Tag color='teal' size='large' shape='circle' prefixIcon={<Focus size={14} />}>
  245. {t('变焦')}
  246. </Tag>
  247. );
  248. case 'CUSTOM_ZOOM':
  249. return (
  250. <Tag color='teal' size='large' shape='circle' prefixIcon={<Move3D size={14} />}>
  251. {t('自定义变焦-提交')}
  252. </Tag>
  253. );
  254. case 'MODAL':
  255. return (
  256. <Tag color='green' size='large' shape='circle' prefixIcon={<Monitor size={14} />}>
  257. {t('窗口处理')}
  258. </Tag>
  259. );
  260. case 'SWAP_FACE':
  261. return (
  262. <Tag color='light-green' size='large' shape='circle' prefixIcon={<UserCheck size={14} />}>
  263. {t('换脸')}
  264. </Tag>
  265. );
  266. default:
  267. return (
  268. <Tag color='white' size='large' shape='circle' prefixIcon={<HelpCircle size={14} />}>
  269. {t('未知')}
  270. </Tag>
  271. );
  272. }
  273. }
  274. function renderCode(code) {
  275. switch (code) {
  276. case 1:
  277. return (
  278. <Tag color='green' size='large' shape='circle' prefixIcon={<CheckCircle size={14} />}>
  279. {t('已提交')}
  280. </Tag>
  281. );
  282. case 21:
  283. return (
  284. <Tag color='lime' size='large' shape='circle' prefixIcon={<Clock size={14} />}>
  285. {t('等待中')}
  286. </Tag>
  287. );
  288. case 22:
  289. return (
  290. <Tag color='orange' size='large' shape='circle' prefixIcon={<Copy size={14} />}>
  291. {t('重复提交')}
  292. </Tag>
  293. );
  294. case 0:
  295. return (
  296. <Tag color='yellow' size='large' shape='circle' prefixIcon={<FileX size={14} />}>
  297. {t('未提交')}
  298. </Tag>
  299. );
  300. default:
  301. return (
  302. <Tag color='white' size='large' shape='circle' prefixIcon={<HelpCircle size={14} />}>
  303. {t('未知')}
  304. </Tag>
  305. );
  306. }
  307. }
  308. function renderStatus(type) {
  309. switch (type) {
  310. case 'SUCCESS':
  311. return (
  312. <Tag color='green' size='large' shape='circle' prefixIcon={<CheckCircle size={14} />}>
  313. {t('成功')}
  314. </Tag>
  315. );
  316. case 'NOT_START':
  317. return (
  318. <Tag color='grey' size='large' shape='circle' prefixIcon={<Pause size={14} />}>
  319. {t('未启动')}
  320. </Tag>
  321. );
  322. case 'SUBMITTED':
  323. return (
  324. <Tag color='yellow' size='large' shape='circle' prefixIcon={<Clock size={14} />}>
  325. {t('队列中')}
  326. </Tag>
  327. );
  328. case 'IN_PROGRESS':
  329. return (
  330. <Tag color='blue' size='large' shape='circle' prefixIcon={<Loader size={14} />}>
  331. {t('执行中')}
  332. </Tag>
  333. );
  334. case 'FAILURE':
  335. return (
  336. <Tag color='red' size='large' shape='circle' prefixIcon={<XCircle size={14} />}>
  337. {t('失败')}
  338. </Tag>
  339. );
  340. case 'MODAL':
  341. return (
  342. <Tag color='yellow' size='large' shape='circle' prefixIcon={<AlertCircle size={14} />}>
  343. {t('窗口等待')}
  344. </Tag>
  345. );
  346. default:
  347. return (
  348. <Tag color='white' size='large' shape='circle' prefixIcon={<HelpCircle size={14} />}>
  349. {t('未知')}
  350. </Tag>
  351. );
  352. }
  353. }
  354. const renderTimestamp = (timestampInSeconds) => {
  355. const date = new Date(timestampInSeconds * 1000); // 从秒转换为毫秒
  356. const year = date.getFullYear(); // 获取年份
  357. const month = ('0' + (date.getMonth() + 1)).slice(-2); // 获取月份,从0开始需要+1,并保证两位数
  358. const day = ('0' + date.getDate()).slice(-2); // 获取日期,并保证两位数
  359. const hours = ('0' + date.getHours()).slice(-2); // 获取小时,并保证两位数
  360. const minutes = ('0' + date.getMinutes()).slice(-2); // 获取分钟,并保证两位数
  361. const seconds = ('0' + date.getSeconds()).slice(-2); // 获取秒钟,并保证两位数
  362. return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; // 格式化输出
  363. };
  364. // 修改renderDuration函数以包含颜色逻辑
  365. function renderDuration(submit_time, finishTime) {
  366. if (!submit_time || !finishTime) return 'N/A';
  367. const start = new Date(submit_time);
  368. const finish = new Date(finishTime);
  369. const durationMs = finish - start;
  370. const durationSec = (durationMs / 1000).toFixed(1);
  371. const color = durationSec > 60 ? 'red' : 'green';
  372. return (
  373. <Tag color={color} size='large' shape='circle' prefixIcon={<Clock size={14} />}>
  374. {durationSec} {t('秒')}
  375. </Tag>
  376. );
  377. }
  378. // 定义所有列
  379. const allColumns = [
  380. {
  381. key: COLUMN_KEYS.SUBMIT_TIME,
  382. title: t('提交时间'),
  383. dataIndex: 'submit_time',
  384. render: (text, record, index) => {
  385. return <div>{renderTimestamp(text / 1000)}</div>;
  386. },
  387. },
  388. {
  389. key: COLUMN_KEYS.DURATION,
  390. title: t('花费时间'),
  391. dataIndex: 'finish_time',
  392. render: (finish, record) => {
  393. return renderDuration(record.submit_time, finish);
  394. },
  395. },
  396. {
  397. key: COLUMN_KEYS.CHANNEL,
  398. title: t('渠道'),
  399. dataIndex: 'channel_id',
  400. className: isAdmin() ? 'tableShow' : 'tableHiddle',
  401. render: (text, record, index) => {
  402. return isAdminUser ? (
  403. <div>
  404. <Tag
  405. color={colors[parseInt(text) % colors.length]}
  406. size='large'
  407. shape='circle'
  408. prefixIcon={<Hash size={14} />}
  409. onClick={() => {
  410. copyText(text);
  411. }}
  412. >
  413. {' '}
  414. {text}{' '}
  415. </Tag>
  416. </div>
  417. ) : (
  418. <></>
  419. );
  420. },
  421. },
  422. {
  423. key: COLUMN_KEYS.TYPE,
  424. title: t('类型'),
  425. dataIndex: 'action',
  426. render: (text, record, index) => {
  427. return <div>{renderType(text)}</div>;
  428. },
  429. },
  430. {
  431. key: COLUMN_KEYS.TASK_ID,
  432. title: t('任务ID'),
  433. dataIndex: 'mj_id',
  434. render: (text, record, index) => {
  435. return <div>{text}</div>;
  436. },
  437. },
  438. {
  439. key: COLUMN_KEYS.SUBMIT_RESULT,
  440. title: t('提交结果'),
  441. dataIndex: 'code',
  442. className: isAdmin() ? 'tableShow' : 'tableHiddle',
  443. render: (text, record, index) => {
  444. return isAdminUser ? <div>{renderCode(text)}</div> : <></>;
  445. },
  446. },
  447. {
  448. key: COLUMN_KEYS.TASK_STATUS,
  449. title: t('任务状态'),
  450. dataIndex: 'status',
  451. className: isAdmin() ? 'tableShow' : 'tableHiddle',
  452. render: (text, record, index) => {
  453. return <div>{renderStatus(text)}</div>;
  454. },
  455. },
  456. {
  457. key: COLUMN_KEYS.PROGRESS,
  458. title: t('进度'),
  459. dataIndex: 'progress',
  460. render: (text, record, index) => {
  461. return (
  462. <div>
  463. {
  464. <Progress
  465. stroke={
  466. record.status === 'FAILURE'
  467. ? 'var(--semi-color-warning)'
  468. : null
  469. }
  470. percent={text ? parseInt(text.replace('%', '')) : 0}
  471. showInfo={true}
  472. aria-label='drawing progress'
  473. style={{ minWidth: '160px' }}
  474. />
  475. }
  476. </div>
  477. );
  478. },
  479. },
  480. {
  481. key: COLUMN_KEYS.IMAGE,
  482. title: t('结果图片'),
  483. dataIndex: 'image_url',
  484. render: (text, record, index) => {
  485. if (!text) {
  486. return t('无');
  487. }
  488. return (
  489. <Button
  490. onClick={() => {
  491. setModalImageUrl(text);
  492. setIsModalOpenurl(true);
  493. }}
  494. className="!rounded-full"
  495. >
  496. {t('查看图片')}
  497. </Button>
  498. );
  499. },
  500. },
  501. {
  502. key: COLUMN_KEYS.PROMPT,
  503. title: 'Prompt',
  504. dataIndex: 'prompt',
  505. render: (text, record, index) => {
  506. if (!text) {
  507. return t('无');
  508. }
  509. return (
  510. <Typography.Text
  511. ellipsis={{ showTooltip: true }}
  512. style={{ width: 100 }}
  513. onClick={() => {
  514. setModalContent(text);
  515. setIsModalOpen(true);
  516. }}
  517. >
  518. {text}
  519. </Typography.Text>
  520. );
  521. },
  522. },
  523. {
  524. key: COLUMN_KEYS.PROMPT_EN,
  525. title: 'PromptEn',
  526. dataIndex: 'prompt_en',
  527. render: (text, record, index) => {
  528. if (!text) {
  529. return t('无');
  530. }
  531. return (
  532. <Typography.Text
  533. ellipsis={{ showTooltip: true }}
  534. style={{ width: 100 }}
  535. onClick={() => {
  536. setModalContent(text);
  537. setIsModalOpen(true);
  538. }}
  539. >
  540. {text}
  541. </Typography.Text>
  542. );
  543. },
  544. },
  545. {
  546. key: COLUMN_KEYS.FAIL_REASON,
  547. title: t('失败原因'),
  548. dataIndex: 'fail_reason',
  549. fixed: 'right',
  550. render: (text, record, index) => {
  551. if (!text) {
  552. return t('无');
  553. }
  554. return (
  555. <Typography.Text
  556. ellipsis={{ showTooltip: true }}
  557. style={{ width: 100 }}
  558. onClick={() => {
  559. setModalContent(text);
  560. setIsModalOpen(true);
  561. }}
  562. >
  563. {text}
  564. </Typography.Text>
  565. );
  566. },
  567. },
  568. ];
  569. // 根据可见性设置过滤列
  570. const getVisibleColumns = () => {
  571. return allColumns.filter((column) => visibleColumns[column.key]);
  572. };
  573. const [logs, setLogs] = useState([]);
  574. const [loading, setLoading] = useState(true);
  575. const [activePage, setActivePage] = useState(1);
  576. const [logCount, setLogCount] = useState(0);
  577. const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
  578. const [isModalOpenurl, setIsModalOpenurl] = useState(false);
  579. const [showBanner, setShowBanner] = useState(false);
  580. // 定义模态框图片URL的状态和更新函数
  581. const [modalImageUrl, setModalImageUrl] = useState('');
  582. let now = new Date();
  583. // Form 初始值
  584. const formInitValues = {
  585. channel_id: '',
  586. mj_id: '',
  587. dateRange: [
  588. timestamp2string(now.getTime() / 1000 - 2592000),
  589. timestamp2string(now.getTime() / 1000 + 3600)
  590. ],
  591. };
  592. // Form API 引用
  593. const [formApi, setFormApi] = useState(null);
  594. const [stat, setStat] = useState({
  595. quota: 0,
  596. token: 0,
  597. });
  598. // 获取表单值的辅助函数
  599. const getFormValues = () => {
  600. const formValues = formApi ? formApi.getValues() : {};
  601. // 处理时间范围
  602. let start_timestamp = timestamp2string(now.getTime() / 1000 - 2592000);
  603. let end_timestamp = timestamp2string(now.getTime() / 1000 + 3600);
  604. if (formValues.dateRange && Array.isArray(formValues.dateRange) && formValues.dateRange.length === 2) {
  605. start_timestamp = formValues.dateRange[0];
  606. end_timestamp = formValues.dateRange[1];
  607. }
  608. return {
  609. channel_id: formValues.channel_id || '',
  610. mj_id: formValues.mj_id || '',
  611. start_timestamp,
  612. end_timestamp,
  613. };
  614. };
  615. const enrichLogs = (items) => {
  616. return items.map((log) => ({
  617. ...log,
  618. timestamp2string: timestamp2string(log.created_at),
  619. key: '' + log.id,
  620. }));
  621. };
  622. const syncPageData = (payload) => {
  623. const items = enrichLogs(payload.items || []);
  624. setLogs(items);
  625. setLogCount(payload.total || 0);
  626. setActivePage(payload.page || 1);
  627. setPageSize(payload.page_size || pageSize);
  628. };
  629. const loadLogs = async (page = 1, size = pageSize) => {
  630. setLoading(true);
  631. const { channel_id, mj_id, start_timestamp, end_timestamp } = getFormValues();
  632. let localStartTimestamp = Date.parse(start_timestamp);
  633. let localEndTimestamp = Date.parse(end_timestamp);
  634. const url = isAdminUser
  635. ? `/api/mj/?p=${page}&page_size=${size}&channel_id=${channel_id}&mj_id=${mj_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`
  636. : `/api/mj/self/?p=${page}&page_size=${size}&mj_id=${mj_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
  637. const res = await API.get(url);
  638. const { success, message, data } = res.data;
  639. if (success) {
  640. syncPageData(data);
  641. } else {
  642. showError(message);
  643. }
  644. setLoading(false);
  645. };
  646. const pageData = logs;
  647. const handlePageChange = (page) => {
  648. loadLogs(page, pageSize).then();
  649. };
  650. const handlePageSizeChange = async (size) => {
  651. localStorage.setItem('mj-page-size', size + '');
  652. await loadLogs(1, size);
  653. };
  654. const refresh = async () => {
  655. await loadLogs(1, pageSize);
  656. };
  657. const copyText = async (text) => {
  658. if (await copy(text)) {
  659. showSuccess(t('已复制:') + text);
  660. } else {
  661. // setSearchKeyword(text);
  662. Modal.error({ title: t('无法复制到剪贴板,请手动复制'), content: text });
  663. }
  664. };
  665. useEffect(() => {
  666. const localPageSize = parseInt(localStorage.getItem('mj-page-size')) || ITEMS_PER_PAGE;
  667. setPageSize(localPageSize);
  668. loadLogs(1, localPageSize).then();
  669. }, []);
  670. useEffect(() => {
  671. const mjNotifyEnabled = localStorage.getItem('mj_notify_enabled');
  672. if (mjNotifyEnabled !== 'true') {
  673. setShowBanner(true);
  674. }
  675. }, []);
  676. // 列选择器模态框
  677. const renderColumnSelector = () => {
  678. return (
  679. <Modal
  680. title={t('列设置')}
  681. visible={showColumnSelector}
  682. onCancel={() => setShowColumnSelector(false)}
  683. footer={
  684. <div className="flex justify-end">
  685. <Button
  686. theme="light"
  687. onClick={() => initDefaultColumns()}
  688. className="!rounded-full"
  689. >
  690. {t('重置')}
  691. </Button>
  692. <Button
  693. theme="light"
  694. onClick={() => setShowColumnSelector(false)}
  695. className="!rounded-full"
  696. >
  697. {t('取消')}
  698. </Button>
  699. <Button
  700. type='primary'
  701. onClick={() => setShowColumnSelector(false)}
  702. className="!rounded-full"
  703. >
  704. {t('确定')}
  705. </Button>
  706. </div>
  707. }
  708. >
  709. <div style={{ marginBottom: 20 }}>
  710. <Checkbox
  711. checked={Object.values(visibleColumns).every((v) => v === true)}
  712. indeterminate={
  713. Object.values(visibleColumns).some((v) => v === true) &&
  714. !Object.values(visibleColumns).every((v) => v === true)
  715. }
  716. onChange={(e) => handleSelectAll(e.target.checked)}
  717. >
  718. {t('全选')}
  719. </Checkbox>
  720. </div>
  721. <div className="flex flex-wrap max-h-96 overflow-y-auto rounded-lg p-4" style={{ border: '1px solid var(--semi-color-border)' }}>
  722. {allColumns.map((column) => {
  723. // 为非管理员用户跳过管理员专用列
  724. if (
  725. !isAdminUser &&
  726. (column.key === COLUMN_KEYS.CHANNEL ||
  727. column.key === COLUMN_KEYS.SUBMIT_RESULT)
  728. ) {
  729. return null;
  730. }
  731. return (
  732. <div key={column.key} className="w-1/2 mb-4 pr-2">
  733. <Checkbox
  734. checked={!!visibleColumns[column.key]}
  735. onChange={(e) =>
  736. handleColumnVisibilityChange(column.key, e.target.checked)
  737. }
  738. >
  739. {column.title}
  740. </Checkbox>
  741. </div>
  742. );
  743. })}
  744. </div>
  745. </Modal>
  746. );
  747. };
  748. return (
  749. <>
  750. {renderColumnSelector()}
  751. <Layout>
  752. <Card
  753. className="!rounded-2xl mb-4"
  754. title={
  755. <div className="flex flex-col w-full">
  756. <div className="flex flex-col md:flex-row justify-between items-center">
  757. <div className="flex items-center text-orange-500 mb-2 md:mb-0">
  758. <IconEyeOpened className="mr-2" />
  759. {loading ? (
  760. <Skeleton.Title
  761. style={{
  762. width: 300,
  763. marginBottom: 0,
  764. marginTop: 0
  765. }}
  766. />
  767. ) : (
  768. <Text>
  769. {isAdminUser && showBanner
  770. ? t('当前未开启Midjourney回调,部分项目可能无法获得绘图结果,可在运营设置中开启。')
  771. : t('Midjourney 任务记录')}
  772. </Text>
  773. )}
  774. </div>
  775. </div>
  776. <Divider margin="12px" />
  777. {/* 搜索表单区域 */}
  778. <Form
  779. initValues={formInitValues}
  780. getFormApi={(api) => setFormApi(api)}
  781. onSubmit={refresh}
  782. allowEmpty={true}
  783. autoComplete="off"
  784. layout="vertical"
  785. trigger="change"
  786. stopValidateWithError={false}
  787. >
  788. <div className="flex flex-col gap-4">
  789. <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
  790. {/* 时间选择器 */}
  791. <div className="col-span-1 lg:col-span-2">
  792. <Form.DatePicker
  793. field='dateRange'
  794. className="w-full"
  795. type='dateTimeRange'
  796. placeholder={[t('开始时间'), t('结束时间')]}
  797. showClear
  798. pure
  799. />
  800. </div>
  801. {/* 任务 ID */}
  802. <Form.Input
  803. field='mj_id'
  804. prefix={<IconSearch />}
  805. placeholder={t('任务 ID')}
  806. className="!rounded-full"
  807. showClear
  808. pure
  809. />
  810. {/* 渠道 ID - 仅管理员可见 */}
  811. {isAdminUser && (
  812. <Form.Input
  813. field='channel_id'
  814. prefix={<IconSearch />}
  815. placeholder={t('渠道 ID')}
  816. className="!rounded-full"
  817. showClear
  818. pure
  819. />
  820. )}
  821. </div>
  822. {/* 操作按钮区域 */}
  823. <div className="flex justify-between items-center">
  824. <div></div>
  825. <div className="flex gap-2">
  826. <Button
  827. type='primary'
  828. htmlType='submit'
  829. loading={loading}
  830. className="!rounded-full"
  831. >
  832. {t('查询')}
  833. </Button>
  834. <Button
  835. theme='light'
  836. onClick={() => {
  837. if (formApi) {
  838. formApi.reset();
  839. // 重置后立即查询,使用setTimeout确保表单重置完成
  840. setTimeout(() => {
  841. refresh();
  842. }, 100);
  843. }
  844. }}
  845. className="!rounded-full"
  846. >
  847. {t('重置')}
  848. </Button>
  849. <Button
  850. theme='light'
  851. type='tertiary'
  852. icon={<IconSetting />}
  853. onClick={() => setShowColumnSelector(true)}
  854. className="!rounded-full"
  855. >
  856. {t('列设置')}
  857. </Button>
  858. </div>
  859. </div>
  860. </div>
  861. </Form>
  862. </div>
  863. }
  864. shadows='always'
  865. bordered={false}
  866. >
  867. <Table
  868. columns={getVisibleColumns()}
  869. dataSource={logs}
  870. rowKey='key'
  871. loading={loading}
  872. scroll={{ x: 'max-content' }}
  873. className="rounded-xl overflow-hidden"
  874. size="middle"
  875. empty={
  876. <Empty
  877. image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
  878. darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
  879. description={t('搜索无结果')}
  880. style={{ padding: 30 }}
  881. />
  882. }
  883. pagination={{
  884. formatPageText: (page) =>
  885. t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
  886. start: page.currentStart,
  887. end: page.currentEnd,
  888. total: logCount,
  889. }),
  890. currentPage: activePage,
  891. pageSize: pageSize,
  892. total: logCount,
  893. pageSizeOptions: [10, 20, 50, 100],
  894. showSizeChanger: true,
  895. onPageSizeChange: handlePageSizeChange,
  896. onPageChange: handlePageChange,
  897. }}
  898. />
  899. </Card>
  900. <Modal
  901. visible={isModalOpen}
  902. onOk={() => setIsModalOpen(false)}
  903. onCancel={() => setIsModalOpen(false)}
  904. closable={null}
  905. bodyStyle={{ height: '400px', overflow: 'auto' }} // 设置模态框内容区域样式
  906. width={800} // 设置模态框宽度
  907. >
  908. <p style={{ whiteSpace: 'pre-line' }}>{modalContent}</p>
  909. </Modal>
  910. <ImagePreview
  911. src={modalImageUrl}
  912. visible={isModalOpenurl}
  913. onVisibleChange={(visible) => setIsModalOpenurl(visible)}
  914. />
  915. </Layout>
  916. </>
  917. );
  918. };
  919. export default LogsTable;