trustbase-seller-backend 0.4.0 → 0.4.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trustbase-seller-backend",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "TrustBase Seller Backend (merchant edition) — 商品上链/订单/支付/质押 write API for TrustBase 保障链生态. 单卖家自托管形态: 签名 key 即卖家身份, 私钥不出门.",
5
5
  "main": "run-unified.js",
6
6
  "bin": {
@@ -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,'&amp;').replace(/</g,'&lt;');}
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;
@@ -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>
@@ -540,3 +540,26 @@ test("多应用: CRUD + 默认唯一 + 旧配置自动迁移 + /info apps 列表
540
540
  assert.strictEqual(r.status, 400);
541
541
  } finally { srv.close(); }
542
542
  });
543
+
544
+ test("配置页操作反馈: toast 组件 + 成功/失败文案 + 保存中禁用 (页面断言)", async () => {
545
+ const { srv, base } = await startServer();
546
+ try {
547
+ const r = await fetch(base + "/sp/config", { headers: { Accept: "text/html" } });
548
+ assert.strictEqual(r.status, 200);
549
+ const html = await r.text();
550
+ // toast 组件 (右上浮层, 绿/红, 自动消失)
551
+ assert.ok(html.includes('id="toast"'), "toast 容器");
552
+ assert.ok(html.includes("showToast"), "toast 函数");
553
+ assert.ok(html.includes("position:fixed"), "浮层定位");
554
+ assert.ok(html.includes("2500"), "2.5s 自动消失");
555
+ // 成功/失败文案
556
+ assert.ok(html.includes("✓ 已保存,商户端立即生效"), "成功文案");
557
+ assert.ok(html.includes("r.json.message"), "失败显示后端 message 原文");
558
+ // 保存中禁用
559
+ assert.ok(html.includes("busy(btn,true)") && html.includes("保存中…"), "保存中禁用+文案");
560
+ // 所有 mutating 操作接入: 应用保存/设默认/删除/素材上传/名片保存
561
+ for (const fn of ["saveApp", "setDefault", "delApp", "upFormAsset", "saveProfile"]) {
562
+ assert.ok(html.includes(fn), fn + " 存在");
563
+ }
564
+ } finally { srv.close(); }
565
+ });