feat: 增加科目总账明细表excel导出 和 凭证增加记账凭证work导出
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * æç® DOCX çæå¨ï¼é¶ä¾èµï¼ |
| | | * |
| | | * åªå®ç°å¯¼åºåæ®æéç WordprocessingML åéï¼é¡µé¢å°ºå¯¸/页边è·ãæ®µè½ |
| | | * ï¼ç¼©è¿ãè¡è·ãå¶è¡¨ä½ã对é½ãåä½åå·å ç²ï¼ãè¡¨æ ¼ï¼åºå®å宽ãåå¹¶åå
æ ¼ã |
| | | * è¾¹æ¡ãè¡é«ãåå
æ ¼åç´å¯¹é½ï¼ã |
| | | * |
| | | * ææ¡£æ¨¡åï¼ |
| | | * buildDocxBlob({ |
| | | * page: { width, height, margin: { top, right, bottom, left } }, |
| | | * blocks: [ |
| | | * { type: "paragraph", indentLeft, spacing: { before, line, lineRule }, |
| | | * align, tabs: [2310], runs: [{ text, size, bold }, { tab: true }] }, |
| | | * { type: "table", width, indent, grid: [821, ...], |
| | | * rows: [{ height, heightRule, cells: [ |
| | | * { text, width, gridSpan, align, bold, size, vAlign } ] }] }, |
| | | * ], |
| | | * }) |
| | | * |
| | | * 尺寸åä½ç»ä¸ä½¿ç¨ twipsï¼1/20 ç£
ï¼ï¼åå·ä½¿ç¨åç£
ï¼sz=21 å³ 10.5 ç£
ï¼ã |
| | | */ |
| | | import { createZip, utf8 } from "./zipStore"; |
| | | |
| | | const DOCX_MIME = |
| | | "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; |
| | | const W_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"; |
| | | const R_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"; |
| | | |
| | | const escapeXml = (value) => |
| | | String(value) |
| | | .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F]/g, "") |
| | | .replace(/&/g, "&") |
| | | .replace(/</g, "<") |
| | | .replace(/>/g, ">") |
| | | .replace(/"/g, """) |
| | | .replace(/'/g, "'"); |
| | | |
| | | const ATTR_ORDER = ["val", "color", "sz", "space", "w", "type", "before", "after", "line", "lineRule", "left", "right", "firstLine", "hanging"]; |
| | | |
| | | const el = (tag, attrs, children) => { |
| | | const keys = attrs ? Object.keys(attrs).filter((k) => attrs[k] !== undefined && attrs[k] !== null) : []; |
| | | keys.sort((a, b) => { |
| | | const ia = ATTR_ORDER.indexOf(a); |
| | | const ib = ATTR_ORDER.indexOf(b); |
| | | return (ia < 0 ? 99 : ia) - (ib < 0 ? 99 : ib); |
| | | }); |
| | | const attrText = keys.map((k) => ` w:${k}="${escapeXml(attrs[k])}"`).join(""); |
| | | if (children === undefined) return `<w:${tag}${attrText}/>`; |
| | | return `<w:${tag}${attrText}>${children}</w:${tag}>`; |
| | | }; |
| | | |
| | | /** çæ run ç屿§èç¹ï¼é¡ºåºå¿
须符å CT_RPr ç schema å®ä¹ï¼ */ |
| | | const buildRunProps = (run) => { |
| | | const parts = []; |
| | | if (run.font) { |
| | | parts.push( |
| | | el("rFonts", { ascii: run.font, hAnsi: run.font, eastAsia: run.font, cs: run.font }) |
| | | ); |
| | | } |
| | | if (run.bold) { |
| | | parts.push(el("b"), el("bCs")); |
| | | } |
| | | if (run.color) parts.push(el("color", { val: run.color })); |
| | | if (run.size) { |
| | | const sz = String(run.size); |
| | | parts.push(el("sz", { val: sz }), el("szCs", { val: sz })); |
| | | } |
| | | return parts.length ? el("rPr", null, parts.join("")) : ""; |
| | | }; |
| | | |
| | | const buildRuns = (runs = []) => |
| | | runs |
| | | .map((run) => { |
| | | const rPr = buildRunProps(run); |
| | | if (run.tab) return `<w:r>${rPr}<w:tab/></w:r>`; |
| | | const text = run.text === undefined || run.text === null ? "" : String(run.text); |
| | | if (!text) return rPr ? `<w:r>${rPr}</w:r>` : ""; |
| | | return `<w:r>${rPr}<w:t xml:space="preserve">${escapeXml(text)}</w:t></w:r>`; |
| | | }) |
| | | .join(""); |
| | | |
| | | /** çææ®µè½å±æ§èç¹ï¼åå
ç´ é¡ºåºå¿
须符å CT_PPr */ |
| | | const buildParaProps = (block) => { |
| | | const parts = []; |
| | | if (block.tabs && block.tabs.length) { |
| | | parts.push( |
| | | el( |
| | | "tabs", |
| | | null, |
| | | block.tabs.map((pos) => el("tab", { val: "left", pos: String(pos) })).join("") |
| | | ) |
| | | ); |
| | | } |
| | | if (block.spacing) { |
| | | parts.push( |
| | | el("spacing", { |
| | | before: block.spacing.before, |
| | | after: block.spacing.after, |
| | | line: block.spacing.line, |
| | | lineRule: block.spacing.lineRule, |
| | | }) |
| | | ); |
| | | } |
| | | if (block.indentLeft || block.indentRight || block.indentFirstLine) { |
| | | parts.push( |
| | | el("ind", { |
| | | left: block.indentLeft, |
| | | right: block.indentRight, |
| | | firstLine: block.indentFirstLine, |
| | | }) |
| | | ); |
| | | } |
| | | if (block.align) parts.push(el("jc", { val: block.align })); |
| | | if (block.runs && block.runs.length) { |
| | | const rPr = buildRunProps(block.runs[0]); |
| | | if (rPr) parts.push(rPr); |
| | | } |
| | | return parts.length ? el("pPr", null, parts.join("")) : ""; |
| | | }; |
| | | |
| | | const buildParagraph = (block) => { |
| | | if (block.type === "empty") return "<w:p/>"; |
| | | return `<w:p>${buildParaProps(block)}${buildRuns(block.runs)}</w:p>`; |
| | | }; |
| | | |
| | | const BORDER_SIDES = ["top", "left", "bottom", "right", "insideH", "insideV"]; |
| | | |
| | | const buildTableBorders = (border) => { |
| | | if (!border) return ""; |
| | | const sides = BORDER_SIDES.map((side) => { |
| | | if (!border[side]) return ""; |
| | | return el(side, { |
| | | val: border[side].val || "single", |
| | | color: border[side].color || "000000", |
| | | sz: String(border[side].sz || 6), |
| | | space: "0", |
| | | }); |
| | | }).join(""); |
| | | return sides ? el("tblBorders", null, sides) : ""; |
| | | }; |
| | | |
| | | const buildCell = (cell) => { |
| | | const parts = []; |
| | | if (cell.width) parts.push(el("tcW", { w: String(cell.width), type: "dxa" })); |
| | | if (cell.gridSpan && cell.gridSpan > 1) { |
| | | parts.push(el("gridSpan", { val: String(cell.gridSpan) })); |
| | | } |
| | | if (cell.vAlign) parts.push(el("vAlign", { val: cell.vAlign })); |
| | | |
| | | // 空åå
æ ¼ä¹ä¿ç段è½å±æ§ï¼å¯¹é½/缩è¿ï¼ï¼ä¾¿äºåç»å¨ Word ä¸ç»§ç»ç¼è¾ |
| | | const paragraph = buildParagraph({ |
| | | align: cell.align, |
| | | indentLeft: cell.indentLeft, |
| | | spacing: cell.spacing, |
| | | runs: cell.text |
| | | ? [{ text: cell.text, size: cell.size, bold: cell.bold, font: cell.font }] |
| | | : [], |
| | | }); |
| | | |
| | | return `<w:tc>${parts.length ? el("tcPr", null, parts.join("")) : ""}${paragraph}</w:tc>`; |
| | | }; |
| | | |
| | | const buildTable = (table) => { |
| | | const tblPrParts = []; |
| | | if (table.width) tblPrParts.push(el("tblW", { w: String(table.width), type: "dxa" })); |
| | | if (table.indent) tblPrParts.push(el("tblInd", { w: String(table.indent), type: "dxa" })); |
| | | const borders = buildTableBorders(table.border); |
| | | if (borders) tblPrParts.push(borders); |
| | | tblPrParts.push(el("tblLayout", { type: "fixed" })); |
| | | if (table.cellMargin !== undefined && table.cellMargin !== null) { |
| | | const m = table.cellMargin; |
| | | tblPrParts.push( |
| | | el( |
| | | "tblCellMar", |
| | | null, |
| | | ["top", "left", "bottom", "right"] |
| | | .map((side) => el(side, { w: String(m), type: "dxa" })) |
| | | .join("") |
| | | ) |
| | | ); |
| | | } |
| | | |
| | | const grid = (table.grid || []) |
| | | .map((w) => el("gridCol", { w: String(w) })) |
| | | .join(""); |
| | | |
| | | const rows = (table.rows || []) |
| | | .map((row) => { |
| | | const trPr = row.height |
| | | ? el("trPr", null, el("trHeight", { val: String(row.height), hRule: row.heightRule || "atLeast" })) |
| | | : ""; |
| | | const cells = (row.cells || []).map(buildCell).join(""); |
| | | return `<w:tr>${trPr}${cells}</w:tr>`; |
| | | }) |
| | | .join(""); |
| | | |
| | | return `<w:tbl>${el("tblPr", null, tblPrParts.join(""))}${el( |
| | | "tblGrid", |
| | | null, |
| | | grid |
| | | )}${rows}</w:tbl>`; |
| | | }; |
| | | |
| | | const buildSectPr = (page) => { |
| | | const margin = page.margin || {}; |
| | | return el( |
| | | "sectPr", |
| | | null, |
| | | [ |
| | | el("pgSz", { w: String(page.width), h: String(page.height) }), |
| | | el("pgMar", { |
| | | top: String(margin.top || 0), |
| | | right: String(margin.right || 0), |
| | | bottom: String(margin.bottom || 0), |
| | | left: String(margin.left || 0), |
| | | header: "0", |
| | | footer: "0", |
| | | gutter: "0", |
| | | }), |
| | | el("cols", { space: "720", num: "1" }), |
| | | ].join("") |
| | | ); |
| | | }; |
| | | |
| | | const buildDocumentXml = (model) => { |
| | | const body = (model.blocks || []) |
| | | .map((block) => (block.type === "table" ? buildTable(block) : buildParagraph(block))) |
| | | .join(""); |
| | | return [ |
| | | '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>', |
| | | `<w:document xmlns:w="${W_NS}" xmlns:r="${R_NS}">`, |
| | | `<w:body>${body}${buildSectPr(model.page || {})}</w:body>`, |
| | | "</w:document>", |
| | | ].join(""); |
| | | }; |
| | | |
| | | const buildStylesXml = (defaultFont, defaultSize) => { |
| | | const fonts = el("rFonts", { |
| | | ascii: defaultFont, |
| | | hAnsi: defaultFont, |
| | | eastAsia: defaultFont, |
| | | cs: defaultFont, |
| | | }); |
| | | const sz = String(defaultSize); |
| | | return [ |
| | | '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>', |
| | | `<w:styles xmlns:w="${W_NS}">`, |
| | | el( |
| | | "docDefaults", |
| | | null, |
| | | el( |
| | | "rPrDefault", |
| | | null, |
| | | el("rPr", null, `${fonts}${el("sz", { val: sz })}${el("szCs", { val: sz })}`) |
| | | ) + |
| | | el( |
| | | "pPrDefault", |
| | | null, |
| | | el("pPr", null, el("spacing", { line: "240", lineRule: "auto" })) |
| | | ) |
| | | ), |
| | | '<w:style w:type="paragraph" w:default="1" w:styleId="Normal"><w:name w:val="Normal"/><w:qFormat/></w:style>', |
| | | "</w:styles>", |
| | | ].join(""); |
| | | }; |
| | | |
| | | /** |
| | | * çæ docx æä»¶ç Blobã |
| | | * @param {{page?: object, blocks?: Array, defaultFont?: string, defaultSize?: number}} model |
| | | * @returns {Blob} |
| | | */ |
| | | export function buildDocxBlob(model = {}) { |
| | | const font = model.defaultFont || "å®ä½"; |
| | | const size = model.defaultSize || 21; |
| | | |
| | | const contentTypes = [ |
| | | '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>', |
| | | '<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">', |
| | | '<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>', |
| | | '<Default Extension="xml" ContentType="application/xml"/>', |
| | | '<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>', |
| | | '<Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/>', |
| | | "</Types>", |
| | | ].join(""); |
| | | |
| | | const files = [ |
| | | { name: "[Content_Types].xml", data: utf8(contentTypes) }, |
| | | { |
| | | name: "_rels/.rels", |
| | | data: utf8( |
| | | [ |
| | | '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>', |
| | | '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">', |
| | | `<Relationship Id="rId1" Type="${R_NS}/officeDocument" Target="word/document.xml"/>`, |
| | | "</Relationships>", |
| | | ].join("") |
| | | ), |
| | | }, |
| | | { name: "word/document.xml", data: utf8(buildDocumentXml(model)) }, |
| | | { |
| | | name: "word/_rels/document.xml.rels", |
| | | data: utf8( |
| | | [ |
| | | '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>', |
| | | '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">', |
| | | `<Relationship Id="rId1" Type="${R_NS}/styles" Target="styles.xml"/>`, |
| | | "</Relationships>", |
| | | ].join("") |
| | | ), |
| | | }, |
| | | { name: "word/styles.xml", data: utf8(buildStylesXml(font, size)) }, |
| | | ]; |
| | | |
| | | return createZip(files, DOCX_MIME); |
| | | } |
| | | |
| | | export default buildDocxBlob; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * æç® XLSX çæå¨ï¼é¶ä¾èµï¼ |
| | | * |
| | | * åªå®ç°å¯¼åºåºæ¯æéç OOXML åéï¼å宽ãè¡é«ãåå¹¶åå
æ ¼ãåä½ãè¾¹æ¡ã |
| | | * 坹齿¹å¼ãæ°åæ ¼å¼ã纯è²å¡«å
ãæ°æ®ä¸å¾å为 inlineStr / numberï¼ |
| | | * ä¸çæ sharedStringsï¼é¿å
é¢å¤çé¨ä»¶ä¸ç´¢å¼ç»´æ¤ã |
| | | * |
| | | * ç¨æ³ï¼ |
| | | * const blob = buildXlsxBlob([{ |
| | | * name: "Sheet1", |
| | | * columns: [{ width: 14 }], |
| | | * rows: [{ height: 20, cells: [{ value: "æ é¢", style: { ... } }] }], |
| | | * merges: [{ r1: 0, c1: 0, r2: 0, c2: 7 }], |
| | | * }]); |
| | | */ |
| | | |
| | | import { createZip, utf8 } from "./zipStore"; |
| | | |
| | | const escapeXml = (value) => |
| | | String(value) |
| | | // å»é¤ XML 1.0 ä¸å
è®¸çæ§å¶å符 |
| | | .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F]/g, "") |
| | | .replace(/&/g, "&") |
| | | .replace(/</g, "<") |
| | | .replace(/>/g, ">") |
| | | .replace(/"/g, """) |
| | | .replace(/'/g, "'"); |
| | | |
| | | const columnName = (index) => { |
| | | let name = ""; |
| | | let n = index + 1; |
| | | while (n > 0) { |
| | | const remainder = (n - 1) % 26; |
| | | name = String.fromCharCode(65 + remainder) + name; |
| | | n = Math.floor((n - 1) / 26); |
| | | } |
| | | return name; |
| | | }; |
| | | |
| | | const cellRef = (rowIndex, colIndex) => `${columnName(colIndex)}${rowIndex + 1}`; |
| | | |
| | | const XLSX_MIME = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; |
| | | |
| | | const BORDER_STYLE_BY_CODE = { |
| | | 1: "thin", |
| | | 2: "medium", |
| | | 3: "dashed", |
| | | 4: "dotted", |
| | | 5: "thick", |
| | | 6: "double", |
| | | 7: "hair", |
| | | 8: "mediumDashed", |
| | | 9: "dashDot", |
| | | 10: "mediumDashDot", |
| | | 11: "dashDotDot", |
| | | 12: "mediumDashDotDot", |
| | | 13: "slantDashDot", |
| | | }; |
| | | |
| | | const resolveBorderStyle = (value) => { |
| | | if (!value) return ""; |
| | | if (typeof value === "number") return BORDER_STYLE_BY_CODE[value] || "thin"; |
| | | return value === true ? "thin" : String(value); |
| | | }; |
| | | |
| | | /** ææ ·å¼å¯¹è±¡å½ä¸åæå¯æ¯è¾ç keyï¼å¹¶æ¶éåå表 */ |
| | | const collectStyles = (styleList) => { |
| | | const fonts = []; |
| | | const fills = []; |
| | | const borders = []; |
| | | const numFmts = []; |
| | | const xfs = []; |
| | | |
| | | const fontIndex = new Map(); |
| | | const fillIndex = new Map(); |
| | | const borderIndex = new Map(); |
| | | const numFmtIndex = new Map(); |
| | | |
| | | // é»è®¤åä½ / å¡«å
/ è¾¹æ¡å ä½ï¼fill 0=noneã1=gray125 æ¯ Excel çåºå®çº¦å®ï¼ |
| | | const internFont = (font) => { |
| | | const key = JSON.stringify(font || {}); |
| | | if (fontIndex.has(key)) return fontIndex.get(key); |
| | | const index = fonts.length; |
| | | fonts.push(font || {}); |
| | | fontIndex.set(key, index); |
| | | return index; |
| | | }; |
| | | const internFill = (fill) => { |
| | | const key = JSON.stringify(fill || null); |
| | | if (fillIndex.has(key)) return fillIndex.get(key); |
| | | const index = fills.length; |
| | | fills.push(fill || null); |
| | | fillIndex.set(key, index); |
| | | return index; |
| | | }; |
| | | const internBorder = (border) => { |
| | | const normalized = border || {}; |
| | | const key = JSON.stringify(normalized); |
| | | if (borderIndex.has(key)) return borderIndex.get(key); |
| | | const index = borders.length; |
| | | borders.push(normalized); |
| | | borderIndex.set(key, index); |
| | | return index; |
| | | }; |
| | | const internNumFmt = (formatCode) => { |
| | | if (!formatCode) return 0; |
| | | if (numFmtIndex.has(formatCode)) return numFmtIndex.get(formatCode); |
| | | const id = 164 + numFmts.length; |
| | | numFmts.push({ id, code: formatCode }); |
| | | numFmtIndex.set(formatCode, id); |
| | | return id; |
| | | }; |
| | | |
| | | // é¢çé»è®¤æ¡ç® |
| | | internFont({ name: "å®ä½", size: 11 }); |
| | | internFill(null); |
| | | internFill({ patternType: "gray125" }); |
| | | internBorder({}); |
| | | |
| | | styleList.forEach((style) => { |
| | | const source = style || {}; |
| | | xfs.push({ |
| | | fontId: internFont(source.font), |
| | | fillId: internFill(source.fill), |
| | | borderId: internBorder(source.border), |
| | | numFmtId: internNumFmt(source.numFmt), |
| | | align: source.align || null, |
| | | }); |
| | | }); |
| | | |
| | | return { fonts, fills, borders, numFmts, xfs }; |
| | | }; |
| | | |
| | | const buildFontXml = (font) => { |
| | | const parts = ["<font>"]; |
| | | if (font.bold) parts.push("<b/>"); |
| | | if (font.italic) parts.push("<i/>"); |
| | | if (font.strike) parts.push("<strike/>"); |
| | | if (font.underline) { |
| | | parts.push(font.underline === "double" ? '<u val="double"/>' : "<u/>"); |
| | | } |
| | | parts.push(`<sz val="${font.size || 11}"/>`); |
| | | if (font.color) parts.push(`<color rgb="${font.color}"/>`); |
| | | parts.push(`<name val="${escapeXml(font.name || "å®ä½")}"/>`); |
| | | if (font.charset) parts.push(`<charset val="${font.charset}"/>`); |
| | | parts.push("</font>"); |
| | | return parts.join(""); |
| | | }; |
| | | |
| | | const buildFillXml = (fill) => { |
| | | if (!fill) return '<fill><patternFill patternType="none"/></fill>'; |
| | | const patternType = fill.patternType || "solid"; |
| | | const fg = fill.color ? `<fgColor rgb="${fill.color}"/>` : "<fgColor indexed=\"64\"/>"; |
| | | return `<fill><patternFill patternType="${patternType}">${fg}<bgColor indexed="64"/></patternFill></fill>`; |
| | | }; |
| | | |
| | | const buildBorderXml = (border) => { |
| | | const side = (name) => { |
| | | const style = resolveBorderStyle(border[name]); |
| | | return style ? `<${name} style="${style}"><color indexed="64"/></${name}>` : `<${name}/>`; |
| | | }; |
| | | return `<border>${side("left")}${side("right")}${side("top")}${side("bottom")}<diagonal/></border>`; |
| | | }; |
| | | |
| | | const buildXfXml = (xf) => { |
| | | const attrs = [ |
| | | `numFmtId="${xf.numFmtId}"`, |
| | | `fontId="${xf.fontId}"`, |
| | | `fillId="${xf.fillId}"`, |
| | | `borderId="${xf.borderId}"`, |
| | | 'xfId="0"', |
| | | ]; |
| | | if (xf.numFmtId) attrs.push('applyNumberFormat="1"'); |
| | | if (xf.fontId) attrs.push('applyFont="1"'); |
| | | if (xf.fillId > 1) attrs.push('applyFill="1"'); |
| | | if (xf.borderId) attrs.push('applyBorder="1"'); |
| | | |
| | | const align = xf.align || {}; |
| | | const alignAttrs = []; |
| | | if (align.horizontal) alignAttrs.push(`horizontal="${align.horizontal}"`); |
| | | if (align.vertical) alignAttrs.push(`vertical="${align.vertical}"`); |
| | | if (align.wrapText) alignAttrs.push('wrapText="1"'); |
| | | if (align.textRotation) alignAttrs.push(`textRotation="${align.textRotation}"`); |
| | | if (alignAttrs.length) { |
| | | attrs.push('applyAlignment="1"'); |
| | | return `<xf ${attrs.join(" ")}><alignment ${alignAttrs.join(" ")}/></xf>`; |
| | | } |
| | | return `<xf ${attrs.join(" ")}/>`; |
| | | }; |
| | | |
| | | const buildStylesXml = (styleTable) => { |
| | | const { fonts, fills, borders, numFmts, xfs } = styleTable; |
| | | const numFmtXml = numFmts.length |
| | | ? `<numFmts count="${numFmts.length}">${numFmts |
| | | .map((item) => `<numFmt numFmtId="${item.id}" formatCode="${escapeXml(item.code)}"/>`) |
| | | .join("")}</numFmts>` |
| | | : ""; |
| | | |
| | | return [ |
| | | '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>', |
| | | '<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">', |
| | | numFmtXml, |
| | | `<fonts count="${fonts.length}">${fonts.map(buildFontXml).join("")}</fonts>`, |
| | | `<fills count="${fills.length}">${fills.map(buildFillXml).join("")}</fills>`, |
| | | `<borders count="${borders.length}">${borders.map(buildBorderXml).join("")}</borders>`, |
| | | '<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>', |
| | | `<cellXfs count="${xfs.length}">${xfs.map(buildXfXml).join("")}</cellXfs>`, |
| | | '<cellStyles count="1"><cellStyle name="常è§" xfId="0" builtinId="0"/></cellStyles>', |
| | | "</styleSheet>", |
| | | ].join(""); |
| | | }; |
| | | |
| | | const buildSheetXml = (sheet, styleTable) => { |
| | | const rows = sheet.rows || []; |
| | | const columns = sheet.columns || []; |
| | | |
| | | const colsXml = columns.length |
| | | ? `<cols>${columns |
| | | .map((column, index) => { |
| | | const width = typeof column === "number" ? column : column.width; |
| | | if (!width) return ""; |
| | | return `<col min="${index + 1}" max="${index + 1}" width="${width}" customWidth="1"/>`; |
| | | }) |
| | | .join("")}</cols>` |
| | | : ""; |
| | | |
| | | let maxCol = 0; |
| | | const rowXml = rows |
| | | .map((row, rowIndex) => { |
| | | const cells = row.cells || []; |
| | | const cellsXml = cells |
| | | .map((cell, colIndex) => { |
| | | if (!cell || cell.value === undefined || cell.value === null || cell.value === "") { |
| | | if (!cell || cell.style === undefined) return ""; |
| | | return `<c r="${cellRef(rowIndex, colIndex)}" s="${cell.style}"/>`; |
| | | } |
| | | const ref = cellRef(rowIndex, colIndex); |
| | | const styleAttr = cell.style === undefined ? "" : ` s="${cell.style}"`; |
| | | if (typeof cell.value === "number" && Number.isFinite(cell.value)) { |
| | | return `<c r="${ref}"${styleAttr}><v>${cell.value}</v></c>`; |
| | | } |
| | | if (typeof cell.value === "boolean") { |
| | | return `<c r="${ref}"${styleAttr} t="b"><v>${cell.value ? 1 : 0}</v></c>`; |
| | | } |
| | | return `<c r="${ref}"${styleAttr} t="inlineStr"><is><t xml:space="preserve">${escapeXml( |
| | | cell.value |
| | | )}</t></is></c>`; |
| | | }) |
| | | .filter(Boolean) |
| | | .join(""); |
| | | |
| | | const cellCount = cells.length; |
| | | if (cellCount > maxCol) maxCol = cellCount; |
| | | if (!cellsXml) return ""; |
| | | const heightAttr = row.height ? ` ht="${row.height}" customHeight="1"` : ""; |
| | | return `<row r="${rowIndex + 1}"${heightAttr}>${cellsXml}</row>`; |
| | | }) |
| | | .filter(Boolean) |
| | | .join(""); |
| | | |
| | | const merges = sheet.merges || []; |
| | | const mergesXml = merges.length |
| | | ? `<mergeCells count="${merges.length}">${merges |
| | | .map( |
| | | (merge) => |
| | | `<mergeCell ref="${cellRef(merge.r1, merge.c1)}:${cellRef(merge.r2, merge.c2)}"/>` |
| | | ) |
| | | .join("")}</mergeCells>` |
| | | : ""; |
| | | |
| | | const lastRow = rows.length; |
| | | const dimension = lastRow && maxCol ? `A1:${cellRef(lastRow - 1, maxCol - 1)}` : "A1"; |
| | | |
| | | return [ |
| | | '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>', |
| | | '<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">', |
| | | `<dimension ref="${dimension}"/>`, |
| | | '<sheetViews><sheetView workbookViewId="0"/></sheetViews>', |
| | | '<sheetFormatPr defaultRowHeight="14.25"/>', |
| | | colsXml, |
| | | `<sheetData>${rowXml}</sheetData>`, |
| | | mergesXml, |
| | | "</worksheet>", |
| | | ] |
| | | .filter(Boolean) |
| | | .join(""); |
| | | }; |
| | | |
| | | /** |
| | | * çæ xlsx æä»¶ç Blobã |
| | | * @param {Array<{name:string, columns?:Array, rows?:Array, merges?:Array}>} sheets |
| | | * @returns {Blob} |
| | | */ |
| | | export function buildXlsxBlob(sheets) { |
| | | const sheetList = Array.isArray(sheets) ? sheets : [sheets]; |
| | | |
| | | // æ ·å¼å
¨è¡¨å»éï¼ææå·¥ä½è¡¨çæ ·å¼å
±ç¨ä¸å¼ 表 |
| | | const flatStyles = []; |
| | | const styleIds = new Map(); |
| | | const internStyle = (style) => { |
| | | const key = JSON.stringify(style || {}); |
| | | if (styleIds.has(key)) return styleIds.get(key); |
| | | const id = flatStyles.length; |
| | | flatStyles.push(style || {}); |
| | | styleIds.set(key, id); |
| | | return id; |
| | | }; |
| | | |
| | | // å
ææ¯ä¸ªåå
æ ¼çæ ·å¼å¯¹è±¡æ¿æ¢æå
¨å±æ ·å¼ä¸æ |
| | | const preparedSheets = sheetList.map((sheet, sheetIndex) => ({ |
| | | name: (sheet.name || `Sheet${sheetIndex + 1}`).replace(/[\\/*?:[\]]/g, "").slice(0, 31), |
| | | columns: sheet.columns || [], |
| | | merges: sheet.merges || [], |
| | | rows: (sheet.rows || []).map((row) => ({ |
| | | height: row.height, |
| | | cells: (row.cells || []).map((cell) => { |
| | | if (!cell) return null; |
| | | if (cell.style === undefined || cell.style === null) { |
| | | return { value: cell.value, style: 0 }; |
| | | } |
| | | if (typeof cell.style === "number") return { value: cell.value, style: cell.style }; |
| | | return { value: cell.value, style: internStyle(cell.style) }; |
| | | }), |
| | | })), |
| | | })); |
| | | |
| | | // æ ·å¼è¡¨é¦é¡¹å¿
é¡»æ¯é»è®¤æ ·å¼ï¼ä¿è¯ç©ºç style=0 å¯ç¨ |
| | | if (flatStyles.length === 0) flatStyles.push({}); |
| | | const styleTable = collectStyles(flatStyles); |
| | | |
| | | const sheetFiles = preparedSheets.map((sheet, index) => ({ |
| | | name: `xl/worksheets/sheet${index + 1}.xml`, |
| | | data: utf8(buildSheetXml(sheet, styleTable)), |
| | | })); |
| | | |
| | | const workbookSheets = preparedSheets |
| | | .map( |
| | | (sheet, index) => |
| | | `<sheet name="${escapeXml(sheet.name)}" sheetId="${index + 1}" r:id="rId${index + 1}"/>` |
| | | ) |
| | | .join(""); |
| | | |
| | | const workbookRels = [ |
| | | ...preparedSheets.map( |
| | | (sheet, index) => |
| | | `<Relationship Id="rId${index + 1}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet${ |
| | | index + 1 |
| | | }.xml"/>` |
| | | ), |
| | | `<Relationship Id="rId${preparedSheets.length + 1}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>`, |
| | | ].join(""); |
| | | |
| | | const contentTypes = [ |
| | | '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>', |
| | | '<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">', |
| | | '<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>', |
| | | '<Default Extension="xml" ContentType="application/xml"/>', |
| | | '<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>', |
| | | ...preparedSheets.map( |
| | | (sheet, index) => |
| | | `<Override PartName="/xl/worksheets/sheet${ |
| | | index + 1 |
| | | }.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>` |
| | | ), |
| | | '<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>', |
| | | "</Types>", |
| | | ].join(""); |
| | | |
| | | const files = [ |
| | | { |
| | | name: "[Content_Types].xml", |
| | | data: utf8(contentTypes), |
| | | }, |
| | | { |
| | | name: "_rels/.rels", |
| | | data: utf8( |
| | | [ |
| | | '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>', |
| | | '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">', |
| | | '<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>', |
| | | "</Relationships>", |
| | | ].join("") |
| | | ), |
| | | }, |
| | | { |
| | | name: "xl/workbook.xml", |
| | | data: utf8( |
| | | [ |
| | | '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>', |
| | | '<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">', |
| | | `<sheets>${workbookSheets}</sheets>`, |
| | | "</workbook>", |
| | | ].join("") |
| | | ), |
| | | }, |
| | | { |
| | | name: "xl/_rels/workbook.xml.rels", |
| | | data: utf8( |
| | | [ |
| | | '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>', |
| | | '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">', |
| | | workbookRels, |
| | | "</Relationships>", |
| | | ].join("") |
| | | ), |
| | | }, |
| | | { |
| | | name: "xl/styles.xml", |
| | | data: utf8(buildStylesXml(styleTable)), |
| | | }, |
| | | ...sheetFiles, |
| | | ]; |
| | | |
| | | return createZip(files, XLSX_MIME); |
| | | } |
| | | |
| | | export default buildXlsxBlob; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * æç® ZIP æå
ï¼store åå¨ï¼ä¸å缩ï¼ã |
| | | * |
| | | * OOXML 容å¨ï¼xlsx / docxï¼æ¬è´¨é½æ¯ zip å
ï¼è¿éåªå®ç°åæä»¶æéçæå°éåï¼ |
| | | * æ¬å°æä»¶å¤´ + ä¸å¤®ç®å½ + ç®å½ç»æè®°å½ãä¸å缩å¯ä»¥çæ deflate ä¾èµï¼ |
| | | * ä½ç§¯å¯¹å¯¼åºåºæ¯å¯ä»¥æ¥åï¼ä¸ææè§£æå¨é½å
¼å®¹ã |
| | | */ |
| | | |
| | | const CRC_TABLE = (() => { |
| | | const table = new Uint32Array(256); |
| | | for (let i = 0; i < 256; i += 1) { |
| | | let c = i; |
| | | for (let k = 0; k < 8; k += 1) { |
| | | c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; |
| | | } |
| | | table[i] = c >>> 0; |
| | | } |
| | | return table; |
| | | })(); |
| | | |
| | | const crc32 = (bytes) => { |
| | | let c = 0xffffffff; |
| | | for (let i = 0; i < bytes.length; i += 1) { |
| | | c = CRC_TABLE[(c ^ bytes[i]) & 0xff] ^ (c >>> 8); |
| | | } |
| | | return (c ^ 0xffffffff) >>> 0; |
| | | }; |
| | | |
| | | const encoder = new TextEncoder(); |
| | | |
| | | export const utf8 = (text) => encoder.encode(text); |
| | | |
| | | /** |
| | | * @param {Array<{name: string, data: Uint8Array}>} files |
| | | * @param {string} mimeType |
| | | * @returns {Blob} |
| | | */ |
| | | export function createZip(files, mimeType) { |
| | | const now = new Date(); |
| | | const dosTime = |
| | | (now.getHours() << 11) | (now.getMinutes() << 5) | Math.floor(now.getSeconds() / 2); |
| | | const dosDate = |
| | | ((now.getFullYear() - 1980) << 9) | ((now.getMonth() + 1) << 5) | now.getDate(); |
| | | |
| | | const localParts = []; |
| | | const centralParts = []; |
| | | let offset = 0; |
| | | |
| | | files.forEach((file) => { |
| | | const nameBytes = utf8(file.name); |
| | | const data = file.data; |
| | | const crc = crc32(data); |
| | | |
| | | const local = new Uint8Array(30 + nameBytes.length); |
| | | const lv = new DataView(local.buffer); |
| | | lv.setUint32(0, 0x04034b50, true); |
| | | lv.setUint16(4, 20, true); |
| | | lv.setUint16(6, 0x0800, true); |
| | | lv.setUint16(8, 0, true); |
| | | lv.setUint16(10, dosTime, true); |
| | | lv.setUint16(12, dosDate, true); |
| | | lv.setUint32(14, crc, true); |
| | | lv.setUint32(18, data.length, true); |
| | | lv.setUint32(22, data.length, true); |
| | | lv.setUint16(26, nameBytes.length, true); |
| | | lv.setUint16(28, 0, true); |
| | | local.set(nameBytes, 30); |
| | | |
| | | const central = new Uint8Array(46 + nameBytes.length); |
| | | const cv = new DataView(central.buffer); |
| | | cv.setUint32(0, 0x02014b50, true); |
| | | cv.setUint16(4, 20, true); |
| | | cv.setUint16(6, 20, true); |
| | | cv.setUint16(8, 0x0800, true); |
| | | cv.setUint16(10, 0, true); |
| | | cv.setUint16(12, dosTime, true); |
| | | cv.setUint16(14, dosDate, true); |
| | | cv.setUint32(16, crc, true); |
| | | cv.setUint32(20, data.length, true); |
| | | cv.setUint32(24, data.length, true); |
| | | cv.setUint16(28, nameBytes.length, true); |
| | | cv.setUint16(30, 0, true); |
| | | cv.setUint16(32, 0, true); |
| | | cv.setUint16(34, 0, true); |
| | | cv.setUint16(36, 0, true); |
| | | cv.setUint32(38, 0, true); |
| | | cv.setUint32(42, offset, true); |
| | | central.set(nameBytes, 46); |
| | | |
| | | localParts.push(local, data); |
| | | centralParts.push(central); |
| | | offset += local.length + data.length; |
| | | }); |
| | | |
| | | const centralSize = centralParts.reduce((sum, part) => sum + part.length, 0); |
| | | const end = new Uint8Array(22); |
| | | const ev = new DataView(end.buffer); |
| | | ev.setUint32(0, 0x06054b50, true); |
| | | ev.setUint16(4, 0, true); |
| | | ev.setUint16(6, 0, true); |
| | | ev.setUint16(8, files.length, true); |
| | | ev.setUint16(10, files.length, true); |
| | | ev.setUint32(12, centralSize, true); |
| | | ev.setUint32(16, offset, true); |
| | | ev.setUint16(20, 0, true); |
| | | |
| | | return new Blob([...localParts, ...centralParts, end], { type: mimeType }); |
| | | } |
| | | |
| | | export default createZip; |
| | |
| | | <script setup> |
| | | import { ref, reactive, onMounted, computed, watch, nextTick } from "vue"; |
| | | import { ElMessage } from "element-plus"; |
| | | import { saveAs } from "file-saver"; |
| | | import { listAccountSubject } from "@/api/financialManagement/accountSubject"; |
| | | import { getDetailLedger } from "@/api/financialManagement/ledger"; |
| | | import { buildDetailLedgerBlob, buildDetailLedgerFileName } from "./detailLedgerExport"; |
| | | |
| | | defineOptions({ |
| | | name: "ç§ç®æç»è´¦", |
| | |
| | | ElMessage.info("æå°åè½"); |
| | | }; |
| | | |
| | | // å¯¼åº Excelï¼çé¢ä¸ãæ å½¢èµäº§æç»è´¦.xlsãæ¨¡æ¿ä¿æä¸è´ |
| | | const handleOut = () => { |
| | | if (!currentSubject.value) { |
| | | ElMessage.warning("请å
éæ©ä¼è®¡ç§ç®"); |
| | | return; |
| | | } |
| | | if (!dataList.value.length) { |
| | | ElMessage.warning("ææ æç»æ°æ®ï¼æ æ³å¯¼åº"); |
| | | return; |
| | | } |
| | | const options = { |
| | | subject: currentSubject.value, |
| | | startMonth: filters.startMonth, |
| | | endMonth: filters.endMonth, |
| | | rows: dataList.value, |
| | | }; |
| | | try { |
| | | saveAs(buildDetailLedgerBlob(options), buildDetailLedgerFileName(options)); |
| | | ElMessage.success("å¯¼åºæå"); |
| | | } catch (error) { |
| | | console.error(error); |
| | | ElMessage.error("导åºå¤±è´¥ï¼è¯·ç¨åéè¯"); |
| | | } |
| | | }; |
| | | |
| | | onMounted(async () => { |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * ç§ç®æç»è´¦å¯¼åºã |
| | | * |
| | | * çé¢ãåä½ãè¾¹æ¡ãå宽ãè¡é«ãåå¹¶åå
æ ¼åç
§æ¬åèæ¨¡æ¿ |
| | | * ãæ å½¢èµäº§æç»è´¦.xlsãï¼ä»
æå
å®¹æ¿æ¢ä¸ºå½åæ¥è¯¢å°çè´¦ç°¿æ°æ®ã |
| | | */ |
| | | import { buildXlsxBlob } from "@/utils/xlsxWriter"; |
| | | |
| | | /** 模æ¿ï¼å两è¡ä¸ä¿¡æ¯è¡ä½¿ç¨ Arial 12 */ |
| | | const TITLE_FONT = { name: "Arial", size: 12 }; |
| | | /** è¡¨å¤´ä¸æ°æ®è¡ä½¿ç¨ å®ä½ 11ï¼åè²ä¸ä¸å线åç¨é»è®¤å¼ */ |
| | | const DATA_FONT = { name: "å®ä½", size: 11 }; |
| | | |
| | | /** 模æ¿ï¼è¡¨å¤´åè¾¹ç»çº¿ï¼æ°æ®è¡åªæå·¦å³ä¸ä¸è¾¹ */ |
| | | const HEADER_BORDER = { left: "thin", right: "thin", top: "thin", bottom: "thin" }; |
| | | const DATA_BORDER = { left: "thin", right: "thin", bottom: "thin" }; |
| | | |
| | | const STYLE = { |
| | | title: { font: TITLE_FONT, align: { horizontal: "center", vertical: "bottom" } }, |
| | | infoLabel: { |
| | | font: TITLE_FONT, |
| | | align: { horizontal: "right", vertical: "bottom" }, |
| | | border: { bottom: "thin" }, |
| | | }, |
| | | infoValue: { |
| | | font: TITLE_FONT, |
| | | align: { horizontal: "left", vertical: "bottom" }, |
| | | border: { bottom: "thin" }, |
| | | }, |
| | | header: { |
| | | font: DATA_FONT, |
| | | align: { horizontal: "center", vertical: "justify" }, |
| | | border: HEADER_BORDER, |
| | | }, |
| | | date: { |
| | | font: DATA_FONT, |
| | | numFmt: "yyyy-mm-dd", |
| | | align: { horizontal: "center", vertical: "justify" }, |
| | | border: DATA_BORDER, |
| | | }, |
| | | center: { |
| | | font: DATA_FONT, |
| | | align: { horizontal: "center", vertical: "justify" }, |
| | | border: DATA_BORDER, |
| | | }, |
| | | text: { |
| | | font: DATA_FONT, |
| | | align: { horizontal: "left", vertical: "justify" }, |
| | | border: DATA_BORDER, |
| | | }, |
| | | amount: { |
| | | font: DATA_FONT, |
| | | numFmt: "#,##0.00", |
| | | align: { horizontal: "right", vertical: "justify" }, |
| | | border: DATA_BORDER, |
| | | }, |
| | | }; |
| | | |
| | | /** 模æ¿å宽ï¼Excel åç¬¦å®½åº¦ï¼ */ |
| | | const COLUMN_WIDTHS = [13.42578125, 11.7109375, 48.42578125, 22.5703125, 16.42578125, 17, 16.7109375]; |
| | | |
| | | const COLUMN_LABELS = ["åè¯æ¥æ", "åè¯åå·", "åè¯æè¦", "åæ¹éé¢", "è´·æ¹éé¢", "ä½é¢æ¹å", "ä½é¢"]; |
| | | |
| | | const ROW_HEIGHT = { title: 20, info: 15, header: 15.6, data: 19 }; |
| | | |
| | | const toNumber = (value) => { |
| | | const result = Number(value); |
| | | return Number.isFinite(result) ? result : 0; |
| | | }; |
| | | |
| | | /** "2026-09" -> "2026å¹´09æ" */ |
| | | const formatPeriod = (month) => { |
| | | const matched = /^(\d{4})-(\d{2})$/.exec(String(month || "")); |
| | | return matched ? `${matched[1]}å¹´${matched[2]}æ` : String(month || ""); |
| | | }; |
| | | |
| | | /** |
| | | * å¤æè®°è´¦è¡ç±»åã |
| | | * ä¼å
åå端ç rowTypeï¼ç¼ºå¤±æ¶ææè¦æåå
åºã |
| | | */ |
| | | const resolveRowKind = (row) => { |
| | | const type = row?.rowType; |
| | | if (type) return type; |
| | | const summary = String(row?.summary || ""); |
| | | if (summary.includes("æå")) return "opening"; |
| | | if (summary.includes("æ¬æ")) return "monthly_total"; |
| | | if (summary.includes("累计") || summary.includes("å计")) return "yearly_total"; |
| | | return "entry"; |
| | | }; |
| | | |
| | | /** |
| | | * æå端è¡å½ä¸åææ¨¡æ¿è¡ï¼ |
| | | * æåä½é¢è¡æ¸
ç©ºæ¥æä¸åè¯åå·ï¼æ±æ»è¡ææ¡æ¹æãæ¬æç´¯è®¡ / æ¬å¹´ç´¯è®¡ãã |
| | | */ |
| | | const toTemplateRows = (list) => { |
| | | const rows = []; |
| | | |
| | | (Array.isArray(list) ? list : []).forEach((row) => { |
| | | const kind = resolveRowKind(row); |
| | | if (kind === "opening") { |
| | | rows.push({ ...row, summary: "æåä½é¢", date: "", voucherNo: "" }); |
| | | return; |
| | | } |
| | | if (kind === "monthly_total") { |
| | | rows.push({ ...row, summary: "æ¬æç´¯è®¡", voucherNo: "" }); |
| | | return; |
| | | } |
| | | if (kind === "yearly_total") { |
| | | rows.push({ ...row, summary: "æ¬å¹´ç´¯è®¡", voucherNo: "" }); |
| | | return; |
| | | } |
| | | rows.push(row); |
| | | }); |
| | | |
| | | return rows; |
| | | }; |
| | | |
| | | const buildLedgerRow = (row) => ({ |
| | | height: ROW_HEIGHT.data, |
| | | cells: [ |
| | | { value: row.date || "", style: STYLE.date }, |
| | | { value: row.voucherNo || "", style: STYLE.center }, |
| | | { value: row.summary || "", style: STYLE.text }, |
| | | { value: toNumber(row.debit), style: STYLE.amount }, |
| | | { value: toNumber(row.credit), style: STYLE.amount }, |
| | | { value: row.direction || "", style: STYLE.center }, |
| | | { value: toNumber(row.balance), style: STYLE.amount }, |
| | | ], |
| | | }); |
| | | |
| | | /** |
| | | * æé æç»è´¦å·¥ä½è¡¨æè¿°ã |
| | | * @param {{subject?: {code?: string, name?: string}, startMonth?: string, endMonth?: string, rows?: Array}} options |
| | | */ |
| | | export function buildDetailLedgerSheet({ subject, startMonth, endMonth, rows } = {}) { |
| | | const sheetRows = [ |
| | | { height: ROW_HEIGHT.title, cells: [{ value: "æç»è´¦", style: STYLE.title }] }, |
| | | { |
| | | height: ROW_HEIGHT.info, |
| | | cells: [ |
| | | { value: "ç§ç®ç¼ç ï¼", style: STYLE.infoLabel }, |
| | | { value: subject?.code || "", style: STYLE.infoValue }, |
| | | { value: "ç§ç®åç§°ï¼", style: STYLE.infoLabel }, |
| | | { value: subject?.name || "", style: STYLE.infoValue }, |
| | | { value: "ä¼è®¡æé´ï¼", style: STYLE.infoLabel }, |
| | | { |
| | | value: `${formatPeriod(startMonth)}-${formatPeriod(endMonth)}`, |
| | | style: STYLE.infoValue, |
| | | }, |
| | | ], |
| | | }, |
| | | { |
| | | height: ROW_HEIGHT.header, |
| | | cells: COLUMN_LABELS.map((label) => ({ value: label, style: STYLE.header })), |
| | | }, |
| | | ]; |
| | | |
| | | toTemplateRows(rows).forEach((row) => sheetRows.push(buildLedgerRow(row))); |
| | | |
| | | return { |
| | | name: "æ°æ®è¡¨1", |
| | | columns: COLUMN_WIDTHS.map((width) => ({ width })), |
| | | rows: sheetRows, |
| | | merges: [{ r1: 0, c1: 0, r2: 0, c2: 7 }], |
| | | }; |
| | | } |
| | | |
| | | /** çææç»è´¦ xlsx ç Blob */ |
| | | export function buildDetailLedgerBlob(options) { |
| | | return buildXlsxBlob([buildDetailLedgerSheet(options)]); |
| | | } |
| | | |
| | | /** çæå¯¼åºæä»¶åï¼å¦ãæ å½¢èµäº§æç»è´¦_2026-09_2026-09.xlsxã */ |
| | | export function buildDetailLedgerFileName({ subject, startMonth, endMonth } = {}) { |
| | | const subjectName = String(subject?.name || subject?.code || "ç§ç®").replace(/[\\/:*?"<>|]/g, ""); |
| | | const period = [startMonth, endMonth].filter(Boolean).join("_"); |
| | | return period ? `${subjectName}æç»è´¦_${period}.xlsx` : `${subjectName}æç»è´¦.xlsx`; |
| | | } |
| | | |
| | | export default buildDetailLedgerBlob; |
| | |
| | | </template> |
| | | <template #operation="{ row }"> |
| | | <el-button type="primary" link @click="view(row)">æ¥ç</el-button> |
| | | <el-button type="primary" link @click="exportVoucher(row)">导åº</el-button> |
| | | <el-button type="primary" link @click="edit(row)" v-if="canEditVoucher(row.status)">ç¼è¾</el-button> |
| | | <el-button type="success" link @click="handlePost(row)" v-if="canEditVoucher(row.status)">è¿è´¦</el-button> |
| | | <el-button type="danger" link @click="handleCancel(row)" v-if="canEditVoucher(row.status)">ä½åº</el-button> |
| | |
| | | <script setup> |
| | | import { ref, reactive, onMounted, computed, nextTick } from "vue"; |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import { saveAs } from "file-saver"; |
| | | import FormDialog from "@/components/Dialog/FormDialog.vue"; |
| | | import AttachmentUploadFile from "@/components/AttachmentUpload/file/index.vue"; |
| | | import FileList from "@/components/Dialog/FileList.vue"; |
| | |
| | | cancelVoucher, |
| | | getVoucherDetail, |
| | | } from "@/api/financialManagement/voucher"; |
| | | import { buildVoucherDocxBlob, buildVoucherFileName } from "./voucherExport"; |
| | | |
| | | defineOptions({ |
| | | name: "åè¯ç®¡ç", |
| | |
| | | { label: "è´·æ¹éé¢", prop: "credit", dataType: "slot", slot: "credit" }, |
| | | { label: "å¶å人", prop: "creator", width: "100" }, |
| | | { label: "ç¶æ", prop: "status", dataType: "slot", slot: "status" }, |
| | | { label: "æä½", prop: "operation", dataType: "slot", slot: "operation", width: "220", fixed: "right" }, |
| | | { label: "æä½", prop: "operation", dataType: "slot", slot: "operation", width: "280", fixed: "right" }, |
| | | ]; |
| | | |
| | | const dataList = ref([]); |
| | |
| | | ElMessage.info("导å
¥åè½"); |
| | | }; |
| | | |
| | | // 导åºéä¸åè¯çæç»åå½ï¼Wordï¼çé¢ä¸ä¼è®¡åè¯æ¨¡æ¿ä¸è´ï¼ |
| | | const exportVoucher = async (row) => { |
| | | try { |
| | | const { data } = await getVoucherDetail(row.id); |
| | | const detail = data || row; |
| | | saveAs(buildVoucherDocxBlob(detail), buildVoucherFileName(detail)); |
| | | ElMessage.success("å¯¼åºæå"); |
| | | } catch (error) { |
| | | console.error(error); |
| | | ElMessage.error("导åºå¤±è´¥ï¼è¯·ç¨åéè¯"); |
| | | } |
| | | }; |
| | | |
| | | const handleOut = () => { |
| | | ElMessage.success("å¯¼åºæå"); |
| | | }; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * è®°è´¦åè¯å¯¼åºï¼Wordï¼ã |
| | | * |
| | | * çé¢ç
§æ¬åèæ¨¡æ¿ãæ å½¢èµäº§ä¼è®¡åè¯.docxãï¼æ¨ªååè¯çº¸ãå
¬å¸å + ãè®°è´¦åè¯ã |
| | | * 大æ é¢åè¡ãåè¯ä¿¡æ¯è¡ã6 åæç»åå½è¡¨ï¼åºå·/ç§ç®ä»£ç /ç§ç®åç§°/æè¦/åæ¹/è´·æ¹ï¼ |
| | | * ä¸å计è¡ãåºé¨å¶å人ã |
| | | * |
| | | * 尺寸åä½ä¸º twipsï¼1/20 ç£
ï¼ï¼åå·ä¸ºåç£
ï¼21 = 10.5 ç£
ï¼ã |
| | | */ |
| | | import { buildDocxBlob } from "@/utils/docxWriter"; |
| | | |
| | | /** åè¯æ é¢ */ |
| | | const VOUCHER_TITLE = "è®°è´¦åè¯"; |
| | | /** å
¬å¸ */ |
| | | const COMPANY = "å
¬å¸åç§°ï¼"; |
| | | /** æ¬ä½å¸ï¼æ¨¡æ¿åºå®ä¸ºäººæ°å¸ */ |
| | | const CURRENCY = "人æ°å¸"; |
| | | |
| | | /** æ é¢è¡ç¼©è¿ï¼æ²¿ç¨æ¨¡æ¿éå
¬å¸åæå¨ä½ç½® */ |
| | | const TITLE_INDENT = 1823; |
| | | /** ãè®°è´¦åè¯ãè½ç¹ï¼å³æ¨¡æ¿éå
¬å¸åå é´è·ä¹åçåå§ä½ç½® */ |
| | | const TITLE_TAB = 6863; |
| | | |
| | | /** åè¯çº¸ï¼260mm à 125mm 横å */ |
| | | const PAGE = { |
| | | width: 14740, |
| | | height: 7087, |
| | | margin: { top: 202, right: 375, bottom: 0, left: 1095 }, |
| | | }; |
| | | |
| | | /** è¡¨æ ¼åå®½ï¼æ¨¡æ¿åå¼ï¼ */ |
| | | const GRID = [821, 1725, 3345, 3704, 1769, 1895]; |
| | | const TABLE_WIDTH = 13259; |
| | | const TABLE_INDENT = 5; |
| | | const TABLE_BORDER = { |
| | | top: { val: "single", color: "000000", sz: 6 }, |
| | | left: { val: "single", color: "000000", sz: 6 }, |
| | | bottom: { val: "single", color: "000000", sz: 6 }, |
| | | right: { val: "single", color: "000000", sz: 6 }, |
| | | insideH: { val: "single", color: "000000", sz: 6 }, |
| | | insideV: { val: "single", color: "000000", sz: 6 }, |
| | | }; |
| | | |
| | | const ROW_HEIGHT = { header: 385, data: 373, total: 463 }; |
| | | |
| | | const FONT_SIZE = { |
| | | title: 44, |
| | | info: 21, |
| | | header: 21, |
| | | index: 18, |
| | | cell: 21, |
| | | totalLabel: 24, |
| | | }; |
| | | |
| | | /** ä¿¡æ¯è¡åä¸ªåæ®µçå¶è¡¨ä½ï¼ç¸å¯¹å·¦è¾¹è·ï¼ï¼ä¸æ¨¡æ¿ä¸æåè½ç¹ä¸è´ */ |
| | | const INFO_TABS = [2645, 6215, 9785]; |
| | | /** å¶å人è½ç¹ï¼æ²¿ç¨æ¨¡æ¿ä¸å¶å人æå¨ä½ç½® */ |
| | | const CREATOR_POSITION = 9498; |
| | | /** 左对é½åå
æ ¼ç左侧çç½ */ |
| | | const CELL_INDENT = 110; |
| | | |
| | | const COLUMN_LABELS = ["åºå·", "ç§ç®ä»£ç ", "ç§ç®åç§°", "æè¦", "åæ¹", "è´·æ¹"]; |
| | | |
| | | /** é颿 ¼å¼ï¼å¸¦ååä½ä¸¤ä½å°æ°ï¼æ é颿¶ç空ï¼ä¸æ¨¡æ¿ä¸è´ */ |
| | | const formatAmount = (value) => { |
| | | const amount = Number(value); |
| | | if (!Number.isFinite(amount) || amount === 0) return ""; |
| | | return amount.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ","); |
| | | }; |
| | | |
| | | /** åè¯åä¸ç¼å·ï¼'è®°-2' -> { prefix: 'è®°', num: '2' } */ |
| | | const splitVoucherNo = (voucherNo) => { |
| | | const text = String(voucherNo || "").trim(); |
| | | if (!text) return { prefix: "", num: "" }; |
| | | const index = text.indexOf("-"); |
| | | if (index < 0) return { prefix: text, num: "" }; |
| | | return { prefix: text.slice(0, index), num: text.slice(index + 1) }; |
| | | }; |
| | | |
| | | const normalizeDate = (value) => { |
| | | const text = String(value || ""); |
| | | return /^\d{4}-\d{2}-\d{2}/.test(text) ? text.slice(0, 10) : text; |
| | | }; |
| | | |
| | | const buildHeaderRow = () => ({ |
| | | height: ROW_HEIGHT.header, |
| | | cells: COLUMN_LABELS.map((label, index) => ({ |
| | | text: label, |
| | | width: GRID[index], |
| | | align: "center", |
| | | bold: true, |
| | | size: FONT_SIZE.header, |
| | | vAlign: "center", |
| | | })), |
| | | }); |
| | | |
| | | const buildEntryRow = (entry, index) => ({ |
| | | height: ROW_HEIGHT.data, |
| | | cells: [ |
| | | { |
| | | text: String(index + 1), |
| | | width: GRID[0], |
| | | align: "left", |
| | | indentLeft: CELL_INDENT + 5, |
| | | size: FONT_SIZE.index, |
| | | vAlign: "center", |
| | | }, |
| | | { |
| | | text: entry.subjectCode || "", |
| | | width: GRID[1], |
| | | align: "left", |
| | | indentLeft: CELL_INDENT, |
| | | size: FONT_SIZE.cell, |
| | | vAlign: "center", |
| | | }, |
| | | { |
| | | text: entry.subjectName || "", |
| | | width: GRID[2], |
| | | align: "left", |
| | | indentLeft: CELL_INDENT, |
| | | size: FONT_SIZE.cell, |
| | | vAlign: "center", |
| | | }, |
| | | { |
| | | text: entry.summary || "", |
| | | width: GRID[3], |
| | | align: "left", |
| | | indentLeft: CELL_INDENT, |
| | | size: FONT_SIZE.cell, |
| | | vAlign: "center", |
| | | }, |
| | | { |
| | | text: formatAmount(entry.debit), |
| | | width: GRID[4], |
| | | align: "center", |
| | | size: FONT_SIZE.cell, |
| | | vAlign: "center", |
| | | }, |
| | | { |
| | | text: formatAmount(entry.credit), |
| | | width: GRID[5], |
| | | align: "center", |
| | | size: FONT_SIZE.cell, |
| | | vAlign: "center", |
| | | }, |
| | | ], |
| | | }); |
| | | |
| | | const buildTotalRow = (totalDebit, totalCredit) => ({ |
| | | height: ROW_HEIGHT.total, |
| | | cells: [ |
| | | { text: "", width: GRID[0] + GRID[1] + GRID[2], gridSpan: 3, vAlign: "center" }, |
| | | { |
| | | text: "å计", |
| | | width: GRID[3], |
| | | align: "center", |
| | | size: FONT_SIZE.totalLabel, |
| | | vAlign: "center", |
| | | }, |
| | | { |
| | | text: formatAmount(totalDebit), |
| | | width: GRID[4], |
| | | align: "center", |
| | | size: FONT_SIZE.cell, |
| | | vAlign: "center", |
| | | }, |
| | | { |
| | | text: formatAmount(totalCredit), |
| | | width: GRID[5], |
| | | align: "center", |
| | | size: FONT_SIZE.cell, |
| | | vAlign: "center", |
| | | }, |
| | | ], |
| | | }); |
| | | |
| | | /** |
| | | * æé è®°è´¦åè¯ææ¡£æ¨¡åã |
| | | * @param {{voucherNo?: string, voucherDate?: string, creator?: string, entries?: Array}} voucher |
| | | */ |
| | | export function buildVoucherDocModel(voucher = {}) { |
| | | const entries = (Array.isArray(voucher.entries) ? voucher.entries : []).filter(Boolean); |
| | | const { prefix, num } = splitVoucherNo(voucher.voucherNo); |
| | | const date = normalizeDate(voucher.voucherDate); |
| | | const creator = voucher.creator || ""; |
| | | |
| | | const totalDebit = entries.reduce((sum, item) => sum + Number(item.debit || 0), 0); |
| | | const totalCredit = entries.reduce((sum, item) => sum + Number(item.credit || 0), 0); |
| | | |
| | | const infoRuns = [ |
| | | { text: `åè¯åï¼${prefix}`, size: FONT_SIZE.info }, |
| | | { tab: true }, |
| | | { text: `åè¯ç¼å·ï¼${num}`, size: FONT_SIZE.info }, |
| | | { tab: true }, |
| | | { text: `åè¯æ¥æï¼${date}`, size: FONT_SIZE.info }, |
| | | { tab: true }, |
| | | { text: `æ¬ä½å¸ï¼${CURRENCY}`, size: FONT_SIZE.info }, |
| | | ]; |
| | | |
| | | const footerRuns = [{ text: `å¶å人ï¼${creator}`, size: FONT_SIZE.info }]; |
| | | |
| | | return { |
| | | defaultFont: "å®ä½", |
| | | defaultSize: FONT_SIZE.info, |
| | | page: PAGE, |
| | | blocks: [ |
| | | { |
| | | type: "paragraph", |
| | | indentLeft: TITLE_INDENT, |
| | | spacing: { before: 86, line: 220, lineRule: "auto" }, |
| | | tabs: [TITLE_TAB], |
| | | runs: [ |
| | | { text: COMPANY, size: FONT_SIZE.info }, |
| | | { tab: true }, |
| | | { text: VOUCHER_TITLE, size: FONT_SIZE.title }, |
| | | ], |
| | | }, |
| | | { |
| | | type: "paragraph", |
| | | indentLeft: 335, |
| | | spacing: { before: 156, line: 219, lineRule: "auto" }, |
| | | tabs: INFO_TABS, |
| | | runs: infoRuns, |
| | | }, |
| | | { type: "empty" }, |
| | | { |
| | | type: "table", |
| | | width: TABLE_WIDTH, |
| | | indent: TABLE_INDENT, |
| | | grid: GRID, |
| | | border: TABLE_BORDER, |
| | | cellMargin: 0, |
| | | rows: [ |
| | | buildHeaderRow(), |
| | | ...entries.map((entry, index) => buildEntryRow(entry, index)), |
| | | buildTotalRow(totalDebit, totalCredit), |
| | | ], |
| | | }, |
| | | { |
| | | type: "paragraph", |
| | | indentLeft: CREATOR_POSITION, |
| | | spacing: { before: 237, line: 219, lineRule: "auto" }, |
| | | runs: footerRuns, |
| | | }, |
| | | ], |
| | | }; |
| | | } |
| | | |
| | | /** çæè®°è´¦åè¯ docx ç Blob */ |
| | | export function buildVoucherDocxBlob(voucher) { |
| | | return buildDocxBlob(buildVoucherDocModel(voucher)); |
| | | } |
| | | |
| | | /** çæå¯¼åºæä»¶åï¼å¦ãè®°è´¦åè¯_è®°-2_2026-07-31.docxã */ |
| | | export function buildVoucherFileName(voucher = {}) { |
| | | const { prefix, num } = splitVoucherNo(voucher.voucherNo); |
| | | const label = `${prefix}${num}`.replace(/[\\/:*?"<>|]/g, ""); |
| | | const date = normalizeDate(voucher.voucherDate).replace(/[\\/:*?"<>|]/g, ""); |
| | | const parts = ["è®°è´¦åè¯", label, date].filter(Boolean); |
| | | return `${parts.join("_")}.docx`; |
| | | } |
| | | |
| | | export default buildVoucherDocxBlob; |