# opp_report_detail.dspy - 研发报告详情弹窗(2026-09-09 用户要求) # 入参:report_id。展示四节(研发场景/输入/输出/成果,MdWidget 渲染)+ 成果文件列表(逐个下载)。 # 权限:check_report_visible(与列表同一可见性规则——owner 过滤语义不漂移)。 # 文件下载走 opp_report_file_dl.dspy(只传 report_id+文件名,服务端回查清单命中才给文件,防路径穿越)。 import json as _json from urllib.parse import quote as _quote uid = await get_user() if not uid: return {"widgettype": "Message", "options": {"title": "未登录", "message": "请先登录"}} report_id = ((params_kw or {}).get("report_id") or "").strip() if not report_id: return {"widgettype": "Message", "options": {"title": "缺少参数", "message": "缺少 report_id"}} dbname = get_module_dbname('pipeline-opportunity') async with DBPools().sqlorContext(dbname) as sor: from pipeline_opportunity.opp_report_capability import ( check_report_visible, parse_report_sections, list_report_files, REPORT_SECTIONS) visible, rep = await check_report_visible(sor, report_id, uid) await sor.sqlExe("COMMIT", {}) if not visible: return {"widgettype": "Message", "options": { "title": "无权查看", "message": "报告不存在,或你不是该报告所属项目的 owner"}} files = await list_report_files(sor, rep) dl_url = entire_url("/pipeline-opportunity/api/opp_report_file_dl.dspy") STATUS_TXT = { "draft": "草稿", "confirmed": "已确认", "approval_initiated": "审批中", "approved": "审批通过", "rejected": "已驳回", } body = [] # ── 标题 ── body.append({"widgettype": "Text", "options": { "text": str(rep.get("title") or rep.get("software") or "研发报告"), "cfontsize": 1.15, "color": "#0f172a", "halign": "left", "wrap": True, "margin": "0 0 6px 0"}}) # ── 元信息 ── st = str(rep.get("status") or "") proj = str(rep.get("project_name") or "") meta = "%s | %s | %s | 创建人 %s" % ( str(rep.get("created_at") or "")[:16], ("项目:" + proj) if proj else "平台级调研(未挂项目)", STATUS_TXT.get(st, st or "—"), str(rep.get("created_by") or "—")) body.append({"widgettype": "Text", "options": { "text": meta, "cfontsize": 0.78, "color": "#64748b", "halign": "left", "wrap": True, "margin": "0 0 10px 0"}}) # ── 四节:研发场景 / 输入 / 输出 / 成果(按节解析正文;空节如实标注,不编造)── sections = parse_report_sections(str(rep.get("content") or "")) for name in REPORT_SECTIONS: md = (sections.get(name) or "").strip() body.append({"widgettype": "Text", "options": { "text": name, "cfontsize": 0.95, "color": "#0f172a", "halign": "left", "margin": "0 0 4px 0"}}) if md: shown = md[:20000] if len(md) > 20000: shown += "\n\n……(本节 %d 字,已截断显示)" % len(md) body.append({"widgettype": "MdWidget", "options": { "mdtext": shown, "width": "100%", "padding": "8px 12px", "border": "1px solid #e2e8f0", "borderRadius": "8px", "margin": "0 0 10px 0", "bgcolor": "#ffffff"}}) else: body.append({"widgettype": "Text", "options": { "text": "(本报告未填写此节)", "cfontsize": 0.82, "color": "#94a3b8", "halign": "left", "margin": "0 0 10px 0"}}) # ── 成果文件(多个列出,逐个下载)── body.append({"widgettype": "Text", "options": { "text": "成果文件(%d)" % len(files), "cfontsize": 0.95, "color": "#0f172a", "halign": "left", "margin": "0 0 4px 0"}}) if not files: body.append({"widgettype": "Text", "options": { "text": "暂无成果文件(报告提交人工确认时会自动生成 PPT 存入项目 deliverables/)", "cfontsize": 0.82, "color": "#94a3b8", "halign": "left", "margin": "0 0 10px 0"}}) for f in files: size = int(f.get("size") or 0) if size >= 1048576: size_txt = "%.1f MB" % (size / 1048576.0) elif size > 0: size_txt = "%.0f KB" % (size / 1024.0) else: size_txt = "" # 下载:只传 report_id + 文件名,服务端回查清单命中才返回文件(防路径穿越) href = dl_url + "?report_id=" + _quote(report_id) + "&name=" + _quote(str(f.get("name") or "")) body.append({ "widgettype": "HBox", "options": {"width": "100%", "gap": "10px", "alignItems": "center", "padding": "6px 12px", "border": "1px solid #e2e8f0", "borderRadius": "8px", "margin": "0 0 6px 0", "bgcolor": "#ffffff"}, "subwidgets": [ {"widgettype": "Text", "options": {"text": "📄 " + str(f.get("name") or "文件"), "cfontsize": 0.88, "color": "#0f172a", "halign": "left", "wrap": True}}, {"widgettype": "Text", "options": {"text": size_txt, "cfontsize": 0.75, "color": "#94a3b8"}}, {"widgettype": "Filler"}, {"widgettype": "Button", "options": {"label": "下载", "css": "small"}, "binds": [{"wid": "self", "event": "click", "actiontype": "script", "target": "self", "script": "window.location.href=" + _json.dumps(href) + ";"}]}] }) return { "widgettype": "PopupWindow", "id": "opp_report_detail_pw", "options": {"title": "研发报告详情", "width": "72%", "height": "86%", "auto_open": True, "resizable": True}, "subwidgets": [{ "widgettype": "VBox", "options": {"css": "filler", "width": "100%", "height": "100%", "padding": "12px 16px", "gap": "0px", "overflow": "auto"}, "subwidgets": body }] }