This commit is contained in:
ylj20011123 2026-01-26 18:34:19 +08:00
parent 83cb30fc12
commit 63f1b3dc8a
8 changed files with 329 additions and 133 deletions

BIN
dist.zip

Binary file not shown.

View File

@ -1,6 +1,6 @@
{ {
"name": "ant-design-pro", "name": "ant-design-pro",
"version": "4.5.128", "version": "4.5.131",
"private": true, "private": true,
"description": "An out-of-box UI solution for enterprise applications", "description": "An out-of-box UI solution for enterprise applications",
"scripts": { "scripts": {

View File

@ -787,7 +787,7 @@ const OrderDetailModal = ({ modalVisible, handleCloseModal, currentRow, detailTy
} }
<Col span={8} className="memberInfoDetailItem"> <Col span={8} className="memberInfoDetailItem">
<ProFormText <ProFormText
name={"OrderDesc"} name={"SALEBILL_DESC"}
label={"订单备注"} label={"订单备注"}
readonly readonly
style={{ marginBottom: '16px' }} style={{ marginBottom: '16px' }}
@ -802,6 +802,8 @@ const OrderDetailModal = ({ modalVisible, handleCloseModal, currentRow, detailTy
<div style={{ marginBottom: '16px' }}> <div style={{ marginBottom: '16px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
<span style={{ fontWeight: 'bold', fontSize: '14px' }}></span> <span style={{ fontWeight: 'bold', fontSize: '14px' }}></span>
{
Number(currentRow?.SALEBILL_STATE) >= 3000 ? '' :
<Button <Button
type="dashed" type="dashed"
icon={<PlusOutlined />} icon={<PlusOutlined />}
@ -811,6 +813,7 @@ const OrderDetailModal = ({ modalVisible, handleCloseModal, currentRow, detailTy
> >
</Button> </Button>
}
</div> </div>
{logisticsList.map((logistics, index) => ( {logisticsList.map((logistics, index) => (
<div key={logistics.id} style={{ marginBottom: '12px', padding: '12px', border: '1px solid #d9d9d9', borderRadius: '6px', backgroundColor: '#fafafa' }}> <div key={logistics.id} style={{ marginBottom: '12px', padding: '12px', border: '1px solid #d9d9d9', borderRadius: '6px', backgroundColor: '#fafafa' }}>
@ -859,6 +862,8 @@ const OrderDetailModal = ({ modalVisible, handleCloseModal, currentRow, detailTy
</div> </div>
</Col> </Col>
<Col span={6}> <Col span={6}>
{
Number(currentRow?.SALEBILL_STATE) >= 3000 ? '' :
<Button <Button
type="text" type="text"
danger danger
@ -869,6 +874,7 @@ const OrderDetailModal = ({ modalVisible, handleCloseModal, currentRow, detailTy
> >
</Button> </Button>
}
</Col> </Col>
</Row> </Row>
</div> </div>

View File

@ -137,7 +137,7 @@ const MallOrderManage: React.FC<{ currentUser: CurrentUser, isComponent?: boolea
"0": "全部", "0": "全部",
"3000": "零售商城", "3000": "零售商城",
"3001": "工会商城", "3001": "工会商城",
"3002": "品诺商城", // "3002": "品诺商城",
"3010": "积分商城" "3010": "积分商城"
}, },
initialValue: '0', initialValue: '0',
@ -315,7 +315,7 @@ const MallOrderManage: React.FC<{ currentUser: CurrentUser, isComponent?: boolea
valueEnum: { valueEnum: {
"3000": "零售商城", "3000": "零售商城",
"3001": "工会商城", "3001": "工会商城",
"3002": "品诺商城", // "3002": "品诺商城",
"3010": "积分商城" "3010": "积分商城"
}, },
// valueEnum: { // valueEnum: {
@ -424,40 +424,41 @@ const MallOrderManage: React.FC<{ currentUser: CurrentUser, isComponent?: boolea
// } // }
// } // }
// }, // },
// { {
// title: '供应商', title: '供应商',
// dataIndex: "MERCHANTS_IDS", dataIndex: "MERCHANTS_IDS",
// valueType: 'select', valueType: 'select',
// request: async () => { request: async () => {
// const req = { const req = {
// searchParameter: { searchParameter: {
// OWNERUNIT_ID: currentUser?.OwnerUnitId, OWNERUNIT_ID: currentUser?.OwnerUnitId,
// PROVINCE_CODE: currentUser?.ProvinceCode, PROVINCE_CODE: currentUser?.ProvinceCode,
// MERCHANTS_TYPE: "" MERCHANTS_TYPE: ""
// }, },
// PageIndex: 1, PageIndex: 1,
// PageSize: 999999, PageSize: 999999,
// } }
// const data = await handeGetMERCHANTSList(req); const data = await handeGetMERCHANTSList(req);
// return data.List return data.List
// }, },
// hideInTable: true, hideInTable: true,
// fieldProps: { fieldProps: {
// allowClear: true, allowClear: true,
// showSearch: true, showSearch: true,
// filterTreeNode: (input, node) => { filterTreeNode: (input, node) => {
// // ✅ 输入时根据 AUTOTYPE_NAME 模糊匹配 // ✅ 输入时根据 AUTOTYPE_NAME 模糊匹配
// return node?.MERCHANTS_NAME?.toLowerCase()?.includes(input.toLowerCase()); return node?.MERCHANTS_NAME?.toLowerCase()?.includes(input.toLowerCase());
// }, },
// treeDefaultExpandAll: true, treeDefaultExpandAll: true,
// fieldNames: { fieldNames: {
// label: 'MERCHANTS_NAME', label: 'MERCHANTS_NAME',
// value: 'MERCHANTS_ID', value: 'MERCHANTS_ID',
// }, },
// disabled: currentUser?.UserPattern === 4000 disabled: currentUser?.UserPattern === 4000
// }, },
hideInSearch: currentUser?.UserPattern === 4000,
// initialValue: currentUser?.UserPattern === 4000 ? currentUser?.SupplierID : "" // initialValue: currentUser?.UserPattern === 4000 ? currentUser?.SupplierID : ""
// }, },
{ {
title: '单位名称', title: '单位名称',
dataIndex: "COMPANY_IDS", dataIndex: "COMPANY_IDS",
@ -789,15 +790,17 @@ const MallOrderManage: React.FC<{ currentUser: CurrentUser, isComponent?: boolea
// 跟交易台账保持一致的 导出Excel // 跟交易台账保持一致的 导出Excel
const handleGetExportData = async () => { const handleGetExportData = async () => {
console.log('searchParams', searchParams);
setGetExportDataLoading(true) setGetExportDataLoading(true)
const req: any = { const req: any = {
ExportType: 1, ExportType: 1,
OwnerUnitId: "911", OwnerUnitId: "911",
CompanyId: searchParams?.CompanyId || "", CompanyId: searchParams?.COMPANY_IDS || "",
MerchantId: currentUser?.SupplierID || "", MerchantId: currentUser?.UserPattern === 4000 ? currentUser?.SupplierID : searchParams?.MERCHANTS_IDS || "",
SaleBillState: searchParams?.OrderStatus === '0' ? '' : (searchParams?.OrderStatus || ""), SaleBillState: searchParams?.orderStatus === '0' ? '' : (searchParams?.orderStatus || ""),
SaleBillType: searchParams?.OrderType === '0' ? '' : (searchParams?.OrderType || ""), SaleBillType: searchParams?.orderType === '0' ? '' : (searchParams?.orderType || ""),
ChannelType: searchParams?.PaymentMethod || "", ChannelType: searchParams?.CHANNEL_TYPE === '0' ? '' : searchParams?.CHANNEL_TYPE || "",
StartDate: searchParams?.ORDER_DATE_Start || "", StartDate: searchParams?.ORDER_DATE_Start || "",
EndDate: searchParams?.ORDER_DATE_End || "", EndDate: searchParams?.ORDER_DATE_End || "",
// SearchKeyName: "SupplierName,CommodityName,OrderCode", // SearchKeyName: "SupplierName,CommodityName,OrderCode",

View File

@ -14,7 +14,7 @@ import ProTable from "@ant-design/pro-table";
import ReactHTMLTableToExcel from "react-html-table-to-excel"; import ReactHTMLTableToExcel from "react-html-table-to-excel";
import LeftSelectTree from "@/pages/reports/settlementAccount/component/leftSelectTree"; import LeftSelectTree from "@/pages/reports/settlementAccount/component/leftSelectTree";
import PageTitleBox from "@/components/PageTitleBox"; import PageTitleBox from "@/components/PageTitleBox";
import { handeGetCOMPANYList, handeGetOnlineBillAccountList, handeGetSalebillAccountList, handeGetSupplierSaleBillList } from "../service"; import { handeGetCOMPANYList, handeGetMERCHANTSList, handeGetOnlineBillAccountList, handeGetSalebillAccountList, handeGetSupplierSaleBillList } from "../service";
import moment from 'moment' import moment from 'moment'
import OrderDetailModal from "../BookingMealOrder/components/orderDetailModal"; import OrderDetailModal from "../BookingMealOrder/components/orderDetailModal";
import { exportXlsxFromProColumnsExcelJS, formatTreeData, handleSetlogSave } from "@/utils/format"; import { exportXlsxFromProColumnsExcelJS, formatTreeData, handleSetlogSave } from "@/utils/format";
@ -78,17 +78,6 @@ const TradingLedger: React.FC<{ currentUser: CurrentUser }> = (props) => {
placeholder: "请输入供货商/购买的商品/订单编号/会员名称/电话号码" placeholder: "请输入供货商/购买的商品/订单编号/会员名称/电话号码"
} }
}, },
{
dataIndex: "CompanyId",
title: "所属单位",
hideInTable: true,
valueType: 'select',
fieldProps: {
showSearch: true,
options: companyList,
filterOption: (input: any, option: any) => (option?.label ?? '').toLowerCase().includes(input.toLowerCase()),
}
},
{ {
title: '下单时间', title: '下单时间',
dataIndex: 'search_date', dataIndex: 'search_date',
@ -114,6 +103,98 @@ const TradingLedger: React.FC<{ currentUser: CurrentUser }> = (props) => {
// initialValue: [moment().subtract(1, 'M').format('YYYY-MM-DD'), moment().format('YYYY-MM-DD')], // initialValue: [moment().subtract(1, 'M').format('YYYY-MM-DD'), moment().format('YYYY-MM-DD')],
initialValue: [moment().startOf('M'), moment()], initialValue: [moment().startOf('M'), moment()],
}, },
{
title: "订单状态",
dataIndex: "OrderStatus",
valueType: "select",
valueEnum: {
"0": "全部",
"1005": "订单待支付",
"1010": "订单待发货",
"2010": "订单已发货",
"3000": "订单已完成",
"8000": "退款申请中",
"8900": "订单已退款",
"9000": "订单已关闭",
"9999": "订单已撤销"
},
initialValue: '0',
hideInTable: true,
},
{
title: "订单类型",
dataIndex: "OrderType",
valueType: "select",
valueEnum: {
"0": "全部",
"3000": "零售商城",
"3001": "工会商城",
// "3002": "品诺商城",
"3010": "积分商城"
},
initialValue: '0',
hideInTable: true,
},
{
title: "支付方式",
dataIndex: "CHANNEL_TYPE",
valueType: "select",
valueEnum: {
"0": "全部",
"工会余额": "工会余额",
"组合支付": "组合支付",
"微信": "微信"
},
initialValue: '0',
hideInTable: true,
},
{
title: '供应商',
dataIndex: "MERCHANTS_IDS",
valueType: 'select',
request: async () => {
const req = {
searchParameter: {
OWNERUNIT_ID: currentUser?.OwnerUnitId,
PROVINCE_CODE: currentUser?.ProvinceCode,
MERCHANTS_TYPE: ""
},
PageIndex: 1,
PageSize: 999999,
}
const data = await handeGetMERCHANTSList(req);
return data.List
},
hideInTable: true,
fieldProps: {
allowClear: true,
showSearch: true,
filterTreeNode: (input, node) => {
// ✅ 输入时根据 AUTOTYPE_NAME 模糊匹配
return node?.MERCHANTS_NAME?.toLowerCase()?.includes(input.toLowerCase());
},
treeDefaultExpandAll: true,
fieldNames: {
label: 'MERCHANTS_NAME',
value: 'MERCHANTS_ID',
},
disabled: currentUser?.UserPattern === 4000
},
hideInSearch: currentUser?.UserPattern === 4000,
// initialValue: currentUser?.UserPattern === 4000 ? currentUser?.SupplierID : ""
},
{
dataIndex: "CompanyId",
title: "所属单位",
hideInTable: true,
valueType: 'select',
fieldProps: {
showSearch: true,
options: companyList,
filterOption: (input: any, option: any) => (option?.label ?? '').toLowerCase().includes(input.toLowerCase()),
}
},
{ {
title: "序号", title: "序号",
dataIndex: "index", dataIndex: "index",
@ -226,6 +307,7 @@ const TradingLedger: React.FC<{ currentUser: CurrentUser }> = (props) => {
// "3002": "品诺商城", // "3002": "品诺商城",
"3010": "积分商城" "3010": "积分商城"
}, },
hideInSearch: true,
initialValue: '0', initialValue: '0',
}, },
{ {
@ -305,6 +387,7 @@ const TradingLedger: React.FC<{ currentUser: CurrentUser }> = (props) => {
}, },
align: "center", align: "center",
initialValue: '0', initialValue: '0',
hideInSearch: true
}, },
// { // {
// dataIndex: 'desc', // dataIndex: 'desc',
@ -575,10 +658,10 @@ const TradingLedger: React.FC<{ currentUser: CurrentUser }> = (props) => {
ExportType: type, ExportType: type,
OwnerUnitId: "911", OwnerUnitId: "911",
CompanyId: searchParams?.CompanyId || "", CompanyId: searchParams?.CompanyId || "",
MerchantId: currentUser?.SupplierID || "", MerchantId: currentUser?.UserPattern === 4000 ? currentUser?.SupplierID : searchParams?.MERCHANTS_IDS || "",
SaleBillState: searchParams?.OrderStatus === '0' ? '' : (searchParams?.OrderStatus || ""), SaleBillState: searchParams?.orderStatus === '0' ? '' : (searchParams?.orderStatus || ""),
SaleBillType: searchParams?.OrderType === '0' ? '' : (searchParams?.OrderType || ""), SaleBillType: searchParams?.orderType === '0' ? '' : (searchParams?.orderType || ""),
ChannelType: searchParams?.PaymentMethod || "", ChannelType: searchParams?.CHANNEL_TYPE === '0' ? '' : searchParams?.CHANNEL_TYPE || "",
StartDate: searchParams?.ORDER_DATE_Start || "", StartDate: searchParams?.ORDER_DATE_Start || "",
EndDate: searchParams?.ORDER_DATE_End || "", EndDate: searchParams?.ORDER_DATE_End || "",
// SearchKeyName: "SupplierName,CommodityName,OrderCode", // SearchKeyName: "SupplierName,CommodityName,OrderCode",
@ -695,16 +778,17 @@ const TradingLedger: React.FC<{ currentUser: CurrentUser }> = (props) => {
// SearchParameter: { // SearchParameter: {
OwnerUnitId: "911", OwnerUnitId: "911",
CompanyId: params?.CompanyId || "", CompanyId: params?.CompanyId || "",
MerchantId: "", MerchantId: currentUser?.UserPattern === 4000 ? currentUser?.SupplierID : params?.MERCHANTS_IDS || "",
SaleBillState: params?.OrderStatus === '0' ? '' : (params?.OrderStatus || ""), SaleBillState: params?.OrderStatus === '0' ? '' : (params?.OrderStatus || ""),
SaleBillType: params?.OrderType === '0' ? '' : (params?.OrderType || ""), SaleBillType: params?.OrderType === '0' ? '' : (params?.OrderType || ""),
ChannelType: params?.PaymentMethod || "", ChannelType: params?.CHANNEL_TYPE === '0' ? '' : params?.CHANNEL_TYPE || "",
StartDate: params?.ORDER_DATE_Start || "", StartDate: params?.ORDER_DATE_Start || "",
EndDate: params?.ORDER_DATE_End || "", EndDate: params?.ORDER_DATE_End || "",
SearchKeyName: "MERCHANTS_NAME,COMMODITY_NAME,SALEBILL_CHILD_CODE,ORDER_PERSON,ORDER_PERSONTEL", SearchKeyName: "MERCHANTS_NAME,COMMODITY_NAME,SALEBILL_CHILD_CODE,ORDER_PERSON,ORDER_PERSONTEL",
SearchKeyValue: params?.searchText || "", SearchKeyValue: params?.searchText || "",
SortStr: "" SortStr: ""
} }
console.log('reqreqreq222', req);
const data = await handeGetSalebillAccountList(req) const data = await handeGetSalebillAccountList(req)
setSearchParams(params) setSearchParams(params)
setCurrentSearchText(params?.searchText || "") setCurrentSearchText(params?.searchText || "")

View File

@ -29,15 +29,33 @@ function flattenTree<T extends Record<string, any>>(
return out; return out;
} }
/** 抽取 React 节点文本 */ /** 抽取 React 节点文本(支持换行) */
function extractText(node: any): string { function extractText(node: any): string {
if (node == null || node === false) return ''; if (node == null || node === false) return '';
if (typeof node === 'string' || typeof node === 'number') return String(node); if (typeof node === 'string' || typeof node === 'number') {
const children = node?.props?.children; // Excel 内部单元格换行的唯一标准是 \n
return String(node).replace(/<br\s*\/?>/gi, '\n').replace(/\r\n/g, '\n');
}
// 支持 React 的 <br/> 节点
if (node?.type === 'br') return '\n';
const props = node?.props;
if (props) {
// 渲染 dangerouslySetInnerHTML
const html = props?.dangerouslySetInnerHTML?.__html;
if (typeof html === 'string') {
return html
.replace(/<br\s*\/?>/gi, '\n')
.replace(/<[^>]+>/g, '')
.replace(/&nbsp;/g, ' ');
}
// 递归渲染 children
const children = props?.children;
if (Array.isArray(children)) return children.map(extractText).join(''); if (Array.isArray(children)) return children.map(extractText).join('');
if (children != null) return extractText(children); if (children != null) return extractText(children);
const html = node?.props?.dangerouslySetInnerHTML?.__html; }
if (typeof html === 'string') return html.replace(/<[^>]+>/g, '');
return String(node ?? ''); return String(node ?? '');
} }
@ -143,7 +161,7 @@ function getCellValue(col: AnyCol, record: any, rowIndex: number) {
/** 估算列宽(简单) */ /** 估算列宽(简单) */
const estimateWidth = (v: any) => { const estimateWidth = (v: any) => {
const s = (v ?? '').toString(); const s = (v ?? '').toString();
const len = Array.from(s).reduce((n, ch) => n + (/[^\x00-\xff]/.test(ch) ? 2 : 1), 0); const len = Array.from(s).reduce((n: number, ch: any) => n + (/[^\x00-\xff]/.test(ch) ? 2 : 1), 0);
return Math.min(Math.max(len + 2, 8), 60); return Math.min(Math.max(len + 2, 8), 60);
}; };
@ -196,14 +214,28 @@ export async function exportXlsxFromProColumnsExcelJS(
let currentRowIndex = 1; let currentRowIndex = 1;
if (topTitle) { if (topTitle) {
const row = ws.getRow(currentRowIndex); const row = ws.getRow(currentRowIndex);
// 写入一个单元格,再合并整行 const titleContent = extractText(topTitle);
row.getCell(1).value = topTitle;
ws.mergeCells(currentRowIndex, 1, currentRowIndex, columnCount); // 重要:先执行合并,再分发数据和样式
// 居中 + 加粗 + 较大字号 ws.mergeCells(currentRowIndex, 1, currentRowIndex, columnCount || 1);
const cell = ws.getCell(currentRowIndex, 1);
cell.alignment = { horizontal: 'center', vertical: 'middle' }; // 对合并后的 Master Cell 进行赋值和核心样式设置
cell.font = { bold: true, size: 14 }; const masterCell = ws.getCell(currentRowIndex, 1);
row.height = 22; masterCell.value = titleContent;
masterCell.font = { bold: true, size: 14, name: 'Microsoft YaHei' };
// 重要:由于合并单元格会共享样式,直接对 Row 应用对齐是最稳妥的
row.alignment = { horizontal: 'center', vertical: 'middle', wrapText: true };
// 双重加固:对合并范围内的所有单元格显式设置 wrapText (解决某些版本只看 Master Cell 却失效的问题)
for (let i = 1; i <= (columnCount || 1); i++) {
row.getCell(i).alignment = { horizontal: 'center', vertical: 'middle', wrapText: true };
}
// 动态行高计算(关键:确保换行后的内容不被遮盖)
const lines = titleContent.split('\n').length;
row.height = Math.max(lines * 32, 40);
currentRowIndex += 1; currentRowIndex += 1;
} }
@ -231,17 +263,23 @@ export async function exportXlsxFromProColumnsExcelJS(
currentRowIndex += 1; currentRowIndex += 1;
} }
// 3) 多级表头(全部居中 + 加粗 // 3) 多级表头(全部居中 + 加粗 + 换行支持
const headerStartRow = currentRowIndex; const headerStartRow = currentRowIndex;
headerAOA.forEach((r, idx) => { headerAOA.forEach((r, idx) => {
const row = ws.getRow(headerStartRow + idx); const row = ws.getRow(headerStartRow + idx);
let maxRowLines = 1;
r.forEach((v, cIdx) => { r.forEach((v, cIdx) => {
const text = extractText(v);
const cell = row.getCell(cIdx + 1); const cell = row.getCell(cIdx + 1);
cell.value = v; cell.value = text;
cell.alignment = { horizontal: 'center', vertical: 'middle' }; cell.alignment = { horizontal: 'center', vertical: 'middle', wrapText: true };
cell.font = { bold: true }; cell.font = { bold: true };
const lines = text.split('\n').length;
if (lines > maxRowLines) maxRowLines = lines;
}); });
row.height = 18; row.alignment = { horizontal: 'center', vertical: 'middle', wrapText: true };
row.height = Math.max(22, maxRowLines * 20);
}); });
// 应用表头合并 // 应用表头合并
merges.forEach(m => { merges.forEach(m => {
@ -257,24 +295,35 @@ export async function exportXlsxFromProColumnsExcelJS(
for (let i = 0; i < batch.length; i++) { for (let i = 0; i < batch.length; i++) {
const rec = batch[i]; const rec = batch[i];
const row = ws.getRow(currentRowIndex + i); const row = ws.getRow(currentRowIndex + i);
let maxRowLines = 1;
leafCols.forEach((col, j) => { leafCols.forEach((col, j) => {
// 这里 rowIndex 仍然传全局行号 start + i序号列会自动正确 const val = getCellValue(col, rec, start + i);
row.getCell(j + 1).value = getCellValue(col, rec, start + i); const text = extractText(val);
const cell = row.getCell(j + 1);
cell.value = text;
cell.alignment = { vertical: 'middle', wrapText: true };
const lines = text.split('\n').length;
if (lines > maxRowLines) maxRowLines = lines;
}); });
// 适度让出主线程ExcelJS 是纯 JS通常也很稳如需进一步优化可用 setTimeout 分批 row.height = Math.max(18, maxRowLines * 16);
} }
currentRowIndex += batch.length; currentRowIndex += batch.length;
// 5) 列宽:基于表头 + 采样数据估算 // 5) 列宽与默认样式
const sampleRows = Math.min(batch.length, 200); const sampleRows = Math.min(batch.length, 200);
for (let c = 1; c <= columnCount; c++) { for (let c = 1; c <= columnCount; c++) {
const wsCol = ws.getColumn(c);
// 默认开启换行
wsCol.alignment = { vertical: 'middle', wrapText: true };
const headerMax = Math.max(...headerAOA.map(r => estimateWidth(r[c - 1]))); const headerMax = Math.max(...headerAOA.map(r => estimateWidth(r[c - 1])));
let dataMax = 8; let dataMax = 8;
for (let i = 0; i < sampleRows; i++) { for (let i = 0; i < sampleRows; i++) {
const v = leafCols[c - 1] ? getCellValue(leafCols[c - 1], batch[i], start + i) : ''; const v = leafCols[c - 1] ? getCellValue(leafCols[c - 1], batch[i], start + i) : '';
dataMax = Math.max(dataMax, estimateWidth(v)); dataMax = Math.max(dataMax, estimateWidth(v));
} }
ws.getColumn(c).width = Math.max(headerMax, dataMax); wsCol.width = Math.max(headerMax, dataMax);
} }
// 6) 冻结窗格:冻结到(标题 + 信息行 + 表头)这一行的下一行 // 6) 冻结窗格:冻结到(标题 + 信息行 + 表头)这一行的下一行
@ -290,7 +339,9 @@ export async function exportXlsxFromProColumnsExcelJS(
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement('a'); const a = document.createElement('a');
a.href = url; a.href = url;
a.download = `${filename}.xlsx`; // 清理文件名中的换行符,防止下载异常
const safeFilename = (filename || '数据导出').replace(/[\r\n]/g, ' ');
a.download = `${safeFilename}.xlsx`;
a.click(); a.click();
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
} }

View File

@ -923,15 +923,33 @@ function flattenTree<T extends Record<string, any>>(
return out; return out;
} }
/** 抽取 React 节点文本 */ /** 抽取 React 节点文本(支持换行) */
function extractText(node: any): string { function extractText(node: any): string {
if (node == null || node === false) return ''; if (node == null || node === false) return '';
if (typeof node === 'string' || typeof node === 'number') return String(node); if (typeof node === 'string' || typeof node === 'number') {
const children = node?.props?.children; // Excel 内部单元格换行的标准是 \n
return String(node).replace(/<br\s*\/?>/gi, '\n').replace(/\r\n/g, '\n');
}
// 支持 React 的 <br/> 节点
if (node?.type === 'br') return '\n';
const props = node?.props;
if (props) {
// 渲染 dangerouslySetInnerHTML
const html = props?.dangerouslySetInnerHTML?.__html;
if (typeof html === 'string') {
return html
.replace(/<br\s*\/?>/gi, '\n')
.replace(/<[^>]+>/g, '')
.replace(/&nbsp;/g, ' ');
}
// 递归渲染 children
const children = props?.children;
if (Array.isArray(children)) return children.map(extractText).join(''); if (Array.isArray(children)) return children.map(extractText).join('');
if (children != null) return extractText(children); if (children != null) return extractText(children);
const html = node?.props?.dangerouslySetInnerHTML?.__html; }
if (typeof html === 'string') return html.replace(/<[^>]+>/g, '');
return String(node ?? ''); return String(node ?? '');
} }
/** 过滤 hideInTable父级联动 */ /** 过滤 hideInTable父级联动 */
@ -1059,7 +1077,7 @@ function getCellValue(col: AnyCol, record: any, rowIndex: number) {
/** 估算列宽(简单) */ /** 估算列宽(简单) */
const estimateWidth = (v: any) => { const estimateWidth = (v: any) => {
const s = (v ?? '').toString(); const s = (v ?? '').toString();
const len = Array.from(s).reduce((n, ch) => n + (/[^\x00-\xff]/.test(ch) ? 2 : 1), 0); const len = Array.from(s).reduce((n: number, ch: any) => n + (/[^\x00-\xff]/.test(ch) ? 2 : 1), 0);
return Math.min(Math.max(len + 2, 8), 60); return Math.min(Math.max(len + 2, 8), 60);
}; };
@ -1126,12 +1144,27 @@ export async function exportXlsxFromProColumnsExcelJS(
// 顶部标题 // 顶部标题
if (topTitle) { if (topTitle) {
const row = ws.getRow(currentRowIndex); const row = ws.getRow(currentRowIndex);
row.getCell(1).value = topTitle; const titleContent = extractText(topTitle);
ws.mergeCells(currentRowIndex, 1, currentRowIndex, columnCount);
const cell = ws.getCell(currentRowIndex, 1); // 重要:先执行合并,再分发数据和样式
cell.alignment = { horizontal: 'center', vertical: 'middle' }; ws.mergeCells(currentRowIndex, 1, currentRowIndex, columnCount || 1);
cell.font = { bold: true, size: 14 };
row.height = 22; // 对合并后的 Master Cell 进行赋值和核心样式设置
const masterCell = ws.getCell(currentRowIndex, 1);
masterCell.value = titleContent;
// 字体正常,不加粗
masterCell.font = { size: 14, name: 'Microsoft YaHei' };
// 强制对主单元格设置居中和换行
masterCell.alignment = { horizontal: 'center', vertical: 'middle', wrapText: true };
// 强制整行设置换行对齐属性作为辅助
row.alignment = { horizontal: 'center', vertical: 'middle', wrapText: true };
// 计算行高 (基准 32 保证显示完整)
const lines = titleContent.split('\n').length;
row.height = Math.max(lines * 32, 40);
currentRowIndex += 1; currentRowIndex += 1;
} }
@ -1157,17 +1190,23 @@ export async function exportXlsxFromProColumnsExcelJS(
currentRowIndex += 1; currentRowIndex += 1;
} }
// 表头(保持原有样式与合并 // 表头(多级支持
const headerStartRow = currentRowIndex; const headerStartRow = currentRowIndex;
headerAOA.forEach((r, idx) => { headerAOA.forEach((r, idx) => {
const row = ws.getRow(headerStartRow + idx); const row = ws.getRow(headerStartRow + idx);
let maxRowLines = 1;
r.forEach((v, cIdx) => { r.forEach((v, cIdx) => {
const text = extractText(v);
const cell = row.getCell(cIdx + 1); const cell = row.getCell(cIdx + 1);
cell.value = v; cell.value = text;
cell.alignment = { horizontal: 'center', vertical: 'middle' }; cell.alignment = { horizontal: 'center', vertical: 'middle', wrapText: true };
cell.font = { bold: true }; cell.font = { bold: true };
const lines = text.split('\n').length;
if (lines > maxRowLines) maxRowLines = lines;
}); });
row.height = 18; row.alignment = { horizontal: 'center', vertical: 'middle', wrapText: true };
row.height = Math.max(22, maxRowLines * 18);
}); });
merges.forEach(m => { merges.forEach(m => {
ws.mergeCells(headerStartRow + (m.r1 - 1), m.c1, headerStartRow + (m.r2 - 1), m.c2); ws.mergeCells(headerStartRow + (m.r1 - 1), m.c1, headerStartRow + (m.r2 - 1), m.c2);
@ -1182,25 +1221,38 @@ export async function exportXlsxFromProColumnsExcelJS(
for (let i = 0; i < batch.length; i++) { for (let i = 0; i < batch.length; i++) {
const rec = batch[i]; const rec = batch[i];
const row = ws.getRow(currentRowIndex + i); const row = ws.getRow(currentRowIndex + i);
let maxRowLines = 1;
leafCols.forEach((col, j) => { leafCols.forEach((col, j) => {
const cell = row.getCell(j + 1); const cell = row.getCell(j + 1);
const v = getCellValue(col, rec, start + i); const val = getCellValue(col, rec, start + i);
cell.value = v; const text = extractText(val);
if (col.align) cell.alignment = { horizontal: col.align, vertical: 'middle' }; cell.value = text;
const alignment: any = { vertical: 'middle', wrapText: true };
if (col.align) alignment.horizontal = col.align;
cell.alignment = alignment;
const lines = text.split('\n').length;
if (lines > maxRowLines) maxRowLines = lines;
}); });
row.height = Math.max(18, maxRowLines * 16);
} }
currentRowIndex += batch.length; currentRowIndex += batch.length;
// 列宽估算 // 列宽估算
const sampleRows = Math.min(batch.length, 200);
for (let c = 1; c <= columnCount; c++) { for (let c = 1; c <= columnCount; c++) {
const wsCol = ws.getColumn(c);
// 注意:不再此处设置 wsCol.alignment防止覆盖标题和表头的水平居中样式
// 对齐已在数据行渲染阶段(Step 4)按列配置精准应用
const headerMax = Math.max(...headerAOA.map(r => estimateWidth(r[c - 1]))); const headerMax = Math.max(...headerAOA.map(r => estimateWidth(r[c - 1])));
let dataMax = 8; let dataMax = 8;
for (let i = 0; i < sampleRows; i++) { const sampleRowsLimit = Math.min(batch.length, 100);
for (let i = 0; i < sampleRowsLimit; i++) {
const v = leafCols[c - 1] ? getCellValue(leafCols[c - 1], batch[i], start + i) : ''; const v = leafCols[c - 1] ? getCellValue(leafCols[c - 1], batch[i], start + i) : '';
dataMax = Math.max(dataMax, estimateWidth(v)); dataMax = Math.max(dataMax, estimateWidth(v));
} }
ws.getColumn(c).width = Math.max(headerMax, dataMax); wsCol.width = Math.max(headerMax, dataMax);
} }
// 注意:不再对 ws.views 进行任何设置(避免冻结表头) // 注意:不再对 ws.views 进行任何设置(避免冻结表头)

View File

@ -1,4 +1,4 @@
// 由 scripts/writeVersion.js 自动生成 // 由 scripts/writeVersion.js 自动生成
export const VERSION = "4.5.128"; export const VERSION = "4.5.131";
export const GIT_HASH = "772dc23"; export const GIT_HASH = "83cb30f";
export const BUILD_TIME = "2026-01-23T11:34:36.109Z"; export const BUILD_TIME = "2026-01-26T10:28:52.569Z";