feat: add view_assets page — browse media by group with bricks widgets

- init.py: add rl_list_assets_client() — validates org ownership, syncs
  from vendor, returns asset list with status/url/type
- view_assets.ui: dropdown to select vendor_group_id, submit triggers
  asset list display
- submit_list_assets.dspy: returns responsive card grid with Image/
  VideoPlayer/AudioPlayer widgets, per-asset refresh status + download
- index.ui: add '查看素材' card for customers
- load_path.py: register view_assets.ui under logined RBAC
This commit is contained in:
yumoqing 2026-05-30 10:31:08 +08:00
parent daf82107fb
commit 6491182249
5 changed files with 286 additions and 0 deletions

View File

@ -584,6 +584,52 @@ async def rl_sync_asset_status_user(org_id, asset_id, user_id):
return {"success": True, "status": status, "url": url}
async def rl_list_assets_client(org_id, vendor_group_id):
"""Client API: List assets for a vendor_group_id, sync from vendor first."""
dbname = _get_dbname()
db = DBPools()
# Validate org owns this group, get vendor + local_group_id
async with db.sqlorContext(dbname) as sor:
grp_recs = await sor.R("rl_org_group", {
"org_id": org_id,
"vendor_group_id": vendor_group_id,
})
if not grp_recs:
return {"success": False, "message": "无效的认证组合或无权访问"}
vendor = grp_recs[0].vendor
local_group_id = grp_recs[0].local_group_id
# Sync from vendor to get fresh status
try:
await rl_sync_assets_from_vendor(org_id, local_group_id)
except Exception as e:
debug(f"rl_list_assets_client: vendor sync failed: {e}")
# Query local assets
async with db.sqlorContext(dbname) as sor:
recs = await sor.R("rl_asset", {
"org_id": org_id,
"group_id": local_group_id,
})
if not recs:
return {"success": True, "assets": []}
assets = []
for r in recs:
assets.append({
"id": r.id,
"name": getattr(r, "name", ""),
"status": getattr(r, "status", ""),
"url": getattr(r, "url", ""),
"asset_type": getattr(r, "asset_type", ""),
"vendor_asset_id": getattr(r, "vendor_asset_id", ""),
"create_time": getattr(r, "create_time", ""),
})
return {"success": True, "assets": assets}
async def rl_handle_callback(byted_token, project_name="default"):
"""
Callback handler: vendor POSTs here after H5 auth completes.
@ -719,6 +765,7 @@ def load_reallife_asset():
g.rl_verify_user = rl_verify_user
g.rl_upload_user = rl_upload_user
g.rl_sync_asset_status_user = rl_sync_asset_status_user
g.rl_list_assets_client = rl_list_assets_client
g.rl_handle_callback = rl_handle_callback
g.rl_query_groups = rl_query_groups

View File

@ -56,6 +56,7 @@ PATHS_LOGINED = [
# 客户可用页面
f"/{MOD}/create_validate.ui",
f"/{MOD}/upload_asset.ui",
f"/{MOD}/view_assets.ui",
# API — 所有 api/ 下的 .dspy脚本内部通过 get_user() 做权限校验)
f"/{MOD}/api/%",

View File

@ -0,0 +1,146 @@
vendor_group_id = params_kw.get('vendor_group_id', '')
if not vendor_group_id:
return json.dumps({
"widgettype": "Error",
"options": {"title": "错误", "message": "请选择认证组合"}
})
org_id = (await get_userorgid()) or '0'
result = await rl_list_assets_client(org_id, vendor_group_id)
if not result.get('success'):
return json.dumps({
"widgettype": "Error",
"options": {"title": "查询失败", "message": result.get('message', '未知错误')}
})
assets = result.get('assets', [])
if not assets:
return json.dumps({
"widgettype": "VBox",
"id": "rl_asset_results",
"options": {"padding": "16px", "gap": "12px"},
"subwidgets": [
{
"widgettype": "Text",
"options": {"text": "该认证组合下暂无素材,请先上传素材。"}
}
]
})
# Build asset cards
cards = []
for a in assets:
status = a.get('status', '')
name = a.get('name', '')
url = a.get('url', '')
atype = a.get('asset_type', '')
create_time = str(a.get('create_time', ''))[:16]
asset_id = a.get('id', '')
vendor_asset_id = a.get('vendor_asset_id', '')
# Status icon
s_lower = status.lower() if status else ''
if s_lower in ('active', 'available', 'ready'):
status_icon = "✅"
elif s_lower in ('processing', 'pending', 'submitted'):
status_icon = "⏳"
elif s_lower in ('failed', 'error'):
status_icon = "❌"
else:
status_icon = "📋"
# Build card subwidgets
card_subs = [
{
"widgettype": "HBox",
"options": {"gap": "8px", "alignItems": "center", "marginBottom": "4px"},
"subwidgets": [
{"widgettype": "Text", "options": {"text": name or "未命名", "fontWeight": "bold"}},
{"widgettype": "Text", "options": {"text": f"{status_icon} {status}", "cfontsize": 0.9}}
]
},
{
"widgettype": "Text",
"options": {"text": f"类型: {atype} 创建: {create_time}", "cfontsize": 0.9}
}
]
# Media preview
if url:
if atype == 'Image':
card_subs.append({
"widgettype": "Image",
"options": {"url": url, "cwidth": 15, "cheight": 10, "objectFit": "cover", "borderRadius": "4px", "marginTop": "8px"}
})
elif atype == 'Video':
card_subs.append({
"widgettype": "VideoPlayer",
"options": {"url": url, "cwidth": 20, "cheight": 12, "marginTop": "8px"}
})
elif atype == 'Audio':
card_subs.append({
"widgettype": "AudioPlayer",
"options": {"url": url, "marginTop": "8px"}
})
# Refresh status button (always show for processing assets, optional for others)
check_url = request.path.rsplit('/', 1)[0] + '/submit_query_status.dspy'
btn_subs = [
{
"widgettype": "Button",
"options": {"label": "刷新状态"},
"binds": [{
"wid": "self", "event": "click",
"actiontype": "script", "target": "self",
"script": "(async function(){" \
"var url='" + check_url + "?_webbricks_=1&asset_id=" + asset_id + "';" \
"var r=await fetch(url);" \
"var j=await r.json();" \
"await bricks.show_resp_message_or_error({json:async function(){return j}});" \
"})()"
}]
}
]
if url:
btn_subs.append({
"widgettype": "Button",
"options": {"label": "下载"},
"binds": [{
"wid": "self", "event": "click",
"actiontype": "script", "target": "self",
"script": "window.open('" + url + "', '_blank')"
}]
})
card_subs.append({
"widgettype": "HBox",
"options": {"gap": "8px", "marginTop": "8px"},
"subwidgets": btn_subs
})
cards.append({
"widgettype": "VBox",
"options": {"css": "card", "padding": "12px", "borderRadius": "8px"},
"subwidgets": card_subs
})
# Wrap cards in ResponsableBox for responsive grid
result_widget = {
"widgettype": "VBox",
"id": "rl_asset_results",
"options": {"padding": "16px", "gap": "12px"},
"subwidgets": [
{"widgettype": "Text", "options": {"text": f"共 {len(assets)} 个素材", "fontWeight": "bold"}},
{
"widgettype": "ResponsableBox",
"options": {"gap": "12px", "minWidth": "300px"},
"subwidgets": cards
}
]
}
return json.dumps(result_widget, ensure_ascii=False)

View File

@ -121,6 +121,44 @@
}
}
]
},
{
"widgettype": "VBox",
"options": {
"css": "card",
"cheight": 12,
"cwidth": 23,
"padding": "20px",
"cursor": "pointer",
"borderRadius": "8px"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "app.rl_content",
"options": {
"url": "{{entire_url('view_assets.ui')}}"
},
"mode": "replace"
}
],
"subwidgets": [
{
"widgettype": "Title5",
"options": {
"text": "🖼️ 查看素材"
}
},
{
"widgettype": "Text",
"options": {
"text": "按认证组合查看已上传的素材及状态",
"cfontsize": 1.2
}
}
]
}
{% endif %}
{% if is_admin %}

54
wwwroot/view_assets.ui Normal file
View File

@ -0,0 +1,54 @@
{% set current_group = params_kw.get('vendor_group_id', '') %}
{
"widgettype": "VBox",
"options": {
"width": "100%",
"padding": "16px"
},
"subwidgets": [
{
"widgettype": "Title4",
"options": {
"text": "查看素材",
"fontWeight": "600",
"marginBottom": "16px"
}
},
{
"widgettype": "Form",
"id": "view_assets_form",
"options": {
"submit_url": "{{entire_url('api/submit_list_assets.dspy')}}",
"fields": [
{
"uitype": "code",
"name": "vendor_group_id",
"label": "认证组合",
"dataurl": "{{entire_url('api/get_org_groups.dspy')}}",
"data_field": "value",
"text_field": "text",
"required": true,
"value": "{{ current_group }}"
}
]
},
"binds": [
{
"wid": "self",
"event": "submited",
"actiontype": "urlwidget",
"target": "app.rl_asset_results",
"options": {
"method": "POST"
},
"mode": "replace"
}
]
},
{
"widgettype": "VBox",
"id": "rl_asset_results",
"options": {"padding": "16px"}
}
]
}