trustbase-seller-backend 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/routes/sp-applyment.js +41 -10
- package/test-sp-applyment.js +30 -1
package/package.json
CHANGED
package/routes/sp-applyment.js
CHANGED
|
@@ -480,6 +480,11 @@ input:focus,select:focus{outline:none;border-color:#C4633C;box-shadow:0 0 0 3px
|
|
|
480
480
|
.tag{font-size:11px;border:1px solid #D4CDBF;border-radius:999px;padding:1px 8px;color:#6B655B;margin-left:4px}
|
|
481
481
|
.msg{font-size:13px;margin-top:8px}
|
|
482
482
|
.msg.err{color:#DC2626}.msg.ok{color:#16A34A}
|
|
483
|
+
#toast{position:fixed;top:16px;right:16px;z-index:99;display:flex;flex-direction:column;gap:8px;pointer-events:none}
|
|
484
|
+
.toast{pointer-events:auto;border-radius:10px;padding:12px 18px;font-size:14px;box-shadow:0 4px 16px rgba(51,48,43,.12);animation:tin .18s ease-out}
|
|
485
|
+
.toast.ok{background:#F0FDF4;border:1px solid #BBF7D0;color:#166534}
|
|
486
|
+
.toast.err{background:#FEF2F2;border:1px solid #FCA5A5;color:#991B1B}
|
|
487
|
+
@keyframes tin{from{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:none}}
|
|
483
488
|
@media (max-width:640px){.slots{grid-template-columns:1fr}.app-row{flex-direction:column;align-items:flex-start}}
|
|
484
489
|
</style>
|
|
485
490
|
</head>
|
|
@@ -488,6 +493,8 @@ input:focus,select:focus{outline:none;border-color:#C4633C;box-shadow:0 0 0 3px
|
|
|
488
493
|
<h1>服务商配置</h1>
|
|
489
494
|
<div class="sub">TrustBase 服务商运营台 · 应用/名片/状态 一屏三件事</div>
|
|
490
495
|
|
|
496
|
+
<div id="toast"></div>
|
|
497
|
+
|
|
491
498
|
<div class="chips" id="chips"></div>
|
|
492
499
|
|
|
493
500
|
<div class="box">
|
|
@@ -539,7 +546,21 @@ function api(path,opts){
|
|
|
539
546
|
function esc(s){return String(s==null?'':s).replace(/&/g,'&').replace(/</g,'<');}
|
|
540
547
|
function maskMid(m){m=String(m||'');return m.length>16?m.slice(0,12)+'…'+m.slice(-4):m;}
|
|
541
548
|
function maskApp(a){a=String(a||'');return a.length>10?a.slice(0,6)+'****'+a.slice(-4):a;}
|
|
542
|
-
var TYPE_LABEL={app:'APP',mini_program:'小程序',web:'网站'};
|
|
549
|
+
var TYPE_LABEL={app:'APP',mini_program:'小程序',web:'网站'};
|
|
550
|
+
// 操作反馈 toast (2026-10-03 xiaomu 实测: 保存无提示): 右上浮层, 成功绿/失败红, 2.5s 自动消失
|
|
551
|
+
function showToast(ok,text){
|
|
552
|
+
var box=document.getElementById('toast');
|
|
553
|
+
var t=document.createElement('div');
|
|
554
|
+
t.className='toast '+(ok?'ok':'err');
|
|
555
|
+
t.textContent=text;
|
|
556
|
+
box.appendChild(t);
|
|
557
|
+
setTimeout(function(){t.style.opacity='0';t.style.transition='opacity .3s';setTimeout(function(){t.remove();},300);},2500);
|
|
558
|
+
}
|
|
559
|
+
function busy(btn,on,txtBusy){
|
|
560
|
+
if(!btn) return;
|
|
561
|
+
if(on){btn.dataset.txt=btn.textContent;btn.disabled=true;btn.textContent=txtBusy||'保存中…';}
|
|
562
|
+
else{btn.disabled=false;btn.textContent=btn.dataset.txt||btn.textContent;}
|
|
563
|
+
}
|
|
543
564
|
|
|
544
565
|
function render(){
|
|
545
566
|
var c=CFG, comp=c.completeness;
|
|
@@ -574,8 +595,8 @@ function renderSlots(){
|
|
|
574
595
|
return '<div class="slot"><div class="t">'+kk[1]+'</div>'
|
|
575
596
|
+(mid?'<div class="mid">✓ '+esc(maskMid(mid))+'</div>':'')
|
|
576
597
|
+'<img id="img-'+kk[0]+'" style="display:none">'
|
|
577
|
-
+'<label class="upbtn">'+(mid?'替换':'点击上传')+'<input type="file" accept="image/*" style="display:none" onchange="upFormAsset(event
|
|
578
|
-
+(mid?'<button class="upbtn del" onclick="formAssets[
|
|
598
|
+
+'<label class="upbtn">'+(mid?'替换':'点击上传')+'<input type="file" accept="image/*" style="display:none" onchange="upFormAsset(event,\\x27'+kk[0]+'\\x27)"></label>'
|
|
599
|
+
+(mid?'<button class="upbtn del" onclick="formAssets[\\x27'+kk[0]+'\\x27]=\\x27\\x27;renderSlots()">删除</button>':'')
|
|
579
600
|
+'</div>';
|
|
580
601
|
}).join('');
|
|
581
602
|
}
|
|
@@ -612,25 +633,31 @@ function resetForm(){
|
|
|
612
633
|
}
|
|
613
634
|
async function saveApp(){
|
|
614
635
|
var m=document.getElementById('app-msg');
|
|
636
|
+
var btn=document.querySelector('.box .btn[onclick="saveApp()"]');
|
|
615
637
|
var body={name:document.getElementById('f_name').value.trim(),
|
|
616
638
|
appid:document.getElementById('f_appid').value.trim(),
|
|
617
639
|
type:document.getElementById('f_type').value,
|
|
618
640
|
assets:formAssets, is_default:document.getElementById('f_default').checked};
|
|
619
|
-
if(!body.name||!body.appid){m.className='msg err';m.textContent='应用名称和 AppID 必填';return;}
|
|
641
|
+
if(!body.name||!body.appid){showToast(false,'应用名称和 AppID 必填');m.className='msg err';m.textContent='应用名称和 AppID 必填';return;}
|
|
642
|
+
busy(btn,true);
|
|
620
643
|
var r=editingId?await api('/api/sp/apps/'+editingId,{method:'PUT',body:JSON.stringify(body)})
|
|
621
644
|
:await api('/api/sp/apps',{method:'POST',body:JSON.stringify(body)});
|
|
645
|
+
busy(btn,false);
|
|
622
646
|
m.className='msg '+(r.json.ok?'ok':'err');
|
|
623
647
|
m.textContent=r.json.ok?'✓ 已保存':(r.json.message||'保存失败');
|
|
624
|
-
if(r.json.ok){resetForm();load();}
|
|
648
|
+
if(r.json.ok){showToast(true,'✓ 已保存,商户端立即生效');resetForm();load();}
|
|
649
|
+
else showToast(false,r.json.message||'保存失败');
|
|
625
650
|
}
|
|
626
651
|
async function setDefault(id){
|
|
627
652
|
var r=await api('/api/sp/apps/'+id,{method:'PUT',body:JSON.stringify({is_default:true})});
|
|
628
|
-
if(r.json.ok) load();
|
|
653
|
+
if(r.json.ok){showToast(true,'✓ 已设为默认,商户端立即生效');load();}
|
|
654
|
+
else showToast(false,r.json.message||'操作失败');
|
|
629
655
|
}
|
|
630
656
|
async function delApp(id){
|
|
631
657
|
if(!confirm('删除这个应用? (不影响已提交的进件)')) return;
|
|
632
658
|
var r=await api('/api/sp/apps/'+id,{method:'DELETE'});
|
|
633
|
-
if(r.json.ok) load();
|
|
659
|
+
if(r.json.ok){showToast(true,'✓ 已删除,商户端立即生效');load();}
|
|
660
|
+
else showToast(false,r.json.message||'删除失败');
|
|
634
661
|
}
|
|
635
662
|
function shrink(file,cb){
|
|
636
663
|
var rd=new FileReader();
|
|
@@ -655,11 +682,13 @@ async function upFormAsset(ev,kind){
|
|
|
655
682
|
shrink(file,async function(err,b64){
|
|
656
683
|
if(err){m.className='msg err';m.textContent=err.message;return;}
|
|
657
684
|
var r=await api('/api/sp/applyment/upload',{method:'POST',body:JSON.stringify({filename:kind+'.jpg',content_b64:b64})});
|
|
658
|
-
if(r.json.media_id){formAssets[kind]=r.json.media_id;m.textContent='';renderSlots();}
|
|
659
|
-
else{m.className='msg err';m.textContent=r.json.message||'上传失败';}
|
|
685
|
+
if(r.json.media_id){formAssets[kind]=r.json.media_id;m.textContent='';renderSlots();showToast(true,'✓ 素材已上传');}
|
|
686
|
+
else{m.className='msg err';m.textContent=r.json.message||'上传失败';showToast(false,r.json.message||'上传失败');}
|
|
660
687
|
});
|
|
661
688
|
}
|
|
662
689
|
async function saveProfile(){
|
|
690
|
+
var btn=document.querySelector('.btn[onclick="saveProfile()"]');
|
|
691
|
+
busy(btn,true);
|
|
663
692
|
var bp=Number(document.getElementById('p_bp').value);
|
|
664
693
|
var r=await api('/api/sp/config',{method:'PUT',body:JSON.stringify({
|
|
665
694
|
sp_name:document.getElementById('p_name').value.trim(),
|
|
@@ -667,9 +696,11 @@ async function saveProfile(){
|
|
|
667
696
|
sp_official_url:document.getElementById('p_url').value.trim(),
|
|
668
697
|
sp_contact:document.getElementById('p_contact').value.trim(),
|
|
669
698
|
commission_bp:bp})});
|
|
699
|
+
busy(btn,false);
|
|
670
700
|
var m=document.getElementById('p-msg');
|
|
671
701
|
m.className='msg '+(r.json.ok?'ok':'err'); m.textContent=r.json.ok?'✓ 已保存 (商户端 /api/sp/info 立即生效)':(r.json.message||'保存失败');
|
|
672
|
-
if(r.json.ok) load();
|
|
702
|
+
if(r.json.ok){showToast(true,'✓ 已保存,商户端立即生效');load();}
|
|
703
|
+
else showToast(false,r.json.message||'保存失败');
|
|
673
704
|
}
|
|
674
705
|
adminKey(); load();
|
|
675
706
|
</script>
|
package/test-sp-applyment.js
CHANGED
|
@@ -435,7 +435,13 @@ test("SP 配置页后端: config 读写 (表优先 env) + 配齐度 + info 立
|
|
|
435
435
|
// 页面: Accept text/html 无 key 也 200 (admin key 页面内输入)
|
|
436
436
|
r = await fetch(base + "/sp/config", { headers: { Accept: "text/html" } });
|
|
437
437
|
assert.strictEqual(r.status, 200);
|
|
438
|
-
|
|
438
|
+
const pageHtml = await r.text();
|
|
439
|
+
assert.ok(pageHtml.includes("服务商配置"));
|
|
440
|
+
// 回归 (0.4.2): 页面内联脚本必须可编译 — 模板里 \x27 会被服务端提前求值成双引号导致整页 JS 死掉 (保存按钮无反应),
|
|
441
|
+
// 必须用 \\x27 让浏览器拿到字面 \x27 再自行求值
|
|
442
|
+
const scriptMatch = pageHtml.match(/<script>([\s\S]*?)<\/script>/);
|
|
443
|
+
assert.ok(scriptMatch, "配置页应有内联脚本");
|
|
444
|
+
new Function(scriptMatch[1]); // 语法错误会抛
|
|
439
445
|
// 读默认 (env 兜底)
|
|
440
446
|
let j = await (await fetch(base + "/api/sp/config", { headers: { ...H, Accept: "application/json" } })).json();
|
|
441
447
|
assert.strictEqual(j.ok, true);
|
|
@@ -540,3 +546,26 @@ test("多应用: CRUD + 默认唯一 + 旧配置自动迁移 + /info apps 列表
|
|
|
540
546
|
assert.strictEqual(r.status, 400);
|
|
541
547
|
} finally { srv.close(); }
|
|
542
548
|
});
|
|
549
|
+
|
|
550
|
+
test("配置页操作反馈: toast 组件 + 成功/失败文案 + 保存中禁用 (页面断言)", async () => {
|
|
551
|
+
const { srv, base } = await startServer();
|
|
552
|
+
try {
|
|
553
|
+
const r = await fetch(base + "/sp/config", { headers: { Accept: "text/html" } });
|
|
554
|
+
assert.strictEqual(r.status, 200);
|
|
555
|
+
const html = await r.text();
|
|
556
|
+
// toast 组件 (右上浮层, 绿/红, 自动消失)
|
|
557
|
+
assert.ok(html.includes('id="toast"'), "toast 容器");
|
|
558
|
+
assert.ok(html.includes("showToast"), "toast 函数");
|
|
559
|
+
assert.ok(html.includes("position:fixed"), "浮层定位");
|
|
560
|
+
assert.ok(html.includes("2500"), "2.5s 自动消失");
|
|
561
|
+
// 成功/失败文案
|
|
562
|
+
assert.ok(html.includes("✓ 已保存,商户端立即生效"), "成功文案");
|
|
563
|
+
assert.ok(html.includes("r.json.message"), "失败显示后端 message 原文");
|
|
564
|
+
// 保存中禁用
|
|
565
|
+
assert.ok(html.includes("busy(btn,true)") && html.includes("保存中…"), "保存中禁用+文案");
|
|
566
|
+
// 所有 mutating 操作接入: 应用保存/设默认/删除/素材上传/名片保存
|
|
567
|
+
for (const fn of ["saveApp", "setDefault", "delApp", "upFormAsset", "saveProfile"]) {
|
|
568
|
+
assert.ok(html.includes(fn), fn + " 存在");
|
|
569
|
+
}
|
|
570
|
+
} finally { srv.close(); }
|
|
571
|
+
});
|