# opp_demand_items.dspy - 某软件主题的众包需求分项列表(国内猪八戒/一品威客) # 入参:category=<主题名>,sort=time|budget(2026-09-15 用户需求:支持按赏金 # 大到小 / 按时间后到前排序;排序在数据源侧全量执行后再截断,缺省 time)。 # 交互(2026-09-08 用户定夺): # 每条分项整行点击 → opp_item_detail.dspy 弹出具体内容(爬取平台数据+附件下载); # 来源写进每条说明文字(来源:猪八戒/一品威客),不再渲染来源按钮。 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_demand_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, record_type="demand", sort=sort) 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 = [] 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" % ( str(it.get("publish_time") or "")[:10], (("赏金 %.2f 万" % budget) if budget else "赏金面议"), src_name) 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": "›", "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: 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_demand_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 }] }