202 lines
8.6 KiB
Plaintext
202 lines
8.6 KiB
Plaintext
# 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
|
||
}]
|
||
}
|