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 - + + + + 导出 + + + 清除 @@ -148,8 +158,8 @@