@seasonkoh/webaz 0.1.28 → 0.1.30

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.
Files changed (198) hide show
  1. package/README.md +3 -2
  2. package/README.zh-CN.md +7 -6
  3. package/dist/bond-refund-blockers.js +50 -0
  4. package/dist/bond-slash.js +100 -0
  5. package/dist/bond-terms.js +31 -0
  6. package/dist/currency.js +16 -0
  7. package/dist/deposit-rails.js +52 -0
  8. package/dist/direct-pay-aml-monitor.js +67 -0
  9. package/dist/direct-pay-aml-review.js +40 -0
  10. package/dist/direct-pay-base-bond-entry.js +5 -0
  11. package/dist/direct-pay-bond-rail-clearance.js +99 -0
  12. package/dist/direct-pay-cancel-refund.js +160 -0
  13. package/dist/direct-pay-compliance-ingress.js +145 -0
  14. package/dist/direct-pay-controls.js +136 -0
  15. package/dist/direct-pay-create.js +231 -0
  16. package/dist/direct-pay-deferral-quota.js +43 -0
  17. package/dist/direct-pay-disclosures.js +50 -0
  18. package/dist/direct-pay-eligibility.js +46 -0
  19. package/dist/direct-pay-fee-ar.js +241 -0
  20. package/dist/direct-pay-fee-prepay-request.js +80 -0
  21. package/dist/direct-pay-launch-readiness.js +108 -0
  22. package/dist/direct-pay-launch-summary.js +68 -0
  23. package/dist/direct-pay-ledger.js +94 -0
  24. package/dist/direct-pay-returns.js +104 -0
  25. package/dist/direct-pay-stock.js +9 -0
  26. package/dist/direct-receive-account-qr.js +77 -0
  27. package/dist/direct-receive-accounts.js +82 -0
  28. package/dist/direct-receive-deferral.js +171 -0
  29. package/dist/direct-receive-deposits.js +365 -0
  30. package/dist/direct-receive-payment-instruction.js +26 -0
  31. package/dist/free-shipping.js +37 -0
  32. package/dist/fx-rates.js +78 -0
  33. package/dist/layer0-foundation/L0-1-database/schema.js +777 -1
  34. package/dist/layer0-foundation/L0-2-state-machine/engine.js +1 -0
  35. package/dist/layer0-foundation/L0-2-state-machine/genuine-sale.js +15 -5
  36. package/dist/layer0-foundation/L0-2-state-machine/transitions.js +114 -0
  37. package/dist/layer0-foundation/L0-5-manifest/manifest.js +1 -1
  38. package/dist/layer1-agent/L1-1-mcp-server/cli.js +64 -0
  39. package/dist/layer1-agent/L1-1-mcp-server/network-mode.js +11 -0
  40. package/dist/layer1-agent/L1-1-mcp-server/server.js +99 -32
  41. package/dist/layer2-business/L2-6-notifications/notification-engine.js +110 -41
  42. package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +133 -31
  43. package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +16 -4
  44. package/dist/layer3-trust/L3-1-dispute-engine/mutual-cancel.js +156 -0
  45. package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +4 -4
  46. package/dist/ledger.js +12 -3
  47. package/dist/mcp.js +23 -4
  48. package/dist/merchant-bond-domain.js +64 -0
  49. package/dist/merchant-bond-exposure.js +78 -0
  50. package/dist/merchant-bond-watcher.js +26 -0
  51. package/dist/payment-rails.js +29 -0
  52. package/dist/platform-receive-accounts.js +94 -0
  53. package/dist/product-verification.js +93 -0
  54. package/dist/pwa/acp-feed.js +3 -2
  55. package/dist/pwa/arbitration-read-admin.js +37 -0
  56. package/dist/pwa/arbitrator-lifecycle.js +100 -0
  57. package/dist/pwa/contract-fingerprint.js +18 -0
  58. package/dist/pwa/direct-pay-guards.js +20 -0
  59. package/dist/pwa/direct-pay-order-redaction.js +42 -0
  60. package/dist/pwa/endpoint-actions.js +3 -0
  61. package/dist/pwa/human-presence.js +2 -6
  62. package/dist/pwa/public/app-account.js +9 -9
  63. package/dist/pwa/public/app-admin-disputes.js +55 -0
  64. package/dist/pwa/public/app-agent-appeal.js +90 -0
  65. package/dist/pwa/public/app-agent-approvals.js +93 -0
  66. package/dist/pwa/public/app-agent-pair.js +127 -0
  67. package/dist/pwa/public/app-ai.js +10 -10
  68. package/dist/pwa/public/app-arbitrator-admin.js +87 -0
  69. package/dist/pwa/public/app-arbitrator-entry.js +9 -0
  70. package/dist/pwa/public/app-bond-deferral-ui.js +9 -0
  71. package/dist/pwa/public/app-bond-refund-ui.js +66 -0
  72. package/dist/pwa/public/app-bond-slash-ui.js +74 -0
  73. package/dist/pwa/public/app-bond-terms-ui.js +23 -0
  74. package/dist/pwa/public/app-bond-ui.js +108 -0
  75. package/dist/pwa/public/app-chat-poll.js +29 -0
  76. package/dist/pwa/public/app-contribution-hub.js +23 -0
  77. package/dist/pwa/public/app-create-kinds.js +17 -0
  78. package/dist/pwa/public/app-direct-pay-accounts.js +141 -0
  79. package/dist/pwa/public/app-direct-pay-buyer.js +72 -0
  80. package/dist/pwa/public/app-direct-pay-cancel-refund.js +72 -0
  81. package/dist/pwa/public/app-direct-pay-compliance.js +67 -0
  82. package/dist/pwa/public/app-direct-pay-copy.js +12 -0
  83. package/dist/pwa/public/app-direct-pay-deferral-admin.js +72 -0
  84. package/dist/pwa/public/app-direct-pay-deferral.js +61 -0
  85. package/dist/pwa/public/app-direct-pay-fee-center.js +33 -0
  86. package/dist/pwa/public/app-direct-pay-fee-history.js +39 -0
  87. package/dist/pwa/public/app-direct-pay-fee-ops.js +112 -0
  88. package/dist/pwa/public/app-direct-pay-fee-request.js +81 -0
  89. package/dist/pwa/public/app-direct-pay-fee-requests-admin.js +70 -0
  90. package/dist/pwa/public/app-direct-pay-memo.js +14 -0
  91. package/dist/pwa/public/app-direct-pay-negotiation.js +35 -0
  92. package/dist/pwa/public/app-direct-pay-pay.js +15 -0
  93. package/dist/pwa/public/app-direct-pay-paymodal.js +32 -0
  94. package/dist/pwa/public/app-direct-pay-product-verify.js +103 -0
  95. package/dist/pwa/public/app-direct-pay-readiness.js +38 -0
  96. package/dist/pwa/public/app-direct-pay-reconcile.js +19 -0
  97. package/dist/pwa/public/app-direct-pay-returns.js +56 -0
  98. package/dist/pwa/public/app-direct-pay-reveal.js +82 -0
  99. package/dist/pwa/public/app-direct-pay-sales-report.js +70 -0
  100. package/dist/pwa/public/app-direct-pay-store-verify.js +100 -0
  101. package/dist/pwa/public/app-direct-pay.js +226 -0
  102. package/dist/pwa/public/app-discover.js +20 -20
  103. package/dist/pwa/public/app-dispute-close-ui.js +38 -0
  104. package/dist/pwa/public/app-external-links.js +32 -0
  105. package/dist/pwa/public/app-free-shipping-ui.js +29 -0
  106. package/dist/pwa/public/app-gmv-rail-split.js +12 -0
  107. package/dist/pwa/public/app-listing-commerce-ui.js +72 -0
  108. package/dist/pwa/public/app-listings.js +4 -4
  109. package/dist/pwa/public/app-mutual-cancel.js +54 -0
  110. package/dist/pwa/public/app-notif-templates-orders.js +43 -0
  111. package/dist/pwa/public/app-notif-templates.js +22 -0
  112. package/dist/pwa/public/app-order-accept-ui.js +158 -0
  113. package/dist/pwa/public/app-order-errors.js +50 -0
  114. package/dist/pwa/public/app-order-labels.js +19 -0
  115. package/dist/pwa/public/app-order-rail-filter.js +26 -0
  116. package/dist/pwa/public/app-platform-receive-accounts.js +140 -0
  117. package/dist/pwa/public/app-poll-governor.js +22 -0
  118. package/dist/pwa/public/app-prelaunch-waz.js +39 -0
  119. package/dist/pwa/public/app-price.js +55 -0
  120. package/dist/pwa/public/app-product-media.js +15 -0
  121. package/dist/pwa/public/app-profile.js +10 -10
  122. package/dist/pwa/public/app-purchase-terms-ui.js +68 -0
  123. package/dist/pwa/public/app-sale-regions-ui.js +38 -0
  124. package/dist/pwa/public/app-seller.js +5 -5
  125. package/dist/pwa/public/app-shop.js +19 -19
  126. package/dist/pwa/public/app-trade-tax-ui.js +33 -0
  127. package/dist/pwa/public/app.js +264 -295
  128. package/dist/pwa/public/i18n.js +1068 -197
  129. package/dist/pwa/public/index.html +58 -0
  130. package/dist/pwa/public/openapi.json +1845 -653
  131. package/dist/pwa/public/style.css +3 -0
  132. package/dist/pwa/routes/admin-analytics.js +4 -2
  133. package/dist/pwa/routes/admin-direct-receive-deposits.js +533 -0
  134. package/dist/pwa/routes/admin-protocol-params.js +16 -0
  135. package/dist/pwa/routes/admin-reports.js +31 -5
  136. package/dist/pwa/routes/agent-governance.js +1 -1
  137. package/dist/pwa/routes/agent-grants.js +253 -32
  138. package/dist/pwa/routes/analytics.js +2 -0
  139. package/dist/pwa/routes/arbitrator.js +67 -14
  140. package/dist/pwa/routes/bond-seller.js +162 -0
  141. package/dist/pwa/routes/buyer-feeds.js +2 -1
  142. package/dist/pwa/routes/chat.js +6 -1
  143. package/dist/pwa/routes/dashboards.js +2 -1
  144. package/dist/pwa/routes/direct-pay-availability.js +196 -0
  145. package/dist/pwa/routes/direct-pay-cancel-refund.js +111 -0
  146. package/dist/pwa/routes/direct-pay-disclosure-acks.js +79 -0
  147. package/dist/pwa/routes/direct-pay-pending-accept.js +222 -0
  148. package/dist/pwa/routes/direct-pay-returns.js +74 -0
  149. package/dist/pwa/routes/direct-pay-timeouts.js +248 -0
  150. package/dist/pwa/routes/direct-receive-accounts.js +155 -0
  151. package/dist/pwa/routes/direct-receive-payment-instructions.js +45 -0
  152. package/dist/pwa/routes/disputes-read.js +20 -6
  153. package/dist/pwa/routes/disputes-write.js +91 -33
  154. package/dist/pwa/routes/external-anchors.js +4 -2
  155. package/dist/pwa/routes/fee-prepay-requests.js +66 -0
  156. package/dist/pwa/routes/fx.js +12 -0
  157. package/dist/pwa/routes/governance-onboarding.js +46 -8
  158. package/dist/pwa/routes/leaderboard.js +47 -9
  159. package/dist/pwa/routes/listings.js +2 -2
  160. package/dist/pwa/routes/logistics.js +6 -2
  161. package/dist/pwa/routes/manifests.js +38 -0
  162. package/dist/pwa/routes/me-data.js +5 -2
  163. package/dist/pwa/routes/mutual-cancel.js +62 -0
  164. package/dist/pwa/routes/orders-action.js +297 -16
  165. package/dist/pwa/routes/orders-create.js +21 -6
  166. package/dist/pwa/routes/orders-read.js +106 -8
  167. package/dist/pwa/routes/p2p-products.js +2 -2
  168. package/dist/pwa/routes/platform-receive-accounts.js +111 -0
  169. package/dist/pwa/routes/products-create.js +15 -4
  170. package/dist/pwa/routes/products-links.js +34 -0
  171. package/dist/pwa/routes/products-list.js +2 -1
  172. package/dist/pwa/routes/products-update.js +37 -3
  173. package/dist/pwa/routes/profile-identity.js +4 -2
  174. package/dist/pwa/routes/promoter.js +3 -0
  175. package/dist/pwa/routes/referral.js +4 -0
  176. package/dist/pwa/routes/returns.js +60 -4
  177. package/dist/pwa/routes/rewards-apply.js +10 -5
  178. package/dist/pwa/routes/rewards-clearing-mature.js +96 -0
  179. package/dist/pwa/routes/rewards-escrow-expire.js +6 -2
  180. package/dist/pwa/routes/seller-directpay-report.js +110 -0
  181. package/dist/pwa/routes/seller-quota.js +5 -1
  182. package/dist/pwa/routes/shipping-templates.js +219 -0
  183. package/dist/pwa/routes/shops.js +2 -1
  184. package/dist/pwa/routes/snf.js +4 -1
  185. package/dist/pwa/routes/url-claim.js +2 -2
  186. package/dist/pwa/routes/users-public.js +8 -0
  187. package/dist/pwa/routes/wallet-read.js +3 -0
  188. package/dist/pwa/routes/webauthn.js +3 -3
  189. package/dist/pwa/server.js +112 -132
  190. package/dist/runtime/agent-grant-scopes.js +69 -1
  191. package/dist/runtime/webaz-schema-helpers.js +161 -3
  192. package/dist/sale-regions.js +116 -0
  193. package/dist/shipping-templates.js +119 -0
  194. package/dist/store-verification.js +77 -0
  195. package/dist/trade-tax.js +99 -0
  196. package/dist/trade-terms.js +76 -0
  197. package/dist/version.js +1 -1
  198. package/package.json +128 -2
@@ -171,7 +171,7 @@ window.doRegisterPasskey = async (deviceLabel) => {
171
171
  window.requestPasskeyGate = async (purpose, purposeData) => {
172
172
  if (!isWebAuthnSupported()) throw new Error(t('当前设备不支持 Passkey'))
173
173
  const start = await POST('/webauthn/auth/start', { purpose, purpose_data: purposeData })
174
- if (start.error) throw new Error(start.error)
174
+ if (start.error) { const _e = new Error(start.error); _e.code = start.error_code; throw _e } // code=NO_PASSKEY_REGISTERED → 前端才引导注册;其余(取消/设备)不误导
175
175
  const opts = start.options
176
176
  const publicKey = {
177
177
  ...opts,
@@ -566,7 +566,7 @@ async function render(page, params) {
566
566
  const { status, body } = await apiWithStatus('GET', '/me')
567
567
  if (status === 200 && body && !body.error) {
568
568
  state.user = body
569
- connectSSE(); maybePromptPlacementBind(); refreshCartBadge(); maybeShowInstallBanner(); maybeClaimPendingProductShare(); maybeClaimPendingShopReferral(); setTimeout(refreshCompareBadge, 0); refreshChatsBadge(); refreshFeedbackBadge(); refreshSnfBadge(); setTimeout(ensureSnfPull, 800)
569
+ connectSSE(); maybePromptPlacementBind(); refreshCartBadge(); maybeShowInstallBanner(); maybeClaimPendingProductShare(); maybeClaimPendingShopReferral(); setTimeout(refreshCompareBadge, 0); refreshChatsBadge(); refreshFeedbackBadge(); refreshSnfBadge(); setTimeout(ensureSnfPull, 800); if (window.arbEntryHydrate) arbEntryHydrate()
570
570
  try { p2pStart() } catch (e) { console.warn('[P2P] start err', e) }
571
571
  } else if (status === 401 || status === 403) {
572
572
  // 真·鉴权失败:清掉两层存储
@@ -644,11 +644,11 @@ async function render(page, params) {
644
644
  return renderContributeTasks(app)
645
645
  case 'seller':
646
646
  if (state.user?.role === 'logistics') return renderLogistics(app)
647
- if (state.user?.role === 'arbitrator') return renderDisputeList(app)
647
+ if (state.canArbitrate) return renderDisputeList(app) // PR-E:仲裁入口跟随 can_arbitrate,非 role==='arbitrator'
648
648
  return renderSeller(app)
649
649
  case 'edit-product': return renderEditProduct(app, params[0])
650
650
  case 'wallet': return renderWallet(app)
651
- case 'agents': return renderConnectedAgents(app)
651
+ case 'agents': return renderConnectedAgents(app); case 'pair': return window.renderAgentPair(app); case 'agent-approvals': return window.renderAgentApprovals(app) // RFC-020 #pair 配对授权页 / #agent-approvals 扩权审批页
652
652
  case 'note': return renderNoteLanding(app, params[0])
653
653
  case 'tag': return renderTagPage(app, decodeURIComponent(params[0] || ''))
654
654
  case 'governance': return renderGovernance(app)
@@ -721,7 +721,7 @@ async function render(page, params) {
721
721
  if (params[0] === 'public-ideas') return renderAdminPublicIdeas(app)
722
722
  if (params[0] === 'task-proposals') return renderAdminTaskProposals(app)
723
723
  if (params[0] === 'quota-requests') return renderAdminBuildTaskQuota(app)
724
- if (params[0] === 'operator-claims') return renderAdminOperatorClaims(app)
724
+ if (params[0] === 'operator-claims') return renderAdminOperatorClaims(app); if (params[0] === 'deferrals') return renderAdminDirectPayDeferrals(app); if (params[0] === 'product-verifications') return renderAdminProductVerifications(app); if (params[0] === 'store-verifications') return renderAdminStoreVerifications(app); if (params[0] === 'compliance') return renderAdminDirectReceiveCompliance(app); if (params[0] === 'dp-ops') return renderAdminDirectPayHub(app); if (params[0] === 'dp-fee') return renderAdminDirectPayFeeOps(app); if (params[0] === 'platform-receive') return renderAdminPlatformReceiveAccounts(app); if (params[0] === 'fee-prepay-requests') return renderAdminFeePrepayRequests(app); if (params[0] === 'arbitrators') return renderAdminArbitrators(app); if (params[0] === 'contribution-ops') return renderAdminContributionHub(app); if (params[0] === 'bond-deposits') return renderAdminBondDeposits(app); if (params[0] === 'agent-strikes') return renderAdminAgentStrikes(app) // PR-②c 缓交 / PR-④b 逐品 / PR-⑤b 店铺 / PR-⑧ 合规录入 / PR-B 商户运营 hub + 平台服务费 + 平台收款方式 + 预充值申请审核 / PR-F 仲裁员管理
725
725
  if (params[0] === 'params') return renderAdminParams(app)
726
726
  if (params[0] === 'timeline' && params[1]) return renderAdminUserTimeline(app, params[1])
727
727
  if (params[0] === 'timeline') return renderAdminUserTimelinePicker(app)
@@ -845,14 +845,14 @@ async function render(page, params) {
845
845
  // M7.4:order 含 has_pending_claim=1 时附加一枚"验证中"chip
846
846
  function orderStatusBadges(order) {
847
847
  if (!order) return ''
848
- const main = statusBadge(order.status)
848
+ const main = statusBadge(order.status, order.payment_rail)
849
849
  if (Number(order.has_pending_claim) === 1) {
850
850
  return `${main} <span style="display:inline-block;font-size:10px;background:#eef2ff;color:#3730a3;padding:2px 8px;border-radius:99px;font-weight:600;margin-left:4px">🔎 ${t('验证中')}</span>`
851
851
  }
852
852
  return main
853
853
  }
854
854
 
855
- function statusBadge(status) {
855
+ function statusBadge(status, rail) {
856
856
  const map = {
857
857
  created: ['gray', t('待付款')],
858
858
  paid: ['blue', t('待接单')],
@@ -875,7 +875,8 @@ function statusBadge(status) {
875
875
  dispute_dismissed: ['gray', t('争议驳回')],
876
876
  expired: ['gray', t('已过期')],
877
877
  }
878
- const [color, label] = map[status] || ['gray', status]
878
+ // 直付(非托管)争议终态:不发生退款/资金释放 用信誉裁决语义(dpTerminalBadge,见 app-order-labels.js)
879
+ const [color, label] = (rail === 'direct_p2p' && ((window.dpTerminalBadge && window.dpTerminalBadge(status)) || (window.dpNegotiationBadge && window.dpNegotiationBadge(status)) || (window.dpAcceptBadge && window.dpAcceptBadge(status)))) || map[status] || ['gray', status]
879
880
  return `<span class="badge badge-${color}">${label}</span>`
880
881
  }
881
882
 
@@ -1017,7 +1018,7 @@ function roleHome(role) {
1017
1018
  function preLaunchBannerHTML() {
1018
1019
  if (window._protocolPhase && window._protocolPhase !== 'pre_launch') return ''
1019
1020
  return `<div style="background:#fef3c7;border:1px solid #fde68a;border-radius:8px;padding:10px 14px;font-size:12px;color:#92400e;text-align:center;line-height:1.5;margin:8px 12px 12px">
1020
- ⚠️ <strong>${t('协议尚未公开上线 · 数据为测试 / demo · 请勿据此投资或承诺第三方')}</strong>
1021
+ ⚠️ <strong>${t('直付(direct pay)已上线,是你与卖家的真实场外付款;WebAZ 非托管——不代持、不担保、不退款 · 平台托管(escrow)尚未上线(模拟测试币)· 价格按 USDC 计价仅供展示 · 邀请制预发布,勿据此投资或向第三方承诺')}</strong>
1021
1022
  <a href="#welcome" style="margin-left:8px;color:#7c2d12;text-decoration:underline;font-weight:600">${t('详情')}</a>
1022
1023
  </div>`
1023
1024
  }
@@ -1027,10 +1028,10 @@ function preLaunchBannerHTML() {
1027
1028
  // 没有 Passkey 登录 / 找回路径,丢 key 换设备后 Passkey 救不回账号(与弹窗"增强不替代恢复邮箱"一致)。
1028
1029
  function recoveryBannerHTML() {
1029
1030
  const u = state.user
1030
- if (!u || u.role === 'admin') return ''
1031
- const hasRecovery = u.has_password || u.email_verified
1032
- if (hasRecovery) return ''
1033
- return `<div style="background:#fef2f2;border:1px solid #fca5a5;border-radius:8px;padding:10px 14px;font-size:12px;color:#991b1b;line-height:1.6;margin:8px 12px 12px">
1031
+ if (!u || u.role === 'admin' || u.has_password || u.email_verified) return ''
1032
+ if (!location.hash.startsWith('#me') && Date.now() < +(localStorage.getItem('webaz_recovery_dismiss_until') || 0)) return '' // 走查批次2·横幅降噪:可关闭 7 天冷却;「我的」域恒显(设置入口所在,安全提醒不静音到死角)
1033
+ return `<div data-rcv style="background:#fef2f2;border:1px solid #fca5a5;border-radius:8px;padding:10px 14px;font-size:12px;color:#991b1b;line-height:1.6;margin:8px 12px 12px;position:relative">
1034
+ <button title="${t('暂时隐藏(7 天)')}" onclick="localStorage.setItem('webaz_recovery_dismiss_until', String(Date.now() + 7 * 86400e3)); this.closest('[data-rcv]').remove()" style="position:absolute;top:6px;right:8px;background:none;border:none;cursor:pointer;color:#b91c1c;font-size:14px;padding:2px 6px">×</button>
1034
1035
  🚨 <strong>${t('账户还没有恢复方式')}</strong> — ${t('闪退或换设备清缓存后可能永久无法登录。')}
1035
1036
  <div style="margin-top:8px;display:flex;gap:8px;flex-wrap:wrap">
1036
1037
  <button class="btn btn-sm" style="font-size:11px;padding:4px 10px;background:#dc2626;color:#fff;border:none;border-radius:6px;cursor:pointer" onclick="navigate('#me/settings')">${t('立即设置密码 / 绑定邮箱')} →</button>
@@ -2045,7 +2046,7 @@ window.adminUserBatch = async (action) => {
2045
2046
  reason = prompt(t('暂停原因(必填,会写入审计)')) || ''
2046
2047
  if (!reason.trim()) return alert(t('需填写原因'))
2047
2048
  }
2048
- if (!confirm(t('确认') + label + ' ' + ids.length + ' ' + t('个用户?'))) return
2049
+ if (!confirm((action === 'suspend' ? t('确认暂停') : t('确认恢复')) + ' ' + ids.length + ' ' + t('个用户?'))) return
2049
2050
  let ok = 0, fail = 0
2050
2051
  for (const uid of ids) {
2051
2052
  const path = action === 'suspend' ? `/admin/users/${uid}/suspend` : `/admin/users/${uid}/resume`
@@ -2645,7 +2646,7 @@ async function renderAdminOrders(app, opts = {}) {
2645
2646
  <div style="display:flex;justify-content:space-between;align-items:flex-start;gap:8px">
2646
2647
  <div style="flex:1;min-width:0">
2647
2648
  <div style="font-weight:600">${escHtml(o.product_title)}</div>
2648
- <div style="font-size:12px;color:#6b7280;margin-top:2px">${o.total_amount} WAZ · ${fmtTime(o.created_at)}</div>
2649
+ <div style="font-size:12px;color:#6b7280;margin-top:2px">${o.payment_rail === 'direct_p2p' ? `<span style="background:#fef3c7;color:#92400e;border-radius:99px;padding:1px 8px;font-size:10px;font-weight:600;margin-right:4px">${t('直付')}</span>${o.total_amount} USDC` : `${o.total_amount} WAZ`} · ${fmtTime(o.created_at)}</div>
2649
2650
  <div style="font-size:11px;color:#6b7280;margin-top:4px">${t('买家')}: ${escHtml(o.buyer_name)} · ${t('卖家')}: ${escHtml(o.seller_name)}</div>
2650
2651
  <div style="font-size:11px;color:#9ca3af;margin-top:2px">${o.id}</div>
2651
2652
  </div>
@@ -2671,34 +2672,7 @@ window.doAdminOrderFilter = () => {
2671
2672
  renderAdminOrders(document.getElementById('app'), { status })
2672
2673
  }
2673
2674
 
2674
- async function renderAdminDisputes(app) {
2675
- if (!state.user) { renderLogin(); return }
2676
- if (!isAdmin()) { app.innerHTML = shell(`<div class="alert alert-info">${t('仅限管理员')}</div>`, 'admin'); return }
2677
- app.innerHTML = shell(loading$(), 'admin')
2678
- const data = await GET('/admin/disputes')
2679
- if (data.error) { app.innerHTML = shell(alert$('error', data.error), 'admin'); return }
2680
- const list = data.disputes.map(d => `
2681
- <div class="card" style="margin-bottom:10px;font-size:13px">
2682
- <div style="display:flex;justify-content:space-between;align-items:flex-start;gap:8px">
2683
- <div style="flex:1;min-width:0">
2684
- <div style="font-weight:600">${escHtml(d.product_title || '—')}</div>
2685
- <div style="font-size:12px;color:#6b7280;margin-top:2px">${escHtml((d.reason || '').slice(0, 80))}</div>
2686
- <div style="font-size:11px;color:#6b7280;margin-top:6px">${t('原告')}: ${escHtml(d.initiator_name || '—')} → ${t('被告')}: ${escHtml(d.defendant_name || '—')}</div>
2687
- <div style="font-size:11px;color:#9ca3af;margin-top:4px">${d.id} · ${fmtTime(d.created_at)}</div>
2688
- </div>
2689
- <div style="display:flex;flex-direction:column;gap:6px;align-items:flex-end">
2690
- <span style="background:#fef3c7;color:#92400e;padding:2px 8px;border-radius:8px;font-size:11px">${d.status}</span>
2691
- ${d.total_amount ? `<div style="font-size:12px;color:#374151">${d.total_amount} WAZ</div>` : ''}
2692
- </div>
2693
- </div>
2694
- </div>`).join('') || `<div class="empty"><div class="empty-icon">⚖️</div><div class="empty-text">${t('暂无争议')}</div></div>`
2695
- app.innerHTML = shell(`
2696
- <h1 class="page-title">⚖️ ${t('争议查看')}</h1>
2697
- <div style="margin-bottom:16px"><button class="btn btn-outline btn-sm" style="width:auto" onclick="navigate('#admin')">${t('返回概览')}</button></div>
2698
- <div style="font-size:12px;color:#6b7280;margin-bottom:10px">${t('共')} ${data.disputes.length} ${t('条结果')}(${t('含已结案')})</div>
2699
- ${list}
2700
- `, 'admin')
2701
- }
2675
+ // renderAdminDisputes 已抽出到 app-admin-disputes.js(window.renderAdminDisputes;可区分状态/直付-托管/进度/指派/结案 + 可管理:过滤+钻取)
2702
2676
 
2703
2677
  async function renderAdminTasks(app, opts = {}) {
2704
2678
  if (!state.user) { renderLogin(); return }
@@ -3378,7 +3352,7 @@ window.openResignModal = (role) => {
3378
3352
  <li>${t('已 assigned 但未完成的 case 必须先完成 / 转交,否则无法卸任')}</li>
3379
3353
  </ul>
3380
3354
  </div>
3381
- <div style="font-size:13px;color:#374151;margin-bottom:8px">${t('输入')} <code style="background:#fee2e2;padding:2px 6px;border-radius:4px;color:#991b1b">${expected}</code> ${t('确认')}:</div>
3355
+ <div style="font-size:13px;color:#374151;margin-bottom:8px">${t('输入')} <code style="background:#fee2e2;padding:2px 6px;border-radius:4px;color:#991b1b">${expected}</code> ${t('以确认')}:</div>
3382
3356
  <input id="resign-confirm-input" type="text" placeholder="${expected}" style="width:100%;padding:8px;border:1px solid #d1d5db;border-radius:4px;font-family:monospace" oninput="document.getElementById('resign-submit-btn').disabled = this.value !== '${expected}'">
3383
3357
  <div id="resign-msg" style="margin-top:12px"></div>
3384
3358
  <div style="margin-top:16px;display:flex;gap:8px;justify-content:flex-end">
@@ -3505,7 +3479,7 @@ async function renderAdminGovernance(app) {
3505
3479
  <div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px">
3506
3480
  <div style="flex:1">
3507
3481
  <div style="font-weight:600;font-size:14px">${a.role === 'arbitrator' ? '⚖' : '🛡'} ${escHtml(a.user_name || '')} <span style="color:#6b7280;font-size:12px">(${a.handle ? '@' + escHtml(a.handle) : a.user_id})</span></div>
3508
- <div style="font-size:12px;color:#6b7280;margin-top:4px">${t('申请')}: ${roleLabel} · ${t('Region')}: ${escHtml(a.region || '—')} · ${t('Email')}: ${a.email ? '✓' : '✗'}</div>
3482
+ <div style="font-size:12px;color:#6b7280;margin-top:4px">${t('角色')}: ${roleLabel} · ${t('Region')}: ${escHtml(a.region || '—')} · ${t('Email')}: ${a.email ? '✓' : '✗'}</div>
3509
3483
  <div style="font-size:12px;margin-top:6px;display:flex;gap:12px">
3510
3484
  <span style="color:${quizOk ? '#16a34a' : '#dc2626'}">${quizOk ? '✓' : '✗'} ${t('题目')} ${a.quiz_score != null ? a.quiz_score + '%' : '—'}</span>
3511
3485
  <span style="color:${caseOk ? '#16a34a' : '#dc2626'}">${caseOk ? '✓' : '✗'} ${t('案例 review')}</span>
@@ -3970,7 +3944,7 @@ async function renderAdminQuotaApps(app) {
3970
3944
  <div style="font-weight:600">${escHtml(a.user_name)} <span style="font-size:11px;color:#6b7280">${a.email || ''}</span></div>
3971
3945
  <div style="font-size:11px;color:#9ca3af;margin-top:2px">${a.user_id} · ${fmtTime(a.applied_at)}</div>
3972
3946
  <div style="font-size:13px;margin-top:8px">
3973
- ${t('当前配额')}: <strong>${a.current_quota}</strong> → ${t('申请')}: <strong>${a.requested_quota}</strong>
3947
+ ${t('当前配额')}: <strong>${a.current_quota}</strong> → ${t('申请配额')}: <strong>${a.requested_quota}</strong>
3974
3948
  </div>
3975
3949
  ${a.reason ? `<div style="font-size:12px;color:#6b7280;margin-top:6px;padding:6px;background:#f9fafb;border-radius:4px">${escHtml(a.reason)}</div>` : ''}
3976
3950
  <div style="display:flex;gap:6px;margin-top:10px">
@@ -4392,7 +4366,7 @@ function renderWelcome(app) {
4392
4366
  <div style="min-height:100vh;background:#FFFFFF;font-family:-apple-system,BlinkMacSystemFont,'SF Pro Display','SF Pro Text','PingFang SC',sans-serif;-webkit-font-smoothing:antialiased;position:relative">
4393
4367
  <!-- Top nav — frosted glass,左返回,右语言切换(当前语言高亮) -->
4394
4368
  <div style="position:sticky;top:0;z-index:10;display:flex;justify-content:space-between;align-items:center;padding:14px 18px;background:rgba(255,255,255,0.85);-webkit-backdrop-filter:saturate(180%) blur(20px);backdrop-filter:saturate(180%) blur(20px);border-bottom:1px solid #E4E4E7">
4395
- <button class="w-nav-back" onclick="navigate('#login')" style="background:none;border:none;cursor:pointer;font-size:14px;color:#3F3F46;padding:6px 10px;font-weight:500;transition:color 0.15s;font-family:inherit">← ${T('返回首页', 'Back to Home')}</button>
4369
+ <button class="w-nav-back" onclick="navigate('#buy')" style="background:none;border:none;cursor:pointer;font-size:14px;color:#3F3F46;padding:6px 10px;font-weight:500;transition:color 0.15s;font-family:inherit">← ${T('返回首页', 'Back to Home')}</button>
4396
4370
  <span class="w-lang" style="font-size:13px;font-weight:500">
4397
4371
  <span class="${en ? 'w-lang-inactive' : 'w-lang-active'}" ${en ? `onclick="toggleLang();renderWelcome(document.getElementById('app'))"` : ''}>中文</span>
4398
4372
  <span class="w-lang-sep">/</span>
@@ -5473,7 +5447,7 @@ async function renderPromoter(app) {
5473
5447
  <div class="card" style="text-align:center;padding:10px 6px">
5474
5448
  <div style="font-size:11px;color:#6b7280">💰 ${t('累计收益')}</div>
5475
5449
  <div style="font-size:17px;font-weight:700;color:#059669;margin-top:2px">${totalEarnings.toFixed(2)}</div>
5476
- <div style="font-size:10px;color:#9ca3af">WAZ</div>
5450
+ <div style="font-size:10px;color:#9ca3af">WAZ</div>${(data.earnings?.clearing_total||0)>0?`<div style="font-size:9px;color:#9ca3af;margin-top:1px" title="${t('清算中:退货期满后到账')}">${t('清算中')} ${Number(data.earnings.clearing_total).toFixed(2)}</div>`:''}
5477
5451
  </div>
5478
5452
  <div class="card" style="text-align:center;padding:10px 6px">
5479
5453
  <div style="font-size:11px;color:#6b7280">📊 ${t('近 30 日')}</div>
@@ -5558,9 +5532,7 @@ async function renderPromoter(app) {
5558
5532
  const ratioPct = cs.clicks.raw > 0 ? Math.round((cs.clicks.unique / cs.clicks.raw) * 100) : null
5559
5533
  // 我买过的(合并进"我的产品分享"作为商品来源子列表)
5560
5534
  const bought = shareHub.bought_products || []
5561
- const buildProductImg = (images) => {
5562
- try { const arr = typeof images === 'string' ? JSON.parse(images) : (images || []); return arr[0] || '' } catch { return '' }
5563
- }
5535
+ const buildProductImg = (images) => window.productThumbSrc ? window.productThumbSrc(images) : ''
5564
5536
  const renderBoughtRow = (p) => {
5565
5537
  const img = buildProductImg(p.images)
5566
5538
  const rate = Number(p.commission_rate || 0)
@@ -5586,7 +5558,7 @@ async function renderPromoter(app) {
5586
5558
 
5587
5559
  return `
5588
5560
  <div id="bought-${orderId || p.id}" style="display:flex;gap:8px;align-items:center;padding:8px 10px;border:${isHighlight ? '2px solid #4f46e5' : '1px solid #f3f4f6'};border-radius:8px;margin-bottom:6px;background:${isHighlight ? '#eef2ff' : '#fff'}">
5589
- ${img ? `<img src="${escHtml(img)}" style="width:36px;height:36px;border-radius:6px;object-fit:cover;flex-shrink:0">` : `<div style="width:36px;height:36px;background:#f3f4f6;border-radius:6px;display:flex;align-items:center;justify-content:center;font-size:16px;flex-shrink:0">📦</div>`}
5561
+ ${img ? `<img src="${escHtml(img)}" onerror="this.outerHTML='📦'" style="width:36px;height:36px;border-radius:6px;object-fit:cover;flex-shrink:0">` : `<div style="width:36px;height:36px;background:#f3f4f6;border-radius:6px;display:flex;align-items:center;justify-content:center;font-size:16px;flex-shrink:0">📦</div>`}
5590
5562
  <div style="flex:1;min-width:0">
5591
5563
  <div style="font-size:12px;font-weight:600;color:#1f2937;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escHtml(p.title)}</div>
5592
5564
  <div style="font-size:10px;color:#9ca3af;margin-top:1px">
@@ -6325,8 +6297,6 @@ async function renderReviewsFeed(app) {
6325
6297
  const rows = items.length === 0
6326
6298
  ? `<div style="text-align:center;padding:40px;color:#9ca3af"><div style="font-size:48px">📺</div><div style="font-size:13px;margin-top:8px">${t('暂无评测内容')}</div></div>`
6327
6299
  : items.map(s => {
6328
- let prodImg = ''
6329
- try { const imgs = typeof s.product_images === 'string' ? JSON.parse(s.product_images) : s.product_images; if (Array.isArray(imgs) && imgs[0]) prodImg = imgs[0] } catch {}
6330
6300
  return `
6331
6301
  <div class="card" style="padding:10px 12px;margin-bottom:8px;cursor:pointer" onclick="openExternalReview('${s.id}', '${escHtml(s.external_url || '').replace(/'/g, "\\'")}')">
6332
6302
  <div style="display:flex;gap:10px;align-items:center">
@@ -6334,7 +6304,7 @@ async function renderReviewsFeed(app) {
6334
6304
  <div style="flex:1;min-width:0">
6335
6305
  <div style="font-size:13px;font-weight:600;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical">${escHtml(s.title || s.external_url || '(无标题)')}</div>
6336
6306
  <div style="font-size:11px;color:#9ca3af;margin-top:2px">@${escHtml(s.owner_handle || s.owner_name || '?')} · ${s.click_count} 👁 · ❤ ${s.like_count || 0} · ${fmtTime(s.created_at)}</div>
6337
- ${s.product_title ? `<div style="font-size:11px;color:#6366f1;margin-top:2px">📦 ${escHtml(s.product_title)} · ${s.product_price} WAZ</div>` : ''}
6307
+ ${s.product_title ? `<div style="font-size:11px;color:#6366f1;margin-top:2px">📦 ${escHtml(s.product_title)} · ${window.fmtPrice(s.product_price)}</div>` : ''}
6338
6308
  </div>
6339
6309
  </div>
6340
6310
  </div>`
@@ -6448,20 +6418,16 @@ window.lookupAnchorAction = async () => {
6448
6418
  // 商品区块
6449
6419
  const productBlock = (r.product) ? (() => {
6450
6420
  const p = r.product
6451
- let img = ''
6452
- try {
6453
- const imgs = typeof p.images === 'string' ? JSON.parse(p.images) : p.images
6454
- if (Array.isArray(imgs) && imgs[0]) img = String(imgs[0])
6455
- } catch {}
6421
+ let img = window.productThumbSrc(p.images)
6456
6422
  const idx = calcIndex(p)
6457
6423
  const idxColor = idx == null ? '#9ca3af' : idx >= 8 ? '#dc2626' : idx >= 6 ? '#f59e0b' : '#6b7280'
6458
6424
  return `
6459
6425
  <div style="border:1px solid #e5e7eb;border-radius:8px;padding:12px;margin-bottom:10px">
6460
6426
  <div style="display:flex;gap:10px;margin-bottom:10px">
6461
- ${img ? `<img src="${img}" style="width:64px;height:64px;object-fit:cover;border-radius:6px;flex-shrink:0">` : `<div style="width:64px;height:64px;background:#f3f4f6;border-radius:6px;display:flex;align-items:center;justify-content:center;font-size:24px;color:#9ca3af;flex-shrink:0">📦</div>`}
6427
+ ${img ? `<img src="${escHtml(img)}" onerror="this.outerHTML='📦'" style="width:64px;height:64px;object-fit:cover;border-radius:6px;flex-shrink:0">` : `<div style="width:64px;height:64px;background:#f3f4f6;border-radius:6px;display:flex;align-items:center;justify-content:center;font-size:24px;color:#9ca3af;flex-shrink:0">📦</div>`}
6462
6428
  <div style="flex:1;min-width:0">
6463
6429
  <div style="font-size:14px;font-weight:600;color:#1f2937;line-height:1.4;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden">${escHtml(p.title)}</div>
6464
- <div style="font-size:17px;font-weight:800;color:#dc2626;margin-top:4px">${Number(p.price).toFixed(2)} <span style="font-size:11px;color:#9ca3af;font-weight:500">WAZ</span></div>
6430
+ <div style="font-size:17px;font-weight:800;color:#dc2626;margin-top:4px">${window.fmtPrice(p.price)}</div>
6465
6431
  <div style="font-size:11px;color:#6b7280;margin-top:2px">@${escHtml(p.seller_handle || p.seller_name?.slice(0,8) || '?')}</div>
6466
6432
  </div>
6467
6433
  </div>
@@ -6514,17 +6480,17 @@ async function renderGroupBuysLive(app) {
6514
6480
  ? `<div style="text-align:center;padding:40px;color:#9ca3af"><div style="font-size:48px">👥</div><div style="font-size:13px;margin-top:8px">${t('暂无进行中的团购')}</div></div>`
6515
6481
  : items.map(it => {
6516
6482
  let imageUrl = ''
6517
- try { const imgs = typeof it.images === 'string' ? JSON.parse(it.images) : it.images; if (Array.isArray(imgs) && imgs[0]) imageUrl = imgs[0] } catch {}
6483
+ imageUrl = window.productThumbSrc(it.images) || imageUrl
6518
6484
  const pct = (Number(it.discount_pct) * 100).toFixed(0)
6519
6485
  const final = (Number(it.original_price) * (1 - Number(it.discount_pct))).toFixed(2)
6520
6486
  const progress = Math.min(100, (Number(it.joined_count) / Number(it.target_count)) * 100)
6521
6487
  return `
6522
6488
  <div class="card" style="padding:12px;margin-bottom:8px;cursor:pointer;border-left:3px solid #16a34a" onclick="location.hash='#group-buy/${it.id}'">
6523
6489
  <div style="display:flex;gap:10px;align-items:center;margin-bottom:8px">
6524
- <div style="font-size:32px;flex-shrink:0">${imageUrl ? `<img src="${escHtml(imageUrl)}" style="width:48px;height:48px;border-radius:6px;object-fit:cover">` : getCategoryIcon(it.category) || '📦'}</div>
6490
+ <div style="font-size:32px;flex-shrink:0">${imageUrl ? `<img src="${escHtml(imageUrl)}" onerror="this.outerHTML='📦'" style="width:48px;height:48px;border-radius:6px;object-fit:cover">` : getCategoryIcon(it.category) || '📦'}</div>
6525
6491
  <div style="flex:1;min-width:0">
6526
6492
  <div style="font-size:13px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escHtml(it.product_title)}</div>
6527
- <div style="font-size:12px;margin-top:2px"><span style="color:#16a34a;font-weight:700">${final}</span> WAZ <span style="font-size:10px;color:#9ca3af;text-decoration:line-through margin-left:4px">${it.original_price}</span> <span style="font-size:10px;color:#16a34a;background:#dcfce7;padding:1px 5px;border-radius:99px;margin-left:4px;font-weight:600">-${pct}%</span></div>
6493
+ <div style="font-size:12px;margin-top:2px"><span style="color:#16a34a;font-weight:700">${window.fmtPrice(final)}</span> <span style="font-size:10px;color:#9ca3af;text-decoration:line-through margin-left:4px">${it.original_price} USDC</span> <span style="font-size:10px;color:#16a34a;background:#dcfce7;padding:1px 5px;border-radius:99px;margin-left:4px;font-weight:600">-${pct}%</span></div>
6528
6494
  <div style="font-size:10px;color:#9ca3af;margin-top:2px">@${escHtml(it.seller_handle || '')} · ${t('截止')} ${fmtTime(it.ends_at)}</div>
6529
6495
  </div>
6530
6496
  </div>
@@ -6562,7 +6528,7 @@ async function renderGroupBuyDetail(app, id) {
6562
6528
  <h1 class="page-title">👥 ${escHtml(r.product_title)}</h1>
6563
6529
  <div class="card" style="background:linear-gradient(135deg,#ecfdf5,#fff);padding:14px;margin-bottom:10px">
6564
6530
  <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
6565
- <div><div style="font-size:24px;font-weight:800;color:#16a34a">${final} WAZ</div><div style="font-size:11px;color:#9ca3af;text-decoration:line-through">${r.original_price} WAZ</div></div>
6531
+ <div><div style="font-size:24px;font-weight:800;color:#16a34a">${window.fmtPrice(final)}</div><div style="font-size:11px;color:#9ca3af;text-decoration:line-through">${r.original_price} USDC</div></div>
6566
6532
  <div style="font-size:18px;font-weight:700;color:#16a34a;background:#dcfce7;padding:6px 12px;border-radius:8px">-${pct}%</div>
6567
6533
  </div>
6568
6534
  <div style="font-size:12px;color:#374151;margin-bottom:4px">${t('已成团')} <strong>${joined}/${r.target_count}</strong> · ${t('剩余时间')} ${remainingStr}</div>
@@ -6572,7 +6538,7 @@ async function renderGroupBuyDetail(app, id) {
6572
6538
  : expired ? `<div style="color:#9ca3af">${t('已结束,等待结算')}</div>`
6573
6539
  : isOwn ? `<div style="color:#9ca3af">${t('自己开的团,等待成团')}</div>`
6574
6540
  : isParticipant ? `<button class="btn btn-outline btn-sm" style="width:100%;color:#dc2626;border-color:#fca5a5" onclick="leaveGroupBuy('${r.id}')">${t('退出团购')}</button>`
6575
- : state.user?.role === 'buyer' ? `<button class="btn btn-primary" style="width:100%" onclick="openJoinGroupBuy('${r.id}')">+ ${t('加入团购')} (${r.original_price} WAZ ${t('预付')})</button>`
6541
+ : state.user?.role === 'buyer' ? `<button class="btn btn-primary" style="width:100%" onclick="openJoinGroupBuy('${r.id}')">+ ${t('加入团购')} (${window.fmtPrice(r.original_price)} ${t('预付')})</button>`
6576
6542
  : `<div style="color:#9ca3af">${t('仅买家可加入')}</div>`}
6577
6543
  </div>
6578
6544
  <div style="font-size:13px;font-weight:600;margin:14px 0 8px">${t('已加入')} (${joined})</div>
@@ -6647,7 +6613,7 @@ async function renderSellerFlashSales(app) {
6647
6613
  <div style="font-size:13px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:60%">${escHtml(it.product_title)}</div>
6648
6614
  <span style="font-size:10px;font-weight:600;color:#fff;background:${phaseColor};padding:2px 8px;border-radius:99px">${phaseLabel}</span>
6649
6615
  </div>
6650
- <div style="font-size:12px;color:#374151">${it.sale_price} WAZ <span style="font-size:11px;color:#9ca3af;text-decoration:line-through;margin-left:4px">${it.original_price}</span></div>
6616
+ <div style="font-size:12px;color:#374151">${window.fmtPrice(it.sale_price)} <span style="font-size:11px;color:#9ca3af;text-decoration:line-through;margin-left:4px">${it.original_price} USDC</span></div>
6651
6617
  <div style="font-size:11px;color:#9ca3af;margin-top:2px">${fmtTime(it.starts_at)} → ${fmtTime(it.ends_at)}${it.max_qty > 0 ? ' · ' + t('已售') + ' ' + it.sold_count + '/' + it.max_qty : ''}</div>
6652
6618
  ${phase === 'scheduled' ? `<button class="btn btn-outline btn-sm" style="margin-top:6px;font-size:10px;padding:3px 8px;color:#dc2626;border-color:#fca5a5" onclick="deleteFlashSale('${it.id}')">${t('取消')}</button>` : ''}
6653
6619
  </div>`
@@ -6810,7 +6776,7 @@ window.aiSuggestPrice = async () => {
6810
6776
  if (res.error) { if (hint) hint.innerHTML = '<span style="color:#dc2626">' + escHtml(res.error) + '</span>'; return }
6811
6777
  const priceInp = document.getElementById('ep-price')
6812
6778
  if (hint) {
6813
- hint.innerHTML = `💡 ${t('建议')} <strong style="color:#16a34a;cursor:pointer;text-decoration:underline" onclick="document.getElementById('ep-price').value=${res.suggested_price}">${res.suggested_price}</strong> WAZ · ${t('区间')} ${res.low_price}-${res.high_price} · ${escHtml(res.reasoning || '')}`
6779
+ hint.innerHTML = `💡 ${t('建议价')} <strong style="color:#16a34a;cursor:pointer;text-decoration:underline" onclick="document.getElementById('ep-price').value=${res.suggested_price}">${res.suggested_price}</strong> WAZ · ${t('区间')} ${res.low_price}-${res.high_price} · ${escHtml(res.reasoning || '')}`
6814
6780
  }
6815
6781
  }
6816
6782
 
@@ -6904,10 +6870,10 @@ async function renderReferral(app) {
6904
6870
 
6905
6871
  <div class="card" style="background:linear-gradient(135deg,#ecfdf5,#fff);padding:14px;margin-bottom:10px">
6906
6872
  <div style="font-size:13px;font-weight:600;margin-bottom:8px">💰 ${t('邀请收益')}</div>
6907
- <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:8px;text-align:center">
6873
+ <div style="display:grid;grid-template-columns:repeat(${Number(e.clearing_waz||0)>0?4:3},1fr);gap:8px;text-align:center">
6908
6874
  <div><div style="font-size:18px;font-weight:700;color:#16a34a">${Number(e.total_waz || 0).toFixed(2)}</div><div style="font-size:10px;color:#9ca3af">${t('累计收益')}</div></div>
6909
6875
  <div><div style="font-size:18px;font-weight:700;color:#4f46e5">${Number(e.month_waz || 0).toFixed(2)}</div><div style="font-size:10px;color:#9ca3af">${t('近 30 天')}</div></div>
6910
- <div><div style="font-size:18px;font-weight:700;color:#d97706">${Number(e.today_waz || 0).toFixed(2)}</div><div style="font-size:10px;color:#9ca3af">${t('今日')}</div></div>
6876
+ <div><div style="font-size:18px;font-weight:700;color:#d97706">${Number(e.today_waz || 0).toFixed(2)}</div><div style="font-size:10px;color:#9ca3af">${t('今日')}</div></div>${Number(e.clearing_waz||0)>0?`<div><div style="font-size:18px;font-weight:700;color:#9ca3af" title="${t('清算中:退货期满后到账')}">${Number(e.clearing_waz).toFixed(2)}</div><div style="font-size:10px;color:#9ca3af">${t('清算中')}</div></div>`:''}
6911
6877
  </div>
6912
6878
  <div style="font-size:10px;color:#9ca3af;margin-top:6px;text-align:center">${e.total_records} ${t('条记录')} · WAZ</div>
6913
6879
  </div>
@@ -8065,7 +8031,7 @@ async function renderFollowFeed(app) {
8065
8031
  : items.map(it => {
8066
8032
  const meta = TYPE_META[it.type] || { icon: '·', label: it.type, color: '#6b7280' }
8067
8033
  let imageUrl = ''
8068
- try { const imgs = typeof it.images === 'string' ? JSON.parse(it.images) : it.images; if (Array.isArray(imgs) && imgs[0]) imageUrl = imgs[0] } catch {}
8034
+ imageUrl = window.productThumbSrc(it.images) || imageUrl
8069
8035
  return `
8070
8036
  <div class="card" style="padding:10px 12px;margin-bottom:8px;cursor:pointer" onclick="location.hash='#order-product/${it.product_id}'">
8071
8037
  <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">
@@ -8076,10 +8042,10 @@ async function renderFollowFeed(app) {
8076
8042
  <span style="font-size:10px;color:#9ca3af">${fmtTime(it.ts)}</span>
8077
8043
  </div>
8078
8044
  <div style="display:flex;gap:10px;align-items:center">
8079
- <div style="font-size:28px;flex-shrink:0">${imageUrl ? `<img src="${escHtml(imageUrl)}" style="width:48px;height:48px;border-radius:6px;object-fit:cover">` : getCategoryIcon(it.category) || '📦'}</div>
8045
+ <div style="font-size:28px;flex-shrink:0">${imageUrl ? `<img src="${escHtml(imageUrl)}" onerror="this.outerHTML='📦'" style="width:48px;height:48px;border-radius:6px;object-fit:cover">` : getCategoryIcon(it.category) || '📦'}</div>
8080
8046
  <div style="flex:1;min-width:0">
8081
8047
  <div style="font-size:14px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escHtml(it.title)}</div>
8082
- <div style="font-size:13px;color:#4f46e5;margin-top:2px;font-weight:600">${it.price} WAZ <span style="font-size:11px;color:#9ca3af;font-weight:400">· ${stockBadgeHtml(it)}</span></div>
8048
+ <div style="font-size:13px;color:#4f46e5;margin-top:2px;font-weight:600">${window.fmtPrice(it.price)} <span style="font-size:11px;color:#9ca3af;font-weight:400">· ${stockBadgeHtml(it)}</span></div>
8083
8049
  </div>
8084
8050
  </div>
8085
8051
  </div>`
@@ -8123,8 +8089,8 @@ async function renderReturnsCenter(app) {
8123
8089
  ])
8124
8090
  const items = list?.items || []
8125
8091
  const reasons = RETURN_REASON_LABEL()
8126
- const STATUS_COLORS = { pending:'#d97706', refunded:'#16a34a', rejected:'#dc2626', cancelled:'#6b7280', accepted_pickup_pending:'#0891b2', picked_up:'#4f46e5' }
8127
- const STATUS_LABEL = () => ({ pending: t('待处理'), refunded: t('已退款'), rejected: t('已拒绝'), cancelled: t('已取消'), accepted_pickup_pending: '📦 ' + t('待物流上门取件'), picked_up: '🚛 ' + t('已揽收,待卖家确认收到') })
8092
+ const STATUS_COLORS = { pending:'#d97706', refunded:'#16a34a', rejected:'#dc2626', cancelled:'#6b7280', accepted_pickup_pending:'#0891b2', picked_up:'#4f46e5', ...(window.dpReturnStatusColors ? window.dpReturnStatusColors() : {}) }
8093
+ const STATUS_LABEL = () => ({ pending: t('待处理'), refunded: t('已退款'), rejected: t('已拒绝'), cancelled: t('已取消'), accepted_pickup_pending: '📦 ' + t('待物流上门取件'), picked_up: '🚛 ' + t('已揽收,待卖家确认收到'), ...(window.dpReturnStatusLabels ? window.dpReturnStatusLabels() : {}) })
8128
8094
 
8129
8095
  const statsHtml = stats ? `
8130
8096
  <div class="card" style="background:linear-gradient(135deg,#fff7ed,#fef3c7);margin-bottom:14px">
@@ -8990,7 +8956,7 @@ window.doLogin = async () => {
8990
8956
  if (!key) return showMsg('error', t('请粘贴 api_key'))
8991
8957
  state.apiKey = key
8992
8958
  const user = await GET('/me')
8993
- if (user.error) { state.apiKey = null; return showMsg('error', t('无效的 api_key,请重新输入')) }
8959
+ if (user.error) { if (user.error_code === 'AGENT_BLOCKED' && window.renderAgentBlockedAppeal) { try { document.querySelectorAll('.js-modal, .js-sheet').forEach(m => m.remove()) } catch {}; return void window.renderAgentBlockedAppeal(user.error) } state.apiKey = null; return showMsg('error', t('无效的 api_key,请重新输入')) }
8994
8960
  state.user = user
8995
8961
  await persistApiKey(key)
8996
8962
  connectSSE()
@@ -9176,7 +9142,7 @@ function renderShareBanner(variant) {
9176
9142
  const tgtKind = ctx.target_kind
9177
9143
  const tgtLine = tgt
9178
9144
  ? (tgtKind === 'product'
9179
- ? `🎁 <strong>${escHtml(tgt.title)}</strong> · ${tgt.price} WAZ <span style="color:#9ca3af">@ ${escHtml(tgt.seller_name)}</span>`
9145
+ ? `🎁 <strong>${escHtml(tgt.title)}</strong> · ${window.fmtPrice(tgt.price)} <span style="color:#9ca3af">@ ${escHtml(tgt.seller_name)}</span>`
9180
9146
  : tgtKind === 'user'
9181
9147
  ? `👤 <strong>${escHtml(tgt.name)}</strong>${tgt.bio ? ` · ${escHtml(tgt.bio).slice(0, 40)}` : ''}`
9182
9148
  : '')
@@ -10137,7 +10103,7 @@ async function renderBuyPage(app, productId) {
10137
10103
  const bottomBar = canBuy ? `
10138
10104
  <div style="display:flex;gap:8px;align-items:center">
10139
10105
  <div style="flex:0 0 auto">
10140
- <div style="font-size:18px;font-weight:800;color:#dc2626;line-height:1">${livePrice} <span style="font-size:11px;font-weight:600">WAZ</span></div>
10106
+ <div style="font-size:18px;font-weight:800;color:#dc2626;line-height:1">${window.fmtPrice(livePrice)}</div>
10141
10107
  ${state._flashSale ? `<div style="font-size:10px;color:#9ca3af;text-decoration:line-through">${p.price}</div>` : ''}
10142
10108
  </div>
10143
10109
  <button class="btn btn-outline" onclick="addToCart('${p.id}')" style="width:auto;padding:11px 14px;font-size:13px" title="${t('加入购物车')}">🛒+</button>
@@ -10151,18 +10117,18 @@ async function renderBuyPage(app, productId) {
10151
10117
  ${originBadge}
10152
10118
  <div class="card">
10153
10119
  ${productImageGallery(p)}
10154
- <h2 style="font-size:18px;font-weight:700;margin-bottom:6px">${escHtml(p.title)}</h2>
10120
+ <h2 style="font-size:18px;font-weight:700;margin-bottom:6px">${escHtml(p.title)}</h2>${window.extLinksBarHtml ? window.extLinksBarHtml(productId) : ''}
10155
10121
  ${state._flashSale ? `
10156
10122
  <div style="background:linear-gradient(135deg,#dc2626,#f59e0b);color:#fff;border-radius:8px;padding:8px 12px;margin-bottom:8px;display:flex;justify-content:space-between;align-items:center">
10157
10123
  <div>
10158
10124
  <div style="font-size:10px;font-weight:600;opacity:0.9">⚡ ${t('限时')}</div>
10159
- <div style="font-size:18px;font-weight:800">${state._flashSale.sale_price} WAZ <span style="font-size:11px;font-weight:400;text-decoration:line-through;opacity:0.7">${p.price}</span></div>
10125
+ <div style="font-size:18px;font-weight:800">${window.fmtPrice(state._flashSale.sale_price)} <span style="font-size:11px;font-weight:400;text-decoration:line-through;opacity:0.7">${p.price} USDC</span></div>
10160
10126
  </div>
10161
10127
  <div style="text-align:right;font-size:10px;opacity:0.9">${t('截止')}<br><span style="font-size:11px;font-weight:600">${fmtTime(state._flashSale.ends_at)}</span></div>
10162
10128
  </div>` : `
10163
10129
  <div style="display:flex;align-items:baseline;gap:8px;margin-bottom:8px">
10164
10130
  <span style="font-size:22px;font-weight:800;color:#dc2626">${p.price}</span>
10165
- <span style="font-size:12px;color:#6b7280">WAZ${usdHint(p.price)}</span>
10131
+ <span style="font-size:12px;color:#6b7280" data-usdc-local="${p.price}">USDC${window._fxLocal(p.price) ? ' ≈ ' + window._fxLocal(p.price) : ''}</span>
10166
10132
  </div>`}
10167
10133
  ${Number(p.trial_quota_remaining) > 0 ? `
10168
10134
  <div style="background:linear-gradient(135deg,#faf5ff,#f3e8ff);border:1px solid #e9d5ff;border-radius:8px;padding:10px 12px;margin-bottom:10px">
@@ -10322,7 +10288,7 @@ async function renderBuyPage(app, productId) {
10322
10288
  offers: {
10323
10289
  '@type': 'Offer',
10324
10290
  price: p.price,
10325
- priceCurrency: p.currency || 'WAZ', // 不再硬编码,读表里的 currency
10291
+ priceCurrency: 'USDC', // display/pricing unit (USDC≈USD); matches the human USDC display (PR-1c)
10326
10292
  availability: (Number(p.stock) || 0) > 0 ? 'https://schema.org/InStock' : 'https://schema.org/OutOfStock',
10327
10293
  url: location.origin + '/#order-product/' + p.id,
10328
10294
  seller: { '@type': 'Organization', '@id': location.origin + '/#u/' + p.seller_id, name: p.seller_name || p.seller_handle || p.seller_id },
@@ -10366,14 +10332,14 @@ window.openBuySheet = function(productId) {
10366
10332
  const summary = hasDefault ? buildAddrString(defaultAddr) : ''
10367
10333
  const max = buyMaxFor(prod)
10368
10334
  const variants = state._variants || []
10369
- const firstImg = (() => { try { const arr = typeof prod.images === 'string' ? JSON.parse(prod.images) : (prod.images || []); return arr[0] || '' } catch { return '' } })()
10335
+ const firstImg = window.productThumbSrc(prod.images)
10370
10336
 
10371
10337
  const html = `
10372
10338
  <div style="display:flex;gap:10px;align-items:center;padding-bottom:12px;border-bottom:1px solid #f3f4f6">
10373
- ${firstImg ? `<img src="${escHtml(firstImg)}" style="width:64px;height:64px;border-radius:8px;object-fit:cover;flex-shrink:0">` : `<div style="width:64px;height:64px;background:#f3f4f6;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:24px;flex-shrink:0">📦</div>`}
10339
+ ${firstImg ? `<img src="${escHtml(firstImg)}" onerror="this.outerHTML='📦'" style="width:64px;height:64px;border-radius:8px;object-fit:cover;flex-shrink:0">` : `<div style="width:64px;height:64px;background:#f3f4f6;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:24px;flex-shrink:0">📦</div>`}
10374
10340
  <div style="flex:1;min-width:0">
10375
10341
  <div style="font-size:13px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escHtml(prod.title)}</div>
10376
- <div style="font-size:18px;font-weight:800;color:#dc2626;margin-top:2px">${livePrice} <span style="font-size:11px;font-weight:600">WAZ</span></div>
10342
+ <div style="font-size:18px;font-weight:800;color:#dc2626;margin-top:2px">${window.fmtPrice(livePrice)}</div>
10377
10343
  </div>
10378
10344
  ${variants.length === 0 && max > 0 ? `
10379
10345
  <div style="display:flex;align-items:center;gap:6px;flex-shrink:0">
@@ -10471,11 +10437,12 @@ window.openBuySheet = function(productId) {
10471
10437
  </div>
10472
10438
  </details>
10473
10439
 
10440
+ ${window.dpRailSelectorHtml ? window.dpRailSelectorHtml(prod.id, prod.price) : ''}${window.shipRegionBlockHtml ? window.shipRegionBlockHtml(prod.id) : ''}${window.purchaseTermsBlockHtml ? window.purchaseTermsBlockHtml(prod.id) : ''}
10474
10441
  <div id="buy-msg" style="margin-top:10px"></div>
10475
10442
  <div id="tax-preview-slot" style="margin-top:8px"></div>
10476
10443
 
10477
10444
  <div style="position:sticky;bottom:0;left:0;right:0;background:#fff;padding-top:12px;margin-top:12px;border-top:1px solid #f3f4f6">
10478
- <button class="btn btn-primary" id="btn-doBuy" onclick="doBuy('${prod.id}', ${livePrice}); closeSheet()" style="width:100%;padding:14px;font-size:15px;font-weight:700">${t('确认下单')} · ${livePrice} WAZ</button>
10445
+ <button class="btn btn-primary" id="btn-doBuy" onclick="doBuy('${prod.id}', ${livePrice})" style="width:100%;padding:14px;font-size:15px;font-weight:700">${t('确认下单')} · ${window.fmtPrice(livePrice)}</button>
10479
10446
  </div>
10480
10447
  `
10481
10448
  openSheet(html, { title: t('下单') })
@@ -11246,6 +11213,7 @@ window.qtyStep = (delta, max) => {
11246
11213
  const cur = Math.floor(Number(inp.value) || 1)
11247
11214
  const next = Math.max(1, Math.min(max, cur + delta))
11248
11215
  inp.value = next
11216
+ if (window.dpRenderAccountFx) window.dpRenderAccountFx() // D3:数量变→刷新分账号 FX(按订单总额,非单价)
11249
11217
  }
11250
11218
 
11251
11219
  // B5 捐赠 chip 选择 — 高亮当前选中 + 更新提示文案
@@ -11275,6 +11243,7 @@ window.qtyClamp = (max) => {
11275
11243
  if (!Number.isFinite(v) || v < 1) v = 1
11276
11244
  if (v > max) v = max
11277
11245
  inp.value = v
11246
+ if (window.dpRenderAccountFx) window.dpRenderAccountFx() // D3:数量变→刷新分账号 FX(按订单总额,非单价)
11278
11247
  }
11279
11248
 
11280
11249
  window.doBuy = async (productId, price) => {
@@ -11315,9 +11284,11 @@ window.doBuy = async (productId, price) => {
11315
11284
  // 数量(默认 1)— 服务端会再校验 stock + MAX_PER_ORDER
11316
11285
  const qtyInp = document.getElementById('inp-qty')
11317
11286
  const quantity = qtyInp ? Math.max(1, Math.min(Number(qtyInp.max) || 1, Math.floor(Number(qtyInp.value) || 1))) : 1
11318
- const res = await POST('/orders', { product_id: productId, shipping_address: addr, notes, sponsor_hint, coupon_code, delivery_window, variant_id, expected_price, buy_insurance, anonymous_recipient, donation_pct, quantity, ...giftPayload })
11287
+ const payment_rail = window.dpSelectedRail ? window.dpSelectedRail() : 'escrow'; if (!payment_rail) { const _dp=document.querySelector('input[name="dp-rail"]:checked')?.value==='direct_p2p'; const _m=document.getElementById('buy-msg'); if(_m) _m.innerHTML = alert$('error', _dp ? t('直付暂未就绪或不可用,请稍候重试或改选支付方式') : t('请选择支付方式')); const _b=document.getElementById('dp-rail-block'); if(_b) _b.open=true; return } if (window.closeSheet) window.closeSheet() // #28 空 rail 一律拦(永不静默落 escrow);[PRELAUNCH-WAZ-SIM] 未选→空;校验通过才关 sheet(块内 return 不关→提示+展开可见,修 P2)
11288
+ const res = await POST('/orders', { product_id: productId, shipping_address: addr, notes, sponsor_hint, coupon_code, delivery_window, variant_id, expected_price, buy_insurance, anonymous_recipient, donation_pct, quantity, payment_rail, ship_to_region: (window.shipSelectedRegion ? window.shipSelectedRegion() : undefined), direct_receive_account_id: (payment_rail === 'direct_p2p' && window.dpSelectedAccountId) ? (window.dpSelectedAccountId() || undefined) : undefined, ...giftPayload })
11289
+ if (payment_rail === 'direct_p2p') return void (window.dpAfterCreate && window.dpAfterCreate(res))
11319
11290
  if (res.error_code === 'PRICE_CHANGED') {
11320
- document.getElementById('buy-msg').innerHTML = alert$('error', `${t('价格已变动')}:${res.old_price} → ${res.new_price} WAZ · ${t('请刷新页面')}`)
11291
+ document.getElementById('buy-msg').innerHTML = alert$('error', `${t('价格已变动')}:${window.fmtPrice(res.old_price)} → ${window.fmtPrice(res.new_price)} · ${t('请刷新页面')}`)
11321
11292
  return
11322
11293
  }
11323
11294
  if (res.error) { document.getElementById('buy-msg').innerHTML = alert$('error', res.error); return }
@@ -11339,11 +11310,11 @@ window.checkCoupon = async (productId, basePrice) => {
11339
11310
  const r = await GET(`/coupons/preview?code=${encodeURIComponent(code)}&product_id=${productId}`)
11340
11311
  if (!r.ok) {
11341
11312
  resultEl.innerHTML = `<span style="color:#dc2626">✗ ${escHtml(r.error || t('优惠码无效'))}</span>`
11342
- if (btnEl) btnEl.innerHTML = `${t('立即下单')} · ${basePrice} WAZ`
11313
+ if (btnEl) btnEl.innerHTML = `${t('立即下单')} · ${window.fmtPrice(basePrice)}`
11343
11314
  return
11344
11315
  }
11345
- resultEl.innerHTML = `<span style="color:#16a34a">✓ ${t('已优惠')} ${r.discount} WAZ · ${t('最终')} ${r.final_price} WAZ</span>`
11346
- if (btnEl) btnEl.innerHTML = `${t('立即下单')} · ${r.final_price} WAZ <span style="font-size:10px;text-decoration:line-through;opacity:0.7">${basePrice}</span>`
11316
+ resultEl.innerHTML = `<span style="color:#16a34a">✓ ${t('已优惠')} ${window.fmtPrice(r.discount)} · ${t('最终')} ${window.fmtPrice(r.final_price)}</span>`
11317
+ if (btnEl) btnEl.innerHTML = `${t('立即下单')} · ${window.fmtPrice(r.final_price)} <span style="font-size:10px;text-decoration:line-through;opacity:0.7">${basePrice} USDC</span>`
11347
11318
  }
11348
11319
 
11349
11320
  // ─── P13: 购物车页 ────────────────────────────────────────────
@@ -11712,6 +11683,7 @@ async function renderOrders(app) {
11712
11683
  const ordersScope = state.ordersScope || (state.user.role === 'seller' ? 'sold' : 'all')
11713
11684
  if (ordersScope === 'sold') orders = orders.filter(o => o.seller_id === state.user.id)
11714
11685
  else if (ordersScope === 'bought') orders = orders.filter(o => o.buyer_id === state.user.id)
11686
+ const railChipsHtml = window.orderRailChipsHtml(orders); orders = window.orderRailApply(orders) // 类型(支付轨)筛选 chip + 过滤;逻辑在 app-order-rail-filter.js
11715
11687
  const groups = {
11716
11688
  all: { label: t('全部'), icon: '📋', match: () => true },
11717
11689
  pay: { label: t('待付款'), icon: '💳', match: o => o.status === 'created' },
@@ -11781,18 +11753,18 @@ async function renderOrders(app) {
11781
11753
  <div class="order-icon">${pending ? '⏰' : getCategoryIcon(o.category)}</div>
11782
11754
  <div class="order-info">
11783
11755
  <div class="order-title">${escHtml(o.product_title)}</div>
11784
- <div class="order-meta">${fmtTime(o.created_at)} · ${o.buyer_id === state.user.id ? t('我买的') : t('我卖的')}</div>
11756
+ <div class="order-meta">${fmtTime(o.created_at)} · ${o.buyer_id === state.user.id ? t('我买的') : t('我卖的')}${window.orderRailBadge(o)}</div>
11785
11757
  <div style="margin-top:6px">${orderStatusBadges(o)}</div>
11786
11758
  </div>
11787
- <div class="order-amount">${o.total_amount}</div>
11759
+ <div class="order-amount">${window.orderAmountHtml(o)}</div>
11788
11760
  </div>
11789
11761
  ${canRebuy ? `<div style="border-top:1px solid #f3f4f6;padding:8px 14px;display:flex;justify-content:flex-end"><button onclick="event.stopPropagation();navigate('#order-product/${o.product_id}')" style="background:#eef2ff;color:#3730a3;border:1px solid #c7d2fe;padding:4px 12px;border-radius:99px;font-size:11px;font-weight:600;cursor:pointer">🔁 ${t('再次购买')}</button></div>` : ''}
11790
11762
  </div>`
11791
11763
  }).join('')
11792
11764
 
11793
11765
  const exportRole = state.user?.role === 'seller' ? 'seller' : 'buyer'
11794
- const exportBtn = `<button class="btn btn-outline btn-sm" style="float:right;font-size:11px;padding:4px 10px" onclick="exportOrders('${exportRole}')">📥 ${t('导出 CSV')}</button>`
11795
- app.innerHTML = shell(`<h1 class="page-title">${t('我的订单')} ${exportBtn}</h1>${mainTabsHtml}${scopeChipsHtml}${notePromptPlaceholder('orders')}${tabsHtml}${pendingBanner}${list}`, 'orders')
11766
+ const exportBtn = `<button class="btn btn-outline btn-sm" style="float:right;font-size:11px;padding:4px 10px" onclick="exportOrders('${exportRole}','${state.ordersRail || 'all'}')">📥 ${t('导出 CSV')}</button>`
11767
+ app.innerHTML = shell(`<h1 class="page-title">${t('我的订单')} ${exportBtn}</h1>${mainTabsHtml}${scopeChipsHtml}${railChipsHtml}${notePromptPlaceholder('orders')}${tabsHtml}${pendingBanner}${list}`, 'orders')
11796
11768
  hydrateNotePrompt('orders')
11797
11769
  }
11798
11770
 
@@ -11824,9 +11796,9 @@ window.printOrderReceipt = async (orderId) => {
11824
11796
  <div class="row"><span>${t('商品')}</span><strong>${escHtml(product?.title || '—')}</strong></div>
11825
11797
  ${variantStr ? `<div class="row"><span>${t('规格')}</span><span>${escHtml(variantStr)}</span></div>` : ''}
11826
11798
  <div class="row"><span>${t('数量')}</span><span>${order.quantity}</span></div>
11827
- <div class="row"><span>${t('单价')}</span><span>${order.unit_price} WAZ</span></div>
11828
- ${Number(order.coupon_discount) > 0 ? `<div class="row"><span>${t('优惠')}</span><span style="color:#dc2626">-${order.coupon_discount} WAZ</span></div>` : ''}
11829
- <div class="total">${t('总计')}: ${order.total_amount} WAZ</div>
11799
+ <div class="row"><span>${t('单价')}</span><span>${order.unit_price} USDC</span></div>
11800
+ ${Number(order.coupon_discount) > 0 ? `<div class="row"><span>${t('优惠')}</span><span style="color:#dc2626">-${order.coupon_discount} USDC</span></div>` : ''}
11801
+ <div class="total">${t('总计')}: ${order.total_amount} USDC</div>
11830
11802
  <hr style="margin:14px 0;border:none;border-top:1px solid #ddd">
11831
11803
  <div class="muted">${t('收货地址')}: ${escHtml(order.shipping_address || '—')}</div>
11832
11804
  <div class="muted">${t('订单状态')}: ${order.status}</div>
@@ -11835,12 +11807,10 @@ window.printOrderReceipt = async (orderId) => {
11835
11807
  w.document.close()
11836
11808
  }
11837
11809
 
11838
- window.exportOrders = (role) => {
11839
- // 直接打开端点 浏览器下载(Authorization 头通过 token query 传递)
11840
- const url = `/api/orders/export?role=${role}&token=${encodeURIComponent(state.apiKey)}`
11841
- // 浏览器导航触发下载(Content-Disposition: attachment)
11842
- // 由于 auth() 当前只读 Authorization 头,导出端点需支持 query token;改用 fetch + blob 下载更稳
11843
- fetch(`/api/orders/export?role=${role}`, { headers: { Authorization: 'Bearer ' + state.apiKey } })
11810
+ window.exportOrders = (role, rail) => {
11811
+ const railQ = (rail === 'escrow' || rail === 'direct_p2p') ? `&rail=${rail}` : '' // 导出与当前筛选一致(对账分轨)
11812
+ // auth() 只读 Authorization 头 → 用 fetch + blob 下载(Content-Disposition: attachment)
11813
+ fetch(`/api/orders/export?role=${role}${railQ}`, { headers: { Authorization: 'Bearer ' + state.apiKey } })
11844
11814
  .then(async (r) => {
11845
11815
  const truncated = r.headers.get('X-Truncated') === '1'
11846
11816
  const limit = r.headers.get('X-Truncated-Limit')
@@ -11940,7 +11910,7 @@ async function renderOrdersShare(app, mainTabsHtml) {
11940
11910
  <div style="font-size:28px;flex-shrink:0">${getCategoryIcon(o.category)}</div>
11941
11911
  <div style="flex:1;min-width:0">
11942
11912
  <div style="font-size:13px;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escHtml(o.product_title)}</div>
11943
- <div style="font-size:11px;color:#6b7280;margin-top:2px"><a href="#order/${o.id}" style="color:#6b7280;text-decoration:none">${t('订单')} ${orderShort}</a> · ${fmtTime(o.updated_at || o.created_at)}${typeof o.total_amount !== 'undefined' ? ` · ${o.total_amount} WAZ` : ''}</div>
11913
+ <div style="font-size:11px;color:#6b7280;margin-top:2px"><a href="#order/${o.id}" style="color:#6b7280;text-decoration:none">${t('订单')} ${orderShort}</a> · ${fmtTime(o.updated_at || o.created_at)}${typeof o.total_amount !== 'undefined' ? ` · ${window.orderAmountHtml(o)}` : ''}</div>
11944
11914
  </div>
11945
11915
  <div style="display:flex;gap:4px;flex-shrink:0">
11946
11916
  <button class="btn btn-outline btn-sm" style="width:auto;padding:5px 8px;font-size:10px;white-space:nowrap" onclick="shareCompletedOrder('${o.product_id}', '${escHtml(o.product_title || '').replace(/'/g, "\\'")}')">🔗 ${t('添加口令')}</button>
@@ -12184,10 +12154,10 @@ function orderStageTimeline(order, history) {
12184
12154
  { key: 'completed', label: t('完成'), icon: '✓' },
12185
12155
  ]
12186
12156
  // 异常状态:单独 banner,不画时间线(防误导)
12187
- const ANOMALY = ['disputed', 'cancelled', 'fault_seller', 'fault_buyer', 'fault_logistics', 'refunded_partial', 'refunded_full', 'dispute_dismissed', 'expired']
12157
+ const ANOMALY = ['disputed', 'payment_query', 'cancelled', 'fault_seller', 'fault_buyer', 'fault_logistics', 'refunded_partial', 'refunded_full', 'dispute_dismissed', 'expired']
12188
12158
  if (ANOMALY.includes(order.status)) {
12189
12159
  const colorMap = {
12190
- disputed: '#f59e0b', cancelled: '#6b7280',
12160
+ disputed: '#f59e0b', payment_query: '#f59e0b', cancelled: '#6b7280',
12191
12161
  fault_seller: '#dc2626', fault_buyer: '#dc2626', fault_logistics: '#dc2626',
12192
12162
  refunded_full: '#16a34a', refunded_partial: '#3b82f6', dispute_dismissed: '#6b7280', expired: '#9ca3af',
12193
12163
  }
@@ -12201,7 +12171,7 @@ function orderStageTimeline(order, history) {
12201
12171
  return `<div style="background:#fff;border:0.5px solid #e5e7eb;border-radius:12px;padding:14px 16px;margin-bottom:10px;display:flex;align-items:center;gap:10px">
12202
12172
  <div style="width:8px;height:8px;border-radius:50%;background:${c};flex-shrink:0"></div>
12203
12173
  <div style="flex:1">
12204
- <div style="font-size:14px;font-weight:600;color:#1f2937">${labelMap[order.status] || order.status}</div>
12174
+ <div style="font-size:14px;font-weight:600;color:#1f2937">${(order.payment_rail === 'direct_p2p' && ((window.dpTerminalLabel && window.dpTerminalLabel(order.status)) || (window.dpNegotiationLabel && window.dpNegotiationLabel(order.status)) || (window.dpAcceptLabel && window.dpAcceptLabel(order.status)))) || labelMap[order.status] || order.status}</div>
12205
12175
  <div style="font-size:11px;color:#8e8e93;margin-top:2px">${t('查看下方时间线了解流转详情')}</div>
12206
12176
  </div>
12207
12177
  </div>`
@@ -12221,7 +12191,7 @@ function orderStageTimeline(order, history) {
12221
12191
  // "当前" = 正在等待的下一阶段(不是已完成的最后阶段)
12222
12192
  // created → 等付款;paid → 等接单;accepted → 等发货;shipped/in_transit → 等送达;
12223
12193
  // delivered → 等买家确认;confirmed/completed → 全部完成
12224
- const statusToIdx = { created: 1, paid: 2, accepted: 3, shipped: 4, picked_up: 4, in_transit: 4, delivered: 5, confirmed: 6, completed: 6 }
12194
+ const statusToIdx = { created: 1, direct_pay_window: 1, direct_expired_unconfirmed: 1, paid: 2, accepted: 3, shipped: 4, picked_up: 4, in_transit: 4, delivered: 5, confirmed: 6, completed: 6 }
12225
12195
  let currentIdx = statusToIdx[order.status] ?? 0
12226
12196
 
12227
12197
  const dot = (s, i) => {
@@ -12255,7 +12225,7 @@ function orderStageTimeline(order, history) {
12255
12225
  // 与顶部紧凑 stepper 互补:stepper 给总览,timeline 给细节
12256
12226
  function orderTrackingTimeline(order, history, trackingInfo, STATUS_ZH) {
12257
12227
  // 异常订单不显示物流时间线(争议/取消等)— 由顶部 stepper 异常 banner 兜底
12258
- const ANOMALY = ['disputed', 'cancelled', 'fault_seller', 'fault_buyer', 'fault_logistics', 'refunded_partial', 'refunded_full', 'dispute_dismissed', 'expired']
12228
+ const ANOMALY = ['disputed', 'payment_query', 'cancelled', 'fault_seller', 'fault_buyer', 'fault_logistics', 'refunded_partial', 'refunded_full', 'dispute_dismissed', 'expired']
12259
12229
  if (ANOMALY.includes(order.status)) return ''
12260
12230
 
12261
12231
  // 已完成节点 lookup
@@ -12385,14 +12355,18 @@ function nextActionCard(order, isBuyer, isSeller, activeDeadline, isOverdue) {
12385
12355
  // 我自己是违约方时只看到警告,不显示按钮(不能自己触发对自己的判责)
12386
12356
  const meAtFault = hint.overdueFault && hint.overdueFault === ('fault_' + (isBuyer ? 'buyer' : isSeller ? 'seller' : ''))
12387
12357
  const canForceTrigger = isOverdue && hint.overdueFault && !meAtFault && (isBuyer || isSeller)
12358
+ // 直付(非托管):WebAZ 不退款/不放款 → 超时后果只写信誉责任,绝不写"退款/资金释放"
12359
+ const overdueConsequence = order.payment_rail === 'direct_p2p'
12360
+ ? ({ paid: '卖家信誉扣分;直付非托管,WebAZ 不退款/不放款', accepted: '卖家信誉扣分;直付非托管,WebAZ 不退款/不放款', in_transit: '物流/履约信誉扣分;直付非托管,WebAZ 不退款', delivered: '系统自动确认直付订单完成;不涉及平台放款' }[order.status] || hint.overdueConsequence)
12361
+ : hint.overdueConsequence
12388
12362
  return `<div style="background:${bg};border:0.5px solid ${isOverdue ? '#fecaca' : '#e5e7eb'};border-radius:12px;padding:14px 16px;margin-bottom:10px">
12389
12363
  <div style="display:flex;align-items:center;gap:10px">
12390
12364
  <div style="font-size:22px;line-height:1">${icon}</div>
12391
12365
  <div style="flex:1">
12392
12366
  <div style="font-size:14px;font-weight:600;color:${tone}">${t(myHint)}</div>
12393
12367
  ${activeDeadline?.deadline ? `<div style="font-size:11px;color:#8e8e93;margin-top:2px">${isOverdue ? t('已超时,协议将自动判责') : fmtCountdown(activeDeadline.deadline)}</div>` : ''}
12394
- ${isOverdue && hint.overdueConsequence ? `<div style="margin-top:8px;padding:8px 10px;background:#fff;border-radius:6px;font-size:12px;color:#7c2d12;border:1px dashed #fecaca">
12395
- ⚖️ ${t('协议处置')}:${t(hint.overdueConsequence)}
12368
+ ${isOverdue && overdueConsequence ? `<div style="margin-top:8px;padding:8px 10px;background:#fff;border-radius:6px;font-size:12px;color:#7c2d12;border:1px dashed #fecaca">
12369
+ ⚖️ ${t('协议处置')}:${t(overdueConsequence)}
12396
12370
  </div>` : ''}
12397
12371
  ${canForceTrigger ? `<button onclick="forceTimeoutCheck('${order.id}')" style="margin-top:10px;background:#dc2626;color:#fff;border:none;border-radius:8px;padding:8px 14px;font-size:13px;font-weight:600;cursor:pointer">⚡ ${t('立即触发判责')}</button>
12398
12372
  <div style="font-size:10px;color:#9ca3af;margin-top:6px">${t('协议巡检每 5 分钟一次;你也可立即触发,无需等待。')}</div>` : ''}
@@ -12441,9 +12415,10 @@ async function renderOrderDetail(app, orderId) {
12441
12415
 
12442
12416
  // 操作按钮
12443
12417
  const actions = getActions(order, isBuyer, isSeller, isLogistic)
12418
+ window._dpOrderRail = order.payment_rail // direct_p2p:路由 mark_paid/confirm 到 Passkey 门(动作在渲染后触发,早设即可);hydrate 移到最终 app.innerHTML 之后(见下)——在此调度会撞 claim-task 的 await 间隙、#dp-order-instr 尚未渲染而 if(!box) 静默 bail → 框永久"加载中"
12444
12419
 
12445
12420
  const STATUS_ZH = {
12446
- created:'待付款', paid:'待接单', accepted:'待发货', shipped:'已发货',
12421
+ created:'待付款', direct_pay_window:'直付待付款', direct_expired_unconfirmed:'直付超时未确认', paid:'待接单', accepted:'待发货', shipped:'已发货',
12447
12422
  picked_up:'已揽收', in_transit:'运输中', delivered:'待确认',
12448
12423
  confirmed:'已确认', completed:'已完成', disputed:'争议中',
12449
12424
  cancelled:'已取消', fault_seller:'卖家违约', fault_buyer:'买家违约', fault_logistics:'物流违约',
@@ -12451,11 +12426,11 @@ async function renderOrderDetail(app, orderId) {
12451
12426
 
12452
12427
  const historyHtml = (history || []).map(h => `
12453
12428
  <div class="timeline-item">
12454
- <div><span class="timeline-status">${STATUS_ZH[h.from_status] || h.from_status} → ${STATUS_ZH[h.to_status] || h.to_status}</span></div>
12455
- <div class="timeline-actor">${h.actor_name}(${h.actor_role_name || h.actor_role})</div>
12429
+ <div><span class="timeline-status">${t(STATUS_ZH[h.from_status] || h.from_status)} → ${t(STATUS_ZH[h.to_status] || h.to_status)}</span></div>
12430
+ <div class="timeline-actor">${escHtml(h.actor_name)}(${escHtml(h.actor_role_name || h.actor_role)})</div>
12456
12431
  <div class="timeline-time">${fmtTime(h.created_at)}</div>
12457
- ${h.notes ? `<div class="timeline-evidence" style="color:#6b7280">💬 ${h.notes}</div>` : ''}
12458
- ${(h.evidence_items || []).map(e => `<div class="timeline-evidence">📎 ${e.description}</div>`).join('')}
12432
+ ${h.notes ? `<div class="timeline-evidence" style="color:#6b7280">💬 ${escHtml(h.notes)}</div>` : ''}
12433
+ ${(h.evidence_items || []).map(e => `<div class="timeline-evidence">📎 ${escHtml(e.description)}</div>`).join('')}
12459
12434
  </div>`).join('')
12460
12435
 
12461
12436
  // 物流追踪完整时间轴 — 含订单全流转 + 未到达节点的截止提示
@@ -12501,21 +12476,21 @@ async function renderOrderDetail(app, orderId) {
12501
12476
  </div>
12502
12477
  </div>
12503
12478
 
12504
- ${orderStageTimeline(order, history)}
12479
+ ${window.wazEscrowOrderBanner ? window.wazEscrowOrderBanner(order, isBuyer) : ''}${orderStageTimeline(order, history)}
12505
12480
  ${nextActionCard(order, isBuyer, isSeller, activeDeadline, isOverdue)}
12506
12481
 
12507
12482
  <div class="card">
12508
- <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
12509
- <div style="font-size:13px;color:#6b7280;font-family:monospace">${order.id}</div>
12510
- <div style="display:flex;gap:6px;align-items:center">
12511
- ${(isBuyer || isSeller) ? `<button class="btn btn-sm" style="background:#eef2ff;color:#4338ca;font-size:11px;padding:4px 10px" onclick="openChatForContext('order','${order.id}')">💬 ${t('联系对方')}</button>` : ''}
12483
+ <div style="display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:8px;margin-bottom:12px">
12484
+ <div style="font-size:12px;color:#9ca3af;font-family:monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1 1 120px;min-width:0" title="${escHtml(order.id)}">${order.id}</div>
12485
+ <div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap;flex-shrink:0;white-space:nowrap">
12486
+ ${(isBuyer || isSeller) ? `<button class="btn btn-sm" style="background:#eef2ff;color:#4338ca;font-size:11px;padding:4px 10px;white-space:nowrap" onclick="openChatForContext('order','${order.id}')">💬 ${t('联系对方')}</button>` : ''}
12512
12487
  ${orderStatusBadges(order)}
12513
12488
  </div>
12514
12489
  </div>
12515
- <div class="detail-row"><span class="detail-label">${t('商品')}</span><span class="detail-value">${product?.title || ''}</span></div>
12516
- <div class="detail-row"><span class="detail-label">${t('金额')}</span><span class="detail-value" style="color:#4f46e5">${order.total_amount} WAZ${usdHint(order.total_amount)}${Number(order.insurance_premium) > 0 ? ` <span style="font-size:10px;color:#6366f1;background:#eef2ff;padding:1px 6px;border-radius:99px;margin-left:4px">🛡 ${t('已购保险')} +${order.insurance_premium}</span>` : ''}</span></div>
12490
+ <div class="detail-row" style="align-items:flex-start"><span class="detail-label" style="flex-shrink:0;margin-right:12px">${t('商品')}</span><span class="detail-value" style="text-align:left;max-width:78%;word-break:break-word;line-height:1.5">${escHtml(product?.title || '')}</span></div>
12491
+ <div class="detail-row"><span class="detail-label">${t('金额')}</span><span class="detail-value" style="color:#4f46e5">${window.orderAmountHtml(order)}${Number(order.insurance_premium) > 0 ? ` <span style="font-size:10px;color:#6366f1;background:#eef2ff;padding:1px 6px;border-radius:99px;margin-left:4px">🛡 ${t('已购保险')} +${order.insurance_premium}</span>` : ''}</span></div>
12517
12492
  <div class="detail-row"><span class="detail-label">${t('下单时间')}</span><span class="detail-value">${fmtTime(order.created_at)}</span></div>
12518
- ${order.shipping_address ? `<div class="detail-row"><span class="detail-label">${t('收货地址')}</span><span class="detail-value">${order.shipping_address}</span></div>` : ''}
12493
+ ${order.shipping_address ? `<div class="detail-row"><span class="detail-label">${t('收货地址')}</span><span class="detail-value">${escHtml(order.shipping_address)}</span></div>` : ''}
12519
12494
  ${order.anonymous_recipient && order.recipient_code ? `<div class="detail-row"><span class="detail-label">🔒 ${t('取件代号')}</span><span class="detail-value" style="font-family:monospace;font-size:14px;font-weight:700;color:#166534;background:#dcfce7;padding:2px 10px;border-radius:6px;letter-spacing:1px">${escHtml(order.recipient_code)}</span></div><div style="font-size:11px;color:#15803d;padding:0 14px 8px">${t('凭此代号到自提点取件 · 卖家和物流不知道你的真实姓名')}</div>` : ''}
12520
12495
  ${order.anonymous_recipient && !order.recipient_code ? `<div class="detail-row"><span class="detail-label">🔒 ${t('匿名订单')}</span><span class="detail-value" style="font-size:11px;color:#6b7280">${t('买家选择了匿名收货')}</span></div>` : ''}
12521
12496
  ${Number(order.donation_amount) > 0 ? `<div class="detail-row"><span class="detail-label">❤️ ${t('随单捐赠')}</span><span class="detail-value" style="color:#dc2626;font-weight:600">${order.donation_amount} WAZ</span></div>` : ''}
@@ -12525,6 +12500,7 @@ async function renderOrderDetail(app, orderId) {
12525
12500
  ${order.content_hash_at_order ? `<div class="detail-row"><span class="detail-label">🔒 ${t('P2P 内容哈希')}</span><span class="detail-value" style="font-family:monospace;font-size:11px;word-break:break-all">${escHtml(order.content_hash_at_order)}</span></div>` : ''}
12526
12501
  ${activeDeadline?.deadline ? `<div class="detail-row"><span class="detail-label">${t('截止')}</span><span class="detail-value" style="color:${isOverdue ? '#dc2626' : '#6b7280'};font-size:12px">${fmtTime(activeDeadline.deadline)}</span></div>` : ''}
12527
12502
  </div>
12503
+ ${order.payment_rail === 'direct_p2p' && isBuyer && window.dpOrderDisclosureHtml ? window.dpOrderDisclosureHtml(order) : ''}${window.dpReconcileCard ? window.dpReconcileCard(order, isSeller) : ''}${order.payment_rail === 'direct_p2p' && window.dpNegotiationCard ? window.dpNegotiationCard(order) : ''}${window.dpCancelRefundCard ? window.dpCancelRefundCard(order, isBuyer, isSeller) : ''}${window.dpPendingAcceptCard ? window.dpPendingAcceptCard(order, isBuyer, isSeller) : ''}${window.mutualCancelCard ? window.mutualCancelCard(order, isBuyer, isSeller) : ''}
12528
12504
 
12529
12505
  ${trackingHtml}
12530
12506
  ${disputeHtml}
@@ -12591,7 +12567,7 @@ async function renderOrderDetail(app, orderId) {
12591
12567
  <div class="action-title">${t('完整状态历史')}</div>
12592
12568
  <div class="timeline">${historyHtml || `<div style="color:#6b7280;font-size:13px">${t('暂无记录')}</div>`}</div>
12593
12569
  </div>
12594
- `, 'orders')
12570
+ `, 'orders'); if (order.payment_rail === 'direct_p2p' && isBuyer && window.dpHydrateOrderDisclosure) window.dpHydrateOrderDisclosure(order.id) // direct_p2p 买家:最终 DOM 已就位,#dp-order-instr 必命中(both-acked→收款说明快照;否则 D1/D2 门)。卡片与 hydrate 均 isBuyer 门(端点本就 NOT_ORDER_BUYER,非买家渲染只会得红色报错噪音)。退货/评价 widget 注入各自子容器,不动此框
12595
12571
 
12596
12572
  // Wave B-3: 退货 widget — 异步加载(仅 completed 订单可退)
12597
12573
  // 买家:有退货窗口可申请/查看;卖家:有退货申请时内联查看+处理(accept/reject/received),无申请则隐藏卡
@@ -13062,14 +13038,14 @@ async function renderReturnWidgetForOrder(order, product) {
13062
13038
  const events = detail?.timeline || []
13063
13039
  const item = detail?.item || mine
13064
13040
  const isBuyer = state.user && state.user.id === item.buyer_id
13065
- const STATUS_COLORS = { pending:'#d97706', accepted:'#16a34a', refunded:'#16a34a', rejected:'#dc2626', cancelled:'#6b7280', escalated:'#6b21a8' }
13066
- const STATUS_LABEL = () => ({ pending: t('待卖家处理'), accepted: t('已同意'), refunded: t('已退款'), rejected: t('已拒绝'), cancelled: t('已取消'), escalated: t('已升级仲裁') })
13041
+ const STATUS_COLORS = { pending:'#d97706', accepted:'#16a34a', refunded:'#16a34a', rejected:'#dc2626', cancelled:'#6b7280', escalated:'#6b21a8', ...(window.dpReturnStatusColors ? window.dpReturnStatusColors() : {}) }
13042
+ const STATUS_LABEL = () => ({ pending: t('待卖家处理'), accepted: t('已同意'), refunded: t('已退款'), rejected: t('已拒绝'), cancelled: t('已取消'), escalated: t('已升级仲裁'), ...(window.dpReturnStatusLabels ? window.dpReturnStatusLabels() : {}) })
13067
13043
 
13068
13044
  const isActive = !['refunded', 'cancelled', 'escalated'].includes(item.status)
13069
13045
  const canEscalate = isBuyer && (
13070
13046
  item.status === 'rejected' ||
13071
- (item.status === 'pending' && (Date.now() - new Date(item.created_at).getTime()) >= 7 * 86400 * 1000)
13072
- )
13047
+ (item.status === 'pending' && (Date.now() - new Date(item.created_at).getTime()) >= 7 * 86400 * 1000) ||
13048
+ (window.dpReturnCanEscalate ? window.dpReturnCanEscalate(item, order) : false))
13073
13049
 
13074
13050
  area.innerHTML = `
13075
13051
  <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
@@ -13079,11 +13055,11 @@ async function renderReturnWidgetForOrder(order, product) {
13079
13055
 
13080
13056
  ${isSellerView && item.status === 'pending' ? `
13081
13057
  <div style="display:flex;gap:8px;margin-bottom:8px">
13082
- <button class="btn btn-success btn-sm" style="flex:1;font-size:12px" onclick="decideReturn('${item.id}','accept','${order.id}')">${t('接受退款')}</button>
13058
+ <button class="btn btn-success btn-sm" style="flex:1;font-size:12px" onclick="decideReturn('${item.id}','accept','${order.id}')">${order.payment_rail === 'direct_p2p' ? t('同意退货(场外退款)') : t('接受退款')}</button>
13083
13059
  <button class="btn btn-outline btn-sm" style="flex:1;font-size:12px;color:#dc2626;border-color:#fecaca" onclick="decideReturn('${item.id}','reject','${order.id}')">${t('拒绝退货')}</button>
13084
13060
  </div>` : ''}
13085
13061
  ${isSellerView && item.status === 'picked_up' ? `
13086
- <button class="btn btn-success btn-sm" style="width:100%;font-size:12px;margin-bottom:8px" onclick="confirmReturnReceived('${item.id}','${order.id}')">${t('✓ 已收到退货 · 触发退款')}</button>` : ''}
13062
+ <button class="btn btn-success btn-sm" style="width:100%;font-size:12px;margin-bottom:8px" onclick="confirmReturnReceived('${item.id}','${order.id}')">${order.payment_rail === 'direct_p2p' ? '✓ ' + t('已收到退货') : t('✓ 已收到退货 · 触发退款')}</button>` : ''}${window.dpReturnHandshake ? window.dpReturnHandshake(item, isBuyer, isSellerView, order) : ''}
13087
13063
 
13088
13064
  <div style="padding:10px;background:#fafafa;border-radius:8px;margin-bottom:8px">
13089
13065
  <div style="font-size:11px;font-weight:600;color:#6b7280;margin-bottom:8px">🧾 ${t('协商时间线')} · ${events.length} ${t('条')}</div>
@@ -13100,8 +13076,8 @@ async function renderReturnWidgetForOrder(order, product) {
13100
13076
 
13101
13077
  ${canEscalate ? `
13102
13078
  <div style="margin-top:8px;padding:8px 10px;background:#fef2f2;border:1px solid #fecaca;border-radius:6px">
13103
- <div style="font-size:11px;color:#991b1b;margin-bottom:6px">${item.status === 'rejected' ? t('卖家已拒绝 — 如有异议可升级至平台仲裁') : t('卖家 7 天内未回应 — 可升级至平台仲裁')}</div>
13104
- <button class="btn btn-sm" style="background:#6b21a8;color:#fff;padding:5px 14px;font-size:11px" onclick="escalateReturn('${item.id}','${order.id}')">⚖️ ${t('升级至仲裁')}</button>
13079
+ <div style="font-size:11px;color:#991b1b;margin-bottom:6px">${(window.dpReturnCanEscalate && window.dpReturnCanEscalate(item, order)) ? window.dpReturnEscalateHint(item) : item.status === 'rejected' ? t('卖家已拒绝 — 如有异议可升级至平台仲裁') : t('卖家 7 天内未回应 — 可升级至平台仲裁')}</div>
13080
+ <button class="btn btn-sm" style="background:#6b21a8;color:#fff;padding:5px 14px;font-size:11px" onclick="escalateReturn('${item.id}','${order.id}','${order.payment_rail || ''}')">⚖️ ${t('升级至仲裁')}</button>
13105
13081
  </div>
13106
13082
  ` : ''}
13107
13083
  `
@@ -13115,7 +13091,7 @@ async function renderReturnWidgetForOrder(order, product) {
13115
13091
 
13116
13092
  area.innerHTML = `
13117
13093
  <div style="margin-bottom:8px">${t('退货窗口')}:${returnDays} ${t('天')} · ${remainText}</div>
13118
- <button class="btn btn-outline btn-sm" onclick="openReturnRequestModal('${order.id}', ${Number(order.total_amount)})">${t('申请退货')}</button>
13094
+ <button class="btn btn-outline btn-sm" onclick="openReturnRequestModal('${order.id}', ${Number(order.total_amount)}, '${order.payment_rail === 'direct_p2p' ? 'USDC' : 'WAZ'}')">${t('申请退货')}</button>
13119
13095
  `
13120
13096
  }
13121
13097
 
@@ -13135,8 +13111,8 @@ window.sendReturnMessage = async (returnId, orderId) => {
13135
13111
  }
13136
13112
  }
13137
13113
 
13138
- window.escalateReturn = async (returnId, orderId) => {
13139
- if (!confirm(t('确定升级到仲裁?协议会冻结卖家资产并指派仲裁员审核。'))) return
13114
+ window.escalateReturn = async (returnId, orderId, rail) => {
13115
+ if (!confirm(rail === 'direct_p2p' ? t('确定升级到仲裁?直付订单为信誉裁决:仲裁不经手资金,按证据对卖家作信誉处罚。') : t('确定升级到仲裁?协议会冻结卖家资产并指派仲裁员审核。'))) return
13140
13116
  const res = await POST(`/return-requests/${returnId}/escalate`, {})
13141
13117
  if (res.error) return toast$(res.error, 'error')
13142
13118
  toast$(t('已升级仲裁'), 'success')
@@ -13214,10 +13190,10 @@ window.selectVariantOption = (dim, val, basePrice) => {
13214
13190
  const stockTag = Number(match.stock) > 0
13215
13191
  ? `<span style="color:#16a34a">● ${t('有货')}</span>`
13216
13192
  : `<span style="color:#dc2626">${t('缺货')}</span>`
13217
- summary.innerHTML = `✓ ${price} WAZ · ${stockTag}`
13193
+ summary.innerHTML = `✓ ${window.fmtPrice(price)} · ${stockTag}`
13218
13194
  if (buyBtn) {
13219
13195
  buyBtn.disabled = Number(match.stock) <= 0
13220
- buyBtn.innerHTML = `${t('立即下单')} · ${price} WAZ`
13196
+ buyBtn.innerHTML = `${t('立即下单')} · ${window.fmtPrice(price)}`
13221
13197
  }
13222
13198
  }
13223
13199
 
@@ -13287,18 +13263,21 @@ window.toggleOrderAddrEdit = (expand) => {
13287
13263
  }
13288
13264
  }
13289
13265
 
13290
- // Wave G-5: WAZ USD 近似(用 USDC peg)显示
13291
- function wazToUsd(waz) {
13292
- const rate = Number(state._wazUsdcRate || 1)
13293
- if (rate <= 0) return null
13294
- return Number(waz) / rate // 1 USDC ≈ 1 USD
13295
- }
13296
- // 渲染 $X.XX 小字(默认 buyer / 全局,可选 hidePrefix)
13297
- function usdHint(waz, opts = {}) {
13298
- const usd = wazToUsd(waz)
13299
- if (usd == null || !Number.isFinite(usd)) return ''
13300
- const txt = `≈ $${usd.toFixed(2)}`
13301
- return `<span style="font-size:10px;color:#9ca3af;margin-left:4px;font-weight:400">${txt}</span>`
13266
+ // 订单金额显示(RFC-014 USDC 计价):escrowUSDC + 买家本地币参考(fmtPrice,纯展示,无真实托管);
13267
+ // direct_p2p → USDC + 【买家下单时所选卖家收款账户币种】的应付额(dpFxInCurrency,不是本地币——买家只能按卖家
13268
+ // 支持的币种付款)。币种取自账号快照(currency 属非敏感元数据,pre-ack 也可见;仅 instruction/qr 受披露门)
13269
+ // 该币种无汇率 dpFxInCurrency 回落币种码,绝不臆造换算。
13270
+ window.orderAmountHtml = (o) => {
13271
+ const usdc = Number(o && o.total_amount)
13272
+ if (o && o.payment_rail === 'direct_p2p') {
13273
+ let cur = ''
13274
+ try { cur = String(JSON.parse(o.direct_pay_account_snapshot || '{}').currency || '').toUpperCase() } catch {}
13275
+ const base = `${Number.isFinite(usdc) ? (Number.isInteger(usdc) ? usdc : usdc.toFixed(2)) : ''} USDC`
13276
+ if (!cur || cur === 'USDC' || cur === 'USD') return window.fmtPrice(usdc) // USDC 账户:USDC + 买家本地法币参考(fmtPrice)
13277
+ const pay = window.dpFxInCurrency ? window.dpFxInCurrency(usdc, cur) : cur
13278
+ return `${base} <span style="font-size:10px;color:#9ca3af;margin-left:4px;font-weight:400">${t('应付')} ≈ ${pay}</span>`
13279
+ }
13280
+ return window.fmtPrice(usdc)
13302
13281
  }
13303
13282
 
13304
13283
  // Wave C-1: 规格格式化(订单详情 / 列表用)
@@ -13351,7 +13330,7 @@ function RETURN_REASON_LABEL() {
13351
13330
  }
13352
13331
  }
13353
13332
 
13354
- window.openReturnRequestModal = (orderId, total) => {
13333
+ window.openReturnRequestModal = (orderId, total, cur = 'WAZ') => {
13355
13334
  const labels = RETURN_REASON_LABEL()
13356
13335
  const html = `
13357
13336
  <div class="js-modal" style="background:rgba(0,0,0,0.6);position:fixed;inset:0;z-index:1000;display:flex;align-items:flex-end;justify-content:center" onclick="this.remove()">
@@ -13368,9 +13347,9 @@ window.openReturnRequestModal = (orderId, total) => {
13368
13347
  <textarea class="form-control" id="ret-text" rows="3" maxlength="500" placeholder="${t('描述问题(可选,便于卖家判断)')}"></textarea>
13369
13348
  </div>
13370
13349
  <div class="form-group">
13371
- <label class="form-label">${t('退款金额')} (WAZ) *</label>
13350
+ <label class="form-label">${t('退款金额')} (${cur}) *</label>
13372
13351
  <input class="form-control" id="ret-amount" type="number" min="0.01" max="${total}" step="0.01" value="${total}">
13373
- <div style="font-size:11px;color:#9ca3af;margin-top:2px">${t('订单总额')}: ${total} WAZ · ${t('可申请部分退款')}</div>
13352
+ <div style="font-size:11px;color:#9ca3af;margin-top:2px">${t('订单总额')}: ${total} ${cur} · ${t('可申请部分退款')}${cur === 'USDC' ? ' · ' + t('直付退款在协议外完成,金额为参考') : ''}</div>
13374
13353
  </div>
13375
13354
  <div class="form-group" style="background:#f0fdf4;border:1px solid #86efac;border-radius:8px;padding:10px 12px">
13376
13355
  <label style="display:flex;align-items:flex-start;gap:8px;font-size:12px;color:#166534;cursor:pointer">
@@ -13422,37 +13401,27 @@ function getActions(order, isBuyer, isSeller, isLogistic) {
13422
13401
  const s = order.status
13423
13402
  const isInPerson = order.fulfillment_mode === 'in_person'
13424
13403
  const isSelfFulfillSeller = isSeller && !order.logistics_id
13404
+ // 直付(direct_p2p):买家在 direct_pay_window 标记"我已付款"(Passkey 门) 或取消(不门控);其余阶段沿用通用规则
13405
+ if (order.payment_rail === 'direct_p2p' && isBuyer && s === 'direct_pay_window')
13406
+ return [{ action: 'mark_paid', label: '✅ 我已付款', style: 'success' }, { action: 'cancel', label: '取消订单', style: 'secondary' }]
13407
+ if (order.payment_rail === 'direct_p2p' && window.dpNegotiationActions) { const _na = window.dpNegotiationActions(order, isBuyer, isSeller); if (_na) return _na } // 货款协商(payment_query/disputed)动作
13425
13408
  // M8 面交订单:买家在 paid / accepted 都可"面交完成"直接结算
13426
- if (isInPerson && isBuyer && (s === 'paid' || s === 'accepted')) {
13409
+ if (isInPerson && isBuyer && (s === 'paid' || s === 'accepted'))
13427
13410
  return [{ action: 'confirm_in_person', label: '🤝 面交完成 / 确认收货', style: 'success' }]
13428
- }
13429
13411
  if (isSeller && s === 'paid')
13430
- return [
13431
- { action: 'accept', label: '接单', style: 'success' },
13432
- { action: 'decline', label: '拒绝接单', style: 'danger', custom: 'decline' },
13433
- ]
13412
+ return [{ action: 'accept', label: '接单', style: 'success' }, { action: 'decline', label: '拒绝接单', style: 'danger', custom: 'decline' }]
13434
13413
  if (isSeller && s === 'accepted' && !isInPerson)
13435
- return [{ action: 'ship', label: '确认发货', style: 'success', logisticsSelector: true,
13436
- trackingInput: true,
13437
- evidencePlaceholder: '包装状态描述 / 货物说明(可选)' }]
13414
+ return [{ action: 'ship', label: '确认发货', style: 'success', logisticsSelector: true, trackingInput: true, evidencePlaceholder: '包装状态描述 / 货物说明(可选)' }, ...(order.payment_rail === 'direct_p2p' ? [{ action: 'report_nonpayment', label: '未收到货款(告知买家核实)', style: 'danger' }] : [])]
13438
13415
  if (isSeller && s === 'accepted' && isInPerson)
13439
13416
  return [{ action: 'noop_in_person', label: '🤝 面交中(等待买家确认)', style: 'secondary', disabled: true }]
13440
13417
  if ((isLogistic || isSelfFulfillSeller) && s === 'shipped')
13441
- return [{ action: 'pickup', label: '✅ 确认揽收', style: 'success', needsEvidence: true,
13442
- noteLabel: '快递单号 *', evidencePlaceholder: '如:SF1234567890',
13443
- helperText: isSelfFulfillSeller ? '自履约订单:你负责回传揽收/单号,超时仍按卖家责任处理。' : '' }]
13418
+ return [{ action: 'pickup', label: '✅ 确认揽收', style: 'success', needsEvidence: true, noteLabel: '快递单号 *', evidencePlaceholder: '如:SF1234567890', helperText: isSelfFulfillSeller ? '自履约订单:你负责回传揽收/单号,超时仍按卖家责任处理。' : '' }]
13444
13419
  if ((isLogistic || isSelfFulfillSeller) && s === 'picked_up')
13445
13420
  return [{ action: 'transit', label: '🚛 开始运输', style: 'primary' }]
13446
13421
  if ((isLogistic || isSelfFulfillSeller) && s === 'in_transit')
13447
- return [{ action: 'deliver', label: '📬 确认投递', style: 'success', needsEvidence: true,
13448
- noteLabel: '投递凭证', evidencePlaceholder: '门牌照片描述 / 收件人姓名 / 签收时间',
13449
- helperText: isSelfFulfillSeller ? '自履约投递需留存签收/门牌/交付说明,买家确认后才结算。' : '' }]
13422
+ return [{ action: 'deliver', label: '📬 确认投递', style: 'success', needsEvidence: true, noteLabel: '投递凭证', evidencePlaceholder: '门牌照片描述 / 收件人姓名 / 签收时间', helperText: isSelfFulfillSeller ? '自履约投递需留存签收/门牌/交付说明,买家确认后才结算。' : '' }]
13450
13423
  if (isBuyer && s === 'delivered')
13451
- return [
13452
- { action: 'confirm', label: '确认收货', style: 'success' },
13453
- { action: 'dispute', label: '发起争议', style: 'danger', needsEvidence: true,
13454
- noteLabel: '争议理由', evidencePlaceholder: '描述问题(货不对版/货损/未收到等)' },
13455
- ]
13424
+ return [{ action: 'confirm', label: '确认收货', style: 'success' }, { action: 'dispute', label: '未收到货 / 有问题?', style: 'danger', needsEvidence: true, noteLabel: '争议理由', evidencePlaceholder: '描述问题(未收到 / 货不对版 / 货损等)', helperText: '未收到货?可先联系卖家核实(可能晚到或放了代收点)。确认异常再提交 —— 发起争议后自动确认将暂停,不会因超时被视为已收货。' }]
13456
13425
  return null
13457
13426
  }
13458
13427
 
@@ -13507,13 +13476,15 @@ function renderActions(orderId, actions, order, logisticsCompanies = []) {
13507
13476
 
13508
13477
  window.handleAction = async (orderId, action, idx, needsEvidence, hasLogisticsSelector) => {
13509
13478
  const msgEl = document.getElementById('action-msg')
13479
+ // 直付:买家 mark_paid/confirm/confirm_in_person 是 RISK 动作 → 走两次披露 + Passkey 门(后端硬强制)
13480
+ if (window._dpOrderRail === 'direct_p2p' && ['mark_paid', 'confirm', 'confirm_in_person'].includes(action)) return void (window.dpHandleAction && window.dpHandleAction(orderId, action))
13510
13481
 
13511
13482
  // M8 面交确认:走专用端点,跳过物流状态机
13512
13483
  if (action === 'confirm_in_person') {
13513
13484
  if (!confirm(t('确认已收到物品?escrow 将立即释放给卖家,无法撤销。'))) return
13514
13485
  msgEl.innerHTML = `<div class="alert alert-info"><span class="spinner"></span>${t('确认中...')}</div>`
13515
13486
  const res = await POST(`/orders/${orderId}/confirm-in-person`, {})
13516
- if (res.error) { msgEl.innerHTML = alert$('error', res.error); return }
13487
+ if (res.error) { msgEl.innerHTML = alert$('error', window.orderErrorText ? window.orderErrorText(res.error_code, res.error) : res.error); return }
13517
13488
  msgEl.innerHTML = alert$('success', t('面交完成 — escrow 已释放'))
13518
13489
  maybeThankSponsorAndClear()
13519
13490
  setTimeout(() => renderOrderDetail(document.getElementById('app'), orderId), 1000)
@@ -13521,7 +13492,7 @@ window.handleAction = async (orderId, action, idx, needsEvidence, hasLogisticsSe
13521
13492
  }
13522
13493
  if (action === 'noop_in_person') return
13523
13494
  const confirmText = {
13524
- ship: t('确认已经发货?发货后买家将看到物流信息,超时/虚假发货可能进入争议或判责。'),
13495
+ ship: (window._dpOrderRail === 'direct_p2p' && window.dpPayRef) ? `${t('直付订单:发货前请核实货款已到账 —— 银行流水附言应为')} ${window.dpPayRef(orderId)}${t('(同买家同金额多单务必逐单核对参考号)。发货即视为你确认已收到货款;发货后不可再报告未收款,只能走争议。')}` : t('确认已经发货?发货后买家将看到物流信息,超时/虚假发货可能进入争议或判责。'),
13525
13496
  pickup: t('确认已揽收并回传凭证?请确保单号或揽收说明真实可追踪。'),
13526
13497
  deliver: t('确认已投递?请确保投递凭证真实,买家仍需确认收货后才结算。'),
13527
13498
  confirm: t('确认收货?escrow 将进入结算流程,无法撤销。'),
@@ -13570,7 +13541,7 @@ window.handleAction = async (orderId, action, idx, needsEvidence, hasLogisticsSe
13570
13541
 
13571
13542
  const res = await POST(`/orders/${orderId}/action`, body)
13572
13543
  if (res.error) {
13573
- msgEl.innerHTML = alert$('error', res.error)
13544
+ msgEl.innerHTML = alert$('error', window.orderErrorText ? window.orderErrorText(res.error_code, res.error) : res.error)
13574
13545
  } else {
13575
13546
  msgEl.innerHTML = alert$('success', t('操作成功!'))
13576
13547
  // S8: confirm action 触发 completed → 检查是否首单,若是则致谢 + 清 ctx
@@ -13757,13 +13728,13 @@ function evidenceRequestCard(req, currentUserId) {
13757
13728
  // evidence_types 存为 JSON 数组字符串
13758
13729
  let types = []
13759
13730
  try { types = typeof req.evidence_types === 'string' ? JSON.parse(req.evidence_types) : (req.evidence_types || []) } catch(e) {}
13760
- const typeLabels = types.map(t => `${EVIDENCE_TYPE_ICONS[t] || ''}${EVIDENCE_TYPE_LABELS[t] || t}`).join(' ')
13731
+ const typeLabels = types.map(et => `${EVIDENCE_TYPE_ICONS[et] || ''}${t(EVIDENCE_TYPE_LABELS[et] || et)}`).join(' ')
13761
13732
 
13762
13733
  const submittedHtml = (req.submitted_items || []).length > 0 ? `
13763
13734
  <div style="margin-top:8px">
13764
13735
  ${req.submitted_items.map(it => `
13765
13736
  <div style="background:#f0fdf4;border-radius:6px;padding:6px 8px;margin-top:4px;font-size:12px">
13766
- <span style="color:#6b7280">${EVIDENCE_TYPE_ICONS[it.type] || ''} ${EVIDENCE_TYPE_LABELS[it.type] || escHtml(it.type)}</span>
13737
+ <span style="color:#6b7280">${EVIDENCE_TYPE_ICONS[it.type] || ''} ${EVIDENCE_TYPE_LABELS[it.type] ? t(EVIDENCE_TYPE_LABELS[it.type]) : escHtml(it.type)}</span>
13767
13738
  <div style="margin-top:2px">${escHtml(it.description)}</div>
13768
13739
  ${it.file_hash ? `<div style="margin-top:3px;font-family:monospace;font-size:10px;color:#9ca3af" title="Phase 0 模拟锚点,Phase 2 替换为 IPFS CID / 链上 TX">🔒 ${escHtml(it.file_hash)}</div>` : ''}
13769
13740
  </div>`).join('')}
@@ -13771,26 +13742,26 @@ function evidenceRequestCard(req, currentUserId) {
13771
13742
 
13772
13743
  const submitForm = isMe && req.status === 'pending' ? `
13773
13744
  <div id="er-form-${req.id}" style="margin-top:10px;background:#fffbeb;border-radius:8px;padding:10px">
13774
- <div style="font-size:12px;font-weight:600;color:#92400e;margin-bottom:6px">提交所需证据</div>
13745
+ <div style="font-size:12px;font-weight:600;color:#92400e;margin-bottom:6px">${t('提交所需证据')}</div>
13775
13746
  <div id="er-msg-${req.id}"></div>
13776
13747
  <select class="form-control" id="er-type-${req.id}" style="margin-bottom:6px;font-size:13px">
13777
- <option value="">— 选择证据类型 —</option>
13778
- ${types.map(t => `<option value="${t}">${EVIDENCE_TYPE_ICONS[t]} ${EVIDENCE_TYPE_LABELS[t] || t}</option>`).join('')}
13748
+ <option value="">${t('— 选择证据类型 —')}</option>
13749
+ ${types.map(et => `<option value="${et}">${EVIDENCE_TYPE_ICONS[et]} ${t(EVIDENCE_TYPE_LABELS[et] || et)}</option>`).join('')}
13779
13750
  </select>
13780
13751
  <textarea class="form-control" id="er-desc-${req.id}" placeholder="${t('详细描述内容(如图片描述、文字陈述、链上 TX hash 等)')}" style="margin-bottom:6px;font-size:13px"></textarea>
13781
- <input class="form-control" id="er-hash-${req.id}" placeholder="(可选)文件哈希 / IPFS CID / 链上 TX ID" style="margin-bottom:6px;font-size:12px;font-family:monospace">
13782
- <button class="btn btn-primary btn-sm" style="width:auto" onclick="handleSubmitEvidence('${req.id}','${req.dispute_id}')">提交证据</button>
13752
+ <input class="form-control" id="er-hash-${req.id}" placeholder="${t('(可选)文件哈希 / IPFS CID / 链上 TX ID')}" style="margin-bottom:6px;font-size:12px;font-family:monospace">
13753
+ <button class="btn btn-primary btn-sm" style="width:auto" onclick="handleSubmitEvidence('${req.id}','${req.dispute_id}')">${t('提交证据')}</button>
13783
13754
  </div>` : ''
13784
13755
 
13785
13756
  return `
13786
13757
  <div style="border:1px solid ${isMe && req.status === 'pending' ? '#f59e0b' : '#e5e7eb'};border-radius:8px;padding:10px;margin-top:8px;background:${isMe && req.status === 'pending' ? '#fffdf0' : '#fff'}">
13787
13758
  <div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:4px">
13788
- <div style="font-size:13px;font-weight:600">${isMe ? '👤 需要你提供' : `请求 → ${req.requested_from_name || '对方'}(${req.requested_from_role || ''})`}</div>
13759
+ <div style="font-size:13px;font-weight:600">${isMe ? t('👤 需要你提供') : `${t('请求')} → ${req.requested_from_name || t('对方')}(${req.requested_from_role || ''})`}</div>
13789
13760
  ${erStatusBadge(req.status)}
13790
13761
  </div>
13791
- <div style="font-size:12px;color:#6b7280;margin-bottom:4px">类型:${typeLabels}</div>
13762
+ <div style="font-size:12px;color:#6b7280;margin-bottom:4px">${t('类型:')}${typeLabels}</div>
13792
13763
  ${req.description ? `<div style="font-size:13px;margin-bottom:4px">${req.description}</div>` : ''}
13793
- <div style="font-size:11px;color:#9ca3af">截止:${fmtTime(req.deadline)}</div>
13764
+ <div style="font-size:11px;color:#9ca3af">${t('截止:')}${fmtTime(req.deadline)}</div>
13794
13765
  ${submittedHtml}
13795
13766
  ${submitForm}
13796
13767
  </div>`
@@ -13811,7 +13782,7 @@ const TL_ROLE_META = {
13811
13782
  }
13812
13783
  const TL_TYPE_META = {
13813
13784
  open: { icon: '🚩', title: '发起争议', border: '#dc2626' },
13814
- evidence: { icon: '📎', title: '提交证据', border: '#3b82f6' },
13785
+ evidence: { icon: '📎', title: '证据提交', border: '#3b82f6' },
13815
13786
  response: { icon: '📝', title: '被告反驳', border: '#f59e0b' },
13816
13787
  evidence_request: { icon: '📋', title: '仲裁员索证', border: '#a855f7' },
13817
13788
  ruling: { icon: '🏛️', title: '仲裁裁决', border: '#6b21a8' },
@@ -13838,8 +13809,8 @@ function buildTimelineEvent(ev, dispute, user, actors) {
13838
13809
  const isMe = user && actor && actor.id === user.id
13839
13810
  // 当无 actor(如 system resolved 事件),不重复显示与 role chip 相同的名字
13840
13811
  const actorLabel = actor
13841
- ? (actor.handle ? '@' + escHtml(actor.handle) : escHtml(actor.name))
13842
- : ''
13812
+ ? (actor.name ? escHtml(actor.name) : (actor.handle ? '@' + escHtml(actor.handle) : ''))
13813
+ : '' // 走查批次3:昵称优先(自动生成的 @handle 可读性差;上方 party chips 本就用昵称,对齐)
13843
13814
 
13844
13815
  // 内容主体 — 按 type 分支
13845
13816
  let bodyHtml = ''
@@ -13849,7 +13820,7 @@ function buildTimelineEvent(ev, dispute, user, actors) {
13849
13820
  } else if (ev.type === 'evidence') {
13850
13821
  const meta = ev.meta || {}
13851
13822
  const typeIcon = EVIDENCE_TYPE_ICONS[meta.evidence_type] || '📎'
13852
- const typeLabel = EVIDENCE_TYPE_LABELS[meta.evidence_type] || meta.evidence_type
13823
+ const typeLabel = t(EVIDENCE_TYPE_LABELS[meta.evidence_type] || meta.evidence_type)
13853
13824
  const sizeKb = meta.size ? `${(Number(meta.size)/1024).toFixed(1)} KB` : ''
13854
13825
  const blobChip = meta.has_blob ? `<a href="/api/evidence/${escHtml(meta.evidence_id)}/blob" target="_blank" style="display:inline-block;background:#eff6ff;color:#1d4ed8;padding:2px 8px;border-radius:99px;font-size:11px;text-decoration:none;margin-right:4px">📥 ${t('打开附件')}${sizeKb ? ' · '+sizeKb : ''}</a>` : ''
13855
13826
  const hashChip = meta.file_hash ? `<span style="font-family:monospace;font-size:10px;color:#9ca3af;background:#f3f4f6;padding:1px 6px;border-radius:4px" title="${escHtml(meta.file_hash)}">🔒 ${escHtml(String(meta.file_hash).slice(0,12))}…</span>` : ''
@@ -13886,14 +13857,14 @@ function buildTimelineEvent(ev, dispute, user, actors) {
13886
13857
  } else if (ev.type === 'ruling' || ev.type === 'resolved') {
13887
13858
  const meta = ev.meta || {}
13888
13859
  const rulingLabel = meta.ruling || meta.ruling_type
13889
- const rulingText = rulingLabel ? (RULING_LABELS[rulingLabel] || rulingLabel) : ''
13860
+ const rulingText = rulingLabel ? ((dispute.payment_rail === 'direct_p2p' && window.dpRulingLabel && window.dpRulingLabel(rulingLabel)) || t(RULING_LABELS[rulingLabel] || rulingLabel)) : ''
13890
13861
  let liability = []
13891
13862
  try { liability = typeof meta.liability_parties === 'string' ? JSON.parse(meta.liability_parties) : (meta.liability_parties || []) } catch {}
13892
13863
  bodyHtml = `
13893
13864
  ${rulingText ? `<div style="font-weight:700;font-size:14px;color:${typeMeta.border};margin-bottom:4px">${t(rulingText)}</div>` : ''}
13894
13865
  ${ev.body ? `<div style="font-size:13px;line-height:1.5;white-space:pre-wrap;color:#374151">${escHtml(ev.body)}</div>` : ''}
13895
- ${meta.refund_amount != null ? `<div style="font-size:12px;margin-top:6px">💸 ${t('退款金额')}:<strong>${meta.refund_amount} WAZ</strong></div>` : ''}
13896
- ${liability.length > 0 ? `<div style="margin-top:6px">
13866
+ ${dispute.payment_rail !== 'direct_p2p' && meta.refund_amount != null ? `<div style="font-size:12px;margin-top:6px">💸 ${t('退款金额')}:<strong>${meta.refund_amount} WAZ</strong></div>` : ''}
13867
+ ${dispute.payment_rail !== 'direct_p2p' && liability.length > 0 ? `<div style="margin-top:6px">
13897
13868
  <div style="font-size:11px;color:#6b7280;margin-bottom:2px">${t('责任分配')}:</div>
13898
13869
  ${liability.map(lp => `<div style="font-size:12px;margin-top:2px">${(TL_ROLE_META[lp.role]||TL_ROLE_META.unknown).icon} ${t(lp.role)} ${t('承担')} ${lp.amount} WAZ${lp.insurance_cap!=null?` (${t('保险上限')} ${lp.insurance_cap})`:''}</div>`).join('')}
13899
13870
  </div>` : ''}`
@@ -13938,7 +13909,7 @@ function buildDisputeTimelineSection(dispute, user) {
13938
13909
 
13939
13910
  function buildDisputeHtml(dispute, user) {
13940
13911
  const isDefendant = user && user.id === dispute.defendant_id
13941
- const isArbitrator = user && user.role === 'arbitrator'
13912
+ const isArbitrator = !!state.canArbitrate // PR-E:跟随后端 can_arbitrate(active whitelist),非 user.role;后端 COI/assign 才是边界
13942
13913
  // is_party is set by the server; fallback to local check
13943
13914
  const isParty = dispute.is_party || (user && dispute.parties && dispute.parties.some(p => p.id === user.id))
13944
13915
  const parties = dispute.parties || []
@@ -14082,20 +14053,13 @@ function buildDisputeHtml(dispute, user) {
14082
14053
  const arbitrateSection = isArbitrator && (dispute.status === 'open' || dispute.status === 'in_review') ? `
14083
14054
  <div style="margin-top:12px;border-top:1px solid #fecaca;padding-top:12px">
14084
14055
  <div style="font-weight:600;font-size:13px;margin-bottom:8px">⚖️ 仲裁员裁定(截止 ${fmtTime(dispute.arbitrate_deadline)})</div>
14085
- <div style="font-size:12px;color:#92400e;background:#fffbeb;border:1px solid #fcd34d;border-radius:6px;padding:6px 10px;margin-bottom:8px">
14086
- 💰 败诉方须缴纳仲裁费:订单金额 × 1%(最低 1 WAZ)。部分退款时双方各付 0.5%。仲裁费 50% 归仲裁员,50% 归协议。
14087
- </div>
14088
- <div id="arbitrate-msg"></div>
14056
+ ${window.dpArbFeeNote ? window.dpArbFeeNote(dispute.payment_rail) : ''}
14057
+ <input type="hidden" id="arb-rail" value="${dispute.payment_rail || ''}"><div id="arbitrate-msg"></div>
14089
14058
 
14090
14059
  <div class="form-group" style="margin-bottom:10px">
14091
14060
  <label style="font-size:12px;color:#6b7280;display:block;margin-bottom:4px">裁定方式</label>
14092
14061
  <div style="display:flex;flex-direction:column;gap:6px">
14093
- ${[
14094
- ['refund_buyer', '🔵 全额退款买家(买家胜诉,卖家承担)'],
14095
- ['release_seller', '🟢 资金释放给卖家(卖家胜诉)'],
14096
- ['partial_refund', '🟡 部分退款(折中,需填金额)'],
14097
- ['liability_split', '⚖️ 责任分配(指定各方赔付额)'],
14098
- ].map(([val, label]) => `
14062
+ ${(window.dpArbRulingOptions ? window.dpArbRulingOptions(dispute.payment_rail, dispute.can_dismiss_to_negotiation) : []).map(([val, label]) => `
14099
14063
  <label style="display:flex;align-items:center;gap:8px;font-size:13px;cursor:pointer;padding:8px;background:#f9fafb;border-radius:6px">
14100
14064
  <input type="radio" name="arb-ruling-radio" value="${val}" onclick="onArbRulingChange('${val}')"> ${label}
14101
14065
  </label>`).join('')}
@@ -14344,14 +14308,15 @@ window.handleArbitrate = async (disputeId) => {
14344
14308
  if (!reason) { msgEl.innerHTML = alert$('error', t('请填写裁定理由')); return }
14345
14309
 
14346
14310
  let body = { ruling, reason }
14311
+ const nc = document.getElementById('arb-rail')?.value === 'direct_p2p' // 直付:仅信誉裁决 → 不读/不发任何退款/赔付金额
14347
14312
 
14348
- if (ruling === 'partial_refund') {
14313
+ if (!nc && ruling === 'partial_refund') {
14349
14314
  const amount = document.getElementById('arb-amount')?.value
14350
14315
  if (!amount) { msgEl.innerHTML = alert$('error', t('部分退款需填写退款金额')); return }
14351
14316
  body = { ...body, refund_amount: Number(amount) }
14352
14317
  }
14353
14318
 
14354
- if (ruling === 'liability_split') {
14319
+ if (!nc && ruling === 'liability_split') {
14355
14320
  const liabilityParties = []
14356
14321
  let totalCalc = 0
14357
14322
  for (const chk of document.querySelectorAll('.arb-liable-chk:checked')) {
@@ -14377,8 +14342,8 @@ window.handleArbitrate = async (disputeId) => {
14377
14342
  body = { ...body, liability_parties: liabilityParties, refund_amount: totalCalc }
14378
14343
  }
14379
14344
 
14380
- msgEl.innerHTML = `<div class="alert alert-info"><span class="spinner"></span>${t('裁定中...')}</div>`
14381
- const res = await POST(`/disputes/${disputeId}/arbitrate`, body)
14345
+ let _arbTk; try { _arbTk = await requestPasskeyGate('arbitrate', { dispute_id: disputeId }) } catch (e) { msgEl.innerHTML = alert$('error', (e && e.message ? e.message + ' — ' : '') + t('仲裁裁定需现场真人 Passkey 验证')); return }; msgEl.innerHTML = `<div class="alert alert-info"><span class="spinner"></span>${t('裁定中...')}</div>` // 所有 ruling(含驳回)先取 arbitrate gate token
14346
+ const res = await POST(`/disputes/${disputeId}/arbitrate`, { ...body, webauthn_token: _arbTk })
14382
14347
  if (res.error) { msgEl.innerHTML = alert$('error', res.error); return }
14383
14348
 
14384
14349
  // 显示仲裁费明细
@@ -14391,10 +14356,11 @@ window.handleArbitrate = async (disputeId) => {
14391
14356
 
14392
14357
  // 裁定方式切换 → 控制相关输入框显隐
14393
14358
  window.onArbRulingChange = (ruling) => {
14359
+ const nc = document.getElementById('arb-rail')?.value === 'direct_p2p' // 直付:无金额/无赔付 → 两个金额区块永不出现
14394
14360
  const partialRow = document.getElementById('arb-partial-row')
14395
14361
  const liabilityBlock = document.getElementById('arb-liability-block')
14396
- if (partialRow) partialRow.style.display = ruling === 'partial_refund' ? '' : 'none'
14397
- if (liabilityBlock) liabilityBlock.style.display = ruling === 'liability_split' ? '' : 'none'
14362
+ if (partialRow) partialRow.style.display = (!nc && ruling === 'partial_refund') ? '' : 'none'
14363
+ if (liabilityBlock) liabilityBlock.style.display = (!nc && ruling === 'liability_split') ? '' : 'none'
14398
14364
  }
14399
14365
 
14400
14366
  // 责任分配:勾选责任方时启用金额输入框,并实时计算合计
@@ -14577,7 +14543,7 @@ async function hydrateSimilarCases(disputeId) {
14577
14543
 
14578
14544
  async function renderDisputeList(app) {
14579
14545
  if (!state.user) { renderLogin(); return }
14580
- if (state.user.role !== 'arbitrator') {
14546
+ if (!state.canArbitrate) { // PR-E:跟随后端 can_arbitrate(active whitelist),whitelist-only(role=buyer)真人仲裁员也能进
14581
14547
  app.innerHTML = shell(`
14582
14548
  <h1 class="page-title">${t('争议仲裁台')}</h1>
14583
14549
  <div class="alert alert-info">${t('此功能仅限仲裁员使用。')}<br>${t('你的角色')}:${state.user.role}</div>
@@ -14767,7 +14733,7 @@ async function renderLogistics(app) {
14767
14733
  <div style="flex:1;min-width:0">
14768
14734
  <div style="font-size:13px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${escHtml(o.product_title)}${windowChip}</div>
14769
14735
  ${o.shipping_address ? `<div style="font-size:11px;color:#6b7280;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:2px">📍 ${escHtml(o.shipping_address)}</div>` : ''}
14770
- <div style="font-size:10px;color:${urgent ? '#dc2626' : '#9ca3af'};margin-top:2px${urgent ? ';font-weight:600' : ''}">${o.total_amount} WAZ · ${o.ship_deadline ? `${urgent ? '⚠ ' : ''}${t('截止')} ${fmtTime(o.ship_deadline)}` : fmtTime(o.created_at)}</div>
14736
+ <div style="font-size:10px;color:${urgent ? '#dc2626' : '#9ca3af'};margin-top:2px${urgent ? ';font-weight:600' : ''}">${window.orderAmountHtml(o)} · ${o.ship_deadline ? `${urgent ? '⚠ ' : ''}${t('截止')} ${fmtTime(o.ship_deadline)}` : fmtTime(o.created_at)}</div>
14771
14737
  </div>
14772
14738
  ${act ? `<div style="font-size:11px;color:#065f46;font-weight:600;flex-shrink:0;white-space:nowrap">${act.short} →</div>` : ''}
14773
14739
  </div>
@@ -14987,7 +14953,7 @@ function renderInsightsBlock(d) {
14987
14953
  <div class="card" style="padding:12px">
14988
14954
  <div style="font-size:10px;color:#9ca3af;text-transform:uppercase">${t('30 天 GMV')}</div>
14989
14955
  <div style="font-size:18px;font-weight:800;color:#15803d;margin-top:2px">${Number(s.gmv||0).toFixed(0)} <span style="font-size:11px;font-weight:600;color:#6b7280">WAZ</span></div>
14990
- <div style="font-size:10px;color:#6b7280;margin-top:2px">${deltaChip(vs.gmv_pct||0, t('环比'))}</div>
14956
+ <div style="font-size:10px;color:#6b7280;margin-top:2px">${deltaChip(vs.gmv_pct||0, t('环比'))}</div>${window.gmvRailSplitHtml ? window.gmvRailSplitHtml(s.gmv_escrow, s.gmv_direct_pay) : ''}
14991
14957
  </div>
14992
14958
  <div class="card" style="padding:12px">
14993
14959
  <div style="font-size:10px;color:#9ca3af;text-transform:uppercase">${t('30 天订单')}</div>
@@ -15310,7 +15276,7 @@ async function renderSeller(app) {
15310
15276
  <div class="order-meta">${fmtTime(o.created_at)}</div>
15311
15277
  <div style="margin-top:6px">${orderStatusBadges(o)}</div>
15312
15278
  </div>
15313
- <div class="order-amount">${o.total_amount} WAZ</div>
15279
+ <div class="order-amount">${window.orderAmountHtml(o)}</div>
15314
15280
  </div>
15315
15281
  </div>`).join('')
15316
15282
  const acceptHtml = paidOrders.length === 0
@@ -15391,7 +15357,7 @@ async function renderSeller(app) {
15391
15357
  <div style="flex:1;min-width:0;cursor:pointer" onclick="navigate('#edit-product/${p.id}')">
15392
15358
  <div style="font-size:13px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${escHtml(p.title)}</div>
15393
15359
  <div style="font-size:11px;color:#6b7280;margin-top:2px;display:flex;gap:8px;flex-wrap:wrap">
15394
- <span><strong style="color:#374151">${p.price}</strong> WAZ</span>
15360
+ <span><strong style="color:#374151">${window.fmtPrice(p.price)}</strong></span>
15395
15361
  <span style="color:${outOfStock?'#dc2626':lowStock?'#d97706':'#6b7280'}${outOfStock||lowStock?';font-weight:600':''}">${t('库存')} ${p.stock}${outOfStock?' ⛔':lowStock?' ⚠':''}</span>
15396
15362
  ${p.completion_count > 0 ? `<span>🛒 ${p.completion_count}</span>` : ''}
15397
15363
  </div>
@@ -15424,7 +15390,7 @@ async function renderSeller(app) {
15424
15390
  <div style="display:flex;justify-content:space-between;align-items:flex-start;gap:8px">
15425
15391
  <div style="flex:1;min-width:0">
15426
15392
  <div style="font-weight:600">${escHtml(p.title)}</div>
15427
- <div style="font-size:13px;color:#6b7280;margin-top:2px">${p.price} WAZ · ${t('库存')} ${p.stock}</div>
15393
+ <div style="font-size:13px;color:#6b7280;margin-top:2px">${window.fmtPrice(p.price)} · ${t('库存')} ${p.stock}</div>
15428
15394
  ${autoDelistedRecently ? `<div style="font-size:11px;color:#92400e;margin-top:2px;padding:4px 8px;background:#fef3c7;border-radius:6px;display:inline-block">📦 ${t('售罄自动下架')} · ${new Date(p.auto_delisted_at).toLocaleDateString()}</div>` : ''}
15429
15395
  ${p.has_pending_task ? `<div style="font-size:11px;color:#d97706;margin-top:2px">⏳ ${t('链接核验中,请等待验证结果')}</div>` : ''}
15430
15396
  ${p.all_links_revoked ? `<div style="font-size:11px;color:#ef4444;margin-top:2px">❌ ${t('所有链接已失效,请先添加新链接')}</div>` : ''}
@@ -15445,7 +15411,7 @@ async function renderSeller(app) {
15445
15411
  <div style="display:flex;justify-content:space-between;align-items:flex-start;gap:8px">
15446
15412
  <div style="flex:1;min-width:0">
15447
15413
  <div style="font-weight:600;text-decoration:line-through;color:#9ca3af">${escHtml(p.title)}</div>
15448
- <div style="font-size:13px;color:#d1d5db;margin-top:2px">${p.price} WAZ</div>
15414
+ <div style="font-size:13px;color:#d1d5db;margin-top:2px">${window.fmtPrice(p.price)}</div>
15449
15415
  </div>
15450
15416
  <div style="display:flex;gap:6px;flex-shrink:0;flex-wrap:wrap;justify-content:flex-end">
15451
15417
  <span class="badge badge-red">${t('回收箱')}</span>
@@ -15516,8 +15482,10 @@ async function renderSeller(app) {
15516
15482
  ${subTabBtn('products', '📦 ' + t('商品'))}
15517
15483
  ${subTabBtn('marketing', '💎 ' + t('营销'))}
15518
15484
  ${subTabBtn('skills', '⚡ ' + t('Skill'))}
15485
+ ${subTabBtn('settings', '⚙️ ' + t('设置'))}
15519
15486
  </div>
15520
15487
  `
15488
+ const settingsSection = sellerSubTab === 'settings' ? ((window.dpSellerReadinessSection ? window.dpSellerReadinessSection() : '') + (window.dpSellerFeeSection ? window.dpSellerFeeSection() : '') + (window.dpSalesReportSection ? window.dpSalesReportSection() : '') + (window.dpFeeRequestSection ? window.dpFeeRequestSection() : '') + (window.dpSellerDeferralSection ? window.dpSellerDeferralSection() : '') + (window.dpSellerProductVerifySection ? window.dpSellerProductVerifySection() : '') + (window.dpSellerStoreVerifySection ? window.dpSellerStoreVerifySection() : '') + (window.dpSellerInstructionSection ? window.dpSellerInstructionSection() : '') + (window.draAccountsSection ? window.draAccountsSection() : '') + (window.shipSellerSettingsSection ? window.shipSellerSettingsSection() : '') + (window.bondSellerSection ? window.bondSellerSection() : '')) : ''
15521
15489
 
15522
15490
  // 数据中心区块(30 天聚合 — 销售曲线 / Top 商品 / 客户洞察 / 状态分布)
15523
15491
  const insightsBlock = insights ? renderInsightsBlock(insights) : ''
@@ -15569,6 +15537,7 @@ async function renderSeller(app) {
15569
15537
  <div style="font-weight:600;font-size:13px;color:#1e40af;margin-top:6px">${t('普通上架')}</div>
15570
15538
  <div style="font-size:10px;color:#1d4ed8;margin-top:2px">${t('标准商品 · 即买即发')}</div>
15571
15539
  </div>
15540
+ ${window.freeShippingMarketingCard ? window.freeShippingMarketingCard() : ''}
15572
15541
  <div onclick="location.hash='#seller-trials'" class="card" style="padding:14px;cursor:pointer;background:linear-gradient(135deg,#faf5ff,#fdf2f8);border-color:#ddd6fe">
15573
15542
  <div style="font-size:24px">🎁</div>
15574
15543
  <div style="font-weight:600;font-size:13px;color:#6b21a8;margin-top:6px">${t('测评免单活动')}</div>
@@ -15580,8 +15549,7 @@ async function renderSeller(app) {
15580
15549
  const skillsSection = sellerSubTab === 'skills' ? `
15581
15550
  <div style="font-size:12px;color:#6b7280;margin-bottom:10px">${t('Skill 自动化 · 让 Agent 替你接单/报价/发货')}</div>
15582
15551
  ` : ''
15583
- // 注:Skill 真实内容由下方 #my-skills-list 同步渲染(mySkills 已同步就绪);
15584
- // 此前这里有个 #skill-mgmt-content loading$() 占位但全文件无人填充 → spinner 永转,已删除。
15552
+ // 注:Skill 真实内容由下方 #my-skills-list 同步渲染(mySkills 已同步就绪);此前 #skill-mgmt-content loading$() 占位无人填充 → spinner 永转,已删除。
15585
15553
 
15586
15554
  const productsSection = sellerSubTab === 'products' ? `
15587
15555
  <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
@@ -15600,6 +15568,7 @@ async function renderSeller(app) {
15600
15568
  ${marketingSection}
15601
15569
  ${skillsSection}
15602
15570
  ${productsSection}
15571
+ ${settingsSection}
15603
15572
 
15604
15573
  <!-- 商品分类标签页(仅 products sub-tab 下显示)-->
15605
15574
  ${sellerSubTab === 'products' ? `
@@ -15657,7 +15626,7 @@ async function renderSeller(app) {
15657
15626
  <div class="form-group"><label class="form-label">${t('规格参数')}<span style="font-size:11px;color:#9ca3af;font-weight:400;margin-left:6px">${t('每行:材质: 陶瓷')}</span></label>
15658
15627
  <textarea class="form-control" id="imp-specs" rows="3"></textarea></div>
15659
15628
  <div style="display:flex;gap:12px">
15660
- <div class="form-group" style="flex:1"><label class="form-label">${t('价格(WAZ)')}</label><input class="form-control" id="imp-price" type="number"></div>
15629
+ <div class="form-group" style="flex:1"><label class="form-label">${t('价格(USDC)')}</label><input class="form-control" id="imp-price" type="number"></div>
15661
15630
  <div class="form-group" style="flex:1"><label class="form-label">${t('库存')}</label><input class="form-control" id="imp-stock" type="number" value="1"></div>
15662
15631
  </div>
15663
15632
  <div id="imp-price-hint" style="font-size:12px;color:#059669;margin:-8px 0 12px;padding:8px 12px;background:#f0fdf4;border-radius:6px;display:none"></div>
@@ -15688,7 +15657,7 @@ async function renderSeller(app) {
15688
15657
  <div id="add-product-form" style="display:none">
15689
15658
  <div class="divider"></div>
15690
15659
  <div class="card">
15691
- <div style="font-weight:700;margin-bottom:16px">${t('上架新商品')}</div>
15660
+ ${window.createKindChooserHtml ? window.createKindChooserHtml('new') : ''}<div style="font-weight:700;margin-bottom:16px">${t('上架新商品')}</div>
15692
15661
  <div id="add-msg"></div>
15693
15662
  <div id="seo-score-bar-prd"></div> <!-- #1052 SEO 友好度评分,setupSeoScoreBar('prd') 启用 -->
15694
15663
 
@@ -15711,7 +15680,7 @@ async function renderSeller(app) {
15711
15680
  <div class="form-group"><label class="form-label">${t('商品描述')} *<span style="font-size:11px;color:#9ca3af;font-weight:400;margin-left:6px">${t('面向 Agent 检索,写核心参数而非营销语言')}</span></label>
15712
15681
  <textarea class="form-control" id="prd-desc" rows="3" placeholder="${t('材质、尺寸、颜色、适用场景...')}"></textarea></div>
15713
15682
  <div style="display:flex;gap:12px">
15714
- <div class="form-group" style="flex:1"><label class="form-label">${t('价格(WAZ)')} *</label><input class="form-control" id="prd-price" type="number" placeholder="199" oninput="updateTrialCost()"></div>
15683
+ <div class="form-group" style="flex:1"><label class="form-label">${t('价格(USDC)')} *</label><input class="form-control" id="prd-price" type="number" placeholder="199" oninput="updateTrialCost()"></div>
15715
15684
  <div class="form-group" style="flex:1"><label class="form-label">${t('库存')}</label><input class="form-control" id="prd-stock" type="number" value="1"></div>
15716
15685
  </div>
15717
15686
 
@@ -15726,7 +15695,7 @@ async function renderSeller(app) {
15726
15695
 
15727
15696
  <div class="form-group"><label class="form-label">${t('规格参数')}<span style="font-size:11px;color:#9ca3af;font-weight:400;margin-left:6px">${t('每行一个,格式:材质: 陶瓷')}</span></label>
15728
15697
  <textarea class="form-control" id="prd-specs" rows="3" placeholder="${t('材质: 陶瓷&#10;容量: 350ml&#10;颜色: 白色')}"></textarea></div>
15729
-
15698
+ <div class="form-group"><label class="form-label">${t('商品类型')}</label><select class="form-control" id="prd-type"><option value="retail">${t('零售')}</option><option value="wholesale">${t('批发')}</option><option value="service">${t('服务')}</option><option value="digital">${t('数字')}</option></select></div>
15730
15699
  <div style="display:flex;gap:12px">
15731
15700
  <div class="form-group" style="flex:1"><label class="form-label">${t('分类')}</label>
15732
15701
  <select class="form-control" id="prd-cat">
@@ -15739,7 +15708,7 @@ async function renderSeller(app) {
15739
15708
  <div class="form-group" style="flex:1"><label class="form-label">${t('备货时间(小时)')}</label>
15740
15709
  <input class="form-control" id="prd-handling" type="number" value="24" min="1"></div>
15741
15710
  </div>
15742
-
15711
+ <div style="display:flex;gap:12px"><div class="form-group" style="flex:1"><label class="form-label">${t('品牌')}</label><input class="form-control" id="prd-brand"></div><div class="form-group" style="flex:1"><label class="form-label">${t('型号')}</label><input class="form-control" id="prd-model"></div></div>
15743
15712
  <details style="margin-bottom:16px">
15744
15713
  <summary style="font-size:13px;color:#6b7280;cursor:pointer;padding:4px 0">${t('售后与物流(填写越完整,Agent 越优先选择你)')}</summary>
15745
15714
  <div style="margin-top:12px;display:flex;gap:12px">
@@ -15758,8 +15727,8 @@ async function renderSeller(app) {
15758
15727
  <input type="checkbox" id="prd-fragile" style="width:16px;height:16px">
15759
15728
  <label for="prd-fragile" style="font-size:13px">${t('易碎品,需特殊包装')}</label>
15760
15729
  </div>
15761
- </details>
15762
-
15730
+ </details>${window.listingCommerceSectionHtml ? window.listingCommerceSectionHtml(null) : ''}
15731
+ <div style="display:flex;gap:8px;align-items:center;margin-bottom:12px"><label class="form-label" style="margin:0;font-size:13px">${t('低库存阈值')}</label><input class="form-control" id="prd-low-stock" type="number" value="3" min="0" style="width:90px"><input type="checkbox" id="prd-auto-delist" checked style="width:16px;height:16px"><label for="prd-auto-delist" style="font-size:13px">${t('售罄自动下架')}</label></div>
15763
15732
  <!-- 2026-05-24 #981:测评免单 (Trial Review Refund) -->
15764
15733
  <details style="margin-bottom:16px" id="prd-trial-section">
15765
15734
  <summary style="font-size:13px;color:#9333ea;cursor:pointer;padding:6px 0;font-weight:600">🎁 ${t('测评免单(新品冷启动)')} <span style="font-size:11px;color:#9ca3af;font-weight:400">${t('(买家原价下单 → 笔记达 reach 阈值后系统自动退款)')}</span></summary>
@@ -15851,6 +15820,7 @@ async function renderSeller(app) {
15851
15820
  </div>
15852
15821
  </div>
15853
15822
  `, 'seller')
15823
+ if (sellerSubTab === 'settings') { window.dpHydrateInstruction && window.dpHydrateInstruction(); window.dpHydrateSellerReadiness && window.dpHydrateSellerReadiness(); window.dpHydrateSellerFee && window.dpHydrateSellerFee(); window.dpHydrateSalesReport && window.dpHydrateSalesReport(); window.dpHydrateFeeRequest && window.dpHydrateFeeRequest(); window.dpHydrateSellerDeferral && window.dpHydrateSellerDeferral(); window.dpHydrateSellerProductVerify && window.dpHydrateSellerProductVerify(); window.dpHydrateSellerStoreVerify && window.dpHydrateSellerStoreVerify(); window.draHydrateAccounts && window.draHydrateAccounts(); window.shipHydrateSellerSettings && window.shipHydrateSellerSettings(); window.bondHydrateSeller && window.bondHydrateSeller() }
15854
15824
 
15855
15825
  // 智能下单"我也要上架"跳过来时:自动切到商品 tab + 展开手工上架表单 + 预填标题
15856
15826
  // hashchange 可能多次触发 renderSeller — 用 window cache 保证每次重渲都能应用
@@ -16124,13 +16094,13 @@ window.doAddProduct = async () => {
16124
16094
  const desc = document.getElementById('prd-desc').value.trim()
16125
16095
  const price = Number(document.getElementById('prd-price').value)
16126
16096
  const stock = Number(document.getElementById('prd-stock').value) || 1
16127
- const category = document.getElementById('prd-cat').value
16097
+ const category = document.getElementById('prd-cat').value; const productType = document.getElementById('prd-type')?.value || 'retail'
16128
16098
  const msgEl = document.getElementById('add-msg')
16129
16099
 
16130
16100
  if (!title || !desc || !price) { msgEl.innerHTML = alert$('error', t('请填写商品名、描述、价格')); return }
16131
16101
 
16132
- const extTitle = (document.getElementById('prd-ext-title')?.value || '').trim()
16133
- const commissionPct = Number(document.getElementById('prd-commission')?.value || 10)
16102
+ const extTitle = (document.getElementById('prd-ext-title')?.value || '').trim(); const brand = (document.getElementById('prd-brand')?.value || '').trim(); const model = (document.getElementById('prd-model')?.value || '').trim()
16103
+ const commissionPct = Number(document.getElementById('prd-commission')?.value || 10); const lowStock = document.getElementById('prd-low-stock')?.value; const autoDelist = document.getElementById('prd-auto-delist')?.checked ? 1 : 0
16134
16104
  if (!(commissionPct >= 1 && commissionPct <= 50)) {
16135
16105
  msgEl.innerHTML = alert$('error', t('佣金比例需在 1%-50% 之间')); return
16136
16106
  }
@@ -16138,7 +16108,7 @@ window.doAddProduct = async () => {
16138
16108
  const checkedAliases = Array.from(document.querySelectorAll('#prd-alias-candidates input[type="checkbox"]:checked'))
16139
16109
  .map(cb => ({ type: cb.dataset.type, value: cb.dataset.value }))
16140
16110
  const payload = {
16141
- title, description: desc, price, stock, category,
16111
+ title, description: desc, price, stock, category, product_type: productType, brand, model,
16142
16112
  specs: parseSpecs(document.getElementById('prd-specs').value),
16143
16113
  handling_hours: Number(document.getElementById('prd-handling').value) || 24,
16144
16114
  ship_regions: document.getElementById('prd-ship-regions').value.trim() || '全国',
@@ -16146,14 +16116,14 @@ window.doAddProduct = async () => {
16146
16116
  return_days: Number(document.getElementById('prd-return').value) ?? 7,
16147
16117
  return_condition: document.getElementById('prd-return-cond').value.trim(),
16148
16118
  warranty_days: Number(document.getElementById('prd-warranty').value) ?? 0,
16149
- fragile: document.getElementById('prd-fragile').checked ? 1 : 0,
16119
+ fragile: document.getElementById('prd-fragile').checked ? 1 : 0, low_stock_threshold: lowStock === '' || lowStock == null ? undefined : Number(lowStock), auto_delist_on_zero: autoDelist,
16150
16120
  commission_rate: commissionPct / 100,
16151
16121
  ...(extTitle ? { external_title: extTitle } : {}),
16152
16122
  ...(checkedAliases.length ? { aliases: checkedAliases } : {}),
16153
- ...(state._addProductImgs?.length ? { image_hashes: state._addProductImgs.map(it => it.hash) } : {}),
16123
+ ...(state._addProductImgs?.length ? { image_hashes: state._addProductImgs.map(it => it.hash) } : {}), ...(window.listingCommerceHasOverrides && window.listingCommerceHasOverrides() ? { create_status: 'warehouse' } : {}), // S4:含覆盖的新品先落仓库,全落定后激活
16154
16124
  }
16155
16125
  const res = await POST('/products', payload)
16156
- if (res.error) { msgEl.innerHTML = alert$('error', res.error); return }
16126
+ if (res.error) { msgEl.innerHTML = alert$('error', res.error); return } if (res.product_id && window.listingCommerceSave) { const lc = await window.listingCommerceSave(res.product_id, { activate: payload.create_status === 'warehouse' }); if (!lc.ok) { msgEl.innerHTML = alert$('error', (payload.create_status === 'warehouse' ? t('商品已存为仓库(未公开),配送/税费设置失败,请到「我的商品」编辑修复后上架:') : t('单品配送/税费设置失败:')) + lc.error); return } }
16157
16127
 
16158
16128
  // 产品创建成功 → 把每张图的 blob 存到本地 IDB + 注册 manifest(带 thumbnail)
16159
16129
  // 服务端只拿到了 hash 数组;blob 字节留在卖家节点
@@ -16604,7 +16574,7 @@ async function renderEditProduct(app, productId) {
16604
16574
  <div style="display:flex;gap:12px">
16605
16575
  <div class="form-group" style="flex:1">
16606
16576
  <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px">
16607
- <label class="form-label" style="margin:0">${t('价格(WAZ)')}</label>
16577
+ <label class="form-label" style="margin:0">${t('价格(USDC)')}</label>
16608
16578
  <button class="btn btn-outline btn-sm" style="font-size:10px;padding:3px 8px;color:#16a34a;border-color:#86efac" onclick="aiSuggestPrice()">💰 ${t('AI 建议')}</button>
16609
16579
  </div>
16610
16580
  <input class="form-control" id="ep-price" type="number" value="${p.price || ''}">
@@ -16703,7 +16673,7 @@ async function renderEditProduct(app, productId) {
16703
16673
  <label for="ep-fragile" style="font-size:13px">${t('易碎品,需特殊包装')}</label>
16704
16674
  </div>
16705
16675
  </div>
16706
- </details>
16676
+ </details>${window.listingCommerceSectionHtml ? window.listingCommerceSectionHtml(p) : ''}
16707
16677
  <button class="btn btn-primary" onclick="doUpdateProduct('${productId}')">${t('保存修改')}</button>
16708
16678
  </div>
16709
16679
 
@@ -16888,7 +16858,7 @@ window.doUpdateProduct = async (productId) => {
16888
16858
  body: JSON.stringify(payload),
16889
16859
  }).then(r => r.json())
16890
16860
  if (btn) { btn.disabled = false; btn.textContent = t('保存修改') }
16891
- if (res.error) { msgEl.innerHTML = alert$('error', res.error); return }
16861
+ if (res.error) { msgEl.innerHTML = alert$('error', res.error); return } if (window.listingCommerceSave) { const lc = await window.listingCommerceSave(productId); if (!lc.ok) { msgEl.innerHTML = alert$('error', t('单品配送/税费设置失败:') + lc.error); return } } // S4 单品覆盖
16892
16862
  msgEl.innerHTML = alert$('success', t('已保存'))
16893
16863
  setTimeout(() => msgEl.innerHTML = '', 2000)
16894
16864
  }
@@ -17116,7 +17086,7 @@ async function p2pConnectTo(peerId) {
17116
17086
 
17117
17087
  const offer = await pc.createOffer()
17118
17088
  await pc.setLocalDescription(offer)
17119
- await POST('/signaling/send', { to: peerId, type: 'offer', data: offer })
17089
+ await POST('/signaling/send', { to: peerId, type: 'offer', data: offer }); if (window.pollBoost) pollBoost() // 主动建 P2P 会话 → 握手期间信令保持 3s
17120
17090
  return entry
17121
17091
  }
17122
17092
 
@@ -17300,8 +17270,8 @@ async function p2pStart() {
17300
17270
  p2pStop()
17301
17271
  await p2pHeartbeatTick()
17302
17272
  await p2pSignalingTick()
17303
- _heartbeatTimer = setInterval(p2pHeartbeatTick, 60_000)
17304
- _signalingTimer = setInterval(p2pSignalingTick, 3_000)
17273
+ _heartbeatTimer = setInterval(() => { if (!document.hidden) p2pHeartbeatTick() }, 60_000) // 后台/锁屏暂停(poll-governor)
17274
+ _signalingTimer = setInterval(() => { if (!window.pollGate || window.pollGate('signaling')) p2pSignalingTick() }, 3_000); window._p2pSigTick = p2pSignalingTick // 按需化:governor 定 3s/60s 节奏+后台暂停(缺 governor fail-open 维持 3s);_p2pSigTick 供回前台补拉
17305
17275
  }
17306
17276
 
17307
17277
  function p2pStop() {
@@ -17342,7 +17312,7 @@ window.p2pUnpinContent = async (hash) => {
17342
17312
  async function p2pSignalingTick() {
17343
17313
  if (!state.user) return
17344
17314
  try {
17345
- const data = await GET('/signaling/poll')
17315
+ const data = await GET('/signaling/poll'); if (window.pollActivity) pollActivity('signaling', (data.signals || []).length) // 有信令进来 → governor 恢复 3s 快节奏
17346
17316
  for (const s of (data.signals || [])) {
17347
17317
  try { await p2pHandleSignal(s) } catch (e) { console.warn('[P2P] signal handle err', e) }
17348
17318
  }
@@ -17433,7 +17403,7 @@ async function doBatchBuy(urls, addr, auto) {
17433
17403
  <span style="font-size:12px;font-weight:700;color:${recColor};white-space:nowrap">${recLabel}</span>
17434
17404
  </div>
17435
17405
  ${res.best_product ? `<div style="margin-top:6px;font-size:12px;color:#16a34a;display:flex;align-items:center;gap:6px;flex-wrap:wrap">
17436
- <span>${t('最佳替代')}:${escHtml(res.best_product.title)} · <strong>${res.best_product.price} WAZ</strong></span>
17406
+ <span>${t('最佳替代')}:${escHtml(res.best_product.title)} · <strong>${window.fmtPrice(res.best_product.price)}</strong></span>
17437
17407
  <button class="btn btn-primary btn-sm" style="width:auto;padding:3px 10px;font-size:11px" onclick="navigate('#order-product/${res.best_product.id}')">${t('下单')}</button>
17438
17408
  </div>` : ''}
17439
17409
  ${res.auto_bought ? `<div style="margin-top:6px;font-size:12px;color:#16a34a">✅ ${t('已自动下单')} <a href="#order/${res.order_id}" style="color:#16a34a;font-weight:600">${res.order_id}</a></div>` : ''}`
@@ -17648,7 +17618,7 @@ window.doAgentBuy = async () => {
17648
17618
  const bestCard = res.best_product ? `
17649
17619
  <div style="background:#f0fdf4;border:1px solid #bbf7d0;border-radius:10px;padding:12px;margin:12px 0">
17650
17620
  <div style="font-weight:600;font-size:14px;margin-bottom:4px">${res.best_product.title}</div>
17651
- <div style="font-size:18px;font-weight:700;color:#16a34a;margin-bottom:4px">${res.best_product.price} WAZ</div>
17621
+ <div style="font-size:18px;font-weight:700;color:#16a34a;margin-bottom:4px">${window.fmtPrice(res.best_product.price)}</div>
17652
17622
  <div style="font-size:12px;color:#6b7280;margin-bottom:8px">${res.best_product.agent_summary || ''}</div>
17653
17623
  ${!res.auto_bought ? `<button class="btn btn-primary btn-sm" style="width:auto" onclick="navigate('#order-product/${res.best_product.id}')">${t('查看并下单')}</button>` : ''}
17654
17624
  </div>` : ''
@@ -17668,7 +17638,7 @@ window.doAgentBuy = async () => {
17668
17638
  <div style="font-size:13px;font-weight:500">${p.url_match ? '🎯 ' : ''}${p.title}</div>
17669
17639
  <div style="font-size:11px;color:#6b7280">${p.agent_summary || ''}${p.url_match ? ` · <span style="color:#16a34a">${t('同款商品')}</span>` : ''}</div>
17670
17640
  </div>
17671
- <div style="font-weight:700;color:#1d4ed8;white-space:nowrap;margin-left:8px">${p.price} WAZ</div>
17641
+ <div style="font-weight:700;color:#1d4ed8;white-space:nowrap;margin-left:8px">${window.fmtPrice(p.price)}</div>
17672
17642
  </div>`).join('')}
17673
17643
  </div>` : ''
17674
17644
 
@@ -17705,7 +17675,7 @@ async function renderVerifyTasks(app) {
17705
17675
  // ── Verifier 状态 banner(依据 /verifier/status) ─────────────
17706
17676
  const vState = vStatus?.state || 'none'
17707
17677
  let verifierBanner = ''
17708
- if (vState === 'none') {
17678
+ if (vState === 'none' && state.user?.role !== 'verifier' && !(state.user?.roles || []).includes('verifier')) { // 已是审核员(role 直授,无申请记录)不再邀请"申请审核员资格"(走查:身份矛盾卡)
17709
17679
  verifierBanner = `
17710
17680
  <div class="card" style="margin-bottom:16px;background:linear-gradient(135deg,#f5f3ff,#fce7f3);border-color:#c4b5fd">
17711
17681
  <div style="font-size:15px;font-weight:600;margin-bottom:6px">🛡 ${t('申请审核员资格')}</div>
@@ -18349,7 +18319,7 @@ async function renderWallet(app) {
18349
18319
  <div style="background:#ecfdf5;border-radius:6px;padding:8px">
18350
18320
  <div style="font-size:9px;color:#065f46">📡 ${t('推荐分润')}</div>
18351
18321
  <div style="font-size:14px;font-weight:700;color:#059669;margin-top:2px">${((inc.commissions?.l1?.total||0)+(inc.commissions?.l2?.total||0)+(inc.commissions?.l3?.total||0)).toFixed(2)}</div>
18352
- <div style="font-size:9px;color:#9ca3af">${((inc.commissions?.l1?.count||0)+(inc.commissions?.l2?.count||0)+(inc.commissions?.l3?.count||0))} ${t('单')}</div>
18322
+ <div style="font-size:9px;color:#9ca3af">${((inc.commissions?.l1?.count||0)+(inc.commissions?.l2?.count||0)+(inc.commissions?.l3?.count||0))} ${t('单')}</div>${(inc.commission_clearing||0)>0?`<div style="font-size:8px;color:#9ca3af" title="${t('清算中:退货期满后到账')}">${t('清算中')} ${Number(inc.commission_clearing).toFixed(2)}</div>`:''}
18353
18323
  </div>
18354
18324
  <div style="background:#fef3c7;border-radius:6px;padding:8px">
18355
18325
  <div style="font-size:9px;color:#92400e">⏳ ${t('待结算')}</div>
@@ -18794,10 +18764,10 @@ function connectSSE() {
18794
18764
  if (data.type === 'init') {
18795
18765
  updateBadge(data.unread)
18796
18766
  } else {
18797
- // 实时推送:更新角标 + 显示 toast
18767
+ // 实时推送:更新角标 + 显示 toast(N1:模板通知按 viewer locale 渲染,旧推送回退原文)
18798
18768
  state.unread++
18799
18769
  updateBadge(state.unread)
18800
- showToast(data.title, data.body)
18770
+ const _tn = window.notifRender ? window.notifRender(data) : data; showToast(_tn.title, _tn.body)
18801
18771
  }
18802
18772
  }
18803
18773
  state.sse.onerror = () => {
@@ -19848,7 +19818,7 @@ async function shInjectStrip(containerId, opts = {}) {
19848
19818
  ${it.cover ? `<img src="${escAttr(it.cover)}" style="width:100%;height:100%;object-fit:cover">` : `<div style="font-size:28px">${shCatIcon(it.category)}</div>`}
19849
19819
  </div>
19850
19820
  <div style="padding:6px">
19851
- <div style="font-size:13px;font-weight:700;color:#dc2626">${Number(it.price).toFixed(0)} WAZ</div>
19821
+ <div style="font-size:13px;font-weight:700;color:#dc2626">${window.fmtPrice(it.price)}</div>
19852
19822
  <div style="font-size:10px;color:#6b7280;line-height:1.3;margin-top:2px;display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;overflow:hidden">${escHtml(it.title)}</div>
19853
19823
  <div style="font-size:9px;color:${cond.color};margin-top:3px;font-weight:600">${t(cond.label)}</div>
19854
19824
  </div>
@@ -19874,8 +19844,8 @@ const SKM_KINDS = [
19874
19844
  function skmKindMeta(k) { return SKM_KINDS.find(x => x.id === k) || { icon: '⚙️', label: k } }
19875
19845
  function skmBillingLabel(mode, price) {
19876
19846
  if (mode === 'free') return `🆓 ${t('免费')}`
19877
- if (mode === 'per_use') return `🔁 ${Number(price).toFixed(0)} WAZ/${t('次')}`
19878
- return `💰 ${Number(price).toFixed(0)} WAZ`
19847
+ if (mode === 'per_use') return `🔁 ${window.fmtPrice(price)}/${t('次')}`
19848
+ return `💰 ${window.fmtPrice(price)}`
19879
19849
  }
19880
19850
  const SKM_STATUS = {
19881
19851
  submitted: { label: '审核中', color: '#f59e0b' },
@@ -19964,9 +19934,9 @@ async function renderSkillDetail(app, id) {
19964
19934
  } else if (l.billing_mode === 'one_time') {
19965
19935
  actionBtn = owned
19966
19936
  ? `<button class="btn btn-primary" style="width:100%;font-size:14px;padding:10px" onclick="skmReadContent('${l.id}','one_time')">${t('查看正文')}</button>`
19967
- : `<button class="btn btn-primary" style="width:100%;font-size:14px;padding:10px" onclick="skmPurchase('${l.id}')">${t('购买')} · ${Number(l.price).toFixed(0)} WAZ</button>`
19937
+ : `<button class="btn btn-primary" style="width:100%;font-size:14px;padding:10px" onclick="skmPurchase('${l.id}')">${t('购买')} · ${window.fmtPrice(l.price)}</button>`
19968
19938
  } else {
19969
- actionBtn = `<button class="btn btn-primary" style="width:100%;font-size:14px;padding:10px" onclick="skmReadContent('${l.id}','per_use')">${t('使用')} · ${Number(l.price).toFixed(0)} WAZ/${t('次')}</button>
19939
+ actionBtn = `<button class="btn btn-primary" style="width:100%;font-size:14px;padding:10px" onclick="skmReadContent('${l.id}','per_use')">${t('使用')} · ${window.fmtPrice(l.price)}/${t('次')}</button>
19970
19940
  <div style="font-size:10px;color:#9ca3af;text-align:center;margin-top:6px">${t('按次付费:每次查看正文都会扣费')}</div>`
19971
19941
  }
19972
19942
  app.innerHTML = shell(`
@@ -20209,7 +20179,7 @@ async function renderSecondhandMarket(app) {
20209
20179
  <button class="btn btn-outline btn-sm" style="font-size:12px;padding:0 10px;white-space:nowrap" onclick="document.getElementById('sh-adv').style.display=document.getElementById('sh-adv').style.display==='none'?'block':'none'">⚙ ${t('筛选')}</button>
20210
20180
  </div>
20211
20181
  <div id="sh-adv" style="display:${(_shFilters.minP || _shFilters.maxP || _shFilters.conds.size>0) ? 'block':'none'};background:#f9fafb;border:1px solid #e5e7eb;border-radius:6px;padding:10px;margin-bottom:10px">
20212
- <div style="font-size:11px;color:#6b7280;margin-bottom:6px">${t('价格区间 (WAZ)')}</div>
20182
+ <div style="font-size:11px;color:#6b7280;margin-bottom:6px">${t('价格区间 (USDC)')}</div>
20213
20183
  <div style="display:flex;gap:6px;align-items:center;margin-bottom:10px">
20214
20184
  <input id="sh-minP" class="form-control" type="number" placeholder="${t('最低')}" style="font-size:12px;flex:1" value="${escAttr(_shFilters.minP)}" oninput="_shFilters.minP=this.value">
20215
20185
  <span style="color:#9ca3af">—</span>
@@ -20283,7 +20253,7 @@ function shItemCard(it) {
20283
20253
  ${Number(it.negotiable) === 1 ? `<span style="position:absolute;top:6px;left:6px;background:rgba(0,0,0,0.7);color:#fff;font-size:9px;padding:2px 6px;border-radius:4px">${t('可议价')}</span>` : ''}
20284
20254
  </div>
20285
20255
  <div style="padding:8px">
20286
- <div style="font-size:14px;font-weight:700;color:#dc2626">${Number(it.price).toFixed(0)} WAZ</div>
20256
+ <div style="font-size:14px;font-weight:700;color:#dc2626">${window.fmtPrice(it.price)}</div>
20287
20257
  <div style="font-size:11px;color:#374151;line-height:1.3;margin:2px 0;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden">${escHtml(it.title)}</div>
20288
20258
  <div style="display:flex;align-items:center;gap:4px;font-size:9px;margin-top:4px">
20289
20259
  <span style="background:${cond.color}1a;color:${cond.color};padding:1px 5px;border-radius:3px;font-weight:600">${t(cond.label)}</span>
@@ -20303,10 +20273,12 @@ async function renderSecondhandPublish(app) {
20303
20273
  <button onclick="history.back()" style="background:none;border:none;font-size:18px;cursor:pointer;color:#6b7280">←</button>
20304
20274
  <h1 style="font-size:18px;font-weight:600;margin:0">${t('发布闲置')}</h1>
20305
20275
  </div>
20276
+ ${window.createKindChooserHtml ? window.createKindChooserHtml('secondhand') : ''}
20306
20277
 
20307
20278
  <div class="card" style="margin-bottom:12px">
20308
20279
  <div class="card-body">
20309
20280
  <div style="font-size:12px;color:#374151;font-weight:600;margin-bottom:8px">${t('图片(首张为封面,最多 9 张)')}</div>
20281
+ <div style="font-size:11px;color:#b45309;background:#fffbeb;border:1px solid #fde68a;border-radius:6px;padding:6px 8px;margin-bottom:8px;line-height:1.5">📸 ${t('请上传真实实物照,勿用网图/广告图;如经举报核实为虚假图片,商品可能被下架')}</div>
20310
20282
  <input type="file" accept="image/*" multiple id="sh-imgs-input" style="display:none" onchange="shHandleImagesChange(this)">
20311
20283
  <div id="sh-imgs-grid" style="display:grid;grid-template-columns:repeat(3,1fr);gap:6px"></div>
20312
20284
  <button class="btn btn-outline btn-sm" style="margin-top:8px;font-size:12px" onclick="document.getElementById('sh-imgs-input').click()">+ ${t('选择图片')}</button>
@@ -20496,7 +20468,7 @@ function shMineRenderItems(allItems) {
20496
20468
  <span style="position:absolute;top:6px;right:6px;background:${statusColor[it.status] || '#9ca3af'};color:#fff;font-size:9px;padding:2px 6px;border-radius:4px;font-weight:600">${t(statusLabel[it.status] || it.status)}</span>
20497
20469
  </div>
20498
20470
  <div style="padding:8px">
20499
- <div style="font-size:14px;font-weight:700;color:#dc2626">${Number(it.price).toFixed(0)} WAZ</div>
20471
+ <div style="font-size:14px;font-weight:700;color:#dc2626">${window.fmtPrice(it.price)}</div>
20500
20472
  <div style="font-size:11px;color:#374151;line-height:1.3;margin:2px 0;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden">${escHtml(it.title)}</div>
20501
20473
  <div style="display:flex;justify-content:space-between;font-size:9px;color:#9ca3af;margin-top:4px">
20502
20474
  <span>👁 ${it.view_count || 0}</span>
@@ -20539,12 +20511,11 @@ async function renderSecondhandDetail(app, id) {
20539
20511
  </div>
20540
20512
  <div style="padding:14px">
20541
20513
  <div style="display:flex;align-items:flex-start;justify-content:space-between;gap:10px;margin-bottom:8px">
20542
- <div style="font-size:24px;font-weight:700;color:#dc2626">${Number(it.price).toFixed(2)} WAZ</div>
20514
+ <div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap"><div style="font-size:24px;font-weight:700;color:#dc2626">${window.fmtPrice(it.price)}</div><span style="background:${cond.color}1a;color:${cond.color};font-size:11px;padding:3px 9px;border-radius:99px;font-weight:600;white-space:nowrap">${t(cond.label)}</span></div>
20543
20515
  ${Number(it.negotiable) === 1 ? `<span style="background:#fef3c7;color:#92400e;font-size:10px;padding:3px 8px;border-radius:99px;font-weight:600;white-space:nowrap">${t('可议价')}</span>` : ''}
20544
20516
  </div>
20545
20517
  <h2 style="font-size:16px;color:#111827;line-height:1.4;margin:0 0 10px">${escHtml(it.title)}</h2>
20546
20518
  <div style="display:flex;flex-wrap:wrap;gap:6px;font-size:10px;margin-bottom:12px">
20547
- <span style="background:${cond.color}1a;color:${cond.color};padding:3px 8px;border-radius:4px;font-weight:600">${t(cond.label)}</span>
20548
20519
  <span style="background:#f3f4f6;color:#6b7280;padding:3px 8px;border-radius:4px">${shCatIcon(it.category)} ${t(shCatLabel(it.category))}</span>
20549
20520
  <span style="background:#f3f4f6;color:#6b7280;padding:3px 8px;border-radius:4px">📦 ${t(ffLabel)}</span>
20550
20521
  ${it.region ? `<span style="background:#f3f4f6;color:#6b7280;padding:3px 8px;border-radius:4px">📍 ${escHtml(it.region)}</span>` : ''}
@@ -20558,8 +20529,9 @@ async function renderSecondhandDetail(app, id) {
20558
20529
  <div style="color:#9ca3af;font-size:10px">@${escHtml(it.seller_handle || '')} · ${t('已浏览')} ${it.view_count}</div>
20559
20530
  </div>
20560
20531
  </div>
20561
- <div style="background:#eef2ff;border-radius:8px;padding:10px;margin-bottom:14px;font-size:11px;color:#3730a3;line-height:1.5">
20562
- 🔒 ${t('WebAZ escrow 保障:付款后资金锁仓,确认收货才放给卖家;面交也可托管。协议费 1%。')}
20532
+ <div style="background:#eef2ff;border-radius:8px;padding:10px 12px;margin-bottom:14px;font-size:10.5px;color:#4338ca;line-height:1.7">
20533
+ <div style="font-weight:600;color:#3730a3;margin-bottom:4px">🛡️ ${t('WebAZ 交易保障')}<span style="font-weight:400;color:#6366f1;margin-left:6px">${t('协议费 1%')}</span></div>
20534
+ 🔒 ${t('资金托管·确认收货才放款')} · 🤝 ${t('面交也可托管')} · 🔎 ${t('成色声明可社区验证')}
20563
20535
  </div>
20564
20536
  ${isOwn ? `
20565
20537
  <button class="btn btn-outline" style="width:100%;font-size:13px;color:#6b7280;border-color:#d1d5db" onclick="doShToggleClose('${it.id}', '${it.status}')">${it.status === 'closed' ? t('重新上架') : (it.status === 'available' ? t('下架') : '')}</button>
@@ -20584,7 +20556,7 @@ async function renderSecondhandDetail(app, id) {
20584
20556
  ${o.cover ? `<img src="${escAttr(o.cover)}" style="width:100%;height:100%;object-fit:cover">` : `<div style="font-size:30px">${shCatIcon(o.category)}</div>`}
20585
20557
  </div>
20586
20558
  <div style="padding:6px">
20587
- <div style="font-size:13px;font-weight:700;color:#dc2626">${Number(o.price).toFixed(0)} WAZ</div>
20559
+ <div style="font-size:13px;font-weight:700;color:#dc2626">${window.fmtPrice(o.price)}</div>
20588
20560
  <div style="font-size:10px;color:#6b7280;line-height:1.3;margin-top:2px;display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;overflow:hidden">${escHtml(o.title)}</div>
20589
20561
  <div style="font-size:9px;color:${ocond.color};margin-top:3px;font-weight:600">${t(ocond.label)}</div>
20590
20562
  </div>
@@ -20622,7 +20594,7 @@ window.openShBuyModal = (id, price, ff) => {
20622
20594
  const canMeet = ff === 'in_person' || ff === 'both'
20623
20595
  _openModal(`
20624
20596
  <h2 style="font-size:16px;font-weight:600;margin-bottom:6px">${t('购买二手物品')}</h2>
20625
- <div style="font-size:18px;color:#dc2626;font-weight:700;margin-bottom:14px">${price.toFixed(2)} WAZ</div>
20597
+ <div style="font-size:18px;color:#dc2626;font-weight:700;margin-bottom:14px">${window.fmtPrice(price)}</div>
20626
20598
  <div style="font-size:12px;color:#374151;font-weight:600;margin-bottom:6px">${t('履约方式')}</div>
20627
20599
  <div style="display:flex;gap:8px;margin-bottom:12px">
20628
20600
  ${canShip ? `<label style="flex:1;border:1px solid #e5e7eb;border-radius:6px;padding:8px;cursor:pointer;font-size:12px;display:flex;align-items:center;gap:6px"><input type="radio" name="sh-ff" value="shipping" ${canShip && !canMeet ? 'checked' : ''}>🚚 ${t('快递')}</label>` : ''}
@@ -21153,7 +21125,7 @@ async function renderRfqFeed(app) {
21153
21125
  const bidsLabel = it.bid_count > 0 ? `💎 ${it.bid_count} ${t('个报价')}` : t('等待商家报价')
21154
21126
  return `<div class="card" style="margin-bottom:8px;padding:12px;cursor:pointer" onclick="navigate('#rfq/${it.id}')">
21155
21127
  <div style="font-size:13px;line-height:1.5">
21156
- ✍️ ${feedActor(it.buyer_id, it.buyer_name, it.buyer_handle)} ${t('求购')} <strong>${escHtml(it.title || it.category)}</strong>
21128
+ ✍️ ${feedActor(it.buyer_id, it.buyer_name, it.buyer_handle)} ${t('想买')} <strong>${escHtml(it.title || it.category)}</strong>
21157
21129
  ${it.qty ? ` × ${it.qty}` : ''}
21158
21130
  ${it.budget ? ` · ${t('预算')} ${it.budget} WAZ` : ''}
21159
21131
  </div>
@@ -21492,7 +21464,7 @@ async function renderMessages(app, deepSub) {
21492
21464
  body = conversations.length === 0
21493
21465
  ? `<div style="text-align:center;color:#9ca3af;padding:40px 0;font-size:13px">${t('暂无会话 — 在订单 / RFQ / 跟卖详情页发起聊天')}</div>`
21494
21466
  : conversations.map(it => {
21495
- const kindLabel = it.kind === 'order' ? '📦 ' + t('订单') : it.kind === 'rfq' ? '📩 ' + t('求购') : '🏷 ' + t('问商品')
21467
+ const kindLabel = (it.kind === 'order' ? '📦 ' + t('订单') : it.kind === 'rfq' ? '📩 ' + t('求购') : '🏷 ' + t('问商品')) + (it.context_title ? ' · ' + escHtml(it.context_title) : '') + ((it.kind === 'order' || it.kind === 'rfq') && it.context_id ? ' #' + String(it.context_id).slice(0, 10) : '') // 订单/问商品显商品名 + 订单/求购显短号 → 同一对方多会话可区分
21496
21468
  return `
21497
21469
  <div class="card" style="padding:12px;margin-bottom:8px;cursor:pointer;${it.my_unread > 0 ? 'border-left:3px solid #4f46e5' : ''}" onclick="location.hash='#chat/${it.id}'">
21498
21470
  <div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px">
@@ -21522,9 +21494,9 @@ async function renderMessages(app, deepSub) {
21522
21494
  }
21523
21495
  body = notifications.length === 0
21524
21496
  ? `<div style="text-align:center;color:#9ca3af;padding:40px 0;font-size:13px">🔔 ${t('暂无通知')}</div>`
21525
- : notifications.map(n => {
21526
- let actions = []
21527
- try { actions = n.actions ? JSON.parse(n.actions) : [] } catch {}
21497
+ : notifications.map(nRaw => {
21498
+ const n = window.notifRender ? window.notifRender(nRaw) : nRaw // N1:template_key → viewer locale 渲染;旧行回退存量 title/body
21499
+ let actions = []; try { actions = n.actions ? JSON.parse(n.actions) : [] } catch {}
21528
21500
  const route = routeFor(n)
21529
21501
  const hasActions = Array.isArray(actions) && actions.length > 0
21530
21502
  // 卡片 onclick:仅当无 actions 时整卡可点(兼容旧 notif)
@@ -21694,7 +21666,7 @@ async function renderChatDetail(app, id) {
21694
21666
 
21695
21667
  // 自动滚到底
21696
21668
  const msgsEl = document.getElementById('chat-msgs')
21697
- if (msgsEl) msgsEl.scrollTop = msgsEl.scrollHeight
21669
+ if (msgsEl) { msgsEl.scrollTop = msgsEl.scrollHeight; if (window.startChatPoll) window.startChatPoll(id, messages) } // #30 进入会话后开实时轮询(传初始消息→按末条 id 判新,自清理离开即停)
21698
21670
  }
21699
21671
 
21700
21672
  function renderChatBubble(m, myId) {
@@ -22105,7 +22077,7 @@ async function renderAuctionsFeed(app) {
22105
22077
  const tag = bidCount > 0 ? `🔥 ${bidCount} ${t('次出价')}` : t('暂无出价 · 先到先得')
22106
22078
  return `<div class="card" style="margin-bottom:8px;padding:12px;cursor:pointer" onclick="navigate('#auction/${d.id}')">
22107
22079
  <div style="font-size:13px;line-height:1.5">
22108
- 💎 ${feedActor(d.seller_id, d.seller_name, d.seller_handle)} ${t('发起拍卖')} <strong>${escHtml(d.title)}</strong>
22080
+ 💎 ${feedActor(d.seller_id, d.seller_name, d.seller_handle)} ${t('发起了拍卖')} <strong>${escHtml(d.title)}</strong>
22109
22081
  </div>
22110
22082
  <div style="font-size:11px;color:#6b7280;margin-top:4px">${t('当前')} <span style="color:#dc2626;font-weight:600">${d.current_price} WAZ</span> · ${tag} · ${ts}</div>
22111
22083
  </div>`
@@ -22114,7 +22086,7 @@ async function renderAuctionsFeed(app) {
22114
22086
  <div style="font-size:13px;line-height:1.5">
22115
22087
  ♻️ ${feedActor(d.seller_id, d.seller_name, d.seller_handle)} ${t('发布二手')} <strong>${escHtml(d.title)}</strong>
22116
22088
  </div>
22117
- <div style="font-size:11px;color:#6b7280;margin-top:4px">${d.price} WAZ · ${d.condition_grade || ''} · ${ts}</div>
22089
+ <div style="font-size:11px;color:#6b7280;margin-top:4px">${window.fmtPrice(d.price)} · ${d.condition_grade || ''} · ${ts}</div>
22118
22090
  </div>`
22119
22091
  }
22120
22092
  }).join('')
@@ -22433,7 +22405,7 @@ async function renderLeaderboard(app) {
22433
22405
  const hasRating = u.rating_count > 0
22434
22406
  const ratingColor = hasRating && u.avg_rating >= 4.5 ? '#16a34a' : hasRating && u.avg_rating >= 4 ? '#d97706' : '#9ca3af'
22435
22407
  return `
22436
- <div class="card" style="padding:12px;margin-bottom:8px;display:flex;gap:10px;align-items:center;cursor:pointer" onclick="location.hash='#shop/${u.id}'">
22408
+ <div class="card" style="padding:12px;margin-bottom:8px;display:flex;gap:10px;align-items:center;${u.handle ? 'cursor:pointer' : ''}" ${u.handle ? `onclick="location.hash='#shop/${u.handle}'"` : ''}>
22437
22409
  <div style="font-size:24px;font-weight:800;color:${i<3?'#dc2626':'#9ca3af'};min-width:32px;text-align:center">${i+1}</div>
22438
22410
  <div style="flex:1;min-width:0">
22439
22411
  <div style="font-weight:600;font-size:14px">@${escHtml(u.handle || u.name?.slice(0,8) || '?')}</div>
@@ -22458,7 +22430,7 @@ async function renderLeaderboard(app) {
22458
22430
  🤖 @${escHtml(u.handle || u.name?.slice(0,8) || '?')}
22459
22431
  <span style="font-size:10px;background:${lvlColor}22;color:${lvlColor};padding:1px 7px;border-radius:99px;font-weight:600">${lvlLabel}</span>
22460
22432
  </div>
22461
- <div style="font-size:11px;color:#6b7280;margin-top:2px">${u.calls_30d || 0} ${t('次调用')} (30d) · ${u.keys_count} ${t('个 key')}</div>
22433
+ <div style="font-size:11px;color:#6b7280;margin-top:2px">${t('活跃度')}: ${t({ active: '活跃', quiet: '较少', dormant: '休眠' }[u.activity] || '休眠')}</div>
22462
22434
  </div>
22463
22435
  <div style="text-align:right">
22464
22436
  <div style="font-size:13px;font-weight:700;color:${lvlColor}">${ts}</div>
@@ -22535,7 +22507,7 @@ async function renderLeaderboard(app) {
22535
22507
  </div>
22536
22508
  </div>
22537
22509
  <div style="text-align:right">
22538
- <div style="font-size:14px;font-weight:800;color:#dc2626">${p.price} <span style="font-size:10px;color:#9ca3af">WAZ</span></div>
22510
+ <div style="font-size:14px;font-weight:800;color:#dc2626">${window.fmtPrice(p.price)}</div>
22539
22511
  <div style="font-size:10px;color:#854d0e;margin-top:1px">${pctLabel}</div>
22540
22512
  </div>
22541
22513
  </div>`
@@ -22546,14 +22518,14 @@ async function renderLeaderboard(app) {
22546
22518
  <div style="flex:1;min-width:0">
22547
22519
  <div style="font-weight:600;font-size:14px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escHtml(p.title)}</div>
22548
22520
  <div style="font-size:11px;color:#6b7280;margin-top:2px">
22549
- @${escHtml(p.seller_handle || p.seller_name?.slice(0,8) || '?')} · ${p.price} WAZ ·
22521
+ @${escHtml(p.seller_handle || p.seller_name?.slice(0,8) || '?')} · ${window.fmtPrice(p.price)} ·
22550
22522
  🛒 ${p.completion_count || 0} · 🤝 ${p.unique_sharer_count || 0} · ❤️ ${p.total_likes || 0}
22551
22523
  </div>
22552
22524
  </div>
22553
22525
  <div style="text-align:right;font-size:12px;font-weight:700;color:#4f46e5">${Number(p.rank_score).toFixed(1)}</div>
22554
22526
  </div>
22555
22527
  `).join('') : items.map((c, i) => `
22556
- <div class="card" style="padding:12px;margin-bottom:8px;display:flex;gap:10px;align-items:center;cursor:pointer" onclick="location.hash='#u/${c.id}'">
22528
+ <div class="card" style="padding:12px;margin-bottom:8px;display:flex;gap:10px;align-items:center;${c.handle ? 'cursor:pointer' : ''}" ${c.handle ? `onclick="location.hash='#u/${c.handle}'"` : ''}>
22557
22529
  <div style="font-size:24px;font-weight:800;color:${i<3?'#dc2626':'#9ca3af'};min-width:32px;text-align:center">${i+1}</div>
22558
22530
  <div style="flex:1;min-width:0">
22559
22531
  <div style="font-weight:600;font-size:14px">@${escHtml(c.handle || c.name?.slice(0,8) || '?')}${c.region ? ' · ' + regionLabel(c.region) : ''}</div>
@@ -22608,16 +22580,16 @@ async function renderP2pBoard(app) {
22608
22580
  <div style="font-size:11px;color:#6b7280;margin-bottom:14px;line-height:1.5">${t('详情存卖家节点 · WebAZ 只锚定哈希 · 客户端验证防作弊')}</div>
22609
22581
  ${items.length === 0 ? `<div style="text-align:center;color:#9ca3af;padding:40px 0">${t('暂无 P2P 商品')}</div>` : items.map(it => {
22610
22582
  let thumb = ''
22611
- try { const imgs = JSON.parse(it.thumbnail_json || '[]'); thumb = imgs[0] || '' } catch {}
22583
+ thumb = window.productThumbSrc(it.thumbnail_json) || thumb
22612
22584
  return `
22613
22585
  <div class="card" style="padding:12px;margin-bottom:10px;cursor:pointer;display:flex;gap:10px" onclick="location.hash='#p2p-shop/${it.id}'">
22614
- ${thumb ? `<img src="${escHtml(thumb)}" style="width:80px;height:80px;object-fit:cover;border-radius:6px;flex-shrink:0">` : `<div style="width:80px;height:80px;background:#f3f4f6;border-radius:6px;display:flex;align-items:center;justify-content:center;font-size:32px;flex-shrink:0">🌐</div>`}
22586
+ ${thumb ? `<img src="${escHtml(thumb)}" onerror="this.outerHTML='🌐'" style="width:80px;height:80px;object-fit:cover;border-radius:6px;flex-shrink:0">` : `<div style="width:80px;height:80px;background:#f3f4f6;border-radius:6px;display:flex;align-items:center;justify-content:center;font-size:32px;flex-shrink:0">🌐</div>`}
22615
22587
  <div style="flex:1;min-width:0">
22616
22588
  <div style="font-weight:600;font-size:14px;margin-bottom:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escHtml(it.title)}</div>
22617
22589
  <div style="font-size:11px;color:#6b7280;margin-bottom:3px">@${escHtml(it.seller_handle || it.seller_id.slice(0,8))} · ${it.region}</div>
22618
22590
  <div style="display:flex;justify-content:space-between;align-items:center">
22619
22591
  <span style="font-size:10px;font-family:monospace;color:#9ca3af">🔒 ${it.content_hash.slice(0,12)}…</span>
22620
- <span style="color:#dc2626;font-weight:700;font-size:14px">${it.price} <span style="font-size:10px;color:#9ca3af;font-weight:500">WAZ</span></span>
22592
+ <span style="color:#dc2626;font-weight:700;font-size:14px">${window.fmtPrice(it.price)}</span>
22621
22593
  </div>
22622
22594
  </div>
22623
22595
  </div>`
@@ -22712,7 +22684,7 @@ async function renderP2pDetail(app, id) {
22712
22684
  if (r?.error) { document.getElementById('p2p-detail').innerHTML = alert$('error', r.error); return }
22713
22685
  const p = r.product
22714
22686
  let thumb = ''
22715
- try { const imgs = JSON.parse(p.thumbnail_json || '[]'); thumb = imgs[0] || '' } catch {}
22687
+ thumb = window.productThumbSrc(p.thumbnail_json) || thumb
22716
22688
 
22717
22689
  // 客户端尝试从 peer_endpoint 拉详情并验签
22718
22690
  let verifyStatus = `<span style="color:#9ca3af">${t('未尝试拉取')}</span>`
@@ -22746,11 +22718,11 @@ async function renderP2pDetail(app, id) {
22746
22718
  document.getElementById('p2p-detail').innerHTML = `
22747
22719
  <button class="btn btn-sm" onclick="history.back()" style="background:#f3f4f6;margin-bottom:14px">←</button>
22748
22720
  <div style="display:flex;gap:14px;margin-bottom:14px">
22749
- ${thumb ? `<img src="${escHtml(thumb)}" style="width:120px;height:120px;object-fit:cover;border-radius:8px;flex-shrink:0">` : ''}
22721
+ ${thumb ? `<img src="${escHtml(thumb)}" onerror="this.outerHTML='🌐'" style="width:120px;height:120px;object-fit:cover;border-radius:8px;flex-shrink:0">` : ''}
22750
22722
  <div style="flex:1">
22751
22723
  <h2 style="font-size:18px;font-weight:700;margin:0 0 4px">${escHtml(p.title)}</h2>
22752
22724
  <div style="font-size:12px;color:#6b7280;margin-bottom:6px">@${escHtml(p.seller_handle || p.seller_id.slice(0,8))} · ${p.region}</div>
22753
- <div style="color:#dc2626;font-weight:700;font-size:22px;margin-bottom:6px">${p.price} <span style="font-size:11px;color:#9ca3af;font-weight:500">WAZ</span></div>
22725
+ <div style="color:#dc2626;font-weight:700;font-size:22px;margin-bottom:6px">${window.fmtPrice(p.price)}</div>
22754
22726
  <div style="font-size:11px;color:#6b7280">${t('库存')} ${p.stock}</div>
22755
22727
  </div>
22756
22728
  </div>
@@ -22769,7 +22741,7 @@ async function renderP2pDetail(app, id) {
22769
22741
  </div>` : `
22770
22742
  <div class="card" style="padding:14px;margin-bottom:12px;background:#fafafa;color:#9ca3af;text-align:center">${t('完整详情未能从节点获取 — 仅展示 WebAZ 上的最小信息')}</div>`}
22771
22743
 
22772
- ${state.user?.role === 'buyer' && p.stock > 0 ? `<button class="btn btn-primary" style="width:100%" onclick="location.hash='#order-product/${p.id}'">${t('购买')} ${p.price} WAZ</button>` : ''}
22744
+ ${state.user?.role === 'buyer' && p.stock > 0 ? `<button class="btn btn-primary" style="width:100%" onclick="location.hash='#order-product/${p.id}'">${t('购买')} ${window.fmtPrice(p.price)}</button>` : ''}
22773
22745
  `
22774
22746
  }
22775
22747
 
@@ -22830,7 +22802,7 @@ function renderPriceHistoryHtml(r) {
22830
22802
  <div style="flex:1;background:#fafafa;padding:8px;border-radius:6px;text-align:center">
22831
22803
  <div style="font-size:10px;color:#6b7280;margin-bottom:2px">${label}</div>
22832
22804
  <div style="font-size:13px;font-weight:600">${fmtPrice(data.avg)} <span style="font-size:9px;color:#9ca3af">${t('均价')}</span></div>
22833
- <div style="font-size:10px;color:#6b7280;margin-top:2px">${data.sales}${t('单')} · ${fmtPrice(data.volume)} WAZ</div>
22805
+ <div style="font-size:10px;color:#6b7280;margin-top:2px">${data.sales}${t('单')} · ${fmtPrice(data.volume)} USDC</div>
22834
22806
  </div>`
22835
22807
 
22836
22808
  return `
@@ -22840,7 +22812,7 @@ function renderPriceHistoryHtml(r) {
22840
22812
  ${windowCard(t('近 90 天'), w.d90)}
22841
22813
  ${windowCard(t('全部'), w.lifetime)}
22842
22814
  </div>
22843
- ${r.category_avg_30d != null ? `<div style="font-size:11px;color:#6b7280;margin-bottom:10px">${t('同类目 30 天均价')}:<strong style="color:#374151">${fmtPrice(r.category_avg_30d)} WAZ</strong>${r.current_price && r.category_avg_30d ? ` · ${t('当前')}${r.current_price > r.category_avg_30d ? '↑' : '↓'}${(Math.abs((r.current_price - r.category_avg_30d) / r.category_avg_30d) * 100).toFixed(0)}%` : ''}</div>` :
22815
+ ${r.category_avg_30d != null ? `<div style="font-size:11px;color:#6b7280;margin-bottom:10px">${t('同类目 30 天均价')}:<strong style="color:#374151">${fmtPrice(r.category_avg_30d)} USDC</strong>${r.current_price && r.category_avg_30d ? ` · ${t('当前')}${r.current_price > r.category_avg_30d ? '↑' : '↓'}${(Math.abs((r.current_price - r.category_avg_30d) / r.category_avg_30d) * 100).toFixed(0)}%` : ''}</div>` :
22844
22816
  `<div style="font-size:11px;color:#9ca3af;margin-bottom:10px;font-style:italic">${t('未设置具体类目 — 无同类对比(卖家可在商品编辑页选择类目)')}</div>`}
22845
22817
  <div style="font-size:11px;color:#374151;margin-bottom:6px;font-weight:600">${t('价位分布')}(${t('90 天,按成交单数')})</div>
22846
22818
  ${bucketsHtml}
@@ -22917,11 +22889,8 @@ async function renderAdminProtocol(app) {
22917
22889
  ${adminLinkCard('🏦', t('慈善基金拨款'), t('从基金池拨款给指定用户'), '#admin/fund-disburse')}
22918
22890
  ${adminLinkCard('📜', t('审计日志'), t('全部 admin 写操作'), '#admin/audit')}
22919
22891
  ${adminLinkCard('🛑', t('错误监控'), t('24h 趋势 + burst 告警'), '#admin/errors')}
22920
- ${adminLinkCard('📨', t('Welcome 提交'), t('#welcome 留下的邮箱订阅 + 建议'), '#admin/public-ideas')}
22921
- ${adminLinkCard('🛠️', t('任务建议收件箱'), t('陌生人 / agent 提交的共建任务建议;审阅 转正式任务'), '#admin/task-proposals')}
22922
- ${((state.user && state.user.admin_type || 'root') === 'root') ? adminLinkCard('🎟️', t('建任务额度审核'), t('非根管理员的建任务扩容申请;批准 = 限时计数授权(仅 root)'), '#admin/quota-requests') : ''}
22923
- ${adminLinkCard('🔗', t('关联个人贡献账号'), t('把本管理席位的协调贡献归属到你的真实个人账号(需对方确认 + root 审批)'), '#me/operator-claims')}
22924
- ${((state.user && state.user.admin_type || 'root') === 'root') ? adminLinkCard('🪪', t('操作席位关联审批'), t('管理席位→个人贡献账号的关联申请;确认 + 审批 / 撤销(仅 root)'), '#admin/operator-claims') : ''}
22892
+ ${adminLinkCard('🌱', t('共建运营'), t('共建任务建议 / Welcome 提交 / 建任务额度 / 贡献账号关联'), '#admin/contribution-ops')}
22893
+ ${((state.user && state.user.admin_type || 'root') === 'root') ? adminLinkCard('💳', t('Direct Pay 商户运营'), t('直付:合规录入 / 缓交 / 商品·店铺验证 / 平台服务费预充值 / 上线控制(仅 root)'), '#admin/dp-ops') : ''}${((state.user && state.user.admin_type || 'root') === 'root') ? adminLinkCard('⚖', t('仲裁员管理'), t('授权/暂停/撤销真人仲裁员(唯一授权源=active 白名单,需现场 Passkey)'), '#admin/arbitrators') + adminLinkCard('🚦', t('Agent 封禁与申诉'), t('待审申诉队列(批准=立即解封)/ 主动签发 strike'), '#admin/agent-strikes') : ''}
22925
22894
  </div>
22926
22895
  `, 'admin-protocol')
22927
22896
  }
@@ -22971,7 +22940,7 @@ async function renderAdminWishReports(app) {
22971
22940
  }
22972
22941
  window.setWrStatus = (s) => { state._wrStatus = s; renderAdminWishReports(document.getElementById('app')) }
22973
22942
  window.adminWrAction = async (id, action) => {
22974
- if (!confirm(t('确认') + (action === 'dismiss' ? t('驳回') : t('下架')) + t('?'))) return
22943
+ if (!confirm(action === 'dismiss' ? t('确认驳回?') : t('确认下架?'))) return
22975
22944
  const r = await PATCH('/admin/wish-reports/' + id, { action })
22976
22945
  if (r?.error) return alert(r.error)
22977
22946
  renderAdminWishReports(document.getElementById('app'))
@@ -24493,7 +24462,7 @@ async function renderNoteLanding(app, noteId) {
24493
24462
  <div style="flex:1;min-width:0">
24494
24463
  <div style="font-size:10px;color:#9a3412;font-weight:600;margin-bottom:2px">📦 ${t('笔记关联商品')}</div>
24495
24464
  <div style="font-weight:700;font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escHtml(product.title || '')}</div>
24496
- <div style="font-size:15px;font-weight:800;color:#dc2626;margin-top:2px">${Number(product.price || 0).toFixed(2)} WAZ</div>
24465
+ <div style="font-size:15px;font-weight:800;color:#dc2626;margin-top:2px">${window.fmtPrice(product.price || 0)}</div>
24497
24466
  </div>
24498
24467
  <button class="btn btn-primary btn-sm" style="white-space:nowrap" onclick="event.stopPropagation();navigate('#order-product/${product.id}')">${t('去购买')}</button>
24499
24468
  </div>` : ''
@@ -25182,7 +25151,7 @@ async function renderSharesFeed(app, sort) {
25182
25151
  <span>❤️ ${n.stats.likes || 0} · 🔗 ${n.stats.clicks || 0}</span>
25183
25152
  </div>
25184
25153
  ${noteAuthBadges(n.badges, 'sm') ? `<div style="margin-top:4px">${noteAuthBadges(n.badges, 'sm')}</div>` : ''}
25185
- ${n.product ? `<div style="font-size:11px;color:#92400e;margin-top:4px;background:#fef3c7;padding:3px 8px;border-radius:4px;display:inline-block">📦 ${escHtml((n.product.title || '').slice(0, 20))} · ${Number(n.product.price).toFixed(0)} WAZ</div>` : ''}
25154
+ ${n.product ? `<div style="font-size:11px;color:#92400e;margin-top:4px;background:#fef3c7;padding:3px 8px;border-radius:4px;display:inline-block">📦 ${escHtml((n.product.title || '').slice(0, 20))} · ${window.fmtPrice(n.product.price)}</div>` : ''}
25186
25155
  </div>
25187
25156
  </div>
25188
25157
  </div>
@@ -25683,7 +25652,7 @@ async function renderBuyerTrials(app) {
25683
25652
  <div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px;margin-bottom:8px">
25684
25653
  <div style="flex:1;min-width:0">
25685
25654
  <div style="font-weight:600;font-size:14px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escHtml(c.product_title || '商品')}</div>
25686
- <div style="font-size:11px;color:#6b7280;margin-top:2px">${c.product_price} WAZ · ${fmtTime(c.claimed_at)}</div>
25655
+ <div style="font-size:11px;color:#6b7280;margin-top:2px">${window.fmtPrice(c.product_price)} · ${fmtTime(c.claimed_at)}</div>
25687
25656
  </div>
25688
25657
  <span style="flex-shrink:0;font-size:10px;background:${st.bg};color:${st.color};padding:2px 8px;border-radius:99px;font-weight:600">${st.label}</span>
25689
25658
  </div>
@@ -25724,7 +25693,7 @@ async function renderSellerTrials(app) {
25724
25693
  <div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px;margin-bottom:8px">
25725
25694
  <div style="flex:1;min-width:0">
25726
25695
  <div style="font-weight:600;font-size:14px">${escHtml(c.product_title || '商品')}</div>
25727
- <div style="font-size:11px;color:#6b7280;margin-top:2px">${c.product_price} WAZ · ${t('阈值')} ${c.reach_threshold} · ${t('启动')} ${fmtTime(c.created_at)}</div>
25696
+ <div style="font-size:11px;color:#6b7280;margin-top:2px">${window.fmtPrice(c.product_price)} · ${t('阈值')} ${c.reach_threshold} · ${t('启动')} ${fmtTime(c.created_at)}</div>
25728
25697
  </div>
25729
25698
  <span style="flex-shrink:0;font-size:10px;background:${c.status==='active'?'#dcfce7':'#f3f4f6'};color:${c.status==='active'?'#166534':'#6b7280'};padding:2px 8px;border-radius:99px;font-weight:600">${c.status==='active' ? t('进行中') : t('已关闭')}</span>
25730
25699
  </div>