# opp_item_detail.dspy - 单条数据项的具体内容弹窗(数据来自数据爬取平台) # 入参:id=<爬虫平台条目id>。展示公告/需求全量字段 + 公告全文 + 来源说明 + 附件列表。 # 附件点击下载:走 opp_attachment_dl.dspy 代理(平台副本优先,缺失回退原始地址)。 import json as _json uid = await get_user() if not uid: return {"widgettype": "Message", "options": {"title": "未登录", "message": "请先登录"}} item_id = str((params_kw or {}).get("id") or "").strip() if not item_id: return {"widgettype": "Message", "options": {"title": "缺少参数", "message": "缺少 id"}} dbname = get_module_dbname('pipeline-opportunity') async with DBPools().sqlorContext(dbname) as sor: from pipeline_opportunity.opp_data_capability import tender_detail, tender_attachments from pipeline_opportunity.opp_common import source_text ok, det = await tender_detail(sor, item_id) await sor.sqlExe("COMMIT", {}) att_ok, atts = await tender_attachments(sor, item_id) await sor.sqlExe("COMMIT", {}) dl_base = entire_url("/pipeline-opportunity/api/opp_attachment_dl.dspy") body = [] if not ok or not isinstance(det, dict): body.append({"widgettype": "Text", "options": { "text": "获取详情失败:" + str(det)[:200], "color": "#dc2626", "padding": "12px", "halign": "left"}}) else: # ── 标题 ── body.append({"widgettype": "Text", "options": { "text": str(det.get("title") or ""), "cfontsize": 1.15, "color": "#0f172a", "halign": "left", "wrap": True, "margin": "0 0 8px 0"}}) # ── 字段区 ── rt = str(det.get("record_type") or "tender") budget = det.get("budget_wan") or 0 fields = [] fields.append(("发布时间", str(det.get("publish_time") or "—"))) if rt == "tender": fields.append(("公告类型", str(det.get("notice_type") or "—"))) fields.append(("地区", str(det.get("region") or "—"))) fields.append(("采购人", str(det.get("buyer") or "—"))) fields.append(("预算", ("%.2f 万元" % budget) if budget else "未公开")) if det.get("item_category"): fields.append(("品目", str(det.get("item_category")))) if det.get("project_code"): fields.append(("项目编号", str(det.get("project_code")))) if det.get("agency"): fields.append(("代理机构", str(det.get("agency")))) if det.get("contact") or det.get("contact_phone"): fields.append(("联系人", ("%s %s" % (str(det.get("contact") or ""), str(det.get("contact_phone") or ""))).strip())) else: fields.append(("需求类型", str(det.get("notice_type") or "—"))) if det.get("item_category"): fields.append(("站内类目", str(det.get("item_category")))) fields.append(("赏金/预算", ("%.2f 万元" % budget) if budget else "未公开")) if det.get("ai_score"): fields.append(("AI相关度", str(det.get("ai_score")))) field_rows = [] for k, v in fields: field_rows.append({ "widgettype": "HBox", "options": {"width": "100%", "gap": "8px", "padding": "2px 0"}, "subwidgets": [ {"widgettype": "Text", "options": {"text": k, "cfontsize": 0.85, "color": "#64748b", "halign": "left", "width": "90px"}}, {"widgettype": "Text", "options": {"text": v, "cfontsize": 0.9, "color": "#0f172a", "halign": "left", "wrap": True}} ] }) body.append({"widgettype": "VBox", "options": { "width": "100%", "padding": "8px 12px", "gap": "2px", "border": "1px solid #e2e8f0", "borderRadius": "8px", "margin": "0 0 10px 0", "bgcolor": "#f8fafc"}, "subwidgets": field_rows}) # ── 来源说明(来源写进说明,不做来源按钮;2026-09-08 用户定夺)── src_name = source_text(str(det.get("source") or ""), str(det.get("source_url") or "")) src_lines = ["来源:" + src_name + "(数据爬取平台采集)"] url = str(det.get("url") or "") src_url = str(det.get("source_url") or "") src_note = "|".join(src_lines) link_btns = [] if url: link_btns.append({"widgettype": "Button", "options": {"label": "打开原文页", "css": "small"}, "binds": [{"wid": "self", "event": "click", "actiontype": "script", "target": "self", "script": "window.open(" + _json.dumps(url) + ",'_blank','noopener');"}]}) if src_url: link_btns.append({"widgettype": "Button", "options": {"label": "打开采集来源页", "css": "small"}, "binds": [{"wid": "self", "event": "click", "actiontype": "script", "target": "self", "script": "window.open(" + _json.dumps(src_url) + ",'_blank','noopener');"}]}) body.append({ "widgettype": "HBox", "options": {"width": "100%", "gap": "10px", "alignItems": "center", "margin": "0 0 10px 0"}, "subwidgets": [ {"widgettype": "Text", "options": {"text": src_note, "cfontsize": 0.8, "color": "#64748b", "halign": "left", "wrap": True}}, {"widgettype": "Filler"} ] + link_btns }) # ── 公告/需求全文(markdown 渲染,链接可点击;2026-09-09 用户定夺改 MdWidget)── detail_text = str(det.get("detail_text") or "").strip() body.append({"widgettype": "Text", "options": { "text": "全文内容", "cfontsize": 0.95, "color": "#0f172a", "halign": "left", "margin": "0 0 4px 0"}}) if detail_text: shown = detail_text[:20000] if len(detail_text) > 20000: shown += "\n\n……(全文 %d 字,已截断显示,完整内容见原文页)" % len(detail_text) 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(atts) if att_ok else 0), "cfontsize": 0.95, "color": "#0f172a", "halign": "left", "margin": "0 0 4px 0"}}) if not att_ok: # 附件端点未就绪(爬虫平台附件子系统部署中)——如实提示,不静默 body.append({"widgettype": "Text", "options": { "text": "附件服务暂不可用:" + str(atts)[:120], "cfontsize": 0.8, "color": "#94a3b8", "halign": "left", "margin": "0 0 10px 0"}}) elif not atts: body.append({"widgettype": "Text", "options": { "text": "该条暂无附件", "cfontsize": 0.8, "color": "#94a3b8", "halign": "left", "margin": "0 0 10px 0"}}) else: for a in atts: a_name = str(a.get("name") or "附件") a_url = str(a.get("url") or "") a_id = str(a.get("id") or "") a_size = int(a.get("size") or 0) size_txt = "" if a_size >= 1048576: size_txt = "%.1f MB" % (a_size / 1048576.0) elif a_size > 0: size_txt = "%.0f KB" % (a_size / 1024.0) act_btns = [] if a.get("downloaded"): # 平台已存副本 → 走代理下载 dl_query = "?tender_id=" + item_id if a_id: dl_query += "&att_id=" + a_id elif a_url: dl_query += "&att_url=" + a_url dl_href = dl_base + dl_query act_btns.append({"widgettype": "Button", "options": {"label": "下载", "css": "small"}, "binds": [{"wid": "self", "event": "click", "actiontype": "script", "target": "self", "script": "window.location.href=" + _json.dumps(dl_href) + ";"}]}) elif a_url: # 平台未存副本(如需登录才能取)→ 如实给原始页入口,不伪装成可下载 act_btns.append({"widgettype": "Button", "options": {"label": "打开原始页", "css": "small"}, "binds": [{"wid": "self", "event": "click", "actiontype": "script", "target": "self", "script": "window.open(" + _json.dumps(a_url) + ",'_blank','noopener');"}]}) 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": "📎 " + a_name, "cfontsize": 0.88, "color": "#0f172a", "halign": "left", "wrap": True}}, {"widgettype": "Text", "options": {"text": size_txt, "cfontsize": 0.75, "color": "#94a3b8"}}, {"widgettype": "Filler"} ] + act_btns }) return { "widgettype": "PopupWindow", "id": "opp_item_detail_pw", "options": {"title": "数据项详情", "width": "70%", "height": "85%", "auto_open": True, "resizable": True}, "subwidgets": [{ "widgettype": "VBox", "options": {"css": "filler", "width": "100%", "height": "100%", "padding": "12px 16px", "gap": "0px", "overflow": "auto"}, "subwidgets": body }] }