feat(报告): 测试计划批量导出 PDF 报告
This commit is contained in:
parent
69024bbe4c
commit
3f8c1da5e1
|
@ -182,3 +182,8 @@ export function logTestPlanReportExport(reportId: string) {
|
|||
export function logTestPlanReportBatchExport(data: BatchApiParams) {
|
||||
return MSR.post({ url: reportUrl.TestPlanBatchReportExportUrl, data });
|
||||
}
|
||||
|
||||
// 批量导出报告日志
|
||||
export function testPlanBatchReportExportGetIds(data: BatchApiParams) {
|
||||
return MSR.post<string[]>({ url: reportUrl.TestPlanBatchReportExportGetIdsUrl, data });
|
||||
}
|
||||
|
|
|
@ -72,3 +72,5 @@ export const getReportShareLayoutUrl = '/test-plan/report/share/get-layout';
|
|||
export const TestPlanReportExportUrl = '/test-plan/report/export';
|
||||
// 测试计划-报告-批量导出日志
|
||||
export const TestPlanBatchReportExportUrl = '/test-plan/report/batch-export';
|
||||
// 测试计划-报告-批量导出获取报告 ID 集合
|
||||
export const TestPlanBatchReportExportGetIdsUrl = '/test-plan/report/batch-param';
|
||||
|
|
|
@ -18,19 +18,7 @@ export const PAGE_PDF_WIDTH_RATIO = CONTAINER_WIDTH / PDF_WIDTH; // 页面容器
|
|||
// 实际每页高度 = PDF页面高度/页面容器宽度与 pdf 宽度的比例(这里比例*SCALE_RATIO 是因为html2canvas截图时生成的是 SCALE_RATIO 倍的清晰度)
|
||||
export const IMAGE_HEIGHT = Math.ceil(PAGE_HEIGHT * PAGE_PDF_WIDTH_RATIO * SCALE_RATIO);
|
||||
|
||||
const commonOdfTableConfig: Partial<UserOptions> = {
|
||||
headStyles: {
|
||||
fillColor: '#793787',
|
||||
},
|
||||
styles: {
|
||||
font: 'AlibabaPuHuiTi-3-55-Regular',
|
||||
},
|
||||
rowPageBreak: 'avoid',
|
||||
margin: { top: 16, left: 16, right: 16, bottom: 16 },
|
||||
tableWidth: PDF_WIDTH,
|
||||
};
|
||||
|
||||
export type PdfTableConfig = Pick<UserOptions, 'columnStyles' | 'columns' | 'body'>;
|
||||
export type PdfTableConfig = Pick<UserOptions, 'tableId' | 'columnStyles' | 'columns' | 'body'>;
|
||||
|
||||
/**
|
||||
* 导出PDF
|
||||
|
@ -43,6 +31,7 @@ export default async function exportPDF(
|
|||
name: string,
|
||||
contentId: string,
|
||||
autoTableConfig: PdfTableConfig[],
|
||||
tableTitleMap?: Record<string, string>,
|
||||
doneCallback?: () => void
|
||||
) {
|
||||
const element = document.getElementById(contentId);
|
||||
|
@ -95,6 +84,26 @@ export default async function exportPDF(
|
|||
}
|
||||
const lastImagePageUseHeight =
|
||||
(canvasHeight > IMAGE_HEIGHT ? canvasHeight - IMAGE_HEIGHT : canvasHeight) / PAGE_PDF_WIDTH_RATIO / SCALE_RATIO; // 最后一页带图片的pdf页面被图片占用的高度
|
||||
|
||||
const commonOdfTableConfig: Partial<UserOptions> = {
|
||||
headStyles: {
|
||||
fillColor: '#793787',
|
||||
},
|
||||
styles: {
|
||||
font: 'AlibabaPuHuiTi-3-55-Regular',
|
||||
},
|
||||
rowPageBreak: 'avoid',
|
||||
margin: { top: 16, left: 16, right: 16, bottom: 16 },
|
||||
tableWidth: PDF_WIDTH,
|
||||
willDrawPage: (data) => {
|
||||
const title = tableTitleMap?.[data.table.id as string];
|
||||
if (title && data.cursor) {
|
||||
pdf.text(title, 16, data.cursor.y + 4);
|
||||
// 在文字后加入 8px 高的空白
|
||||
data.cursor.y += 12;
|
||||
}
|
||||
},
|
||||
};
|
||||
autoTableConfig.forEach((config, index) => {
|
||||
autoTable(pdf, {
|
||||
...config,
|
||||
|
|
|
@ -48,8 +48,40 @@ export default function useOpenNewPage() {
|
|||
);
|
||||
}
|
||||
|
||||
function openNewPageWithParams(
|
||||
name: RouteRecordName | undefined,
|
||||
query: Record<string, any> = {},
|
||||
params: Record<string, any> = {}
|
||||
) {
|
||||
const pId = query.pId || appStore.currentProjectId;
|
||||
if (pId) {
|
||||
// 如果传入参数指定了项目 id,则使用传入的项目 id
|
||||
delete query.pId;
|
||||
}
|
||||
const orgId = query.orgId || appStore.currentOrgId;
|
||||
if (orgId) {
|
||||
// 如果传入参数指定了组织 id,则使用传入的组织 id
|
||||
delete query.orgId;
|
||||
}
|
||||
const queryParams = new URLSearchParams(query).toString();
|
||||
const newTab = window.open(
|
||||
`${window.location.origin}#${router.resolve({ name }).fullPath}?orgId=${orgId}&pId=${pId}&${queryParams}`,
|
||||
'_blank'
|
||||
);
|
||||
|
||||
// 等待新标签页加载完成后发送消息
|
||||
if (newTab) {
|
||||
newTab.onload = () => {
|
||||
setTimeout(() => {
|
||||
newTab.postMessage(JSON.stringify(params), window.location.origin);
|
||||
}, 300);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
openNewPage,
|
||||
openNewPageWidthSingleParam,
|
||||
openNewPageWithParams,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { TreeNodeData } from '@arco-design/web-vue';
|
||||
|
||||
import type { FilterResult } from '@/components/pure/ms-advance-filter/type';
|
||||
|
||||
import { RequestMethods } from '@/enums/apiEnum';
|
||||
|
||||
// 请求返回结构
|
||||
|
@ -51,6 +53,7 @@ export interface BatchApiParams {
|
|||
versionId?: string; // 版本 ID
|
||||
refId?: string; // 版本来源
|
||||
protocols?: string[]; // 协议集合
|
||||
combineSearch?: FilterResult;
|
||||
}
|
||||
|
||||
// 移动模块树
|
||||
|
|
|
@ -153,12 +153,12 @@ export function desensitize(str: string): string {
|
|||
* @param str 标题的动态内容
|
||||
* @returns 转化后的字符串
|
||||
*/
|
||||
export function characterLimit(str?: string): string {
|
||||
export function characterLimit(str?: string, length?: number): string {
|
||||
if (!str) return '';
|
||||
if (str.length <= 20) {
|
||||
if (str.length <= (length || 20)) {
|
||||
return str;
|
||||
}
|
||||
return `${str.slice(0, 20 - 3)}...`;
|
||||
return `${str.slice(0, length || 20 - 3)}...`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -271,7 +271,7 @@
|
|||
dataIndex: 'operation',
|
||||
fixed: 'right',
|
||||
title: hasAnyPermission(['PROJECT_API_REPORT:READ+DELETE']) ? 'common.operation' : '',
|
||||
width: hasAnyPermission(['PROJECT_API_REPORT:READ+DELETE']) ? 130 : 50,
|
||||
width: hasAnyPermission(['PROJECT_API_REPORT:READ+DELETE']) ? 100 : 50,
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -135,7 +135,7 @@
|
|||
const keyword = ref<string>('');
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const { openNewPage } = useOpenNewPage();
|
||||
const { openNewPage, openNewPageWithParams } = useOpenNewPage();
|
||||
|
||||
type ReportShowType = 'All' | 'INDEPENDENT' | 'INTEGRATED';
|
||||
const showType = ref<ReportShowType>('All');
|
||||
|
@ -340,6 +340,11 @@
|
|||
eventTag: 'batchStop',
|
||||
permission: ['PROJECT_TEST_PLAN_REPORT:READ+DELETE'],
|
||||
},
|
||||
{
|
||||
label: 'common.export',
|
||||
eventTag: 'batchExport',
|
||||
permission: ['PROJECT_TEST_PLAN_REPORT:READ+EXPORT'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
@ -362,6 +367,15 @@
|
|||
projectId: appStore.currentProjectId,
|
||||
};
|
||||
|
||||
if (event.eventTag === 'batchExport') {
|
||||
openNewPageWithParams(
|
||||
FullPageEnum.FULL_PAGE_TEST_PLAN_EXPORT_PDF,
|
||||
{
|
||||
type: showType.value,
|
||||
},
|
||||
batchParams.value
|
||||
);
|
||||
} else if (event.eventTag === 'batchStop') {
|
||||
openModal({
|
||||
type: 'error',
|
||||
title: t('report.delete.tip', {
|
||||
|
@ -386,6 +400,7 @@
|
|||
},
|
||||
hideCancel: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function searchList() {
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
<template>
|
||||
<a-spin :loading="loading" :tip="t('report.detail.exportingPdf')" class="report-detail-container">
|
||||
<a-spin
|
||||
:loading="batchLoading || loading"
|
||||
:tip="batchLoading ? batchExportTip : t('report.detail.exportingPdf')"
|
||||
class="report-detail-container"
|
||||
>
|
||||
<div id="report-detail" class="report-detail">
|
||||
<div class="report-header">
|
||||
<div class="flex-1 break-all">{{ detail.name }}</div>
|
||||
|
@ -139,7 +143,9 @@
|
|||
getReportShareBugList,
|
||||
getReportShareFeatureCaseList,
|
||||
getScenarioPage,
|
||||
logTestPlanReportBatchExport,
|
||||
logTestPlanReportExport,
|
||||
testPlanBatchReportExportGetIds,
|
||||
} from '@/api/modules/test-plan/report';
|
||||
import {
|
||||
commonConfig,
|
||||
|
@ -151,8 +157,9 @@
|
|||
} from '@/config/testPlan';
|
||||
import exportPDF, { PAGE_PDF_WIDTH_RATIO, PdfTableConfig } from '@/hooks/useExportPDF';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import { addCommasToNumber } from '@/utils';
|
||||
import { addCommasToNumber, characterLimit } from '@/utils';
|
||||
|
||||
import { BatchApiParams } from '@/models/common';
|
||||
import type {
|
||||
configItem,
|
||||
countDetail,
|
||||
|
@ -175,13 +182,6 @@
|
|||
const reportId = ref<string>(route.query.id as string);
|
||||
const isGroup = computed(() => route.query.type === 'GROUP');
|
||||
const loading = ref<boolean>(true);
|
||||
const richText = ref<{ summary: string; richTextTmpFileIds?: string[] }>({
|
||||
summary: '',
|
||||
});
|
||||
|
||||
const reportForm = ref({
|
||||
reportName: '',
|
||||
});
|
||||
|
||||
/**
|
||||
* 分享share
|
||||
|
@ -620,24 +620,19 @@
|
|||
return status && iconTypeStatus[status] ? iconTypeStatus[status] : iconTypeStatus.DEFAULT;
|
||||
}
|
||||
|
||||
async function getDetail() {
|
||||
try {
|
||||
loading.value = true;
|
||||
detail.value = await getReportDetail(reportId.value);
|
||||
const { defaultLayout, id, name, summary } = detail.value;
|
||||
isDefaultLayout.value = defaultLayout;
|
||||
richText.value.summary = summary;
|
||||
reportForm.value.reportName = name;
|
||||
initOptionsData();
|
||||
if (!defaultLayout && id) {
|
||||
async function realExportPdf(name: string) {
|
||||
const tableArr: PdfTableConfig[] = [];
|
||||
if (!isDefaultLayout.value) {
|
||||
await getDefaultLayout();
|
||||
} else {
|
||||
innerCardList.value = (isGroup.value ? cloneDeep(defaultGroupConfig) : cloneDeep(defaultSingleConfig)).filter(
|
||||
(e: any) => [ReportCardTypeEnum.CUSTOM_CARD, ReportCardTypeEnum.SUMMARY].includes(e.value)
|
||||
);
|
||||
}
|
||||
if (isGroup.value) {
|
||||
await initGroupList();
|
||||
nextTick(async () => {
|
||||
exportPDF(
|
||||
name,
|
||||
'report-detail',
|
||||
[
|
||||
{
|
||||
tableArr.push({
|
||||
tableId: 'group',
|
||||
columnStyles: {
|
||||
testPlanName: { cellWidth: 710 / PAGE_PDF_WIDTH_RATIO },
|
||||
resultStatus: { cellWidth: 130 / PAGE_PDF_WIDTH_RATIO },
|
||||
|
@ -656,22 +651,12 @@
|
|||
passRate: `${e.passRate}%`,
|
||||
passThreshold: `${e.passThreshold}%`,
|
||||
})) as RowInput[],
|
||||
},
|
||||
],
|
||||
() => {
|
||||
loading.value = false;
|
||||
Message.success(t('report.detail.exportPdfSuccess'));
|
||||
}
|
||||
);
|
||||
});
|
||||
} else {
|
||||
innerCardList.value = (isGroup.value ? cloneDeep(defaultGroupConfig) : cloneDeep(defaultSingleConfig)).filter(
|
||||
(e: any) => [ReportCardTypeEnum.CUSTOM_CARD, ReportCardTypeEnum.SUMMARY].includes(e.value)
|
||||
);
|
||||
}
|
||||
await Promise.all([initBugList(), initCaseList(), initApiList(), initScenarioList()]);
|
||||
const tableArr: PdfTableConfig[] = [];
|
||||
if (fullBugList.value.length > 0) {
|
||||
tableArr.push({
|
||||
tableId: 'bug',
|
||||
columnStyles: {
|
||||
num: { cellWidth: 120 / PAGE_PDF_WIDTH_RATIO },
|
||||
title: { cellWidth: 600 / PAGE_PDF_WIDTH_RATIO },
|
||||
|
@ -688,16 +673,23 @@
|
|||
});
|
||||
}
|
||||
if (fullCaseList.value.length > 0) {
|
||||
tableArr.push({
|
||||
columnStyles: {
|
||||
const columnStyles: Record<string, any> = {
|
||||
num: { cellWidth: 100 / PAGE_PDF_WIDTH_RATIO },
|
||||
name: { cellWidth: 480 / PAGE_PDF_WIDTH_RATIO },
|
||||
name: { cellWidth: 240 / PAGE_PDF_WIDTH_RATIO },
|
||||
executeResult: { cellWidth: 110 / PAGE_PDF_WIDTH_RATIO },
|
||||
testPlanName: { cellWidth: 240 / PAGE_PDF_WIDTH_RATIO },
|
||||
priority: { cellWidth: 110 / PAGE_PDF_WIDTH_RATIO },
|
||||
moduleName: { cellWidth: 200 / PAGE_PDF_WIDTH_RATIO },
|
||||
executeUser: { cellWidth: 100 / PAGE_PDF_WIDTH_RATIO },
|
||||
relationCaseCount: { cellWidth: 90 / PAGE_PDF_WIDTH_RATIO },
|
||||
},
|
||||
};
|
||||
if (!isGroup.value) {
|
||||
delete columnStyles.testPlanName;
|
||||
columnStyles.name.cellWidth = 480 / PAGE_PDF_WIDTH_RATIO;
|
||||
}
|
||||
tableArr.push({
|
||||
tableId: 'case',
|
||||
columnStyles,
|
||||
columns: caseColumns.value.map((item) => ({
|
||||
...item,
|
||||
title: t(item.title as string),
|
||||
|
@ -710,17 +702,24 @@
|
|||
})) as RowInput[],
|
||||
});
|
||||
}
|
||||
const apiColumnStyles: Record<string, any> = {
|
||||
num: { cellWidth: 150 / PAGE_PDF_WIDTH_RATIO },
|
||||
name: { cellWidth: 240 / PAGE_PDF_WIDTH_RATIO },
|
||||
executeResult: { cellWidth: 110 / PAGE_PDF_WIDTH_RATIO },
|
||||
testPlanName: { cellWidth: 230 / PAGE_PDF_WIDTH_RATIO },
|
||||
priority: { cellWidth: 80 / PAGE_PDF_WIDTH_RATIO },
|
||||
moduleName: { cellWidth: 170 / PAGE_PDF_WIDTH_RATIO },
|
||||
executeUser: { cellWidth: 120 / PAGE_PDF_WIDTH_RATIO },
|
||||
bugCount: { cellWidth: 90 / PAGE_PDF_WIDTH_RATIO },
|
||||
};
|
||||
if (!isGroup.value) {
|
||||
delete apiColumnStyles.testPlanName;
|
||||
apiColumnStyles.name.cellWidth = 480 / PAGE_PDF_WIDTH_RATIO;
|
||||
}
|
||||
if (fullApiList.value.length > 0) {
|
||||
tableArr.push({
|
||||
columnStyles: {
|
||||
num: { cellWidth: 130 / PAGE_PDF_WIDTH_RATIO },
|
||||
name: { cellWidth: 450 / PAGE_PDF_WIDTH_RATIO },
|
||||
executeResult: { cellWidth: 110 / PAGE_PDF_WIDTH_RATIO },
|
||||
priority: { cellWidth: 80 / PAGE_PDF_WIDTH_RATIO },
|
||||
moduleName: { cellWidth: 200 / PAGE_PDF_WIDTH_RATIO },
|
||||
executeUser: { cellWidth: 130 / PAGE_PDF_WIDTH_RATIO },
|
||||
bugCount: { cellWidth: 90 / PAGE_PDF_WIDTH_RATIO },
|
||||
},
|
||||
tableId: 'apiCase',
|
||||
columnStyles: apiColumnStyles,
|
||||
columns: apiColumns.value.map((item) => ({
|
||||
...item,
|
||||
title: t(item.title as string),
|
||||
|
@ -733,17 +732,10 @@
|
|||
})) as RowInput[],
|
||||
});
|
||||
}
|
||||
if (apiColumns.value.length > 0) {
|
||||
if (fullScenarioList.value.length > 0) {
|
||||
tableArr.push({
|
||||
columnStyles: {
|
||||
num: { cellWidth: 100 / PAGE_PDF_WIDTH_RATIO },
|
||||
name: { cellWidth: 480 / PAGE_PDF_WIDTH_RATIO },
|
||||
executeResult: { cellWidth: 110 / PAGE_PDF_WIDTH_RATIO },
|
||||
priority: { cellWidth: 80 / PAGE_PDF_WIDTH_RATIO },
|
||||
moduleName: { cellWidth: 200 / PAGE_PDF_WIDTH_RATIO },
|
||||
executeUser: { cellWidth: 130 / PAGE_PDF_WIDTH_RATIO },
|
||||
bugCount: { cellWidth: 90 / PAGE_PDF_WIDTH_RATIO },
|
||||
},
|
||||
tableId: 'scenario',
|
||||
columnStyles: apiColumnStyles,
|
||||
columns: apiColumns.value.map((item) => ({
|
||||
...item,
|
||||
title: t(item.title as string),
|
||||
|
@ -756,13 +748,37 @@
|
|||
})) as RowInput[],
|
||||
});
|
||||
}
|
||||
nextTick(async () => {
|
||||
exportPDF(name, 'report-detail', tableArr, () => {
|
||||
await nextTick(async () => {
|
||||
exportPDF(
|
||||
name,
|
||||
'report-detail',
|
||||
tableArr,
|
||||
{
|
||||
group: t('report.detail.subPlanDetails'),
|
||||
bug: t('report.detail.bugDetails'),
|
||||
case: t('report.detail.featureCaseDetails'),
|
||||
apiCase: t('report.detail.apiCaseDetails'),
|
||||
scenario: t('report.detail.scenarioCaseDetails'),
|
||||
},
|
||||
() => {
|
||||
loading.value = false;
|
||||
Message.success(t('report.detail.exportPdfSuccess'));
|
||||
});
|
||||
Message.success(t('report.detail.exportPdfSuccess', { name: characterLimit(name, 50) }));
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async function getDetail(_reportId?: string) {
|
||||
try {
|
||||
loading.value = true;
|
||||
if (_reportId) {
|
||||
reportId.value = _reportId;
|
||||
}
|
||||
detail.value = await getReportDetail(reportId.value);
|
||||
const { defaultLayout, name } = detail.value;
|
||||
isDefaultLayout.value = defaultLayout;
|
||||
initOptionsData();
|
||||
await realExportPdf(name);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
|
@ -770,13 +786,59 @@
|
|||
}
|
||||
}
|
||||
|
||||
async function logExport() {
|
||||
async function logExport(params?: BatchApiParams) {
|
||||
if (params) {
|
||||
await logTestPlanReportBatchExport(params);
|
||||
} else {
|
||||
await logTestPlanReportExport(route.query.id as string);
|
||||
}
|
||||
}
|
||||
|
||||
const batchLoading = ref<boolean>(false);
|
||||
const batchIds = ref<string[]>([]);
|
||||
const exportCurrent = ref<number>(0);
|
||||
const exportTotal = ref<number>(0);
|
||||
const batchExportTip = computed(() =>
|
||||
t('report.detail.batchExportingPdf', { current: exportCurrent.value, total: exportTotal.value })
|
||||
);
|
||||
|
||||
async function initBatchIds(params: BatchApiParams) {
|
||||
try {
|
||||
batchLoading.value = true;
|
||||
batchIds.value = await testPlanBatchReportExportGetIds(params);
|
||||
exportTotal.value = batchIds.value.length;
|
||||
while (batchIds.value.length > 0) {
|
||||
exportCurrent.value += 1;
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await getDetail(batchIds.value.shift());
|
||||
}
|
||||
nextTick(() => {
|
||||
// 等最后一条成功提示后清空
|
||||
Message.clear();
|
||||
Message.success(t('report.detail.batchExportPdfSuccess'));
|
||||
});
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
} finally {
|
||||
batchLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
window.addEventListener('message', (event) => {
|
||||
if (event.origin !== window.location.origin) {
|
||||
return;
|
||||
}
|
||||
// 初始化批量导出报告 id 集合
|
||||
const batchParams = event.data;
|
||||
initBatchIds(batchParams);
|
||||
logExport(batchParams);
|
||||
});
|
||||
if (reportId.value) {
|
||||
getDetail();
|
||||
logExport();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -59,5 +59,7 @@ export default {
|
|||
'report.detail.manualGenReportSuccess': 'The custom generated report was successful',
|
||||
'report.detail.exportPdf': 'Export PDF',
|
||||
'report.detail.exportingPdf': 'Exporting PDF report...',
|
||||
'report.detail.exportPdfSuccess': 'PDF report exported successfully',
|
||||
'report.detail.exportPdfSuccess': '{name} report exported successfully',
|
||||
'report.detail.batchExportPdfSuccess': 'Batch export of PDF reports completed',
|
||||
'report.detail.batchExportingPdf': 'Export progress: {current}/{total}',
|
||||
};
|
||||
|
|
|
@ -58,6 +58,8 @@ export default {
|
|||
'report.detail.reportNameNotEmpty': '报告名称不能为空',
|
||||
'report.detail.manualGenReportSuccess': '自定义生成报告成功',
|
||||
'report.detail.exportPdf': '导出 PDF',
|
||||
'report.detail.exportingPdf': 'PDF报告导出中...',
|
||||
'report.detail.exportPdfSuccess': 'PDF报告导出成功',
|
||||
'report.detail.exportingPdf': 'PDF 报告导出中...',
|
||||
'report.detail.exportPdfSuccess': '{name} 报告导出成功',
|
||||
'report.detail.batchExportPdfSuccess': '批量导出 PDF 报告已完成',
|
||||
'report.detail.batchExportingPdf': '导出进度:{current}/{total}',
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue