perf: 导出word 的目录索引

main
Lei OT 4 months ago
parent 13aa0b1ea0
commit d9e9ffa362

@ -1,60 +1,16 @@
import { Button } from 'antd';
// import { useProductsAuditStatesMapVal, useProductsTypesMapVal } from '@/hooks/useProductsSets';
// import { useTranslation } from 'react-i18next';
// import useProductsStore, { getAgencyAllExtrasAction } from '@/stores/Products/Index';
// import RequireAuth from '@/components/RequireAuth';
// import { PERM_PRODUCTS_OFFER_AUDIT } from '@/config';
// import dayjs from 'dayjs';
import { exportDoc } from './Index';
// import AgencyContract from '../Print/AgencyContract';
import { saveAs } from 'file-saver';
import { Packer } from 'docx';
// import { isEmpty } from '@/utils/commons';
const ExportDocxBtn = ({ ...props }) => {
// const { t } = useTranslation();
// const [agencyProducts] = useProductsStore((state) => [state.agencyProducts]);
// const [activeAgency] = useProductsStore((state) => [state.activeAgency]);
// const { travel_agency_id, use_year, audit_state } = params;
// const auditStatesMap = useProductsAuditStatesMapVal();
// const productsTypesMapVal = useProductsTypesMapVal();
// const { getRemarkList } = useProductsStore((selector) => ({
// getRemarkList: selector.getRemarkList,
// }));
// const handleDownload = async () => {
// // await refresh();
// const _agencyExtras = await getAgencyAllExtrasAction(params);
// const agencyExtras = Object.keys(_agencyExtras).reduce((acc, pid) => {
// const pitemExtras = _agencyExtras[pid];
// const _pitem = (pitemExtras || []).map(eitem => ({ ...eitem, info: { ...eitem.info, product_type_name_txt: productsTypesMapVal[eitem.info.product_type_id]?.label || eitem.info.product_type_name } } ));
// return { ...acc, [pid]: _pitem };
// }, {});
// const remarks = await getRemarkList();
// const remarksMappedByType = remarks.reduce((r, v) => ({ ...r, [v.product_type_id]: v }), {});
// const documentCreator = new AgencyContract();
// const doc = documentCreator.create([
// params,
// activeAgency,
// agencyProducts,
// agencyExtras,
// // remarks,
// remarksMappedByType,
// ]);
// const _d = dayjs().format('YYYYMMDD_HH.mm.ss.SSS'); // Date.now().toString(32)
// // console.log(params);
// const _state = isEmpty(audit_state) ? '' : auditStatesMap[audit_state].label;
// Packer.toBlob(doc).then((blob) => {
// saveAs(blob, `${activeAgency.travel_agency_name}${use_year}-${_state}-${_d}.docx`);
// });
// };
const ExportDocxBtn = ({ subject, title, sectionsData, ...props }) => {
return (
<>
<Button size='small' onClick={() => {}}>
导出 .docx
</Button>
<Button size='small'
onClick={() => {
exportDoc({title, subject, sectionsData});
}}
>
导出 .docx
</Button>
</>
);
};

@ -220,7 +220,7 @@ const createHeaderRight = () =>
],
});
// Template function
const createDoc = async (agencyName, title, sectionsData) => {
const createDoc = async ({title, subject, sectionsData}) => {
const logoArrayBuffer = await getLogoArrayBuffer(logoPath);
const image = new docx.ImageRun({
@ -475,13 +475,14 @@ const createDoc = async (agencyName, title, sectionsData) => {
}),
},
children: [
createTitle(agencyName),
createTitle(title),
new docx.TableOfContents('toc', {
hyperlink: true,
headingStyleRange: '1-5',
useAppliedParagraphOutlineLevel: true,
}),
createTitle(title),
createTitle(subject),
...sectionsData.reduce((arr, { tableTitle, tableColumns, tableData }) => {
const _tableTitle = new Paragraph({
text: tableTitle,
@ -500,8 +501,8 @@ const createDoc = async (agencyName, title, sectionsData) => {
};
// Export function
export async function exportDoc(agencyName, title, sectionsData) {
const doc = await createDoc(agencyName, title, sectionsData);
export async function exportDoc({title, subject, sectionsData}) {
const doc = await createDoc({title, subject, sectionsData});
const blob = await Packer.toBlob(doc);
saveAs(blob, `${title}.${agencyName}.docx`);
saveAs(blob, `${subject}.${title}.docx`);
}

@ -1,308 +0,0 @@
import { isEmpty } from '@/utils/commons';
import dayjs from 'dayjs';
import {
AlignmentType,
BorderStyle,
Document,
Footer,
Header,
HeadingLevel,
LevelFormat,
NumberFormat,
PageNumber,
Paragraph,
Tab,
Table,
TableCell,
TableRow,
TabStopType,
TextRun,
WidthType,
Media,
} from 'docx';
// import { splitTable_6, splitTable_7, splitTable_B, splitTable_D, splitTable_J, splitTable_Q, splitTable_R, splitTable_8 } from '@/hooks/useProductsQuotationFormat';
// import { formatGroupSize } from '@/hooks/useProductsSets';
import logo from './cht letter header logo.png';
const unitMap = {
'0': '人',
'1': '团',
};
const DOC_TITLE = '地接合同';
const pageMargins = {
top: `15mm`,
bottom: `15mm`,
left: `10mm`,
right: `10mm`,
};
const tableBorderNone = {
top: { style: BorderStyle.NONE, size: 0, color: 'FFFFFF' },
bottom: { style: BorderStyle.NONE, size: 0, color: 'FFFFFF' },
left: { style: BorderStyle.NONE, size: 0, color: 'FFFFFF' },
right: { style: BorderStyle.NONE, size: 0, color: 'FFFFFF' },
};
const tableBorderOne = {
top: { style: BorderStyle.SINGLE, space: 0, size: 6, color: 'auto' },
bottom: { style: BorderStyle.SINGLE, space: 0, size: 6, color: 'auto' },
left: { style: BorderStyle.SINGLE, space: 0, size: 6, color: 'auto' },
right: { style: BorderStyle.SINGLE, space: 0, size: 6, color: 'auto' },
};
const tableBorderInner = {
top: { style: BorderStyle.NONE, space: 0, size: 6, color: 'auto' },
bottom: { style: BorderStyle.INSET, space: 0, size: 6, color: 'auto' },
left: { style: BorderStyle.NONE, space: 0, size: 6, color: 'auto' },
right: { style: BorderStyle.INSET, space: 0, size: 6, color: 'auto' },
};
const tableBorderInnerB = {
top: { style: BorderStyle.NONE, space: 0, size: 6, color: 'auto' },
bottom: { style: BorderStyle.INSET, space: 0, size: 6, color: 'auto' },
left: { style: BorderStyle.NONE, space: 0, size: 6, color: 'auto' },
right: { style: BorderStyle.NONE, space: 0, size: 6, color: 'auto' },
};
const tableBorderInnerDashB = {
top: { style: BorderStyle.NONE, space: 0, size: 6, color: 'auto' },
bottom: { style: BorderStyle.DASHED, space: 0, size: 6, color: 'auto' },
left: { style: BorderStyle.NONE, space: 0, size: 6, color: 'auto' },
right: { style: BorderStyle.NONE, space: 0, size: 6, color: 'auto' },
};
const tableBorderInnerT = {
top: { style: BorderStyle.INSET, space: 0, size: 6, color: 'auto' },
bottom: { style: BorderStyle.NONE, space: 0, size: 6, color: 'auto' },
left: { style: BorderStyle.NONE, space: 0, size: 6, color: 'auto' },
right: { style: BorderStyle.NONE, space: 0, size: 6, color: 'auto' },
};
const tableBorderInnerR = {
top: { style: BorderStyle.NONE, space: 0, size: 6, color: 'auto' },
bottom: { style: BorderStyle.NONE, space: 0, size: 6, color: 'auto' },
left: { style: BorderStyle.NONE, space: 0, size: 6, color: 'auto' },
right: { style: BorderStyle.INSET, space: 0, size: 6, color: 'auto' },
};
/**
* @version
* @date 2025-08-20
*/
export default class TemplateLetter2025 {
#remarkList = {};
create([headerParams, activeAgency, agencyProducts, agencyExtras, remarks]) {
this.#remarkList = remarks;
const { use_year } = headerParams;
const h1 = `${activeAgency.travel_agency_name}${use_year}${DOC_TITLE}`;
const yearStart = dayjs(`${use_year}-01-01`).format('YYYY.MM.DD');
const yearEnd = dayjs(`${use_year}-12-31`).format('YYYY.MM.DD');
const document = new Document({
creator: 'China Highlights',
title: h1,
subject: 'CH信笺2025',
styles: {
paragraphStyles: [
{
id: 'Normal',
name: 'Normal',
quickFormat: true,
run: {
size: 22,
font: { name: '宋体' },
color: '000000',
},
paragraph: {
spacing: {
after: 10,
},
},
},
{
id: 'Title',
name: 'Title',
basedOn: 'Normal',
next: 'Normal',
quickFormat: true,
run: {
size: 44,
font: { name: '宋体' },
color: '000000',
},
paragraph: {
spacing: {
before: 200,
after: 200,
},
},
},
{
id: 'Heading1',
name: 'Heading 1',
basedOn: 'Normal',
next: 'Normal',
quickFormat: true,
run: {
size: 32,
font: { name: '宋体' },
color: '000000',
},
paragraph: {
spacing: {
before: 200,
after: 200,
},
},
},
{
id: 'Heading2',
name: 'Heading 2',
basedOn: 'Normal',
next: 'Normal',
quickFormat: true,
run: {
size: 28,
font: { name: '宋体' },
color: '000000',
},
paragraph: {
spacing: {
before: 120,
after: 120,
},
},
},
{
id: 'TableHeader',
name: 'Table Header',
basedOn: 'Normal',
next: 'Normal',
quickFormat: true,
run: {
size: 22,
font: { name: '宋体' },
color: '000000',
bold: true,
},
paragraph: {
spacing: {
before: 80,
after: 80,
},
},
},
],
},
numbering: {
config: [
{
reference: 'products-type',
levels: [{ level: 0, text: '%1、', format: LevelFormat.CHINESE_COUNTING }],
},
{
reference: 'terms',
levels: [{ level: 0, text: '%1.', format: LevelFormat.DECIMAL }],
},
],
},
sections: [
{
properties: {
page: {
pageNumbers: {
start: 1,
formatType: NumberFormat.DECIMAL,
},
margin: pageMargins,
},
},
headers: {
default: new Header({
children: [
this.createPageHeaderRightText(`Tel: 86-773-2885311`, { italics: false }),
this.createPageHeaderRightText(`Fax: 86-773-2827424`, { italics: false }),
this.createPageHeaderRightText(`E-mail: products@chinahighlights.com`, { italics: false }),
this.createPageHeaderRightText(`Web site: https://www.chinahighlights.com`, { italics: false }),
],
}),
},
footers: {
default: new Footer({
children: [
new Paragraph({
alignment: AlignmentType.CENTER,
children: [
new TextRun({
children: ['第', PageNumber.CURRENT, '页'],
size: 20,
}),
new TextRun({
children: [', 共 ', PageNumber.TOTAL_PAGES, '页'],
size: 20,
}),
],
}),
new Paragraph({
alignment: AlignmentType.RIGHT,
children: [
new TextRun({
text: `${new Date().toLocaleString()}系统生成`,
italics: true,
size: 20,
}),
],
}),
],
}),
},
children: [],
},
],
});
return document;
}
createHeading(text) {
return new Paragraph({
text: text,
heading: HeadingLevel.HEADING_1,
alignment: AlignmentType.CENTER,
});
}
createTitle(text) {
return new Paragraph({
text: text,
heading: HeadingLevel.TITLE,
alignment: AlignmentType.CENTER,
});
}
createSubHeading(text, style = {}) {
return new Paragraph({
text: text,
heading: HeadingLevel.HEADING_1,
...style,
});
}
async createPageHeaderLeftLogo(doc) {
const response = await fetch(logo);
const imageBuffer = await response.arrayBuffer();
return new Paragraph({
children: [
Media.addImage(doc, imageBuffer, 120, 60), // width, height
],
});
}
createPageHeaderRightText(text, style = {}) {
return new Paragraph({
alignment: AlignmentType.RIGHT,
children: [
new TextRun({
text: text,
italics: true,
size: 20,
...style,
}),
],
});
}
}

@ -7,6 +7,7 @@ import SearchForm from '../components/search/SearchForm';
import useHostCaseStore from '../zustand/HostCase';
import { useShallow } from 'zustand/shallow';
import { exportDoc } from '../components/TemplateLetter2025/Index';
import ExportDocxBtn from '../components/TemplateLetter2025/ExportDocxBtn';
const HostCaseReport = ({ ...props }) => {
const { date_picker_store } = useContext(stores_Context);
@ -26,6 +27,7 @@ const HostCaseReport = ({ ...props }) => {
const summaryCols = [
{ title: '接团数', dataIndex: 'group_count', width: '6rem' },
{ title: 'feedback团数', dataIndex: 'feedbak_group', width: '6rem' },
{ title: '东道主团数', dataIndex: 'group_count_dongdaozhu', width1: '8rem' },
{ title: '东道主个数', dataIndex: 'case_count_dongdaozhu', width1: '8rem' },
{ title: '东道主实施比例', dataIndex: 'dongdaozhu_rate' },
@ -75,17 +77,15 @@ const HostCaseReport = ({ ...props }) => {
</Row>
<div className="max-w-screen-xl " style={{}}>
<Divider orientation="right">
<Button size='small'
onClick={() => {
exportDoc(forExport.agencyName, forExport.title, [
{ tableTitle: '2025年东道主总体情况', tableColumns: summaryCols, tableData: caseSummary },
{ tableTitle: '2025年导游实施东道主情况', tableColumns: guideSummaryCols, tableData: caseSummaryByGuide },
{ tableTitle: '2025年精品案例', tableColumns: featuredCaseCols, tableData: caseFeatured },
]);
}}
>
导出 .docx
</Button>
<ExportDocxBtn
subject={forExport.title}
title={forExport.agencyName}
sectionsData={[
{ tableTitle: '2025年东道主总体情况', tableColumns: summaryCols, tableData: caseSummary },
{ tableTitle: '2025年导游实施东道主情况', tableColumns: guideSummaryCols, tableData: caseSummaryByGuide },
{ tableTitle: '2025年精品案例', tableColumns: featuredCaseCols, tableData: caseFeatured },
]}
/>
</Divider>
<Typography.Title level={3}>2025年东道主总体情况</Typography.Title>
<Table dataSource={caseSummary} columns={summaryCols} loading={loading} pagination={false} bordered />

Loading…
Cancel
Save