feat(references): 分项明细列表支持排序——budget标的金额大到小/time发布时间后到前;数据源侧先全局排序再截断(金额TopN是真TopN非局部排序);三个分项弹窗(招标/国内众包/海外)顶部加排序切换按钮(当前态primary高亮,点击重建弹窗带sort参数);弹窗标题回显当前排序;agent工具opp_category_references/opp_overseas_references加sort参数(白名单收敛,非法值回落time)

This commit is contained in:
yumoqing 2026-09-15 14:36:04 +08:00
parent 7a8afab054
commit 684ec3ba69
5 changed files with 129 additions and 27 deletions

View File

@ -49,9 +49,10 @@ OPP_TOOLS = [
parameters={"tender_id": "招标信息ID"}, category="data"), parameters={"tender_id": "招标信息ID"}, category="data"),
ToolDefinition(name="opp_crawler_stats", description="查看数据爬取平台采集健康度(总量/24h新增/最近运行)", ToolDefinition(name="opp_crawler_stats", description="查看数据爬取平台采集健康度(总量/24h新增/最近运行)",
parameters={}, category="data"), parameters={}, category="data"),
ToolDefinition(name="opp_category_references", description="查某软件主题的分项明细(每条带公告原文URL+采集来源,供逐条查证/举例)。⚠️ 明细是「举例佐证」用的,不是「统计」用的——排名/数量/金额统计先用 opp_hot_software(已返回 tender_count+total_budget_wan),只在需要给2~5个样例链接时才调本工具且 limit≤10", ToolDefinition(name="opp_category_references", description="查某软件主题的分项明细(每条带公告原文URL+采集来源,供逐条查证/举例;sort=budget 按标的金额大到小 / time 按发布时间后到前)。⚠️ 明细是「举例佐证」用的,不是「统计」用的——排名/数量/金额统计先用 opp_hot_software(已返回 tender_count+total_budget_wan),只在需要给2~5个样例链接时才调本工具且 limit≤10",
parameters={"category": "软件主题名(须与热点排名中的category一致)", parameters={"category": "软件主题名(须与热点排名中的category一致)",
"days": "统计窗口天数(默认30)", "limit": "条数(默认20,最大50——只取样例不要拉全量)"}, category="data"), "days": "统计窗口天数(默认30)", "limit": "条数(默认20,最大50——只取样例不要拉全量)",
"sort": "排序(可选: budget=标的金额大到小/time=时间后到前,默认time)"}, category="data"),
ToolDefinition(name="opp_demands", description="查众包平台需求明细列表(国内猪八戒zbj/一品威客epwk + 海外freelancer/pph,每条带详情页URL可查证)。⚠️ 主题排名/数量/金额统计先用 opp_hot_demands(国内)/opp_hot_overseas(海外)——它们已聚合好排名;本工具只用于按关键词找具体需求样例,limit≤10", ToolDefinition(name="opp_demands", description="查众包平台需求明细列表(国内猪八戒zbj/一品威客epwk + 海外freelancer/pph,每条带详情页URL可查证)。⚠️ 主题排名/数量/金额统计先用 opp_hot_demands(国内)/opp_hot_overseas(海外)——它们已聚合好排名;本工具只用于按关键词找具体需求样例,limit≤10",
parameters={"keyword": "标题关键词(建议必填以缩小范围)", "days": "统计窗口天数(默认30)", parameters={"keyword": "标题关键词(建议必填以缩小范围)", "days": "统计窗口天数(默认30)",
"source": "来源(可选: zbj猪八戒/epwk一品威客/freelancer海外/pph海外,不传=全部)", "source": "来源(可选: zbj猪八戒/epwk一品威客/freelancer海外/pph海外,不传=全部)",
@ -60,9 +61,10 @@ OPP_TOOLS = [
parameters={"days": "统计窗口天数(默认30)", "top": "Top条数(默认10)"}, category="data"), parameters={"days": "统计窗口天数(默认30)", "top": "Top条数(默认10)"}, category="data"),
ToolDefinition(name="opp_hot_overseas", description="【统计首选】海外众包需求热点主题排名(Freelancer/PeoplePerHour,英文标题按8大主题归类)——直接返回各主题数量+预算合计+按来源分布,无需拉明细自己数。用户问国外/海外市场、中外众包排名时用本工具", ToolDefinition(name="opp_hot_overseas", description="【统计首选】海外众包需求热点主题排名(Freelancer/PeoplePerHour,英文标题按8大主题归类)——直接返回各主题数量+预算合计+按来源分布,无需拉明细自己数。用户问国外/海外市场、中外众包排名时用本工具",
parameters={"days": "统计窗口天数(默认30)", "top": "Top条数(默认10)"}, category="data"), parameters={"days": "统计窗口天数(默认30)", "top": "Top条数(默认10)"}, category="data"),
ToolDefinition(name="opp_overseas_references", description="查某海外需求主题的分项明细(每条带Freelancer/PeoplePerHour详情页URL,供逐条查证/举例)。⚠️ 统计先用 opp_hot_overseas(已返回 tender_count+total_budget_wan+by_source),只在需要2~5个样例链接时才调本工具且 limit≤10", ToolDefinition(name="opp_overseas_references", description="查某海外需求主题的分项明细(每条带Freelancer/PeoplePerHour详情页URL,供逐条查证/举例;sort=budget 按赏金大到小 / time 按发布时间后到前)。⚠️ 统计先用 opp_hot_overseas(已返回 tender_count+total_budget_wan+by_source),只在需要2~5个样例链接时才调本工具且 limit≤10",
parameters={"category": "海外主题名(须与 opp_hot_overseas 排名中的category一致)", parameters={"category": "海外主题名(须与 opp_hot_overseas 排名中的category一致)",
"days": "统计窗口天数(默认30)", "limit": "条数(默认20,最大50——只取样例不要拉全量)"}, category="data"), "days": "统计窗口天数(默认30)", "limit": "条数(默认20,最大50——只取样例不要拉全量)",
"sort": "排序(可选: budget=赏金大到小/time=时间后到前,默认time)"}, category="data"),
# ── 报告与审批(商机闭环)── # ── 报告与审批(商机闭环)──
ToolDefinition(name="opp_create_report", description="创建软件研发报告草稿。从热门软件中选定的方向写入报告,数据基础章节自动从爬取平台拉取。正文必须按四节模板组织(## 研发场景 / ## 输入 / ## 输出 / ## 成果),商店详情页按节展示", ToolDefinition(name="opp_create_report", description="创建软件研发报告草稿。从热门软件中选定的方向写入报告,数据基础章节自动从爬取平台拉取。正文必须按四节模板组织(## 研发场景 / ## 输入 / ## 输出 / ## 成果),商店详情页按节展示",
@ -337,13 +339,16 @@ async def _h_category_references(sor, p, ctx):
return _fmt(False, "缺少 category(软件主题名)") return _fmt(False, "缺少 category(软件主题名)")
ok, res = await category_references( ok, res = await category_references(
sor, category, days=int(p.get("days") or 30), sor, category, days=int(p.get("days") or 30),
limit=min(int(p.get("limit") or 200), 1000)) limit=min(int(p.get("limit") or 200), 1000),
sort=str(p.get("sort") or "").strip())
if not ok: if not ok:
return _fmt(False, res) return _fmt(False, res)
return json.dumps({ return json.dumps({
"category": res.get("category"), "category": res.get("category"),
"窗口": "%d天" % res.get("window_days"), "窗口": "%d天" % res.get("window_days"),
"分项总数": res.get("count"), "分项总数": res.get("count"),
"排序": {"budget": "标的金额大到小", "time": "发布时间后到前"}.get(
res.get("sort") or "time", "发布时间后到前"),
"分项": res.get("items"), "分项": res.get("items"),
"说明": "每条分项的 url 为公告原文、source_url 为采集来源,均可点击查证", "说明": "每条分项的 url 为公告原文、source_url 为采集来源,均可点击查证",
}, ensure_ascii=False, default=str) }, ensure_ascii=False, default=str)
@ -393,7 +398,8 @@ async def _h_overseas_references(sor, p, ctx):
return _fmt(False, {"error": "缺少 category 参数"}) return _fmt(False, {"error": "缺少 category 参数"})
ok, res = await overseas_references( ok, res = await overseas_references(
sor, category=category, days=int(p.get("days") or 30), sor, category=category, days=int(p.get("days") or 30),
limit=min(int(p.get("limit") or 200), 500)) limit=min(int(p.get("limit") or 200), 500),
sort=str(p.get("sort") or "").strip())
if not ok: if not ok:
return _fmt(False, res) return _fmt(False, res)
return json.dumps(res, ensure_ascii=False, default=str) return json.dumps(res, ensure_ascii=False, default=str)

View File

@ -55,17 +55,22 @@ async def hot_software(sor, days=30, top=10):
{"days": days, "top": top}) {"days": days, "top": top})
async def category_references(sor, category, days=30, limit=20, record_type="tender"): async def category_references(sor, category, days=30, limit=20, record_type="tender",
sort=""):
"""某软件主题的分项明细(references 可查证,举例佐证用)。 """某软件主题的分项明细(references 可查证,举例佐证用)。
每条带公告原文/详情页 url + 采集来源 source_url,供界面逐条查证。 每条带公告原文/详情页 url + 采集来源 source_url,供界面逐条查证。
record_type: tender=招标(默认) / demand=众包需求发布(猪八戒/一品威客)。 record_type: tender=招标(默认) / demand=众包需求发布(猪八戒/一品威客)。
sort: ""=缺省(时间后到前) / time=发布时间后到前 / budget=标的金额大到小。
排序在数据源侧全量执行后再截断(「金额前 N」是全窗口真 TopN)。
limit 硬上限 _DETAIL_LIMIT_MAX(50)——统计用 hot_software 聚合,勿拉全量明细。 limit 硬上限 _DETAIL_LIMIT_MAX(50)——统计用 hot_software 聚合,勿拉全量明细。
""" """
return await crawler_get(sor, "/api/category_references", params = {"category": category, "days": days,
{"category": category, "days": days, "limit": _clamp_detail_limit(limit),
"limit": _clamp_detail_limit(limit), "record_type": record_type}
"record_type": record_type}) if sort:
params["sort"] = sort
return await crawler_get(sor, "/api/category_references", params)
async def crawler_stats(sor): async def crawler_stats(sor):
@ -99,15 +104,18 @@ async def hot_overseas(sor, days=30, top=10):
{"days": days, "top": top}) {"days": days, "top": top})
async def overseas_references(sor, category, days=30, limit=20): async def overseas_references(sor, category, days=30, limit=20, sort=""):
"""某海外需求主题的分项明细(references 可查证,举例佐证用)。 """某海外需求主题的分项明细(references 可查证,举例佐证用)。
每条带海外平台详情页 url,供界面逐条查证。 每条带海外平台详情页 url,供界面逐条查证。
sort: ""=缺省(时间后到前) / time=发布时间后到前 / budget=赏金(折人民币)大到小。
limit 硬上限 _DETAIL_LIMIT_MAX(50)——统计用 hot_overseas 聚合,勿拉全量明细。 limit 硬上限 _DETAIL_LIMIT_MAX(50)——统计用 hot_overseas 聚合,勿拉全量明细。
""" """
return await crawler_get(sor, "/api/overseas_references", params = {"category": category, "days": days,
{"category": category, "days": days, "limit": _clamp_detail_limit(limit)}
"limit": _clamp_detail_limit(limit)}) if sort:
params["sort"] = sort
return await crawler_get(sor, "/api/overseas_references", params)
async def query_tenders(sor, keyword="", region="", buyer="", notice_type="", async def query_tenders(sor, keyword="", region="", buyer="", notice_type="",

View File

@ -1,5 +1,7 @@
# opp_demand_items.dspy - 某软件主题的众包需求分项列表(国内猪八戒/一品威客) # opp_demand_items.dspy - 某软件主题的众包需求分项列表(国内猪八戒/一品威客)
# 入参:category=<主题名>。交互(2026-09-08 用户定夺): # 入参:category=<主题名>,sort=time|budget(2026-09-15 用户需求:支持按赏金
# 大到小 / 按时间后到前排序;排序在数据源侧全量执行后再截断,缺省 time)。
# 交互(2026-09-08 用户定夺):
# 每条分项整行点击 → opp_item_detail.dspy 弹出具体内容(爬取平台数据+附件下载); # 每条分项整行点击 → opp_item_detail.dspy 弹出具体内容(爬取平台数据+附件下载);
# 来源写进每条说明文字(来源:猪八戒/一品威客),不再渲染来源按钮。 # 来源写进每条说明文字(来源:猪八戒/一品威客),不再渲染来源按钮。
@ -13,17 +15,44 @@ category = ((params_kw or {}).get("category") or "").strip()
days = int((params_kw or {}).get("days") or 30) days = int((params_kw or {}).get("days") or 30)
if not category: if not category:
return {"widgettype": "Message", "options": {"title": "缺少参数", "message": "缺少 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') dbname = get_module_dbname('pipeline-opportunity')
detail_url = entire_url("/pipeline-opportunity/api/opp_item_detail.dspy") detail_url = entire_url("/pipeline-opportunity/api/opp_item_detail.dspy")
self_url = entire_url("/pipeline-opportunity/api/opp_demand_items.dspy")
async with DBPools().sqlorContext(dbname) as sor: async with DBPools().sqlorContext(dbname) as sor:
from pipeline_opportunity.opp_data_capability import category_references from pipeline_opportunity.opp_data_capability import category_references
from pipeline_opportunity.opp_common import source_text from pipeline_opportunity.opp_common import source_text
ok, res = await category_references(sor, category, days=days, limit=500, record_type="demand") ok, res = await category_references(sor, category, days=days, limit=500,
record_type="demand", sort=sort)
await sor.sqlExe("COMMIT", {}) await sor.sqlExe("COMMIT", {})
# ── 排序切换按钮(点击重建弹窗带 sort 参数;对齐 opp_mining_popup 刷新范式)──
def _sort_btn(label, target_sort, active):
script = ("var old=bricks.getWidgetById('opp_demand_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 = [] rows = []
if not ok: if not ok:
rows.append({"widgettype": "Text", "options": { rows.append({"widgettype": "Text", "options": {
@ -79,16 +108,17 @@ else:
"subwidgets": row_subs}) "subwidgets": row_subs})
total = res.get("count", len(rows)) if ok else 0 total = res.get("count", len(rows)) if ok else 0
_sort_label = "赏金大到小" if sort == "budget" else "时间后到前"
return { return {
"widgettype": "PopupWindow", "widgettype": "PopupWindow",
"id": "opp_demand_items_pw", "id": "opp_demand_items_pw",
"options": {"title": "众包需求分项:%s(%d 条)" % (category, total), "options": {"title": "众包需求分项:%s(%d 条 · %s)" % (category, total, _sort_label),
"width": "80%", "height": "82%", "auto_open": True, "resizable": True}, "width": "80%", "height": "82%", "auto_open": True, "resizable": True},
"subwidgets": [{ "subwidgets": [{
"widgettype": "VBox", "widgettype": "VBox",
"options": {"css": "filler", "width": "100%", "height": "100%", "options": {"css": "filler", "width": "100%", "height": "100%",
"padding": "12px 16px", "gap": "0px", "overflow": "auto"}, "padding": "12px 16px", "gap": "0px", "overflow": "auto"},
"subwidgets": [ "subwidgets": [sort_bar,
{"widgettype": "Text", "options": { {"widgettype": "Text", "options": {
"otext": "点击任一条目弹出具体内容(数据来自数据爬取平台);每条说明中已标注来源平台。", "text": "点击任一条目弹出具体内容(数据来自数据爬取平台);每条说明中已标注来源平台。", "i18n": True, "otext": "点击任一条目弹出具体内容(数据来自数据爬取平台);每条说明中已标注来源平台。", "text": "点击任一条目弹出具体内容(数据来自数据爬取平台);每条说明中已标注来源平台。", "i18n": True,
"cfontsize": 0.78, "color": "#64748b", "halign": "left", "margin": "0 0 8px 0"}} "cfontsize": 0.78, "color": "#64748b", "halign": "left", "margin": "0 0 8px 0"}}

View File

@ -1,5 +1,7 @@
# opp_overseas_items.dspy - 某海外需求主题的分项列表(references 可查证) # opp_overseas_items.dspy - 某海外需求主题的分项列表(references 可查证)
# 入参:category=<海外主题名>。交互(2026-09-08 用户定夺): # 入参:category=<海外主题名>,sort=time|budget(2026-09-15 用户需求:支持按赏金
# (折人民币)大到小 / 按时间后到前排序;排序在数据源侧全量执行后再截断,缺省 time)。
# 交互(2026-09-08 用户定夺):
# 每条分项整行点击 → opp_item_detail.dspy 弹出具体内容(爬取平台数据+附件下载); # 每条分项整行点击 → opp_item_detail.dspy 弹出具体内容(爬取平台数据+附件下载);
# 来源写进每条说明文字(来源:Freelancer.com / PeoplePerHour),不再渲染来源按钮。 # 来源写进每条说明文字(来源:Freelancer.com / PeoplePerHour),不再渲染来源按钮。
@ -13,17 +15,43 @@ category = ((params_kw or {}).get("category") or "").strip()
days = int((params_kw or {}).get("days") or 30) days = int((params_kw or {}).get("days") or 30)
if not category: if not category:
return {"widgettype": "Message", "options": {"title": "缺少参数", "message": "缺少 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') dbname = get_module_dbname('pipeline-opportunity')
detail_url = entire_url("/pipeline-opportunity/api/opp_item_detail.dspy") detail_url = entire_url("/pipeline-opportunity/api/opp_item_detail.dspy")
self_url = entire_url("/pipeline-opportunity/api/opp_overseas_items.dspy")
async with DBPools().sqlorContext(dbname) as sor: async with DBPools().sqlorContext(dbname) as sor:
from pipeline_opportunity.opp_data_capability import overseas_references from pipeline_opportunity.opp_data_capability import overseas_references
from pipeline_opportunity.opp_common import source_text from pipeline_opportunity.opp_common import source_text
ok, res = await overseas_references(sor, category, days=days, limit=500) ok, res = await overseas_references(sor, category, days=days, limit=500, sort=sort)
await sor.sqlExe("COMMIT", {}) await sor.sqlExe("COMMIT", {})
# ── 排序切换按钮(点击重建弹窗带 sort 参数;对齐 opp_mining_popup 刷新范式)──
def _sort_btn(label, target_sort, active):
script = ("var old=bricks.getWidgetById('opp_overseas_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 = [] rows = []
if not ok: if not ok:
rows.append({"widgettype": "Text", "options": { rows.append({"widgettype": "Text", "options": {
@ -79,16 +107,17 @@ else:
"subwidgets": row_subs}) "subwidgets": row_subs})
total = res.get("matched", len(rows)) if ok else 0 total = res.get("matched", len(rows)) if ok else 0
_sort_label = "赏金大到小" if sort == "budget" else "时间后到前"
return { return {
"widgettype": "PopupWindow", "widgettype": "PopupWindow",
"id": "opp_overseas_items_pw", "id": "opp_overseas_items_pw",
"options": {"title": "海外需求分项:%s(%d 条)" % (category, total), "options": {"title": "海外需求分项:%s(%d 条 · %s)" % (category, total, _sort_label),
"width": "80%", "height": "82%", "auto_open": True, "resizable": True}, "width": "80%", "height": "82%", "auto_open": True, "resizable": True},
"subwidgets": [{ "subwidgets": [{
"widgettype": "VBox", "widgettype": "VBox",
"options": {"css": "filler", "width": "100%", "height": "100%", "options": {"css": "filler", "width": "100%", "height": "100%",
"padding": "12px 16px", "gap": "0px", "overflow": "auto"}, "padding": "12px 16px", "gap": "0px", "overflow": "auto"},
"subwidgets": [ "subwidgets": [sort_bar,
{"widgettype": "Text", "options": { {"widgettype": "Text", "options": {
"otext": "点击任一条目弹出具体内容(数据来自数据爬取平台);每条说明中已标注来源平台。", "text": "点击任一条目弹出具体内容(数据来自数据爬取平台);每条说明中已标注来源平台。", "i18n": True, "otext": "点击任一条目弹出具体内容(数据来自数据爬取平台);每条说明中已标注来源平台。", "text": "点击任一条目弹出具体内容(数据来自数据爬取平台);每条说明中已标注来源平台。", "i18n": True,
"cfontsize": 0.78, "color": "#64748b", "halign": "left", "margin": "0 0 8px 0"}} "cfontsize": 0.78, "color": "#64748b", "halign": "left", "margin": "0 0 8px 0"}}

View File

@ -1,5 +1,7 @@
# opp_references_items.dspy - 某软件主题的分项列表(references 可查证) # opp_references_items.dspy - 某软件主题的分项列表(references 可查证)
# 入参:category=<主题名>。交互(2026-09-08 用户定夺): # 入参:category=<主题名>,sort=time|budget(2026-09-15 用户需求:支持按标的金额
# 大到小 / 按时间后到前排序;排序在数据源侧全量执行后再截断,缺省 time)。
# 交互(2026-09-08 用户定夺):
# 每条分项整行点击 → opp_item_detail.dspy 弹出具体内容(爬取平台全文+附件下载); # 每条分项整行点击 → opp_item_detail.dspy 弹出具体内容(爬取平台全文+附件下载);
# 来源写进每条说明文字(来源:xxx),不再渲染来源按钮。 # 来源写进每条说明文字(来源:xxx),不再渲染来源按钮。
@ -13,17 +15,43 @@ category = ((params_kw or {}).get("category") or "").strip()
days = int((params_kw or {}).get("days") or 30) days = int((params_kw or {}).get("days") or 30)
if not category: if not category:
return {"widgettype": "Message", "options": {"title": "缺少参数", "message": "缺少 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') dbname = get_module_dbname('pipeline-opportunity')
detail_url = entire_url("/pipeline-opportunity/api/opp_item_detail.dspy") 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: async with DBPools().sqlorContext(dbname) as sor:
from pipeline_opportunity.opp_data_capability import category_references from pipeline_opportunity.opp_data_capability import category_references
from pipeline_opportunity.opp_common import source_text from pipeline_opportunity.opp_common import source_text
ok, res = await category_references(sor, category, days=days, limit=500) ok, res = await category_references(sor, category, days=days, limit=500, sort=sort)
await sor.sqlExe("COMMIT", {}) 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 = [] rows = []
if not ok: if not ok:
rows.append({"widgettype": "Text", "options": { rows.append({"widgettype": "Text", "options": {
@ -84,16 +112,17 @@ else:
"subwidgets": row_subs}) "subwidgets": row_subs})
total = res.get("count", len(rows)) if ok else 0 total = res.get("count", len(rows)) if ok else 0
_sort_label = "标的金额大到小" if sort == "budget" else "时间后到前"
return { return {
"widgettype": "PopupWindow", "widgettype": "PopupWindow",
"id": "opp_refs_items_pw", "id": "opp_refs_items_pw",
"options": {"title": "分项明细:%s(%d 条)" % (category, total), "options": {"title": "分项明细:%s(%d 条 · %s)" % (category, total, _sort_label),
"width": "80%", "height": "82%", "auto_open": True, "resizable": True}, "width": "80%", "height": "82%", "auto_open": True, "resizable": True},
"subwidgets": [{ "subwidgets": [{
"widgettype": "VBox", "widgettype": "VBox",
"options": {"css": "filler", "width": "100%", "height": "100%", "options": {"css": "filler", "width": "100%", "height": "100%",
"padding": "12px 16px", "gap": "0px", "overflow": "auto"}, "padding": "12px 16px", "gap": "0px", "overflow": "auto"},
"subwidgets": [ "subwidgets": [sort_bar,
{"widgettype": "Text", "options": { {"widgettype": "Text", "options": {
"otext": "点击任一条目弹出具体内容(公告全文来自数据爬取平台,有附件的可直接下载);每条说明中已标注数据来源。", "text": "点击任一条目弹出具体内容(公告全文来自数据爬取平台,有附件的可直接下载);每条说明中已标注数据来源。", "i18n": True, "otext": "点击任一条目弹出具体内容(公告全文来自数据爬取平台,有附件的可直接下载);每条说明中已标注数据来源。", "text": "点击任一条目弹出具体内容(公告全文来自数据爬取平台,有附件的可直接下载);每条说明中已标注数据来源。", "i18n": True,
"cfontsize": 0.78, "color": "#64748b", "halign": "left", "margin": "0 0 8px 0"}} "cfontsize": 0.78, "color": "#64748b", "halign": "left", "margin": "0 0 8px 0"}}