- fp_calc.calc()返回键实锤核对: total_fp/counts/errors(原total_ufp/by_type兜底猜错,已修) - promote门禁修正: resolve_approval会把status置approved,confirmed只是中间态→门禁接受confirmed/approval_initiated/approved(原只认confirmed会导致审批通过后反而不能立项) - _cluster_ctx补项目隔离(对齐_check_batch_visible口径)+全personal类别如实拒绝可行性样本不足 - 八节模板兜底节=需求规格;四节存量解析口径零变化 - opp_approvals CRUD手改(项目过滤)被xls2ui重生成吃掉→已checkout还原,只保留opp_reports新列部分
129 lines
5.6 KiB
Plaintext
129 lines
5.6 KiB
Plaintext
# 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,
|
||
sections_for_type)
|
||
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": "已驳回",
|
||
}
|
||
|
||
TYPE_TXT = {"research": "调研", "feasibility": "可行性研究"}
|
||
|
||
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 "")
|
||
rt = str(rep.get("report_type") or "research")
|
||
proj = str(rep.get("project_name") or "")
|
||
meta = "%s | %s | %s | %s | 创建人 %s" % (
|
||
str(rep.get("created_at") or "")[:16],
|
||
("项目:" + proj) if proj else "平台级调研(未挂项目)",
|
||
TYPE_TXT.get(rt, rt or "调研"),
|
||
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"}})
|
||
|
||
# ── 分节渲染:research 四节 / feasibility 八节(按 report_type 选模板;
|
||
# 空节如实标注,不编造)──
|
||
sections = parse_report_sections(str(rep.get("content") or ""), rt)
|
||
for name in sections_for_type(rt):
|
||
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": {
|
||
"otext": "(本报告未填写此节)", "text": "(本报告未填写此节)", "i18n": True, "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": {
|
||
"otext": "暂无成果文件(报告提交人工确认时会自动生成 PPT 存入项目 deliverables/)", "text": "暂无成果文件(报告提交人工确认时会自动生成 PPT 存入项目 deliverables/)", "i18n": True,
|
||
"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
|
||
}]
|
||
}
|