Compare commits
65 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
9edfa0c5b8 | 7 days ago |
|
|
195d0facaa | 2 weeks ago |
|
|
0f2041cdc8 | 2 weeks ago |
|
|
ce4e23fe6c | 2 weeks ago |
|
|
62ba5b5973 | 1 month ago |
|
|
986f27eb28 | 1 month ago |
|
|
9a59a5ac0e | 1 month ago |
|
|
11be41a446 | 2 months ago |
|
|
da3f4458ff | 2 months ago |
|
|
e2d91d9df8 | 2 months ago |
|
|
594426048c | 2 months ago |
|
|
d6c34f145e | 2 months ago |
|
|
90714b6f82 | 2 months ago |
|
|
ae07f7593b | 3 months ago |
|
|
460dd47f1c | 3 months ago |
|
|
46f3a52784 | 3 months ago |
|
|
93fa13ac9d | 3 months ago |
|
|
5351239f5f | 3 months ago |
|
|
a44a05de26 | 3 months ago |
|
|
c023dd5b68 | 3 months ago |
|
|
16e6ec574a | 3 months ago |
|
|
ead5b37ded | 3 months ago |
|
|
5f7a942842 | 3 months ago |
|
|
1f5d6a9047 | 3 months ago |
|
|
7cb91abea2 | 3 months ago |
|
|
8581038aeb | 3 months ago |
|
|
3b24aa1373 | 3 months ago |
|
|
cb994aff29 | 3 months ago |
|
|
a48ed8ff03 | 3 months ago |
|
|
8cd446d8f8 | 3 months ago |
|
|
559ba14e35 | 3 months ago |
|
|
a2b920b313 | 3 months ago |
|
|
9793f62b34 | 3 months ago |
|
|
a66ed759f8 | 3 months ago |
|
|
a9a2fa8a29 | 3 months ago |
|
|
3da72b3287 | 3 months ago |
|
|
259afa80f1 | 3 months ago |
|
|
d9fccc5433 | 3 months ago |
|
|
407ac32e93 | 3 months ago |
|
|
7a9bd5531e | 3 months ago |
|
|
dfc6515029 | 3 months ago |
|
|
d5bc8a639f | 3 months ago |
|
|
e6fe06a533 | 3 months ago |
|
|
af9fab4ea8 | 3 months ago |
|
|
7c00c9167e | 3 months ago |
|
|
e581f9df73 | 3 months ago |
|
|
abb95fc4d0 | 3 months ago |
|
|
b0700feebb | 4 months ago |
|
|
b9b12297e1 | 4 months ago |
|
|
2bfd6e9f7f | 4 months ago |
|
|
a6a8139e78 | 4 months ago |
|
|
59bc3d0a18 | 4 months ago |
|
|
d86d9f8282 | 4 months ago |
|
|
313b9bdaa4 | 4 months ago |
|
|
e39fe19559 | 4 months ago |
|
|
97f6bdeb9e | 4 months ago |
|
|
4f42623896 | 4 months ago |
|
|
3116dc8ac3 | 4 months ago |
|
|
214266fa74 | 4 months ago |
|
|
18e199deac | 4 months ago |
|
|
d9e9ffa362 | 4 months ago |
|
|
13aa0b1ea0 | 4 months ago |
|
|
63a6604b0b | 4 months ago |
|
|
b4bca91fea | 4 months ago |
|
|
ced0756c1c | 4 months ago |
@ -0,0 +1,17 @@
|
||||
import { Button } from 'antd';
|
||||
import { exportDoc } from './Index';
|
||||
|
||||
const ExportDocxBtn = ({ subject, title, sectionsData, ...props }) => {
|
||||
return (
|
||||
<>
|
||||
<Button size='small'
|
||||
onClick={() => {
|
||||
exportDoc({title, subject, sectionsData});
|
||||
}}
|
||||
>
|
||||
导出 .docx
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default ExportDocxBtn;
|
||||
@ -0,0 +1,517 @@
|
||||
import {
|
||||
Packer,
|
||||
Paragraph,
|
||||
Table,
|
||||
TableRow,
|
||||
TableCell,
|
||||
WidthType,
|
||||
TextRun,
|
||||
AlignmentType,
|
||||
FrameAnchorType,
|
||||
HorizontalPositionAlign,
|
||||
VerticalPositionAlign,
|
||||
HeadingLevel,
|
||||
LevelFormat,
|
||||
NumberFormat,
|
||||
PageNumber,
|
||||
BorderStyle,
|
||||
LineRuleType,
|
||||
HorizontalPositionRelativeFrom,
|
||||
VerticalPositionRelativeFrom,
|
||||
} from 'docx';
|
||||
import * as docx from 'docx';
|
||||
import { saveAs } from 'file-saver';
|
||||
import logoPath from './cht letter header logo.png';
|
||||
|
||||
const pageMargins = {
|
||||
top: `10mm`,
|
||||
bottom: `10mm`,
|
||||
left: `20mm`,
|
||||
right: `20mm`,
|
||||
};
|
||||
// Helper function (e.g., in a separate file or within your component logic)
|
||||
async function getLogoArrayBuffer(logoUrl) {
|
||||
const response = await fetch(logoUrl);
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
return arrayBuffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 只支持两级的嵌套表头
|
||||
*/
|
||||
function buildTable(cols, data) {
|
||||
const spanCols = cols.some((col) => col?.children?.length)
|
||||
? [null].reduce((a, c, ri) => {
|
||||
const r0col = cols.map((col) => ({ ...col, rowSpan: col?.children?.length ? 1 : 2, columnSpan: col?.children?.length ? col.children.length : 1 }));
|
||||
a.push(r0col);
|
||||
const r1col = cols
|
||||
.filter((cc) => cc?.children?.length)
|
||||
.reduce((ac, cc) => {
|
||||
const allChild = cc.children.map((ccc) => ({ ...ccc }));
|
||||
ac.push(...allChild);
|
||||
return ac;
|
||||
}, []);
|
||||
a.push(r1col);
|
||||
return a;
|
||||
}, [])
|
||||
: [cols];
|
||||
const flatCols = cols.some((col) => col?.children?.length)
|
||||
? cols.reduce((a, col) => {
|
||||
col?.children?.length === undefined && a.push({ ...col, rowSpan: col?.children?.length ? 1 : 2, colSpan: col?.children?.length ? col.children.length : 1 });
|
||||
const r1col = (col?.children || []).map((ccc) => ({ ...ccc, rowSpan: 1, colSpan: 1 }));
|
||||
return a.concat(r1col);
|
||||
}, [])
|
||||
: cols;
|
||||
const thead = spanCols.map(
|
||||
(th) =>
|
||||
new TableRow({
|
||||
children: th.map(
|
||||
(td) =>
|
||||
new TableCell({
|
||||
rowSpan: td.rowSpan,
|
||||
columnSpan: td.columnSpan,
|
||||
children: [new Paragraph({ text: td.title, alignment: AlignmentType.CENTER, style: 'TableHeader' })],
|
||||
verticalAlign: AlignmentType.CENTER,
|
||||
...(td.width ? { width: { size: 1200, type: WidthType.DXA } } : {}),
|
||||
})
|
||||
),
|
||||
})
|
||||
);
|
||||
const rows = data.map(
|
||||
(row, ci) =>
|
||||
new TableRow({
|
||||
children: flatCols.map(
|
||||
(cell) =>
|
||||
new TableCell({
|
||||
// width: { size: 100 / flatCols.length, type: WidthType.PERCENTAGE },
|
||||
margins: {
|
||||
top: 100,
|
||||
bottom: 100,
|
||||
left: 100,
|
||||
right: 100,
|
||||
},
|
||||
alignment: typeof row[cell.dataIndex] === 'number' ? AlignmentType.RIGHT : AlignmentType.START,
|
||||
verticalAlign: AlignmentType.CENTER,
|
||||
children: [
|
||||
new Paragraph({
|
||||
text: String(row[cell.dataIndex]),
|
||||
verticalAlign: AlignmentType.CENTER,
|
||||
alignment: typeof row[cell.dataIndex] === 'number' ? AlignmentType.RIGHT : AlignmentType.START,
|
||||
}),
|
||||
],
|
||||
})
|
||||
),
|
||||
})
|
||||
);
|
||||
return new Table({
|
||||
rows: [...thead, ...rows],
|
||||
width: { size: 100, type: WidthType.PERCENTAGE },
|
||||
});
|
||||
}
|
||||
|
||||
function createTitle(text) {
|
||||
return new Paragraph({
|
||||
text,
|
||||
heading: HeadingLevel.TITLE,
|
||||
alignment: AlignmentType.CENTER,
|
||||
pageBreakBefore: true,
|
||||
});
|
||||
}
|
||||
|
||||
const createHeaderText = () =>
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun('Hello World'), // ✅ Required before Textbox
|
||||
// new docx.Textbox({
|
||||
// // anchor: {
|
||||
// // horizontal: FrameAnchorType.MARGIN,
|
||||
// // vertical: FrameAnchorType.MARGIN,
|
||||
// // },
|
||||
// // alignment: {
|
||||
// // x: HorizontalPositionAlign.RIGHT,
|
||||
// // y: VerticalPositionAlign.TOP,
|
||||
// // },
|
||||
// alignment: 'right',
|
||||
// verticalAlign: AlignmentType.END,
|
||||
// children: [
|
||||
// new TextRun({
|
||||
// text: '111Tel: 86-773-2885311',
|
||||
// break: 1,
|
||||
// }),
|
||||
// new TextRun({
|
||||
// text: 'Fax: 86-773-2827424',
|
||||
// break: 1,
|
||||
// }),
|
||||
// new TextRun({
|
||||
// text: 'E-mail: products@chinahighlights.com',
|
||||
// break: 1,
|
||||
// }),
|
||||
// new TextRun({
|
||||
// text: 'Web site: https://www.chinahighlights.com',
|
||||
// break: 1,
|
||||
// }),
|
||||
// ],
|
||||
// positioning: 'Floating',
|
||||
// floating: {
|
||||
// horizontalPosition: {
|
||||
// relative: HorizontalPositionAlign.LEFT,
|
||||
// offset: 0,
|
||||
// },
|
||||
// verticalPosition: {
|
||||
// relative: VerticalPositionAlign.TOP,
|
||||
// offset: 0,
|
||||
// },
|
||||
// },
|
||||
// // style: { width: "200pt", height: "auto" }
|
||||
// }),
|
||||
],
|
||||
});
|
||||
const createHeaderRight = () =>
|
||||
new Paragraph({
|
||||
frame: {
|
||||
// position: 0,
|
||||
width: 4000,
|
||||
height: 800,
|
||||
anchor: {
|
||||
horizontal: FrameAnchorType.MARGIN,
|
||||
vertical: FrameAnchorType.MARGIN,
|
||||
},
|
||||
alignment: {
|
||||
x: HorizontalPositionAlign.RIGHT,
|
||||
y: VerticalPositionAlign.TOP,
|
||||
},
|
||||
},
|
||||
alignment: AlignmentType.RIGHT,
|
||||
verticalAlign: AlignmentType.END,
|
||||
style: 'Header',
|
||||
// thematicBreak: true,
|
||||
// border: {
|
||||
// top: { color: 'auto', space: 10, value: BorderStyle.DOUBLE, size: 20 },
|
||||
// bottom: { color: 'bf192a', space: 10, value: BorderStyle.DOUBLE, size: 20 },
|
||||
// left: { color: 'auto', space: 10, value: BorderStyle.DOUBLE, size: 20 },
|
||||
// right: { color: 'auto', space: 10, value: BorderStyle.DOUBLE, size: 20 },
|
||||
// },
|
||||
floating: {
|
||||
horizontalPosition: {
|
||||
relative: HorizontalPositionAlign.LEFT,
|
||||
offset: 0,
|
||||
},
|
||||
verticalPosition: {
|
||||
relative: VerticalPositionAlign.TOP,
|
||||
offset: 0,
|
||||
},
|
||||
},
|
||||
children: [
|
||||
new TextRun({
|
||||
text: 'Tel: 86-773-2885311',
|
||||
break: 1,
|
||||
}),
|
||||
new TextRun({
|
||||
text: 'Fax: 86-773-2827424',
|
||||
break: 1,
|
||||
}),
|
||||
new TextRun({
|
||||
text: 'E-mail: products@chinahighlights.com',
|
||||
break: 1,
|
||||
}),
|
||||
new TextRun({
|
||||
text: 'Web site: https://www.chinahighlights.com',
|
||||
break: 1,
|
||||
}),
|
||||
],
|
||||
});
|
||||
// Template function
|
||||
const createDoc = async ({title, subject, sectionsData}) => {
|
||||
const logoArrayBuffer = await getLogoArrayBuffer(logoPath);
|
||||
|
||||
const image = new docx.ImageRun({
|
||||
type: 'png',
|
||||
data: logoArrayBuffer,
|
||||
// Set dimensions in **EMUs** (English Metric Units) or **pixels**.
|
||||
// If using pixels, docx converts them, but EMUs (914400 EMUs = 1 inch) are standard.
|
||||
// E.g., for a 75x75 pixel image, you might use:
|
||||
// width: 75,
|
||||
// height: 75,
|
||||
transformation: {
|
||||
width: 100,
|
||||
height: 66,
|
||||
},
|
||||
// positioning: 'Floating',
|
||||
// floating: {
|
||||
// horizontalPosition: {
|
||||
// relative: HorizontalPositionAlign.LEFT,
|
||||
// offset: 0,
|
||||
// },
|
||||
// verticalPosition: {
|
||||
// relative: VerticalPositionAlign.TOP,
|
||||
// offset: 0,
|
||||
// },
|
||||
// },
|
||||
});
|
||||
|
||||
const sectionSettings = {
|
||||
properties: {
|
||||
page: {
|
||||
pageNumbers: {
|
||||
start: 1,
|
||||
formatType: NumberFormat.DECIMAL,
|
||||
},
|
||||
margin: pageMargins,
|
||||
},
|
||||
},
|
||||
headers: {
|
||||
default: new docx.Header({
|
||||
children: [
|
||||
new docx.Paragraph({
|
||||
children: [image],
|
||||
alignment: docx.AlignmentType.LEFT, // Align the image in the header
|
||||
style: 'Header',
|
||||
// todo: 边框位置被图文框占用
|
||||
// thematicBreak: true,
|
||||
border: {
|
||||
top: { style: 'none', size: 10, space: 0, color: 'bf192a' },
|
||||
bottom: { style: 'inset', size: 20, space: 0, color: 'bf192a' },
|
||||
left: { style: 'none', size: 10, space: 0, color: 'bf192a' },
|
||||
right: { style: 'none', size: 10, space: 0, color: 'bf192a' },
|
||||
},
|
||||
}),
|
||||
createHeaderRight(),
|
||||
// createHeaderText(),
|
||||
],
|
||||
}),
|
||||
},
|
||||
footers: {
|
||||
default: new docx.Footer({
|
||||
children: [
|
||||
new Paragraph({
|
||||
alignment: AlignmentType.END,
|
||||
style: 'Footer',
|
||||
// thematicBreak: true,
|
||||
border: {
|
||||
top: { style: 'none', size: 10, space: 0, color: 'bf192a' },
|
||||
bottom: { style: 'inset', size: 20, space: 0, color: 'bf192a' },
|
||||
left: { style: 'none', size: 10, space: 0, color: 'bf192a' },
|
||||
right: { style: 'none', size: 10, space: 0, color: 'bf192a' },
|
||||
},
|
||||
children: [
|
||||
new TextRun({
|
||||
children: ['- ', PageNumber.CURRENT, ' -'],
|
||||
size: 18,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new Paragraph({
|
||||
alignment: AlignmentType.CENTER,
|
||||
style: 'Footer',
|
||||
children: [
|
||||
new TextRun({
|
||||
text: '中国 桂林市七里店路70号创意产业园6号楼4层 桂林海纳国际旅行社有限公司 邮编541004',
|
||||
// break: 1,
|
||||
}),
|
||||
new TextRun({
|
||||
text: 'China Highlights, Discovery Your Way (Since 1959)!',
|
||||
break: 1,
|
||||
font: 'Arial',
|
||||
bold: true,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
// new Paragraph({
|
||||
// alignment: AlignmentType.RIGHT,
|
||||
// children: [
|
||||
// new TextRun({
|
||||
// text: `${new Date().toLocaleString()}系统生成`,
|
||||
// italics: true,
|
||||
// size: 20,
|
||||
// }),
|
||||
// ],
|
||||
// }),
|
||||
],
|
||||
}),
|
||||
},
|
||||
};
|
||||
const doc = new docx.Document({
|
||||
creator: 'China Highlights',
|
||||
subject: 'CH信笺2025',
|
||||
styles: {
|
||||
paragraphStyles: [
|
||||
{
|
||||
id: 'Header',
|
||||
name: 'Header',
|
||||
quickFormat: true,
|
||||
basedOn: 'Normal',
|
||||
next: 'Normal',
|
||||
run: {
|
||||
size: 16,
|
||||
font: { name: 'Verdana' },
|
||||
color: '000000',
|
||||
// underline: { type: 'single', width: 1, color: 'bf192a' },
|
||||
},
|
||||
paragraph: {
|
||||
spacing: {
|
||||
// after: 200,
|
||||
// line: 240,
|
||||
// lineRule: LineRuleType.AT_LEAST,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Footer',
|
||||
name: 'Footer',
|
||||
quickFormat: true,
|
||||
run: {
|
||||
size: 16,
|
||||
font: { name: 'Verdana' },
|
||||
color: '000000',
|
||||
// underline: { type: 'single', width: 1, color: 'bf192a' },
|
||||
},
|
||||
paragraph: {
|
||||
spacing: {
|
||||
after: 100,
|
||||
line: 240,
|
||||
// lineRule:
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
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: 'header1',
|
||||
levels: [{ level: 0, text: '%1、', format: LevelFormat.CHINESE_COUNTING }],
|
||||
},
|
||||
],
|
||||
},
|
||||
// 目录
|
||||
features: {
|
||||
updateFields: true,
|
||||
},
|
||||
sections: [
|
||||
{
|
||||
...sectionSettings,
|
||||
children: [
|
||||
createTitle(title),
|
||||
new docx.TableOfContents('toc', {
|
||||
hyperlink: true,
|
||||
headingStyleRange: '1-5',
|
||||
useAppliedParagraphOutlineLevel: true,
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
...sectionSettings,
|
||||
children: [
|
||||
|
||||
createTitle(subject),
|
||||
...sectionsData.reduce((arr, { tableTitle, tableColumns, tableData }) => {
|
||||
const _tableTitle = new Paragraph({
|
||||
text: tableTitle,
|
||||
heading: HeadingLevel.HEADING_1,
|
||||
alignment: AlignmentType.START,
|
||||
numbering: { reference: 'header1', level: 0 },
|
||||
});
|
||||
const table = buildTable(tableColumns, tableData);
|
||||
return [...arr, _tableTitle, table];
|
||||
}, []),
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
return doc;
|
||||
};
|
||||
|
||||
// Export function
|
||||
export async function exportDoc({title, subject, sectionsData}) {
|
||||
const doc = await createDoc({title, subject, sectionsData});
|
||||
const blob = await Packer.toBlob(doc);
|
||||
saveAs(blob, `${subject}.${title}.docx`);
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import { Select } from 'antd';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
// 导游语种列表
|
||||
export const languageOptions = [
|
||||
{ value: '', label: '所有语种' },
|
||||
{ value: '102001', label: '英语' },
|
||||
{ value: '102002', label: '普通话' },
|
||||
{ value: '102003', label: '日语' },
|
||||
{ value: '102004', label: '韩语' },
|
||||
{ value: '102005', label: '德语' },
|
||||
{ value: '102006', label: '法语' },
|
||||
{ value: '102007', label: '意大利语' },
|
||||
{ value: '102008', label: '西班牙语' },
|
||||
{ value: '102009', label: '俄语' },
|
||||
{ value: '102010', label: '粤语' },
|
||||
{ value: '102011', label: '印尼语' },
|
||||
{ value: '102012', label: '泰国语' },
|
||||
{ value: '102013', label: '葡萄牙语' }
|
||||
];
|
||||
|
||||
export const GuideLanguageSelect = ({ value, onChange, ...props }) => {
|
||||
return (
|
||||
<div>
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
defaultValue={['']}
|
||||
placeholder="选择导游语种"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
allowClear={true}
|
||||
options={languageOptions}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default observer(GuideLanguageSelect);
|
||||
@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { Select } from 'antd';
|
||||
import { observer } from 'mobx-react';
|
||||
import { lineClass } from './../../libs/ht';
|
||||
|
||||
export const LineClassSelector = ({ value, onChange, ...props }) => {
|
||||
return (
|
||||
<div>
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
placeholder="选择来源类型"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
allowClear
|
||||
labelInValue
|
||||
{...props}
|
||||
options={lineClass}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default observer(LineClassSelector);
|
||||
@ -1,95 +0,0 @@
|
||||
import { HT_HOST }from "../config";
|
||||
|
||||
function checkStatus(response) {
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
return response;
|
||||
} else {
|
||||
const message =
|
||||
'Fetch error: ' + response.url + ' ' + response.status + ' (' +
|
||||
response.statusText + ')';
|
||||
const error = new Error(message);
|
||||
error.response = response;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function fetchText(url) {
|
||||
return fetch(url)
|
||||
.then(checkStatus)
|
||||
.then(response => response.text())
|
||||
.catch(error => {
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
export function fetchJSON(url, data) {
|
||||
let params = '';
|
||||
let ifp = '';
|
||||
if (data) {
|
||||
params = new URLSearchParams(data).toString();
|
||||
ifp = params ? '?' : ifp;
|
||||
}
|
||||
ifp = url.includes('?') ? '' : ifp;
|
||||
const host = /^https?:\/\//i.test(url) ? '': HT_HOST;
|
||||
return fetch(`${host}${url}${ifp}${params}`)
|
||||
.then(checkStatus)
|
||||
.then(response => response.json())
|
||||
.catch(error => {
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
export function postForm(url, data) {
|
||||
return fetch(url, {
|
||||
method: 'POST',
|
||||
body: data
|
||||
}).then(checkStatus)
|
||||
.then(response => response.json())
|
||||
.catch(error => {
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
export function postJSON(url, obj) {
|
||||
const host = /^https?:\/\//i.test(url) ? '': HT_HOST;
|
||||
return fetch(`${host}${url}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(obj),
|
||||
headers: {
|
||||
'Content-type': 'application/json; charset=UTF-8'
|
||||
}
|
||||
}).then(checkStatus)
|
||||
.then(response => response.json())
|
||||
.catch(error => {
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
export function postStream(url, obj) {
|
||||
return fetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(obj),
|
||||
headers: {
|
||||
'Content-type': 'application/octet-stream'
|
||||
}
|
||||
}).then(checkStatus)
|
||||
.then(response => response.json())
|
||||
.catch(error => {
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
export function delJSON(url, obj) {
|
||||
const host = /^https?:\/\//i.test(url) ? '': HT_HOST;
|
||||
return fetch(`${host}${url}`, {
|
||||
method: 'DELETE',
|
||||
body: JSON.stringify(obj),
|
||||
headers: {
|
||||
'Content-type': 'application/json; charset=UTF-8'
|
||||
}
|
||||
}).then(checkStatus)
|
||||
.then(response => response.json())
|
||||
.catch(error => {
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
@ -0,0 +1,132 @@
|
||||
import { useContext } from 'react';
|
||||
import { Row, Col, Typography, Space, Table, Divider, Button } from 'antd';
|
||||
import { stores_Context } from '../config';
|
||||
import { observer } from 'mobx-react';
|
||||
import { toJS } from 'mobx';
|
||||
import SearchForm from '../components/search/SearchForm';
|
||||
import useHostCaseStore from '../zustand/HostCase';
|
||||
import { useShallow } from 'zustand/shallow';
|
||||
import { exportDoc } from '../components/TemplateLetter2025/Index';
|
||||
import { RenderVSDataCell } from './../components/Data';
|
||||
import ExportDocxBtn from '../components/TemplateLetter2025/ExportDocxBtn';
|
||||
|
||||
const sorter = (a, b, key) => parseInt(a[key]) - parseInt(b[key]);
|
||||
const HostCaseReport = ({ ...props }) => {
|
||||
const { date_picker_store } = useContext(stores_Context);
|
||||
// const host_case_data = customer_store.host_case_data;
|
||||
|
||||
const [loading, reset, searchValues, setSearchValues, forExport, searchValuesToSub] = useHostCaseStore(
|
||||
useShallow((state) => [state.loading, state.reset, state.searchValues, state.setSearchValues, state.forExport, state.searchValuesToSub])
|
||||
);
|
||||
const [caseSummary, caseSummaryByGuide, caseFeatured, getCaseReport] = useHostCaseStore(
|
||||
useShallow((state) => [state.caseSummary, state.caseSummaryByGuide, state.caseFeatured, state.getCaseReport])
|
||||
);
|
||||
|
||||
const getData = async (formVal) => {
|
||||
reset();
|
||||
await getCaseReport(formVal);
|
||||
};
|
||||
|
||||
|
||||
const summaryCols = [
|
||||
{ title: '接团数', dataIndex: 'group_count', width: '6rem',
|
||||
sorter: (a, b) => sorter(a,b, 'group_count'),
|
||||
render: (text, r) => <RenderVSDataCell data1={r.group_count} data2={r.diff?.group_count} showDiffData={searchValuesToSub.DateDiff1} />,
|
||||
},
|
||||
// { title: 'feedback团数', dataIndex: 'feedbak_group', width: '6rem' },
|
||||
{ title: '东道主团数', dataIndex: 'group_count_dongdaozhu', width1: '8rem',
|
||||
sorter: (a, b) => sorter(a,b, 'group_count_dongdaozhu'),
|
||||
render: (text, r) => <RenderVSDataCell data1={r.group_count_dongdaozhu} data2={r.diff?.group_count_dongdaozhu} showDiffData={searchValuesToSub.DateDiff1} />,
|
||||
},
|
||||
{ title: '东道主个数', dataIndex: 'case_count_dongdaozhu', width1: '8rem',
|
||||
sorter: (a, b) => sorter(a,b, 'case_count_dongdaozhu'),
|
||||
render: (text, r) => <RenderVSDataCell data1={r.case_count_dongdaozhu} data2={r.diff?.case_count_dongdaozhu} showDiffData={searchValuesToSub.DateDiff1} />,
|
||||
},
|
||||
{ title: '东道主实施比例', dataIndex: 'dongdaozhu_rate',
|
||||
render: (text, r) => <RenderVSDataCell data1={parseFloat(r.dongdaozhu_rate)} data2={parseFloat(r.diff?.dongdaozhu_rate)} dataSuffix='%' showDiffData={searchValuesToSub.DateDiff1} />,
|
||||
},
|
||||
{
|
||||
title: '各类型个数',
|
||||
children: [
|
||||
{ title: 'Live There', dataIndex: 'live_there_count',
|
||||
sorter: (a, b) => sorter(a, b, 'live_there_count'),
|
||||
render: (text, r) => <RenderVSDataCell data1={r.live_there_count} data2={r.diff?.live_there_count} showDiffData={searchValuesToSub.DateDiff1} />,
|
||||
},
|
||||
{ title: '动机圆梦', dataIndex: 'dream_fulfillment_count',
|
||||
sorter: (a, b) => sorter(a, b, 'dream_fulfillment_count'),
|
||||
render: (text, r) => <RenderVSDataCell data1={r.dream_fulfillment_count} data2={r.diff?.dream_fulfillment_count} showDiffData={searchValuesToSub.DateDiff1} />,
|
||||
},
|
||||
{ title: '仪式感创造', dataIndex: 'ceremony_creation_count',
|
||||
sorter: (a, b) => sorter(a, b, 'ceremony_creation_count'),
|
||||
render: (text, r) => <RenderVSDataCell data1={r.ceremony_creation_count} data2={r.diff?.ceremony_creation_count} showDiffData={searchValuesToSub.DateDiff1} />,
|
||||
},
|
||||
{ title: '遗憾弥补', dataIndex: 'regret_compensation_count',
|
||||
sorter: (a, b) => sorter(a, b, 'regret_compensation_count'),
|
||||
render: (text, r) => <RenderVSDataCell data1={r.regret_compensation_count} data2={r.diff?.regret_compensation_count} showDiffData={searchValuesToSub.DateDiff1} />,
|
||||
},
|
||||
{ title: '力挽狂澜', dataIndex: 'rescue_mission_count',
|
||||
sorter: (a, b) => sorter(a, b, 'rescue_mission_count'),
|
||||
render: (text, r) => <RenderVSDataCell data1={r.rescue_mission_count} data2={r.diff?.rescue_mission_count} showDiffData={searchValuesToSub.DateDiff1} />,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
const guideSummaryCols = [{ title: '姓名', dataIndex: 'TGI_Name', width: '6rem' }, ...summaryCols];
|
||||
const featuredCaseCols = [
|
||||
{ title: '团号', dataIndex: 'GRI_No', width: '16rem' },
|
||||
{ title: '导游', dataIndex: 'chinese_name', width: '8rem' },
|
||||
{ title: '案例类型', dataIndex: 'case_name', width: '8rem' },
|
||||
{ title: '案例', dataIndex: 'case_text' },
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<Row gutter={16} className={toJS(date_picker_store.siderBroken) ? '' : 'sticky-top'}>
|
||||
<Col className="gutter-row" span={24}>
|
||||
<SearchForm
|
||||
defaultValue={{
|
||||
initialValue: {
|
||||
...toJS(date_picker_store.formValues),
|
||||
...searchValues,
|
||||
},
|
||||
shows: ['years', 'agency','DepartmentList', 'guide_lgc'],
|
||||
fieldProps: {
|
||||
DepartmentList: { show_all: false, mode: 'multiple' },
|
||||
dates: { hide_vs: true },
|
||||
years: { hide_vs: false },
|
||||
agency: { rules: [{ required: true, message: '请选择地接社' }] },
|
||||
},
|
||||
}}
|
||||
onSubmit={(_err, obj, form) => {
|
||||
setSearchValues(obj, form);
|
||||
getData(obj);
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<div className="max-w-screen-xl " style={{}}>
|
||||
<Divider orientation="right">
|
||||
<ExportDocxBtn
|
||||
subject={forExport.title}
|
||||
title={forExport.agencyName}
|
||||
sectionsData={[
|
||||
{ tableTitle: forExport.year+ '年东道主总体情况', tableColumns: summaryCols, tableData: caseSummary },
|
||||
{ tableTitle: forExport.year+ '年导游实施东道主情况', tableColumns: guideSummaryCols, tableData: caseSummaryByGuide },
|
||||
{ tableTitle: forExport.year+ '年精品案例', tableColumns: featuredCaseCols, tableData: caseFeatured },
|
||||
]}
|
||||
/>
|
||||
</Divider>
|
||||
<Typography.Title level={3}>{forExport.year}年东道主总体情况</Typography.Title>
|
||||
<Table size='small' dataSource={caseSummary} columns={summaryCols} loading={loading} pagination={false} bordered />
|
||||
<Typography.Title level={3}>{forExport.year}年导游实施东道主情况</Typography.Title>
|
||||
<Table size='small' dataSource={caseSummaryByGuide} columns={guideSummaryCols} loading={loading} pagination={false} bordered rowKey={'TGI_SN'} />
|
||||
<Typography.Title level={3}>{forExport.year}年精品案例</Typography.Title>
|
||||
<Table size='small' dataSource={caseFeatured} columns={featuredCaseCols} loading={loading} pagination={false} bordered rowKey={'case_id'} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default observer(HostCaseReport);
|
||||
@ -0,0 +1,367 @@
|
||||
import { useContext } from 'react';
|
||||
import { Row, Col, Tabs, Table, Divider, Spin, Checkbox, Space } from 'antd';
|
||||
import { ContainerOutlined, BlockOutlined, SmileOutlined, MobileOutlined, CustomerServiceOutlined, IeOutlined } from '@ant-design/icons';
|
||||
import { Line, Pie } from '@ant-design/charts';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import * as comm from '@haina/utils-commons';
|
||||
import DateGroupRadio from '../../components/DateGroupRadio';
|
||||
import SearchForm from '../../components/search/SearchForm';
|
||||
import { TableExportBtn } from '../../components/Data';
|
||||
import { RenderVSDataCell } from './../../components/Data';
|
||||
|
||||
import { observer } from 'mobx-react';
|
||||
import { toJS } from 'mobx';
|
||||
import { stores_Context } from '../../config';
|
||||
import { useShallow } from 'zustand/shallow';
|
||||
import useToBOrderStore, { orderCountDataMapper, orderCountDataFieldMapper } from '../../zustand/ToBOrder';
|
||||
|
||||
const ToBOrder = observer(() => {
|
||||
const { date_picker_store: searchFormStore } = useContext(stores_Context);
|
||||
|
||||
const [searchValues, setSearchValues] = useToBOrderStore(useShallow((state) => [state.searchValues, state.setSearchValues]));
|
||||
const [activeTab, setActiveTab] = useToBOrderStore(useShallow((state) => [state.activeTab, state.setActiveTab]));
|
||||
const [loading, typeLoading, onTabChange] = useToBOrderStore(useShallow((state) => [state.loading, state.typeLoading, state.onTabChange]));
|
||||
|
||||
const orderCountDataRaw = useToBOrderStore((state) => state.orderCountDataRaw);
|
||||
const [orderCountDataLines, avgLineValue] = useToBOrderStore(useShallow((state) => [state.orderCountDataLines, state.avgLineValue]));
|
||||
const [onChangeDateGroup, activeDateGroupRadio] = useToBOrderStore(useShallow((state) => [state.onChangeDateGroup, state.activeDateGroupRadio]));
|
||||
|
||||
const orderCountDataByType = useToBOrderStore((state) => state.orderCountDataByType);
|
||||
const result = orderCountDataByType[activeTab] || {};
|
||||
|
||||
const getToBOrderCount = useToBOrderStore((state) => state.getToBOrderCount);
|
||||
|
||||
const showDiff = !comm.isEmpty(searchFormStore.start_date_cp);
|
||||
|
||||
const avg_line_y = Math.round(avgLineValue);
|
||||
const lineConfig = {
|
||||
data: orderCountDataLines,
|
||||
padding: 'auto',
|
||||
xField: 'xField',
|
||||
yField: 'yField',
|
||||
seriesField: 'seriesField',
|
||||
// xAxis: {
|
||||
// type: "timeCat",
|
||||
// },
|
||||
point: {
|
||||
size: 4,
|
||||
shape: 'cicle',
|
||||
},
|
||||
annotations: [
|
||||
{
|
||||
type: 'text',
|
||||
position: ['start', avg_line_y],
|
||||
content: avg_line_y,
|
||||
offsetX: -15,
|
||||
style: {
|
||||
fill: '#F4664A',
|
||||
textBaseline: 'bottom',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'line',
|
||||
start: [-10, avg_line_y],
|
||||
end: ['max', avg_line_y],
|
||||
style: {
|
||||
stroke: '#F4664A',
|
||||
lineDash: [2, 2],
|
||||
},
|
||||
},
|
||||
],
|
||||
label: {}, // 显示标签
|
||||
legend: {
|
||||
itemValue: {
|
||||
formatter: (text, item) => {
|
||||
const items = orderCountDataLines.filter((d) => d.seriesField === item.value); // 按站点筛选
|
||||
return items.length ? items.reduce((a, b) => a + b.yField, 0) : ''; // 计算总数
|
||||
},
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
customItems: (originalItems) => {
|
||||
// process originalItems,
|
||||
return originalItems.map((ele) => ({ ...ele, name: ele.data?.seriesField || ele.data?.xField }));
|
||||
},
|
||||
title: (title, datum) => {
|
||||
let ret = title;
|
||||
switch (activeDateGroupRadio) {
|
||||
case 'day':
|
||||
ret = `${title} ${comm.getWeek(datum.xField)}`; // 显示周几
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
},
|
||||
// smooth: true,
|
||||
};
|
||||
const pieConfig = {
|
||||
appendPadding: 10,
|
||||
data: [],
|
||||
angleField: 'OrderCount',
|
||||
colorField: 'OrderType',
|
||||
radius: 0.8,
|
||||
label: {
|
||||
type: 'outer',
|
||||
content: '{name} {value} \t {percentage}',
|
||||
},
|
||||
legend: false, // 不显示图例
|
||||
interactions: [
|
||||
{
|
||||
type: 'element-selected',
|
||||
},
|
||||
{
|
||||
type: 'element-active',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const tableProps = {
|
||||
dataSource: result?.ordercount1 || [], // table_data.dataSource,
|
||||
columns: [
|
||||
{
|
||||
title: '#',
|
||||
fixed: 'left',
|
||||
children: [
|
||||
{
|
||||
title: (
|
||||
<span>
|
||||
<div>{result.ordercountTotal1?.groups}</div>
|
||||
{showDiff ? <div>{result.ordercountTotal2?.groups}</div> : null}
|
||||
</span>
|
||||
),
|
||||
titleX: `${result.ordercountTotal1?.groups}` + (showDiff ? ` vs ${result.ordercountTotal2?.groups}` : ''),
|
||||
dataIndex: 'OrderType',
|
||||
fixed: 'left',
|
||||
render: (text, record) => <NavLink to={`/tob_orders_sub/${activeTab}/${record.OrderTypeSN}/${encodeURIComponent(record.OrderType)}`}>{text}</NavLink>,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '数量',
|
||||
children: [
|
||||
{
|
||||
title: (
|
||||
<RenderVSDataCell
|
||||
showDiffData={showDiff}
|
||||
data1={result.ordercountTotal1?.OrderCount}
|
||||
data2={result.ordercountTotal2?.OrderCount}
|
||||
diffPercent={result.ordercountTotal1?.OrderCount_vs}
|
||||
diffData={result.ordercountTotal1?.OrderCount_diff}
|
||||
/>
|
||||
),
|
||||
titleX: [result.ordercountTotal1?.OrderCount, result.ordercountTotal2?.OrderCount].join(' vs '),
|
||||
dataIndex: 'OrderCount',
|
||||
render: (text, r) => <RenderVSDataCell showDiffData={showDiff} data1={text} data2={r.diff?.OrderCount} diffPercent={r.OrderCount_vs} diffData={r.OrderCount_diff} />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '成交数',
|
||||
children: [
|
||||
{
|
||||
title: (
|
||||
<RenderVSDataCell
|
||||
showDiffData={showDiff}
|
||||
data1={result.ordercountTotal1?.CJCount}
|
||||
data2={result.ordercountTotal2?.CJCount}
|
||||
diffPercent={result.ordercountTotal1?.CJCount_vs}
|
||||
diffData={result.ordercountTotal1?.CJCount_diff}
|
||||
/>
|
||||
),
|
||||
titleX: [result.ordercountTotal1?.CJCount, result.ordercountTotal2?.CJCount].join(' vs '),
|
||||
dataIndex: 'CJCount',
|
||||
render: (text, r) => <RenderVSDataCell showDiffData={showDiff} data1={text} data2={r.diff?.CJCount} diffPercent={r.CJCount_vs} diffData={r.CJCount_diff} />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '成交人数',
|
||||
children: [
|
||||
{
|
||||
title: (
|
||||
<RenderVSDataCell
|
||||
showDiffData={showDiff}
|
||||
data1={result.ordercountTotal1?.CJPersonNum}
|
||||
data2={result.ordercountTotal2?.CJPersonNum}
|
||||
diffPercent={result.ordercountTotal1?.CJPersonNum_vs}
|
||||
diffData={result.ordercountTotal1?.CJPersonNum_diff}
|
||||
/>
|
||||
),
|
||||
titleX: [result.ordercountTotal1?.CJPersonNum, result.ordercountTotal2?.CJPersonNum].join(' vs '),
|
||||
dataIndex: 'CJPersonNum',
|
||||
render: (text, r) => <RenderVSDataCell showDiffData={showDiff} data1={text} data2={r.diff?.CJPersonNum} diffPercent={r.CJPersonNum_vs} diffData={r.CJPersonNum_diff} />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '成交率',
|
||||
children: [
|
||||
{
|
||||
title: (
|
||||
<RenderVSDataCell
|
||||
showDiffData={showDiff}
|
||||
data1={result.ordercountTotal1?.CJrate}
|
||||
data2={result.ordercountTotal2?.CJrate}
|
||||
diffPercent={result.ordercountTotal1?.CJrate_vs}
|
||||
diffData={result.ordercountTotal1?.CJrate_diff}
|
||||
/>
|
||||
),
|
||||
titleX: [result.ordercountTotal1?.CJrate, result.ordercountTotal2?.CJrate].join(' vs '),
|
||||
dataIndex: 'CJrate',
|
||||
render: (text, r) => <RenderVSDataCell showDiffData={showDiff} data1={text} data2={r.diff?.CJrate} diffPercent={r.CJrate_vs} diffData={r.CJrate_diff} />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '成交毛利(预计)',
|
||||
children: [
|
||||
{
|
||||
title: (
|
||||
<RenderVSDataCell
|
||||
showDiffData={showDiff}
|
||||
data1={result.ordercountTotal1?.YJLY}
|
||||
data2={result.ordercountTotal2?.YJLY}
|
||||
diffPercent={result.ordercountTotal1?.YJLY_vs}
|
||||
diffData={result.ordercountTotal1?.YJLY_diff}
|
||||
/>
|
||||
),
|
||||
titleX: [result.ordercountTotal1?.YJLY, result.ordercountTotal2?.YJLY].join(' vs '),
|
||||
dataIndex: 'YJLY',
|
||||
render: (text, r) => <RenderVSDataCell showDiffData={showDiff} data1={text} data2={r.diff?.YJLY} diffPercent={r.YJLY_vs} diffData={r.YJLY_diff} />,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
title: '单个订单价值',
|
||||
children: [
|
||||
{
|
||||
title: (
|
||||
<RenderVSDataCell
|
||||
showDiffData={showDiff}
|
||||
data1={result.ordercountTotal1?.Ordervalue}
|
||||
data2={result.ordercountTotal2?.Ordervalue}
|
||||
diffPercent={result.ordercountTotal1?.Ordervalue_vs}
|
||||
diffData={result.ordercountTotal1?.Ordervalue_diff}
|
||||
/>
|
||||
),
|
||||
titleX: [result.ordercountTotal1?.Ordervalue, result.ordercountTotal2?.Ordervalue].join(' vs '),
|
||||
dataIndex: 'Ordervalue',
|
||||
render: (text, r) => <RenderVSDataCell showDiffData={showDiff} data1={text} data2={r.diff?.Ordervalue} diffPercent={r.Ordervalue_vs} diffData={r.Ordervalue_diff} />,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
size: 'small',
|
||||
pagination: false,
|
||||
scroll: { x: 100 * 7 },
|
||||
loading,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<Row gutter={16} className={toJS(searchFormStore.siderBroken) ? '' : 'sticky-top'}>
|
||||
<Col className="gutter-row" span={24}>
|
||||
<SearchForm
|
||||
defaultValue={{
|
||||
initialValue: {
|
||||
...toJS(searchFormStore.formValues),
|
||||
...searchValues,
|
||||
},
|
||||
//
|
||||
shows: ['DateType', 'WebCode', 'IncludeTickets', 'DepartmentList', 'dates'],
|
||||
fieldProps: {
|
||||
DepartmentList: { show_all: false, mode: 'multiple' },
|
||||
WebCode: { show_all: false, mode: 'multiple' },
|
||||
years: { hide_vs: true },
|
||||
},
|
||||
}}
|
||||
onSubmit={(_err, obj, form, str) => {
|
||||
setSearchValues(obj, form);
|
||||
getToBOrderCount(obj);
|
||||
onTabChange(activeTab);
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={[16, { sm: 16, lg: 32 }]}>
|
||||
<Col span={24} style={{ textAlign: 'right' }}>
|
||||
<DateGroupRadio
|
||||
visible={orderCountDataLines.length !== 0}
|
||||
dataRaw={orderCountDataRaw}
|
||||
onChange={onChangeDateGroup}
|
||||
value={activeDateGroupRadio}
|
||||
dataMapper={orderCountDataMapper}
|
||||
fieldMapper={orderCountDataFieldMapper}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Spin spinning={loading}>
|
||||
<Line {...lineConfig} />
|
||||
</Spin>
|
||||
</Col>
|
||||
|
||||
<Col span={24}>
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={(active_key) => onTabChange(active_key)}
|
||||
items={[
|
||||
{
|
||||
key: 'customer_types',
|
||||
label: (
|
||||
<span>
|
||||
<CustomerServiceOutlined />
|
||||
分销客户
|
||||
</span>
|
||||
),
|
||||
},
|
||||
].map((ele) => {
|
||||
return {
|
||||
...ele,
|
||||
children: (
|
||||
<>
|
||||
<Table sticky key={`table_to_xlsx_${ele.key}`} {...tableProps} loading={typeLoading} />
|
||||
<Divider orientation="right" plain>
|
||||
<TableExportBtn label={ele.key} {...{ columns: tableProps.columns, dataSource: tableProps.dataSource }} />
|
||||
</Divider>
|
||||
</>
|
||||
),
|
||||
};
|
||||
})}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<div>
|
||||
<h3>各项占比</h3>
|
||||
{/* <Checkbox
|
||||
checked={true}
|
||||
// onChange={(e) => setIsShowEmpty(e.target.checked)}
|
||||
>
|
||||
包含空值
|
||||
</Checkbox> */}
|
||||
</div>
|
||||
<Spin spinning={typeLoading}>
|
||||
<Row>
|
||||
<Col sm={24} lg={12}>
|
||||
<Pie {...pieConfig} data={result?.ordercount1 || []} innerRadius={0.6} statistic={{ title: false, content: { content: '数量' } }} />
|
||||
<Pie {...pieConfig} data={result?.ordercount1 || []} angleField="YJLYx" innerRadius={0.6} statistic={{ title: false, content: { content: '预计毛利' } }} />
|
||||
</Col>
|
||||
|
||||
{showDiff && (
|
||||
<Col sm={24} lg={12}>
|
||||
<Pie {...pieConfig} data={result?.ordercount2 || []} innerRadius={0.6} statistic={{ title: false, content: { content: '数量' } }} />
|
||||
<Pie {...pieConfig} data={result?.ordercount2 || []} angleField="YJLYx" innerRadius={0.6} statistic={{ title: false, content: { content: '预计毛利' } }} />
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
</Spin>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
export default ToBOrder;
|
||||
@ -0,0 +1,172 @@
|
||||
import { create } from 'zustand';
|
||||
import { devtools } from 'zustand/middleware';
|
||||
import { immer } from 'zustand/middleware/immer';
|
||||
import { fetchJSON } from '@haina/utils-request';
|
||||
import { HT_HOST } from '../config';
|
||||
import { formatDate, isEmpty, sortBy } from '@haina/utils-commons';
|
||||
import { groupsMappedByKey, sitesMappedByCode, pivotBy } from './../libs/ht';
|
||||
|
||||
const defaultParams = {};
|
||||
|
||||
export const fetchRegularCustomer = async (params) => {
|
||||
const _params = {
|
||||
Website: params.WebCode || '', // CHT,AH,JH,GH,ZWQD,GH_ZWQD_HW,GHKYZG,GHKYHW,HTravel
|
||||
DEI_SNList: params.DepartmentList || '', // 1,2,28,7
|
||||
ApplydateCheck: params.DateType === 'applyDate' ? 1 : 0, //
|
||||
EntrancedateCheck: params.DateType === 'startDate' ? 1 : 0, //
|
||||
ConfirmDateCheck: params.DateType === 'confirmDate' ? 1 : 0, //
|
||||
ApplydateStart: params.Date1 || '',
|
||||
ApplydateEnd: params.Date2 || '',
|
||||
EntrancedateStart: params.Date1 || '',
|
||||
EntrancedateEnd: params.Date2 || '',
|
||||
ConfirmdateStart: params.Date1 || '',
|
||||
ConfirmdateEnd: params.Date2 || '',
|
||||
IsDetail: '', //
|
||||
IncludeTickets: '', //
|
||||
...params,
|
||||
};
|
||||
const { WebCode, DepartmentList, DateType, Date1, Date2, ...readyParams } = _params;
|
||||
const [result1=[], result2=[]] = await Promise.all([
|
||||
fetchJSON(HT_HOST + '/service-tourdesign/RegularCusOrder', { ...defaultParams, ...readyParams }),
|
||||
...(params.DateDiff1 && params.IsDetail === 0
|
||||
? [
|
||||
fetchJSON(HT_HOST + '/service-tourdesign/RegularCusOrder', {
|
||||
...defaultParams,
|
||||
...readyParams,
|
||||
ApplydateStart: params.DateDiff1 || '',
|
||||
ApplydateEnd: params.DateDiff2 || '',
|
||||
EntrancedateStart: params.DateDiff1 || '',
|
||||
EntrancedateEnd: params.DateDiff2 || '',
|
||||
ConfirmdateStart: params.DateDiff1 || '',
|
||||
ConfirmdateEnd: params.DateDiff2 || '',
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
]);
|
||||
if (params.IsDetail === 1) {
|
||||
return { result1, result2 };
|
||||
}
|
||||
const ret = {};
|
||||
const result1Mapped = result1.reduce((r, v) => ({ ...r, [v.ItemName]: v }), {});
|
||||
const allKeys = [...new Set([...result1.map((e) => e.ItemName), ...result2.map((e) => e.ItemName)])];
|
||||
const result2Mapped = result2.reduce((r, v) => ({ ...r, [v.ItemName]: v }), {});
|
||||
const x = {};
|
||||
allKeys.forEach((key) => {
|
||||
x[key] = { ...(result1Mapped?.[key] || { ItemName: key }), diff: result2Mapped[key] || {} };
|
||||
});
|
||||
ret.result1 = Object.values(x);
|
||||
return { result1: ret.result1 }; // { result1, result2 };
|
||||
};
|
||||
|
||||
// 老客户: 日期对应的数据字段
|
||||
const dateTypeDataHelper = {
|
||||
applyDate: 'SumOrder',
|
||||
startDate: 'ConfirmOrder',
|
||||
confirmDate: 'ConfirmOrder',
|
||||
};
|
||||
|
||||
/**
|
||||
* 构建 老客户系列数据
|
||||
* * 用明细数据计算
|
||||
* @param {[]} details
|
||||
* @param {string} pivotByOrder
|
||||
* @param {string} pivotByDate
|
||||
* @returns
|
||||
*/
|
||||
const buildSeriesDataFromDetails = (details, pivotByOrder, pivotByDate) => {
|
||||
const dataDetail = (details || []).map((ele) => ({
|
||||
...ele,
|
||||
key: ele.COLI_ID,
|
||||
orderState: ele.OrderState,
|
||||
applyDate: formatDate(new Date(ele.COLI_ApplyDate)),
|
||||
startDate: ele.COLI_OrderStartDate,
|
||||
confirmDate: formatDate(new Date(ele.COLI_ConfirmDate)),
|
||||
}));
|
||||
const { data: IsOldData } = pivotBy(
|
||||
dataDetail.filter((ele) => ele.COLI_IsOld === '是'),
|
||||
[['COLI_IsOld'], [], pivotByDate]
|
||||
);
|
||||
const { data: isCusCommendData } = pivotBy(
|
||||
dataDetail.filter((ele) => ele.COLI_IsCusCommend === '是'),
|
||||
[['COLI_IsCusCommend'], [], pivotByDate]
|
||||
);
|
||||
// console.log('IsOldData====', IsOldData, '\nisCusCommend', isCusCommendData);
|
||||
// 合并成两个系列
|
||||
const seriesData = []
|
||||
.concat(
|
||||
IsOldData.map((ele) => ({ ...ele, _ylabel: '老客户' })),
|
||||
isCusCommendData.map((ele) => ({ ...ele, _ylabel: '老客户推荐' }))
|
||||
)
|
||||
.sort(sortBy(pivotByDate));
|
||||
return seriesData;
|
||||
};
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------------------------------------
|
||||
*/
|
||||
const initialState = {
|
||||
loading: false,
|
||||
loading2: false,
|
||||
searchValues: {
|
||||
DepartmentList: ['1', '2', '28', '7', '33'].map((kk) => groupsMappedByKey[kk]),
|
||||
WebCode: ['CHT', 'AH', 'JH', 'GH', 'ZWQD', 'GH_ZWQD_HW', 'GHKYZG', 'GHKYHW', 'HTravel'].map((kk) => sitesMappedByCode[kk]),
|
||||
DateType: { key: 'applyDate', label: '提交日期' },
|
||||
IncludeTickets: { key: '0', label: '不含门票' },
|
||||
},
|
||||
searchValuesToSub: {},
|
||||
|
||||
regular: { data: [], details: [], total_data_tips: '', pivotData: [], pivotY: 'SumOrder', pivotX: '' },
|
||||
};
|
||||
|
||||
const useCustomerRelationsStore = create(
|
||||
devtools(
|
||||
immer((set, get) => ({
|
||||
...initialState,
|
||||
reset: () => set(initialState),
|
||||
|
||||
setLoading: (loading) => set({ loading }),
|
||||
setLoading2: (loading2) => set({ loading2 }),
|
||||
setSearchValues: (obj, values) => set((state) => ({ searchValues: values, searchValuesToSub: obj })),
|
||||
setSearchValuesToSub: (values) => set((state) => ({ searchValuesToSub: values })),
|
||||
|
||||
// 获取数据 ---------------------------------------------------------------------------------------------------
|
||||
|
||||
// 老客户
|
||||
getRegularCustomer: async (params) => {
|
||||
const { setLoading, setLoading2 } = get();
|
||||
const { IsDetail } = params;
|
||||
setLoading(true);
|
||||
setLoading2(IsDetail === 1);
|
||||
const pivotByOrder = dateTypeDataHelper[params.DateType];
|
||||
const pivotByDate = params.DateType;
|
||||
try {
|
||||
const {result1, result2} = await fetchRegularCustomer(params);
|
||||
set((state) => {
|
||||
if (IsDetail === 1) {
|
||||
state.regular.details = result1;
|
||||
|
||||
const dump_l = (result1 || []).filter((ele) => ele.COLI_IsOld !== '' && ele.COLI_IsCusCommend !== '').length;
|
||||
state.regular.total_data_tips = dump_l > 0 ? `包含 ${dump_l} 条同时勾选的数据` : '';
|
||||
/** 使用明细数据画图 */
|
||||
const seriesData = buildSeriesDataFromDetails(result1, pivotByOrder, pivotByDate);
|
||||
state.regular.pivotData = seriesData;
|
||||
state.regular.pivotX = pivotByDate;
|
||||
state.regular.pivotY = pivotByOrder;
|
||||
} else {
|
||||
console.log('0000');
|
||||
state.regular.data = result1;
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
IsDetail === 1 && setLoading2(false);
|
||||
}
|
||||
},
|
||||
})),
|
||||
{ name: 'CustomerRelations' }
|
||||
)
|
||||
);
|
||||
|
||||
export default useCustomerRelationsStore;
|
||||
@ -0,0 +1,132 @@
|
||||
import { create } from 'zustand';
|
||||
import { devtools } from 'zustand/middleware';
|
||||
import { immer } from 'zustand/middleware/immer';
|
||||
import { fetchJSON } from '@haina/utils-request';
|
||||
import { HT_HOST } from '../config';
|
||||
import { isEmpty } from '@haina/utils-commons';
|
||||
|
||||
const defaultParams = {};
|
||||
|
||||
export const fetchAgentGroupCount = async (params) => {
|
||||
const { errcode, errmsg, ...result } = await fetchJSON(HT_HOST + '/service-web/QueryData/GetAgentGroupInfoALL', {
|
||||
...defaultParams,
|
||||
// ...params,
|
||||
DateType: params.DateType,
|
||||
VEI_SN: params.agency || '',
|
||||
DepList: params.DepartmentList || '',
|
||||
Country: params.countryArea || '',
|
||||
Date1: params.Date1,
|
||||
Date2: params.Date2,
|
||||
OldDate1: params.DateDiff1 || '',
|
||||
OldDate2: params.DateDiff2 || '',
|
||||
});
|
||||
if (errcode !== 0) {
|
||||
return {};
|
||||
}
|
||||
const { result1, result2, total1, total2 } = result;
|
||||
const result1Mapped = result1.reduce((r, v) => ({ ...r, [v.EOI_ObjSN]: v }), {});
|
||||
const ret = {};
|
||||
if (isEmpty(params.DateDiff1)) {
|
||||
ret.result1 = Object.values(result1Mapped).filter((row) => row.EOI_ObjSN !== -1);
|
||||
ret.total1 = { ...result1Mapped['-1'] };
|
||||
return ret;
|
||||
} else {
|
||||
const allKeys = [...new Set([...result1.map((e) => e.EOI_ObjSN), ...result2.map((e) => e.EOI_ObjSN)])];
|
||||
const result2Mapped = result2.reduce((r, v) => ({ ...r, [v.EOI_ObjSN]: v }), {});
|
||||
|
||||
const x = {};
|
||||
allKeys.forEach((key) => {
|
||||
x[key] = { ...(result1Mapped?.[key] || { EOI_ObjSN: key, vi: key, key, VendorName: result2Mapped[key]?.VendorName || '--' }), diff: result2Mapped[key] || {} };
|
||||
});
|
||||
ret.result1 = Object.values(x).filter((row) => row.EOI_ObjSN !== -1);
|
||||
ret.total1 = { ...result1Mapped['-1'], diff: result2Mapped['-1'] || {} };
|
||||
return ret;
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchGroupListByAgentId = async (params) => {
|
||||
const { agentId, DateType, Date1, Date2, DateDiff1, DateDiff2, DepartmentList, countryArea } = params;
|
||||
const { errcode, errmsg, ...result } = await fetchJSON(HT_HOST + '/service-web/QueryData/GetAgentGroupInfo', {
|
||||
VEI_SN: agentId || '',
|
||||
DateType: DateType || '',
|
||||
Date1: Date1 || '',
|
||||
Date2: Date2 || '',
|
||||
OldDate1: DateDiff1 || '',
|
||||
OldDate2: DateDiff2 || '',
|
||||
DepList: DepartmentList || '',
|
||||
});
|
||||
if (errcode !== 0) {
|
||||
return {};
|
||||
}
|
||||
result.title1 = Date1 ? `${Date1}~${Date2}` : '';
|
||||
result.title2 = DateDiff1 ? `${DateDiff1}~${DateDiff2}` : '';
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------------------------------------
|
||||
*/
|
||||
const initialState = {
|
||||
loading: false,
|
||||
typeLoading: false,
|
||||
searchValues: {
|
||||
DateType: { key: 'departureDate', label: '抵达日期' },
|
||||
WebCode: { key: 'all', label: '所有来源' },
|
||||
IncludeTickets: { key: '1', label: '含门票' },
|
||||
DepartmentList: { key: 'All', label: '所有来源' },
|
||||
countryArea: { key: 'china', label: '国内' },
|
||||
},
|
||||
searchValuesToSub: {
|
||||
DateType: 'departureDate',
|
||||
WebCode: 'all',
|
||||
IncludeTickets: '1',
|
||||
DepartmentList: 'All',
|
||||
},
|
||||
|
||||
agentCountList: [],
|
||||
agentCountTotal: {},
|
||||
|
||||
agencyName: '',
|
||||
agencyGroups: { total1: {}, result1: [], total2: {}, result2: [] },
|
||||
|
||||
};
|
||||
|
||||
const useCustomerServicesStore = create(
|
||||
devtools(
|
||||
immer((set, get) => ({
|
||||
...initialState,
|
||||
reset: () => set(initialState),
|
||||
|
||||
setLoading: (loading) => set({ loading }),
|
||||
setSearchValues: (obj, values) => set((state) => ({ searchValues: values, searchValuesToSub: obj })),
|
||||
setSearchValuesToSub: (values) => set((state) => ({ searchValuesToSub: values })),
|
||||
|
||||
getAgentGroupCount: async (params) => {
|
||||
const { setLoading } = get();
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetchAgentGroupCount(params);
|
||||
set({ agentCountList: res.result1 || [], agentCountTotal: res.total1 || {} });
|
||||
} catch (error) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
|
||||
getGroupListByAgentId: async (agentId) => {
|
||||
const { setLoading, searchValuesToSub } = get();
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetchGroupListByAgentId({ ...searchValuesToSub, agentId });
|
||||
set({ agencyGroups: res, agencyName: res.result1?.[0]?.VendorName || '' });
|
||||
} catch (error) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
})),
|
||||
{ name: 'CustomerServices' }
|
||||
)
|
||||
);
|
||||
|
||||
export default useCustomerServicesStore;
|
||||
@ -0,0 +1,141 @@
|
||||
import { create } from 'zustand';
|
||||
import { devtools } from 'zustand/middleware';
|
||||
import { immer } from 'zustand/middleware/immer';
|
||||
import { fetchJSON } from '@haina/utils-request';
|
||||
import { HT_HOST } from '../config';
|
||||
import moment from 'moment';
|
||||
|
||||
/**
|
||||
* Transforms an array of row data into an array of objects,
|
||||
* using a separate array of column names as keys.
|
||||
*
|
||||
* @param {string[]} cols - The array of column names (keys).
|
||||
* @param {any[][]} rows - The array of row data (values).
|
||||
* @returns {Object[]} The transformed array of objects.
|
||||
*/
|
||||
export const transformRows = (cols, rows) => {
|
||||
return rows.map((row) => {
|
||||
return row.reduce((acc, val, i) => {
|
||||
acc[cols[i].display_name] = val;
|
||||
return acc;
|
||||
}, {});
|
||||
});
|
||||
};
|
||||
|
||||
export const transSearchParams = (params) => {
|
||||
const newsearchParams = {
|
||||
...params,
|
||||
vei_sn: params.agency,
|
||||
DepartmentList: params.DepartmentList === 'ALL' ? undefined : params.DepartmentList,
|
||||
};
|
||||
const oldseachPararms = {
|
||||
...params,
|
||||
Date1: params.DateDiff1,
|
||||
Date2: params.DateDiff2,
|
||||
vei_sn: params.agency,
|
||||
DepartmentList: params.DepartmentList === 'ALL' ? undefined : params.DepartmentList,
|
||||
};
|
||||
return {
|
||||
newsearchParams,
|
||||
oldseachPararms,
|
||||
};
|
||||
};
|
||||
export const fetchCaseSummary = async (params) => {
|
||||
const {newsearchParams, oldseachPararms} = transSearchParams(params);
|
||||
const { errcode, result } = await fetchJSON(`${HT_HOST}/service-Analyse2/dong_dao_zhu_total`, newsearchParams);
|
||||
let response2;
|
||||
if(oldseachPararms.DateDiff1){
|
||||
response2 = await fetchJSON(`${HT_HOST}/service-Analyse2/dong_dao_zhu_total`, oldseachPararms);
|
||||
}else{
|
||||
response2 = { errcode: 0, result: [] };
|
||||
}
|
||||
const caseSummary = result.map((item, index) => ({
|
||||
...item,
|
||||
diff: response2.errcode !== 0 ? {} : (response2.result[index] || {dongdaozhu_rate: "0%"})
|
||||
}));
|
||||
|
||||
return errcode !== 0 ? [] : (caseSummary || []);
|
||||
};
|
||||
|
||||
export const fetchCaseSummaryByGuide = async (params) => {
|
||||
const {newsearchParams, oldseachPararms} = transSearchParams(params);
|
||||
const { errcode, result } = await fetchJSON(`${HT_HOST}/service-Analyse2/dong_dao_zhu_tour_guide`, newsearchParams);
|
||||
let response2;
|
||||
if(oldseachPararms.DateDiff1){
|
||||
response2 = await fetchJSON(`${HT_HOST}/service-Analyse2/dong_dao_zhu_tour_guide`, oldseachPararms);
|
||||
}else{
|
||||
response2 = { errcode: 0, result: [] };
|
||||
}
|
||||
const caseSummaryByGuide = result.map((item, index) => ({
|
||||
...item,
|
||||
diff: response2.errcode !== 0 ? {} : (response2.result.find(r => r.TGI_SN === item.TGI_SN) || {dongdaozhu_rate: "0%"})
|
||||
}));
|
||||
return errcode !== 0 ? [] : (caseSummaryByGuide || []); // .sort(sortDescBy('case_count_dongdaozhu'));
|
||||
};
|
||||
|
||||
export const fetchCaseFeatured = async (params) => {
|
||||
const {newsearchParams, oldseachPararms} = transSearchParams(params);
|
||||
const { errcode, result } = await fetchJSON(`${HT_HOST}/service-Analyse2/dong_dao_zhu_case`, newsearchParams);
|
||||
return errcode !== 0 ? [] : (result || []);
|
||||
};
|
||||
|
||||
/**
|
||||
* 东道主报告----------------------------------------------------------------------------------------------
|
||||
*/
|
||||
const initialState = {
|
||||
loading: false,
|
||||
loadingCase: false,
|
||||
caseSummary: [],
|
||||
caseSummaryByGuide: [],
|
||||
caseFeatured: [],
|
||||
|
||||
forExport: {
|
||||
agencyName: '',
|
||||
title: '',
|
||||
year: '',
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const useHostCaseStore = create(
|
||||
devtools(
|
||||
immer((set, get) => ({
|
||||
...initialState,
|
||||
searchValues: {},
|
||||
searchValuesToSub: {},
|
||||
reset: () => set(initialState),
|
||||
|
||||
setLoading: (loading) => set({ loading }),
|
||||
setSearchValues: (obj, values) => set((state) => ({ searchValues: values, searchValuesToSub: obj })),
|
||||
setSearchValuesToSub: (values) => set((state) => ({ searchValuesToSub: values })),
|
||||
|
||||
setLoadingCase: (loadingCase) => set({ loadingCase }),
|
||||
setCaseFeatured: (caseFeatured) => set({ caseFeatured }),
|
||||
setCaseSummary: (caseSummary) => set({ caseSummary }),
|
||||
setCaseSummaryByGuide: (caseSummaryByGuide) => set({ caseSummaryByGuide }),
|
||||
|
||||
async getCaseReport(params) {
|
||||
const { setLoading, searchValues } = get();
|
||||
setLoading(true);
|
||||
const [summary, guideSummary, featured] = await Promise.all([
|
||||
fetchCaseSummary(params),
|
||||
fetchCaseSummaryByGuide(params),
|
||||
fetchCaseFeatured(params)
|
||||
]);
|
||||
set({
|
||||
caseSummary: summary,
|
||||
caseSummaryByGuide: guideSummary,
|
||||
caseFeatured: featured,
|
||||
forExport: {
|
||||
title: `${moment(params.Date1).format('YYYY年')}总结`,
|
||||
agencyName: searchValues.agency.label,
|
||||
year: moment(params.Date1).format('YYYY'),
|
||||
},
|
||||
});
|
||||
setLoading(false);
|
||||
},
|
||||
})),
|
||||
{ name: 'hostCaseReportStore' }
|
||||
)
|
||||
);
|
||||
export default useHostCaseStore;
|
||||
@ -0,0 +1,122 @@
|
||||
import { create } from 'zustand';
|
||||
import { devtools } from 'zustand/middleware';
|
||||
import { immer } from 'zustand/middleware/immer';
|
||||
import { groupsMappedByCode } from '../libs/ht';
|
||||
import { fetchJSON } from '@haina/utils-request';
|
||||
import { HT_HOST } from '../config';
|
||||
import { groupBy, isEmpty, } from '@haina/utils-commons';
|
||||
|
||||
/**
|
||||
* 顾问业绩 (例会数据)
|
||||
*/
|
||||
|
||||
const defaultParams = { OrderType: 227001, IsDYTJ: '', IncludeTickets: 1, Team: '', WebCodeFX: '', CusType: '', OldCus: 0, lineClass: '', IsDetail: -1 };
|
||||
|
||||
export const fetchMeetingDataSales = async (params) => {
|
||||
const { errcode, errmsg, result } = await fetchJSON(HT_HOST + '/service-web/QueryData/WLCountForMeetingNew', {
|
||||
...defaultParams,
|
||||
...params,
|
||||
WebCode: (params.WebCode || '').replace('all', ''),
|
||||
OPI_SN: params.operator || '',
|
||||
});
|
||||
const ret =
|
||||
errcode !== 0
|
||||
? []
|
||||
: (result || [])
|
||||
// .filter((ele) =>
|
||||
// Object.keys(ele)
|
||||
// .filter((col) => !['OPI_SN', 'OPI_Name', 'COLI_LineClass', 'LineClass', 'vi'].includes(col))
|
||||
// .some((col) => !isEmpty(ele[col])),
|
||||
// )
|
||||
.map((ele) => ({ ...ele, key: `${ele.OPI_SN}_${ele.COLI_LineClass || ''}_${ele.vi || ''}` }));
|
||||
const byOPI = groupBy(structuredClone(ret), 'OPI_SN');
|
||||
const OPIValue = Object.keys(byOPI).reduce((r, opisn) => {
|
||||
byOPI[opisn].forEach((ele, xi) => {
|
||||
ele.rowSpan = xi === 0 ? byOPI[opisn].length : 0;
|
||||
});
|
||||
return [...r, ...byOPI[opisn]];
|
||||
}, []);
|
||||
const byLineClass = groupBy(structuredClone(ret), 'LineClass');
|
||||
const LineClassValue = Object.keys(byLineClass).reduce((r, gkey) => {
|
||||
byLineClass[gkey].forEach((ele, xi) => {
|
||||
ele.rowSpan = xi === 0 ? byLineClass[gkey].length : 0;
|
||||
});
|
||||
return [...r, ...byLineClass[gkey]];
|
||||
}, []);
|
||||
return { result: ret, OPIValue, LineClassValue };
|
||||
};
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------------------------------------
|
||||
*/
|
||||
const initialState = {
|
||||
loading: false,
|
||||
typeLoading: false,
|
||||
searchValues: {
|
||||
// DateType: { key: 'applyDate', label: '提交日期' },
|
||||
WebCode: { key: 'all', label: '所有来源' },
|
||||
IncludeTickets: { key: '1', label: '含门票' },
|
||||
DepartmentList: groupsMappedByCode.GH, // { key: 'All', label: '所有来源' }, //
|
||||
},
|
||||
searchValuesToSub: {
|
||||
// DateType: 'applyDate',
|
||||
WebCode: 'all',
|
||||
IncludeTickets: '1',
|
||||
DepartmentList: -1, // -1: All
|
||||
},
|
||||
|
||||
salesDataTotal: [],
|
||||
salesData: [],
|
||||
matrixData: {},
|
||||
matrixtableMajorKey: 'opi',
|
||||
matrixTableData: [],
|
||||
|
||||
// 二级页面
|
||||
};
|
||||
|
||||
const useSalesInsightStore = create(
|
||||
devtools(
|
||||
immer((set, get) => ({
|
||||
...initialState,
|
||||
reset: () => set(initialState),
|
||||
|
||||
setLoading: (loading) => set({ loading }),
|
||||
setTypeLoading: (typeLoading) => set({ typeLoading }),
|
||||
|
||||
setSearchValues: (obj, values) => set((state) => ({ searchValues: values, searchValuesToSub: obj })),
|
||||
setSearchValuesToSub: (values) => set((state) => ({ searchValuesToSub: values })),
|
||||
setActiveTab: (tab) => set({ activeTab: tab }),
|
||||
|
||||
|
||||
// site effects
|
||||
getMeetingDataSales: async (params) => {
|
||||
const { setTypeLoading, } = get();
|
||||
setTypeLoading(true);
|
||||
try {
|
||||
const res = await fetchMeetingDataSales(params);
|
||||
if (params.IsDetail === 1) {
|
||||
set({ matrixData: res, matrixTableData: res.OPIValue, matrixtableMajorKey: 'opi', salesData: res.result });
|
||||
}
|
||||
else {
|
||||
set({ salesDataTotal: res.result });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setTypeLoading(false);
|
||||
}
|
||||
},
|
||||
|
||||
onMatrixChange: () => {
|
||||
const { matrixtableMajorKey, matrixData } = get();
|
||||
const newKey = matrixtableMajorKey === 'opi' ? 'lineclass' : 'opi';
|
||||
const dataKey = matrixtableMajorKey === 'opi' ? 'LineClassValue' : 'OPIValue' ;
|
||||
set({ matrixtableMajorKey: newKey, matrixTableData: matrixData?.[dataKey] });
|
||||
},
|
||||
|
||||
// sub
|
||||
})),
|
||||
{ name: 'SalesInsight' }
|
||||
)
|
||||
);
|
||||
export default useSalesInsightStore;
|
||||
@ -0,0 +1,241 @@
|
||||
import { create } from 'zustand';
|
||||
import { devtools } from 'zustand/middleware';
|
||||
import { immer } from 'zustand/middleware/immer';
|
||||
import { groupsMappedByCode } from '../libs/ht';
|
||||
import { fetchJSON } from '@haina/utils-request';
|
||||
import { HT_HOST } from '../config';
|
||||
import { resultDataCb } from '../components/DateGroupRadio/date';
|
||||
import { isEmpty, price_to_number } from '@haina/utils-commons';
|
||||
|
||||
/**
|
||||
* 分销(ToB)订单
|
||||
*/
|
||||
|
||||
const defaultParams = {};
|
||||
|
||||
export const fetchToBOrderCount = async (params, type = '', typeVal = '') => {
|
||||
const { errcode, errmsg, ...result } = await fetchJSON(HT_HOST + '/service-web/QueryData/GetOrderCount_FX', {
|
||||
...defaultParams,
|
||||
...params,
|
||||
COLI_ApplyDate1: params.Date1,
|
||||
COLI_ApplyDate2: params.Date2,
|
||||
COLI_ApplyDateOld1: params.DateDiff1 || '',
|
||||
COLI_ApplyDateOld2: params.DateDiff2 || '',
|
||||
OrderType: type,
|
||||
OrderType_val: typeVal,
|
||||
});
|
||||
return errcode !== 0 ? {} : (result || {});
|
||||
};
|
||||
|
||||
const _typeRes = {
|
||||
'ordercount1': [],
|
||||
'ordercount2': [],
|
||||
'ordercountTotal1': {
|
||||
'OrderType': '合计',
|
||||
'OrderCount': 0,
|
||||
'CJCount': 0,
|
||||
'CJPersonNum': 0,
|
||||
'YJLY': '',
|
||||
'CJrate': '0%',
|
||||
'Ordervalue': '',
|
||||
'groups': '',
|
||||
'key': 1,
|
||||
},
|
||||
'ordercountTotal2': {},
|
||||
};
|
||||
export const fetchToBOrderCountByType = async (type, params) => {
|
||||
const { errcode, errmsg, ...result } = await fetchJSON(HT_HOST + '/service-web/QueryData/GetOrderCountByType_FX', {
|
||||
...defaultParams,
|
||||
...params,
|
||||
COLI_ApplyDate1: params.Date1,
|
||||
COLI_ApplyDate2: params.Date2,
|
||||
COLI_ApplyDateOld1: params.DateDiff1 || '',
|
||||
COLI_ApplyDateOld2: params.DateDiff2 || '',
|
||||
OrderType: type,
|
||||
});
|
||||
const res = errcode !== 0 ? _typeRes : (result || _typeRes);
|
||||
const rows1Map = res.ordercount1.reduce((a, row1) => ({ ...a, [row1.OrderTypeSN]: {...row1, YJLYx: price_to_number(row1.YJLY)} }), {});
|
||||
const rows2Map = res.ordercount2.reduce((a, row2) => ({ ...a, [row2.OrderTypeSN]: {...row2, YJLYx: price_to_number(row2.YJLY)} }), {});
|
||||
|
||||
const mixRow1 = res.ordercount1.map((row1) => ({ ...row1, YJLYx: price_to_number(row1.YJLY), diff: rows2Map[row1.OrderTypeSN] || {} }));
|
||||
// Diff: elements in rows2 but not in rows1
|
||||
const diff = [...new Set(Object.keys(rows2Map).filter((x) => !new Set(Object.keys(rows1Map)).has(x)))];
|
||||
mixRow1.push(...diff.map((sn) => ({ diff: rows2Map[sn], OrderType: rows2Map[sn].OrderType, OrderTypeSN: rows2Map[sn].OrderTypeSN })));
|
||||
|
||||
return { ...res, ordercount1: mixRow1, ordercount2: res.ordercount2.map((row1) => ({ ...row1, YJLYx: price_to_number(row1.YJLY), })) };
|
||||
};
|
||||
|
||||
const _detailRes = { ordercount1: [], ordercount2: [] };
|
||||
export const fetchToBOrderDetailByType = async (params, type = '', typeVal = '', orderContent = 'detail') => {
|
||||
const { errcode, errmsg, ...result } = await fetchJSON(HT_HOST + '/service-web/QueryData/GetOrderCountByType_Sub_FX', {
|
||||
...defaultParams,
|
||||
SubOrderType: orderContent,
|
||||
...params,
|
||||
COLI_ApplyDate1: params.Date1,
|
||||
COLI_ApplyDate2: params.Date2,
|
||||
COLI_ApplyDateOld1: params.DateDiff1 || '',
|
||||
COLI_ApplyDateOld2: params.DateDiff2 || '',
|
||||
OrderType: type,
|
||||
OrderType_val: typeVal,
|
||||
});
|
||||
const res = errcode !== 0 ? _detailRes : (result || _detailRes);
|
||||
const dateStr = [params.Date1, params.Date2].map((d) => d.substring(0, 10)).join('~');
|
||||
const dateDiffStr = isEmpty(params.DateDiff1) ? '' : [params.DateDiff1, params.DateDiff2].map((d) => d.substring(0, 10)).join('~');
|
||||
const ret = [
|
||||
{ dateRangeStr: dateStr, data: res.ordercount1 },
|
||||
{ dateRangeStr: dateDiffStr, data: res.ordercount2 },
|
||||
];
|
||||
return ret;
|
||||
};
|
||||
|
||||
const calculateLineData = (value, data, avg1) => {
|
||||
const groupByDate = data.reduce((r, v) => {
|
||||
(r[v.ApplyDate] || (r[v.ApplyDate] = [])).push(v);
|
||||
return r;
|
||||
}, {});
|
||||
const _data = Object.keys(groupByDate)
|
||||
.reduce((r, _d) => {
|
||||
const xAxisGroup = groupByDate[_d].reduce((a, v) => {
|
||||
(a[v.groups] || (a[v.groups] = [])).push(v);
|
||||
return a;
|
||||
}, {});
|
||||
Object.keys(xAxisGroup).map((_group) => {
|
||||
const summaryVal = xAxisGroup[_group].reduce((rows, row) => rows + row.orderCount, 0);
|
||||
r.push({ ...xAxisGroup[_group][0], orderCount: summaryVal });
|
||||
return _group;
|
||||
});
|
||||
return r;
|
||||
}, [])
|
||||
.map((row) => ({ xField: row.ApplyDate, yField: row.orderCount, seriesField: row.groups }));
|
||||
return { lines: _data, dateRadioValue: value, avgLineValue: avg1 };
|
||||
};
|
||||
|
||||
export const orderCountDataMapper = { 'data1': 'ordercount1', data2: 'ordercount2' };
|
||||
export const orderCountDataFieldMapper = { 'dateKey': 'ApplyDate', 'valueKey': 'orderCount', 'seriesKey': 'id', _f: 'sum' };
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------------------------------------
|
||||
*/
|
||||
const initialState = {
|
||||
loading: false,
|
||||
typeLoading: false,
|
||||
searchValues: {
|
||||
DateType: { key: 'applyDate', label: '提交日期' },
|
||||
WebCode: { key: 'all', label: '所有来源' },
|
||||
IncludeTickets: { key: '1', label: '含门票' },
|
||||
DepartmentList: groupsMappedByCode.GH, // { key: 'All', label: '所有来源' }, //
|
||||
},
|
||||
searchValuesToSub: {
|
||||
DateType: 'applyDate',
|
||||
WebCode: 'all',
|
||||
IncludeTickets: '1',
|
||||
DepartmentList: -1, // -1: All
|
||||
},
|
||||
|
||||
activeTab: 'customer_types',
|
||||
activeDateGroupRadio: 'day',
|
||||
|
||||
orderCountDataRaw: {},
|
||||
orderCountDataLines: [],
|
||||
avgLineValue: 0,
|
||||
|
||||
orderCountDataByType: {},
|
||||
|
||||
// 二级页面
|
||||
orderCountDataRawSub: {},
|
||||
orderCountDataLinesSub: [],
|
||||
avgLineValueSub: 0,
|
||||
activeDateGroupRadioSub: 'day',
|
||||
orderDetails: [],
|
||||
};
|
||||
|
||||
const useToBOrderStore = create(
|
||||
devtools(
|
||||
immer((set, get) => ({
|
||||
...initialState,
|
||||
reset: () => set(initialState),
|
||||
|
||||
setLoading: (loading) => set({ loading }),
|
||||
setTypeLoading: (typeLoading) => set({ typeLoading }),
|
||||
|
||||
setSearchValues: (obj, values) => set((state) => ({ searchValues: values, searchValuesToSub: obj })),
|
||||
setSearchValuesToSub: (values) => set((state) => ({ searchValuesToSub: values })),
|
||||
setActiveTab: (tab) => set({ activeTab: tab }),
|
||||
|
||||
setOrderCountDataLines: (data) => set({ orderCountDataLines: data }),
|
||||
setOrderCountDataByType: (type, data) =>
|
||||
set((state) => {
|
||||
state.orderCountDataByType[type] = data;
|
||||
}),
|
||||
|
||||
// data ----
|
||||
onChangeDateGroup: (value, data, avg1) => {
|
||||
const { lines, dateRadioValue, avgLineValue } = calculateLineData(value, data, avg1);
|
||||
set({ orderCountDataLines: lines, avgLineValue, activeDateGroupRadio: dateRadioValue });
|
||||
},
|
||||
onChangeDateGroupSub: (value, data, avg1) => {
|
||||
const { lines, dateRadioValue, avgLineValue } = calculateLineData(value, data, avg1);
|
||||
set({ orderCountDataLinesSub: lines, avgLineValueSub: avgLineValue, activeDateGroupRadioSub: dateRadioValue });
|
||||
},
|
||||
|
||||
// site effects
|
||||
getToBOrderCount: async (params, type, typeVal) => {
|
||||
const { setLoading } = get();
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetchToBOrderCount(params, type, typeVal);
|
||||
// 第一次得到数据
|
||||
const { lines, dateRadioValue, avgLineValue } = resultDataCb(res, 'day', orderCountDataMapper, orderCountDataFieldMapper, calculateLineData);
|
||||
if (isEmpty(type)) {
|
||||
// index page
|
||||
set({ orderCountDataRaw: res });
|
||||
set({ orderCountDataLines: lines, avgLineValue, activeDateGroupRadio: dateRadioValue });
|
||||
} else {
|
||||
// sub page
|
||||
set({ orderCountDataRawSub: res });
|
||||
set({ orderCountDataLinesSub: lines, avgLineValueSub: avgLineValue, activeDateGroupRadioSub: dateRadioValue });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
|
||||
getToBOrderCount_type: async (type) => {
|
||||
const { setTypeLoading, searchValuesToSub, setOrderCountDataByType } = get();
|
||||
setTypeLoading(true);
|
||||
try {
|
||||
const res = await fetchToBOrderCountByType(type, searchValuesToSub);
|
||||
setOrderCountDataByType(type, res);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setTypeLoading(false);
|
||||
}
|
||||
},
|
||||
|
||||
onTabChange: async (tab) => {
|
||||
const { setActiveTab, getToBOrderCount_type } = get();
|
||||
setActiveTab(tab);
|
||||
await getToBOrderCount_type(tab);
|
||||
},
|
||||
|
||||
// sub
|
||||
getToBOrderDetailByType: async (params, type, typeVal) => {
|
||||
const { setTypeLoading } = get();
|
||||
try {
|
||||
setTypeLoading(true);
|
||||
const res = await fetchToBOrderDetailByType(params, type, typeVal, 'detail');
|
||||
set({ orderDetails: res });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setTypeLoading(false);
|
||||
}
|
||||
},
|
||||
})),
|
||||
{ name: 'ToBOrder' }
|
||||
)
|
||||
);
|
||||
export default useToBOrderStore;
|
||||
Loading…
Reference in New Issue