@seasonkoh/webaz 0.1.16 → 0.1.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -5
- package/dist/layer0-foundation/L0-2-state-machine/engine.js +3 -0
- package/dist/layer1-agent/L1-1-mcp-server/server.js +836 -716
- package/dist/layer2-business/L2-8-feedback/build-feedback-engine.js +169 -0
- package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +16 -0
- package/dist/layer4-economics/L4-3-reputation/reputation-engine.js +1 -0
- package/dist/mcp.js +7 -3
- package/dist/pwa/data/onboarding-cases.js +345 -0
- package/dist/pwa/data/onboarding-quiz.js +247 -0
- package/dist/pwa/public/app.js +1410 -96
- package/dist/pwa/public/i18n.js +280 -2
- package/dist/pwa/public/icon-192.png +0 -0
- package/dist/pwa/public/icon-512.png +0 -0
- package/dist/pwa/public/manifest.json +5 -2
- package/dist/pwa/public/openapi.json +1 -1
- package/dist/pwa/public/sw.js +1 -1
- package/dist/pwa/routes/admin-protocol-params.js +80 -2
- package/dist/pwa/routes/admin-reports.js +14 -9
- package/dist/pwa/routes/auth-read.js +3 -1
- package/dist/pwa/routes/build-feedback.js +67 -0
- package/dist/pwa/routes/disputes-write.js +149 -1
- package/dist/pwa/routes/governance-auto-deactivate.js +108 -0
- package/dist/pwa/routes/governance-onboarding.js +785 -0
- package/dist/pwa/routes/leaderboard.js +10 -2
- package/dist/pwa/routes/orders-action.js +5 -1
- package/dist/pwa/routes/products-meta.js +30 -0
- package/dist/pwa/routes/profile-identity.js +1 -1
- package/dist/pwa/routes/public-utils.js +44 -0
- package/dist/pwa/routes/rewards-apply.js +210 -0
- package/dist/pwa/routes/rewards-auto-downgrade.js +65 -0
- package/dist/pwa/routes/rewards-escrow-expire.js +48 -0
- package/dist/pwa/routes/webauthn.js +1 -1
- package/dist/pwa/server.js +570 -63
- package/package.json +6 -3
package/dist/pwa/public/app.js
CHANGED
|
@@ -548,8 +548,8 @@ async function render(page, params) {
|
|
|
548
548
|
// 只在首次(没有 sponsor_name 时)阻塞等 fetch;后续重渲染走缓存秒过
|
|
549
549
|
try { await initShareCtx() } catch (e) { console.warn('[ShareCtx]', e) }
|
|
550
550
|
|
|
551
|
-
// 未登录时只允许看登录页、找回密钥页、商品、welcome
|
|
552
|
-
if (!state.apiKey && page !== 'login' && page !== 'shop' && page !== 'recover' && page !== 'welcome' && page !== '') {
|
|
551
|
+
// 未登录时只允许看登录页、找回密钥页、商品、welcome 预发布页、governance-onboarding 公开页
|
|
552
|
+
if (!state.apiKey && page !== 'login' && page !== 'shop' && page !== 'recover' && page !== 'welcome' && page !== 'governance-onboarding' && page !== '') {
|
|
553
553
|
// 保存目标 hash 以便登录/注册后跳回
|
|
554
554
|
if (location.hash && !['#login', '#recover'].includes(location.hash)) {
|
|
555
555
|
sessionStorage.setItem('webaz_intended_hash', location.hash)
|
|
@@ -630,6 +630,8 @@ async function render(page, params) {
|
|
|
630
630
|
case 'seller-trials': return renderSellerTrials(app)
|
|
631
631
|
// 2026-05-24 #991:/welcome 预发布页 — 6 区块协议介绍
|
|
632
632
|
case 'welcome': return renderWelcome(app)
|
|
633
|
+
// 2026-06-02 W3.5-B:#governance-onboarding 治理岗位申请页(公开页)
|
|
634
|
+
case 'governance-onboarding': return renderGovernanceOnboarding(app)
|
|
633
635
|
case 'seller':
|
|
634
636
|
if (state.user?.role === 'logistics') return renderLogistics(app)
|
|
635
637
|
if (state.user?.role === 'arbitrator') return renderDisputeList(app)
|
|
@@ -714,9 +716,15 @@ async function render(page, params) {
|
|
|
714
716
|
if (params[0] === 'kyc') return renderAdminKyc(app)
|
|
715
717
|
if (params[0] === 'export') return renderAdminExport(app)
|
|
716
718
|
if (params[0] === 'errors') return renderAdminErrors(app)
|
|
719
|
+
if (params[0] === 'governance') return renderAdminGovernance(app)
|
|
717
720
|
return renderAdminDashboard(app)
|
|
718
721
|
case 'apply-verifier': return renderApplyVerifier(app)
|
|
719
722
|
case 'apply-arbitrator': return renderApplyArbitrator(app)
|
|
723
|
+
case 'apply-rewards': return renderApplyRewards(app)
|
|
724
|
+
case 'rewards-me': return renderRewardsMe(app)
|
|
725
|
+
case 'onboarding-verifier': return renderOnboarding(app, 'verifier')
|
|
726
|
+
case 'onboarding-arbitrator': return renderOnboarding(app, 'arbitrator')
|
|
727
|
+
case 'governance-me': return renderGovernanceMe(app)
|
|
720
728
|
case 'wishlist': return renderWishlist(app)
|
|
721
729
|
case 'waitlist': return renderWaitlist(app)
|
|
722
730
|
case 'returns': return renderReturnsCenter(app)
|
|
@@ -727,6 +735,7 @@ async function render(page, params) {
|
|
|
727
735
|
case 'blocklist': return renderBlocklist(app)
|
|
728
736
|
case 'kyc': return renderKyc(app)
|
|
729
737
|
case 'feedback': return renderFeedback(app)
|
|
738
|
+
case 'build-feedback': return renderMyBuildFeedback(app)
|
|
730
739
|
case 'admin-feedback': return renderAdminFeedback(app)
|
|
731
740
|
case 'admin-payments': return renderAdminPayments(app)
|
|
732
741
|
case 'my-agents': return renderMyAgents(app)
|
|
@@ -991,7 +1000,7 @@ function preLaunchBannerHTML() {
|
|
|
991
1000
|
if (window._protocolPhase && window._protocolPhase !== 'pre_launch') return ''
|
|
992
1001
|
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">
|
|
993
1002
|
⚠️ <strong>${t('协议尚未公开上线 · 数据为测试 / demo · 请勿据此投资或承诺第三方')}</strong>
|
|
994
|
-
<a href="
|
|
1003
|
+
<a href="#welcome" style="margin-left:8px;color:#7c2d12;text-decoration:underline;font-weight:600">${t('详情')}</a>
|
|
995
1004
|
</div>`
|
|
996
1005
|
}
|
|
997
1006
|
|
|
@@ -1129,6 +1138,12 @@ function shell(content, activeTab, opts) {
|
|
|
1129
1138
|
</div>
|
|
1130
1139
|
</div>
|
|
1131
1140
|
` : ''}
|
|
1141
|
+
${state.user ? `
|
|
1142
|
+
<button id="feedback-fab" onclick="openBuildFeedback()" title="${t('反馈 / 建议')}"
|
|
1143
|
+
style="position:fixed;bottom:74px;left:14px;width:48px;height:48px;border-radius:50%;background:#fff;color:#4f46e5;border:1px solid #c7d2fe;cursor:pointer;font-size:22px;box-shadow:0 4px 12px rgba(0,0,0,0.12);z-index:99;display:flex;align-items:center;justify-content:center" aria-label="${t('反馈 / 建议')}">
|
|
1144
|
+
💬
|
|
1145
|
+
</button>
|
|
1146
|
+
` : ''}
|
|
1132
1147
|
${_opts.bottomBar ? `<div class="page-bottom-bar" style="position:fixed;bottom:0;left:0;right:0;z-index:50;background:#fff;border-top:1px solid #e5e7eb;padding:10px 14px;box-shadow:0 -2px 12px rgba(0,0,0,0.04);padding-bottom:max(10px, env(safe-area-inset-bottom))">${_opts.bottomBar}</div>` : ''}
|
|
1133
1148
|
${_opts.hideTabbar ? '' : `<nav class="tabbar">
|
|
1134
1149
|
${tabs.map(tb => `
|
|
@@ -1179,7 +1194,7 @@ window.openAvatarMenu = function() {
|
|
|
1179
1194
|
${item('🏠', t('我的主页'), '#me')}
|
|
1180
1195
|
${item('⚙️', t('设置 / 角色'), '#me/settings')}
|
|
1181
1196
|
${!isTrusted ? item('👁', t('公开主页'), '#u/' + u.id) : ''}
|
|
1182
|
-
${item('🤖', t('我的
|
|
1197
|
+
${item('🤖', t('我的 agents'), '#my-agents')}
|
|
1183
1198
|
|
|
1184
1199
|
${sectionTitle(t('协议'))}
|
|
1185
1200
|
${item('🏛', t('协议治理'), '#governance')}
|
|
@@ -1266,7 +1281,7 @@ async function renderMyAdvanced(app) {
|
|
|
1266
1281
|
|
|
1267
1282
|
<div style="font-size:12px;color:#6b7280;font-weight:600;margin:14px 0 6px">🤖 ${t('Agent 治理')}</div>
|
|
1268
1283
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px">
|
|
1269
|
-
${card('🤖', t('我的
|
|
1284
|
+
${card('🤖', t('我的 agents'), t('谁替我做事 · 撤销控制'), '#my-agents')}
|
|
1270
1285
|
${card('⚡', t('卖家自动化'), skillCount > 0 ? skillCount + ' ' + t('个') : t('未发布'), '#skills')}
|
|
1271
1286
|
${!isTrusted ? card('🪄', t('AI 推荐'), t('给我推商品'), '#ai-recommend') : ''}
|
|
1272
1287
|
${role === 'seller' ? card('🎯', t('Auto-bid'), t('RFQ 自动报价'), '#auto-bid') : ''}
|
|
@@ -1288,7 +1303,18 @@ async function renderMyAdvanced(app) {
|
|
|
1288
1303
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px">
|
|
1289
1304
|
${card('🏛', t('协议治理'), t('参数公开 · 变更可追溯'), '#governance')}
|
|
1290
1305
|
${card('⚖', t('判例库'), t('争议判决公开'), '#judgments')}
|
|
1306
|
+
${card('🎁', t('共建身份'), t('申请制 · 含锁仓金'), '#rewards-me')}
|
|
1291
1307
|
</div>
|
|
1308
|
+
|
|
1309
|
+
<div style="font-size:12px;color:#6b7280;font-weight:600;margin:14px 0 6px">📧 ${t('联系我们')}</div>
|
|
1310
|
+
<a href="mailto:contact@webaz.xyz" class="card" style="padding:14px;display:flex;align-items:center;gap:10px;min-height:64px;text-decoration:none;color:inherit">
|
|
1311
|
+
<div style="font-size:24px;flex-shrink:0">📧</div>
|
|
1312
|
+
<div style="flex:1;min-width:0">
|
|
1313
|
+
<div style="font-weight:600;font-size:14px">contact@webaz.xyz</div>
|
|
1314
|
+
<div style="font-size:11px;color:#9ca3af;margin-top:2px">${t('合作 / 反馈 / 合规咨询')}</div>
|
|
1315
|
+
</div>
|
|
1316
|
+
<div style="color:#9ca3af">›</div>
|
|
1317
|
+
</a>
|
|
1292
1318
|
`
|
|
1293
1319
|
app.innerHTML = shell(sections, 'me')
|
|
1294
1320
|
}
|
|
@@ -1685,7 +1711,7 @@ async function renderProfile(app) {
|
|
|
1685
1711
|
</div>
|
|
1686
1712
|
<div style="display:flex;justify-content:space-between;align-items:center;padding:6px 0;border-bottom:1px solid #f3f4f6;font-size:13px">
|
|
1687
1713
|
<span style="color:#374151">${t('协议')}</span>
|
|
1688
|
-
<a href="https://github.com/
|
|
1714
|
+
<a href="https://github.com/seasonsagents-art/webaz" target="_blank" style="color:#6366f1;text-decoration:none;font-size:12px">${t('源码仓库')} ↗</a>
|
|
1689
1715
|
</div>
|
|
1690
1716
|
<div style="display:flex;justify-content:space-between;align-items:center;padding:6px 0;border-bottom:1px solid #f3f4f6;font-size:13px">
|
|
1691
1717
|
<span style="color:#374151">🔔 ${t('推送通知')}</span>
|
|
@@ -4193,75 +4219,1078 @@ window.doAdminTaskFilter = () => {
|
|
|
4193
4219
|
}
|
|
4194
4220
|
|
|
4195
4221
|
// ─── 用户:申请审核员 ──────────────────────────────────────────
|
|
4196
|
-
|
|
4222
|
+
// ─── 治理岗位申请(W3.5-B governance onboarding,#1093 阶段 1b) ─────
|
|
4223
|
+
// docs/GOVERNANCE-ONBOARDING.md §3.1 流程图前端:
|
|
4224
|
+
// eligibility 检查 + 顶部红色 disclosure + 8s 延迟双勾选 + Passkey 签发 + POST /governance/onboarding/apply
|
|
4225
|
+
async function _renderApplyGovernance(app, role) {
|
|
4197
4226
|
if (!state.user) { renderLogin(); return }
|
|
4198
|
-
|
|
4199
|
-
const
|
|
4200
|
-
|
|
4227
|
+
const returnNav = role === 'arbitrator' ? 'me' : 'verify-tasks'
|
|
4228
|
+
const roleTitle = role === 'arbitrator' ? t('申请仲裁员') : t('申请审核员')
|
|
4229
|
+
const roleIcon = role === 'arbitrator' ? '⚖' : '🛡'
|
|
4230
|
+
|
|
4231
|
+
app.innerHTML = shell(loading$(), returnNav)
|
|
4232
|
+
const data = await GET('/' + role + '/eligibility')
|
|
4233
|
+
if (data.error) { app.innerHTML = shell(alert$('error', data.error), returnNav); return }
|
|
4234
|
+
|
|
4201
4235
|
const checklist = (data.items || []).map(i => `
|
|
4202
4236
|
<div style="display:flex;justify-content:space-between;padding:8px 0;border-bottom:1px solid #f3f4f6;font-size:13px">
|
|
4203
4237
|
<span style="${i.ok ? 'color:#16a34a' : 'color:#dc2626'}">${i.ok ? '✓' : '✗'} ${i.label}</span>
|
|
4204
4238
|
<span style="color:#6b7280">${i.current} / ${i.required}</span>
|
|
4205
4239
|
</div>`).join('')
|
|
4206
4240
|
const canApply = data.eligible
|
|
4241
|
+
const pageLoadedAt = Date.now()
|
|
4242
|
+
|
|
4207
4243
|
app.innerHTML = shell(`
|
|
4208
|
-
<h1 class="page-title"
|
|
4244
|
+
<h1 class="page-title">${roleIcon} ${roleTitle}</h1>
|
|
4245
|
+
|
|
4246
|
+
<div class="card" style="margin-bottom:16px;background:#fef2f2;border-color:#fca5a5">
|
|
4247
|
+
<div style="font-weight:600;color:#991b1b;margin-bottom:6px;font-size:14px">⚠️ ${t('本流程是治理岗位申请,不是赚钱机会')}</div>
|
|
4248
|
+
<div style="font-size:12px;color:#7f1d1d;line-height:1.6">
|
|
4249
|
+
${t('phase A 不发放任何现金 / WAZ 报酬。治理是公共贡献。详 docs/GOVERNANCE-ONBOARDING.md。')}<br>
|
|
4250
|
+
<span style="opacity:0.85">This is a governance application, not an income opportunity. Phase A pays NO cash / WAZ.</span>
|
|
4251
|
+
</div>
|
|
4252
|
+
</div>
|
|
4253
|
+
|
|
4209
4254
|
<div class="card" style="margin-bottom:16px">
|
|
4210
|
-
<div style="font-size:13px;color:#6b7280;margin-bottom:10px">${t('
|
|
4255
|
+
<div style="font-size:13px;color:#6b7280;margin-bottom:10px">${t('门槛(全部通过才能申请)')}:</div>
|
|
4211
4256
|
${checklist}
|
|
4212
4257
|
</div>
|
|
4213
|
-
${canApply
|
|
4214
|
-
? `<button class="btn btn-primary" onclick="doApplyVerifier()">${t('提交申请')}</button>`
|
|
4215
|
-
: `<button class="btn btn-primary" disabled style="opacity:0.5">${t('提交申请')}</button>
|
|
4216
|
-
<div style="margin-top:8px;font-size:12px;color:#6b7280">${t('请先完成上述未达标指标')}</div>`}
|
|
4217
|
-
<div id="apply-msg" style="margin-top:12px"></div>
|
|
4218
|
-
<div style="margin-top:16px"><a href="#verify-tasks" style="font-size:13px;color:#6b7280">← ${t('返回审核任务')}</a></div>
|
|
4219
|
-
`, 'verify-tasks')
|
|
4220
|
-
}
|
|
4221
4258
|
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4259
|
+
${!canApply ? `
|
|
4260
|
+
<div class="card" style="background:#fff7ed;border-color:#fdba74;padding:12px;font-size:13px;color:#7c2d12">
|
|
4261
|
+
${t('请先完成上述未达标指标,完成后再来申请')}
|
|
4262
|
+
</div>
|
|
4263
|
+
<div style="margin-top:16px"><a href="#${returnNav}" style="font-size:13px;color:#6b7280">← ${t('返回')}</a></div>
|
|
4264
|
+
` : `
|
|
4265
|
+
<div class="card" style="margin-bottom:16px">
|
|
4266
|
+
<div style="font-size:13px;color:#374151;margin-bottom:12px;font-weight:600">${t('知情同意(双勾选)')}</div>
|
|
4267
|
+
<label style="display:flex;align-items:flex-start;gap:8px;padding:6px 0;cursor:pointer">
|
|
4268
|
+
<input type="checkbox" id="gov-consent-1" onchange="updateGovApplyState()">
|
|
4269
|
+
<span style="font-size:13px;color:#374151">${t('我已阅读 GOVERNANCE-ONBOARDING / ARBITRATION-PLAYBOOK / META-RULES / CHARTER,理解角色责任')}</span>
|
|
4270
|
+
</label>
|
|
4271
|
+
<label id="gov-consent-2-wrap" style="display:flex;align-items:flex-start;gap:8px;padding:6px 0;cursor:pointer;opacity:0.5">
|
|
4272
|
+
<input type="checkbox" id="gov-consent-2" disabled onchange="updateGovApplyState()">
|
|
4273
|
+
<span style="font-size:13px;color:#374151">${t('我自愿申请,无人收买 / 诱导')} <span id="gov-countdown" style="color:#dc2626;font-weight:600"></span></span>
|
|
4274
|
+
</label>
|
|
4275
|
+
</div>
|
|
4276
|
+
|
|
4277
|
+
<button class="btn btn-primary" id="gov-submit-btn" onclick="doSubmitGovernanceApply('${role}', ${pageLoadedAt})" disabled style="opacity:0.5">
|
|
4278
|
+
🔑 ${t('用 Passkey 签发提交申请')}
|
|
4279
|
+
</button>
|
|
4280
|
+
|
|
4281
|
+
<div id="gov-apply-msg" style="margin-top:12px"></div>
|
|
4282
|
+
<div style="margin-top:16px"><a href="#${returnNav}" style="font-size:13px;color:#6b7280">← ${t('返回')}</a></div>
|
|
4283
|
+
`}
|
|
4284
|
+
`, returnNav)
|
|
4285
|
+
|
|
4286
|
+
if (canApply) {
|
|
4287
|
+
let secondsLeft = 8
|
|
4288
|
+
const tick = () => {
|
|
4289
|
+
const el = document.getElementById('gov-countdown')
|
|
4290
|
+
if (!el) return
|
|
4291
|
+
if (secondsLeft > 0) {
|
|
4292
|
+
el.textContent = '(' + t('等') + ' ' + secondsLeft + 's)'
|
|
4293
|
+
secondsLeft--
|
|
4294
|
+
setTimeout(tick, 1000)
|
|
4295
|
+
} else {
|
|
4296
|
+
el.textContent = ''
|
|
4297
|
+
const cb2 = document.getElementById('gov-consent-2')
|
|
4298
|
+
if (cb2) cb2.disabled = false
|
|
4299
|
+
const wrap = document.getElementById('gov-consent-2-wrap')
|
|
4300
|
+
if (wrap) wrap.style.opacity = '1'
|
|
4301
|
+
}
|
|
4302
|
+
}
|
|
4303
|
+
tick()
|
|
4304
|
+
}
|
|
4229
4305
|
}
|
|
4230
4306
|
|
|
4231
|
-
|
|
4232
|
-
async function renderApplyArbitrator(app) {
|
|
4307
|
+
async function renderApplyRewards(app) {
|
|
4233
4308
|
if (!state.user) { renderLogin(); return }
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
const
|
|
4309
|
+
const returnNav = 'rewards-me'
|
|
4310
|
+
|
|
4311
|
+
app.innerHTML = shell(loading$(), returnNav)
|
|
4312
|
+
const status = await GET('/rewards/status')
|
|
4313
|
+
if (status.error) { app.innerHTML = shell(alert$('error', status.error), returnNav); return }
|
|
4314
|
+
|
|
4315
|
+
if (status.opted_in) {
|
|
4316
|
+
app.innerHTML = shell(`
|
|
4317
|
+
<h1 class="page-title">${t('已是共建身份')}</h1>
|
|
4318
|
+
<div class="card" style="background:#ecfdf5;border-color:#86efac;padding:12px;font-size:13px;color:#064e3b">
|
|
4319
|
+
${t('你已 opted-in。如需查看状态或退出,前往 ')}
|
|
4320
|
+
<a href="#rewards-me" style="color:#16a34a;font-weight:600">${t('共建身份管理')}</a>
|
|
4321
|
+
</div>
|
|
4322
|
+
`, returnNav)
|
|
4323
|
+
return
|
|
4324
|
+
}
|
|
4325
|
+
|
|
4326
|
+
const eli = status.eligibility || {}
|
|
4327
|
+
const canApply = !!eli.can_apply
|
|
4328
|
+
const consentText = status.consent_text_zh || ''
|
|
4329
|
+
const consentTextEn = status.consent_text_en || ''
|
|
4330
|
+
const consentVersion = status.consent_version || ''
|
|
4331
|
+
const delaySec = Number(eli.consent_delay_seconds || 8)
|
|
4332
|
+
const pageLoadedAt = Date.now()
|
|
4333
|
+
|
|
4334
|
+
const checklist = `
|
|
4238
4335
|
<div style="display:flex;justify-content:space-between;padding:8px 0;border-bottom:1px solid #f3f4f6;font-size:13px">
|
|
4239
|
-
<span style="${
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4336
|
+
<span style="${eli.completed_orders >= eli.min_completed_orders ? 'color:#16a34a' : 'color:#dc2626'}">
|
|
4337
|
+
${eli.completed_orders >= eli.min_completed_orders ? '✓' : '✗'} ${t('已完成订单')}
|
|
4338
|
+
</span>
|
|
4339
|
+
<span style="color:#6b7280">${eli.completed_orders} / ${eli.min_completed_orders}</span>
|
|
4340
|
+
</div>
|
|
4341
|
+
<div style="display:flex;justify-content:space-between;padding:8px 0;border-bottom:1px solid #f3f4f6;font-size:13px">
|
|
4342
|
+
<span style="${!eli.require_passkey || eli.passkey_count > 0 ? 'color:#16a34a' : 'color:#dc2626'}">
|
|
4343
|
+
${!eli.require_passkey || eli.passkey_count > 0 ? '✓' : '✗'} ${t('Passkey 已注册')}
|
|
4344
|
+
</span>
|
|
4345
|
+
<span style="color:#6b7280">${eli.passkey_count} ${eli.require_passkey ? `(${t('必需')})` : `(${t('可选')})`}</span>
|
|
4346
|
+
</div>`
|
|
4347
|
+
|
|
4243
4348
|
app.innerHTML = shell(`
|
|
4244
|
-
<h1 class="page-title"
|
|
4349
|
+
<h1 class="page-title">🎁 ${t('申请共建身份')}</h1>
|
|
4350
|
+
|
|
4351
|
+
<div class="card" style="margin-bottom:16px;background:#fef2f2;border-color:#fca5a5">
|
|
4352
|
+
<div style="font-weight:600;color:#991b1b;margin-bottom:6px;font-size:14px">⚠️ ${t('本流程与购物无关')}</div>
|
|
4353
|
+
<div style="font-size:12px;color:#7f1d1d;line-height:1.6">
|
|
4354
|
+
${t('你可以随时退出,不影响任何已下单或未来订单。本流程涉及经济关系登记(三级佣金 + 双轨配对),请仔细阅读全部条款。')}<br>
|
|
4355
|
+
<span style="opacity:0.85">This flow is not part of shopping. You can leave anytime without affecting any orders. This is an economic-relationship registration — please read all terms.</span>
|
|
4356
|
+
</div>
|
|
4357
|
+
</div>
|
|
4358
|
+
|
|
4245
4359
|
<div class="card" style="margin-bottom:16px">
|
|
4246
|
-
<div style="font-size:13px;color:#6b7280;margin-bottom:10px">${t('
|
|
4360
|
+
<div style="font-size:13px;color:#6b7280;margin-bottom:10px">${t('门槛(全部通过才能申请)')}:</div>
|
|
4247
4361
|
${checklist}
|
|
4248
4362
|
</div>
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4363
|
+
|
|
4364
|
+
<div class="card" style="margin-bottom:16px;background:#fafafa">
|
|
4365
|
+
<div style="font-size:12px;color:#6b7280;margin-bottom:6px">${t('同意文本')} (v${consentVersion}):</div>
|
|
4366
|
+
<div style="font-size:13px;color:#374151;line-height:1.7;margin-bottom:8px">${escHtml(consentText)}</div>
|
|
4367
|
+
<div style="font-size:12px;color:#6b7280;line-height:1.7;font-style:italic">${escHtml(consentTextEn)}</div>
|
|
4368
|
+
</div>
|
|
4369
|
+
|
|
4370
|
+
${!canApply ? `
|
|
4371
|
+
<div class="card" style="background:#fff7ed;border-color:#fdba74;padding:12px;font-size:13px;color:#7c2d12">
|
|
4372
|
+
${t('请先完成上述未达标指标,完成后再来申请')}
|
|
4373
|
+
</div>
|
|
4374
|
+
<div style="margin-top:16px"><a href="#${returnNav}" style="font-size:13px;color:#6b7280">← ${t('返回')}</a></div>
|
|
4375
|
+
` : `
|
|
4376
|
+
<div class="card" style="margin-bottom:16px">
|
|
4377
|
+
<div style="font-size:13px;color:#374151;margin-bottom:12px;font-weight:600">${t('知情同意(双勾选)')}</div>
|
|
4378
|
+
<label style="display:flex;align-items:flex-start;gap:8px;padding:6px 0;cursor:pointer">
|
|
4379
|
+
<input type="checkbox" id="rwd-consent-1" onchange="updateRwdApplyState()">
|
|
4380
|
+
<span style="font-size:13px;color:#374151">${t('我已阅读全部条款,理解 commission 与 PV 树结构的合规边界')}</span>
|
|
4381
|
+
</label>
|
|
4382
|
+
<label id="rwd-consent-2-wrap" style="display:flex;align-items:flex-start;gap:8px;padding:6px 0;cursor:pointer;opacity:0.5">
|
|
4383
|
+
<input type="checkbox" id="rwd-consent-2" disabled onchange="updateRwdApplyState()">
|
|
4384
|
+
<span style="font-size:13px;color:#374151">${t('我自愿申请,无人收买 / 诱导')} <span id="rwd-countdown" style="color:#dc2626;font-weight:600"></span></span>
|
|
4385
|
+
</label>
|
|
4386
|
+
</div>
|
|
4387
|
+
|
|
4388
|
+
<button class="btn btn-primary" id="rwd-submit-btn" onclick="doSubmitRewardsApply('${consentVersion}', ${pageLoadedAt})" disabled style="opacity:0.5">
|
|
4389
|
+
🔑 ${t('用 Passkey 签发提交申请')}
|
|
4390
|
+
</button>
|
|
4391
|
+
|
|
4392
|
+
<div id="rwd-apply-msg" style="margin-top:12px"></div>
|
|
4393
|
+
<div style="margin-top:16px"><a href="#${returnNav}" style="font-size:13px;color:#6b7280">← ${t('返回')}</a></div>
|
|
4394
|
+
`}
|
|
4395
|
+
`, returnNav)
|
|
4396
|
+
|
|
4397
|
+
if (canApply) {
|
|
4398
|
+
let secondsLeft = delaySec
|
|
4399
|
+
const tick = () => {
|
|
4400
|
+
const el = document.getElementById('rwd-countdown')
|
|
4401
|
+
if (!el) return
|
|
4402
|
+
if (secondsLeft > 0) {
|
|
4403
|
+
el.textContent = '(' + t('等') + ' ' + secondsLeft + 's)'
|
|
4404
|
+
secondsLeft--
|
|
4405
|
+
setTimeout(tick, 1000)
|
|
4406
|
+
} else {
|
|
4407
|
+
el.textContent = ''
|
|
4408
|
+
const cb2 = document.getElementById('rwd-consent-2')
|
|
4409
|
+
if (cb2) cb2.disabled = false
|
|
4410
|
+
const wrap = document.getElementById('rwd-consent-2-wrap')
|
|
4411
|
+
if (wrap) wrap.style.opacity = '1'
|
|
4412
|
+
}
|
|
4413
|
+
}
|
|
4414
|
+
tick()
|
|
4415
|
+
}
|
|
4416
|
+
}
|
|
4417
|
+
|
|
4418
|
+
window.updateRwdApplyState = () => {
|
|
4419
|
+
const cb1 = document.getElementById('rwd-consent-1')
|
|
4420
|
+
const cb2 = document.getElementById('rwd-consent-2')
|
|
4421
|
+
const btn = document.getElementById('rwd-submit-btn')
|
|
4422
|
+
if (cb1 && cb2 && btn) {
|
|
4423
|
+
const ok = cb1.checked && cb2.checked && !cb2.disabled
|
|
4424
|
+
btn.disabled = !ok
|
|
4425
|
+
btn.style.opacity = ok ? '1' : '0.5'
|
|
4426
|
+
}
|
|
4427
|
+
}
|
|
4428
|
+
|
|
4429
|
+
window.doSubmitRewardsApply = async (consentVersion, pageLoadedAt) => {
|
|
4430
|
+
const msg = document.getElementById('rwd-apply-msg')
|
|
4431
|
+
if (!msg) return
|
|
4432
|
+
|
|
4433
|
+
// Reconstruct consent_hash matching server's expectedApplyConsentHash() exactly
|
|
4434
|
+
const consentText = 'rewards_apply|consent_version=' + consentVersion + '|user=' + state.user.id + '|page_loaded_at=' + pageLoadedAt
|
|
4435
|
+
const encoder = new TextEncoder()
|
|
4436
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', encoder.encode(consentText))
|
|
4437
|
+
const consentHash = Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2,'0')).join('')
|
|
4438
|
+
|
|
4439
|
+
msg.innerHTML = `<div class="alert alert-info"><span class="spinner"></span>${t('请通过 Passkey / 生物识别确认...')}</div>`
|
|
4440
|
+
let passkeyToken
|
|
4441
|
+
try {
|
|
4442
|
+
passkeyToken = await requestPasskeyGate('rewards_apply', { consent_version: consentVersion, consent_hash: consentHash })
|
|
4443
|
+
} catch (e) {
|
|
4444
|
+
msg.innerHTML = alert$('error', t('Passkey 签发失败:') + ' ' + (e?.message || e))
|
|
4445
|
+
return
|
|
4446
|
+
}
|
|
4447
|
+
|
|
4448
|
+
msg.innerHTML = `<div class="alert alert-info"><span class="spinner"></span>${t('提交中...')}</div>`
|
|
4449
|
+
const res = await POST('/rewards/apply', {
|
|
4450
|
+
consent_version: consentVersion,
|
|
4451
|
+
consent_hash: consentHash,
|
|
4452
|
+
page_loaded_at: pageLoadedAt,
|
|
4453
|
+
webauthn_token: passkeyToken,
|
|
4454
|
+
})
|
|
4455
|
+
|
|
4456
|
+
if (res.error) {
|
|
4457
|
+
msg.innerHTML = alert$('error', res.error)
|
|
4458
|
+
return
|
|
4459
|
+
}
|
|
4460
|
+
|
|
4461
|
+
const drained = res.drained_from_escrow || { count: 0, total: 0 }
|
|
4462
|
+
const drainNote = drained.count > 0
|
|
4463
|
+
? '<br>' + t('已从 escrow 拨回:') + ' ' + drained.total + ' WAZ (' + drained.count + ' ' + t('笔') + ')'
|
|
4464
|
+
: ''
|
|
4465
|
+
msg.innerHTML = alert$('success', t('✅ 共建身份激活成功') + drainNote)
|
|
4466
|
+
setTimeout(() => { location.hash = '#rewards-me' }, 2000)
|
|
4467
|
+
}
|
|
4468
|
+
|
|
4469
|
+
async function renderRewardsMe(app) {
|
|
4470
|
+
if (!state.user) { renderLogin(); return }
|
|
4471
|
+
app.innerHTML = shell(loading$(), 'me')
|
|
4472
|
+
const status = await GET('/rewards/status')
|
|
4473
|
+
if (status.error) { app.innerHTML = shell(alert$('error', status.error), 'me'); return }
|
|
4474
|
+
|
|
4475
|
+
const stateLabel = {
|
|
4476
|
+
opted_in: { icon: '✅', color: '#16a34a', label: t('共建身份激活中') },
|
|
4477
|
+
never_activated: { icon: '⚪', color: '#6b7280', label: t('未激活') },
|
|
4478
|
+
auto_downgraded: { icon: '⚠️', color: '#f59e0b', label: t('已自动降级(consent 未确认)') },
|
|
4479
|
+
deactivated: { icon: '🔒', color: '#9ca3af', label: t('已退出') },
|
|
4480
|
+
}[status.state] || { icon: '?', color: '#6b7280', label: status.state }
|
|
4481
|
+
|
|
4482
|
+
const escrow = status.pending_escrow || { count: 0, total_amount: 0 }
|
|
4483
|
+
const expired = status.expired_to_charity || { count: 0, total_amount: 0 }
|
|
4484
|
+
|
|
4485
|
+
app.innerHTML = shell(`
|
|
4486
|
+
<h1 class="page-title">🎁 ${t('共建身份管理')}</h1>
|
|
4487
|
+
|
|
4488
|
+
<div class="card" style="margin-bottom:16px;border-left:4px solid ${stateLabel.color};padding:14px">
|
|
4489
|
+
<div style="display:flex;align-items:center;gap:10px;margin-bottom:6px">
|
|
4490
|
+
<span style="font-size:28px">${stateLabel.icon}</span>
|
|
4491
|
+
<span style="font-size:16px;font-weight:600;color:${stateLabel.color}">${stateLabel.label}</span>
|
|
4492
|
+
</div>
|
|
4493
|
+
${status.consent_version ? `<div style="font-size:12px;color:#9ca3af">${t('Consent version')}: v${status.consent_version}</div>` : ''}
|
|
4494
|
+
</div>
|
|
4495
|
+
|
|
4496
|
+
${escrow.count > 0 ? `
|
|
4497
|
+
<div class="card" style="margin-bottom:16px;background:#fef3c7;border-color:#fde68a;padding:12px">
|
|
4498
|
+
<div style="font-weight:600;color:#92400e;font-size:13px;margin-bottom:4px">💰 ${t('待领 escrow')}</div>
|
|
4499
|
+
<div style="font-size:13px;color:#7c2d12">${escrow.count} ${t('笔')} · ${escrow.total_amount} WAZ</div>
|
|
4500
|
+
<div style="font-size:11px;color:#a16207;margin-top:6px">${t('opt-in 后这笔钱会拨回钱包。30 天未领过期入公益。')}</div>
|
|
4501
|
+
</div>
|
|
4502
|
+
` : ''}
|
|
4503
|
+
|
|
4504
|
+
${expired.count > 0 ? `
|
|
4505
|
+
<div class="card" style="margin-bottom:16px;background:#f3f4f6;padding:10px;font-size:12px;color:#6b7280">
|
|
4506
|
+
${t('历史过期入公益')}: ${expired.count} ${t('笔')} · ${expired.total_amount} WAZ
|
|
4507
|
+
</div>
|
|
4508
|
+
` : ''}
|
|
4509
|
+
|
|
4510
|
+
${status.state === 'opted_in' ? `
|
|
4511
|
+
<div class="card" style="margin-bottom:16px;padding:14px">
|
|
4512
|
+
<div style="font-size:13px;color:#374151;margin-bottom:10px">${t('退出共建身份后,未来 commission 直接入公益(无 escrow)。可随时再申请。')}</div>
|
|
4513
|
+
<button class="btn" style="background:#fee2e2;color:#991b1b;border:1px solid #fca5a5" onclick="doDeactivateRewards()">
|
|
4514
|
+
🔒 ${t('退出共建身份')}
|
|
4515
|
+
</button>
|
|
4516
|
+
<div id="rwd-deact-msg" style="margin-top:10px"></div>
|
|
4517
|
+
</div>
|
|
4518
|
+
` : `
|
|
4519
|
+
<button class="btn btn-primary" onclick="location.hash='#apply-rewards'">
|
|
4520
|
+
🎁 ${status.state === 'never_activated' ? t('申请共建身份') : t('重新申请')}
|
|
4521
|
+
</button>
|
|
4522
|
+
`}
|
|
4523
|
+
|
|
4524
|
+
<div style="margin-top:20px;font-size:11px;color:#9ca3af;line-height:1.6">
|
|
4525
|
+
${t('协议依据:')} <a href="https://github.com/seasonsagents-art/webaz/blob/main/docs/rfcs/RFC-002-rewards-opt-in.md" target="_blank" style="color:#6b7280">RFC-002</a>
|
|
4526
|
+
</div>
|
|
4527
|
+
`, 'me')
|
|
4528
|
+
}
|
|
4529
|
+
|
|
4530
|
+
window.doDeactivateRewards = async () => {
|
|
4531
|
+
const msg = document.getElementById('rwd-deact-msg')
|
|
4532
|
+
if (!msg) return
|
|
4533
|
+
if (!confirm(t('确认退出共建身份?未来 commission 将直接入公益,可随时再申请。'))) return
|
|
4534
|
+
|
|
4535
|
+
msg.innerHTML = `<div class="alert alert-info"><span class="spinner"></span>${t('请通过 Passkey / 生物识别确认...')}</div>`
|
|
4536
|
+
let passkeyToken
|
|
4537
|
+
try {
|
|
4538
|
+
passkeyToken = await requestPasskeyGate('rewards_deactivate', {})
|
|
4539
|
+
} catch (e) {
|
|
4540
|
+
msg.innerHTML = alert$('error', t('Passkey 签发失败:') + ' ' + (e?.message || e))
|
|
4541
|
+
return
|
|
4542
|
+
}
|
|
4543
|
+
|
|
4544
|
+
msg.innerHTML = `<div class="alert alert-info"><span class="spinner"></span>${t('提交中...')}</div>`
|
|
4545
|
+
const res = await POST('/rewards/deactivate', { webauthn_token: passkeyToken })
|
|
4546
|
+
if (res.error) {
|
|
4547
|
+
msg.innerHTML = alert$('error', res.error)
|
|
4548
|
+
return
|
|
4549
|
+
}
|
|
4550
|
+
msg.innerHTML = alert$('success', t('✅ 已退出共建身份'))
|
|
4551
|
+
setTimeout(() => renderRewardsMe(document.getElementById('app')), 1200)
|
|
4552
|
+
}
|
|
4553
|
+
|
|
4554
|
+
async function renderApplyVerifier(app) { return _renderApplyGovernance(app, 'verifier') }
|
|
4555
|
+
async function renderApplyArbitrator(app) { return _renderApplyGovernance(app, 'arbitrator') }
|
|
4556
|
+
|
|
4557
|
+
// ─── Onboarding 学习包 + 题目(spec §4.1 + §4.3,#1093 阶段 2a)─────
|
|
4558
|
+
// 案例研读(§4.2)留下一个 PR
|
|
4559
|
+
async function renderOnboarding(app, role) {
|
|
4560
|
+
if (!state.user) { renderLogin(); return }
|
|
4561
|
+
const returnNav = role === 'arbitrator' ? 'me' : 'verify-tasks'
|
|
4562
|
+
const roleTitle = role === 'arbitrator' ? t('仲裁员上岗 onboarding') : t('审核员上岗 onboarding')
|
|
4563
|
+
const roleIcon = role === 'arbitrator' ? '⚖' : '🛡'
|
|
4564
|
+
|
|
4565
|
+
app.innerHTML = shell(loading$(), returnNav)
|
|
4566
|
+
|
|
4567
|
+
// 同时加载 quiz 题库 + 案例库 + 当前 progress
|
|
4568
|
+
const [quizData, casesData, progress] = await Promise.all([
|
|
4569
|
+
GET('/governance/onboarding/quiz?role=' + role),
|
|
4570
|
+
GET('/governance/onboarding/cases?role=' + role),
|
|
4571
|
+
GET('/governance/onboarding/progress'),
|
|
4572
|
+
])
|
|
4573
|
+
|
|
4574
|
+
if (quizData.error) { app.innerHTML = shell(alert$('error', quizData.error), returnNav); return }
|
|
4575
|
+
if (casesData.error) { app.innerHTML = shell(alert$('error', casesData.error), returnNav); return }
|
|
4576
|
+
|
|
4577
|
+
const currentApp = (progress.applications || []).find(a => a.role === role && a.status === 'pending_onboarding')
|
|
4578
|
+
if (!currentApp) {
|
|
4579
|
+
const roleLabel = role === 'arbitrator' ? t('仲裁员') : t('审核员')
|
|
4580
|
+
app.innerHTML = shell(`
|
|
4581
|
+
<h1 class="page-title">${roleIcon} ${roleTitle}</h1>
|
|
4582
|
+
<div class="card" style="background:#fff7ed;border-color:#fdba74">
|
|
4583
|
+
<div style="font-weight:600;color:#7c2d12">${t('请先提交申请')} (${roleLabel})</div>
|
|
4584
|
+
<div style="font-size:13px;color:#7c2d12;margin-top:8px"><a href="#apply-${role}">#apply-${role}</a> ${t('完成申请')}</div>
|
|
4585
|
+
</div>
|
|
4586
|
+
<div style="margin-top:16px"><a href="#${returnNav}" style="font-size:13px;color:#6b7280">← ${t('返回')}</a></div>
|
|
4587
|
+
`, returnNav)
|
|
4588
|
+
return
|
|
4589
|
+
}
|
|
4590
|
+
|
|
4591
|
+
const studyDocs = [
|
|
4592
|
+
{ name: 'META-RULES-FULL.md', desc: t('10 元规则,特别是 #5 不偏袒 / #6 不滥用'), link: 'https://github.com/seasonsagents-art/webaz/blob/main/docs/META-RULES-FULL.md' },
|
|
4593
|
+
{ name: 'CHARTER.md §3.2 + §6', desc: t('权力边界:多签 + 修改流程'), link: 'https://github.com/seasonsagents-art/webaz/blob/main/docs/CHARTER.md' },
|
|
4594
|
+
{ name: 'SECURITY.md §Iron-Rule', desc: t('真人 Passkey 7 条路径'), link: 'https://github.com/seasonsagents-art/webaz/blob/main/SECURITY.md' },
|
|
4595
|
+
{ name: 'ECONOMIC-MODEL.md §11', desc: t('经济博弈原则 + 关系层估值层'), link: 'https://github.com/seasonsagents-art/webaz/blob/main/docs/ECONOMIC-MODEL.md' },
|
|
4596
|
+
{ name: 'ARBITRATION-PLAYBOOK.md', desc: t('案例决策树 + 4 种结算路径') + (role === 'arbitrator' ? ' (' + t('必读') + ')' : ''), link: 'https://github.com/seasonsagents-art/webaz/blob/main/docs/ARBITRATION-PLAYBOOK.md' },
|
|
4597
|
+
{ name: 'MLM-COMPLIANCE.md', desc: t('合规边界'), link: 'https://github.com/seasonsagents-art/webaz/blob/main/docs/MLM-COMPLIANCE.md' },
|
|
4598
|
+
]
|
|
4599
|
+
|
|
4600
|
+
const studySection = studyDocs.map((d, i) => `
|
|
4601
|
+
<label style="display:flex;align-items:flex-start;gap:8px;padding:8px 0;border-bottom:1px solid #f3f4f6;cursor:pointer">
|
|
4602
|
+
<input type="checkbox" class="study-cb" data-idx="${i}" onchange="updateOnboardingState()">
|
|
4603
|
+
<div style="flex:1">
|
|
4604
|
+
<a href="${d.link}" target="_blank" rel="noopener" style="font-weight:600;color:#1d4ed8;font-size:13px;text-decoration:none">${d.name} ↗</a>
|
|
4605
|
+
<div style="font-size:11px;color:#6b7280;margin-top:2px">${d.desc}</div>
|
|
4606
|
+
</div>
|
|
4607
|
+
</label>`).join('')
|
|
4608
|
+
|
|
4609
|
+
const quizSection = (quizData.questions || []).map((q, idx) => {
|
|
4610
|
+
if (q.type === 'multiple_choice') {
|
|
4611
|
+
const opts = (q.options || []).map(opt => `
|
|
4612
|
+
<label style="display:flex;align-items:flex-start;gap:8px;padding:6px 0;cursor:pointer">
|
|
4613
|
+
<input type="radio" name="${q.id}" value="${opt.key}" onchange="updateOnboardingState()">
|
|
4614
|
+
<span style="font-size:13px;color:#374151">${opt.key}. ${state._lang === 'en' ? opt.text_en : opt.text_zh}</span>
|
|
4615
|
+
</label>`).join('')
|
|
4616
|
+
return `
|
|
4617
|
+
<div class="card" style="margin-bottom:12px">
|
|
4618
|
+
<div style="font-size:13px;font-weight:600;color:#374151;margin-bottom:8px">${idx + 1}. ${state._lang === 'en' ? q.question_en : q.question_zh}</div>
|
|
4619
|
+
${opts}
|
|
4620
|
+
</div>`
|
|
4621
|
+
} else {
|
|
4622
|
+
return `
|
|
4623
|
+
<div class="card" style="margin-bottom:12px">
|
|
4624
|
+
<div style="font-size:13px;font-weight:600;color:#374151;margin-bottom:8px">${idx + 1}. ${state._lang === 'en' ? q.question_en : q.question_zh}</div>
|
|
4625
|
+
<textarea class="form-control short-answer" data-qid="${q.id}" rows="4" placeholder="${t('至少 N 字符').replace('N', String(q.min_chars || 50))}" oninput="updateOnboardingState()" style="font-size:13px"></textarea>
|
|
4626
|
+
</div>`
|
|
4627
|
+
}
|
|
4628
|
+
}).join('')
|
|
4629
|
+
|
|
4630
|
+
const lastScore = currentApp.quiz_score
|
|
4631
|
+
const scoreBanner = lastScore != null ? `
|
|
4632
|
+
<div class="card" style="margin-bottom:16px;background:${lastScore >= 80 ? '#f0fdf4;border-color:#86efac' : '#fff7ed;border-color:#fdba74'}">
|
|
4633
|
+
<div style="font-weight:600">${t('上次得分')}: ${lastScore}% ${lastScore >= 80 ? '✅ ' + t('已合格,等待 maintainer 激活') : '⚠️ ' + t('未合格,可重试')}</div>
|
|
4634
|
+
</div>` : ''
|
|
4635
|
+
|
|
4636
|
+
app.innerHTML = shell(`
|
|
4637
|
+
<h1 class="page-title">${roleIcon} ${roleTitle}</h1>
|
|
4638
|
+
|
|
4639
|
+
${scoreBanner}
|
|
4640
|
+
|
|
4641
|
+
<div class="card" style="margin-bottom:16px;background:#eff6ff;border-color:#93c5fd">
|
|
4642
|
+
<div style="font-weight:600;font-size:13px;color:#1e3a8a;margin-bottom:6px">📚 ${t('§4.1 学习包')}</div>
|
|
4643
|
+
<div style="font-size:12px;color:#1e40af;margin-bottom:10px">${t('阅读以下文档,勾选已读。本步骤不写入 DB(仅本地标记),实际理解由题目和案例考察。')}</div>
|
|
4644
|
+
${studySection}
|
|
4645
|
+
</div>
|
|
4646
|
+
|
|
4647
|
+
<div class="card" style="margin-bottom:16px;background:#f5f3ff;border-color:#c4b5fd">
|
|
4648
|
+
<div style="font-weight:600;font-size:13px;color:#5b21b6;margin-bottom:6px">📝 ${t('§4.3 题目')}</div>
|
|
4649
|
+
<div style="font-size:12px;color:#6b21a8;margin-bottom:10px">
|
|
4650
|
+
${(quizData.questions || []).length} ${t('题(多选 + 短答),合格线 80%')}(${quizData.questions ? Math.ceil(quizData.questions.length * 0.8) : 12}/${quizData.questions ? quizData.questions.length : 15})
|
|
4651
|
+
</div>
|
|
4652
|
+
${quizSection}
|
|
4653
|
+
</div>
|
|
4654
|
+
|
|
4655
|
+
<button class="btn btn-primary" id="onb-submit-btn" onclick="doSubmitOnboarding('${role}')" disabled style="opacity:0.5">
|
|
4656
|
+
${t('提交题目并评分')}
|
|
4657
|
+
</button>
|
|
4658
|
+
|
|
4659
|
+
<div id="onb-msg" style="margin-top:12px"></div>
|
|
4660
|
+
|
|
4661
|
+
<!-- §4.2 案例研读(spec §4.2,task #1093 阶段 2b) -->
|
|
4662
|
+
<div class="card" style="margin-top:24px;margin-bottom:16px;background:#ecfdf5;border-color:#86efac">
|
|
4663
|
+
<div style="font-weight:600;font-size:13px;color:#065f46;margin-bottom:6px">📚 ${t('§4.2 案例研读')}</div>
|
|
4664
|
+
<div style="font-size:12px;color:#047857;margin-bottom:10px">
|
|
4665
|
+
${casesData.total} ${t('个案例,每个需选 verdict + 写理由 ≥ 200 字。本步骤')}<strong>${t('不自动评分')}</strong>${t(',maintainer 上岗签字前对比 expected verdict 评估你的 reasoning 方向。')}
|
|
4666
|
+
</div>
|
|
4667
|
+
${(casesData.cases || []).map((c, idx) => `
|
|
4668
|
+
<details style="margin-bottom:12px;background:white;padding:10px;border-radius:6px;border:1px solid #d1fae5" ${idx === 0 ? 'open' : ''}>
|
|
4669
|
+
<summary style="font-weight:600;font-size:13px;cursor:pointer;color:#065f46">${idx + 1}. ${state._lang === 'en' ? c.scenario_en : c.scenario_zh}</summary>
|
|
4670
|
+
<div style="margin-top:10px">
|
|
4671
|
+
<div style="font-size:12px;color:#374151;font-weight:600;margin-bottom:4px">${t('已知事实')}:</div>
|
|
4672
|
+
<ul style="margin:0;padding-left:18px;font-size:12px;color:#4b5563;line-height:1.7">
|
|
4673
|
+
${(state._lang === 'en' ? c.facts_en : c.facts_zh).map(f => `<li>${f}</li>`).join('')}
|
|
4674
|
+
</ul>
|
|
4675
|
+
<div style="font-size:12px;color:#374151;font-weight:600;margin-top:10px;margin-bottom:4px">${t('选择你的 verdict')}:</div>
|
|
4676
|
+
${(c.decision_options || []).map(opt => `
|
|
4677
|
+
<label style="display:flex;align-items:flex-start;gap:8px;padding:4px 0;cursor:pointer">
|
|
4678
|
+
<input type="radio" name="case-${c.id}-verdict" value="${opt.key}" onchange="updateOnboardingState()">
|
|
4679
|
+
<span style="font-size:12px;color:#374151">${opt.key}: ${state._lang === 'en' ? opt.text_en : opt.text_zh}</span>
|
|
4680
|
+
</label>`).join('')}
|
|
4681
|
+
<div style="font-size:12px;color:#374151;font-weight:600;margin-top:10px;margin-bottom:4px">${t('reasoning(≥ N 字符)').replace('N', String(c.min_review_chars))}:</div>
|
|
4682
|
+
<textarea class="form-control case-reasoning" data-case-id="${c.id}" rows="5" placeholder="${t('解释你为什么选这个 verdict,引用具体事实和 spec 原则...')}" oninput="updateOnboardingState()" style="font-size:12px;width:100%"></textarea>
|
|
4683
|
+
</div>
|
|
4684
|
+
</details>`).join('')}
|
|
4685
|
+
</div>
|
|
4686
|
+
|
|
4687
|
+
<button class="btn btn-primary" id="onb-case-submit-btn" onclick="doSubmitCaseReview('${role}')" disabled style="opacity:0.5">
|
|
4688
|
+
${t('提交案例 review')}
|
|
4689
|
+
</button>
|
|
4690
|
+
|
|
4691
|
+
<div id="onb-case-msg" style="margin-top:12px"></div>
|
|
4692
|
+
<div style="margin-top:16px"><a href="#${returnNav}" style="font-size:13px;color:#6b7280">← ${t('返回')}</a></div>
|
|
4693
|
+
`, returnNav)
|
|
4694
|
+
}
|
|
4695
|
+
|
|
4696
|
+
window.updateOnboardingState = () => {
|
|
4697
|
+
const allDocsChecked = Array.from(document.querySelectorAll('.study-cb')).every(cb => cb.checked)
|
|
4698
|
+
const allMcqAnswered = Array.from(document.querySelectorAll('[name^="mcq-"]')).reduce((acc, el) => {
|
|
4699
|
+
const name = el.name
|
|
4700
|
+
if (!acc.has(name)) acc.set(name, false)
|
|
4701
|
+
if (el.checked) acc.set(name, true)
|
|
4702
|
+
return acc
|
|
4703
|
+
}, new Map())
|
|
4704
|
+
const mcqOk = Array.from(allMcqAnswered.values()).every(v => v) && allMcqAnswered.size > 0
|
|
4705
|
+
const allShortAnswered = Array.from(document.querySelectorAll('.short-answer')).every(ta => ta.value.trim().length >= 50)
|
|
4706
|
+
const btn = document.getElementById('onb-submit-btn')
|
|
4707
|
+
if (btn) {
|
|
4708
|
+
const ok = allDocsChecked && mcqOk && allShortAnswered
|
|
4709
|
+
btn.disabled = !ok
|
|
4710
|
+
btn.style.opacity = ok ? '1' : '0.5'
|
|
4711
|
+
}
|
|
4712
|
+
|
|
4713
|
+
// §4.2 案例 submit 独立启用:每个 case 必须选 verdict + reasoning ≥ 200 字
|
|
4714
|
+
const caseVerdictGroups = new Map()
|
|
4715
|
+
document.querySelectorAll('[name^="case-"][type="radio"]').forEach(el => {
|
|
4716
|
+
if (!caseVerdictGroups.has(el.name)) caseVerdictGroups.set(el.name, false)
|
|
4717
|
+
if (el.checked) caseVerdictGroups.set(el.name, true)
|
|
4718
|
+
})
|
|
4719
|
+
const caseVerdictsOk = caseVerdictGroups.size > 0 && Array.from(caseVerdictGroups.values()).every(v => v)
|
|
4720
|
+
const allCaseReasoningOk = Array.from(document.querySelectorAll('.case-reasoning')).every(ta => ta.value.trim().length >= 200)
|
|
4721
|
+
const caseBtn = document.getElementById('onb-case-submit-btn')
|
|
4722
|
+
if (caseBtn) {
|
|
4723
|
+
const ok = caseVerdictsOk && allCaseReasoningOk
|
|
4724
|
+
caseBtn.disabled = !ok
|
|
4725
|
+
caseBtn.style.opacity = ok ? '1' : '0.5'
|
|
4726
|
+
}
|
|
4727
|
+
}
|
|
4728
|
+
|
|
4729
|
+
// ─── /#governance-me 用户治理面板(#1093 阶段 4)─────
|
|
4730
|
+
// spec docs/GOVERNANCE-ONBOARDING.md §6.1 §7.2:active role 卸任 / auto_deactivate 申诉
|
|
4731
|
+
async function renderGovernanceMe(app) {
|
|
4732
|
+
if (!state.user) { renderLogin(); return }
|
|
4733
|
+
app.innerHTML = shell(loading$(), 'me')
|
|
4734
|
+
const data = await GET('/governance/onboarding/my')
|
|
4735
|
+
if (data.error) { app.innerHTML = shell(alert$('error', data.error), 'me'); return }
|
|
4736
|
+
|
|
4737
|
+
const items = data.items || []
|
|
4738
|
+
const now = Math.floor(Date.now() / 1000)
|
|
4739
|
+
|
|
4740
|
+
// active 角色:有 action 在 ('apply' 但 status='active') 或 ('activate' 且 status='active')
|
|
4741
|
+
// 简化:把 status='active' 全列(可能为 apply row 或 activate row)
|
|
4742
|
+
const actives = items.filter(i => i.status === 'active')
|
|
4743
|
+
const pendings = items.filter(i => i.status === 'pending_onboarding')
|
|
4744
|
+
// auto_deactivate 行(可申诉)
|
|
4745
|
+
const autoDeactivates = items.filter(i => i.action === 'auto_deactivate')
|
|
4746
|
+
// pending appeals
|
|
4747
|
+
const pendingAppeals = items.filter(i => i.action === 'appeal' && i.status === 'pending_review')
|
|
4748
|
+
|
|
4749
|
+
const roleNames = {
|
|
4750
|
+
arbitrator: { zh: '仲裁员', icon: '⚖' },
|
|
4751
|
+
verifier: { zh: '审核员', icon: '🛡' },
|
|
4752
|
+
}
|
|
4753
|
+
|
|
4754
|
+
// active 列表(可卸任)— 按 role 去重(可能有 apply + activate 两行,只显示一个)
|
|
4755
|
+
const seenActive = new Set()
|
|
4756
|
+
const activeBlocks = actives.filter(a => {
|
|
4757
|
+
if (seenActive.has(a.role)) return false
|
|
4758
|
+
seenActive.add(a.role)
|
|
4759
|
+
return true
|
|
4760
|
+
}).map(a => {
|
|
4761
|
+
const rn = roleNames[a.role] || { zh: a.role, icon: '·' }
|
|
4762
|
+
return `
|
|
4763
|
+
<div class="card" style="margin-bottom:12px;border-color:#86efac;background:#f0fdf4">
|
|
4764
|
+
<div style="display:flex;justify-content:space-between;align-items:center;gap:10px">
|
|
4765
|
+
<div>
|
|
4766
|
+
<div style="font-weight:600;font-size:15px">${rn.icon} ${t(rn.zh)} <span style="color:#16a34a;font-size:12px;margin-left:6px">● ${t('在岗')}</span></div>
|
|
4767
|
+
<div style="font-size:12px;color:#6b7280;margin-top:4px">${t('上岗时间')}: ${new Date(a.created_at * 1000).toLocaleDateString()}</div>
|
|
4768
|
+
</div>
|
|
4769
|
+
<button class="btn btn-sm btn-outline" style="color:#dc2626;border-color:#fca5a5" onclick="openResignModal('${a.role}')">${t('卸任')}</button>
|
|
4770
|
+
</div>
|
|
4771
|
+
</div>`
|
|
4772
|
+
}).join('')
|
|
4773
|
+
|
|
4774
|
+
// 申诉窗口检查(14d)
|
|
4775
|
+
const windowDays = 14
|
|
4776
|
+
const appealableBlocks = autoDeactivates.filter(a => (now - a.created_at) <= windowDays * 86400).map(a => {
|
|
4777
|
+
const rn = roleNames[a.role] || { zh: a.role, icon: '·' }
|
|
4778
|
+
const daysLeft = windowDays - Math.floor((now - a.created_at) / 86400)
|
|
4779
|
+
// 检查是否已 appeal
|
|
4780
|
+
const hasAppeal = items.some(i => i.action === 'appeal' && i.source_application_id === a.id)
|
|
4781
|
+
return `
|
|
4782
|
+
<div class="card" style="margin-bottom:12px;border-color:#fde68a;background:#fffbeb">
|
|
4783
|
+
<div style="font-weight:600;font-size:14px;color:#92400e">${rn.icon} ${t('被自动卸任')}: ${t(rn.zh)}</div>
|
|
4784
|
+
<div style="font-size:12px;color:#78350f;margin-top:6px">${t('生效时间')}: ${new Date(a.created_at * 1000).toLocaleDateString()} · ${t('申诉窗口剩余')}: ${daysLeft} ${t('天')}</div>
|
|
4785
|
+
${hasAppeal
|
|
4786
|
+
? `<div style="margin-top:10px;font-size:12px;color:#1e40af">${t('已提交申诉,等待 maintainer 裁决')}</div>`
|
|
4787
|
+
: `<button class="btn btn-sm btn-primary" style="margin-top:10px" onclick="openAppealModal('${a.id}', '${a.role}')">${t('提交申诉')}</button>`}
|
|
4788
|
+
</div>`
|
|
4789
|
+
}).join('')
|
|
4790
|
+
|
|
4791
|
+
const pendingAppealBlocks = pendingAppeals.map(a => {
|
|
4792
|
+
const rn = roleNames[a.role] || { zh: a.role, icon: '·' }
|
|
4793
|
+
return `
|
|
4794
|
+
<div class="card" style="margin-bottom:12px;border-color:#93c5fd;background:#eff6ff">
|
|
4795
|
+
<div style="font-weight:600;font-size:14px;color:#1e3a8a">${rn.icon} ${t('申诉审核中')}: ${t(rn.zh)}</div>
|
|
4796
|
+
<div style="font-size:12px;color:#1e40af;margin-top:6px">${t('提交时间')}: ${new Date(a.created_at * 1000).toLocaleDateString()}</div>
|
|
4797
|
+
<div style="font-size:12px;color:#374151;margin-top:8px;padding:8px;background:#fff;border-radius:4px">${escHtml((a.appeal_reason || '').slice(0, 200))}${(a.appeal_reason || '').length > 200 ? '...' : ''}</div>
|
|
4798
|
+
</div>`
|
|
4799
|
+
}).join('')
|
|
4800
|
+
|
|
4801
|
+
const pendingBlocks = pendings.map(p => {
|
|
4802
|
+
const rn = roleNames[p.role] || { zh: p.role, icon: '·' }
|
|
4803
|
+
return `
|
|
4804
|
+
<div class="card" style="margin-bottom:12px">
|
|
4805
|
+
<div style="font-weight:600;font-size:14px">${rn.icon} ${t(rn.zh)} <span style="color:#d97706;font-size:12px;margin-left:6px">${t('申请审核中')}</span></div>
|
|
4806
|
+
<div style="margin-top:8px"><a href="#onboarding-${p.role}" class="btn btn-sm btn-outline">${t('继续 onboarding')}</a></div>
|
|
4807
|
+
</div>`
|
|
4808
|
+
}).join('')
|
|
4809
|
+
|
|
4810
|
+
const emptyState = (actives.length + autoDeactivates.length + pendings.length + pendingAppeals.length) === 0 ? `
|
|
4811
|
+
<div class="card" style="text-align:center;padding:24px">
|
|
4812
|
+
<div style="font-size:14px;color:#6b7280">${t('暂无治理岗位记录')}</div>
|
|
4813
|
+
<div style="margin-top:12px;display:flex;gap:8px;justify-content:center">
|
|
4814
|
+
<a href="#apply-arbitrator" class="btn btn-sm btn-outline">⚖ ${t('申请仲裁员')}</a>
|
|
4815
|
+
<a href="#apply-verifier" class="btn btn-sm btn-outline">🛡 ${t('申请审核员')}</a>
|
|
4816
|
+
</div>
|
|
4817
|
+
</div>` : ''
|
|
4818
|
+
|
|
4819
|
+
app.innerHTML = shell(`
|
|
4820
|
+
<h1 class="page-title">⚖️ ${t('我的治理岗位')}</h1>
|
|
4821
|
+
<div class="card" style="margin-bottom:16px;background:#eff6ff;border-color:#93c5fd">
|
|
4822
|
+
<div style="font-size:13px;color:#1e3a8a">${t('spec docs/GOVERNANCE-ONBOARDING.md §6 §7')}</div>
|
|
4823
|
+
<div style="font-size:12px;color:#1e40af;margin-top:4px">${t('phase A 治理 = 关系层数据采集 + 公共贡献意愿表达,不预设估值层')}</div>
|
|
4824
|
+
</div>
|
|
4825
|
+
${activeBlocks ? `<h2 style="font-size:14px;color:#374151;margin:16px 0 8px">${t('当前在岗')}</h2>${activeBlocks}` : ''}
|
|
4826
|
+
${appealableBlocks ? `<h2 style="font-size:14px;color:#92400e;margin:16px 0 8px">⚠️ ${t('待申诉(14 天内)')}</h2>${appealableBlocks}` : ''}
|
|
4827
|
+
${pendingAppealBlocks ? `<h2 style="font-size:14px;color:#1e40af;margin:16px 0 8px">${t('申诉审核中')}</h2>${pendingAppealBlocks}` : ''}
|
|
4828
|
+
${pendingBlocks ? `<h2 style="font-size:14px;color:#374151;margin:16px 0 8px">${t('待审申请')}</h2>${pendingBlocks}` : ''}
|
|
4829
|
+
${emptyState}
|
|
4830
|
+
<div style="margin-top:16px"><a href="#me" style="font-size:13px;color:#6b7280">← ${t('返回 #me')}</a></div>
|
|
4255
4831
|
`, 'me')
|
|
4256
4832
|
}
|
|
4257
4833
|
|
|
4258
|
-
window.
|
|
4259
|
-
const
|
|
4834
|
+
window.openResignModal = (role) => {
|
|
4835
|
+
const expected = `RESIGN ${role}`
|
|
4836
|
+
const html = `
|
|
4837
|
+
<div id="resign-modal" style="position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:9999;display:flex;align-items:center;justify-content:center;padding:16px">
|
|
4838
|
+
<div style="background:#fff;border-radius:8px;max-width:480px;width:100%;padding:20px">
|
|
4839
|
+
<h3 style="margin:0 0 12px;color:#dc2626">⚠️ ${t('卸任治理岗位')}: ${role}</h3>
|
|
4840
|
+
<div style="font-size:13px;color:#374151;line-height:1.6;margin-bottom:12px">
|
|
4841
|
+
<ul style="padding-left:20px;margin:0">
|
|
4842
|
+
<li>${t('卸任后历史履职记录保留(关系层,不可逆)')}</li>
|
|
4843
|
+
<li>${t('卸任不影响 reputation / dev_contribution')}</li>
|
|
4844
|
+
<li>${t('30 天内不能重新申请同一角色(冷却期)')}</li>
|
|
4845
|
+
<li>${t('已 assigned 但未完成的 case 必须先完成 / 转交,否则无法卸任')}</li>
|
|
4846
|
+
</ul>
|
|
4847
|
+
</div>
|
|
4848
|
+
<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>
|
|
4849
|
+
<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}'">
|
|
4850
|
+
<div id="resign-msg" style="margin-top:12px"></div>
|
|
4851
|
+
<div style="margin-top:16px;display:flex;gap:8px;justify-content:flex-end">
|
|
4852
|
+
<button class="btn btn-sm btn-outline" onclick="document.getElementById('resign-modal').remove()">${t('取消')}</button>
|
|
4853
|
+
<button id="resign-submit-btn" class="btn btn-sm btn-primary" style="background:#dc2626;border-color:#dc2626" disabled onclick="doResignGovernance('${role}')">🔑 ${t('Passkey 签发卸任')}</button>
|
|
4854
|
+
</div>
|
|
4855
|
+
</div>
|
|
4856
|
+
</div>`
|
|
4857
|
+
document.body.insertAdjacentHTML('beforeend', html)
|
|
4858
|
+
}
|
|
4859
|
+
|
|
4860
|
+
window.doResignGovernance = async (role) => {
|
|
4861
|
+
const expected = `RESIGN ${role}`
|
|
4862
|
+
const input = document.getElementById('resign-confirm-input')
|
|
4863
|
+
const msg = document.getElementById('resign-msg')
|
|
4864
|
+
if (!input || input.value !== expected) return
|
|
4865
|
+
|
|
4866
|
+
msg.innerHTML = `<div class="alert alert-info"><span class="spinner"></span>${t('请求 Passkey...')}</div>`
|
|
4867
|
+
|
|
4868
|
+
let webauthnToken
|
|
4869
|
+
try {
|
|
4870
|
+
webauthnToken = await requestPasskeyGate('governance_resign', { role, action: 'resign' })
|
|
4871
|
+
} catch (e) {
|
|
4872
|
+
msg.innerHTML = alert$('error', t('Passkey 签发失败:') + ' ' + (e?.message || e))
|
|
4873
|
+
return
|
|
4874
|
+
}
|
|
4875
|
+
|
|
4260
4876
|
msg.innerHTML = `<div class="alert alert-info"><span class="spinner"></span>${t('提交中...')}</div>`
|
|
4261
|
-
const res = await POST('/
|
|
4877
|
+
const res = await POST('/governance/onboarding/resign', {
|
|
4878
|
+
role,
|
|
4879
|
+
confirm_text: expected,
|
|
4880
|
+
webauthn_token: webauthnToken,
|
|
4881
|
+
})
|
|
4882
|
+
if (res.error) {
|
|
4883
|
+
if (res.code === 'ACTIVE_CASES_EXIST') {
|
|
4884
|
+
// placeholder pattern(避免 t() 拼接)
|
|
4885
|
+
const tpl = t('尚有 {n} 个未结案 dispute,请先完成裁决后再卸任')
|
|
4886
|
+
msg.innerHTML = alert$('error', tpl.replace('{n}', String(res.open_case_count)))
|
|
4887
|
+
} else {
|
|
4888
|
+
msg.innerHTML = alert$('error', res.error)
|
|
4889
|
+
}
|
|
4890
|
+
return
|
|
4891
|
+
}
|
|
4892
|
+
msg.innerHTML = `<div class="alert alert-success">${t('卸任成功。冷却期至')} ${new Date(res.cooldown_until * 1000).toLocaleDateString()}</div>`
|
|
4893
|
+
setTimeout(() => {
|
|
4894
|
+
const modal = document.getElementById('resign-modal')
|
|
4895
|
+
if (modal) modal.remove()
|
|
4896
|
+
renderGovernanceMe(document.getElementById('app'))
|
|
4897
|
+
}, 1500)
|
|
4898
|
+
}
|
|
4899
|
+
|
|
4900
|
+
window.openAppealModal = (sourceAppId, role) => {
|
|
4901
|
+
const html = `
|
|
4902
|
+
<div id="appeal-modal" style="position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:9999;display:flex;align-items:center;justify-content:center;padding:16px">
|
|
4903
|
+
<div style="background:#fff;border-radius:8px;max-width:520px;width:100%;padding:20px">
|
|
4904
|
+
<h3 style="margin:0 0 12px">${t('对自动卸任提出申诉')}: ${role}</h3>
|
|
4905
|
+
<div style="font-size:12px;color:#6b7280;margin-bottom:12px;line-height:1.6">
|
|
4906
|
+
${t('请详细说明:为何 outlier 计数 / COI 警告 / inactive 不应导致卸任。理由至少 100 字符,maintainer 群多签审议,通过后恢复 active 状态。')}
|
|
4907
|
+
</div>
|
|
4908
|
+
<textarea id="appeal-reason-input" rows="6" placeholder="${t('详细说明你的申诉理由(≥100 字符)')}" style="width:100%;padding:8px;border:1px solid #d1d5db;border-radius:4px;font-size:13px;font-family:inherit" oninput="document.getElementById('appeal-char-count').textContent = this.value.length; document.getElementById('appeal-submit-btn').disabled = this.value.length < 100"></textarea>
|
|
4909
|
+
<div style="font-size:12px;color:#6b7280;margin-top:4px">${t('字符数')}: <span id="appeal-char-count">0</span> / 100</div>
|
|
4910
|
+
<div id="appeal-msg" style="margin-top:12px"></div>
|
|
4911
|
+
<div style="margin-top:16px;display:flex;gap:8px;justify-content:flex-end">
|
|
4912
|
+
<button class="btn btn-sm btn-outline" onclick="document.getElementById('appeal-modal').remove()">${t('取消')}</button>
|
|
4913
|
+
<button id="appeal-submit-btn" class="btn btn-sm btn-primary" disabled onclick="doSubmitAppeal('${sourceAppId}')">${t('提交申诉')}</button>
|
|
4914
|
+
</div>
|
|
4915
|
+
</div>
|
|
4916
|
+
</div>`
|
|
4917
|
+
document.body.insertAdjacentHTML('beforeend', html)
|
|
4918
|
+
}
|
|
4919
|
+
|
|
4920
|
+
window.doSubmitAppeal = async (sourceAppId) => {
|
|
4921
|
+
const input = document.getElementById('appeal-reason-input')
|
|
4922
|
+
const msg = document.getElementById('appeal-msg')
|
|
4923
|
+
if (!input || input.value.length < 100) return
|
|
4924
|
+
|
|
4925
|
+
msg.innerHTML = `<div class="alert alert-info"><span class="spinner"></span>${t('提交中...')}</div>`
|
|
4926
|
+
const res = await POST('/governance/onboarding/appeal', {
|
|
4927
|
+
source_application_id: sourceAppId,
|
|
4928
|
+
appeal_reason: input.value.trim(),
|
|
4929
|
+
})
|
|
4930
|
+
if (res.error) { msg.innerHTML = alert$('error', res.error); return }
|
|
4931
|
+
msg.innerHTML = `<div class="alert alert-success">${t('申诉已提交,等待 maintainer 裁决')}</div>`
|
|
4932
|
+
setTimeout(() => {
|
|
4933
|
+
const modal = document.getElementById('appeal-modal')
|
|
4934
|
+
if (modal) modal.remove()
|
|
4935
|
+
renderGovernanceMe(document.getElementById('app'))
|
|
4936
|
+
}, 1500)
|
|
4937
|
+
}
|
|
4938
|
+
|
|
4939
|
+
// ─── /admin/governance(maintainer activation,#1093 阶段 3)─────
|
|
4940
|
+
async function renderAdminGovernance(app) {
|
|
4941
|
+
if (!state.user) { renderLogin(); return }
|
|
4942
|
+
app.innerHTML = shell(loading$(), 'admin')
|
|
4943
|
+
|
|
4944
|
+
// 阶段 4:同时拉 appeals
|
|
4945
|
+
const [data, appealsData] = await Promise.all([
|
|
4946
|
+
GET('/admin/governance/applications'),
|
|
4947
|
+
GET('/admin/governance/appeals'),
|
|
4948
|
+
])
|
|
4949
|
+
if (data.error) { app.innerHTML = shell(alert$('error', data.error), 'admin'); return }
|
|
4950
|
+
|
|
4951
|
+
const items = data.items || []
|
|
4952
|
+
const appeals = (appealsData && !appealsData.error) ? (appealsData.items || []) : []
|
|
4953
|
+
if (items.length === 0 && appeals.length === 0) {
|
|
4954
|
+
app.innerHTML = shell(`
|
|
4955
|
+
<h1 class="page-title">⚖️ ${t('治理岗位激活')}</h1>
|
|
4956
|
+
<div class="card" style="background:#f0fdf4;border-color:#86efac">
|
|
4957
|
+
<div style="font-weight:600;color:#065f46">${t('当前无待激活申请')}</div>
|
|
4958
|
+
<div style="font-size:13px;color:#047857;margin-top:8px">${t('所有 governance application 均已 active 或未达 onboarding 完成')}</div>
|
|
4959
|
+
</div>
|
|
4960
|
+
<div style="margin-top:16px"><a href="#admin" style="font-size:13px;color:#6b7280">← ${t('返回 admin')}</a></div>
|
|
4961
|
+
`, 'admin')
|
|
4962
|
+
return
|
|
4963
|
+
}
|
|
4964
|
+
|
|
4965
|
+
const list = items.map(a => {
|
|
4966
|
+
const quizOk = a.quiz_passed_at != null
|
|
4967
|
+
const caseOk = a.has_case_review === 1 || a.has_case_review === true
|
|
4968
|
+
const onboardingComplete = quizOk && caseOk
|
|
4969
|
+
const roleLabel = a.role === 'arbitrator' ? t('仲裁员') : t('审核员')
|
|
4970
|
+
return `
|
|
4971
|
+
<div class="card" style="margin-bottom:12px;border-color:${onboardingComplete ? '#86efac' : '#fde68a'}">
|
|
4972
|
+
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px">
|
|
4973
|
+
<div style="flex:1">
|
|
4974
|
+
<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>
|
|
4975
|
+
<div style="font-size:12px;color:#6b7280;margin-top:4px">${t('申请')}: ${roleLabel} · ${t('Region')}: ${escHtml(a.region || '—')} · ${t('Email')}: ${a.email ? '✓' : '✗'}</div>
|
|
4976
|
+
<div style="font-size:12px;margin-top:6px;display:flex;gap:12px">
|
|
4977
|
+
<span style="color:${quizOk ? '#16a34a' : '#dc2626'}">${quizOk ? '✓' : '✗'} ${t('题目')} ${a.quiz_score != null ? a.quiz_score + '%' : '—'}</span>
|
|
4978
|
+
<span style="color:${caseOk ? '#16a34a' : '#dc2626'}">${caseOk ? '✓' : '✗'} ${t('案例 review')}</span>
|
|
4979
|
+
</div>
|
|
4980
|
+
</div>
|
|
4981
|
+
<div style="display:flex;flex-direction:column;gap:6px;align-items:flex-end">
|
|
4982
|
+
<button class="btn btn-sm btn-outline" onclick="viewGovernanceApplication('${a.id}')">${t('查看详情')}</button>
|
|
4983
|
+
${onboardingComplete ? `<button class="btn btn-sm btn-primary" onclick="doActivateGovernance('${a.id}', '${a.user_id}')">🔑 ${t('激活')}</button>` : `<span style="font-size:11px;color:#dc2626">${t('onboarding 未完成')}</span>`}
|
|
4984
|
+
</div>
|
|
4985
|
+
</div>
|
|
4986
|
+
<div id="gov-app-detail-${a.id}" style="margin-top:10px"></div>
|
|
4987
|
+
</div>`
|
|
4988
|
+
}).join('')
|
|
4989
|
+
|
|
4990
|
+
// 阶段 4:待裁决申诉列表
|
|
4991
|
+
const appealsList = appeals.map(ap => {
|
|
4992
|
+
const roleLabel = ap.role === 'arbitrator' ? t('仲裁员') : t('审核员')
|
|
4993
|
+
const reasonShort = (ap.appeal_reason || '').slice(0, 280)
|
|
4994
|
+
return `
|
|
4995
|
+
<div class="card" style="margin-bottom:12px;border-color:#fde68a;background:#fffbeb">
|
|
4996
|
+
<div style="display:flex;justify-content:space-between;gap:10px">
|
|
4997
|
+
<div style="flex:1">
|
|
4998
|
+
<div style="font-weight:600;font-size:14px">${ap.role === 'arbitrator' ? '⚖' : '🛡'} ${escHtml(ap.user_name || '')} <span style="color:#6b7280;font-size:12px">(${ap.handle ? '@' + escHtml(ap.handle) : ap.user_id})</span></div>
|
|
4999
|
+
<div style="font-size:12px;color:#6b7280;margin-top:4px">${t('申诉')}: ${roleLabel} · ${t('提交于')} ${new Date(ap.created_at * 1000).toLocaleDateString()}${ap.auto_deactivate_at ? ' · ' + t('自动卸任于') + ' ' + new Date(ap.auto_deactivate_at * 1000).toLocaleDateString() : ''}</div>
|
|
5000
|
+
<div style="font-size:13px;color:#374151;margin-top:8px;padding:8px;background:#fff;border-radius:4px;line-height:1.6">${escHtml(reasonShort)}${(ap.appeal_reason || '').length > 280 ? '...' : ''}</div>
|
|
5001
|
+
</div>
|
|
5002
|
+
</div>
|
|
5003
|
+
<div style="margin-top:10px;display:flex;gap:6px;justify-content:flex-end">
|
|
5004
|
+
<button class="btn btn-sm btn-outline" style="color:#dc2626;border-color:#fca5a5" onclick="openResolveAppealModal('${ap.id}', 'reject')">${t('驳回')}</button>
|
|
5005
|
+
<button class="btn btn-sm btn-primary" style="background:#16a34a;border-color:#16a34a" onclick="openResolveAppealModal('${ap.id}', 'accept')">${t('接受恢复')}</button>
|
|
5006
|
+
</div>
|
|
5007
|
+
</div>`
|
|
5008
|
+
}).join('')
|
|
5009
|
+
|
|
5010
|
+
app.innerHTML = shell(`
|
|
5011
|
+
<h1 class="page-title">⚖️ ${t('治理岗位激活')}</h1>
|
|
5012
|
+
${appeals.length > 0 ? `
|
|
5013
|
+
<h2 style="font-size:15px;color:#92400e;margin:0 0 8px">${t('待裁决申诉')} (${appeals.length})</h2>
|
|
5014
|
+
<div class="card" style="margin-bottom:12px;background:#fff7ed;border-color:#fdba74">
|
|
5015
|
+
<div style="font-size:12px;color:#7c2d12">${t('spec §7.2:申诉通过 → 恢复 active + 抹除 outlier;驳回 → 公开理由(对应元规则 #1 当一切可见)')}</div>
|
|
5016
|
+
</div>
|
|
5017
|
+
${appealsList}
|
|
5018
|
+
<h2 style="font-size:15px;color:#374151;margin:24px 0 8px">${t('待激活申请')} (${items.length})</h2>
|
|
5019
|
+
` : ''}
|
|
5020
|
+
<div class="card" style="margin-bottom:16px;background:#eff6ff;border-color:#93c5fd">
|
|
5021
|
+
<div style="font-size:13px;color:#1e3a8a">${t('待激活申请')}: <strong>${items.length}</strong></div>
|
|
5022
|
+
<div style="font-size:12px;color:#1e40af;margin-top:6px">${t('激活前:代码自动 re-gate(eligibility 二次校验)+ Iron-Rule Passkey ceremony,防 maintainer 漏检')}</div>
|
|
5023
|
+
</div>
|
|
5024
|
+
${list}
|
|
5025
|
+
<div style="margin-top:16px"><a href="#admin" style="font-size:13px;color:#6b7280">← ${t('返回 admin')}</a></div>
|
|
5026
|
+
`, 'admin')
|
|
5027
|
+
}
|
|
5028
|
+
|
|
5029
|
+
window.openResolveAppealModal = (appealId, decision) => {
|
|
5030
|
+
const isAccept = decision === 'accept'
|
|
5031
|
+
const html = `
|
|
5032
|
+
<div id="resolve-appeal-modal" style="position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:9999;display:flex;align-items:center;justify-content:center;padding:16px">
|
|
5033
|
+
<div style="background:#fff;border-radius:8px;max-width:520px;width:100%;padding:20px">
|
|
5034
|
+
<h3 style="margin:0 0 12px;color:${isAccept ? '#16a34a' : '#dc2626'}">${isAccept ? '✅ ' + t('接受申诉(恢复 active)') : '❌ ' + t('驳回申诉')}</h3>
|
|
5035
|
+
<div style="font-size:12px;color:#6b7280;margin-bottom:12px;line-height:1.6">
|
|
5036
|
+
${isAccept
|
|
5037
|
+
? t('user.roles 将恢复角色,可立即继续履职(抹除 outlier 计数,审计留痕)')
|
|
5038
|
+
: t('user 维持 inactive 状态。处置理由会公开(元规则 #1 当一切可见)')}
|
|
5039
|
+
</div>
|
|
5040
|
+
<textarea id="resolve-text-input" rows="5" placeholder="${t('处置理由(≥30 字符,公开)')}" style="width:100%;padding:8px;border:1px solid #d1d5db;border-radius:4px;font-size:13px;font-family:inherit" oninput="document.getElementById('resolve-char-count').textContent = this.value.length; document.getElementById('resolve-submit-btn').disabled = this.value.length < 30"></textarea>
|
|
5041
|
+
<div style="font-size:12px;color:#6b7280;margin-top:4px">${t('字符数')}: <span id="resolve-char-count">0</span> / 30</div>
|
|
5042
|
+
<div id="resolve-msg" style="margin-top:12px"></div>
|
|
5043
|
+
<div style="margin-top:16px;display:flex;gap:8px;justify-content:flex-end">
|
|
5044
|
+
<button class="btn btn-sm btn-outline" onclick="document.getElementById('resolve-appeal-modal').remove()">${t('取消')}</button>
|
|
5045
|
+
<button id="resolve-submit-btn" class="btn btn-sm btn-primary" disabled onclick="doResolveAppeal('${appealId}', '${decision}')">🔑 ${t('Passkey 签发裁决')}</button>
|
|
5046
|
+
</div>
|
|
5047
|
+
</div>
|
|
5048
|
+
</div>`
|
|
5049
|
+
document.body.insertAdjacentHTML('beforeend', html)
|
|
5050
|
+
}
|
|
5051
|
+
|
|
5052
|
+
window.doResolveAppeal = async (appealId, decision) => {
|
|
5053
|
+
const input = document.getElementById('resolve-text-input')
|
|
5054
|
+
const msg = document.getElementById('resolve-msg')
|
|
5055
|
+
if (!input || input.value.length < 30) return
|
|
5056
|
+
const resolution_text = input.value.trim()
|
|
5057
|
+
|
|
5058
|
+
msg.innerHTML = `<div class="alert alert-info"><span class="spinner"></span>${t('请求 Passkey...')}</div>`
|
|
5059
|
+
let webauthnToken
|
|
5060
|
+
try {
|
|
5061
|
+
webauthnToken = await requestPasskeyGate('governance_appeal_resolve', { appeal_application_id: appealId, decision })
|
|
5062
|
+
} catch (e) {
|
|
5063
|
+
msg.innerHTML = alert$('error', t('Passkey 签发失败:') + ' ' + (e?.message || e))
|
|
5064
|
+
return
|
|
5065
|
+
}
|
|
5066
|
+
|
|
5067
|
+
msg.innerHTML = `<div class="alert alert-info"><span class="spinner"></span>${t('提交中...')}</div>`
|
|
5068
|
+
const res = await POST('/admin/governance/resolve-appeal', {
|
|
5069
|
+
appeal_application_id: appealId,
|
|
5070
|
+
decision,
|
|
5071
|
+
resolution_text,
|
|
5072
|
+
webauthn_token: webauthnToken,
|
|
5073
|
+
})
|
|
5074
|
+
if (res.error) { msg.innerHTML = alert$('error', res.error); return }
|
|
5075
|
+
msg.innerHTML = `<div class="alert alert-success">${t('裁决已记录,user 已通知')}</div>`
|
|
5076
|
+
setTimeout(() => {
|
|
5077
|
+
const modal = document.getElementById('resolve-appeal-modal')
|
|
5078
|
+
if (modal) modal.remove()
|
|
5079
|
+
renderAdminGovernance(document.getElementById('app'))
|
|
5080
|
+
}, 1200)
|
|
5081
|
+
}
|
|
5082
|
+
|
|
5083
|
+
window.viewGovernanceApplication = async (appId) => {
|
|
5084
|
+
const detailEl = document.getElementById('gov-app-detail-' + appId)
|
|
5085
|
+
if (!detailEl) return
|
|
5086
|
+
detailEl.innerHTML = `<div style="font-size:12px;color:#6b7280">${t('加载中...')}</div>`
|
|
5087
|
+
const data = await GET('/admin/governance/application/' + encodeURIComponent(appId))
|
|
5088
|
+
if (data.error) { detailEl.innerHTML = alert$('error', data.error); return }
|
|
5089
|
+
|
|
5090
|
+
const a = data.application
|
|
5091
|
+
const cases = data.cases_with_expected || []
|
|
5092
|
+
const reviewObj = data.parsed_review
|
|
5093
|
+
const reviews = reviewObj?.reviews || []
|
|
5094
|
+
|
|
5095
|
+
// Map case_id → submitted review
|
|
5096
|
+
const reviewMap = new Map(reviews.map(r => [r.case_id, r]))
|
|
5097
|
+
|
|
5098
|
+
const caseRows = cases.map(c => {
|
|
5099
|
+
const submitted = reviewMap.get(c.id)
|
|
5100
|
+
const match = submitted && submitted.chosen_verdict === c.expected_verdict
|
|
5101
|
+
return `
|
|
5102
|
+
<div style="margin-bottom:8px;padding:8px;background:#f9fafb;border-radius:4px;font-size:12px">
|
|
5103
|
+
<div style="font-weight:600;color:#374151">${c.id}: ${state._lang === 'en' ? c.scenario_en : c.scenario_zh}</div>
|
|
5104
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:6px">
|
|
5105
|
+
<div>
|
|
5106
|
+
<div style="color:#6b7280">${t('申请者选')}:</div>
|
|
5107
|
+
<div style="color:${match ? '#16a34a' : '#dc2626'};font-weight:600">${submitted ? submitted.chosen_verdict : '—'}</div>
|
|
5108
|
+
</div>
|
|
5109
|
+
<div>
|
|
5110
|
+
<div style="color:#6b7280">${t('expected')}:</div>
|
|
5111
|
+
<div style="font-weight:600;color:#047857">${c.expected_verdict}</div>
|
|
5112
|
+
</div>
|
|
5113
|
+
</div>
|
|
5114
|
+
<div style="margin-top:6px">
|
|
5115
|
+
<div style="color:#6b7280">${t('reasoning')}:</div>
|
|
5116
|
+
<div style="white-space:pre-wrap;padding:6px;background:white;border-radius:4px;border:1px solid #e5e7eb">${escHtml(submitted?.reasoning || '—')}</div>
|
|
5117
|
+
</div>
|
|
5118
|
+
<div style="margin-top:6px;font-size:11px;color:#6b7280">${t('key principles')}: ${c.key_principles.join(' / ')}</div>
|
|
5119
|
+
</div>`
|
|
5120
|
+
}).join('')
|
|
5121
|
+
|
|
5122
|
+
detailEl.innerHTML = `
|
|
5123
|
+
<div style="border-top:1px solid #e5e7eb;padding-top:10px">
|
|
5124
|
+
<div style="font-weight:600;font-size:13px;margin-bottom:6px">${t('案例 review 对比')}:</div>
|
|
5125
|
+
${caseRows || '<div style="font-size:12px;color:#6b7280">' + t('无 review 数据') + '</div>'}
|
|
5126
|
+
</div>`
|
|
5127
|
+
}
|
|
5128
|
+
|
|
5129
|
+
window.doActivateGovernance = async (applicationId, targetUserId) => {
|
|
5130
|
+
if (!confirm(t('确认激活该申请?将自动 re-gate eligibility + Passkey 签发'))) return
|
|
5131
|
+
|
|
5132
|
+
let webauthnToken
|
|
5133
|
+
try {
|
|
5134
|
+
webauthnToken = await requestPasskeyGate('governance_activate', { application_id: applicationId, target_user_id: targetUserId })
|
|
5135
|
+
} catch (e) {
|
|
5136
|
+
alert(t('Passkey 签发失败:') + ' ' + (e?.message || e))
|
|
5137
|
+
return
|
|
5138
|
+
}
|
|
5139
|
+
|
|
5140
|
+
const res = await POST('/admin/governance/activate', {
|
|
5141
|
+
application_id: applicationId,
|
|
5142
|
+
webauthn_token: webauthnToken,
|
|
5143
|
+
})
|
|
5144
|
+
|
|
5145
|
+
if (res.error) {
|
|
5146
|
+
if (res.missing_requirements) {
|
|
5147
|
+
alert(t('代码自动 re-gate 失败') + ': ' + res.missing_requirements.join(', '))
|
|
5148
|
+
} else {
|
|
5149
|
+
alert(res.error)
|
|
5150
|
+
}
|
|
5151
|
+
return
|
|
5152
|
+
}
|
|
5153
|
+
|
|
5154
|
+
alert(t('✅ 激活成功') + ': ' + res.role)
|
|
5155
|
+
renderAdminGovernance(document.getElementById('app'))
|
|
5156
|
+
}
|
|
5157
|
+
|
|
5158
|
+
window.doSubmitCaseReview = async (role) => {
|
|
5159
|
+
const msg = document.getElementById('onb-case-msg')
|
|
5160
|
+
if (!msg) return
|
|
5161
|
+
|
|
5162
|
+
// 收集 case reviews
|
|
5163
|
+
const reviews = []
|
|
5164
|
+
const caseVerdictMap = new Map()
|
|
5165
|
+
document.querySelectorAll('[name^="case-"][type="radio"]:checked').forEach(el => {
|
|
5166
|
+
// name format: case-<id>-verdict
|
|
5167
|
+
const caseId = el.name.replace(/^case-/, '').replace(/-verdict$/, '')
|
|
5168
|
+
caseVerdictMap.set(caseId, el.value)
|
|
5169
|
+
})
|
|
5170
|
+
document.querySelectorAll('textarea.case-reasoning').forEach(ta => {
|
|
5171
|
+
const caseId = ta.dataset.caseId
|
|
5172
|
+
const verdict = caseVerdictMap.get(caseId)
|
|
5173
|
+
if (caseId && verdict) {
|
|
5174
|
+
reviews.push({ case_id: caseId, chosen_verdict: verdict, reasoning: ta.value.trim() })
|
|
5175
|
+
}
|
|
5176
|
+
})
|
|
5177
|
+
|
|
5178
|
+
msg.innerHTML = `<div class="alert alert-info"><span class="spinner"></span>${t('提交中...')}</div>`
|
|
5179
|
+
const res = await POST('/governance/onboarding/case-review', { role, reviews })
|
|
5180
|
+
|
|
5181
|
+
if (res.error) {
|
|
5182
|
+
if (res.errors && Array.isArray(res.errors)) {
|
|
5183
|
+
const detail = res.errors.map(e => `${e.case_id}: ${e.reason}`).join('<br>')
|
|
5184
|
+
msg.innerHTML = alert$('error', t('案例 review 不完整') + ':<br>' + detail)
|
|
5185
|
+
} else {
|
|
5186
|
+
msg.innerHTML = alert$('error', res.error)
|
|
5187
|
+
}
|
|
5188
|
+
return
|
|
5189
|
+
}
|
|
5190
|
+
|
|
5191
|
+
msg.innerHTML = `
|
|
5192
|
+
<div class="alert alert-success">
|
|
5193
|
+
<div style="font-weight:600">✅ ${t('案例 review 已提交')} (${res.submitted_count} ${t('个案例')})</div>
|
|
5194
|
+
<div style="margin-top:8px;font-size:13px">${t('maintainer 上岗签字前会对比 expected verdict 评估你的 reasoning 方向')}</div>
|
|
5195
|
+
</div>`
|
|
5196
|
+
}
|
|
5197
|
+
|
|
5198
|
+
window.doSubmitOnboarding = async (role) => {
|
|
5199
|
+
const msg = document.getElementById('onb-msg')
|
|
5200
|
+
if (!msg) return
|
|
5201
|
+
|
|
5202
|
+
// 收集答案
|
|
5203
|
+
const answers = []
|
|
5204
|
+
// multi-choice
|
|
5205
|
+
const radioGroups = new Map()
|
|
5206
|
+
document.querySelectorAll('input[type="radio"][name^="mcq-"]:checked').forEach(el => {
|
|
5207
|
+
radioGroups.set(el.name, el.value)
|
|
5208
|
+
})
|
|
5209
|
+
radioGroups.forEach((answer, qid) => answers.push({ question_id: qid, answer }))
|
|
5210
|
+
// short-answer
|
|
5211
|
+
document.querySelectorAll('textarea.short-answer').forEach(ta => {
|
|
5212
|
+
answers.push({ question_id: ta.dataset.qid, answer: ta.value.trim() })
|
|
5213
|
+
})
|
|
5214
|
+
|
|
5215
|
+
msg.innerHTML = `<div class="alert alert-info"><span class="spinner"></span>${t('评分中...')}</div>`
|
|
5216
|
+
const res = await POST('/governance/onboarding/quiz-submit', { role, answers })
|
|
4262
5217
|
if (res.error) { msg.innerHTML = alert$('error', res.error); return }
|
|
4263
|
-
|
|
4264
|
-
|
|
5218
|
+
|
|
5219
|
+
const detail = (res.per_question || []).map(p => `
|
|
5220
|
+
<span style="display:inline-block;margin:2px;padding:2px 6px;background:${p.ok ? '#dcfce7' : '#fee2e2'};color:${p.ok ? '#166534' : '#991b1b'};font-size:11px;border-radius:4px">${p.id} ${p.ok ? '✓' : '✗'}</span>`).join('')
|
|
5221
|
+
|
|
5222
|
+
if (res.passed) {
|
|
5223
|
+
msg.innerHTML = `
|
|
5224
|
+
<div class="alert alert-success">
|
|
5225
|
+
<div style="font-weight:600">✅ ${t('合格')}! ${t('得分')}: ${res.score_pct}% (${res.correct}/${res.total})</div>
|
|
5226
|
+
<div style="margin-top:8px;font-size:12px">${detail}</div>
|
|
5227
|
+
<div style="margin-top:10px;font-size:13px">${t('下一步:等待 maintainer 激活,本阶段未上线 admin UI')}</div>
|
|
5228
|
+
</div>`
|
|
5229
|
+
} else {
|
|
5230
|
+
msg.innerHTML = `
|
|
5231
|
+
<div class="alert alert-error">
|
|
5232
|
+
<div style="font-weight:600">⚠️ ${t('未合格')}: ${res.score_pct}% < ${res.pass_threshold}%</div>
|
|
5233
|
+
<div style="margin-top:8px;font-size:12px">${detail}</div>
|
|
5234
|
+
<div style="margin-top:10px;font-size:13px">${t('可重试提交')}</div>
|
|
5235
|
+
</div>`
|
|
5236
|
+
}
|
|
5237
|
+
}
|
|
5238
|
+
|
|
5239
|
+
window.updateGovApplyState = () => {
|
|
5240
|
+
const cb1 = document.getElementById('gov-consent-1')
|
|
5241
|
+
const cb2 = document.getElementById('gov-consent-2')
|
|
5242
|
+
const btn = document.getElementById('gov-submit-btn')
|
|
5243
|
+
if (cb1 && cb2 && btn) {
|
|
5244
|
+
const ok = cb1.checked && cb2.checked && !cb2.disabled
|
|
5245
|
+
btn.disabled = !ok
|
|
5246
|
+
btn.style.opacity = ok ? '1' : '0.5'
|
|
5247
|
+
}
|
|
5248
|
+
}
|
|
5249
|
+
|
|
5250
|
+
window.doSubmitGovernanceApply = async (role, pageLoadedAt) => {
|
|
5251
|
+
const msg = document.getElementById('gov-apply-msg')
|
|
5252
|
+
if (!msg) return
|
|
5253
|
+
|
|
5254
|
+
// 1. 生成 consent_hash(client-side SHA-256)
|
|
5255
|
+
// PR #22 review fix P1-2:与 server 同步的 disclosure_version,server 重建 hash 校验内容(防"任意 16 字符过关")
|
|
5256
|
+
// 文本格式必须与 src/pwa/routes/governance-onboarding.ts expectedConsentHash() 一致
|
|
5257
|
+
const GOVERNANCE_APPLY_DISCLOSURE_VERSION = 'v1.0-2026-06-02'
|
|
5258
|
+
const consentText = 'governance_apply|disclosure=' + GOVERNANCE_APPLY_DISCLOSURE_VERSION + '|role=' + role + '|user=' + state.user.id + '|page_loaded_at=' + pageLoadedAt
|
|
5259
|
+
const encoder = new TextEncoder()
|
|
5260
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', encoder.encode(consentText))
|
|
5261
|
+
const consentHash = Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2,'0')).join('')
|
|
5262
|
+
|
|
5263
|
+
// 2. Passkey 签发(spec §3.1 Iron-Rule 真人 gate)
|
|
5264
|
+
msg.innerHTML = `<div class="alert alert-info"><span class="spinner"></span>${t('请通过 Passkey / 生物识别确认...')}</div>`
|
|
5265
|
+
let passkeyToken
|
|
5266
|
+
try {
|
|
5267
|
+
passkeyToken = await requestPasskeyGate('governance_apply', { role, consent_hash: consentHash })
|
|
5268
|
+
} catch (e) {
|
|
5269
|
+
msg.innerHTML = alert$('error', t('Passkey 签发失败:') + ' ' + (e?.message || e))
|
|
5270
|
+
return
|
|
5271
|
+
}
|
|
5272
|
+
|
|
5273
|
+
// 3. POST /api/governance/onboarding/apply
|
|
5274
|
+
msg.innerHTML = `<div class="alert alert-info"><span class="spinner"></span>${t('提交中...')}</div>`
|
|
5275
|
+
const res = await POST('/governance/onboarding/apply', {
|
|
5276
|
+
role,
|
|
5277
|
+
consent_hash: consentHash,
|
|
5278
|
+
passkey_sig: passkeyToken,
|
|
5279
|
+
iron_rule_method: 'passkey',
|
|
5280
|
+
page_loaded_at: pageLoadedAt,
|
|
5281
|
+
})
|
|
5282
|
+
|
|
5283
|
+
if (res.error) {
|
|
5284
|
+
if (res.missing_requirements && Array.isArray(res.missing_requirements)) {
|
|
5285
|
+
msg.innerHTML = alert$('error', t('门槛未达标') + ': ' + res.missing_requirements.join(', '))
|
|
5286
|
+
} else {
|
|
5287
|
+
msg.innerHTML = alert$('error', res.error)
|
|
5288
|
+
}
|
|
5289
|
+
return
|
|
5290
|
+
}
|
|
5291
|
+
|
|
5292
|
+
msg.innerHTML = alert$('success', t('申请已提交,等待 maintainer review') + ' (' + (res.application_id || '') + ')')
|
|
5293
|
+
setTimeout(() => navigate('#me'), 2500)
|
|
4265
5294
|
}
|
|
4266
5295
|
|
|
4267
5296
|
window.doWithdrawVerifierApp = async () => {
|
|
@@ -5213,6 +6242,15 @@ function renderWelcome(app) {
|
|
|
5213
6242
|
<a class="w-btn-full w-btn-outline" href="https://t.me/webazer" target="_blank" rel="noopener noreferrer" style="display:flex;align-items:center;justify-content:center;text-decoration:none">${T('加入 Telegram', 'Join Telegram')}</a>
|
|
5214
6243
|
</div>
|
|
5215
6244
|
</div>
|
|
6245
|
+
<div class="w-card w-join-card">
|
|
6246
|
+
<div class="w-join-card-left">
|
|
6247
|
+
<div class="w-card-title">📧 ${T('邮件联系', 'Email us')}</div>
|
|
6248
|
+
<div class="w-card-desc">${T('合作 / 反馈 / 合规咨询', 'Partnerships / feedback / compliance')}</div>
|
|
6249
|
+
</div>
|
|
6250
|
+
<div class="w-join-card-right">
|
|
6251
|
+
<a class="w-btn-full w-btn-outline" href="mailto:contact@webaz.xyz" style="display:flex;align-items:center;justify-content:center;text-decoration:none;font-family:ui-monospace,SF Mono,Menlo,monospace;font-size:13px">contact@webaz.xyz</a>
|
|
6252
|
+
</div>
|
|
6253
|
+
</div>
|
|
5216
6254
|
<div class="w-card w-join-card">
|
|
5217
6255
|
<div class="w-join-card-left">
|
|
5218
6256
|
<div class="w-card-title">🚀 ${T('立即注册', 'Sign up now')}</div>
|
|
@@ -5233,7 +6271,7 @@ function renderWelcome(app) {
|
|
|
5233
6271
|
<a href="https://github.com/seasonsagents-art/webaz/blob/main/docs/META-RULES-FULL.md" target="_blank" rel="noopener">${T('完整元规则', 'Full Meta-Rules')}</a>
|
|
5234
6272
|
<a href="https://github.com/seasonsagents-art/webaz" target="_blank" rel="noopener">GitHub</a>
|
|
5235
6273
|
<a href="#">${T('协议白皮书', 'Whitepaper')}</a>
|
|
5236
|
-
<a href="mailto:contact@webaz.xyz"
|
|
6274
|
+
<a href="mailto:contact@webaz.xyz">📧 contact@webaz.xyz</a>
|
|
5237
6275
|
</div>
|
|
5238
6276
|
</footer>
|
|
5239
6277
|
</div>
|
|
@@ -5306,6 +6344,132 @@ function renderWelcome(app) {
|
|
|
5306
6344
|
})
|
|
5307
6345
|
} catch { /* never break the page */ }
|
|
5308
6346
|
}
|
|
6347
|
+
|
|
6348
|
+
// 2026-06-02 W3.5-B:/governance-onboarding 公开页(无 auth)
|
|
6349
|
+
// 引导潜在 arbitrator/verifier 申请人;phase A 无报酬声明;现有岗位人数 stat
|
|
6350
|
+
async function renderGovernanceOnboarding(app) {
|
|
6351
|
+
const en = window._lang === 'en'
|
|
6352
|
+
const T = (zh, e) => en && e ? e : zh
|
|
6353
|
+
const esc = (s) => String(s ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''')
|
|
6354
|
+
app.innerHTML = `
|
|
6355
|
+
${preLaunchBannerHTML()}
|
|
6356
|
+
<div style="max-width:880px;margin:0 auto;padding:24px 16px 80px;font-family:-apple-system,BlinkMacSystemFont,'SF Pro Text','PingFang SC','Microsoft YaHei UI',sans-serif">
|
|
6357
|
+
<header style="text-align:center;margin-bottom:40px">
|
|
6358
|
+
<h1 style="font-size:clamp(28px,5vw,36px);margin:0 0 12px;color:#18181B">⚖️ ${T('治理上岗', 'Governance Onboarding')}</h1>
|
|
6359
|
+
<p style="color:#52525B;font-size:16px;margin:0">${T('phase A:公共贡献,无报酬', 'Phase A: public contribution, no compensation')}</p>
|
|
6360
|
+
</header>
|
|
6361
|
+
|
|
6362
|
+
<section style="background:#fff;border:1px solid #e4e4e7;border-radius:12px;padding:24px;margin-bottom:24px">
|
|
6363
|
+
<h2 style="font-size:22px;margin:0 0 16px;color:#18181B">📊 ${T('当前在岗', 'Currently active')}</h2>
|
|
6364
|
+
<div id="gov-stats" style="min-height:80px">
|
|
6365
|
+
<div style="color:#a1a1aa;font-size:14px;text-align:center;padding:20px">${T('加载中…', 'Loading…')}</div>
|
|
6366
|
+
</div>
|
|
6367
|
+
</section>
|
|
6368
|
+
|
|
6369
|
+
<section style="background:#fee2e2;border:1px solid #ef4444;border-radius:12px;padding:20px;margin-bottom:24px">
|
|
6370
|
+
<h3 style="margin:0 0 12px;color:#991b1b;font-size:18px">⚠️ ${T('重要:这不是赚钱机会', 'Important: this is NOT an income opportunity')}</h3>
|
|
6371
|
+
<ul style="margin:0;padding-left:20px;color:#7f1d1d;line-height:1.8;font-size:14px">
|
|
6372
|
+
<li>${T('phase A 不发任何现金 / WAZ 报酬', 'No cash / WAZ compensation in phase A')}</li>
|
|
6373
|
+
<li>${T('治理是公共贡献,非雇佣关系', 'Governance is public contribution, not employment')}</li>
|
|
6374
|
+
<li>${T('履职记录关系层永久保留,估值层 phase D DAO 决定', 'Performance records kept in relationship layer (permanent), valuation by phase D DAO')}</li>
|
|
6375
|
+
<li>${T('误以为 income 来源会失望 — 请先确认意愿', 'Mistaking it as income source will disappoint — confirm intent first')}</li>
|
|
6376
|
+
</ul>
|
|
6377
|
+
</section>
|
|
6378
|
+
|
|
6379
|
+
<section style="background:#fff;border:1px solid #e4e4e7;border-radius:12px;padding:24px;margin-bottom:24px">
|
|
6380
|
+
<h3 style="margin:0 0 16px;color:#18181B;font-size:18px">📋 ${T('资格门槛', 'Eligibility')}</h3>
|
|
6381
|
+
<div id="gov-thresholds" style="color:#52525B;font-size:14px;line-height:1.8">
|
|
6382
|
+
<div style="color:#a1a1aa;text-align:center;padding:12px">${T('加载阈值…', 'Loading thresholds…')}</div>
|
|
6383
|
+
</div>
|
|
6384
|
+
</section>
|
|
6385
|
+
|
|
6386
|
+
<section style="background:#dbeafe;border:1px solid #3b82f6;border-radius:12px;padding:20px;margin-bottom:24px">
|
|
6387
|
+
<h3 style="margin:0 0 12px;color:#1e40af;font-size:18px">🛡️ ${T('履职范围', 'Duties')}</h3>
|
|
6388
|
+
<ul style="margin:0;padding-left:20px;color:#1e3a8a;line-height:1.8;font-size:14px">
|
|
6389
|
+
<li><strong>arbitrator</strong> — ${T('仲裁纠纷 dispute(Iron-Rule 真人 Passkey 必备)', 'Arbitrate disputes (Iron-Rule requires real human Passkey)')}</li>
|
|
6390
|
+
<li><strong>verifier</strong> — ${T('验证 claim(同 Iron-Rule)', 'Verify claims (same Iron-Rule)')}</li>
|
|
6391
|
+
<li>${T('卸任 30 天冷却 / outlier 自动 deactivate / 申诉路径', 'Resignation 30d cooldown / outlier auto-deactivate / appeal path')}</li>
|
|
6392
|
+
</ul>
|
|
6393
|
+
</section>
|
|
6394
|
+
|
|
6395
|
+
<section style="background:#fef3c7;border:1px solid #f59e0b;border-radius:12px;padding:20px;margin-bottom:24px">
|
|
6396
|
+
<h3 style="margin:0 0 12px;color:#92400e;font-size:18px">🚀 ${T('如何申请', 'How to apply')}</h3>
|
|
6397
|
+
<ol style="margin:0;padding-left:20px;color:#78350f;line-height:1.8;font-size:14px">
|
|
6398
|
+
<li>${T('登录(若未登录)→ 进入 #me', 'Log in (if needed) → go to #me')}</li>
|
|
6399
|
+
<li>${T('达成资格门槛(Passkey + 30 天 + 5 笔订单 + reputation)', 'Meet thresholds (Passkey + 30d + 5 orders + reputation)')}</li>
|
|
6400
|
+
<li>${T('点 [申请审核员] 入口(verifier)', 'Click [Apply to be verifier] entry')}</li>
|
|
6401
|
+
<li>${T('阅读披露 + 双勾选 + 8s 反诱导延迟 + Passkey 签发', 'Read disclosure + dual checkbox + 8s anti-railroad delay + Passkey sign')}</li>
|
|
6402
|
+
<li>${T('完成 onboarding(学习包 + 案例 + 80% 测试)', 'Complete onboarding (study pack + cases + 80% quiz)')}</li>
|
|
6403
|
+
<li>${T('maintainer Iron-Rule 签发激活', 'Maintainer Iron-Rule signing activation')}</li>
|
|
6404
|
+
</ol>
|
|
6405
|
+
</section>
|
|
6406
|
+
|
|
6407
|
+
<section style="border-top:1px solid #e4e4e7;padding-top:20px;color:#71717a;font-size:13px;line-height:1.7">
|
|
6408
|
+
<p style="margin:0 0 8px"><strong>${T('完整规范', 'Full spec')}:</strong></p>
|
|
6409
|
+
<ul style="margin:0;padding-left:20px">
|
|
6410
|
+
<li><a href="https://github.com/seasonsagents-art/webaz/blob/main/docs/GOVERNANCE-ONBOARDING.md" target="_blank" rel="noopener" style="color:#1d4ed8">GOVERNANCE-ONBOARDING.md</a> — ${T('资格 / 流程 / 卸任 / 申诉', 'eligibility / flow / resignation / appeal')}</li>
|
|
6411
|
+
<li><a href="https://github.com/seasonsagents-art/webaz/blob/main/docs/ARBITRATION-PLAYBOOK.md" target="_blank" rel="noopener" style="color:#1d4ed8">ARBITRATION-PLAYBOOK.md</a> — ${T('arbitrator 决策框架 + 5 模拟案例', 'arbitrator decision framework + 5 simulated cases')}</li>
|
|
6412
|
+
<li>${T('机读 JSON 端点', 'Machine-readable JSON endpoint')}: <code style="background:#f4f4f5;padding:2px 6px;border-radius:4px">/api/governance/onboarding-stats</code></li>
|
|
6413
|
+
</ul>
|
|
6414
|
+
</section>
|
|
6415
|
+
</div>
|
|
6416
|
+
`
|
|
6417
|
+
|
|
6418
|
+
try {
|
|
6419
|
+
const r = await fetch('/api/governance/onboarding-stats', { signal: AbortSignal.timeout(10000) })
|
|
6420
|
+
const j = await r.json()
|
|
6421
|
+
const stats = document.getElementById('gov-stats')
|
|
6422
|
+
const thr = document.getElementById('gov-thresholds')
|
|
6423
|
+
stats.innerHTML = `
|
|
6424
|
+
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:12px">
|
|
6425
|
+
<div style="text-align:center;padding:16px;background:#f4f4f5;border-radius:8px">
|
|
6426
|
+
<div style="font-size:32px;font-weight:700;color:#18181B">${j.active_arbitrators || 0}</div>
|
|
6427
|
+
<div style="font-size:13px;color:#71717a;margin-top:4px">${T('在岗 arbitrator', 'Active arbitrators')}</div>
|
|
6428
|
+
</div>
|
|
6429
|
+
<div style="text-align:center;padding:16px;background:#f4f4f5;border-radius:8px">
|
|
6430
|
+
<div style="font-size:32px;font-weight:700;color:#18181B">${j.active_verifiers || 0}</div>
|
|
6431
|
+
<div style="font-size:13px;color:#71717a;margin-top:4px">${T('在岗 verifier', 'Active verifiers')}</div>
|
|
6432
|
+
</div>
|
|
6433
|
+
<div style="text-align:center;padding:16px;background:#f4f4f5;border-radius:8px">
|
|
6434
|
+
<div style="font-size:32px;font-weight:700;color:#18181B">${j.pending_applications || 0}</div>
|
|
6435
|
+
<div style="font-size:13px;color:#71717a;margin-top:4px">${T('待审申请', 'Pending applications')}</div>
|
|
6436
|
+
</div>
|
|
6437
|
+
</div>
|
|
6438
|
+
<p style="text-align:center;color:#a1a1aa;font-size:12px;margin:12px 0 0">${T('phase A 暂多为 fixture 数据;launch 后接受真实申请', 'Phase A counts are mostly fixtures; real applications post-launch')}</p>
|
|
6439
|
+
`
|
|
6440
|
+
// 真实 enforced 门槛(role-split),来自 onboarding-stats.eligibility — 与 server 代码同步
|
|
6441
|
+
const el = j.eligibility || {}
|
|
6442
|
+
const arb = el.arbitrator || {}
|
|
6443
|
+
const ver = el.verifier || {}
|
|
6444
|
+
const yes = T('需', 'required')
|
|
6445
|
+
thr.innerHTML = `
|
|
6446
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px">
|
|
6447
|
+
<div style="background:#f4f4f5;border-radius:8px;padding:12px">
|
|
6448
|
+
<div style="font-weight:700;margin-bottom:6px">⚖️ Arbitrator</div>
|
|
6449
|
+
<div style="font-size:13px;line-height:1.9;color:#3f3f46">
|
|
6450
|
+
📅 ${T('注册', 'Registered')} ≥ <strong>${esc(arb.registration_days ?? '?')}</strong> ${T('天', 'd')}<br>
|
|
6451
|
+
📦 ${T('完成订单', 'Completed orders')} ≥ <strong>${esc(arb.completed_orders ?? '?')}</strong><br>
|
|
6452
|
+
⭐ ${T('信誉', 'Reputation')} ≥ <strong>${esc(arb.reputation ?? '?')}</strong><br>
|
|
6453
|
+
💰 ${T('钱包余额', 'Balance')} ≥ <strong>${esc(arb.balance_waz ?? '?')}</strong> WAZ<br>
|
|
6454
|
+
✉️ ${T('邮箱验证', 'Email verified')} · 🚫 ${T('零仲裁判输', 'Zero disputes lost')} · ✅ ${T('未曾暂停', 'Never suspended')}
|
|
6455
|
+
</div>
|
|
6456
|
+
</div>
|
|
6457
|
+
<div style="background:#f4f4f5;border-radius:8px;padding:12px">
|
|
6458
|
+
<div style="font-weight:700;margin-bottom:6px">🔍 Verifier</div>
|
|
6459
|
+
<div style="font-size:13px;line-height:1.9;color:#3f3f46">
|
|
6460
|
+
📅 ${T('注册', 'Registered')} ≥ <strong>${esc(ver.registration_days ?? '?')}</strong> ${T('天', 'd')}<br>
|
|
6461
|
+
📦 ${T('完成订单', 'Completed orders')} ≥ <strong>${esc(ver.completed_orders ?? '?')}</strong><br>
|
|
6462
|
+
✉️ ${T('邮箱验证', 'Email verified')} · 🚫 ${T('零仲裁判输', 'Zero disputes lost')} · ✅ ${T('未曾暂停', 'Never suspended')}
|
|
6463
|
+
</div>
|
|
6464
|
+
</div>
|
|
6465
|
+
</div>
|
|
6466
|
+
<div style="margin-top:10px;font-size:13px;color:#3f3f46">📝 ${T('两岗均需 onboarding 测试合格分', 'Both roles: onboarding quiz pass score')} ≥ <strong>${esc(j.quiz_pass_score ?? 80)}%</strong></div>
|
|
6467
|
+
`
|
|
6468
|
+
} catch (e) {
|
|
6469
|
+
document.getElementById('gov-stats').innerHTML = `<div style="color:#dc2626;text-align:center;padding:20px;font-size:14px">${T('加载失败', 'Load failed')}: ${esc(e.message || 'unknown')}</div>`
|
|
6470
|
+
}
|
|
6471
|
+
}
|
|
6472
|
+
|
|
5309
6473
|
function renderRule(num, text) {
|
|
5310
6474
|
const [zh, en] = text.includes(' / ') ? text.split(' / ') : [text, '']
|
|
5311
6475
|
return `<div class="w-rule w-rule-item">
|
|
@@ -5549,11 +6713,11 @@ async function renderPromoter(app) {
|
|
|
5549
6713
|
const pending = atomic.score?.pending_score || 0
|
|
5550
6714
|
const _mlmMax = Number(state.user?.region_max_levels ?? 1)
|
|
5551
6715
|
const _kpiRestricted = _mlmMax <= 1
|
|
5552
|
-
// 两个语义独立的 gate(三级奖励 ≠ PV
|
|
6716
|
+
// 两个语义独立的 gate(三级奖励 ≠ PV 双轨系统)—— 2026-06-04 已解耦:
|
|
5553
6717
|
// - _kpiRestricted (max ≤ 1):紧张地区 — KPI 改时间线 / team 隐 WAZ
|
|
5554
|
-
// - _pvAllowed
|
|
5555
|
-
//
|
|
5556
|
-
const _pvAllowed =
|
|
6718
|
+
// - _pvAllowed:PV 双轨/对碰系统是否开启,读 region_pv_enabled(独立旋钮,不再绑 max≥3)
|
|
6719
|
+
// commission 层级(max_levels) 与 PV 系统(pv_enabled) 分离:可单独开某辖区到 L2/L3 而 PV 仍关。
|
|
6720
|
+
const _pvAllowed = Number(state.user?.region_pv_enabled ?? 0) === 1
|
|
5557
6721
|
// 紧张地区:拼装最近奖励时间线(commission + 对碰 binary 混合,按时间倒序取 5 条)
|
|
5558
6722
|
let kpiBar = ''
|
|
5559
6723
|
if (_kpiRestricted) {
|
|
@@ -5892,8 +7056,8 @@ async function renderPromoter(app) {
|
|
|
5892
7056
|
// 最右侧地区显示(所有模式都加)
|
|
5893
7057
|
const _userRegion = state.user?.region || 'global'
|
|
5894
7058
|
const _regionChip = `<span style="font-size:11px;color:#6b7280;white-space:nowrap;font-weight:400">${regionLabel(_userRegion)}</span>`
|
|
5895
|
-
// PV 双轨系统:只在 _pvAllowed
|
|
5896
|
-
//
|
|
7059
|
+
// PV 双轨系统:只在 _pvAllowed(region_pv_enabled=1,2026-06-04 解耦,独立于 max_levels)才显示完整 tier/对碰/弱腿;
|
|
7060
|
+
// 未开启 PV 的地区 → 走精简卡(分数仍后台计算累积,待 pv_enabled 开启/迁移后兑现)
|
|
5897
7061
|
const atomicSection = atomic.left_invite_url
|
|
5898
7062
|
? (!_pvAllowed
|
|
5899
7063
|
? `<div style="margin-bottom:12px;background:#fff;border:1px solid #e5e7eb;border-radius:8px;padding:14px">
|
|
@@ -8518,6 +9682,80 @@ async function renderFeedback(app) {
|
|
|
8518
9682
|
`, 'me')
|
|
8519
9683
|
}
|
|
8520
9684
|
|
|
9685
|
+
// ─── RFC-004 build feedback(用→建):浮动入口弹窗 + 闭环视图 ───────────
|
|
9686
|
+
window.openBuildFeedback = () => {
|
|
9687
|
+
if (!state.user) { toast$(t('请先登录')); navigate('#login'); return }
|
|
9688
|
+
const page = (location.hash || '#/').split('?')[0] // 非 PII:只取当前页面路由作上下文
|
|
9689
|
+
const html = `
|
|
9690
|
+
<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()">
|
|
9691
|
+
<div style="background:#fff;width:100%;max-width:560px;border-radius:16px 16px 0 0;padding:20px;max-height:82vh;overflow-y:auto" onclick="event.stopPropagation()">
|
|
9692
|
+
<h2 style="font-size:16px;font-weight:700;margin-bottom:4px">💬 ${t('反馈 / 建议')}</h2>
|
|
9693
|
+
<div style="font-size:11px;color:#9ca3af;margin-bottom:12px">${t('帮我们改进 WebAZ。会自动附上当前页面')} <code style="background:#f3f4f6;padding:1px 5px;border-radius:4px">${escHtml(page)}</code> ${t('作为上下文(不含个人信息)')}</div>
|
|
9694
|
+
<div class="form-group">
|
|
9695
|
+
<label class="form-label">${t('类型')}</label>
|
|
9696
|
+
<select class="form-control" id="bfb-type">
|
|
9697
|
+
<option value="ux_issue">${t('体验问题(用着别扭)')}</option>
|
|
9698
|
+
<option value="bug">${t('Bug(报错 / 坏了)')}</option>
|
|
9699
|
+
<option value="proposal">${t('改进提案(建设 · 需绑 Passkey,被采纳记共建信誉)')}</option>
|
|
9700
|
+
</select>
|
|
9701
|
+
</div>
|
|
9702
|
+
<div class="form-group">
|
|
9703
|
+
<label class="form-label">${t('描述')} *</label>
|
|
9704
|
+
<textarea class="form-control" id="bfb-text" rows="5" maxlength="2000" placeholder="${t('发生了什么 / 你的想法(≥5 字)')}"></textarea>
|
|
9705
|
+
</div>
|
|
9706
|
+
<div id="bfb-msg" style="margin:8px 0"></div>
|
|
9707
|
+
<div style="display:flex;gap:8px">
|
|
9708
|
+
<button class="btn btn-gray" style="flex:1" onclick="this.closest('.js-modal').remove()">${t('取消')}</button>
|
|
9709
|
+
<button class="btn btn-primary" style="flex:1" onclick="submitBuildFeedback('${escHtml(page)}')">${t('提交')}</button>
|
|
9710
|
+
</div>
|
|
9711
|
+
<div style="text-align:center;margin-top:12px">
|
|
9712
|
+
<a onclick="this.closest('.js-modal').remove();navigate('#build-feedback')" style="font-size:12px;color:#4f46e5;cursor:pointer">${t('查看我的反馈进度 →')}</a>
|
|
9713
|
+
</div>
|
|
9714
|
+
</div>
|
|
9715
|
+
</div>`
|
|
9716
|
+
const div = document.createElement('div'); div.innerHTML = html; document.body.appendChild(div.firstElementChild)
|
|
9717
|
+
}
|
|
9718
|
+
|
|
9719
|
+
window.submitBuildFeedback = async (page) => {
|
|
9720
|
+
const type = document.getElementById('bfb-type').value
|
|
9721
|
+
const text = document.getElementById('bfb-text').value.trim()
|
|
9722
|
+
const msg = document.getElementById('bfb-msg')
|
|
9723
|
+
if (text.length < 5) { msg.innerHTML = alert$('error', t('请至少写 5 个字')); return }
|
|
9724
|
+
msg.innerHTML = loading$()
|
|
9725
|
+
const res = await POST('/build-feedback', { type, text, area: page, scene: [{ page }] })
|
|
9726
|
+
if (res.error) { msg.innerHTML = alert$('error', res.error); return } // 如 proposal 无 Passkey → 后端返回引导
|
|
9727
|
+
document.querySelector('.js-modal')?.remove()
|
|
9728
|
+
toast$(res.status === 'duplicate' ? t('已有类似建议,已合并 🙌') : t('反馈已提交,谢谢! 🙏'))
|
|
9729
|
+
}
|
|
9730
|
+
|
|
9731
|
+
async function renderMyBuildFeedback(app) {
|
|
9732
|
+
if (!state.user) { renderLogin(); return }
|
|
9733
|
+
app.innerHTML = shell(loading$(), 'me')
|
|
9734
|
+
const r = await GET('/build-feedback/mine')
|
|
9735
|
+
const list = r?.feedback || []
|
|
9736
|
+
const SL = () => ({ received: t('已收到'), triaged: t('已分类'), in_progress: t('处理中'), resolved: t('已采纳 ✅'), declined: t('未采纳'), duplicate: t('已合并') })
|
|
9737
|
+
const SC = { received: '#d97706', triaged: '#4f46e5', in_progress: '#4f46e5', resolved: '#16a34a', declined: '#6b7280', duplicate: '#6b7280' }
|
|
9738
|
+
const TL = () => ({ ux_issue: t('体验问题'), bug: 'Bug', proposal: t('提案') })
|
|
9739
|
+
const rows = list.length === 0
|
|
9740
|
+
? `<div style="text-align:center;padding:40px;color:#9ca3af"><div style="font-size:48px;margin-bottom:8px">💬</div><div style="font-size:13px">${t('还没有反馈 — 用着有问题随时点左下角 💬')}</div></div>`
|
|
9741
|
+
: list.map(f => `
|
|
9742
|
+
<div class="card" style="padding:12px;margin-bottom:8px">
|
|
9743
|
+
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px">
|
|
9744
|
+
<span style="font-size:11px;color:#6b7280">${TL()[f.type] || f.type}${f.area ? ` · <code style="background:#f3f4f6;padding:1px 4px;border-radius:4px">${escHtml(String(f.area))}</code>` : ''}</span>
|
|
9745
|
+
<span style="font-size:10px;font-weight:600;color:#fff;background:${SC[f.status] || '#6b7280'};padding:2px 8px;border-radius:99px;white-space:nowrap">${SL()[f.status] || f.status}</span>
|
|
9746
|
+
</div>
|
|
9747
|
+
<div style="font-size:13px;color:#374151">${escHtml(String(f.body || ''))}</div>
|
|
9748
|
+
${f.resolution ? `<div style="font-size:11px;color:#6b7280;margin-top:6px;padding-top:6px;border-top:1px solid #f3f4f6">↳ ${escHtml(String(f.resolution))}</div>` : ''}
|
|
9749
|
+
${Number(f.credited_points) > 0 ? `<div style="font-size:11px;color:#16a34a;margin-top:4px">🏅 +${f.credited_points} ${t('共建信誉')}</div>` : ''}
|
|
9750
|
+
</div>`).join('')
|
|
9751
|
+
app.innerHTML = shell(`
|
|
9752
|
+
<h1 class="page-title">💬 ${t('我的反馈')}</h1>
|
|
9753
|
+
<div style="font-size:12px;color:#6b7280;margin-bottom:14px">${t('你提的问题 / 建议进展 — 被采纳的提案会记入共建信誉')}</div>
|
|
9754
|
+
<button class="btn btn-primary btn-sm" style="margin-bottom:12px" onclick="openBuildFeedback()">+ ${t('新反馈 / 建议')}</button>
|
|
9755
|
+
${rows}
|
|
9756
|
+
`, 'me')
|
|
9757
|
+
}
|
|
9758
|
+
|
|
8521
9759
|
// W7 客服 ticket-thread 视图
|
|
8522
9760
|
const TICKET_TYPE_META = {
|
|
8523
9761
|
created: { icon: '🛟', title: '新建工单', border: '#d97706' },
|
|
@@ -9572,6 +10810,11 @@ window.sharePromoLink = async (productId, title) => {
|
|
|
9572
10810
|
// 商品级 verified 校验 + 拿/建 shareable,走 /s/<id> 短链
|
|
9573
10811
|
const res = await POST(`/products/${productId}/get-or-create-share`)
|
|
9574
10812
|
if (res.error) {
|
|
10813
|
+
if (res.error === 'rewards_opt_in_required') {
|
|
10814
|
+
const msg = (window._lang === 'en' ? res.message_en : res.message_zh) || res.message_zh || res.error
|
|
10815
|
+
const missing = Array.isArray(res.missing_requirements) ? `\n\n${t('待补')}: ${res.missing_requirements.join(', ')}` : ''
|
|
10816
|
+
return alert(`⚠ ${msg}${missing}\n\n${t('前往 #me 申请共建身份')}`)
|
|
10817
|
+
}
|
|
9575
10818
|
return alert(`⚠ ${res.error}${res.completed_orders === 0 ? '\n' + t('完成该商品的购买后再分享') : ''}`)
|
|
9576
10819
|
}
|
|
9577
10820
|
const link = `${location.origin}${res.short_url}`
|
|
@@ -19303,6 +20546,45 @@ function buildDisputeHtml(dispute, user) {
|
|
|
19303
20546
|
<button class="btn btn-primary btn-sm" style="width:auto" onclick="handleDisputeRespond('${dispute.id}','${dispute.order_id}')">提交反驳证据</button>
|
|
19304
20547
|
</div>` : ''
|
|
19305
20548
|
|
|
20549
|
+
// ── 仲裁员:暂停 / 恢复自动判定时钟(playbook §2.1)────
|
|
20550
|
+
// 补证据期 > 48h 时必须显式暂停,避免被协议层 48h 沉默判架空
|
|
20551
|
+
const isPaused = dispute.auto_judge_paused_until && dispute.auto_judge_paused_until * 1000 > Date.now()
|
|
20552
|
+
const pauseSection = isArbitrator && dispute.status !== 'resolved' && !dispute.ruling_type ? (
|
|
20553
|
+
isPaused ? `
|
|
20554
|
+
<div style="margin-top:12px;background:#fef3c7;border:1px solid #fcd34d;border-radius:8px;padding:12px">
|
|
20555
|
+
<div style="font-size:13px;font-weight:600;color:#92400e;margin-bottom:6px">⏸️ ${t('自动判定时钟已冻结')}</div>
|
|
20556
|
+
<div style="font-size:12px;color:#78350f;line-height:1.6;margin-bottom:8px">
|
|
20557
|
+
${t('到期')}: ${new Date(dispute.auto_judge_paused_until * 1000).toLocaleString()}<br>
|
|
20558
|
+
${t('理由')}: ${escHtml(dispute.auto_judge_pause_reason || '—')}
|
|
20559
|
+
</div>
|
|
20560
|
+
<button class="btn btn-sm btn-outline" style="font-size:12px" onclick="handleArbitratorResume('${dispute.id}')">▶️ ${t('立即解冻(直接进入裁决/补证后)')}</button>
|
|
20561
|
+
</div>
|
|
20562
|
+
` : `
|
|
20563
|
+
<div style="margin-top:12px">
|
|
20564
|
+
<button class="btn btn-outline btn-sm" style="width:auto;font-size:12px" onclick="(function(){var s=document.getElementById('pause-section-${dispute.id}');s.style.display=s.style.display==='none'?'':'none'})()">⏸️ ${t('暂停自动判定时钟')}</button>
|
|
20565
|
+
<div id="pause-section-${dispute.id}" style="display:none;margin-top:10px;background:#fffbeb;border:1px solid #fcd34d;border-radius:8px;padding:12px">
|
|
20566
|
+
<div style="font-size:12px;color:#92400e;line-height:1.6;margin-bottom:8px">${t('playbook §2.1:补证据期 > 48h 时显式调用,冻结协议自动判定时钟。最大窗口 7 天。')}</div>
|
|
20567
|
+
<div id="pause-msg-${dispute.id}"></div>
|
|
20568
|
+
<div style="margin-bottom:8px">
|
|
20569
|
+
<label style="font-size:12px;color:#6b7280;display:block;margin-bottom:4px">${t('暂停理由(≥10 字符,公开 audit_log)')}</label>
|
|
20570
|
+
<textarea class="form-control" id="pause-reason-${dispute.id}" rows="3" placeholder="${t('例:已通知 buyer 补开箱视频,等 72h')}" style="font-size:13px"></textarea>
|
|
20571
|
+
</div>
|
|
20572
|
+
<div style="margin-bottom:10px">
|
|
20573
|
+
<label style="font-size:12px;color:#6b7280;display:block;margin-bottom:4px">${t('冻结到期')}</label>
|
|
20574
|
+
<select class="form-control" id="pause-hours-${dispute.id}" style="font-size:13px">
|
|
20575
|
+
<option value="24">24h</option>
|
|
20576
|
+
<option value="48" selected>48h</option>
|
|
20577
|
+
<option value="72">72h</option>
|
|
20578
|
+
<option value="120">5d</option>
|
|
20579
|
+
<option value="168">7d ${t('(最大)')}</option>
|
|
20580
|
+
</select>
|
|
20581
|
+
</div>
|
|
20582
|
+
<button class="btn btn-primary btn-sm" style="width:auto" onclick="handleArbitratorPause('${dispute.id}')">⏸️ ${t('暂停时钟')}</button>
|
|
20583
|
+
</div>
|
|
20584
|
+
</div>
|
|
20585
|
+
`
|
|
20586
|
+
) : ''
|
|
20587
|
+
|
|
19306
20588
|
// ── 仲裁员:发起补充证据请求 ──────────────────
|
|
19307
20589
|
const requestEvidenceSection = isArbitrator && dispute.status !== 'resolved' ? `
|
|
19308
20590
|
<div style="margin-top:12px">
|
|
@@ -19435,6 +20717,7 @@ function buildDisputeHtml(dispute, user) {
|
|
|
19435
20717
|
${respondSection}
|
|
19436
20718
|
${partyAddEvidenceSection}
|
|
19437
20719
|
${uploadBlobSection}
|
|
20720
|
+
${pauseSection}
|
|
19438
20721
|
${requestEvidenceSection}
|
|
19439
20722
|
${arbitrateSection}
|
|
19440
20723
|
|
|
@@ -19707,6 +20990,30 @@ window.handleRequestEvidence = async (disputeId) => {
|
|
|
19707
20990
|
setTimeout(() => renderDisputeDetail(document.getElementById('app'), disputeId), 1200)
|
|
19708
20991
|
}
|
|
19709
20992
|
|
|
20993
|
+
// task #1093 stage 6: arbitrator pause / resume auto-judge clock (playbook §2.1)
|
|
20994
|
+
window.handleArbitratorPause = async (disputeId) => {
|
|
20995
|
+
const reason = document.getElementById('pause-reason-' + disputeId)?.value?.trim()
|
|
20996
|
+
const hours = Number(document.getElementById('pause-hours-' + disputeId)?.value) || 48
|
|
20997
|
+
const msgEl = document.getElementById('pause-msg-' + disputeId)
|
|
20998
|
+
if (!reason || reason.length < 10) {
|
|
20999
|
+
msgEl.innerHTML = alert$('error', t('暂停理由至少 10 字符'))
|
|
21000
|
+
return
|
|
21001
|
+
}
|
|
21002
|
+
const untilTs = Math.floor(Date.now() / 1000) + hours * 3600
|
|
21003
|
+
msgEl.innerHTML = `<div class="alert alert-info"><span class="spinner"></span>${t('提交中...')}</div>`
|
|
21004
|
+
const res = await POST(`/disputes/${disputeId}/arbitrator-pause-auto-judge`, { reason, until_ts: untilTs })
|
|
21005
|
+
if (res.error) { msgEl.innerHTML = alert$('error', res.error); return }
|
|
21006
|
+
msgEl.innerHTML = alert$('success', t('自动判定时钟已冻结'))
|
|
21007
|
+
setTimeout(() => renderDisputeDetail(document.getElementById('app'), disputeId), 1200)
|
|
21008
|
+
}
|
|
21009
|
+
|
|
21010
|
+
window.handleArbitratorResume = async (disputeId) => {
|
|
21011
|
+
if (!confirm(t('确认立即解冻自动判定时钟? 协议层 48h 沉默判定将重新生效。'))) return
|
|
21012
|
+
const res = await POST(`/disputes/${disputeId}/arbitrator-resume-auto-judge`, {})
|
|
21013
|
+
if (res.error) { alert(res.error); return }
|
|
21014
|
+
renderDisputeDetail(document.getElementById('app'), disputeId)
|
|
21015
|
+
}
|
|
21016
|
+
|
|
19710
21017
|
// 当事方提交证据
|
|
19711
21018
|
window.handleSubmitEvidence = async (requestId, disputeId) => {
|
|
19712
21019
|
const type = document.getElementById(`er-type-${requestId}`)?.value
|
|
@@ -23553,8 +24860,7 @@ async function renderWallet(app) {
|
|
|
23553
24860
|
|
|
23554
24861
|
<!-- 卖家收入分类速览(仅 seller 显示)- 2026-05-24 对碰列只在 PV 允许地区显示 -->
|
|
23555
24862
|
${state.user?.role === 'seller' ? (() => {
|
|
23556
|
-
const
|
|
23557
|
-
const _pvOK = _maxLvl >= 3
|
|
24863
|
+
const _pvOK = Number(state.user?.region_pv_enabled ?? 0) === 1 // 2026-06-04 解耦:PV 列读 pv_enabled,不再绑 max≥3
|
|
23558
24864
|
const cols = _pvOK ? 4 : 3
|
|
23559
24865
|
return `
|
|
23560
24866
|
<div class="card" style="margin-bottom:12px;padding:14px">
|
|
@@ -24548,25 +25854,9 @@ if ('serviceWorker' in navigator) {
|
|
|
24548
25854
|
navigator.serviceWorker.register('/sw.js').catch(() => {})
|
|
24549
25855
|
}
|
|
24550
25856
|
|
|
24551
|
-
//
|
|
24552
|
-
|
|
24553
|
-
window.
|
|
24554
|
-
e.preventDefault()
|
|
24555
|
-
_deferredInstallPrompt = e
|
|
24556
|
-
// 触发一次重渲,让 navbar install 按钮出现
|
|
24557
|
-
try { if (typeof route === 'function') route() } catch {}
|
|
24558
|
-
})
|
|
24559
|
-
window.addEventListener('appinstalled', () => {
|
|
24560
|
-
_deferredInstallPrompt = null
|
|
24561
|
-
try { if (typeof route === 'function') route() } catch {}
|
|
24562
|
-
})
|
|
24563
|
-
window.canInstallPWA = () => !!_deferredInstallPrompt
|
|
24564
|
-
window.doInstallPWA = async () => {
|
|
24565
|
-
if (!_deferredInstallPrompt) return
|
|
24566
|
-
_deferredInstallPrompt.prompt()
|
|
24567
|
-
try { await _deferredInstallPrompt.userChoice } catch {}
|
|
24568
|
-
_deferredInstallPrompt = null
|
|
24569
|
-
}
|
|
25857
|
+
// 上面 line ~25263 已注册 beforeinstallprompt 监听 + doInstallPWA(带 iOS/通用引导 fallback);
|
|
25858
|
+
// 此处只补一个 canInstallPWA helper 给 navbar 按钮判 visibility 用。
|
|
25859
|
+
window.canInstallPWA = () => !!window._installPromptEvent || isIOS()
|
|
24570
25860
|
|
|
24571
25861
|
// 离线状态全局 banner
|
|
24572
25862
|
function ensureOfflineBanner() {
|
|
@@ -27834,9 +29124,20 @@ async function renderLeaderboard(app) {
|
|
|
27834
29124
|
// 2026-05-23 隐私第一原理:buyers/sellers 榜不再展示 GMV 金额(运营状态私密)
|
|
27835
29125
|
const tabBtn = (k, label) => `<button onclick="setLeaderboardKind('${k}')" style="padding:6px 14px;border-radius:99px;font-size:12px;font-weight:600;cursor:pointer;border:1px solid ${kind===k?'#4f46e5':'#e5e7eb'};background:${kind===k?'#eef2ff':'#fff'};color:${kind===k?'#4338ca':'#6b7280'}">${label}</button>`
|
|
27836
29126
|
|
|
29127
|
+
// #1080 audit: spec §6.1 mandatory banner for arbitrators/verifiers leaderboards
|
|
29128
|
+
// (governance roles where reward-distribution misinterpretation is highest risk)
|
|
29129
|
+
const isGovernanceLeaderboard = kind === 'arbitrators' || kind === 'verifiers'
|
|
29130
|
+
const governanceBanner = isGovernanceLeaderboard ? `
|
|
29131
|
+
<div style="margin-bottom:14px;padding:12px;background:#fffbeb;border:1px solid #fcd34d;border-radius:8px;font-size:12px;color:#7c2d12;line-height:1.6">
|
|
29132
|
+
<div style="font-weight:600;margin-bottom:4px">📊 ${t('本榜单仅展示履职数据,不构成 token / 经济利益分配')}</div>
|
|
29133
|
+
<div style="opacity:0.85">${t('Reward distribution 机制由 phase D first DAO 决定。详 docs/GOVERNANCE-LEADERBOARD-SPEC.md。')}</div>
|
|
29134
|
+
</div>
|
|
29135
|
+
` : ''
|
|
29136
|
+
|
|
27837
29137
|
document.getElementById('lb-root').innerHTML = `
|
|
27838
29138
|
<h2 style="font-size:18px;font-weight:700;margin-bottom:6px">🏆 ${t('排行榜')}</h2>
|
|
27839
29139
|
<div style="font-size:11px;color:#6b7280;margin-bottom:14px;line-height:1.5">${t('协议不分发流量;越多真实推荐 / 点赞 → 越靠前')}</div>
|
|
29140
|
+
${governanceBanner}
|
|
27840
29141
|
<div style="display:flex;gap:6px;margin-bottom:14px;flex-wrap:wrap">
|
|
27841
29142
|
${tabBtn('products', '📦 ' + t('商品榜'))}
|
|
27842
29143
|
${tabBtn('value_products', '💎 ' + t('性价比榜'))}
|
|
@@ -27901,16 +29202,21 @@ async function renderLeaderboard(app) {
|
|
|
27901
29202
|
`
|
|
27902
29203
|
}).join('') :
|
|
27903
29204
|
kind === 'arbitrators' ? items.map((u, i) => {
|
|
27904
|
-
|
|
27905
|
-
|
|
29205
|
+
// #1080 audit: spec §4 — fairness 阈值 < 10 cases(比 accuracy 的 < 5 更严,因为
|
|
29206
|
+
// fairness 公式需要更多样本才统计稳定)。当前 arbitrator metric 只 fairness,所以用 10。
|
|
29207
|
+
const casesCount = Number(u.cases_count || 0)
|
|
29208
|
+
const isInsufficient = casesCount < 10
|
|
29209
|
+
const fs = isInsufficient ? t('insufficient data') : (u.fairness_score != null ? (u.fairness_score * 100).toFixed(1) + '%' : '—')
|
|
29210
|
+
// spec §3:无 "best/worst" 价值判断 — 中性灰色显示,不再 green/yellow/red 暗示评级
|
|
29211
|
+
const fsColor = isInsufficient ? '#9ca3af' : '#374151'
|
|
27906
29212
|
const totalRatings = Number(u.total_yes || 0) + Number(u.total_no || 0)
|
|
27907
29213
|
return `
|
|
27908
|
-
<div class="card" style="padding:12px;margin-bottom:8px;display:flex;gap:10px;align-items:center" title="${t('
|
|
27909
|
-
<div style="font-size:24px;font-weight:800;color
|
|
29214
|
+
<div class="card" style="padding:12px;margin-bottom:8px;display:flex;gap:10px;align-items:center" title="${t('仲裁员公平评价(< 10 案显 insufficient data,spec §4)')}">
|
|
29215
|
+
<div style="font-size:24px;font-weight:800;color:#9ca3af;min-width:32px;text-align:center">${i+1}</div>
|
|
27910
29216
|
<div style="flex:1;min-width:0">
|
|
27911
29217
|
<div style="font-weight:600;font-size:14px">⚖ @${escHtml(u.handle || u.name?.slice(0,8) || '?')}</div>
|
|
27912
29218
|
<div style="font-size:11px;color:#6b7280;margin-top:2px">
|
|
27913
|
-
${
|
|
29219
|
+
${casesCount} ${t('案')}
|
|
27914
29220
|
${totalRatings > 0 ? ` · 👍 ${u.total_yes} / 👎 ${u.total_no}` : ` · <span style="color:#9ca3af">${t('暂无公众评价')}</span>`}
|
|
27915
29221
|
</div>
|
|
27916
29222
|
</div>
|
|
@@ -27922,22 +29228,25 @@ async function renderLeaderboard(app) {
|
|
|
27922
29228
|
`
|
|
27923
29229
|
}).join('') :
|
|
27924
29230
|
kind === 'verifiers' ? items.map((u, i) => {
|
|
27925
|
-
|
|
27926
|
-
const
|
|
27927
|
-
const
|
|
29231
|
+
// #1080 audit: spec §4 < 5 cases → "insufficient data"
|
|
29232
|
+
const tasksDone = Number(u.tasks_done || 0)
|
|
29233
|
+
const isInsufficient = tasksDone < 5
|
|
29234
|
+
const acc = isInsufficient ? t('insufficient data') : (u.accuracy != null ? (u.accuracy * 100).toFixed(1) + '%' : '—')
|
|
29235
|
+
// spec §3: 无 "best/worst" 价值判断 — 中性显示
|
|
29236
|
+
const accColor = isInsufficient ? '#9ca3af' : '#374151'
|
|
27928
29237
|
const wrongCount = Number(u.tasks_wrong || 0)
|
|
27929
29238
|
return `
|
|
27930
|
-
<div class="card" style="padding:12px;margin-bottom:8px;display:flex;gap:10px;align-items:center" title="${t('正确数 / 总任务数 · 命中率
|
|
27931
|
-
<div style="font-size:24px;font-weight:800;color
|
|
29239
|
+
<div class="card" style="padding:12px;margin-bottom:8px;display:flex;gap:10px;align-items:center" title="${t('正确数 / 总任务数 · 命中率(< 5 任务显 insufficient data,spec §4)')}">
|
|
29240
|
+
<div style="font-size:24px;font-weight:800;color:#9ca3af;min-width:32px;text-align:center">${i+1}</div>
|
|
27932
29241
|
<div style="flex:1;min-width:0">
|
|
27933
29242
|
<div style="font-weight:600;font-size:14px;display:flex;align-items:center;gap:6px;flex-wrap:wrap">
|
|
27934
29243
|
@${escHtml(u.handle || u.name?.slice(0,8) || '?')}
|
|
27935
29244
|
${u.tier ? `<span style="font-size:10px;background:#ede9fe;color:#5b21b6;padding:1px 6px;border-radius:99px">${u.tier}</span>` : ''}
|
|
27936
|
-
${
|
|
29245
|
+
${isInsufficient ? `<span style="font-size:10px;background:#fef3c7;color:#92400e;padding:1px 6px;border-radius:99px">${t('新人')}</span>` : ''}
|
|
27937
29246
|
</div>
|
|
27938
29247
|
<div style="font-size:11px;color:#6b7280;margin-top:2px">
|
|
27939
|
-
✓ ${u.tasks_correct} ${t('正确')} / ${
|
|
27940
|
-
${wrongCount > 0 ? ` · <span style="color:#
|
|
29248
|
+
✓ ${u.tasks_correct} ${t('正确')} / ${tasksDone} ${t('总任务')}
|
|
29249
|
+
${wrongCount > 0 ? ` · <span style="color:#9ca3af">✗ ${wrongCount} ${t('偏离')}</span>` : ''}
|
|
27941
29250
|
</div>
|
|
27942
29251
|
</div>
|
|
27943
29252
|
<div style="text-align:right">
|
|
@@ -28913,6 +30222,13 @@ async function renderBuyerMyHome(app) {
|
|
|
28913
30222
|
`
|
|
28914
30223
|
}
|
|
28915
30224
|
|
|
30225
|
+
// 阶段 4(#1093):新治理 onboarding 入口 — 始终可访问我的治理岗位面板(卸任 / 申诉 / 历史)
|
|
30226
|
+
const governanceMeSection = (isExternalArb || isExternalVerifier) ? `
|
|
30227
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px">
|
|
30228
|
+
${card('🏛', t('我的治理岗位'), t('在岗 / 申诉 / 卸任'), '#governance-me')}
|
|
30229
|
+
</div>
|
|
30230
|
+
` : ''
|
|
30231
|
+
|
|
28916
30232
|
// ③ 通用索赔任务 tile
|
|
28917
30233
|
const claimsTile = myClaimTasks > 0 ? `
|
|
28918
30234
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px">
|
|
@@ -28921,11 +30237,12 @@ async function renderBuyerMyHome(app) {
|
|
|
28921
30237
|
` : ''
|
|
28922
30238
|
|
|
28923
30239
|
// 拼装:有内容才显标题
|
|
28924
|
-
const hasAnyTrust = verifierSection || arbSection || claimsTile
|
|
30240
|
+
const hasAnyTrust = verifierSection || arbSection || claimsTile || governanceMeSection
|
|
28925
30241
|
const trustGrid = hasAnyTrust ? `
|
|
28926
30242
|
${(!isExternalVerifier && !isExternalArb) ? `<div style="font-size:12px;color:#6b7280;font-weight:600;margin:14px 0 6px">🛡 ${t('信任与协议')}</div>` : ''}
|
|
28927
30243
|
${verifierSection}
|
|
28928
30244
|
${arbSection}
|
|
30245
|
+
${governanceMeSection}
|
|
28929
30246
|
${claimsTile}
|
|
28930
30247
|
` : ''
|
|
28931
30248
|
|
|
@@ -30487,7 +31804,7 @@ async function renderCharityFund(app) {
|
|
|
30487
31804
|
}[k] || '·')
|
|
30488
31805
|
document.getElementById('cf-root').innerHTML = `
|
|
30489
31806
|
<h2 style="font-size:18px;font-weight:700;margin-bottom:6px">🏦 ${t('慈善基金')}</h2>
|
|
30490
|
-
<div style="font-size:11px;color:#6b7280;margin-bottom:14px;line-height:1.5">${t('协议持有 · 公开账本 · 任何人可捐款 ·
|
|
31807
|
+
<div style="font-size:11px;color:#6b7280;margin-bottom:14px;line-height:1.5">${t('协议持有 · 公开账本 · 任何人可捐款 · 专款专用于慈善许愿 · 治理委员会决定出金')}</div>
|
|
30491
31808
|
|
|
30492
31809
|
<div class="card" style="padding:18px;margin-bottom:14px;background:linear-gradient(135deg,#fef2f2,#fff7ed)">
|
|
30493
31810
|
<div style="display:flex;justify-content:space-between;align-items:center">
|
|
@@ -30501,9 +31818,6 @@ async function renderCharityFund(app) {
|
|
|
30501
31818
|
<div style="display:grid;grid-template-columns:repeat(2,1fr);gap:6px;font-size:10px;text-align:left;background:#fff;padding:8px;border-radius:6px">
|
|
30502
31819
|
<div style="display:flex;justify-content:space-between"><span style="color:#9ca3af">💝 ${t('用户捐款')}</span><span style="font-weight:700;color:#9333ea">${Number(f.total_donated||0).toFixed(1)}</span></div>
|
|
30503
31820
|
<div style="display:flex;justify-content:space-between"><span style="color:#9ca3af">🙏 ${t('还愿转入')}</span><span style="font-weight:700;color:#ea580c">${Number(f.total_redirected||0).toFixed(1)}</span></div>
|
|
30504
|
-
<div style="display:flex;justify-content:space-between"><span style="color:#9ca3af">🔗 ${t('分享链兜底')}</span><span style="font-weight:700;color:#0369a1">${Number(f.total_chain_gap||0).toFixed(1)}</span></div>
|
|
30505
|
-
<div style="display:flex;justify-content:space-between"><span style="color:#9ca3af">👤 ${t('孤儿订单兜底')}</span><span style="font-weight:700;color:#7c3aed">${Number(f.total_orphan_sponsor||0).toFixed(1)}</span></div>
|
|
30506
|
-
${Number(f.total_region_cap||0) > 0 ? `<div style="display:flex;justify-content:space-between"><span style="color:#9ca3af">⚖️ ${t('区域禁 MLM 入池')}</span><span style="font-weight:700;color:#dc2626">${Number(f.total_region_cap||0).toFixed(1)}</span></div>` : ''}
|
|
30507
31821
|
</div>
|
|
30508
31822
|
<div style="display:flex;justify-content:space-between;margin-top:6px;font-size:10px;background:#fff;padding:6px 8px;border-radius:6px">
|
|
30509
31823
|
<span style="color:#9ca3af">🤲 ${t('累计已拨款')}</span><span style="font-weight:700;color:#16a34a">${Number(f.total_disbursed||0).toFixed(1)}</span>
|