diff --git a/eai_ap_app/backend-go/internal/api/export_common.go b/eai_ap_app/backend-go/internal/api/export_common.go
new file mode 100644
index 0000000..0525cca
--- /dev/null
+++ b/eai_ap_app/backend-go/internal/api/export_common.go
@@ -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"
+}
diff --git a/eai_ap_app/backend-go/internal/api/export_docx.go b/eai_ap_app/backend-go/internal/api/export_docx.go
new file mode 100644
index 0000000..26098ff
--- /dev/null
+++ b/eai_ap_app/backend-go/internal/api/export_docx.go
@@ -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", ``)
+ zipWriteFile(zw, "_rels/.rels", ``)
+ var doc strings.Builder
+ doc.WriteString(`` + ``)
+ doc.WriteString(`` + escapeXML(report.Topic) + ``)
+ doc.WriteString(`摘要`)
+ doc.WriteString(`` + escapeXML(report.Summary) + ``)
+ for _, ch := range report.Chapters {
+ doc.WriteString(`` + escapeXML(ch.Title) + ``)
+ doc.WriteString(`` + escapeXML(ch.Body) + ``)
+ }
+ doc.WriteString(``)
+ zipWriteFile(zw, "word/document.xml", doc.String())
+ zipWriteFile(zw, "word/styles.xml", ``)
+ zipWriteFile(zw, "word/_rels/document.xml.rels", ``)
+ zipWriteFile(zw, "docProps/core.xml", `` + escapeXML(report.Topic) + ``)
+ zipWriteFile(zw, "docProps/app.xml", `Microsoft Word`)
+ zw.Close()
+ return buf.Bytes(), nil
+}
diff --git a/eai_ap_app/backend-go/internal/api/export_pptx.go b/eai_ap_app/backend-go/internal/api/export_pptx.go
new file mode 100644
index 0000000..c5b235f
--- /dev/null
+++ b/eai_ap_app/backend-go/internal/api/export_pptx.go
@@ -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", ``)
+ zipWriteFile(zw, "_rels/.rels", ``)
+
+ preXML := `` +
+ `` +
+ `` +
+ `` + slideRels + ``
+ zipWriteFile(zw, "ppt/presentation.xml", preXML)
+
+ zipWriteFile(zw, "ppt/_rels/presentation.xml.rels", ``)
+
+ titleSlide := buildSlideTitle(report.Topic, report.Summary)
+ zipWriteFile(zw, "ppt/slides/slide1.xml", titleSlide)
+ zipWriteFile(zw, "ppt/slides/_rels/slide1.xml.rels", ``)
+
+ 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, ``)
+ }
+
+ zipWriteFile(zw, "ppt/slideLayouts/slideLayout1.xml", `标题-457200"/>内容`)
+
+ zipWriteFile(zw, "ppt/theme/theme1.xml", ``)
+
+ zipWriteFile(zw, "ppt/media/", "")
+ zw.Close()
+ return buf.Bytes(), nil
+}
+
+func buildSlideTitle(title, summary string) string {
+ return `` +
+ `` +
+ `` +
+ `` +
+ `` + escapeXML(title) + `` +
+ `` +
+ `-457200"/>` + escapeXML(summary) + `` +
+ ``
+}
+
+func buildSlideChapter(title, body string) string {
+ return `` +
+ `` +
+ `` +
+ `` +
+ `` + escapeXML(title) + `` +
+ `` +
+ `-457200"/>` + escapeXML(body) + `` +
+ ``
+}
diff --git a/eai_ap_app/backend-go/internal/api/export_xlsx.go b/eai_ap_app/backend-go/internal/api/export_xlsx.go
new file mode 100644
index 0000000..88d614d
--- /dev/null
+++ b/eai_ap_app/backend-go/internal/api/export_xlsx.go
@@ -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", ``)
+ zipWriteFile(zw, "_rels/.rels", ``)
+ zipWriteFile(zw, "xl/workbook.xml", ``)
+ zipWriteFile(zw, "xl/_rels/workbook.xml.rels", ``)
+ var sb strings.Builder
+ sb.WriteString(``)
+ for _, s := range ss {
+ sb.WriteString(`` + escapeXML(s) + ``)
+ }
+ sb.WriteString(``)
+ zipWriteFile(zw, "xl/sharedStrings.xml", sb.String())
+ var sb2 strings.Builder
+ sb2.WriteString(``)
+ row := 1
+ sb2.WriteString(`` + escapeXML(report.Topic) + `
`)
+ row = 2
+ sb2.WriteString(`摘要` + escapeXML(report.Summary) + `
`)
+ row = 3
+ sb2.WriteString(`章节内容
`)
+ for _, ch := range report.Chapters {
+ row++
+ sb2.WriteString(`` + escapeXML(ch.Title) + `` + escapeXML(ch.Body) + `
`)
+ }
+ sb2.WriteString(``)
+ zipWriteFile(zw, "xl/worksheets/sheet1.xml", sb2.String())
+ zw.Close()
+ return buf.Bytes(), nil
+}
diff --git a/eai_ap_app/backend-go/internal/api/report_gen.go b/eai_ap_app/backend-go/internal/api/report_gen.go
index eb73eec..a70b79c 100644
--- a/eai_ap_app/backend-go/internal/api/report_gen.go
+++ b/eai_ap_app/backend-go/internal/api/report_gen.go
@@ -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)
+}
diff --git a/eai_ap_app/backend-go/internal/api/router.go b/eai_ap_app/backend-go/internal/api/router.go
index 2ab85df..841e0dd 100644
--- a/eai_ap_app/backend-go/internal/api/router.go
+++ b/eai_ap_app/backend-go/internal/api/router.go
@@ -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)
// 管理员维护
diff --git a/eai_ap_app/frontend/src/api/report.js b/eai_ap_app/frontend/src/api/report.js
index 196ac35..abef084 100644
--- a/eai_ap_app/frontend/src/api/report.js
+++ b/eai_ap_app/frontend/src/api/report.js
@@ -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)
}
diff --git a/eai_ap_app/frontend/src/views/report/ReportGenerationPage.vue b/eai_ap_app/frontend/src/views/report/ReportGenerationPage.vue
index 812b547..3586c09 100644
--- a/eai_ap_app/frontend/src/views/report/ReportGenerationPage.vue
+++ b/eai_ap_app/frontend/src/views/report/ReportGenerationPage.vue
@@ -97,10 +97,20 @@
-
-
- 导出 PDF
-
+
+
+
+ 导出
+
+
+
+ PDF 文档
+ Word 文档
+ PPT 演示文稿
+ Excel 表格
+
+
+
清除
@@ -148,8 +158,8 @@