- 汇总主题/分项条目整行点击(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 同步新交互描述
119 lines
5.3 KiB
Plaintext
119 lines
5.3 KiB
Plaintext
# 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
|
||
}
|