diff --git a/example-vite2/src/example/Basic.tsx b/example-vite2/src/example/Basic.tsx index 7a3a57b..ade8987 100644 --- a/example-vite2/src/example/Basic.tsx +++ b/example-vite2/src/example/Basic.tsx @@ -50,7 +50,6 @@ export default function App() { const onCountBtnClick = () => { const sheets = [ { - name: 'sheet', columns: columns, dataSource } diff --git a/example-vite2/src/example/CustomStyle.tsx b/example-vite2/src/example/CustomStyle.tsx index e5db861..c145951 100644 --- a/example-vite2/src/example/CustomStyle.tsx +++ b/example-vite2/src/example/CustomStyle.tsx @@ -146,6 +146,9 @@ export default function App() { bold: false, backgroundColor: 'ff0000', textAlign: 'left' + }, + body: { + textAlign: 'center' } } } @@ -153,7 +156,7 @@ export default function App() { style({ sheets, - filename: 'output.xlsx' + filename: 'custom style.xlsx' }) } diff --git a/example-vite2/src/example/HeaderGroup.tsx b/example-vite2/src/example/HeaderGroup.tsx index bce9eb5..275f9f6 100644 --- a/example-vite2/src/example/HeaderGroup.tsx +++ b/example-vite2/src/example/HeaderGroup.tsx @@ -92,7 +92,6 @@ export default function App() { const onCountBtnClick = () => { const sheets: ISheet[] = [ { - name: 'sheet', columns, dataSource } @@ -100,7 +99,7 @@ export default function App() { style({ sheets, - filename: 'moreSheet.xlsx', + filename: 'header group.xlsx', }) } diff --git a/example-vite2/src/example/HiddenHeader.tsx b/example-vite2/src/example/HiddenHeader.tsx index bc11e25..e18605f 100644 --- a/example-vite2/src/example/HiddenHeader.tsx +++ b/example-vite2/src/example/HiddenHeader.tsx @@ -181,7 +181,7 @@ export default function App() { nostyle({ sheets, - filename: 'output.xlsx', + filename: 'hidden header.xlsx', hiddenHeader: true, }) } diff --git a/example-vite2/src/example/MoreSheet.tsx b/example-vite2/src/example/MoreSheet.tsx index edd6e1b..e5d629b 100644 --- a/example-vite2/src/example/MoreSheet.tsx +++ b/example-vite2/src/example/MoreSheet.tsx @@ -45,12 +45,10 @@ export default function App() { const onCountBtnClick = () => { const sheets = [ { - name: 'sheet1', columns: columns, dataSource }, { - name: 'sheet2', columns: columns, dataSource } @@ -58,7 +56,7 @@ export default function App() { style({ sheets, - filename: 'moreSheet.xlsx', + filename: 'more sheet.xlsx', }) } diff --git a/example-vite2/src/example/NotExportColumn.tsx b/example-vite2/src/example/NotExportColumn.tsx index 0472ba8..76338c5 100644 --- a/example-vite2/src/example/NotExportColumn.tsx +++ b/example-vite2/src/example/NotExportColumn.tsx @@ -46,7 +46,6 @@ export default function App() { const onCountBtnClick = () => { const sheets = [ { - name: 'sheet', columns: columns, dataSource, } @@ -54,7 +53,7 @@ export default function App() { style({ sheets, - filename: 'basic.xlsx', + filename: 'not export column.xlsx', }) } diff --git a/example-vite2/src/example/Span.tsx b/example-vite2/src/example/Span.tsx index 4a44a21..1454ee5 100644 --- a/example-vite2/src/example/Span.tsx +++ b/example-vite2/src/example/Span.tsx @@ -162,7 +162,6 @@ export default function App() { const onCountBtnClick = () => { const sheets = [ { - name: '普通表格', columns: columns, dataSource, } @@ -170,7 +169,7 @@ export default function App() { style({ sheets, - filename: 'output.xlsx' + filename: '单元格合并.xlsx' }) } diff --git a/example-vite2/src/example/UseWorker.tsx b/example-vite2/src/example/UseWorker.tsx index e81a2e5..1379627 100644 --- a/example-vite2/src/example/UseWorker.tsx +++ b/example-vite2/src/example/UseWorker.tsx @@ -15,7 +15,7 @@ const columns = [ // in Worker 不能传函数 render: ` (val, row, i) => { - console.log(val, row, i) + // console.log(val, row, i) return 'formatted Name: ' + val } ` @@ -53,7 +53,7 @@ const columns = [ }, ] -const filename = 'basic.xlsx' +const filename = 'work in Worker.xlsx' export default function App() { const workerRef = useRef() diff --git a/src/style.ts b/src/style.ts index b7e924c..9bee31a 100644 --- a/src/style.ts +++ b/src/style.ts @@ -1,6 +1,6 @@ import { write as styleWriteFn } from 'xlsx-style-vite' -import getCellStyle from './utils/cellStyle' +import setCellStyle from './utils/setCellStyle' import download from './utils' import { IExcel } from './type' @@ -8,7 +8,7 @@ export default function style(excelProps: IExcel) { return download( Object.assign(excelProps, { write: styleWriteFn, - getCellStyle, + setCellStyle }) ) } diff --git a/src/type.ts b/src/type.ts index 54de594..754f9e0 100644 --- a/src/type.ts +++ b/src/type.ts @@ -1,13 +1,45 @@ +/** + * 主 Excel 配置接口 + */ +export interface IExcel { + sheets: ISheet[]; // 表格中的工作表列表 + filename?: string; // 导出的文件名 + hiddenHeader?: boolean; // 是否隐藏所有工作表的表头 + worker?: boolean; // 是否启用 Worker 多线程 +} + +/** + * 工作表配置接口 + */ +export interface ISheet { + name?: string; // 工作表名称 + columns: IColumn[]; // 列配置 + dataSource: Record[]; // 数据源 + hiddenHeader?: boolean; // 是否隐藏当前表的表头(优先级高于全局配置) + style?: ISheetStyle; // 样式配置 +} + +/** + * 样式配置接口(表头和正文) + */ +export interface ISheetStyle { + header?: IStyleConfig; // 表头样式配置 + body?: IStyleConfig; // 正文样式配置 +} + +/** + * 列配置接口 + */ export type IColumn = { - title: string; - width?: number; - notExport?: boolean; - children?: IColumn[]; - render?: (value: any, row: Record, rowIndex: number) => any | string; - onCell?: Function | string; - colSpan?: number; + title: string; // 列标题 + width?: number; // 列宽度 + notExport?: boolean; // 是否不导出该列 + children?: IColumn[]; // 子列(支持分组表头) + render?: (value: any, row: Record, rowIndex: number) => any | string; // 自定义单元格渲染 + onCell?: Function | string; // 单元格事件 + colSpan?: number; // 合并列数 + [key: string]: any; // 额外字段支持 - [key: string]: any; // dataIndex?: string; 表头分组 父 column 没 dataIndex // rootHeight?: number; // leafCount?: number; @@ -16,85 +48,68 @@ export type IColumn = { // merge?: IMerge; } -export interface ICellStyle { - fontSize?: number; - color?: string; - bold?: boolean; - background?: string; - backgroundColor?: string; - textAlign?: 'left' | 'center' | 'right'; - borderColor?: string; -} - -export interface ISheet { - name?: string; - columns: IColumn[]; - dataSource: Record[]; - hiddenHeader?: boolean; // 导出表格时,是否隐藏表头。默认显示表头 - style?: { - header?: ICellStyle; - body?: ICellStyle; - } -} - -export interface IExcel { - sheets: ISheet[]; - filename?: string; - hiddenHeader?: boolean; // 导出表格时,是否隐藏所有 sheet 表头 - worker?: boolean; -} - -// cell 左上角 -export type Point = { - r: number; - c: number; +/** + * 单元格左上角位置坐标 + */ +type Point = { + r: number; // 行索引 + c: number; // 列索引 } +/** + * 单元格合并信息 + */ export type IMerge = { - title?: string; - s: Point; - e: Point; + title?: string; // 合并后的标题 + s: Point; // 合并起始位置 + e: Point; // 合并结束位置 } -export type getCellStyleType = ( - { - isTitle, - style - }: { - isTitle: boolean, - style?: ISheet['style'] - } -) => styleType - +/** + * 用户配置的样式接口 + */ +export interface IStyleConfig { + fontSize?: number; // 字体大小 + color?: string; // 字体颜色 + bold?: boolean; // 是否加粗 + background?: string; // 背景色(优先级低于 backgroundColor) + backgroundColor?: string; // 背景颜色 + textAlign?: 'left' | 'center' | 'right'; // 文本对齐方式 + borderColor?: string; // 边框颜色 +} -export type Side = 'top' | 'bottom' | 'left' | 'right' +/** + * 边框的方向 + */ +export type Side = 'top' | 'bottom' | 'left' | 'right'; -export type borderType = Record< - Side, - { - style: string, +/** + * 单个单元格样式 + */ +export type ICellStyle = { + font?: { + sz?: number; // 字体大小 color?: { - rgb?: string + rgb: string; // 字体颜色(RGB 格式) } + bold?: boolean; // 是否加粗 } -> - -export type styleType = { - font?: { - sz?: number, - color: { - rgb?: string - }, - bold?: Boolean - }, alignment: { - horizontal?: ICellStyle['textAlign'], - wrapText: Boolean - }, - border: borderType, - fill?: { + horizontal: IStyleConfig['textAlign']; // 水平对齐方式 + wrapText: boolean; // 是否自动换行 + } + border: Record< + Side, + { + style: string; // 边框样式 + color?: { + rgb?: string; // 边框颜色(RGB 格式) + } + } + >; + fill: { fgColor: { - rgb: string + rgb?: string; // 填充颜色(RGB 格式) } } } diff --git a/src/utils/cellStyle.ts b/src/utils/cellStyle.ts deleted file mode 100644 index 0a9e7eb..0000000 --- a/src/utils/cellStyle.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { - getCellStyleType, - styleType, - Side, - borderType, - ICellStyle, -} from "../type" - -/** - * 设置单元格样式 - * isTitle: 是否是表头 - * style: 用户设置的 style 字面量 - */ -const getCellStyle: getCellStyleType = cellStyle => { - let { - isTitle, - style - } = cellStyle - - const s: styleType = { - alignment: { - horizontal: 'left', - wrapText: true - }, - border: getCellBorders() - } - - if (!style) { - style = { - header: {}, - body: {} - } - } else { - if (!style.header) style.header = {} - if (!style.body) style.body = {} - } - - isTitle - ? setStyle({ - s, - style: style.header, - defaultStyle: { - bold: true, - backgroundColor: 'dddddd', - textAlign: 'center' - } - }) - : setStyle({ - s, - style: style.body, - defaultStyle: { - backgroundColor: 'ffffff', - textAlign: 'left' - } - }) - return s -} - -/** - * 返回表格边框 sides: ['top', 'bottom' ...] - */ -function getCellBorders() { - const sides: Side[] = ['top', 'bottom', 'left', 'right'] - return sides.reduce((obj, side: Side) => { - obj[side] = { - style: 'thin' - } - return obj - }, {} as borderType) -} - -/** - * 根据 style,改变 s 部分属性的引用值 - * s: 每个单元格样式 -* style: 用户设置的 style 字面量 - * defaultStyle: 默认样式 - */ -function setStyle({ - s, - style, - defaultStyle -}: { - s: styleType, - style?: ICellStyle, - defaultStyle: ICellStyle & { - backgroundColor: string; - textAlign: 'left' | 'center' | 'right'; - }, -}) { - if (!style) return - const { - fontSize, - color, - bold = defaultStyle.bold, - background, - backgroundColor = defaultStyle.backgroundColor, - textAlign = defaultStyle.textAlign, - borderColor - } = style - - s.font = { - sz: fontSize, - color: { - rgb: color - }, - bold - } - - s.alignment.horizontal = textAlign - s.fill = { - fgColor: { - rgb: background || backgroundColor - } - } - - for (let side in s.border) { - s.border[side as Side].color = { - rgb: borderColor - } - } -} - -export default getCellStyle diff --git a/src/utils/index.ts b/src/utils/index.ts index 52f1218..1df4479 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -2,47 +2,42 @@ import { WorkBook, WritingOptions } from 'xlsx' import getSheets from './sheets' import s2ab from './s2ab' import saveAs from './saveAs' -import { IExcel, getCellStyleType } from '../type'; +import { IExcel, ISheet, ICellStyle } from '../type' -function download( - args: IExcel & - { - getCellStyle?: getCellStyleType; - write: (data: WorkBook, opts: WritingOptions) => any; - } -): Blob | void { +type DownloadParams = IExcel & { + setCellStyle?: ({ isTitle, style }: { isTitle: boolean, style?: ISheet['style'] }) => ICellStyle + write: (data: WorkBook, opts: WritingOptions) => any +} + +export default function download(args: DownloadParams): Blob | void { const { sheets, filename = 'excel.xlsx', hiddenHeader, worker, - getCellStyle, + setCellStyle, write, } = args const Sheets = getSheets({ sheets, hiddenHeader, - getCellStyle, + setCellStyle, }) - - const wbout: string = write( - { - Sheets, - SheetNames: sheets.map(({ name }, index) => name || `sheet${index}`) - }, - { - bookType: 'xlsx', - type: 'binary', - } - ) - + const workBook: WorkBook = { + Sheets, + SheetNames: sheets.map(({ name }, index) => name || `sheet${index + 1}`) + } + const writingOptions: WritingOptions = { + bookType: 'xlsx', + type: 'binary', + } + const wbout: string = write(workBook, writingOptions) const blob = new Blob([s2ab(wbout)], { type: 'application/octet-stream' }) + if (worker) { return blob } else { saveAs(blob, filename) } } - -export default download diff --git a/src/utils/setCellStyle.ts b/src/utils/setCellStyle.ts new file mode 100644 index 0000000..2b369d3 --- /dev/null +++ b/src/utils/setCellStyle.ts @@ -0,0 +1,108 @@ +import { + ISheet, + IStyleConfig, + ICellStyle, + Side +} from '../type' + +/** + * 设置单元格样式 + * @param cellStyleConfig - 包含 isTitle 和 style 的单元格样式配置 + * @returns 完整的单元格样式对象 + */ +export default function setCellStyle( + cellStyleConfig: { isTitle: boolean, style?: ISheet['style'] } +): ICellStyle { + const { + isTitle, + style = { + header: {}, + body: {} + } + } = cellStyleConfig + + const cellStyle: ICellStyle = { + alignment: { + horizontal: 'left', + wrapText: true, + }, + border: createCellBorders(), + fill: { + fgColor: {} + } + } + + // 根据是否是标题设置不同样式 + const appliedStyle = isTitle + ? { ...defaultHeaderStyle, ...style.header } + : { ...defaultBodyStyle, ...style.body } + + applyStyle(cellStyle, appliedStyle) + + return cellStyle +} + +/** + * 默认表头样式 + */ +const defaultHeaderStyle: IStyleConfig = { + bold: true, + backgroundColor: 'dddddd', + textAlign: 'center', +} + +/** + * 默认正文样式 + */ +const defaultBodyStyle: IStyleConfig = { + backgroundColor: 'ffffff', + textAlign: 'left', +} + +/** + * 创建单元格边框样式 + * @returns 表格边框对象 + */ +function createCellBorders(): ICellStyle['border'] { + const sides: Side[] = ['top', 'bottom', 'left', 'right'] + return sides.reduce((borders, side) => { + borders[side] = { style: 'thin' } + return borders + }, {} as ICellStyle['border']) +} + +/** + * 应用样式 + * @param target - 待修改的单元格样式 + * @param source - 用户自定义样式 + */ +function applyStyle( + target: ICellStyle, + source: IStyleConfig, +): void { + if (!source) return + + const { + fontSize, + color, + bold, + background, + backgroundColor = source.backgroundColor, + textAlign = source.textAlign, + borderColor, + } = source + + target.font = { + sz: fontSize, + color: color ? { rgb: color } : undefined, + bold, + } + + target.alignment.horizontal = textAlign + + target.fill.fgColor.rgb = background || backgroundColor + + Object.keys(target.border).forEach((side) => { + target.border[side as Side].color = borderColor ? { rgb: borderColor } : undefined + }) +} diff --git a/src/utils/sheets.ts b/src/utils/sheets.ts index c24bfde..4efa0f8 100644 --- a/src/utils/sheets.ts +++ b/src/utils/sheets.ts @@ -1,21 +1,22 @@ import { utils } from 'xlsx' import { + IExcel, ISheet, - getCellStyleType, + ICellStyle, IMerge, IColumn, } from '../type' // 获取所有 sheets -function getSheets( +export default function getSheets( { sheets, - hiddenHeader: outHiddenHeader, - getCellStyle, + hiddenHeader: excelHiddenHeader, + setCellStyle, }: { sheets: ISheet[], - hiddenHeader?: boolean, - getCellStyle?: getCellStyleType + hiddenHeader?: IExcel['hiddenHeader'], + setCellStyle?: ({ isTitle, style }: { isTitle: boolean, style: ISheet['style'] }) => ICellStyle } ) { return sheets.reduce((map: Record, sheet: ISheet, currentIndex: number) => { @@ -24,7 +25,7 @@ function getSheets( columns: originColumns, dataSource, style, - hiddenHeader = outHiddenHeader + hiddenHeader = excelHiddenHeader } = sheet const columns = excludeNotExportColumns(originColumns) @@ -66,11 +67,11 @@ function getSheets( v: typeof v === 'string' ? v.trim() : v, - s: typeof getCellStyle === 'function' - ? getCellStyle({ - isTitle, - style - }) + s: typeof setCellStyle === 'function' + ? setCellStyle({ + isTitle, + style + }) : undefined, t: typeof v === 'number' || typeof v === 'string' && v.includes('%') ? 'n' @@ -96,8 +97,6 @@ function getSheets( }, {}) } -export default getSheets - // 获取所有由叶子节点组成的 columns,也就是树的宽度 function getAllLeafColumns(columns: IColumn[]): IColumn[] { const allColumns: IColumn[] = [] @@ -197,7 +196,7 @@ const getDataFromColumnsAndDataSource = ( rootHeight, dataSource }: { - hiddenHeader?: Boolean, + hiddenHeader?: boolean, columns: IColumn[], leafColumns: IColumn[], rootHeight: number, @@ -323,7 +322,7 @@ function getBodyMerges({ leafColumns, dataSource }: { - hiddenHeader?: Boolean, + hiddenHeader?: boolean, rootHeight: number, leafColumns: IColumn[], dataSource: Record[] diff --git a/tsconfig.build.json b/tsconfig.build.json index 1815a5a..89be9be 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -5,12 +5,10 @@ "outDir": "dist", "target": "ES5", "module": "es2015", - "lib": ["dom", "ES5", "ES6"], - + "lib": ["dom", "ES5"], "strict": true, "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true + "noUnusedParameters": true }, "include": [ "types", diff --git a/tsconfig.json b/tsconfig.json index 41f909e..14517c4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,21 +1,6 @@ { + "extends": "./tsconfig.build.json", "compilerOptions": { - "moduleResolution": "node", - "declaration": true, "emitDeclarationOnly": true, - - "outDir": "dist", - "target": "ES5", - "module": "es2015", - "lib": ["dom", "ES5", "ES6"], - - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true - }, - "include": [ - "types", - "src/index.ts" - ] + } } \ No newline at end of file