430 lines
24 KiB
Plaintext
430 lines
24 KiB
Plaintext
# ticket_detail_popup.dspy - 工单详情弹窗:正文与操作同屏(复核类弹窗规范)
|
||
# 页面入口与平台待办共用。入参: id=<ticket_id>
|
||
# 视角自适应:客户→确认解决/追问/取消;运维池→认领;受理人→回复客户/转派
|
||
# 附件:消息流内联展示(每行 打开/下载,区分上传方)+ 末尾「全部附件」汇总区;
|
||
# 追问/回复支持多附件上传(FormData multipart,落 FileStorage)
|
||
|
||
import json as _json
|
||
from urllib.parse import quote as _quote
|
||
|
||
user_id = await get_user()
|
||
if not user_id:
|
||
return {"widgettype": "Message", "options": {"title": "未登录", "message": "请先登录后查看待办"}}
|
||
|
||
ticket_id = ((params_kw or {}).get('id') or '').strip()
|
||
PW_ID = 'ticket_todo_detail_pw' if ((params_kw or {}).get('from_todo') or '') == '1' else 'ticket_detail_pw'
|
||
if not ticket_id:
|
||
return {"widgettype": "Message", "options": {"title": "打开失败", "message": "缺少工单 id"}}
|
||
|
||
org_id = await get_userorgid()
|
||
ok, t = await ticket_detail(ticket_id, user_id, org_id)
|
||
if not ok:
|
||
code, msg = t
|
||
return {"widgettype": "Message", "options": {"title": "打开失败", "message": msg}}
|
||
|
||
STATUS_LABEL = {
|
||
'new': '已提交', 'agent_processing': '智能助手处理中', 'agent_replied': '智能助手已回复',
|
||
'human_pending': '待人工认领', 'human_processing': '人工处理中',
|
||
'staff_replied': '人工已回复', 'closed': '已关闭', 'cancelled': '已取消',
|
||
}
|
||
STATUS_COLOR = {
|
||
'new': '#64748b', 'agent_processing': '#2563eb', 'agent_replied': '#10b981',
|
||
'human_pending': '#f0a040', 'human_processing': '#f0a040',
|
||
'staff_replied': '#10b981', 'closed': '#94a3b8', 'cancelled': '#94a3b8',
|
||
}
|
||
SENDER_LABEL = {'customer': '客户', 'agent': '智能助手', 'staff': '人工客服'}
|
||
# 上传方标识色:客户蓝 / 智能助手紫 / 人工客服橙——一眼区分附件是谁传的
|
||
SENDER_COLOR = {'customer': '#2563eb', 'agent': '#8b5cf6', 'staff': '#ea580c'}
|
||
|
||
status = str(t.get('status', ''))
|
||
view = str(t.get('view', 'customer'))
|
||
|
||
|
||
def _fmt_size(n):
|
||
try:
|
||
n = int(n or 0)
|
||
except Exception:
|
||
return ''
|
||
if n >= 1048576:
|
||
return '%.1fMB' % (n / 1048576.0)
|
||
if n >= 1024:
|
||
return '%.0fKB' % (n / 1024.0)
|
||
return '%dB' % n
|
||
|
||
|
||
def _parse_atts(raw):
|
||
if not raw:
|
||
return []
|
||
if isinstance(raw, list):
|
||
return [a for a in raw if isinstance(a, dict)]
|
||
try:
|
||
v = _json.loads(raw)
|
||
return [a for a in v if isinstance(a, dict)] if isinstance(v, list) else []
|
||
except Exception:
|
||
return []
|
||
|
||
|
||
def _att_urls(msg_id, att_id):
|
||
base = entire_url('/ticket/api/ticket_attachment.dspy')
|
||
open_url = base + '?msg_id=' + _quote(str(msg_id)) + '&att_id=' + _quote(str(att_id))
|
||
return open_url, open_url + '&download=1'
|
||
|
||
|
||
def _att_row(sender_type, who_name, fname, size, open_url, dl_url, when=''):
|
||
"""单条附件行:上传方徽标 + 📎文件名(大小) + 打开/下载按钮。"""
|
||
color = SENDER_COLOR.get(sender_type, '#64748b')
|
||
label = SENDER_LABEL.get(sender_type, sender_type)
|
||
if who_name:
|
||
label = label + '·' + str(who_name)
|
||
row = [{
|
||
"widgettype": "Text",
|
||
"options": {"text": label, "cfontsize": 0.72, "color": "#ffffff",
|
||
"bgcolor": color, "padding": "1px 8px", "borderRadius": "8px",
|
||
"whiteSpace": "nowrap"}}]
|
||
if when:
|
||
row.append({"widgettype": "Text",
|
||
"options": {"text": str(when)[:16], "cfontsize": 0.72,
|
||
"color": "#94a3b8", "whiteSpace": "nowrap"}})
|
||
row.append({
|
||
"widgettype": "Text",
|
||
"options": {"text": "📎 " + str(fname) + ('(' + _fmt_size(size) + ')' if size else ''),
|
||
"cfontsize": 0.85, "color": "#334155", "flex": "1 1 auto",
|
||
"overflow": "hidden", "textOverflow": "ellipsis",
|
||
"whiteSpace": "nowrap"}})
|
||
row.append({
|
||
"widgettype": "Button",
|
||
"options": {"label": "打开", "css": "small"},
|
||
"binds": [{"wid": "self", "event": "click", "actiontype": "script", "target": "self",
|
||
"script": "window.open(" + _json.dumps(open_url) + ",'_blank');"}]})
|
||
row.append({
|
||
"widgettype": "Button",
|
||
"options": {"label": "下载", "css": "small"},
|
||
"binds": [{"wid": "self", "event": "click", "actiontype": "script", "target": "self",
|
||
"script": "window.open(" + _json.dumps(dl_url) + ",'_blank');"}]})
|
||
return {"widgettype": "HBox",
|
||
"options": {"width": "100%", "gap": "8px", "padding": "4px 8px",
|
||
"alignItems": "center", "bgcolor": "#f8fafc",
|
||
"border": "1px solid #e2e8f0", "borderRadius": "6px"},
|
||
"subwidgets": row}
|
||
|
||
|
||
def _msg_widgets(m):
|
||
"""一条消息 → [正文MdWidget] + [附件行...]。"""
|
||
st = str(m.get('sender_type', ''))
|
||
who = SENDER_LABEL.get(st, st)
|
||
nick = str(m.get('sender_nick', '') or m.get('sender_name', '') or '')
|
||
if nick:
|
||
who = who + '·' + nick
|
||
vis = str(m.get('visibility', ''))
|
||
tag = '(内部备注)' if vis == 'internal' else ''
|
||
parts = ['', '**' + who + tag + '** · ' + str(m.get('created_at', ''))[:19],
|
||
'', str(m.get('content', ''))]
|
||
ws = [{"widgettype": "MdWidget",
|
||
"options": {"mdtext": '\n'.join(parts), "width": "100%"}}]
|
||
for a in _parse_atts(m.get('attachments')):
|
||
ou, du = _att_urls(m.get('id', ''), a.get('id', ''))
|
||
ws.append(_att_row(st, nick, a.get('name', ''), a.get('size', 0), ou, du))
|
||
return ws
|
||
|
||
|
||
# ── 正文区 widgets:工单信息 + 往来消息流(含内联附件)+ 全部附件汇总 + 流转记录 ──
|
||
head_md = ['## 工单 ' + str(t.get('ticket_no', '')), '',
|
||
'**标题**:' + str(t.get('title', '')),
|
||
'**状态**:' + STATUS_LABEL.get(status, status)
|
||
+ ' | **分类**:' + str(t.get('category', ''))
|
||
+ ' | **优先级**:' + str(t.get('priority', ''))]
|
||
if view == 'staff':
|
||
head_md.append('**客户机构**:' + str(t.get('customer_org_id', ''))
|
||
+ ' | **受理角色**:' + (str(t.get('assignee_role', '')) or '-')
|
||
+ ' | **受理人**:' + (str(t.get('assignee_id', '')) or '-'))
|
||
head_md += ['', '---', '### 往来记录']
|
||
|
||
body_widgets = [{"widgettype": "MdWidget",
|
||
"options": {"mdtext": '\n'.join(head_md), "width": "100%"}}]
|
||
messages = t.get('messages') or []
|
||
for m in messages:
|
||
body_widgets.extend(_msg_widgets(m))
|
||
|
||
# 全部附件汇总区:跨消息聚合,区分上传方,可直接打开/下载
|
||
_all_atts = []
|
||
for m in messages:
|
||
for a in _parse_atts(m.get('attachments')):
|
||
_all_atts.append((m, a))
|
||
if _all_atts:
|
||
body_widgets.append({"widgettype": "MdWidget",
|
||
"options": {"mdtext": '\n'.join(['', '---',
|
||
'### 全部附件(%d 个)' % len(_all_atts)]),
|
||
"width": "100%"}})
|
||
for m, a in _all_atts:
|
||
nick = str(m.get('sender_nick', '') or m.get('sender_name', '') or '')
|
||
ou, du = _att_urls(m.get('id', ''), a.get('id', ''))
|
||
body_widgets.append(_att_row(str(m.get('sender_type', '')), nick,
|
||
a.get('name', ''), a.get('size', 0), ou, du,
|
||
when=m.get('created_at', '')))
|
||
|
||
if view == 'staff' and (t.get('transfers') or []):
|
||
tr_md = ['', '---', '### 流转记录(内部)']
|
||
ACTION_LABEL = {'escalate_to_human': '转人工', 'claim': '认领',
|
||
'transfer_role': '转角色', 'transfer_user': '转人员'}
|
||
for tr in (t.get('transfers') or []):
|
||
act = ACTION_LABEL.get(str(tr.get('action', '')), str(tr.get('action', '')))
|
||
line = '- ' + str(tr.get('created_at', ''))[:19] + ' ' + act
|
||
if tr.get('to_role'):
|
||
line += ' → 角色 ' + str(tr.get('to_role'))
|
||
if tr.get('to_user'):
|
||
line += ' → 人员 ' + str(tr.get('to_user'))
|
||
if tr.get('reason'):
|
||
line += '(' + str(tr.get('reason'))[:80] + ')'
|
||
tr_md.append(line)
|
||
body_widgets.append({"widgettype": "MdWidget",
|
||
"options": {"mdtext": '\n'.join(tr_md), "width": "100%"}})
|
||
|
||
# ── 操作按钮 ──
|
||
base = entire_url('/ticket/api')
|
||
confirm_url = base + '/ticket_confirm.dspy'
|
||
followup_url = base + '/ticket_followup.dspy'
|
||
cancel_url = base + '/ticket_cancel.dspy'
|
||
claim_url = base + '/ticket_claim.dspy'
|
||
reply_url = base + '/ticket_staff_reply.dspy'
|
||
transfer_url = base + '/ticket_transfer.dspy'
|
||
candidates_url = base + '/transfer_candidates.dspy'
|
||
|
||
_tail = ("if(d.success){"
|
||
"var pw=bricks.getWidgetById('" + PW_ID + "',bricks.app);if(pw){pw.destroy();}"
|
||
"var lw=bricks.getWidgetById('my_todos_pw',bricks.app);if(lw){lw.destroy();}"
|
||
"if(window.refreshTodo){window.refreshTodo();}"
|
||
"var mo=new bricks.Message({title:'已处理',message:d.message||'处理成功'});mo.open();"
|
||
"}else{var mf=new bricks.Message({title:'处理失败',message:d.error||'处理失败'});mf.open();}")
|
||
|
||
|
||
def _read_input_js(wid):
|
||
return ("var cw=bricks.getWidgetById(" + _json.dumps(wid) + ",bricks.app);var cv='';"
|
||
"if(cw){cv=(typeof cw.resultValue==='function')?cw.resultValue():"
|
||
"((cw.dom_element&&cw.dom_element.value)||'');}"
|
||
"cv=(cv===null||cv===undefined)?'':String(cv).trim();")
|
||
|
||
|
||
def _read_files_js(wid):
|
||
"""读 UiFile(multiple)选中的 File 数组 → JS 变量 _fs,并做数量/总量前端拦截。
|
||
总量 9.5MB:pipeline-app client_max_size=10MB,超了请求会被拒/挂起,必须前端拦。"""
|
||
return ("var _fw=bricks.getWidgetById(" + _json.dumps(wid) + ",bricks.app);var _fs=[];"
|
||
"if(_fw&&typeof _fw.resultValue==='function'){var _fv=_fw.resultValue();"
|
||
"if(_fv){_fs=Array.isArray(_fv)?_fv:[_fv];}}"
|
||
"var _tot=0;for(var _ti=0;_ti<_fs.length;_ti++){_tot+=(_fs[_ti].size||0);}"
|
||
"if(_fs.length>9){new bricks.Message({title:'附件过多',"
|
||
"message:'单条消息最多上传 9 个附件'}).open();return;}"
|
||
"if(_tot>9.5*1024*1024){new bricks.Message({title:'附件过大',"
|
||
"message:'单次提交附件总大小不能超过 9.5MB'}).open();return;}")
|
||
|
||
|
||
def _post_js(url, body_expr):
|
||
# body_expr 为 dict 时序列化为 JS 对象字面量(2026-09-10 修复:原实现直接
|
||
# 字符串拼接 dict → TypeError 500,所有带按钮的弹窗路径全炸;此前 staff_replied
|
||
# 态不进任何按钮分支才掩盖了此bug)。占位符 "cv_placeholder" 后续替换为 JS 变量 cv。
|
||
if isinstance(body_expr, dict):
|
||
body_js = _json.dumps(body_expr)
|
||
else:
|
||
body_js = str(body_expr)
|
||
return ("var r=await fetch(" + _json.dumps(url) + ",{method:'POST',"
|
||
"headers:{'Content-Type':'application/json'},body:JSON.stringify(" + body_js + ")});"
|
||
"var d=await r.json();" + _tail)
|
||
|
||
|
||
def _post_multipart_js(url, tid, text_var='_fs', extra=None):
|
||
"""FormData multipart 提交(带附件)。文本字段 content 取 JS 变量 cv;
|
||
附件字段 attachments 逐个 append File;extra: dict 附加表单字段。
|
||
不设 Content-Type——浏览器自动带 multipart boundary。"""
|
||
js = ("var fd=new FormData();"
|
||
"fd.append('ticket_id'," + _json.dumps(tid) + ");"
|
||
"fd.append('content',cv);")
|
||
for k, v in (extra or {}).items():
|
||
js += "fd.append(" + _json.dumps(k) + "," + _json.dumps(v) + ");"
|
||
js += ("for(var _fi=0;_fi<" + text_var + ".length;_fi++){"
|
||
"fd.append('attachments'," + text_var + "[_fi]);}"
|
||
"var r=await fetch(" + _json.dumps(url) + ",{method:'POST',body:fd});"
|
||
"var d=await r.json();" + _tail)
|
||
return js
|
||
|
||
|
||
def _upload_box(wid, tip):
|
||
return {
|
||
"widgettype": "VBox",
|
||
"options": {"width": "100%", "gap": "2px"},
|
||
"subwidgets": [
|
||
{"widgettype": "Text",
|
||
"options": {"text": tip, "cfontsize": 0.72, "color": "#94a3b8"}},
|
||
{"widgettype": "UiFile", "id": wid,
|
||
"options": {"name": wid, "width": "100%", "multiple": True,
|
||
"preview": True}}]}
|
||
|
||
|
||
buttons = []
|
||
input_widgets = []
|
||
|
||
if view == 'customer':
|
||
if status in ('agent_replied', 'staff_replied'):
|
||
buttons.append({
|
||
"widgettype": "Button",
|
||
"options": {"label": "✔ 确认解决并关闭", "css": "primary"},
|
||
"binds": [{"wid": "self", "event": "click", "actiontype": "script", "target": "self",
|
||
"script": _post_js(confirm_url, {"ticket_id": ticket_id})}]})
|
||
input_widgets.append({
|
||
"widgettype": "UiText", "id": "tk_followup_input",
|
||
"options": {"name": "tk_followup_input", "placeholder": "问题没解决?在此输入追问内容…",
|
||
"width": "100%", "height": "70px"}})
|
||
input_widgets.append(_upload_box('tk_followup_files',
|
||
'追问附件(可选):最多 9 个,总大小 ≤9.5MB,可拖入或点击选择'))
|
||
buttons.append({
|
||
"widgettype": "Button",
|
||
"options": {"label": "追问", "css": "small"},
|
||
"binds": [{"wid": "self", "event": "click", "actiontype": "script", "target": "self",
|
||
"script": (_read_input_js('tk_followup_input')
|
||
+ "if(!cv){new bricks.Message({title:'请填写追问内容',message:'追问内容不能为空'}).open();return;}"
|
||
+ _read_files_js('tk_followup_files')
|
||
+ _post_multipart_js(followup_url, ticket_id))}]})
|
||
buttons.append({
|
||
"widgettype": "Button",
|
||
"options": {"label": "取消工单", "css": "small danger"},
|
||
"binds": [{"wid": "self", "event": "click", "actiontype": "script", "target": "self",
|
||
"script": _post_js(cancel_url, {"ticket_id": ticket_id})}]})
|
||
elif status in ('new', 'agent_processing'):
|
||
buttons.append({
|
||
"widgettype": "Button",
|
||
"options": {"label": "取消工单", "css": "small danger"},
|
||
"binds": [{"wid": "self", "event": "click", "actiontype": "script", "target": "self",
|
||
"script": _post_js(cancel_url, {"ticket_id": ticket_id})}]})
|
||
else:
|
||
# staff 视角
|
||
if status == 'human_pending':
|
||
buttons.append({
|
||
"widgettype": "Button",
|
||
"options": {"label": "认领工单", "css": "primary"},
|
||
"binds": [{"wid": "self", "event": "click", "actiontype": "script", "target": "self",
|
||
"script": _post_js(claim_url, {"ticket_id": ticket_id})}]})
|
||
elif status == 'human_processing':
|
||
input_widgets.append({
|
||
"widgettype": "UiText", "id": "tk_reply_input",
|
||
"options": {"name": "tk_reply_input", "placeholder": "输入给客户的回复(markdown)…",
|
||
"width": "100%", "height": "90px"}})
|
||
input_widgets.append(_upload_box('tk_reply_files',
|
||
'回复附件(可选):最多 9 个,总大小 ≤9.5MB,可拖入或点击选择'))
|
||
buttons.append({
|
||
"widgettype": "Button",
|
||
"options": {"label": "回复客户", "css": "primary"},
|
||
"binds": [{"wid": "self", "event": "click", "actiontype": "script", "target": "self",
|
||
"script": (_read_input_js('tk_reply_input')
|
||
+ "if(!cv){new bricks.Message({title:'请填写回复',message:'回复内容不能为空'}).open();return;}"
|
||
+ _read_files_js('tk_reply_files')
|
||
+ _post_multipart_js(reply_url, ticket_id))}]})
|
||
buttons.append({
|
||
"widgettype": "Button",
|
||
"options": {"label": "记内部备注", "css": "small"},
|
||
"binds": [{"wid": "self", "event": "click", "actiontype": "script", "target": "self",
|
||
"script": (_read_input_js('tk_reply_input')
|
||
+ "if(!cv){new bricks.Message({title:'请填写备注',message:'备注不能为空'}).open();return;}"
|
||
+ _read_files_js('tk_reply_files')
|
||
+ _post_multipart_js(reply_url, ticket_id,
|
||
extra={"internal": "1"}))}]})
|
||
buttons.append({
|
||
"widgettype": "Button",
|
||
"options": {"label": "转派…", "css": "small"},
|
||
"binds": [{"wid": "self", "event": "click", "actiontype": "script", "target": "self",
|
||
"script": ("var r=await fetch(" + _json.dumps(candidates_url) + ");var d=await r.json();"
|
||
"if(!d.success){new bricks.Message({title:'加载失败',message:d.error||''}).open();return;}"
|
||
"var ropts=(d.data.roles||[]).map(function(x){return {value:x.value,text:'角色:'+x.text};});"
|
||
"var uopts=(d.data.users||[]).map(function(x){return {value:'u:'+x.value,text:'人员:'+x.text};});"
|
||
"var pw2=bricks.getWidgetById('ticket_transfer_pw',bricks.app);if(pw2){pw2.destroy();}"
|
||
"var desc={widgettype:'PopupWindow',id:'ticket_transfer_pw',options:{title:'转派工单',width:'34%',height:'46%',auto_open:true,archor:'cc',movable:true},"
|
||
"subwidgets:[{widgettype:'VBox',options:{padding:'14px',gap:'10px'},subwidgets:["
|
||
"{widgettype:'UiCode',id:'tk_tr_target',options:{name:'tk_tr_target',placeholder:'选择转派目标',data:ropts.concat(uopts),valueField:'value',textField:'text',width:'100%'}},"
|
||
"{widgettype:'UiText',id:'tk_tr_reason',options:{name:'tk_tr_reason',placeholder:'转派原因(必填)',width:'100%',height:'60px'}},"
|
||
"{widgettype:'Button',options:{label:'确认转派',css:'primary'},binds:[{wid:'self',event:'click',actiontype:'script',target:'self',script:"
|
||
+ _json.dumps(
|
||
"var tw=bricks.getWidgetById('tk_tr_target',bricks.app);var tv=tw?(typeof tw.resultValue==='function'?tw.resultValue():''):'';tv=String(tv||'').trim();"
|
||
"var rw=bricks.getWidgetById('tk_tr_reason',bricks.app);var rv=rw?(typeof rw.resultValue==='function'?rw.resultValue():((rw.dom_element&&rw.dom_element.value)||'')):'';rv=String(rv||'').trim();"
|
||
"if(!tv){new bricks.Message({title:'请选择目标',message:'选择转派角色或人员'}).open();return;}"
|
||
"if(!rv){new bricks.Message({title:'请填写原因',message:'转派原因必填'}).open();return;}"
|
||
"var body={ticket_id:" + _json.dumps(ticket_id) + ",reason:rv};"
|
||
"if(tv.indexOf('u:')===0){body.to_user=tv.substring(2);}else{body.to_role=tv;}"
|
||
"var r2=await fetch(" + _json.dumps(transfer_url) + ",{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});"
|
||
"var d2=await r2.json();"
|
||
"if(d2.success){var p2=bricks.getWidgetById('ticket_transfer_pw',bricks.app);if(p2){p2.destroy();}"
|
||
"var pw=bricks.getWidgetById('" + PW_ID + "',bricks.app);if(pw){pw.destroy();}"
|
||
"var lw=bricks.getWidgetById('my_todos_pw',bricks.app);if(lw){lw.destroy();}"
|
||
"if(window.refreshTodo){window.refreshTodo();}"
|
||
"new bricks.Message({title:'已转派',message:d2.message||'转派成功'}).open();}"
|
||
"else{new bricks.Message({title:'转派失败',message:d2.error||''}).open();}")
|
||
+ "}]}]}}]};bricks.widgetBuild(desc,bricks.app);")}]})
|
||
|
||
# 双身份用户(既是工单客户又是staff,如平台管理员自建工单):
|
||
# staff 视角在 agent_replied/staff_replied 态无任何按钮,但此刻待办要求的动作是
|
||
# 「客户确认/追问」——缺按钮导致待办永远清不掉(2026-09-10 用户报障)。
|
||
# 补:staff 视角下若当前用户就是该工单客户,追加客户动作按钮。
|
||
if view == 'staff' and str(t.get('customer_user_id', '')) == str(user_id) \
|
||
and status in ('agent_replied', 'staff_replied'):
|
||
buttons.append({
|
||
"widgettype": "Button",
|
||
"options": {"label": "✔ 确认解决并关闭(客户动作)", "css": "primary"},
|
||
"binds": [{"wid": "self", "event": "click", "actiontype": "script", "target": "self",
|
||
"script": _post_js(confirm_url, {"ticket_id": ticket_id})}]})
|
||
input_widgets.append({
|
||
"widgettype": "UiText", "id": "tk_followup_input",
|
||
"options": {"name": "tk_followup_input", "placeholder": "问题没解决?在此输入追问内容…",
|
||
"width": "100%", "height": "70px"}})
|
||
input_widgets.append(_upload_box('tk_followup_files',
|
||
'追问附件(可选):最多 9 个,总大小 ≤9.5MB,可拖入或点击选择'))
|
||
buttons.append({
|
||
"widgettype": "Button",
|
||
"options": {"label": "追问(客户动作)", "css": "small"},
|
||
"binds": [{"wid": "self", "event": "click", "actiontype": "script", "target": "self",
|
||
"script": (_read_input_js('tk_followup_input')
|
||
+ "if(!cv){new bricks.Message({title:'请填写追问内容',message:'追问内容不能为空'}).open();return;}"
|
||
+ _read_files_js('tk_followup_files')
|
||
+ _post_multipart_js(followup_url, ticket_id))}]})
|
||
|
||
# 追问/回复输入框 content 占位符替换为 JS 变量 cv(JSON 通道遗留兼容)
|
||
for b in buttons:
|
||
for bind in (b.get('binds') or []):
|
||
if 'cv_placeholder' in str(bind.get('script', '')):
|
||
bind['script'] = bind['script'].replace('"cv_placeholder"', 'cv')
|
||
|
||
sub = [{
|
||
"widgettype": "HBox",
|
||
"options": {"width": "100%", "gap": "8px", "alignItems": "center"},
|
||
"subwidgets": [
|
||
{"widgettype": "Text",
|
||
"options": {"text": STATUS_LABEL.get(status, status), "cfontsize": 0.75,
|
||
"color": "#ffffff", "padding": "2px 10px",
|
||
"bgcolor": STATUS_COLOR.get(status, '#64748b'),
|
||
"borderRadius": "10px", "whiteSpace": "nowrap"}},
|
||
{"widgettype": "Text",
|
||
"options": {"text": str(t.get('title', '')), "cfontsize": 1.1, "halign": "left"}}
|
||
]
|
||
}, {
|
||
"widgettype": "VBox",
|
||
"options": {"width": "100%", "flex": "1 1 auto", "minHeight": "200px",
|
||
"border": "1px solid #e2e8f0", "borderRadius": "8px", "overflow": "hidden"},
|
||
"subwidgets": [{
|
||
"widgettype": "VScrollPanel",
|
||
"options": {"css": "filler", "width": "100%", "padding": "6px 12px"},
|
||
"subwidgets": body_widgets}]
|
||
}]
|
||
sub.extend(input_widgets)
|
||
if buttons:
|
||
sub.append({
|
||
"widgettype": "HBox",
|
||
"options": {"width": "100%", "gap": "8px", "justifyContent": "flex-end"},
|
||
"subwidgets": buttons})
|
||
|
||
return {
|
||
"widgettype": "PopupWindow",
|
||
"id": PW_ID,
|
||
"options": {"title": "工单详情", "width": "72%", "height": "82%",
|
||
"auto_open": True, "resizable": True},
|
||
"subwidgets": [{
|
||
"widgettype": "VBox",
|
||
"options": {"css": "filler", "width": "100%", "height": "100%",
|
||
"padding": "12px 16px", "gap": "10px"},
|
||
"subwidgets": sub}]
|
||
}
|