pipeline-opportunity/wwwroot/api/opp_references_popup.dspy
yumoqing 45cec8987e feat(opp): 数据参考三级整行下钻+明细详情弹窗+附件下载代理
- 汇总主题/分项条目整行点击(cursor:pointer)弹下级弹窗,撤「查看分项」按钮
- 明细每条来源写进说明文字(source_text),删除 source_button_widget 来源按钮
- 新 opp_item_detail.dspy: 爬虫平台条目全量字段+公告全文+附件列表
- 新 opp_attachment_dl.dspy: 服务端附件下载代理,归属校验防SSRF,
  降级链 download_url→/api/attachments/{id}/download→原始url,失败如实报错
- opp_data_capability: tender_attachments/normalize_attachment 容错对接
  爬虫平台附件子系统(/api/tenders/{id}/attachments,另一会话开发中,未就绪时降级提示)
- opp_common: crawler_get_raw 原始字节下载辅助; load_path 注册两个新端点
- README/prompt 同步新交互描述
2026-09-08 23:39:16 +08:00

119 lines
5.3 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# opp_references_popup.dspy - 数据参考(references):主题列表(agent_menu_open 弹窗内嵌内容)
# 交互(2026-09-08 用户定夺):每个汇总主题条目整行点击 → 弹窗显示分项/明细项:
# 招标主题 → opp_references_items.dspy;国内众包需求主题 → opp_demand_items.dspy;
# 海外众包需求主题 → opp_overseas_items.dspy
# (原「查看分项」按钮已撤,整行可点,cursor:pointer 提示)
import json as _json
uid = await get_user()
if not uid:
return {"widgettype": "Text", "options": {"text": "请先登录", "color": "#dc2626", "padding": "12px"}}
dbname = get_module_dbname('pipeline-opportunity')
items_url = entire_url("/pipeline-opportunity/api/opp_references_items.dspy")
demand_items_url = entire_url("/pipeline-opportunity/api/opp_demand_items.dspy")
overseas_items_url = entire_url("/pipeline-opportunity/api/opp_overseas_items.dspy")
async with DBPools().sqlorContext(dbname) as sor:
from pipeline_opportunity.opp_data_capability import hot_software, hot_demands, hot_overseas
ok, res = await hot_software(sor, days=30, top=10)
await sor.sqlExe("COMMIT", {})
ok_dm, res_dm = await hot_demands(sor, days=30, top=10)
await sor.sqlExe("COMMIT", {})
ok_ov, res_ov = await hot_overseas(sor, days=30, top=10)
await sor.sqlExe("COMMIT", {})
def _topic_row(cat, cnt, budget, target_url, popup_id, extra_txt=""):
"""汇总主题行:整行点击 → 弹出该主题的分项明细列表。"""
script = ("var old=bricks.getWidgetById(" + _json.dumps(popup_id) + ",bricks.app);"
"if(old&&old.destroy){old.destroy();}"
"var rp=await fetch(" + _json.dumps(target_url) + "+'?category='+encodeURIComponent(" + _json.dumps(cat) + "));"
"var d=await rp.json();"
"if(d){bricks.widgetBuild(d,bricks.app);}")
subs = [
{"widgettype": "Text", "options": {"text": cat, "cfontsize": 1.0,
"color": "#0f172a", "halign": "left"}},
{"widgettype": "Filler"},
{"widgettype": "Text", "options": {"text": "%d 条" % cnt,
"cfontsize": 0.85, "color": "#64748b"}},
]
if extra_txt:
subs.append({"widgettype": "Text", "options": {"text": extra_txt,
"cfontsize": 0.75, "color": "#94a3b8"}})
else:
subs.append({"widgettype": "Text", "options": {
"text": "预算 %s 万" % ("{:,.0f}".format(budget) if budget else "—"),
"cfontsize": 0.85, "color": "#64748b"}})
subs.append({"widgettype": "Text", "options": {"text": "›",
"cfontsize": 1.1, "color": "#94a3b8"}})
return {
"widgettype": "HBox",
"options": {"width": "100%", "gap": "10px", "alignItems": "center",
"padding": "8px 14px", "border": "1px solid #e2e8f0",
"borderRadius": "8px", "margin": "0 0 8px 0", "bgcolor": "#ffffff",
"cursor": "pointer"},
"binds": [{"wid": "self", "event": "click", "actiontype": "script",
"target": "self", "script": script}],
"subwidgets": subs
}
rows = []
if not ok:
rows.append({"widgettype": "Text", "options": {
"text": "数据爬取平台暂不可用:" + str(res)[:200],
"color": "#dc2626", "padding": "12px", "halign": "left"}})
else:
ranking = res.get("ranking") or []
if not ranking:
rows.append({"widgettype": "Text", "options": {
"text": "近30天暂无招标数据", "color": "#64748b", "padding": "12px", "halign": "left"}})
for r in ranking:
rows.append(_topic_row(str(r.get("category", "")), r.get("tender_count", 0),
r.get("total_budget_wan", 0), items_url, "opp_refs_items_pw"))
# ── 国内众包需求板块(猪八戒/一品威客)──
dm_rows = []
if ok_dm:
dm_ranking = res_dm.get("ranking") or []
if dm_ranking:
dm_rows.append({"widgettype": "Text", "options": {
"text": "国内众包软件需求(猪八戒/一品威客,近30天)",
"cfontsize": 0.95, "color": "#0f172a", "halign": "left",
"margin": "14px 0 6px 0", "padding": "6px 0 0 0",
"border": "1px solid #f1f5f9", "borderRadius": "6px", "bgcolor": "#f8fafc"}})
for r in dm_ranking:
dm_rows.append(_topic_row(str(r.get("category", "")), r.get("tender_count", 0),
r.get("total_budget_wan", 0), demand_items_url, "opp_demand_items_pw"))
# ── 海外众包需求板块(Freelancer.com / PeoplePerHour)──
ov_rows = []
if ok_ov:
ov_ranking = res_ov.get("ranking") or []
if ov_ranking:
ov_rows.append({"widgettype": "Text", "options": {
"text": "海外众包软件需求(Freelancer.com / PeoplePerHour,近30天)",
"cfontsize": 0.95, "color": "#0f172a", "halign": "left",
"margin": "14px 0 6px 0", "padding": "6px 0 0 0",
"border": "1px solid #f1f5f9", "borderRadius": "6px", "bgcolor": "#f8fafc"}})
for r in ov_ranking:
by_src = r.get("by_source") or {}
src_txt = " / ".join("%s:%d" % (k, v) for k, v in by_src.items())
ov_rows.append(_topic_row(str(r.get("category", "")), r.get("tender_count", 0),
r.get("total_budget_wan", 0), overseas_items_url, "opp_overseas_items_pw",
extra_txt=src_txt))
return {
"widgettype": "VBox",
"options": {"css": "filler", "width": "100%", "height": "100%", "padding": "12px 16px",
"gap": "6px", "overflow": "auto"},
"subwidgets": [
{"widgettype": "Text", "options": {
"text": "点击任一主题条目,弹出该主题的全部分项明细;明细中再点击任一条,弹出具体内容(数据来自数据爬取平台,含公告全文与附件下载)。",
"cfontsize": 0.8, "color": "#64748b", "halign": "left", "margin": "0 0 8px 0"}}
] + rows + dm_rows + ov_rows
}