CleanLogModal.vue 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865
  1. <template>
  2. <QueryTableModal
  3. :open="open"
  4. :title="title"
  5. :width="1120"
  6. :loading="loading"
  7. :filters="filters"
  8. :filter-schema="filterSchema"
  9. :columns="columns"
  10. :action-column="actionColumn"
  11. :data-source="tableData"
  12. :pagination="pagination"
  13. :total-pages="totalPages"
  14. :page-size-options="pageSizeOptions"
  15. :show-table-setting="false"
  16. :table-setting="{ size: true, setting: true, redo: false, fullScreen: false }"
  17. :can-fullscreen="true"
  18. wrap-class-name="case-clean-log-modal"
  19. @update:open="handleOpenChange"
  20. @search="handleSearch"
  21. @reset="handleReset"
  22. @prev-page="handlePrevPage"
  23. @next-page="handleNextPage"
  24. @page-size-change="handlePageSizeChange"
  25. @page-change="handlePageChange"
  26. @sort-change="handleSortChange"
  27. />
  28. <BasicModal
  29. :open="detailOpen"
  30. title="日志详情"
  31. :width="860"
  32. :showOkBtn="false"
  33. :showCancelBtn="false"
  34. wrapClassName="case-clean-log-detail-modal"
  35. @update:open="handleDetailOpenChange"
  36. @cancel="handleDetailOpenChange(false)"
  37. >
  38. <div class="case-clean-log-detail">
  39. <div class="case-clean-log-detail__meta">
  40. <div class="meta-item">
  41. <span class="label">文件名称</span>
  42. <span class="value">{{ getDisplayText(detailRecord?.fileName) }}</span>
  43. </div>
  44. <div class="meta-item">
  45. <span class="label">工作表</span>
  46. <span class="value">{{ getDisplayText(detailRecord?.sheetName) }}</span>
  47. </div>
  48. <div class="meta-item">
  49. <span class="label">记录时间</span>
  50. <span class="value">{{ getDisplayText(detailRecord?.createDateTime) }}</span>
  51. </div>
  52. </div>
  53. <div class="case-clean-log-detail__content">
  54. <div class="content-title">日志内容</div>
  55. <div class="content-body">{{ getDisplayText(detailRecord?.content) }}</div>
  56. </div>
  57. </div>
  58. </BasicModal>
  59. </template>
  60. <script lang="ts" setup name="ViewsDataCleanLogModal">
  61. import { computed, h, nextTick, reactive, ref, watch } from 'vue';
  62. import componentSetting from '@jeesite/core/settings/componentSetting';
  63. import { BasicModal } from '@jeesite/core/components/Modal';
  64. import { useMessage } from '@jeesite/core/hooks/web/useMessage';
  65. import type { FileInfo } from '@jeesite/types/api';
  66. import QueryTableModal from '@jeesite/case/views/dm/components/QueryTableModal.vue';
  67. import type { CleanErrorLogItem } from '@jeesite/case/api/govern/governApi';
  68. import { cleanErrorLogList } from '@jeesite/case/api/govern/governApi';
  69. const ALL_SCOPE = '__all__';
  70. const DEFAULT_SORT_KEY = 'createDateTime';
  71. const DEFAULT_SORT_ORDER = 'DESC' as const;
  72. const LOG_FETCH_LIMIT = 500;
  73. interface ImportSheetScope {
  74. key: string;
  75. label: string;
  76. querySheetId?: string;
  77. }
  78. interface ImportFileScope {
  79. key: string;
  80. label: string;
  81. queryFileId?: string;
  82. sheets: ImportSheetScope[];
  83. }
  84. interface LogQueryTarget {
  85. fileId?: string;
  86. sheetId?: string;
  87. }
  88. interface LogTableRow {
  89. rowKey: string;
  90. id: number;
  91. fileName: string;
  92. sheetName: string;
  93. content: string;
  94. createDateTime: string;
  95. raw: CleanErrorLogItem;
  96. }
  97. const props = withDefaults(
  98. defineProps<{
  99. open: boolean;
  100. title?: string;
  101. batchId?: string | number;
  102. fileInfos?: FileInfo[];
  103. }>(),
  104. {
  105. title: '清洗日志',
  106. batchId: '',
  107. fileInfos: () => [],
  108. },
  109. );
  110. const emit = defineEmits<{
  111. (e: 'update:open', value: boolean): void;
  112. }>();
  113. const { createMessage } = useMessage();
  114. const loading = ref(false);
  115. const sourceLogs = ref<CleanErrorLogItem[]>([]);
  116. const tableData = ref<LogTableRow[]>([]);
  117. const detailOpen = ref(false);
  118. const detailRecord = ref<CleanErrorLogItem | null>(null);
  119. const filters = reactive({
  120. fileScopeId: ALL_SCOPE,
  121. sheetScopeId: ALL_SCOPE,
  122. orderKey: DEFAULT_SORT_KEY,
  123. sort: DEFAULT_SORT_ORDER as '' | 'ASC' | 'DESC',
  124. });
  125. const pagination = reactive({
  126. page: 1,
  127. limit: componentSetting.table.defaultPageSize,
  128. total: 0,
  129. });
  130. const pageSizeOptions = componentSetting.table.pageSizeOptions.map((value) => ({
  131. label: value,
  132. value,
  133. }));
  134. const normalizeText = (value: unknown) => String(value ?? '').trim();
  135. const normalizeId = (value: unknown) => normalizeText(value);
  136. const normalizeBatchId = (value: unknown) => normalizeText(value);
  137. const isRootItem = (item?: FileInfo | null) => {
  138. const parentId = normalizeId(item?.pid);
  139. return !parentId || parentId === '0' || parentId === 'null' || parentId === 'undefined';
  140. };
  141. const getDisplayText = (value: unknown, fallback = '-') => {
  142. const text = normalizeText(value);
  143. return text || fallback;
  144. };
  145. const createFileScope = (key: string, label: string, queryFileId?: string): ImportFileScope => ({
  146. key,
  147. label,
  148. queryFileId,
  149. sheets: [],
  150. });
  151. const findBatchId = (items?: FileInfo[] | null): string | undefined => {
  152. if (!Array.isArray(items) || !items.length) return undefined;
  153. for (const item of items) {
  154. const batchId = normalizeBatchId(item?.batchId);
  155. if (batchId) return batchId;
  156. const childBatchId = findBatchId(Array.isArray(item?.children) ? item.children : []);
  157. if (childBatchId) return childBatchId;
  158. }
  159. return undefined;
  160. };
  161. const explicitBatchId = computed(() => normalizeBatchId(props.batchId));
  162. const currentBatchId = computed(() => explicitBatchId.value || findBatchId(props.fileInfos));
  163. const importedScopes = computed<ImportFileScope[]>(() => {
  164. const scopes: ImportFileScope[] = [];
  165. const scopeMap = new Map<string, ImportFileScope>();
  166. const rootScopeMap = new Map<string, ImportFileScope>();
  167. const looseSheets: FileInfo[] = [];
  168. const ensureFileScope = (key: string, label: string, queryFileId?: string) => {
  169. const normalizedKey = key || `file_${scopeMap.size + 1}`;
  170. const matched = scopeMap.get(normalizedKey);
  171. if (matched) {
  172. if (!matched.queryFileId && queryFileId) matched.queryFileId = queryFileId;
  173. if (!normalizeText(matched.label) && normalizeText(label)) matched.label = label;
  174. return matched;
  175. }
  176. const nextScope = createFileScope(normalizedKey, label || `文件${scopeMap.size + 1}`, queryFileId);
  177. scopeMap.set(normalizedKey, nextScope);
  178. scopes.push(nextScope);
  179. if (queryFileId) {
  180. rootScopeMap.set(queryFileId, nextScope);
  181. }
  182. return nextScope;
  183. };
  184. const ensureSheetScope = (
  185. scope: ImportFileScope,
  186. key: string,
  187. label: string,
  188. querySheetId?: string,
  189. fallbackIndex = 0,
  190. ) => {
  191. const normalizedKey = key || `${scope.key}_sheet_${fallbackIndex}`;
  192. const matched = scope.sheets.find((item) => item.key === normalizedKey);
  193. if (matched) {
  194. if (!matched.querySheetId && querySheetId) matched.querySheetId = querySheetId;
  195. if (!normalizeText(matched.label) && normalizeText(label)) matched.label = label;
  196. return matched;
  197. }
  198. const nextSheet = {
  199. key: normalizedKey,
  200. label: label || `工作表${scope.sheets.length + 1}`,
  201. querySheetId,
  202. };
  203. scope.sheets.push(nextSheet);
  204. return nextSheet;
  205. };
  206. const registerNestedNode = (item: FileInfo, parentScope?: ImportFileScope, fallbackIndex = 0) => {
  207. const itemId = normalizeId(item.id);
  208. const fileLabel = normalizeText(item.fileName || item.sheetName) || `文件${fallbackIndex + 1}`;
  209. const sheetLabel = normalizeText(item.sheetName || item.fileName) || `工作表${fallbackIndex + 1}`;
  210. const children = Array.isArray(item.children) ? item.children : [];
  211. if (!parentScope && (isRootItem(item) || children.length > 0)) {
  212. const fileScope = ensureFileScope(itemId || `file_${fallbackIndex + 1}`, fileLabel, itemId || undefined);
  213. children.forEach((child, childIndex) => registerNestedNode(child, fileScope, childIndex));
  214. return;
  215. }
  216. if (parentScope) {
  217. ensureSheetScope(parentScope, itemId, sheetLabel, itemId || undefined, fallbackIndex);
  218. children.forEach((child, childIndex) => registerNestedNode(child, parentScope, childIndex));
  219. return;
  220. }
  221. looseSheets.push(item);
  222. };
  223. (props.fileInfos || []).forEach((item, index) => registerNestedNode(item, undefined, index));
  224. looseSheets.forEach((item, index) => {
  225. const parentId = normalizeId(item.pid);
  226. const querySheetId = normalizeId(item.id) || undefined;
  227. const fileLabel = normalizeText(item.fileName) || `文件${index + 1}`;
  228. const sheetLabel = normalizeText(item.sheetName || item.fileName) || `工作表${index + 1}`;
  229. const matchedScope =
  230. (parentId && rootScopeMap.get(parentId)) ||
  231. ensureFileScope(parentId || fileLabel, fileLabel, parentId || undefined);
  232. ensureSheetScope(
  233. matchedScope,
  234. querySheetId || `${matchedScope.key}_sheet_${index}`,
  235. sheetLabel,
  236. querySheetId,
  237. index,
  238. );
  239. });
  240. return scopes;
  241. });
  242. const isSingleFileScope = computed(() => importedScopes.value.length === 1);
  243. const fileOptions = computed(() => [
  244. ...(isSingleFileScope.value ? [] : [{ label: '当前任务全部文件', value: ALL_SCOPE }]),
  245. ...importedScopes.value.map((item) => ({
  246. label: item.label,
  247. value: item.key,
  248. })),
  249. ]);
  250. const selectedFileScope = computed(
  251. () => importedScopes.value.find((item) => item.key === filters.fileScopeId) || null,
  252. );
  253. const defaultFileScopeId = computed(() =>
  254. isSingleFileScope.value ? importedScopes.value[0]?.key || ALL_SCOPE : ALL_SCOPE,
  255. );
  256. const sheetOptions = computed(() => {
  257. if (!selectedFileScope.value) {
  258. return [{ label: '全部工作表', value: ALL_SCOPE }];
  259. }
  260. return [
  261. { label: '全部工作表', value: ALL_SCOPE },
  262. ...selectedFileScope.value.sheets.map((item) => ({
  263. label: item.label,
  264. value: item.key,
  265. })),
  266. ];
  267. });
  268. const filterSchema = computed(
  269. () =>
  270. [
  271. {
  272. field: 'fileScopeId',
  273. label: '文件',
  274. type: 'select',
  275. width: 260,
  276. maxWidth: 320,
  277. options: fileOptions.value,
  278. },
  279. {
  280. field: 'sheetScopeId',
  281. label: '工作表',
  282. type: 'select',
  283. width: 220,
  284. maxWidth: 280,
  285. options: sheetOptions.value,
  286. },
  287. ] as const,
  288. );
  289. const renderTableText = (value: unknown, className?: string) => {
  290. const text = getDisplayText(value);
  291. return h(
  292. 'span',
  293. {
  294. class: className,
  295. title: text,
  296. },
  297. text,
  298. );
  299. };
  300. const columns = [
  301. { title: '序号', dataIndex: 'id', key: 'id', width: 68 },
  302. {
  303. title: '文件名称',
  304. dataIndex: 'fileName',
  305. key: 'fileName',
  306. width: 220,
  307. customRender: ({ text }: { text: string }) => renderTableText(text),
  308. },
  309. {
  310. title: '工作表',
  311. dataIndex: 'sheetName',
  312. key: 'sheetName',
  313. width: 180,
  314. customRender: ({ text }: { text: string }) => renderTableText(text),
  315. },
  316. {
  317. title: '日志内容',
  318. dataIndex: 'content',
  319. key: 'content',
  320. width: 460,
  321. customRender: ({ text }: { text: string }) => renderTableText(text, 'log-content-cell'),
  322. },
  323. {
  324. title: '记录时间',
  325. dataIndex: 'createDateTime',
  326. key: 'createDateTime',
  327. width: 180,
  328. sorter: true,
  329. customRender: ({ text }: { text: string }) => renderTableText(text),
  330. },
  331. ];
  332. const actionColumn = {
  333. title: '操作',
  334. dataIndex: 'action',
  335. key: 'action',
  336. width: 92,
  337. fixed: 'right',
  338. align: 'center',
  339. actions: (record: LogTableRow) => [
  340. {
  341. label: '详情',
  342. onClick: () => {
  343. detailRecord.value = record.raw;
  344. detailOpen.value = true;
  345. },
  346. },
  347. ],
  348. };
  349. const totalPages = computed(() => {
  350. if (!pagination.total) return 1;
  351. return Math.max(1, Math.ceil(pagination.total / pagination.limit));
  352. });
  353. const normalizeList = (response: any): CleanErrorLogItem[] => {
  354. if (Array.isArray(response)) return response;
  355. if (Array.isArray(response?.records)) return response.records;
  356. if (Array.isArray(response?.data?.records)) return response.data.records;
  357. if (Array.isArray(response?.list)) return response.list;
  358. if (Array.isArray(response?.data?.list)) return response.data.list;
  359. return [];
  360. };
  361. const createTargetKey = (target: LogQueryTarget) => `${target.fileId || ''}__${target.sheetId || ''}`;
  362. const buildTargetsByScope = (scope: ImportFileScope, sheetScopeKey?: string) => {
  363. if (sheetScopeKey && sheetScopeKey !== ALL_SCOPE) {
  364. const matchedSheet = scope.sheets.find((item) => item.key === sheetScopeKey);
  365. if (!matchedSheet) return [] as LogQueryTarget[];
  366. return [
  367. {
  368. fileId: scope.queryFileId,
  369. sheetId: matchedSheet.querySheetId,
  370. },
  371. ].filter((item) => item.fileId || item.sheetId);
  372. }
  373. if (scope.queryFileId) {
  374. return [{ fileId: scope.queryFileId }];
  375. }
  376. return scope.sheets
  377. .map((item) => ({
  378. sheetId: item.querySheetId,
  379. }))
  380. .filter((item) => item.sheetId);
  381. };
  382. const resolveRequestTargets = () => {
  383. if (selectedFileScope.value) {
  384. return buildTargetsByScope(selectedFileScope.value, filters.sheetScopeId);
  385. }
  386. const targetMap = new Map<string, LogQueryTarget>();
  387. importedScopes.value.forEach((scope) => {
  388. buildTargetsByScope(scope).forEach((target) => {
  389. targetMap.set(createTargetKey(target), target);
  390. });
  391. });
  392. return targetMap.size ? Array.from(targetMap.values()) : [{}];
  393. };
  394. const matchesTarget = (item: CleanErrorLogItem, target: LogQueryTarget) => {
  395. const fileId = normalizeId(item.fileId);
  396. const sheetId = normalizeId(item.sheetId);
  397. if (target.fileId && target.sheetId) {
  398. return fileId === target.fileId && sheetId === target.sheetId;
  399. }
  400. if (target.fileId) {
  401. return fileId === target.fileId;
  402. }
  403. if (target.sheetId) {
  404. return sheetId === target.sheetId;
  405. }
  406. return true;
  407. };
  408. const getSortNumber = (value: unknown) => {
  409. const timeValue = value ? new Date(String(value)).getTime() : Number.NaN;
  410. return Number.isFinite(timeValue) ? timeValue : 0;
  411. };
  412. const compareText = (left: unknown, right: unknown) =>
  413. getDisplayText(left).localeCompare(getDisplayText(right), 'zh-CN', { numeric: true, sensitivity: 'base' });
  414. const filteredLogs = computed(() => {
  415. let logs = sourceLogs.value.slice();
  416. if (selectedFileScope.value) {
  417. const targets = buildTargetsByScope(selectedFileScope.value, filters.sheetScopeId);
  418. if (targets.length) {
  419. logs = logs.filter((item) => targets.some((target) => matchesTarget(item, target)));
  420. }
  421. }
  422. const sortKey = filters.orderKey || DEFAULT_SORT_KEY;
  423. const sortOrder = filters.sort || DEFAULT_SORT_ORDER;
  424. logs.sort((left, right) => {
  425. let compareResult = 0;
  426. if (sortKey === 'createDateTime') {
  427. compareResult = getSortNumber(left.createDateTime) - getSortNumber(right.createDateTime);
  428. } else {
  429. compareResult = compareText((left as any)?.[sortKey], (right as any)?.[sortKey]);
  430. }
  431. return sortOrder === 'ASC' ? compareResult : -compareResult;
  432. });
  433. return logs;
  434. });
  435. const buildRowKey = (item: CleanErrorLogItem, absoluteIndex: number) =>
  436. normalizeId(item.id) ||
  437. [item.fileId, item.sheetId, item.createDateTime, absoluteIndex].map((value) => normalizeText(value)).join('_');
  438. const syncVisibleRows = () => {
  439. pagination.total = filteredLogs.value.length;
  440. const maxPage = Math.max(1, Math.ceil(Math.max(1, pagination.total) / pagination.limit));
  441. if (pagination.page > maxPage) {
  442. pagination.page = maxPage;
  443. }
  444. const startIndex = (pagination.page - 1) * pagination.limit;
  445. tableData.value = filteredLogs.value.slice(startIndex, startIndex + pagination.limit).map((item, index) => ({
  446. rowKey: buildRowKey(item, startIndex + index),
  447. id: startIndex + index + 1,
  448. fileName: getDisplayText(item.fileName),
  449. sheetName: getDisplayText(item.sheetName),
  450. content: getDisplayText(item.content),
  451. createDateTime: getDisplayText(item.createDateTime),
  452. raw: item,
  453. }));
  454. };
  455. const resetFilters = () => {
  456. filters.fileScopeId = defaultFileScopeId.value;
  457. filters.sheetScopeId = ALL_SCOPE;
  458. filters.orderKey = DEFAULT_SORT_KEY;
  459. filters.sort = DEFAULT_SORT_ORDER;
  460. };
  461. const fetchLogs = async () => {
  462. loading.value = true;
  463. detailOpen.value = false;
  464. try {
  465. const batchId = currentBatchId.value;
  466. const responses = batchId
  467. ? [
  468. await cleanErrorLogList({
  469. page: 1,
  470. limit: LOG_FETCH_LIMIT,
  471. batchId,
  472. }),
  473. ]
  474. : await Promise.all(
  475. // The backend only supports a single fileId/sheetId per request, so current-task logs
  476. // are aggregated client-side from the imported files when batchId is unavailable.
  477. resolveRequestTargets().map((target) =>
  478. cleanErrorLogList({
  479. page: 1,
  480. limit: LOG_FETCH_LIMIT,
  481. ...target,
  482. }),
  483. ),
  484. );
  485. const logMap = new Map<string, CleanErrorLogItem>();
  486. responses.forEach((response) => {
  487. normalizeList(response).forEach((item, index) => {
  488. const key =
  489. normalizeId(item.id) ||
  490. [item.fileId, item.sheetId, item.createDateTime, normalizeText(item.content), index].join('_');
  491. if (!logMap.has(key)) {
  492. logMap.set(key, item);
  493. }
  494. });
  495. });
  496. sourceLogs.value = Array.from(logMap.values());
  497. } catch (error) {
  498. sourceLogs.value = [];
  499. createMessage.error('日志加载失败');
  500. } finally {
  501. loading.value = false;
  502. }
  503. };
  504. const handleSearch = async () => {
  505. pagination.page = 1;
  506. await fetchLogs();
  507. };
  508. const handleReset = async () => {
  509. resetFilters();
  510. pagination.page = 1;
  511. await fetchLogs();
  512. };
  513. const handlePrevPage = () => {
  514. if (pagination.page <= 1) return;
  515. pagination.page -= 1;
  516. };
  517. const handleNextPage = () => {
  518. if (pagination.page >= totalPages.value) return;
  519. pagination.page += 1;
  520. };
  521. const handlePageChange = (page: number, pageSize: number) => {
  522. if (loading.value) return;
  523. if (pageSize !== pagination.limit) {
  524. handlePageSizeChange(pageSize);
  525. return;
  526. }
  527. if (page === pagination.page - 1) {
  528. handlePrevPage();
  529. return;
  530. }
  531. if (page === pagination.page + 1) {
  532. handleNextPage();
  533. return;
  534. }
  535. if (page === pagination.page) return;
  536. pagination.page = page;
  537. };
  538. const handlePageSizeChange = (size: number) => {
  539. pagination.limit = Number(size);
  540. pagination.page = 1;
  541. };
  542. const handleSortChange = (payload: { orderKey: string; sort: 'ASC' | 'DESC' }) => {
  543. filters.orderKey = payload.orderKey;
  544. filters.sort = payload.sort;
  545. pagination.page = 1;
  546. };
  547. const handleOpenChange = (value: boolean) => {
  548. if (!value) {
  549. handleDetailOpenChange(false);
  550. }
  551. emit('update:open', value);
  552. };
  553. const handleDetailOpenChange = (value: boolean) => {
  554. detailOpen.value = value;
  555. if (!value) {
  556. detailRecord.value = null;
  557. }
  558. };
  559. let initFetchScheduled = false;
  560. const scheduleInitFetch = () => {
  561. if (!props.open || initFetchScheduled) return;
  562. initFetchScheduled = true;
  563. nextTick(async () => {
  564. initFetchScheduled = false;
  565. if (!props.open) return;
  566. resetFilters();
  567. pagination.page = 1;
  568. await fetchLogs();
  569. });
  570. };
  571. watch(
  572. () => props.open,
  573. (open) => {
  574. if (!open) {
  575. initFetchScheduled = false;
  576. detailOpen.value = false;
  577. return;
  578. }
  579. scheduleInitFetch();
  580. },
  581. );
  582. watch(
  583. () => props.fileInfos,
  584. () => {
  585. filters.fileScopeId = defaultFileScopeId.value;
  586. if (filters.sheetScopeId !== ALL_SCOPE) {
  587. const hasMatchedSheet = sheetOptions.value.some((item) => item.value === filters.sheetScopeId);
  588. if (!hasMatchedSheet) {
  589. filters.sheetScopeId = ALL_SCOPE;
  590. }
  591. }
  592. if (!props.open) return;
  593. scheduleInitFetch();
  594. },
  595. { deep: true },
  596. );
  597. watch(
  598. () => props.batchId,
  599. () => {
  600. if (!props.open) return;
  601. scheduleInitFetch();
  602. },
  603. );
  604. watch(
  605. () => filters.fileScopeId,
  606. () => {
  607. filters.sheetScopeId = ALL_SCOPE;
  608. },
  609. );
  610. watch(
  611. [filteredLogs, () => pagination.page, () => pagination.limit],
  612. () => {
  613. syncVisibleRows();
  614. },
  615. { immediate: true },
  616. );
  617. defineExpose({
  618. reload: fetchLogs,
  619. });
  620. </script>
  621. <style lang="less">
  622. .case-clean-log-modal {
  623. .ant-modal {
  624. padding-bottom: 0;
  625. }
  626. .ant-modal-content {
  627. border-radius: 14px;
  628. background: linear-gradient(180deg, #fdfefe 0%, #ffffff 100%);
  629. box-shadow: 0 18px 42px rgba(15, 23, 42, 0.14);
  630. }
  631. .ant-modal-header {
  632. padding: 12px 16px;
  633. background: @modal-header-bg-color;
  634. border-bottom-color: #e5e7eb;
  635. }
  636. .ant-modal-title {
  637. color: #111827;
  638. font-size: 16px;
  639. font-weight: 600;
  640. line-height: 24px;
  641. }
  642. .ant-modal-body {
  643. padding: 16px;
  644. }
  645. .ant-modal-footer {
  646. padding: 8px 16px 10px;
  647. border-top-color: #e5e7eb;
  648. background: #fff;
  649. }
  650. .query-table-search {
  651. background: linear-gradient(180deg, #fafcff 0%, #ffffff 100%);
  652. border-color: #e2e8f0;
  653. }
  654. .query-table-content {
  655. .ant-table-thead > tr > th {
  656. background: #f8fafc;
  657. color: #334155;
  658. }
  659. .ant-table-tbody > tr > td {
  660. vertical-align: top;
  661. }
  662. .ant-table-tbody > tr:hover > td {
  663. background: #f8fbff;
  664. }
  665. }
  666. .log-content-cell {
  667. display: -webkit-box;
  668. overflow: hidden;
  669. white-space: normal;
  670. word-break: break-word;
  671. line-height: 1.6;
  672. color: #7f1d1d;
  673. -webkit-box-orient: vertical;
  674. -webkit-line-clamp: 2;
  675. }
  676. }
  677. .case-clean-log-detail-modal {
  678. .ant-modal-content {
  679. border-radius: 14px;
  680. overflow: hidden;
  681. }
  682. .ant-modal-header {
  683. padding: 12px 16px;
  684. border-bottom-color: #e5e7eb;
  685. background: @modal-header-bg-color;
  686. }
  687. .ant-modal-title {
  688. color: #111827;
  689. font-size: 16px;
  690. font-weight: 600;
  691. line-height: 24px;
  692. }
  693. .ant-modal-body {
  694. padding: 16px;
  695. }
  696. }
  697. .case-clean-log-detail {
  698. display: flex;
  699. flex-direction: column;
  700. gap: 16px;
  701. &__meta {
  702. display: grid;
  703. grid-template-columns: repeat(3, minmax(0, 1fr));
  704. gap: 12px;
  705. }
  706. &__content {
  707. padding: 16px;
  708. border: 1px solid #e5e7eb;
  709. border-radius: 12px;
  710. background: #f8fafc;
  711. }
  712. .meta-item {
  713. padding: 14px 16px;
  714. border: 1px solid #e5e7eb;
  715. border-radius: 12px;
  716. background: #fff;
  717. display: flex;
  718. flex-direction: column;
  719. gap: 6px;
  720. min-width: 0;
  721. }
  722. .label {
  723. color: #64748b;
  724. font-size: 12px;
  725. font-weight: 500;
  726. }
  727. .value {
  728. color: #111827;
  729. font-size: 13px;
  730. line-height: 1.6;
  731. word-break: break-word;
  732. }
  733. .content-title {
  734. margin-bottom: 8px;
  735. color: #475569;
  736. font-size: 12px;
  737. font-weight: 600;
  738. }
  739. .content-body {
  740. color: #7f1d1d;
  741. font-size: 13px;
  742. line-height: 1.8;
  743. white-space: pre-wrap;
  744. word-break: break-word;
  745. }
  746. @media (max-width: 768px) {
  747. &__meta {
  748. grid-template-columns: 1fr;
  749. }
  750. }
  751. }
  752. </style>