132 lines
6.3 KiB
Plaintext
132 lines
6.3 KiB
Plaintext
# opp_references_items.dspy - 某软件主题的分项列表(references 可查证)
|
||
# 入参:category=<主题名>,sort=time|budget(2026-09-15 用户需求:支持按标的金额
|
||
# 大到小 / 按时间后到前排序;排序在数据源侧全量执行后再截断,缺省 time)。
|
||
# 交互(2026-09-08 用户定夺):
|
||
# 每条分项整行点击 → opp_item_detail.dspy 弹出具体内容(爬取平台全文+附件下载);
|
||
# 来源写进每条说明文字(来源:xxx),不再渲染来源按钮。
|
||
|
||
import json as _json
|
||
|
||
uid = await get_user()
|
||
if not uid:
|
||
return {"widgettype": "Message", "options": {"title": "未登录", "message": "请先登录"}}
|
||
|
||
category = ((params_kw or {}).get("category") or "").strip()
|
||
days = int((params_kw or {}).get("days") or 30)
|
||
if not category:
|
||
return {"widgettype": "Message", "options": {"title": "缺少参数", "message": "缺少 category"}}
|
||
# 排序白名单收敛(非法/缺省一律 time=发布时间后到前,与历史行为一致)
|
||
sort = ((params_kw or {}).get("sort") or "time").strip()
|
||
if sort not in ("time", "budget"):
|
||
sort = "time"
|
||
|
||
dbname = get_module_dbname('pipeline-opportunity')
|
||
|
||
detail_url = entire_url("/pipeline-opportunity/api/opp_item_detail.dspy")
|
||
self_url = entire_url("/pipeline-opportunity/api/opp_references_items.dspy")
|
||
|
||
async with DBPools().sqlorContext(dbname) as sor:
|
||
from pipeline_opportunity.opp_data_capability import category_references
|
||
from pipeline_opportunity.opp_common import source_text
|
||
ok, res = await category_references(sor, category, days=days, limit=500, sort=sort)
|
||
await sor.sqlExe("COMMIT", {})
|
||
|
||
# ── 排序切换按钮(点击重建弹窗带 sort 参数;对齐 opp_mining_popup 刷新范式)──
|
||
def _sort_btn(label, target_sort, active):
|
||
script = ("var old=bricks.getWidgetById('opp_refs_items_pw',bricks.app);"
|
||
"if(old&&old.destroy){old.destroy();}"
|
||
"var rp=await fetch(" + _json.dumps(self_url) + "+'?category='+encodeURIComponent("
|
||
+ _json.dumps(category) + ")+'&days=" + str(days) + "&sort=" + target_sort + "');"
|
||
"var d=await rp.json();"
|
||
"if(d){bricks.widgetBuild(d,bricks.app);}")
|
||
return {"widgettype": "Button",
|
||
"options": {"label": label, "css": ("primary" if active else "small")},
|
||
"binds": [{"wid": "self", "event": "click", "actiontype": "script",
|
||
"target": "self", "script": script}]}
|
||
|
||
sort_bar = {"widgettype": "HBox", "options": {"width": "100%", "gap": "8px",
|
||
"alignItems": "center", "margin": "0 0 8px 0"},
|
||
"subwidgets": [
|
||
_sort_btn("💰 标的金额(大到小)", "budget", sort == "budget"),
|
||
_sort_btn("⏱ 发布时间(后到前)", "time", sort == "time"),
|
||
{"widgettype": "Filler"},
|
||
]}
|
||
|
||
rows = []
|
||
if not ok:
|
||
rows.append({"widgettype": "Text", "options": {
|
||
"otext": "查询失败:${p0}", "text": "查询失败:${p0}", "i18n": True, "i18n_params": {"p0": str(str(res)[:200])},
|
||
"color": "#dc2626", "padding": "12px", "halign": "left"}})
|
||
else:
|
||
items = res.get("items") or []
|
||
if not items:
|
||
rows.append({"widgettype": "Text", "options": {
|
||
"otext": "该主题近 ${p0} 天无分项", "text": "该主题近 ${p0} 天无分项", "i18n": True, "i18n_params": {"p0": str('%d' % (days,))}, "color": "#64748b", "padding": "12px", "halign": "left"}})
|
||
for it in items:
|
||
item_id = str(it.get("id") or "")
|
||
budget = it.get("budget_wan") or 0
|
||
# 来源写进说明文字(不再做来源按钮)
|
||
src_name = source_text(str(it.get("source") or ""), str(it.get("source_url") or ""))
|
||
meta = "%s | %s | %s | 来源:%s" % (
|
||
str(it.get("publish_time") or "")[:10],
|
||
str(it.get("region") or "—"),
|
||
(("预算 %.0f 万" % budget) if budget else "预算未公开"),
|
||
src_name)
|
||
att_cnt = int(it.get("attachment_count") or 0)
|
||
row_subs = [
|
||
{"widgettype": "Text", "options": {
|
||
"text": str(it.get("title") or ""), "cfontsize": 0.95,
|
||
"color": "#0f172a", "halign": "left", "wrap": True}},
|
||
{"widgettype": "HBox", "options": {"width": "100%", "gap": "10px",
|
||
"alignItems": "center"},
|
||
"subwidgets": [
|
||
{"widgettype": "Text", "options": {"text": meta,
|
||
"cfontsize": 0.75, "color": "#94a3b8", "halign": "left",
|
||
"wrap": True}},
|
||
{"widgettype": "Filler"}
|
||
] + ([{"widgettype": "Text", "options": {
|
||
"text": "📎%d" % att_cnt, "cfontsize": 0.75, "color": "#0ea5e9"}}]
|
||
if att_cnt else []) + [
|
||
{"widgettype": "Text", "options": {"text": "›",
|
||
"cfontsize": 1.0, "color": "#94a3b8"}}]
|
||
}
|
||
]
|
||
row_opts = {"width": "100%", "padding": "8px 12px", "gap": "4px",
|
||
"border": "1px solid #e2e8f0", "borderRadius": "8px",
|
||
"margin": "0 0 6px 0", "bgcolor": "#ffffff"}
|
||
if item_id:
|
||
row_opts["cursor"] = "pointer"
|
||
script = ("var old=bricks.getWidgetById('opp_item_detail_pw',bricks.app);"
|
||
"if(old&&old.destroy){old.destroy();}"
|
||
"var rp=await fetch(" + _json.dumps(detail_url) + "+'?id='+encodeURIComponent(" + _json.dumps(item_id) + "));"
|
||
"var d=await rp.json();"
|
||
"if(d){bricks.widgetBuild(d,bricks.app);}")
|
||
rows.append({
|
||
"widgettype": "VBox", "options": row_opts,
|
||
"binds": [{"wid": "self", "event": "click", "actiontype": "script",
|
||
"target": "self", "script": script}],
|
||
"subwidgets": row_subs})
|
||
else:
|
||
# 无 id 的记录(异常数据)不可点,如实降级为静态行
|
||
rows.append({"widgettype": "VBox", "options": row_opts,
|
||
"subwidgets": row_subs})
|
||
|
||
total = res.get("count", len(rows)) if ok else 0
|
||
_sort_label = "标的金额大到小" if sort == "budget" else "时间后到前"
|
||
return {
|
||
"widgettype": "PopupWindow",
|
||
"id": "opp_refs_items_pw",
|
||
"options": {"title": "分项明细:%s(%d 条 · %s)" % (category, total, _sort_label),
|
||
"width": "80%", "height": "82%", "auto_open": True, "resizable": True},
|
||
"subwidgets": [{
|
||
"widgettype": "VBox",
|
||
"options": {"css": "filler", "width": "100%", "height": "100%",
|
||
"padding": "12px 16px", "gap": "0px", "overflow": "auto"},
|
||
"subwidgets": [sort_bar,
|
||
{"widgettype": "Text", "options": {
|
||
"otext": "点击任一条目弹出具体内容(公告全文来自数据爬取平台,有附件的可直接下载);每条说明中已标注数据来源。", "text": "点击任一条目弹出具体内容(公告全文来自数据爬取平台,有附件的可直接下载);每条说明中已标注数据来源。", "i18n": True,
|
||
"cfontsize": 0.78, "color": "#64748b", "halign": "left", "margin": "0 0 8px 0"}}
|
||
] + rows
|
||
}]
|
||
}
|