f
This commit is contained in:
18
Dockerfile
18
Dockerfile
@@ -37,12 +37,26 @@ FROM alpine:3.19
|
||||
# 设置Alpine镜像源
|
||||
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
|
||||
|
||||
# 安装必要的包
|
||||
RUN apk --no-cache add tzdata curl
|
||||
# 安装必要的包(包含 headless Chrome 所需依赖)
|
||||
# - tzdata: 时区
|
||||
# - curl: 健康检查
|
||||
# - chromium: 无头浏览器,用于 chromedp 生成 HTML 报告 PDF
|
||||
# - nss、freetype、harfbuzz、ttf-freefont: 字体及渲染依赖,避免中文/图标丢失
|
||||
RUN apk --no-cache add \
|
||||
tzdata \
|
||||
curl \
|
||||
chromium \
|
||||
nss \
|
||||
freetype \
|
||||
harfbuzz \
|
||||
ttf-freefont
|
||||
|
||||
# 设置时区
|
||||
ENV TZ=Asia/Shanghai
|
||||
|
||||
# 为 chromedp 指定默认的 Chrome 路径(Alpine 下 chromium 包的可执行文件)
|
||||
ENV CHROME_BIN=/usr/bin/chromium-browser
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
7
go.mod
7
go.mod
@@ -58,6 +58,9 @@ require (
|
||||
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/chromedp/cdproto v0.0.0-20250319231242-a755498943c8 // indirect
|
||||
github.com/chromedp/chromedp v0.13.2 // indirect
|
||||
github.com/chromedp/sysutil v1.1.0 // indirect
|
||||
github.com/clbanning/mxj/v2 v2.7.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
@@ -66,6 +69,7 @@ require (
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||
github.com/gammazero/toposort v0.1.1 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
@@ -73,6 +77,9 @@ require (
|
||||
github.com/go-openapi/spec v0.20.4 // indirect
|
||||
github.com/go-openapi/swag v0.19.15 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/gobwas/ws v1.4.0 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/gofrs/flock v0.8.1 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
|
||||
|
||||
14
go.sum
14
go.sum
@@ -80,6 +80,12 @@ github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F9
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chromedp/cdproto v0.0.0-20250319231242-a755498943c8 h1:AqW2bDQf67Zbq6Tpop/+yJSIknxhiQecO2B8jNYTAPs=
|
||||
github.com/chromedp/cdproto v0.0.0-20250319231242-a755498943c8/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k=
|
||||
github.com/chromedp/chromedp v0.13.2 h1:f6sZFFzCzPLvWSzeuXQBgONKG7zPq54YfEyEj0EplOY=
|
||||
github.com/chromedp/chromedp v0.13.2/go.mod h1:khsDP9OP20GrowpJfZ7N05iGCwcAYxk7qf9AZBzR3Qw=
|
||||
github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM=
|
||||
github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=
|
||||
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
|
||||
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
@@ -115,6 +121,8 @@ github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fq
|
||||
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
||||
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535 h1:yE7argOs92u+sSCRgqqe6eF+cDaVhSPlioy1UkA0p/w=
|
||||
github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535/go.mod h1:BWmvoE1Xia34f3l/ibJweyhrT+aROb/FQ6d+37F0e2s=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
@@ -146,6 +154,12 @@ github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc
|
||||
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
||||
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
|
||||
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
|
||||
|
||||
@@ -2,8 +2,10 @@ package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
@@ -14,6 +16,7 @@ import (
|
||||
api_services "tyapi-server/internal/domains/api/services"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/domains/api/services/processors/qygl"
|
||||
"tyapi-server/internal/shared/pdf"
|
||||
)
|
||||
|
||||
// QYGLReportHandler 企业全景报告页面渲染处理器
|
||||
@@ -135,3 +138,59 @@ func (h *QYGLReportHandler) GetQYGLReportPageByID(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// GetQYGLReportPDFByID 通过编号导出企业全景报告 PDF(基于 headless Chrome 渲染 HTML)
|
||||
// GET /reports/qygl/:id/pdf
|
||||
func (h *QYGLReportHandler) GetQYGLReportPDFByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
c.String(http.StatusBadRequest, "报告编号不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
// 可选:从数据库查一次,用于生成更友好的文件名
|
||||
var fileName = "企业全景报告.pdf"
|
||||
if h.reportRepo != nil {
|
||||
if entity, err := h.reportRepo.FindByReportID(c.Request.Context(), id); err == nil && entity != nil {
|
||||
if entity.EntName != "" {
|
||||
fileName = fmt.Sprintf("%s_企业全景报告.pdf", entity.EntName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 根据当前请求推断访问协议(支持通过反向代理的 X-Forwarded-Proto)
|
||||
scheme := "http"
|
||||
if c.Request.TLS != nil {
|
||||
scheme = "https"
|
||||
} else if forwardedProto := c.Request.Header.Get("X-Forwarded-Proto"); forwardedProto != "" {
|
||||
scheme = forwardedProto
|
||||
}
|
||||
|
||||
// 构建用于 headless 浏览器访问的完整报告页面 URL
|
||||
reportURL := fmt.Sprintf("%s://%s/reports/qygl/%s", scheme, c.Request.Host, id)
|
||||
|
||||
h.logger.Info("开始生成企业全景报告 PDF(headless Chrome)",
|
||||
zap.String("report_id", id),
|
||||
zap.String("url", reportURL),
|
||||
)
|
||||
|
||||
pdfGen := pdf.NewHTMLPDFGenerator(h.logger)
|
||||
pdfBytes, err := pdfGen.GenerateFromURL(c.Request.Context(), reportURL)
|
||||
if err != nil {
|
||||
h.logger.Error("生成企业全景报告 PDF 失败", zap.String("report_id", id), zap.Error(err))
|
||||
c.String(http.StatusInternalServerError, "生成企业报告 PDF 失败,请稍后重试")
|
||||
return
|
||||
}
|
||||
|
||||
if len(pdfBytes) == 0 {
|
||||
h.logger.Error("生成的企业全景报告 PDF 为空", zap.String("report_id", id))
|
||||
c.String(http.StatusInternalServerError, "生成的企业报告 PDF 为空,请稍后重试")
|
||||
return
|
||||
}
|
||||
|
||||
encodedFileName := url.QueryEscape(fileName)
|
||||
c.Header("Content-Type", "application/pdf")
|
||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", encodedFileName))
|
||||
c.Data(http.StatusOK, "application/pdf", pdfBytes)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -28,5 +28,8 @@ func (r *QYGLReportRoutes) Register(router *sharedhttp.GinRouter) {
|
||||
|
||||
// 企业全景报告页面(通过编号查看)
|
||||
engine.GET("/reports/qygl/:id", r.handler.GetQYGLReportPageByID)
|
||||
|
||||
// 企业全景报告 PDF 导出(通过编号)
|
||||
engine.GET("/reports/qygl/:id/pdf", r.handler.GetQYGLReportPDFByID)
|
||||
}
|
||||
|
||||
|
||||
82
internal/shared/pdf/html_pdf_generator.go
Normal file
82
internal/shared/pdf/html_pdf_generator.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package pdf
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/chromedp/cdproto/page"
|
||||
"github.com/chromedp/chromedp"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// HTMLPDFGenerator 使用 headless Chrome 将 HTML 页面渲染为 PDF
|
||||
type HTMLPDFGenerator struct {
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewHTMLPDFGenerator 创建 HTMLPDFGenerator
|
||||
func NewHTMLPDFGenerator(logger *zap.Logger) *HTMLPDFGenerator {
|
||||
if logger == nil {
|
||||
logger = zap.NewNop()
|
||||
}
|
||||
return &HTMLPDFGenerator{
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateFromURL 使用 headless Chrome 打开指定 URL,并导出为 PDF 字节流
|
||||
// 这里固定使用 A4 纵向纸张,开启背景打印
|
||||
func (g *HTMLPDFGenerator) GenerateFromURL(ctx context.Context, url string) ([]byte, error) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
// 整个生成过程增加超时时间,避免长时间卡死
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 创建 Chrome 上下文(使用系统默认的 headless Chrome/Chromium)
|
||||
chromeCtx, cancelChrome := chromedp.NewContext(timeoutCtx)
|
||||
defer cancelChrome()
|
||||
|
||||
var pdfBuf []byte
|
||||
|
||||
tasks := chromedp.Tasks{
|
||||
chromedp.Navigate(url),
|
||||
// 等待页面主体和报告容器就绪,确保数据渲染完成
|
||||
chromedp.WaitReady("body", chromedp.ByQuery),
|
||||
chromedp.WaitVisible(".page", chromedp.ByQuery),
|
||||
chromedp.ActionFunc(func(ctx context.Context) error {
|
||||
g.logger.Info("开始通过 headless Chrome 生成企业报告 PDF", zap.String("url", url))
|
||||
var err error
|
||||
pdfBuf, err = page.PrintToPDF().
|
||||
WithPrintBackground(true).
|
||||
WithPaperWidth(8.27). // A4 宽度(英寸 -> 约 210mm)
|
||||
WithPaperHeight(11.69). // A4 高度(英寸 -> 约 297mm)
|
||||
WithMarginTop(0.4).
|
||||
WithMarginBottom(0.4).
|
||||
WithMarginLeft(0.4).
|
||||
WithMarginRight(0.4).
|
||||
Do(ctx)
|
||||
return err
|
||||
}),
|
||||
}
|
||||
|
||||
if err := chromedp.Run(chromeCtx, tasks); err != nil {
|
||||
g.logger.Error("使用 headless Chrome 生成 HTML 报告 PDF 失败", zap.String("url", url), zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(pdfBuf) == 0 {
|
||||
return nil, fmt.Errorf("生成的 PDF 内容为空")
|
||||
}
|
||||
|
||||
g.logger.Info("通过 headless Chrome 生成企业报告 PDF 成功",
|
||||
zap.String("url", url),
|
||||
zap.Int("pdf_size", len(pdfBuf)),
|
||||
)
|
||||
|
||||
return pdfBuf, nil
|
||||
}
|
||||
|
||||
@@ -2185,16 +2185,11 @@
|
||||
}
|
||||
loadReport();
|
||||
|
||||
// 绑定「保存为 PDF」按钮:html2canvas 截取 .page → 转 JPEG → jsPDF 分页生成 PDF(按钮在 .page 外固定悬浮,不会进 PDF)
|
||||
// 绑定「保存为 PDF」按钮:调用后端 /reports/qygl/:id/pdf 接口,由服务器端 headless Chrome 生成高质量 PDF
|
||||
var saveBtn = document.getElementById("btnSavePdf");
|
||||
var loadingOverlay = document.getElementById("pdfLoadingOverlay");
|
||||
if (saveBtn && window.html2canvas && window.jspdf && window.jspdf.jsPDF) {
|
||||
if (saveBtn) {
|
||||
saveBtn.addEventListener("click", function () {
|
||||
var pageEl = document.querySelector(".page");
|
||||
if (!pageEl) {
|
||||
console.error("未找到 .page 容器");
|
||||
return;
|
||||
}
|
||||
var btnText = saveBtn.textContent;
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.textContent = "生成中...";
|
||||
@@ -2208,135 +2203,68 @@
|
||||
loadingOverlay.setAttribute("aria-hidden", "true");
|
||||
}
|
||||
}
|
||||
// 等待浏览器完成布局与绘制,再截取,避免截到白屏
|
||||
function doCapture() {
|
||||
requestAnimationFrame(function () {
|
||||
requestAnimationFrame(function () {
|
||||
try {
|
||||
(function () {
|
||||
var scale = 1.25;
|
||||
var pdf = new jspdf.jsPDF("p", "mm", "a4");
|
||||
var pageWidthMm = pdf.internal.pageSize.getWidth();
|
||||
var pageHeightMm = pdf.internal.pageSize.getHeight();
|
||||
|
||||
// 以元素宽度为基准,计算每一页对应的像素高度(避免生成超长大图导致 jsPDF 白页)
|
||||
var targetWidthPx = Math.max(
|
||||
1,
|
||||
Math.floor(pageEl.scrollWidth),
|
||||
);
|
||||
var pageHeightPx = Math.max(
|
||||
1,
|
||||
Math.floor((targetWidthPx * pageHeightMm) / pageWidthMm),
|
||||
);
|
||||
var totalHeightPx = Math.max(
|
||||
1,
|
||||
Math.floor(pageEl.scrollHeight),
|
||||
);
|
||||
try {
|
||||
// 从当前 URL 中解析报告编号,路径形如 /reports/qygl/:id
|
||||
var path = window.location.pathname || "";
|
||||
var match = path.match(/\\/reports\\/qygl\\/([^/]+)/);
|
||||
if (!match || !match[1]) {
|
||||
console.error("无法从当前 URL 解析报告编号,路径为", path);
|
||||
restoreBtn();
|
||||
return;
|
||||
}
|
||||
var reportId = match[1];
|
||||
var pdfUrl = "/reports/qygl/" + encodeURIComponent(reportId) + "/pdf";
|
||||
|
||||
var offsetY = 0;
|
||||
var pageIndex = 0;
|
||||
|
||||
function renderSlice() {
|
||||
var sliceHeight = Math.min(
|
||||
pageHeightPx,
|
||||
totalHeightPx - offsetY,
|
||||
);
|
||||
if (sliceHeight <= 0) {
|
||||
// 结束,保存
|
||||
var fileName = "企业全景报告.pdf";
|
||||
if (
|
||||
reportData &&
|
||||
reportData.entName &&
|
||||
typeof reportData.entName === "string"
|
||||
) {
|
||||
fileName =
|
||||
reportData.entName +
|
||||
"_企业全景报告.pdf";
|
||||
}
|
||||
pdf.save(fileName);
|
||||
restoreBtn();
|
||||
return;
|
||||
}
|
||||
|
||||
html2canvas(pageEl, {
|
||||
scale: scale,
|
||||
useCORS: true,
|
||||
allowTaint: false,
|
||||
backgroundColor: "#ffffff",
|
||||
x: 0,
|
||||
y: offsetY,
|
||||
width: targetWidthPx,
|
||||
height: sliceHeight,
|
||||
scrollX: 0,
|
||||
scrollY: 0,
|
||||
windowWidth: targetWidthPx,
|
||||
windowHeight: sliceHeight,
|
||||
onclone: function (clonedDoc) {
|
||||
var body = clonedDoc.body;
|
||||
if (body) {
|
||||
body.style.backgroundColor = "#ffffff";
|
||||
body.style.overflow = "visible";
|
||||
}
|
||||
var clonePage = clonedDoc.querySelector(".page");
|
||||
if (clonePage) {
|
||||
clonePage.style.overflow = "visible";
|
||||
clonePage.style.backgroundColor = "#ffffff";
|
||||
}
|
||||
},
|
||||
})
|
||||
.then(function (canvas) {
|
||||
if (!canvas.width || !canvas.height) {
|
||||
console.error("分片截图为空,宽或高为 0", {
|
||||
offsetY: offsetY,
|
||||
sliceHeight: sliceHeight,
|
||||
});
|
||||
restoreBtn();
|
||||
return;
|
||||
}
|
||||
// 调试:输出每页分片信息
|
||||
console.info("PDF分片截图信息", {
|
||||
page: pageIndex + 1,
|
||||
w: canvas.width,
|
||||
h: canvas.height,
|
||||
offsetY: offsetY,
|
||||
sliceHeight: sliceHeight,
|
||||
totalHeightPx: totalHeightPx,
|
||||
});
|
||||
|
||||
var imgData = canvas.toDataURL("image/jpeg", 0.9);
|
||||
|
||||
if (pageIndex > 0) pdf.addPage();
|
||||
pdf.addImage(
|
||||
imgData,
|
||||
"JPEG",
|
||||
0,
|
||||
0,
|
||||
pageWidthMm,
|
||||
pageHeightMm,
|
||||
);
|
||||
|
||||
pageIndex += 1;
|
||||
offsetY += sliceHeight;
|
||||
|
||||
// 给 UI 一点喘息,避免长任务卡死
|
||||
setTimeout(renderSlice, 0);
|
||||
})
|
||||
.catch(function (e) {
|
||||
console.error("生成 PDF 失败(分片截图阶段)", e);
|
||||
restoreBtn();
|
||||
});
|
||||
}
|
||||
|
||||
renderSlice();
|
||||
})();
|
||||
} catch (e) {
|
||||
console.error("触发生成 PDF 失败", e);
|
||||
restoreBtn();
|
||||
// 通过 fetch 获取 PDF 二进制并触发下载
|
||||
fetch(pdfUrl, {
|
||||
method: "GET",
|
||||
})
|
||||
.then(function (resp) {
|
||||
if (!resp.ok) {
|
||||
throw new Error("生成 PDF 接口返回错误状态:" + resp.status);
|
||||
}
|
||||
var contentType = resp.headers.get("Content-Type") || "";
|
||||
if (
|
||||
contentType.indexOf("application/pdf") === -1 &&
|
||||
contentType.indexOf("application/octet-stream") === -1
|
||||
) {
|
||||
// 可能返回的是错误字符串
|
||||
return resp.text().then(function (txt) {
|
||||
throw new Error("生成 PDF 失败:" + txt);
|
||||
});
|
||||
}
|
||||
return resp.blob();
|
||||
})
|
||||
.then(function (blob) {
|
||||
var fileName = "企业全景报告.pdf";
|
||||
if (
|
||||
reportData &&
|
||||
reportData.entName &&
|
||||
typeof reportData.entName === "string"
|
||||
) {
|
||||
fileName =
|
||||
reportData.entName + "_企业全景报告.pdf";
|
||||
}
|
||||
var url = window.URL.createObjectURL(blob);
|
||||
var a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = fileName;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(url);
|
||||
restoreBtn();
|
||||
})
|
||||
.catch(function (e) {
|
||||
console.error("生成 PDF 失败(后端接口)", e);
|
||||
restoreBtn();
|
||||
alert("生成 PDF 失败,请稍后重试");
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("触发生成 PDF 失败", e);
|
||||
restoreBtn();
|
||||
}
|
||||
doCapture();
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user