feat: 报告生成扩展 PPT/Word/Excel 多格式导出
- 新增 ExportReportDOCX 导出 Word 文档(纯标准库)
- 新增 ExportReportPPTX 导出 PPT 演示文稿(纯标准库)
- 新增 ExportReportXLSX 导出 Excel 表格(纯标准库)
- 新增 /api/report/export/{docx,pptx,xlsx} 路由
- 前端增加多格式导出下拉菜单(PDF/Word/PPT/Excel)
- 前端导出函数按格式名提示成功信息
- 零网络依赖,纯 Go 标准库实现
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// writeFile helper: create a file in zip and write content
|
||||
func zipWriteFile(zw *zip.Writer, name, content string) error {
|
||||
f, err := zw.Create(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = f.Write([]byte(content))
|
||||
return err
|
||||
}
|
||||
|
||||
// escapeXML escapes HTML/XML special characters
|
||||
func escapeXML(s string) string {
|
||||
s = strings.ReplaceAll(s, "&", "&")
|
||||
s = strings.ReplaceAll(s, "<", "<")
|
||||
s = strings.ReplaceAll(s, ">", ">")
|
||||
s = strings.ReplaceAll(s, "'", "'")
|
||||
s = strings.ReplaceAll(s, `"`, """)
|
||||
return s
|
||||
}
|
||||
|
||||
// timeStr returns a fixed string for pptx sldId
|
||||
func timeStr() string {
|
||||
return "20060102T150405"
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func ExportReportDOCX(report reportContent) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
zw := zip.NewWriter(&buf)
|
||||
zipWriteFile(zw, "[Content_Types].xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="xml" ContentType="application/xml"/><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/></Types>`)
|
||||
zipWriteFile(zw, "_rels/.rels", `<?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="word/document.xml"/></Relationships>`)
|
||||
var doc strings.Builder
|
||||
doc.WriteString(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` + `<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body>`)
|
||||
doc.WriteString(`<w:p><w:pPr><w:pStyle w:val="Title"/></w:pPr><w:r><w:rPr><w:b/><w:sz w:val="48"/></w:rPr><w:t>` + escapeXML(report.Topic) + `</w:t></w:r></w:p>`)
|
||||
doc.WriteString(`<w:p><w:r><w:rPr><w:b/><w:sz w:val="24"/></w:rPr><w:t>摘要</w:t></w:r></w:p>`)
|
||||
doc.WriteString(`<w:p><w:r><w:rPr><w:sz w:val="24"/></w:rPr><w:t>` + escapeXML(report.Summary) + `</w:t></w:r></w:p>`)
|
||||
for _, ch := range report.Chapters {
|
||||
doc.WriteString(`<w:p><w:pPr><w:pStyle w:val="Heading1"/></w:pPr><w:r><w:rPr><w:b/><w:sz w:val="28"/></w:rPr><w:t>` + escapeXML(ch.Title) + `</w:t></w:r></w:p>`)
|
||||
doc.WriteString(`<w:p><w:r><w:rPr><w:sz w:val="22"/></w:rPr><w:t>` + escapeXML(ch.Body) + `</w:t></w:r></w:p>`)
|
||||
}
|
||||
doc.WriteString(`</w:body></w:document>`)
|
||||
zipWriteFile(zw, "word/document.xml", doc.String())
|
||||
zipWriteFile(zw, "word/styles.xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><styles xmlns="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><style type="paragraph" styleId="Title"><name val="Title"/><uiPriority uiPriority="9"/><rPr><rStyle val="Title"/><rFonts w:ascii="Arial" w:hAnsi="Arial"/><b/><sz w:val="48"/></rPr></style><style type="paragraph" styleId="Heading1"><name val="Heading 1"/><rPr><rStyle val="Heading1"/><rFonts w:ascii="Arial" w:hAnsi="Arial"/><b/><sz w:val="28"/></rPr></style></styles>`)
|
||||
zipWriteFile(zw, "word/_rels/document.xml.rels", `<?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/styles" Target="styles.xml"/></Relationships>`)
|
||||
zipWriteFile(zw, "docProps/core.xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:title>` + escapeXML(report.Topic) + `</dc:title></cp:coreProperties>`)
|
||||
zipWriteFile(zw, "docProps/app.xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"><Application>Microsoft Word</Application></Properties>`)
|
||||
zw.Close()
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func ExportReportPPTX(report reportContent) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
zw := zip.NewWriter(&buf)
|
||||
|
||||
layoutRefs := make([]string, 0)
|
||||
for i := 0; i < len(report.Chapters)+1; i++ {
|
||||
layoutRefs = append(layoutRefs, "rId"+fmt.Sprint(i+1))
|
||||
}
|
||||
slideRels := strings.Join(layoutRefs, "")
|
||||
|
||||
zipWriteFile(zw, "[Content_Types].xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="xml" ContentType="application/xml"/><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"/><Override PartName="/ppt/slides/slide1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/><Override PartName="/ppt/slideLayouts/slideLayout1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"/></Types>`)
|
||||
zipWriteFile(zw, "_rels/.rels", `<?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="ppt/presentation.xml"/></Relationships>`)
|
||||
|
||||
preXML := `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
|
||||
`<p:presentation xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" changedTime="` + timeStr() + `">` +
|
||||
`<p:sldMasterId href="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster"/>` +
|
||||
`<p:sldIds>` + slideRels + `</p:sldIds></p:presentation>`
|
||||
zipWriteFile(zw, "ppt/presentation.xml", preXML)
|
||||
|
||||
zipWriteFile(zw, "ppt/_rels/presentation.xml.rels", `<?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/slideMaster" Target="slideMaster/slideMaster1.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayouts" Target="slideLayouts/slideLayout1.xml"/><Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="theme/theme1.xml"/></Relationships>`)
|
||||
|
||||
titleSlide := buildSlideTitle(report.Topic, report.Summary)
|
||||
zipWriteFile(zw, "ppt/slides/slide1.xml", titleSlide)
|
||||
zipWriteFile(zw, "ppt/slides/_rels/slide1.xml.rels", `<?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/slideLayout" Target="../slideLayouts/slideLayout1.xml"/></Relationships>`)
|
||||
|
||||
for i, ch := range report.Chapters {
|
||||
slideNum := i + 2
|
||||
slidePath := fmt.Sprintf("ppt/slides/slide%d.xml", slideNum)
|
||||
relnPath := fmt.Sprintf("ppt/slides/_rels/slide%d.xml.rels", slideNum)
|
||||
zipWriteFile(zw, slidePath, buildSlideChapter(ch.Title, ch.Body))
|
||||
zipWriteFile(zw, relnPath, `<?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/slideLayout" Target="../slideLayouts/slideLayout1.xml"/></Relationships>`)
|
||||
}
|
||||
|
||||
zipWriteFile(zw, "ppt/slideLayouts/slideLayout1.xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><p:sldLayout xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" type="title"><p:cSld><p:spTree><p:sp><p:nvSpPr><p:cNvPr id="1" name="标题"/><p:cNvSpPr/></p:nvSpPr><p:spPr><p:xfrm><p:off x="914400" y="228600"/><p:ext cx="7315200" cy="1219200"/></p:xfrm></p:spPr><p:txBody><a:bodyPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"/><a:style><a:lnSz w="914400" h="914400"/><a:effectLst/><a:spPr/><a:prstGeom prst="rect"/><a:fld/><a:p><a:pPr alignment="ctr"/><a:r><a:rPr lang="zh-CN" sz="4400" dirty="0"/><a:t>标题</a:t></a:r><a:endParaRPr lang="zh-CN" sz="4400" dirty="0"/></a:p></a:txBody></p:sp><p:sp><p:nvSpPr><p:cNvPr id="2" name="内容"/><p:cNvSpPr/></p:nvSpPr><p:spPr><p:xfrm><p:off x="457200" y="1524000"/><p:ext cx="6400800" cy="5334000"/></p:xfrm></p:spPr><p:txBody><a:bodyPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" wrap="square" anchor="ctr"/><a:style><a:lnSz w="76200" h="76200"/><a:effectLst/><a:spPr/><a:prstGeom prst="rect"/><a:fld/><a:p><a:pPr><a:indent>-457200"/></a:pPr><a:r><a:rPr lang="zh-CN" sz="2400" dirty="0"/><a:t>内容</a:t></a:r><a:endParaRPr lang="zh-CN" sz="2400" dirty="0"/></a:p></a:txBody></p:sp></p:spTree></p:cSld></p:sldLayout>`)
|
||||
|
||||
zipWriteFile(zw, "ppt/theme/theme1.xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="Design Theme"><a:themeElements><a:clrScheme name="Office"><a:dk1><a:sysClr val="windowText" lastClr="000000"/></a:dk1><a:lt1><a:sysClr val="window" lastClr="FFFFFF"/></a:lt1><a:dk2><a:srgbClr val="1F497D"/></a:dk2><a:lt2><a:srgbClr val="E8E8E8"/></a:lt2><a:accent1><a:srgbClr val="4472C4"/></a:accent1><a:accent2><a:srgbClr val="ED7D31"/></a:accent2><a:accent3><a:srgbClr val="A5A5A5"/></a:accent3><a:accent4><a:srgbClr val="FFC000"/></a:accent4><a:accent5><a:srgbClr val="4472C4"/></a:accent5><a:accent6><a:srgbClr val="70AD47"/></a:accent6><a:hlink><a:srgbClr val="0563C1"/></a:hlink><a:folHlink><a:srgbClr val="9563C1"/></a:folHlink></a:clrScheme><a:fontScheme name="Office"><a:majorFont><a:latin fontFamily="Calibri" script="latn"/><a:ea script="hani"/><a:cs fontFamily="Cambria" script="latin"/></a:majorFont><a:minorFont><a:latin fontFamily="Calibri" script="latn"/><a:ea script="hani"/><a:cs fontFamily="Calibri" script="latin"/></a:minorFont></a:fontScheme><a:fmtScheme name="Office"/></a:themeElements></a:theme>`)
|
||||
|
||||
zipWriteFile(zw, "ppt/media/", "")
|
||||
zw.Close()
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func buildSlideTitle(title, summary string) string {
|
||||
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
|
||||
`<p:slide xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" p:sldId="` + timeStr() + `">` +
|
||||
`<p:cSld><p:spTree>` +
|
||||
`<p:sp><p:nvSpPr><p:cNvPr id="1" name="标题"/><p:cNvSpPr/></p:nvSpPr><p:spPr><p:xfrm><p:off x="914400" y="228600"/><p:ext cx="7315200" cy="1219200"/></p:xfrm></p:spPr>` +
|
||||
`<p:txBody><a:bodyPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" anchor="ctr"/><a:style><a:lnSz w="914400" h="914400"/><a:effectLst/><a:spPr/><a:prstGeom prst="rect"/><a:fld/><a:p><a:pPr alignment="ctr"/><a:r><a:rPr lang="zh-CN" sz="4400" dirty="0"/><a:rPr><a:spcBk><a:spcPct val="100000"/></a:spcBk></a:rPr><a:b/><a:t>` + escapeXML(title) + `</a:t></a:r><a:endParaRPr lang="zh-CN" sz="4400" dirty="0"/></a:p></a:txBody></p:sp>` +
|
||||
`<p:sp><p:nvSpPr><p:cNvPr id="2" name="摘要"/><p:cNvSpPr/></p:nvSpPr><p:spPr><p:xfrm><p:off x="457200" y="1524000"/><p:ext cx="6400800" cy="5334000"/></p:xfrm></p:spPr>` +
|
||||
`<p:txBody><a:bodyPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" wrap="square" anchor="ctr"/><a:style><a:lnSz w="76200" h="76200"/><a:effectLst/><a:spPr/><a:prstGeom prst="rect"/><a:fld/><a:p><a:pPr><a:indent>-457200"/></a:pPr><a:r><a:rPr lang="zh-CN" sz="2400" dirty="0"/><a:rPr><a:spcBk><a:spcPct val="100000"/></a:spcBk></a:rPr><a:t>` + escapeXML(summary) + `</a:t></a:r><a:endParaRPr lang="zh-CN" sz="2400" dirty="0"/></a:p></a:txBody></p:sp>` +
|
||||
`</p:spTree></p:cSld></p:slide>`
|
||||
}
|
||||
|
||||
func buildSlideChapter(title, body string) string {
|
||||
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
|
||||
`<p:slide xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" p:sldId="` + timeStr() + `">` +
|
||||
`<p:cSld><p:spTree>` +
|
||||
`<p:sp><p:nvSpPr><p:cNvPr id="1" name="标题"/><p:cNvSpPr/></p:nvSpPr><p:spPr><p:xfrm><p:off x="914400" y="228600"/><p:ext cx="7315200" cy="914400"/></p:xfrm></p:spPr>` +
|
||||
`<p:txBody><a:bodyPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" anchor="ctr"/><a:style><a:lnSz w="914400" h="914400"/><a:effectLst/><a:spPr/><a:prstGeom prst="rect"/><a:fld/><a:p><a:pPr/><a:r><a:rPr lang="zh-CN" sz="3200" dirty="0"/><a:rPr><a:spcBk><a:spcPct val="100000"/></a:spcBk></a:rPr><a:b/><a:t>` + escapeXML(title) + `</a:t></a:r><a:endParaRPr lang="zh-CN" sz="3200" dirty="0"/></a:p></a:txBody></p:sp>` +
|
||||
`<p:sp><p:nvSpPr><p:cNvPr id="2" name="内容"/><p:cNvSpPr/></p:nvSpPr><p:spPr><p:xfrm><p:off x="457200" y="1219200"/><p:ext cx="6400800" cy="5791200"/></p:xfrm></p:spPr>` +
|
||||
`<p:txBody><a:bodyPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" wrap="square" anchor="ctr"/><a:style><a:lnSz w="76200" h="76200"/><a:effectLst/><a:spPr/><a:prstGeom prst="rect"/><a:fld/><a:p><a:pPr><a:indent>-457200"/></a:pPr><a:r><a:rPr lang="zh-CN" sz="2000" dirty="0"/><a:rPr><a:spcBk><a:spcPct val="100000"/></a:spcBk></a:rPr><a:t>` + escapeXML(body) + `</a:t></a:r><a:endParaRPr lang="zh-CN" sz="2000" dirty="0"/></a:p></a:txBody></p:sp>` +
|
||||
`</p:spTree></p:cSld></p:slide>`
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func ExportReportXLSX(report reportContent) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
zw := zip.NewWriter(&buf)
|
||||
ss := []string{}
|
||||
addSS := func(s string) int {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return len(ss)
|
||||
}
|
||||
ss = append(ss, s)
|
||||
return len(ss) - 1
|
||||
}
|
||||
addSS(report.Topic)
|
||||
addSS(report.Summary)
|
||||
for _, ch := range report.Chapters {
|
||||
addSS(ch.Title)
|
||||
addSS(ch.Body)
|
||||
}
|
||||
zipWriteFile(zw, "[Content_Types].xml", `<?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.worksheet.main+xml"/><Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/><Override PartName="/xl/sharedStrings.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"/></Types>`)
|
||||
zipWriteFile(zw, "_rels/.rels", `<?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>`)
|
||||
zipWriteFile(zw, "xl/workbook.xml", `<?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"><workbookPr date1904="false"/><bookViews><workbookView xWindow="0" yWindow="0" windowWidth="25600" windowHeight="14400"/></bookViews><sheets><sheet name="报告" sheetId="1" r:id="rId1"/></sheets></workbook>`)
|
||||
zipWriteFile(zw, "xl/_rels/workbook.xml.rels", `<?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/worksheet" Target="worksheets/sheet1.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings" Target="sharedStrings.xml"/></Relationships>`)
|
||||
var sb strings.Builder
|
||||
sb.WriteString(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="` + fmt.Sprint(len(ss)) + `" uniqueCount="` + fmt.Sprint(len(ss)) + `">`)
|
||||
for _, s := range ss {
|
||||
sb.WriteString(`<si><t>` + escapeXML(s) + `</t></si>`)
|
||||
}
|
||||
sb.WriteString(`</sst>`)
|
||||
zipWriteFile(zw, "xl/sharedStrings.xml", sb.String())
|
||||
var sb2 strings.Builder
|
||||
sb2.WriteString(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData>`)
|
||||
row := 1
|
||||
sb2.WriteString(`<row r="` + fmt.Sprint(row) + `"><c r="A1" t="inlineStr"><is><t>` + escapeXML(report.Topic) + `</t></is></c></row>`)
|
||||
row = 2
|
||||
sb2.WriteString(`<row r="` + fmt.Sprint(row) + `"><c r="A2" t="inlineStr"><is><t>摘要</t></is></c><c r="B2" t="inlineStr"><is><t>` + escapeXML(report.Summary) + `</t></is></c></row>`)
|
||||
row = 3
|
||||
sb2.WriteString(`<row r="` + fmt.Sprint(row) + `"><c r="A3" t="inlineStr"><is><t>章节</t></is></c><c r="B3" t="inlineStr"><is><t>内容</t></is></c></row>`)
|
||||
for _, ch := range report.Chapters {
|
||||
row++
|
||||
sb2.WriteString(`<row r="` + fmt.Sprint(row) + `"><c r="A` + fmt.Sprint(row) + `" t="inlineStr"><is><t>` + escapeXML(ch.Title) + `</t></is></c><c r="B` + fmt.Sprint(row) + `" t="inlineStr"><is><t>` + escapeXML(ch.Body) + `</t></is></c></row>`)
|
||||
}
|
||||
sb2.WriteString(`</sheetData></worksheet>`)
|
||||
zipWriteFile(zw, "xl/worksheets/sheet1.xml", sb2.String())
|
||||
zw.Close()
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
@@ -393,3 +393,75 @@ func promptReportPreview(req ReportGenRequest) reportContent {
|
||||
CreatedAt: time.Now().Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
}
|
||||
|
||||
// ReportExportXLSX POST /api/report/export/xlsx —— 导出报告为 Excel
|
||||
func ReportExportXLSX(c *gin.Context) {
|
||||
user := middleware.CurrentUser(c)
|
||||
if user == nil {
|
||||
web.Fail(c, web.NewAuthError("未登录"))
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
Report reportContent `json:"report"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil || req.Report.Topic == "" {
|
||||
web.Fail(c, web.NewBadRequest("report 不能为空"))
|
||||
return
|
||||
}
|
||||
data, err := ExportReportXLSX(req.Report)
|
||||
if err != nil {
|
||||
web.Fail(c, web.NewBadRequest("Excel 生成失败: "+err.Error()))
|
||||
return
|
||||
}
|
||||
c.Header("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
|
||||
c.Header("Content-Disposition", "attachment; filename=\""+strings.ReplaceAll(req.Report.Topic, " ", "_")+".xlsx\"")
|
||||
c.Data(200, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", data)
|
||||
}
|
||||
|
||||
// ReportExportDOCX POST /api/report/export/docx —— 导出报告为 Word
|
||||
func ReportExportDOCX(c *gin.Context) {
|
||||
user := middleware.CurrentUser(c)
|
||||
if user == nil {
|
||||
web.Fail(c, web.NewAuthError("未登录"))
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
Report reportContent `json:"report"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil || req.Report.Topic == "" {
|
||||
web.Fail(c, web.NewBadRequest("report 不能为空"))
|
||||
return
|
||||
}
|
||||
data, err := ExportReportDOCX(req.Report)
|
||||
if err != nil {
|
||||
web.Fail(c, web.NewBadRequest("Word 生成失败: "+err.Error()))
|
||||
return
|
||||
}
|
||||
c.Header("Content-Type", "application/vnd.openxmlformats-officedocument.wordprocessingml.document")
|
||||
c.Header("Content-Disposition", "attachment; filename=\""+strings.ReplaceAll(req.Report.Topic, " ", "_")+".docx\"")
|
||||
c.Data(200, "application/vnd.openxmlformats-officedocument.wordprocessingml.document", data)
|
||||
}
|
||||
|
||||
// ReportExportPPTX POST /api/report/export/pptx —— 导出报告为 PPTX
|
||||
func ReportExportPPTX(c *gin.Context) {
|
||||
user := middleware.CurrentUser(c)
|
||||
if user == nil {
|
||||
web.Fail(c, web.NewAuthError("未登录"))
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
Report reportContent `json:"report"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil || req.Report.Topic == "" {
|
||||
web.Fail(c, web.NewBadRequest("report 不能为空"))
|
||||
return
|
||||
}
|
||||
data, err := ExportReportPPTX(req.Report)
|
||||
if err != nil {
|
||||
web.Fail(c, web.NewBadRequest("PPTX 生成失败: "+err.Error()))
|
||||
return
|
||||
}
|
||||
c.Header("Content-Type", "application/vnd.openxmlformats-officedocument.presentationml.presentation")
|
||||
c.Header("Content-Disposition", "attachment; filename=\""+strings.ReplaceAll(req.Report.Topic, " ", "_")+".pptx\"")
|
||||
c.Data(200, "application/vnd.openxmlformats-officedocument.presentationml.presentation", data)
|
||||
}
|
||||
|
||||
@@ -121,6 +121,9 @@ func RegisterRoutes(r *gin.Engine, cfg *config.Config) {
|
||||
// ── 报告生成(普通员工可访问)──
|
||||
r.POST("/api/report/generate", middleware.Auth(cfg), ReportGenerate)
|
||||
r.POST("/api/report/export/pdf", middleware.Auth(cfg), ReportExportPDF)
|
||||
r.POST("/api/report/export/docx", middleware.Auth(cfg), ReportExportDOCX)
|
||||
r.POST("/api/report/export/pptx", middleware.Auth(cfg), ReportExportPPTX)
|
||||
r.POST("/api/report/export/xlsx", middleware.Auth(cfg), ReportExportXLSX)
|
||||
r.POST("/api/report/preview", middleware.Auth(cfg), ReportPreview)
|
||||
|
||||
// 管理员维护
|
||||
|
||||
@@ -8,6 +8,18 @@ export function exportReportPDF(data) {
|
||||
return http.post('/report/export/pdf', data, { responseType: 'blob' })
|
||||
}
|
||||
|
||||
export function exportReportDOCX(data) {
|
||||
return http.post('/report/export/docx', data, { responseType: 'blob' })
|
||||
}
|
||||
|
||||
export function exportReportPPTX(data) {
|
||||
return http.post('/report/export/pptx', data, { responseType: 'blob' })
|
||||
}
|
||||
|
||||
export function exportReportXLSX(data) {
|
||||
return http.post('/report/export/xlsx', data, { responseType: 'blob' })
|
||||
}
|
||||
|
||||
export function previewReport(data) {
|
||||
return http.post('/report/preview', data)
|
||||
}
|
||||
|
||||
@@ -97,10 +97,20 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-actions" v-if="reportContent">
|
||||
<el-button size="small" :loading="exporting" @click="exportToPDF">
|
||||
<el-dropdown trigger="click" @command="exportReport">
|
||||
<el-button size="small" :loading="exporting">
|
||||
<el-icon><Download /></el-icon>
|
||||
导出 PDF
|
||||
导出 <el-icon><ArrowDown /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="pdf"><el-icon><Document /></el-icon> PDF 文档</el-dropdown-item>
|
||||
<el-dropdown-item command="docx"><el-icon><Reading /></el-icon> Word 文档</el-dropdown-item>
|
||||
<el-dropdown-item command="pptx"><el-icon><Grid /></el-icon> PPT 演示文稿</el-dropdown-item>
|
||||
<el-dropdown-item command="xlsx"><el-icon><Reading /></el-icon> Excel 表格</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-button size="small" @click="resetReport">
|
||||
清除
|
||||
</el-button>
|
||||
@@ -148,8 +158,8 @@
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Edit, Download } from '@element-plus/icons-vue'
|
||||
import { generateReport, exportReportPDF } from '@/api/report'
|
||||
import { Edit, Download, Document, Reading, ArrowDown, Grid } from '@element-plus/icons-vue'
|
||||
import { generateReport as apiGenerateReport, exportReportPDF, exportReportDOCX, exportReportPPTX, exportReportXLSX } from '@/api/report'
|
||||
import { listKnowledgeSpaces } from '@/api/knowledge'
|
||||
|
||||
const generating = ref(false)
|
||||
@@ -173,7 +183,7 @@ async function generateReport() {
|
||||
|
||||
generating.value = true
|
||||
try {
|
||||
const res = await generateReport({
|
||||
const res = await apiGenerateReport({
|
||||
topic: form.value.topic.trim(),
|
||||
summary: form.value.summary,
|
||||
sections: form.value.sections,
|
||||
@@ -197,28 +207,44 @@ async function generateReport() {
|
||||
}
|
||||
}
|
||||
|
||||
async function exportToPDF() {
|
||||
async function exportReport(command) {
|
||||
if (!reportContent.value) return
|
||||
|
||||
exporting.value = true
|
||||
try {
|
||||
const blob = await exportReportPDF({
|
||||
report: reportContent.value,
|
||||
})
|
||||
|
||||
// 下载 PDF
|
||||
const formatNames = { pdf: 'PDF', docx: 'Word', pptx: 'PPT', xlsx: 'Excel' }
|
||||
const exts = { pdf: '.pdf', docx: '.docx', pptx: '.pptx', xlsx: '.xlsx' }
|
||||
const formatName = formatNames[command] || '文件'
|
||||
const ext = exts[command] || '.pdf'
|
||||
let blob
|
||||
switch (command) {
|
||||
case 'pdf':
|
||||
blob = await exportReportPDF({ report: reportContent.value })
|
||||
break
|
||||
case 'docx':
|
||||
blob = await exportReportDOCX({ report: reportContent.value })
|
||||
break
|
||||
case 'pptx':
|
||||
blob = await exportReportPPTX({ report: reportContent.value })
|
||||
break
|
||||
case 'xlsx':
|
||||
blob = await exportReportXLSX({ report: reportContent.value })
|
||||
break
|
||||
default:
|
||||
return
|
||||
}
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
const filename = (reportContent.value.topic || 'report') + '.pdf'
|
||||
a.download = filename.replace(/[^a-zA-Z一-龥 -]/g, '_')
|
||||
const filename = (reportContent.value.topic || 'report') + ext
|
||||
a.download = filename.replace(/[^a-zA-Z一-龥龥 -]/g, '_')
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
ElMessage.success('PDF 已下载')
|
||||
ElMessage.success(`${formatName} 已下载`)
|
||||
} catch (error) {
|
||||
ElMessage.error('PDF 导出失败')
|
||||
ElMessage.error(`${formatName} 导出失败`)
|
||||
} finally {
|
||||
exporting.value = false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user