@seasonkoh/webaz 0.1.26 → 0.1.27
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/LICENSE +2 -2
- package/NOTICE +24 -3
- package/README.md +74 -330
- package/README.zh-CN.md +419 -0
- package/dist/layer0-foundation/L0-2-state-machine/genuine-sale.js +21 -0
- package/dist/layer0-foundation/L0-5-manifest/manifest.js +8 -3
- package/dist/layer1-agent/L1-1-mcp-server/auth.js +13 -1
- package/dist/layer1-agent/L1-1-mcp-server/server.js +36 -28
- package/dist/layer2-business/L2-9-contribution/admin-coordination-ingestion-engine.js +181 -0
- package/dist/layer2-business/L2-9-contribution/admin-coordination-resolver.js +114 -0
- package/dist/layer2-business/L2-9-contribution/admin-coordination-store.js +251 -0
- package/dist/layer2-business/L2-9-contribution/admin-operator-claim-workflow.js +390 -0
- package/dist/layer2-business/L2-9-contribution/build-task-agent-metadata-store.js +24 -0
- package/dist/layer2-business/L2-9-contribution/build-task-participation.js +6 -2
- package/dist/layer2-business/L2-9-contribution/build-task-quota.js +337 -0
- package/dist/layer2-business/L2-9-contribution/build-task-read.js +25 -2
- package/dist/layer2-business/L2-9-contribution/build-tasks-engine.js +57 -7
- package/dist/layer2-business/L2-9-contribution/canonical-contribution-target.js +1 -1
- package/dist/layer2-business/L2-9-contribution/contribution-facts-read.js +66 -0
- package/dist/layer2-business/L2-9-contribution/task-proposal-draft.js +187 -18
- package/dist/layer2-business/L2-9-contribution/task-proposal-store.js +29 -4
- package/dist/ledger.js +1 -1
- package/dist/pwa/admin-audit.js +38 -0
- package/dist/pwa/anti-abuse-thresholds.js +135 -0
- package/dist/pwa/cf-origin-guard.js +33 -0
- package/dist/pwa/contract-fingerprint.js +1 -0
- package/dist/pwa/data/onboarding-cases.js +2 -2
- package/dist/pwa/data/onboarding-quiz.js +1 -1
- package/dist/pwa/economic-participation.js +2 -2
- package/dist/pwa/integration-contract.js +46 -4
- package/dist/pwa/internal/pv-settlement.js +12 -0
- package/dist/pwa/internal/wallet-signer.js +26 -0
- package/dist/pwa/public/app.js +679 -679
- package/dist/pwa/public/i18n.js +15 -28
- package/dist/pwa/public/index.html +1 -1
- package/dist/pwa/public/openapi.json +4760 -2769
- package/dist/pwa/pv-kill-switch.js +31 -0
- package/dist/pwa/routes/admin-admins.js +48 -1
- package/dist/pwa/routes/admin-analytics.js +1 -10
- package/dist/pwa/routes/admin-atomic.js +4 -17
- package/dist/pwa/routes/admin-operator-claims.js +280 -0
- package/dist/pwa/routes/admin-reports.js +4 -26
- package/dist/pwa/routes/admin-tokenomics.js +2 -76
- package/dist/pwa/routes/admin-users-lifecycle.js +1 -14
- package/dist/pwa/routes/admin-users-query.js +23 -1
- package/dist/pwa/routes/admin-wallet-ops.js +1 -1
- package/dist/pwa/routes/auth-read.js +1 -5
- package/dist/pwa/routes/auth-register.js +3 -13
- package/dist/pwa/routes/build-task-quota.js +113 -0
- package/dist/pwa/routes/claim-verify.js +15 -11
- package/dist/pwa/routes/contribution-facts.js +18 -0
- package/dist/pwa/routes/dispute-cases.js +5 -4
- package/dist/pwa/routes/growth.js +3 -3
- package/dist/pwa/routes/orders-action.js +27 -10
- package/dist/pwa/routes/orders-create.js +1 -1
- package/dist/pwa/routes/products-meta.js +19 -6
- package/dist/pwa/routes/profile-placement.js +1 -1
- package/dist/pwa/routes/promoter.js +10 -29
- package/dist/pwa/routes/public-build-tasks.js +5 -1
- package/dist/pwa/routes/public-utils.js +9 -12
- package/dist/pwa/routes/referral.js +5 -26
- package/dist/pwa/routes/rewards-apply.js +3 -2
- package/dist/pwa/routes/share-redirects.js +1 -1
- package/dist/pwa/routes/shareables-interactions.js +2 -1
- package/dist/pwa/routes/task-proposals.js +85 -9
- package/dist/pwa/routes/users-public.js +1 -4
- package/dist/pwa/routes/wallet-read.js +2 -14
- package/dist/pwa/routes/webauthn.js +1 -1
- package/dist/pwa/server.js +156 -469
- package/dist/settlement-math.js +3 -3
- package/dist/version.js +6 -4
- package/package.json +33 -7
- package/dist/index.js +0 -182
- package/dist/pwa/public/docs/ECONOMIC-MODEL.md +0 -287
- package/dist/pwa/public/docs/INTEGRATOR.md +0 -67
- package/dist/pwa/public/docs/META-RULES-FULL.md +0 -543
- package/dist/test-dispute.js +0 -153
- package/dist/test-manifest.js +0 -61
- package/dist/test-mcp-tools.js +0 -135
- package/dist/test-reputation.js +0 -116
- package/dist/test-skill-market.js +0 -101
package/dist/pwa/public/app.js
CHANGED
|
@@ -674,6 +674,8 @@ async function render(page, params) {
|
|
|
674
674
|
if (params[0] === 'notes') return renderMyNotes(app)
|
|
675
675
|
if (params[0] === 'settings') return renderMyHome(app, 'settings')
|
|
676
676
|
if (params[0] === 'advanced') return renderMyHome(app, 'advanced')
|
|
677
|
+
if (params[0] === 'quota-requests') return renderMyQuotaRequests(app)
|
|
678
|
+
if (params[0] === 'operator-claims') return renderMyOperatorClaims(app)
|
|
677
679
|
return renderMyHome(app, 'dashboard')
|
|
678
680
|
case 'note-new': return renderNoteCreate(app, params[0]) // order_id
|
|
679
681
|
case 'u': return renderUserProfile(app, params[0])
|
|
@@ -713,6 +715,8 @@ async function render(page, params) {
|
|
|
713
715
|
// 2026-05-24 #welcome 公开 ideas/邮箱订阅查看
|
|
714
716
|
if (params[0] === 'public-ideas') return renderAdminPublicIdeas(app)
|
|
715
717
|
if (params[0] === 'task-proposals') return renderAdminTaskProposals(app)
|
|
718
|
+
if (params[0] === 'quota-requests') return renderAdminBuildTaskQuota(app)
|
|
719
|
+
if (params[0] === 'operator-claims') return renderAdminOperatorClaims(app)
|
|
716
720
|
if (params[0] === 'params') return renderAdminParams(app)
|
|
717
721
|
if (params[0] === 'timeline' && params[1]) return renderAdminUserTimeline(app, params[1])
|
|
718
722
|
if (params[0] === 'timeline') return renderAdminUserTimelinePicker(app)
|
|
@@ -1285,14 +1289,17 @@ async function renderMyAdvanced(app) {
|
|
|
1285
1289
|
app.innerHTML = shell(loading$(), 'me')
|
|
1286
1290
|
const role = state.user.role
|
|
1287
1291
|
const isTrusted = ['admin', 'verifier', 'logistics', 'arbitrator'].includes(role)
|
|
1288
|
-
const [agentRes, skillsRes] = await Promise.all([
|
|
1292
|
+
const [agentRes, skillsRes, ocRes] = await Promise.all([
|
|
1289
1293
|
GET('/agents/me/reputation').catch(() => null),
|
|
1290
1294
|
GET('/skills/mine').catch(() => []),
|
|
1295
|
+
GET('/me/operator-claims').catch(() => null),
|
|
1291
1296
|
])
|
|
1292
1297
|
const trustScore = Math.round(agentRes?.trust_score || 0)
|
|
1293
1298
|
const level = agentRes?.level || 'new'
|
|
1294
1299
|
const lvlColor = { legend: '#dc2626', quality: '#9333ea', trusted: '#4f46e5', new: '#9ca3af' }[level] || '#6b7280'
|
|
1295
1300
|
const skillCount = (Array.isArray(skillsRes) ? skillsRes : []).length
|
|
1301
|
+
// 贡献归属入口:admin 常驻;普通用户仅当确有 operator-claim 关系(pending/active/history)时才显示,保持清爽
|
|
1302
|
+
const hasOperatorClaim = !!(ocRes && Array.isArray(ocRes.relationships) && ocRes.relationships.length)
|
|
1296
1303
|
|
|
1297
1304
|
const card = (icon, label, sub, hash) => `
|
|
1298
1305
|
<div class="card" onclick="location.hash='${hash}'" style="padding:14px;cursor:pointer;display:flex;align-items:center;gap:10px;min-height:64px">
|
|
@@ -1334,6 +1341,7 @@ async function renderMyAdvanced(app) {
|
|
|
1334
1341
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px">
|
|
1335
1342
|
${card('📜', t('Timeline'), t('全部事件按时间排列'), '#me/timeline')}
|
|
1336
1343
|
${card('📡', t('Webhook'), t('订阅事件 push 到外部端点'), '#me/webhooks')}
|
|
1344
|
+
${(role === 'admin' || hasOperatorClaim) ? card('🪪', t('贡献归属'), t('待确认的 admin 关联 / 关联记录'), '#me/operator-claims') : ''}
|
|
1337
1345
|
</div>
|
|
1338
1346
|
|
|
1339
1347
|
<div style="font-size:12px;color:#6b7280;font-weight:600;margin:14px 0 6px">🧠 ${t('技能市场')}</div>
|
|
@@ -1756,7 +1764,7 @@ async function renderProfile(app) {
|
|
|
1756
1764
|
</div>
|
|
1757
1765
|
<div style="display:flex;justify-content:space-between;align-items:center;padding:6px 0;border-bottom:1px solid #f3f4f6;font-size:13px">
|
|
1758
1766
|
<span style="color:#374151">${t('协议')}</span>
|
|
1759
|
-
<a href="https://github.com/
|
|
1767
|
+
<a href="https://github.com/webaz-protocol/webaz" target="_blank" style="color:#6366f1;text-decoration:none;font-size:12px">${t('源码仓库')} ↗</a>
|
|
1760
1768
|
</div>
|
|
1761
1769
|
<div style="display:flex;justify-content:space-between;align-items:center;padding:6px 0;border-bottom:1px solid #f3f4f6;font-size:13px">
|
|
1762
1770
|
<span style="color:#374151">🔔 ${t('推送通知')}</span>
|
|
@@ -3194,6 +3202,7 @@ async function renderAdminTaskProposals(app) {
|
|
|
3194
3202
|
<div style="display:flex;justify-content:space-between;gap:8px;align-items:flex-start">
|
|
3195
3203
|
<div style="font-weight:600;font-size:14px">${escHtml(p.title)}</div>${badge(p.status)}
|
|
3196
3204
|
</div>
|
|
3205
|
+
<div style="font-family:monospace;font-size:11px;color:#6b7280;margin-top:3px">${T('案件 ID', 'Case ID')}: ${escHtml(p.case_id || p.id)}</div>
|
|
3197
3206
|
<div style="font-size:13px;color:#52525B;line-height:1.5;margin-top:6px;white-space:pre-wrap">${escHtml(p.summary)}</div>
|
|
3198
3207
|
${field(T('建议领域', 'Area'), p.suggested_area)}
|
|
3199
3208
|
${field(T('预期结果', 'Outcome'), p.expected_outcome)}
|
|
@@ -3228,8 +3237,12 @@ async function renderAdminTaskProposals(app) {
|
|
|
3228
3237
|
</div>
|
|
3229
3238
|
${field(T('风险', 'Risk'), d.risk_level)}${field(T('可自助认领', 'Auto-claimable'), d.auto_claimable === 1 || d.auto_claimable === true ? T('是', 'yes') : T('否(需真人)', 'no (human)'))}
|
|
3230
3239
|
${field(T('来源建议', 'Source proposal'), d.source_proposal_id)}${field(T('创建人', 'Created by'), d.created_by)}
|
|
3240
|
+
<div id="draft-preview-${escHtml(d.id)}" style="display:none;margin-top:8px;border-top:1px dashed #c7d2fe;padding-top:8px;font-size:12px;color:#52525B;line-height:1.5"></div>
|
|
3231
3241
|
<div style="font-size:10px;color:#9ca3af;margin-top:6px">${T('发布前会校验交接字段;发布后进入正常任务板,可被参与者 agent 发现 / 认领 / 提交 PR。', 'Publish validates the handoff fields; once published it enters the normal task board — discoverable / claimable / PR-submittable by participant agents.')}</div>
|
|
3232
|
-
<
|
|
3242
|
+
<div style="display:flex;gap:6px;margin-top:8px;flex-wrap:wrap;align-items:center">
|
|
3243
|
+
<button onclick="previewDraft('${escHtml(d.id)}')" style="padding:6px 12px;border:1px solid #6366f1;background:#fff;color:#4338ca;border-radius:6px;font-size:12px;cursor:pointer;font-weight:600">👁 ${T('预览将发布内容', 'Preview what will be published')}</button>
|
|
3244
|
+
<button id="pub-btn-${escHtml(d.id)}" disabled title="${T('请先预览将发布的存储内容', 'Preview the stored content first')}" onclick="publishDraft('${escHtml(d.id)}')" style="padding:6px 14px;border:none;background:#d1d5db;color:#6b7280;border-radius:6px;font-size:12px;cursor:not-allowed;font-weight:600">${T('发布到任务板', 'Publish to board')}</button>
|
|
3245
|
+
</div>
|
|
3233
3246
|
</div>`
|
|
3234
3247
|
app.innerHTML = shell(`
|
|
3235
3248
|
<div style="padding:14px;max-width:920px;margin:0 auto">
|
|
@@ -3300,10 +3313,41 @@ window.createTaskDraft = async (id) => {
|
|
|
3300
3313
|
definition_of_done: v('dod'), expected_results: v('expect'),
|
|
3301
3314
|
}
|
|
3302
3315
|
const r = await POST('/admin/task-proposals/' + encodeURIComponent(id) + '/create-task-draft', body)
|
|
3316
|
+
if (r && r.error_code === 'RATE_LIMITED') { showRateLimitAffordance(r); return }
|
|
3303
3317
|
if (r.error) { toast$((r.missing && r.missing.length) ? ((en ? 'Missing: ' : '缺少:') + r.missing.join(', ')) : (r.error || 'failed')); return }
|
|
3304
3318
|
toast$(en ? 'Draft saved (unpublished)' : '草稿已保存(未发布)')
|
|
3305
3319
|
renderAdminTaskProposals(document.getElementById('app'))
|
|
3306
3320
|
}
|
|
3321
|
+
// Pre-publish preview: load the FULL stored draft body so publish is a decision against visible content.
|
|
3322
|
+
// Opening the preview also un-gates the (initially disabled) Publish button for this draft.
|
|
3323
|
+
window.previewDraft = async (taskId) => {
|
|
3324
|
+
// bilingual helper — T is local to renderAdminTaskProposals, NOT a global, so this top-level fn defines its own
|
|
3325
|
+
const T = (zh, en) => (window._lang === 'en' ? en : zh)
|
|
3326
|
+
const box = document.getElementById('draft-preview-' + taskId)
|
|
3327
|
+
if (!box) return
|
|
3328
|
+
box.style.display = ''
|
|
3329
|
+
box.innerHTML = t('加载中...')
|
|
3330
|
+
const r = await GET('/admin/build-task-drafts/' + encodeURIComponent(taskId)).catch(() => null)
|
|
3331
|
+
const d = r && r.draft
|
|
3332
|
+
if (!d) { box.innerHTML = `<span style="color:#dc2626">${T('预览加载失败', 'Preview failed to load')}</span>`; return }
|
|
3333
|
+
const m = d.agent_metadata || {}
|
|
3334
|
+
const li = (arr) => (Array.isArray(arr) && arr.length) ? `<ul style="margin:2px 0 6px 16px;padding:0">${arr.map((x) => `<li>${escHtml(String(x))}</li>`).join('')}</ul>` : `<div style="color:#9ca3af;margin-bottom:6px">—</div>`
|
|
3335
|
+
const txtBlock = (s) => `<div style="white-space:pre-wrap;margin-bottom:6px">${escHtml(String(s || '')) || '<span style="color:#9ca3af">—</span>'}</div>`
|
|
3336
|
+
const sec = (label, html) => `<div style="margin-top:6px"><div style="font-weight:600;color:#374151">${escHtml(label)}</div>${html}</div>`
|
|
3337
|
+
box.innerHTML = `<div style="font-size:11px;color:#6366f1;font-weight:600;margin-bottom:4px">${T('将要发布的存储内容(发布对此生效)', 'Stored content that will be published (publish acts on this)')}</div>`
|
|
3338
|
+
+ sec(T('说明', 'Description'), txtBlock(d.description))
|
|
3339
|
+
+ sec(T('验收标准', 'Acceptance criteria'), li(m.acceptance_criteria))
|
|
3340
|
+
+ sec(T('验证命令', 'Verification commands'), li(m.verification_commands))
|
|
3341
|
+
+ sec(T('允许路径', 'Allowed paths'), li(m.allowed_paths))
|
|
3342
|
+
+ sec(T('禁止路径', 'Forbidden paths'), li(m.forbidden_paths))
|
|
3343
|
+
+ sec(T('禁止动作', 'Forbidden actions'), li(m.prohibited_actions))
|
|
3344
|
+
+ sec(T('交付物', 'Deliverables'), li(m.deliverables))
|
|
3345
|
+
+ sec(T('完成定义', 'Definition of done'), txtBlock(m.definition_of_done))
|
|
3346
|
+
+ sec(T('预期结果', 'Expected results'), txtBlock(m.expected_results))
|
|
3347
|
+
const btn = document.getElementById('pub-btn-' + taskId)
|
|
3348
|
+
if (btn) { btn.disabled = false; btn.title = ''; btn.style.cssText = 'padding:6px 14px;border:none;background:#16a34a;color:#fff;border-radius:6px;font-size:12px;cursor:pointer;font-weight:600' }
|
|
3349
|
+
}
|
|
3350
|
+
|
|
3307
3351
|
window.publishDraft = async (taskId) => {
|
|
3308
3352
|
const en = window._lang === 'en'
|
|
3309
3353
|
const r = await POST('/admin/build-task-drafts/' + encodeURIComponent(taskId) + '/publish', {})
|
|
@@ -3312,6 +3356,449 @@ window.publishDraft = async (taskId) => {
|
|
|
3312
3356
|
renderAdminTaskProposals(document.getElementById('app'))
|
|
3313
3357
|
}
|
|
3314
3358
|
|
|
3359
|
+
// ── PR #18 build-task quota-increase requests ─────────────────────────────────
|
|
3360
|
+
const _qT = (zh, en) => (window._lang === 'en' ? en : zh)
|
|
3361
|
+
const _qStatusBadge = (s) => {
|
|
3362
|
+
const map = {
|
|
3363
|
+
pending: ['#fef9c3', '#854d0e', _qT('待审核', 'Pending')],
|
|
3364
|
+
approved: ['#dcfce7', '#166534', _qT('已批准', 'Approved')],
|
|
3365
|
+
rejected: ['#fee2e2', '#991b1b', _qT('已拒绝', 'Rejected')],
|
|
3366
|
+
expired: ['#f3f4f6', '#6b7280', _qT('已过期', 'Expired')],
|
|
3367
|
+
exhausted: ['#e0e7ff', '#3730a3', _qT('已用完', 'Exhausted')],
|
|
3368
|
+
revoked: ['#fae8ff', '#86198f', _qT('已撤销', 'Revoked')],
|
|
3369
|
+
}
|
|
3370
|
+
const [bg, fg, label] = map[s] || ['#f3f4f6', '#6b7280', s]
|
|
3371
|
+
return `<span style="font-size:11px;background:${bg};color:${fg};padding:2px 8px;border-radius:99px;font-weight:600">${escHtml(label)}</span>`
|
|
3372
|
+
}
|
|
3373
|
+
|
|
3374
|
+
// RATE_LIMITED affordance — shown when build-task creation is capped (structured 429 response).
|
|
3375
|
+
window.showRateLimitAffordance = (r) => {
|
|
3376
|
+
const limit = (r && r.limit) != null ? r.limit : '?'
|
|
3377
|
+
const used = (r && r.used) != null ? r.used : '?'
|
|
3378
|
+
document.getElementById('quota-rl-overlay')?.remove()
|
|
3379
|
+
const ov = document.createElement('div')
|
|
3380
|
+
ov.id = 'quota-rl-overlay'
|
|
3381
|
+
ov.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:9999;display:flex;align-items:center;justify-content:center;padding:20px'
|
|
3382
|
+
ov.innerHTML = `
|
|
3383
|
+
<div style="background:#fff;border-radius:12px;max-width:420px;width:100%;padding:20px;box-shadow:0 10px 40px rgba(0,0,0,.2)">
|
|
3384
|
+
<div style="font-size:16px;font-weight:700;color:#991b1b;margin-bottom:8px">⚠️ ${_qT('已达每日建任务上限', 'Daily task-creation limit reached')}</div>
|
|
3385
|
+
<div style="font-size:13px;color:#374151;line-height:1.6;margin-bottom:14px">
|
|
3386
|
+
${_qT('当前上限', 'Current limit')}: <b>${escHtml(String(limit))}</b> ${_qT('个 / 24 小时', 'tasks / 24h')} · ${_qT('已用', 'Used')}: <b>${escHtml(String(used))}</b><br>
|
|
3387
|
+
${_qT('需要更多额度需经根管理员批准。', 'More headroom requires root-admin approval.')}
|
|
3388
|
+
</div>
|
|
3389
|
+
<div style="display:flex;gap:8px;justify-content:flex-end">
|
|
3390
|
+
<button onclick="document.getElementById('quota-rl-overlay').remove()" style="padding:8px 14px;border:1px solid #d1d5db;background:#fff;color:#374151;border-radius:8px;font-size:13px;cursor:pointer">${_qT('关闭', 'Close')}</button>
|
|
3391
|
+
<button onclick="document.getElementById('quota-rl-overlay').remove();navigate('#me/quota-requests')" style="padding:8px 14px;border:none;background:#4338ca;color:#fff;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer">${_qT('申请增加额度', 'Request extra quota')}</button>
|
|
3392
|
+
</div>
|
|
3393
|
+
</div>`
|
|
3394
|
+
document.body.appendChild(ov)
|
|
3395
|
+
}
|
|
3396
|
+
|
|
3397
|
+
// Requester view — own quota requests + a new-request form.
|
|
3398
|
+
async function renderMyQuotaRequests(app) {
|
|
3399
|
+
if (!state.user) { renderLogin(); return }
|
|
3400
|
+
app.innerHTML = shell(loading$(), 'me')
|
|
3401
|
+
const r = await GET('/me/quota-requests').catch(() => null)
|
|
3402
|
+
if (!r || r.error) { app.innerHTML = shell(alert$('error', (r && r.error) || _qT('加载失败', 'Failed to load')), 'me'); return }
|
|
3403
|
+
const reqs = r.requests || []
|
|
3404
|
+
const hasPending = reqs.some(x => x.status === 'pending')
|
|
3405
|
+
const field = (label, html) => `<div style="margin-bottom:10px"><label style="display:block;font-size:12px;color:#6b7280;margin-bottom:4px">${escHtml(label)}</label>${html}</div>`
|
|
3406
|
+
const inputStyle = 'width:100%;padding:8px;border:1px solid #d1d5db;border-radius:6px;font-size:13px;box-sizing:border-box'
|
|
3407
|
+
|
|
3408
|
+
const form = hasPending
|
|
3409
|
+
? `<div class="card" style="padding:14px;margin-bottom:14px;background:#fffbeb;border:1px solid #fde68a">
|
|
3410
|
+
<div style="font-size:13px;color:#854d0e">${_qT('你已有一个待审核的申请 — 每种额度类型同时只能有一个待审核申请。', 'You already have a pending request — only one pending request per quota type is allowed.')}</div>
|
|
3411
|
+
</div>`
|
|
3412
|
+
: `<div class="card" style="padding:16px;margin-bottom:16px">
|
|
3413
|
+
<div style="font-size:14px;font-weight:700;margin-bottom:12px">📝 ${_qT('申请增加建任务额度', 'Request extra build-task quota')}</div>
|
|
3414
|
+
${field(_qT('额外任务数(必填,正整数)', 'Extra tasks (required, positive integer)'), `<input id="q-count" type="number" min="1" placeholder="10" style="${inputStyle}">`)}
|
|
3415
|
+
${field(_qT('理由(必填)', 'Reason (required)'), `<textarea id="q-reason" rows="3" placeholder="${_qT('为什么需要更多额度', 'Why you need more quota')}" style="${inputStyle}"></textarea>`)}
|
|
3416
|
+
${field(_qT('关联任务/提案/PR(每行一个,可选)', 'Linked task/proposal/PR refs (one per line, optional)'), `<textarea id="q-refs" rows="2" placeholder="#17\\ntp_..." style="${inputStyle}"></textarea>`)}
|
|
3417
|
+
${field(_qT('紧急程度', 'Urgency'), `<select id="q-urgency" style="${inputStyle}"><option value="normal">${_qT('普通', 'Normal')}</option><option value="low">${_qT('低', 'Low')}</option><option value="high">${_qT('高', 'High')}</option></select>`)}
|
|
3418
|
+
${field(_qT('期望有效期(小时,可选)', 'Requested duration (hours, optional)'), `<input id="q-duration" type="number" min="1" placeholder="72" style="${inputStyle}">`)}
|
|
3419
|
+
<button onclick="submitQuotaRequest()" style="margin-top:6px;padding:9px 16px;border:none;background:#4338ca;color:#fff;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer">${_qT('提交申请', 'Submit request')}</button>
|
|
3420
|
+
</div>`
|
|
3421
|
+
|
|
3422
|
+
const card = (x) => {
|
|
3423
|
+
const granted = x.granted_count != null ? x.granted_count : null
|
|
3424
|
+
const remaining = x.remaining != null ? x.remaining : null
|
|
3425
|
+
return `<div class="card" style="padding:14px;margin-bottom:10px">
|
|
3426
|
+
<div style="display:flex;justify-content:space-between;gap:8px;align-items:center">
|
|
3427
|
+
<div style="font-size:13px;font-weight:600">${escHtml(_qT('额外', 'Extra'))} ${escHtml(String(x.requested_extra_count))} · ${escHtml(x.urgency || 'normal')}</div>
|
|
3428
|
+
${_qStatusBadge(x.status)}
|
|
3429
|
+
</div>
|
|
3430
|
+
<div style="font-size:12px;color:#374151;margin-top:6px;white-space:pre-wrap">${escHtml(x.reason || '')}</div>
|
|
3431
|
+
${(x.linked_refs && x.linked_refs.length) ? `<div style="font-size:11px;color:#6b7280;margin-top:4px">${_qT('关联', 'Refs')}: ${x.linked_refs.map(escHtml).join(', ')}</div>` : ''}
|
|
3432
|
+
${x.status === 'approved' ? `<div style="font-size:12px;color:#166534;margin-top:6px">${_qT('授权', 'Granted')}: ${escHtml(String(granted))} · ${_qT('剩余', 'Remaining')}: <b>${escHtml(String(remaining))}</b>${x.expires_at ? ` · ${_qT('到期', 'Expires')}: ${escHtml(x.expires_at)}` : ''}</div>` : ''}
|
|
3433
|
+
${x.status === 'exhausted' ? `<div style="font-size:12px;color:#3730a3;margin-top:6px">${_qT('授权已用完', 'Grant fully used')} (${escHtml(String(granted))})</div>` : ''}
|
|
3434
|
+
${x.status === 'rejected' && x.decision_note ? `<div style="font-size:12px;color:#991b1b;margin-top:6px">${_qT('拒绝原因', 'Rejection reason')}: ${escHtml(x.decision_note)}</div>` : ''}
|
|
3435
|
+
<div style="font-size:10px;color:#9ca3af;margin-top:6px">${escHtml(x.created_at || '')}</div>
|
|
3436
|
+
</div>`
|
|
3437
|
+
}
|
|
3438
|
+
|
|
3439
|
+
const body = `
|
|
3440
|
+
<div style="max-width:560px;margin:0 auto">
|
|
3441
|
+
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px">
|
|
3442
|
+
<div style="font-size:18px;font-weight:700">🎟️ ${_qT('我的额度申请', 'My quota requests')}</div>
|
|
3443
|
+
<a href="#me" style="font-size:12px;color:#4338ca;text-decoration:none">← ${_qT('返回', 'Back')}</a>
|
|
3444
|
+
</div>
|
|
3445
|
+
<div class="card" style="padding:12px;margin-bottom:14px;background:linear-gradient(135deg,#eef2ff,#fff)">
|
|
3446
|
+
<div style="font-size:12px;color:#6b7280">${_qT('当前可用临时额度', 'Current temporary quota available')}</div>
|
|
3447
|
+
<div style="font-size:22px;font-weight:700;color:#4338ca">${escHtml(String(r.remaining_quota || 0))}</div>
|
|
3448
|
+
</div>
|
|
3449
|
+
${form}
|
|
3450
|
+
<div style="font-size:13px;font-weight:600;margin-bottom:8px">${_qT('历史申请', 'Request history')}</div>
|
|
3451
|
+
${reqs.length ? reqs.map(card).join('') : `<div style="font-size:13px;color:#9ca3af">${_qT('暂无申请', 'No requests yet')}</div>`}
|
|
3452
|
+
</div>`
|
|
3453
|
+
app.innerHTML = shell(body, 'me')
|
|
3454
|
+
}
|
|
3455
|
+
|
|
3456
|
+
window.submitQuotaRequest = async () => {
|
|
3457
|
+
const v = (id) => (document.getElementById(id)?.value || '').trim()
|
|
3458
|
+
const count = Number(v('q-count'))
|
|
3459
|
+
const reason = v('q-reason')
|
|
3460
|
+
if (!count || count <= 0) { toast$(_qT('请填写正整数额外任务数', 'Enter a positive extra-task count')); return }
|
|
3461
|
+
if (reason.length < 5) { toast$(_qT('请填写理由(至少 5 字)', 'Reason required (>= 5 chars)')); return }
|
|
3462
|
+
const refs = v('q-refs').split('\n').map(s => s.trim()).filter(Boolean)
|
|
3463
|
+
const duration = v('q-duration')
|
|
3464
|
+
const body = { requested_extra_count: count, reason, linked_refs: refs, urgency: v('q-urgency') || 'normal' }
|
|
3465
|
+
if (duration) body.requested_duration_hours = Number(duration)
|
|
3466
|
+
const r = await POST('/me/quota-requests', body)
|
|
3467
|
+
if (r && r.error) { toast$(r.error_code === 'ALREADY_PENDING' ? _qT('你已有一个待审核申请', 'You already have a pending request') : (r.error || _qT('提交失败', 'Submit failed'))); return }
|
|
3468
|
+
toast$(_qT('申请已提交,等待根管理员审核', 'Submitted — awaiting root-admin review'))
|
|
3469
|
+
renderMyQuotaRequests(document.getElementById('app'))
|
|
3470
|
+
}
|
|
3471
|
+
|
|
3472
|
+
// ── Admin operator-claim workflow (Phase 2): link an admin SEAT → a personal contributor account ──
|
|
3473
|
+
function _ocStatusBadge(s) {
|
|
3474
|
+
const map = {
|
|
3475
|
+
proposed: ['#fef9c3', '#854d0e', _qT('待贡献人确认', 'Awaiting contributor')],
|
|
3476
|
+
confirmed: ['#dbeafe', '#1e40af', _qT('待 root 审批', 'Awaiting root approval')],
|
|
3477
|
+
rejected_by_contributor: ['#fee2e2', '#991b1b', _qT('贡献人已拒绝', 'Rejected by contributor')],
|
|
3478
|
+
approved: ['#dcfce7', '#166534', _qT('已生效', 'Active')],
|
|
3479
|
+
rejected_by_root: ['#fee2e2', '#991b1b', _qT('root 已拒绝', 'Rejected by root')],
|
|
3480
|
+
revoked: ['#fae8ff', '#86198f', _qT('已撤销', 'Revoked')],
|
|
3481
|
+
superseded: ['#f3f4f6', '#6b7280', _qT('已被取代', 'Superseded')],
|
|
3482
|
+
}
|
|
3483
|
+
const [bg, fg, label] = map[s] || ['#f3f4f6', '#6b7280', s]
|
|
3484
|
+
return `<span style="background:${bg};color:${fg};padding:2px 8px;border-radius:999px;font-size:11px;font-weight:600">${escHtml(label)}</span>`
|
|
3485
|
+
}
|
|
3486
|
+
|
|
3487
|
+
// Page for any user: (a) if admin — their seat + a "link personal contributor account" form;
|
|
3488
|
+
// (b) for everyone — claims pointing at ME awaiting my accept/reject.
|
|
3489
|
+
async function renderMyOperatorClaims(app) {
|
|
3490
|
+
if (!state.user) { renderLogin(); return }
|
|
3491
|
+
app.innerHTML = shell(loading$(), 'me')
|
|
3492
|
+
const isAdmin = state.user.role === 'admin' || (Array.isArray(state.user.roles) && state.user.roles.includes('admin'))
|
|
3493
|
+
const pend = await GET('/me/operator-claim-confirmations').catch(() => null)
|
|
3494
|
+
const rel = await GET('/me/operator-claims').catch(() => null) // ALL relationships pointing at me (active/history)
|
|
3495
|
+
const mine = isAdmin ? await GET('/admin/operator-claims/me').catch(() => null) : null
|
|
3496
|
+
const inputStyle = 'width:100%;padding:8px;border:1px solid #d1d5db;border-radius:6px;font-size:13px;box-sizing:border-box'
|
|
3497
|
+
const field = (label, html) => `<div style="margin-bottom:10px"><label style="display:block;font-size:12px;color:#6b7280;margin-bottom:4px">${escHtml(label)}</label>${html}</div>`
|
|
3498
|
+
|
|
3499
|
+
// active approved claim → either an "申请解除" button or a pending-review note. Either PARTY may
|
|
3500
|
+
// request unlink (admin-seat owner OR contributor), so both claimRow and relCard reuse this.
|
|
3501
|
+
const unlinkAreaFor = (c) => {
|
|
3502
|
+
const active = c.status === 'approved' && c.approved
|
|
3503
|
+
if (!active) return ''
|
|
3504
|
+
return c.unlink_pending
|
|
3505
|
+
? `<div style="font-size:11px;color:#b45309;margin-top:8px">⏳ ${_qT('解除申请审批中(待 root)', 'Unlink request pending root review')}</div>`
|
|
3506
|
+
: `<button onclick="requestUnlinkOperatorClaim('${escHtml(c.approved.event_id)}')" style="margin-top:8px;padding:6px 12px;border:1px solid #d1d5db;background:#fff;color:#b91c1c;border-radius:8px;font-size:12px;cursor:pointer">${_qT('申请解除', 'Request unlink')}</button>`
|
|
3507
|
+
}
|
|
3508
|
+
|
|
3509
|
+
const claimRow = (c) => `<div class="card" style="padding:12px;margin-bottom:8px">
|
|
3510
|
+
<div style="display:flex;justify-content:space-between;align-items:center;gap:8px">
|
|
3511
|
+
<div style="font-size:12px;font-weight:600">→ ${escHtml(c.contributor_account_id)}</div>${_ocStatusBadge(c.status)}
|
|
3512
|
+
</div>
|
|
3513
|
+
<div style="font-size:10px;color:#9ca3af;margin-top:4px">${escHtml(c.proposed_at || '')} · ${escHtml(c.claimed_event_id)}</div>${unlinkAreaFor(c)}
|
|
3514
|
+
</div>`
|
|
3515
|
+
|
|
3516
|
+
const adminBlock = isAdmin ? `
|
|
3517
|
+
<div class="card" style="padding:16px;margin-bottom:16px">
|
|
3518
|
+
<div style="font-size:14px;font-weight:700;margin-bottom:4px">🔗 ${_qT('关联个人贡献账号', 'Link a personal contributor account')}</div>
|
|
3519
|
+
<div style="font-size:12px;color:#6b7280;margin-bottom:12px">${_qT('把这个管理席位的协调贡献,归属到你的真实个人账号。需对方确认 + 根管理员审批。', 'Attribute this admin seat\'s coordination work to your real personal account. Requires the contributor to accept + root approval.')}</div>
|
|
3520
|
+
${field(_qT('贡献人账号 ID(必填)', 'Contributor account ID (required)'), `<input id="oc-contributor" placeholder="usr_..." style="${inputStyle}">`)}
|
|
3521
|
+
${field(_qT('理由(可选)', 'Rationale (optional)'), `<input id="oc-rationale" placeholder="${_qT('为何关联', 'why')}" style="${inputStyle}">`)}
|
|
3522
|
+
<button onclick="submitOperatorClaim()" style="margin-top:4px;padding:9px 16px;border:none;background:#4338ca;color:#fff;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer">${_qT('发起关联', 'Propose link')}</button>
|
|
3523
|
+
<div style="font-size:13px;font-weight:600;margin:16px 0 8px">${_qT('本席位的关联记录', 'This seat\'s claims')}</div>
|
|
3524
|
+
${(mine && mine.claims && mine.claims.length) ? mine.claims.map(claimRow).join('') : `<div style="font-size:13px;color:#9ca3af">${_qT('暂无', 'None yet')}</div>`}
|
|
3525
|
+
</div>` : '' /* adminBlock */
|
|
3526
|
+
|
|
3527
|
+
const pendList = (pend && pend.pending) || []
|
|
3528
|
+
const confirmCard = (c) => `<div class="card" style="padding:14px;margin-bottom:10px;background:#fffbeb;border:1px solid #fde68a">
|
|
3529
|
+
<div style="font-size:13px">${_qT('管理席位', 'Admin seat')} <b>${escHtml(c.admin_account_id)}</b> ${_qT('请求关联到你的账号作为贡献归属。', 'requests to attribute its coordination work to your account.')}</div>
|
|
3530
|
+
<div style="font-size:10px;color:#9ca3af;margin:6px 0">${escHtml(c.claimed_event_id)}</div>
|
|
3531
|
+
<div style="display:flex;gap:8px">
|
|
3532
|
+
<button onclick="confirmOperatorClaim('${escHtml(c.claimed_event_id)}','accepted')" style="padding:8px 14px;border:none;background:#166534;color:#fff;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer">${_qT('接受', 'Accept')}</button>
|
|
3533
|
+
<button onclick="confirmOperatorClaim('${escHtml(c.claimed_event_id)}','rejected')" style="padding:8px 14px;border:1px solid #d1d5db;background:#fff;color:#991b1b;border-radius:8px;font-size:13px;cursor:pointer">${_qT('拒绝', 'Reject')}</button>
|
|
3534
|
+
</div>
|
|
3535
|
+
</div>`
|
|
3536
|
+
|
|
3537
|
+
// 我的贡献归属关系(已生效/历史)+ approved 关系可「申请解除」(不是直接撤销;需 Passkey + root 审批)
|
|
3538
|
+
const relList = (rel && rel.relationships) || []
|
|
3539
|
+
const relCard = (c) => {
|
|
3540
|
+
return `<div class="card" style="padding:12px;margin-bottom:8px">
|
|
3541
|
+
<div style="display:flex;justify-content:space-between;align-items:center;gap:8px">
|
|
3542
|
+
<div style="font-size:12px;font-weight:600">${escHtml(c.admin_account_id)} → ${escHtml(c.contributor_account_id)}</div>${_ocStatusBadge(c.status)}
|
|
3543
|
+
</div>
|
|
3544
|
+
<div style="font-size:10px;color:#9ca3af;margin-top:4px">${escHtml(c.proposed_at || '')}</div>${unlinkAreaFor(c)}
|
|
3545
|
+
</div>`
|
|
3546
|
+
}
|
|
3547
|
+
|
|
3548
|
+
const body = `<div style="max-width:560px;margin:0 auto">
|
|
3549
|
+
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px">
|
|
3550
|
+
<div style="font-size:18px;font-weight:700">🪪 ${_qT('贡献归属', 'Contribution attribution')}</div>
|
|
3551
|
+
<a href="#me" style="font-size:12px;color:#4338ca;text-decoration:none">← ${_qT('返回', 'Back')}</a>
|
|
3552
|
+
</div>
|
|
3553
|
+
${adminBlock}
|
|
3554
|
+
<div style="font-size:13px;font-weight:600;margin-bottom:8px">${_qT('待我确认的关联', 'Awaiting my confirmation')}</div>
|
|
3555
|
+
${pendList.length ? pendList.map(confirmCard).join('') : `<div style="font-size:13px;color:#9ca3af;margin-bottom:8px">${_qT('没有待确认的关联', 'No pending links')}</div>`}
|
|
3556
|
+
<div style="font-size:13px;font-weight:600;margin:16px 0 8px">${_qT('我的贡献归属关系 / 历史', 'My contribution-attribution relationships / history')}</div>
|
|
3557
|
+
${relList.length ? relList.map(relCard).join('') : `<div style="font-size:13px;color:#9ca3af">${_qT('暂无关系', 'No relationships yet')}</div>`}
|
|
3558
|
+
</div>`
|
|
3559
|
+
app.innerHTML = shell(body, 'me')
|
|
3560
|
+
}
|
|
3561
|
+
|
|
3562
|
+
window.requestUnlinkOperatorClaim = async (approvedEventId) => {
|
|
3563
|
+
if (!confirm(_qT('确认申请解除该贡献归属关系?需 Passkey 验证,且最终由 root 审批。', 'Request to unlink this attribution relationship? Requires Passkey, then root approval.'))) return
|
|
3564
|
+
let token
|
|
3565
|
+
try { token = await requestPasskeyGate('operator_claim_unlink', { approved_event_id: approvedEventId }) }
|
|
3566
|
+
catch (e) { toast$(e.message || _qT('Passkey 验证失败', 'Passkey verification failed')); return }
|
|
3567
|
+
const reason = (prompt(_qT('解除理由(可选)', 'Reason (optional)')) || '').trim() || undefined
|
|
3568
|
+
const r = await POST('/me/operator-claims/' + encodeURIComponent(approvedEventId) + '/request-unlink', { webauthn_token: token, reason })
|
|
3569
|
+
if (r && r.error) { toast$(r.message || r.error || _qT('提交失败', 'Failed')); return }
|
|
3570
|
+
toast$(_qT('解除申请已提交,等待 root 审批', 'Unlink request submitted — awaiting root review'))
|
|
3571
|
+
renderMyOperatorClaims(document.getElementById('app'))
|
|
3572
|
+
}
|
|
3573
|
+
|
|
3574
|
+
window.submitOperatorClaim = async () => {
|
|
3575
|
+
const contributor = (document.getElementById('oc-contributor')?.value || '').trim()
|
|
3576
|
+
const rationale = (document.getElementById('oc-rationale')?.value || '').trim()
|
|
3577
|
+
if (!contributor) { toast$(_qT('请填写贡献人账号 ID', 'Enter contributor account ID')); return }
|
|
3578
|
+
const r = await POST('/admin/operator-claims', { contributor_account_id: contributor, rationale })
|
|
3579
|
+
if (r && r.error) { toast$(r.message || r.error || _qT('发起失败', 'Failed')); return }
|
|
3580
|
+
toast$(_qT('已发起,等待对方确认 + root 审批', 'Proposed — awaiting contributor + root'))
|
|
3581
|
+
renderMyOperatorClaims(document.getElementById('app'))
|
|
3582
|
+
}
|
|
3583
|
+
window.confirmOperatorClaim = async (claimedEventId, decision) => {
|
|
3584
|
+
const r = await POST('/me/operator-claim-confirmations/' + encodeURIComponent(claimedEventId), { decision })
|
|
3585
|
+
if (r && r.error) { toast$(r.message || r.error || _qT('操作失败', 'Failed')); return }
|
|
3586
|
+
toast$(decision === 'accepted' ? _qT('已接受', 'Accepted') : _qT('已拒绝', 'Rejected'))
|
|
3587
|
+
renderMyOperatorClaims(document.getElementById('app'))
|
|
3588
|
+
}
|
|
3589
|
+
|
|
3590
|
+
// ROOT review queue for operator claims.
|
|
3591
|
+
async function renderAdminOperatorClaims(app, statusFilter) {
|
|
3592
|
+
if (!state.user) { renderLogin(); return }
|
|
3593
|
+
const isRoot = (state.user.admin_type || 'root') === 'root' && (state.user.role === 'admin' || (Array.isArray(state.user.roles) && state.user.roles.includes('admin')))
|
|
3594
|
+
if (!isRoot) { app.innerHTML = shell(`<div class="alert alert-danger">${_qT('仅限根管理员', 'Root admin only')}</div>`, 'admin'); return }
|
|
3595
|
+
app.innerHTML = shell(loading$(), 'admin')
|
|
3596
|
+
const sf = statusFilter || 'confirmed'
|
|
3597
|
+
const r = await GET('/admin/operator-claims' + (sf === 'all' ? '' : '?status=' + encodeURIComponent(sf))).catch(() => null)
|
|
3598
|
+
if (!r || r.error) { app.innerHTML = shell(alert$('error', (r && r.error) || _qT('加载失败', 'Failed to load')), 'admin'); return }
|
|
3599
|
+
const claims = r.claims || []
|
|
3600
|
+
const unlinkRes = await GET('/admin/operator-claims/unlink/requests').catch(() => null)
|
|
3601
|
+
const unlinkReqs = (unlinkRes && unlinkRes.requests) || []
|
|
3602
|
+
const inputStyle = 'width:100%;padding:7px;border:1px solid #d1d5db;border-radius:6px;font-size:12px;box-sizing:border-box'
|
|
3603
|
+
const filterBtn = (s, label) => `<button onclick="renderAdminOperatorClaims(document.getElementById('app'),'${s}')" style="padding:5px 10px;border:1px solid ${sf === s ? '#4338ca' : '#d1d5db'};background:${sf === s ? '#4338ca' : '#fff'};color:${sf === s ? '#fff' : '#374151'};border-radius:6px;font-size:12px;cursor:pointer">${escHtml(label)}</button>`
|
|
3604
|
+
const unlinkCard = (u) => {
|
|
3605
|
+
const rid = u.request_event_id
|
|
3606
|
+
// When THIS root is a party to the relationship/request (self-or-related), root may still decide it
|
|
3607
|
+
// but MUST mark the conflict honestly: approval_kind ∈ {root_approval, founder_bootstrap_override}
|
|
3608
|
+
// (never independent_governance) + conflict_disclosure = self_or_related. Mirrors approveClaim.
|
|
3609
|
+
const markingForm = u.self_or_related ? `
|
|
3610
|
+
<div style="margin-top:8px;padding-top:8px;border-top:1px dashed #fed7aa">
|
|
3611
|
+
<div style="font-size:11px;color:#b45309;margin-bottom:6px">⚠️ ${_qT('你是该关系/申请的关联方:必须如实标记(不可 independent_governance)', 'You are a party to this relationship/request: mark honestly (independent_governance not allowed)')}</div>
|
|
3612
|
+
<div style="display:flex;gap:6px">
|
|
3613
|
+
<select id="uak-${rid}" style="${inputStyle}">
|
|
3614
|
+
<option value="root_approval">root_approval</option>
|
|
3615
|
+
<option value="founder_bootstrap_override">founder_bootstrap_override</option>
|
|
3616
|
+
</select>
|
|
3617
|
+
<select id="ucd-${rid}" style="${inputStyle}">
|
|
3618
|
+
<option value="self_or_related" selected>self_or_related</option>
|
|
3619
|
+
</select>
|
|
3620
|
+
</div>
|
|
3621
|
+
</div>` : ''
|
|
3622
|
+
return `<div class="card" style="padding:12px;margin-bottom:8px;background:#fff7ed;border:1px solid #fed7aa">
|
|
3623
|
+
<div style="font-size:12px;font-weight:600">🔓 ${escHtml(u.admin_account_id)} → ${escHtml(u.contributor_account_id)}${u.self_or_related ? ' 🪞' : ''}</div>
|
|
3624
|
+
<div style="font-size:11px;color:#6b7280;margin-top:4px">${_qT('申请人', 'Requested by')}: ${escHtml(u.requested_by)} (${escHtml(u.requester_role)})${u.reason ? ' · ' + escHtml(u.reason) : ''}</div>
|
|
3625
|
+
${markingForm}
|
|
3626
|
+
<div style="display:flex;gap:8px;margin-top:8px">
|
|
3627
|
+
<button onclick="approveUnlinkReq('${escHtml(rid)}')" style="padding:6px 12px;border:none;background:#b91c1c;color:#fff;border-radius:8px;font-size:12px;font-weight:600;cursor:pointer">${_qT('批准解除', 'Approve unlink')}</button>
|
|
3628
|
+
<button onclick="rejectUnlinkReq('${escHtml(rid)}')" style="padding:6px 12px;border:1px solid #d1d5db;background:#fff;color:#374151;border-radius:8px;font-size:12px;cursor:pointer">${_qT('驳回', 'Reject')}</button>
|
|
3629
|
+
</div>
|
|
3630
|
+
</div>`
|
|
3631
|
+
}
|
|
3632
|
+
|
|
3633
|
+
const card = (c) => {
|
|
3634
|
+
const selfLink = c.admin_account_id === c.contributor_account_id
|
|
3635
|
+
const id = c.claimed_event_id
|
|
3636
|
+
const approveForm = (c.status === 'confirmed' || (selfLink && c.status === 'proposed')) ? `
|
|
3637
|
+
<div style="margin-top:10px;padding-top:10px;border-top:1px solid #eee">
|
|
3638
|
+
${selfLink ? `<div style="font-size:11px;color:#b45309;margin-bottom:6px">⚠️ ${_qT('自链(席位=贡献人):必须 founder_bootstrap_override + self_or_related', 'Self-link (seat == contributor): must be founder_bootstrap_override + self_or_related')}</div>` : ''}
|
|
3639
|
+
<div style="display:flex;gap:6px;margin-bottom:6px">
|
|
3640
|
+
<select id="ak-${id}" style="${inputStyle}">
|
|
3641
|
+
${selfLink ? '' : `<option value="independent_governance">independent_governance</option>`}
|
|
3642
|
+
<option value="root_approval">root_approval</option>
|
|
3643
|
+
<option value="founder_bootstrap_override">founder_bootstrap_override</option>
|
|
3644
|
+
</select>
|
|
3645
|
+
<select id="cd-${id}" style="${inputStyle}">
|
|
3646
|
+
<option value="none">none</option>
|
|
3647
|
+
<option value="self_or_related"${selfLink ? ' selected' : ''}>self_or_related</option>
|
|
3648
|
+
</select>
|
|
3649
|
+
</div>
|
|
3650
|
+
<div style="display:flex;gap:8px">
|
|
3651
|
+
<button onclick="approveOperatorClaim('${escHtml(id)}')" style="padding:7px 14px;border:none;background:#166534;color:#fff;border-radius:8px;font-size:12px;font-weight:600;cursor:pointer">${_qT('批准', 'Approve')}</button>
|
|
3652
|
+
<button onclick="rejectOperatorClaim('${escHtml(id)}')" style="padding:7px 14px;border:1px solid #d1d5db;background:#fff;color:#991b1b;border-radius:8px;font-size:12px;cursor:pointer">${_qT('拒绝', 'Reject')}</button>
|
|
3653
|
+
</div>
|
|
3654
|
+
</div>` : ''
|
|
3655
|
+
const revokeBtn = (c.status === 'approved' && c.approved) ? `<button onclick="revokeOperatorClaim('${escHtml(c.approved.event_id)}')" style="margin-top:8px;padding:6px 12px;border:1px solid #d1d5db;background:#fff;color:#86198f;border-radius:8px;font-size:12px;cursor:pointer">${_qT('撤销', 'Revoke')}</button>` : ''
|
|
3656
|
+
return `<div class="card" style="padding:14px;margin-bottom:10px">
|
|
3657
|
+
<div style="display:flex;justify-content:space-between;align-items:center;gap:8px">
|
|
3658
|
+
<div style="font-size:12px;font-weight:600">${escHtml(c.admin_account_id)} → ${escHtml(c.contributor_account_id)}${selfLink ? ' 🪞' : ''}</div>${_ocStatusBadge(c.status)}
|
|
3659
|
+
</div>
|
|
3660
|
+
<div style="font-size:11px;color:#6b7280;margin-top:4px">${c.confirmation ? `${_qT('贡献人', 'Contributor')}: ${escHtml(c.confirmation.decision)}` : _qT('未确认', 'not confirmed')} · ${escHtml(c.proposed_at || '')}</div>
|
|
3661
|
+
${approveForm}${revokeBtn}
|
|
3662
|
+
</div>`
|
|
3663
|
+
}
|
|
3664
|
+
|
|
3665
|
+
const body = `<div style="max-width:620px;margin:0 auto">
|
|
3666
|
+
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
|
|
3667
|
+
<div style="font-size:18px;font-weight:700">🪪 ${_qT('操作席位关联审批', 'Operator-claim review')}</div>
|
|
3668
|
+
<a href="#admin/protocol" style="font-size:12px;color:#4338ca;text-decoration:none">← ${_qT('返回', 'Back')}</a>
|
|
3669
|
+
</div>
|
|
3670
|
+
${unlinkReqs.length ? `<div style="font-size:13px;font-weight:700;color:#b91c1c;margin-bottom:8px">🔓 ${_qT('待审批的解除申请', 'Pending unlink requests')} (${unlinkReqs.length})</div>${unlinkReqs.map(unlinkCard).join('')}<div style="height:14px"></div>` : ''}
|
|
3671
|
+
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:14px">
|
|
3672
|
+
${filterBtn('confirmed', _qT('待审批', 'Awaiting approval'))}${filterBtn('proposed', _qT('待确认', 'Proposed'))}${filterBtn('approved', _qT('已生效', 'Active'))}${filterBtn('all', _qT('全部', 'All'))}
|
|
3673
|
+
</div>
|
|
3674
|
+
${claims.length ? claims.map(card).join('') : `<div style="font-size:13px;color:#9ca3af">${_qT('暂无', 'None')}</div>`}
|
|
3675
|
+
</div>`
|
|
3676
|
+
app.innerHTML = shell(body, 'admin')
|
|
3677
|
+
}
|
|
3678
|
+
|
|
3679
|
+
window.approveOperatorClaim = async (id) => {
|
|
3680
|
+
const ak = document.getElementById('ak-' + id)?.value
|
|
3681
|
+
const cd = document.getElementById('cd-' + id)?.value
|
|
3682
|
+
const r = await POST('/admin/operator-claims/' + encodeURIComponent(id) + '/approve', { approval_kind: ak, conflict_disclosure: cd })
|
|
3683
|
+
if (r && r.error) { toast$(r.message || r.error || _qT('审批失败', 'Approve failed')); return }
|
|
3684
|
+
toast$(_qT('已批准', 'Approved')); renderAdminOperatorClaims(document.getElementById('app'))
|
|
3685
|
+
}
|
|
3686
|
+
window.rejectOperatorClaim = async (id) => {
|
|
3687
|
+
const r = await POST('/admin/operator-claims/' + encodeURIComponent(id) + '/reject', {})
|
|
3688
|
+
if (r && r.error) { toast$(r.message || r.error || _qT('操作失败', 'Failed')); return }
|
|
3689
|
+
toast$(_qT('已拒绝', 'Rejected')); renderAdminOperatorClaims(document.getElementById('app'))
|
|
3690
|
+
}
|
|
3691
|
+
window.revokeOperatorClaim = async (approvedId) => {
|
|
3692
|
+
const r = await POST('/admin/operator-claims/' + encodeURIComponent(approvedId) + '/revoke', {})
|
|
3693
|
+
if (r && r.error) { toast$(r.message || r.error || _qT('撤销失败', 'Revoke failed')); return }
|
|
3694
|
+
toast$(_qT('已撤销', 'Revoked')); renderAdminOperatorClaims(document.getElementById('app'))
|
|
3695
|
+
}
|
|
3696
|
+
window.approveUnlinkReq = async (requestId) => {
|
|
3697
|
+
if (!confirm(_qT('批准后将解除该贡献归属关系,确认?', 'Approving will unlink (revoke) this attribution. Confirm?'))) return
|
|
3698
|
+
// marking selectors only render when root is self-or-related; pass them through when present.
|
|
3699
|
+
const ak = document.getElementById('uak-' + requestId)?.value
|
|
3700
|
+
const cd = document.getElementById('ucd-' + requestId)?.value
|
|
3701
|
+
const body = ak ? { approval_kind: ak, conflict_disclosure: cd } : {}
|
|
3702
|
+
const r = await POST('/admin/operator-claims/unlink/' + encodeURIComponent(requestId) + '/approve', body)
|
|
3703
|
+
if (r && r.error) { toast$(r.message || r.error || _qT('操作失败', 'Failed')); return }
|
|
3704
|
+
toast$(_qT('已批准解除', 'Unlink approved')); renderAdminOperatorClaims(document.getElementById('app'))
|
|
3705
|
+
}
|
|
3706
|
+
window.rejectUnlinkReq = async (requestId) => {
|
|
3707
|
+
const ak = document.getElementById('uak-' + requestId)?.value
|
|
3708
|
+
const cd = document.getElementById('ucd-' + requestId)?.value
|
|
3709
|
+
const body = ak ? { approval_kind: ak, conflict_disclosure: cd } : {}
|
|
3710
|
+
const r = await POST('/admin/operator-claims/unlink/' + encodeURIComponent(requestId) + '/reject', body)
|
|
3711
|
+
if (r && r.error) { toast$(r.message || r.error || _qT('操作失败', 'Failed')); return }
|
|
3712
|
+
toast$(_qT('已驳回,关系仍有效', 'Rejected — relationship stays active')); renderAdminOperatorClaims(document.getElementById('app'))
|
|
3713
|
+
}
|
|
3714
|
+
|
|
3715
|
+
// ROOT admin review page.
|
|
3716
|
+
async function renderAdminBuildTaskQuota(app, statusFilter) {
|
|
3717
|
+
if (!state.user) { renderLogin(); return }
|
|
3718
|
+
const isRoot = (state.user.admin_type || 'root') === 'root' && (state.user.role === 'admin' || (Array.isArray(state.user.roles) && state.user.roles.includes('admin')))
|
|
3719
|
+
if (!isRoot) { app.innerHTML = shell(`<div class="alert alert-danger">${_qT('仅限根管理员', 'Root admin only')}</div>`, 'admin'); return }
|
|
3720
|
+
app.innerHTML = shell(loading$(), 'admin')
|
|
3721
|
+
const sf = statusFilter || 'pending'
|
|
3722
|
+
const r = await GET('/admin/quota-requests' + (sf === 'all' ? '' : '?status=' + encodeURIComponent(sf))).catch(() => null)
|
|
3723
|
+
if (!r || r.error) { app.innerHTML = shell(alert$('error', (r && r.error) || _qT('加载失败', 'Failed to load')), 'admin'); return }
|
|
3724
|
+
const reqs = r.requests || []
|
|
3725
|
+
const inputStyle = 'width:100%;padding:7px;border:1px solid #d1d5db;border-radius:6px;font-size:12px;box-sizing:border-box'
|
|
3726
|
+
const filterBtn = (s, label) => `<button onclick="renderAdminBuildTaskQuota(document.getElementById('app'),'${s}')" style="padding:5px 10px;border:1px solid ${sf === s ? '#4338ca' : '#d1d5db'};background:${sf === s ? '#4338ca' : '#fff'};color:${sf === s ? '#fff' : '#374151'};border-radius:6px;font-size:12px;cursor:pointer">${escHtml(label)}</button>`
|
|
3727
|
+
|
|
3728
|
+
const card = (x) => {
|
|
3729
|
+
const pending = x.status === 'pending'
|
|
3730
|
+
return `<div class="card" style="padding:14px;margin-bottom:10px">
|
|
3731
|
+
<div style="display:flex;justify-content:space-between;gap:8px;align-items:center">
|
|
3732
|
+
<div style="font-size:13px;font-weight:600">${escHtml(x.requester_user_id)} · ${_qT('请求', 'Wants')} <b>${escHtml(String(x.requested_extra_count))}</b> · ${escHtml(x.urgency || 'normal')}</div>
|
|
3733
|
+
${_qStatusBadge(x.status)}
|
|
3734
|
+
</div>
|
|
3735
|
+
<div style="font-size:12px;color:#374151;margin-top:6px;white-space:pre-wrap">${escHtml(x.reason || '')}</div>
|
|
3736
|
+
${(x.linked_refs && x.linked_refs.length) ? `<div style="font-size:11px;color:#6b7280;margin-top:4px">${_qT('关联', 'Refs')}: ${x.linked_refs.map(escHtml).join(', ')}</div>` : ''}
|
|
3737
|
+
<div style="font-size:10px;color:#9ca3af;margin-top:4px">${escHtml(x.created_at || '')} · ${escHtml(x.id)}</div>
|
|
3738
|
+
<div id="usage-${escHtml(x.id)}" style="font-size:11px;color:#6b7280;margin-top:4px"><button onclick="loadQuotaUsage('${escHtml(x.id)}')" style="padding:3px 8px;border:1px solid #d1d5db;background:#fff;border-radius:6px;font-size:11px;cursor:pointer">${_qT('查看申请人近 24h 用量', 'Load requester 24h usage')}</button></div>
|
|
3739
|
+
${x.status === 'approved' ? `<div style="font-size:12px;color:#166534;margin-top:6px">${_qT('授权', 'Granted')}: ${escHtml(String(x.granted_count))} · ${_qT('剩余', 'Remaining')}: ${escHtml(String(x.remaining))}${x.expires_at ? ` · ${_qT('到期', 'Expires')}: ${escHtml(x.expires_at)}` : ''}
|
|
3740
|
+
<button onclick="revokeQuotaReq('${escHtml(x.id)}')" style="margin-left:8px;padding:3px 8px;border:1px solid #c026d3;background:#fff;color:#86198f;border-radius:6px;font-size:11px;cursor:pointer">${_qT('撤销', 'Revoke')}</button></div>` : ''}
|
|
3741
|
+
${(x.decision_note && (x.status === 'rejected' || x.status === 'approved' || x.status === 'revoked')) ? `<div style="font-size:11px;color:#6b7280;margin-top:4px">${_qT('备注', 'Note')}: ${escHtml(x.decision_note)}</div>` : ''}
|
|
3742
|
+
${pending ? `
|
|
3743
|
+
<div style="margin-top:10px;border-top:1px solid #f1f1f4;padding-top:10px;display:grid;gap:8px">
|
|
3744
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
|
|
3745
|
+
<input id="ap-count-${escHtml(x.id)}" type="number" min="1" value="${escHtml(String(x.requested_extra_count))}" placeholder="${_qT('授权数', 'Grant count')}" style="${inputStyle}">
|
|
3746
|
+
<input id="ap-dur-${escHtml(x.id)}" type="number" min="1" value="${escHtml(String(x.requested_duration_hours || 72))}" placeholder="${_qT('有效期(小时)', 'Duration (h)')}" style="${inputStyle}">
|
|
3747
|
+
</div>
|
|
3748
|
+
<input id="ap-note-${escHtml(x.id)}" placeholder="${_qT('批准备注(可选)', 'Approval note (optional)')}" style="${inputStyle}">
|
|
3749
|
+
<input id="rj-note-${escHtml(x.id)}" placeholder="${_qT('拒绝原因(可选)', 'Rejection note (optional)')}" style="${inputStyle}">
|
|
3750
|
+
<div style="display:flex;gap:8px">
|
|
3751
|
+
<button onclick="approveQuotaReq('${escHtml(x.id)}')" style="padding:7px 14px;border:none;background:#16a34a;color:#fff;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer">${_qT('批准', 'Approve')}</button>
|
|
3752
|
+
<button onclick="rejectQuotaReq('${escHtml(x.id)}')" style="padding:7px 14px;border:1px solid #ef4444;background:#fff;color:#991b1b;border-radius:6px;font-size:12px;cursor:pointer">${_qT('拒绝', 'Reject')}</button>
|
|
3753
|
+
</div>
|
|
3754
|
+
</div>` : ''}
|
|
3755
|
+
</div>`
|
|
3756
|
+
}
|
|
3757
|
+
|
|
3758
|
+
const body = `
|
|
3759
|
+
<div style="max-width:640px;margin:0 auto">
|
|
3760
|
+
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px">
|
|
3761
|
+
<div style="font-size:18px;font-weight:700">🎟️ ${_qT('建任务额度审核', 'Build-task quota review')}</div>
|
|
3762
|
+
<a href="#admin" style="font-size:12px;color:#4338ca;text-decoration:none">← ${_qT('返回', 'Back')}</a>
|
|
3763
|
+
</div>
|
|
3764
|
+
<div style="display:flex;gap:6px;margin-bottom:14px;flex-wrap:wrap">
|
|
3765
|
+
${filterBtn('pending', _qT('待审核', 'Pending'))}${filterBtn('approved', _qT('已批准', 'Approved'))}${filterBtn('rejected', _qT('已拒绝', 'Rejected'))}${filterBtn('all', _qT('全部', 'All'))}
|
|
3766
|
+
</div>
|
|
3767
|
+
${reqs.length ? reqs.map(card).join('') : `<div style="font-size:13px;color:#9ca3af">${_qT('暂无申请', 'No requests')}</div>`}
|
|
3768
|
+
</div>`
|
|
3769
|
+
app.innerHTML = shell(body, 'admin')
|
|
3770
|
+
}
|
|
3771
|
+
|
|
3772
|
+
window.loadQuotaUsage = async (id) => {
|
|
3773
|
+
const box = document.getElementById('usage-' + id)
|
|
3774
|
+
if (box) box.innerHTML = t('加载中...')
|
|
3775
|
+
const r = await GET('/admin/quota-requests/' + encodeURIComponent(id)).catch(() => null)
|
|
3776
|
+
if (box) box.innerHTML = (r && !r.error)
|
|
3777
|
+
? `${_qT('申请人近 24h 已建任务', 'Requester tasks in last 24h')}: <b>${escHtml(String(r.requester_usage_24h))}</b>`
|
|
3778
|
+
: ((r && r.error) || _qT('加载失败', 'Failed'))
|
|
3779
|
+
}
|
|
3780
|
+
window.approveQuotaReq = async (id) => {
|
|
3781
|
+
const v = (p) => (document.getElementById(p + '-' + id)?.value || '').trim()
|
|
3782
|
+
const body = { extra_count: Number(v('ap-count')) || undefined, duration_hours: Number(v('ap-dur')) || undefined, approval_note: v('ap-note') || undefined }
|
|
3783
|
+
const r = await POST('/admin/quota-requests/' + encodeURIComponent(id) + '/approve', body)
|
|
3784
|
+
if (r && r.error) { toast$(r.error_code === 'SELF_DECISION' ? _qT('不能审核自己的申请', 'Cannot decide your own request') : (r.error || _qT('批准失败', 'Approve failed'))); return }
|
|
3785
|
+
toast$(_qT('已批准', 'Approved'))
|
|
3786
|
+
renderAdminBuildTaskQuota(document.getElementById('app'), 'pending')
|
|
3787
|
+
}
|
|
3788
|
+
window.rejectQuotaReq = async (id) => {
|
|
3789
|
+
const note = (document.getElementById('rj-note-' + id)?.value || '').trim()
|
|
3790
|
+
const r = await POST('/admin/quota-requests/' + encodeURIComponent(id) + '/reject', { rejection_note: note || undefined })
|
|
3791
|
+
if (r && r.error) { toast$(r.error_code === 'SELF_DECISION' ? _qT('不能审核自己的申请', 'Cannot decide your own request') : (r.error || _qT('拒绝失败', 'Reject failed'))); return }
|
|
3792
|
+
toast$(_qT('已拒绝', 'Rejected'))
|
|
3793
|
+
renderAdminBuildTaskQuota(document.getElementById('app'), 'pending')
|
|
3794
|
+
}
|
|
3795
|
+
window.revokeQuotaReq = async (id) => {
|
|
3796
|
+
const r = await POST('/admin/quota-requests/' + encodeURIComponent(id) + '/revoke', {})
|
|
3797
|
+
if (r && r.error) { toast$(r.error || _qT('撤销失败', 'Revoke failed')); return }
|
|
3798
|
+
toast$(_qT('已撤销', 'Revoked'))
|
|
3799
|
+
renderAdminBuildTaskQuota(document.getElementById('app'), 'approved')
|
|
3800
|
+
}
|
|
3801
|
+
|
|
3315
3802
|
async function renderAdminKPI(app) {
|
|
3316
3803
|
if (!state.user) { renderLogin(); return }
|
|
3317
3804
|
if (!isAdmin()) { app.innerHTML = shell(`<div class="alert alert-info">${t('仅限管理员')}</div>`, 'admin'); return }
|
|
@@ -3466,22 +3953,17 @@ async function renderAdminDashboard(app) {
|
|
|
3466
3953
|
])
|
|
3467
3954
|
const tk = data.tokenomics || {}
|
|
3468
3955
|
const kpiTokenomics1 = kpiGrid([
|
|
3469
|
-
{ label: t('全球基金池'), value: Number(tk.pool_balance || 0).toFixed(2), unit: 'WAZ' },
|
|
3470
|
-
{ label: t('待结 Score'), value: Number(tk.scores_pending || 0).toFixed(0) },
|
|
3471
|
-
{ label: t('管理津贴池'), value: Number(tk.management_bonus || 0).toFixed(2), unit: 'WAZ' },
|
|
3472
|
-
])
|
|
3473
|
-
const kpiTokenomics2 = kpiGrid([
|
|
3474
3956
|
{ label: t('累计分享分润'),value: Number(tk.commission_total || 0).toFixed(2), unit: 'WAZ' },
|
|
3475
|
-
{ label: t('累计匹配发放'),value: Number(tk.binary_waz_total || 0).toFixed(2), unit: 'WAZ' },
|
|
3476
3957
|
{ label: t('PV 待处理'), value: tk.ledger_pending ?? 0 },
|
|
3958
|
+
{ label: t('参与记录用户'), value: tk.dirty_users ?? 0 },
|
|
3477
3959
|
])
|
|
3960
|
+
const kpiTokenomics2 = ''
|
|
3478
3961
|
// 异常告警 banner — 多条件聚合
|
|
3479
3962
|
const alerts = []
|
|
3480
3963
|
if ((data.active_verifiers ?? 0) < 5) alerts.push({ icon: '⚠️', color: '#dc2626', text: t('活跃审核员不足 5 人 — 请尽快批准申请'), href: '#admin/verifier-applications' })
|
|
3481
3964
|
if ((data.disputes_open ?? 0) > 10) alerts.push({ icon: '⚖️', color: '#dc2626', text: t('待处理争议') + ' > 10:' + data.disputes_open, href: '#admin/disputes' })
|
|
3482
3965
|
if ((data.verifier_apps_pending ?? 0) > 5) alerts.push({ icon: '📥', color: '#d97706', text: t('待审申请积压') + ': ' + data.verifier_apps_pending, href: '#admin/verifier-applications' })
|
|
3483
3966
|
if ((data.users_suspended ?? 0) > (data.users ?? 0) * 0.05) alerts.push({ icon: '⛔', color: '#d97706', text: t('暂停账户占比 > 5%') + ': ' + data.users_suspended, href: '#admin/users' })
|
|
3484
|
-
if (Number(tk.pool_balance || 0) < 100) alerts.push({ icon: '💰', color: '#dc2626', text: t('基金池水位告急') + ': ' + Number(tk.pool_balance || 0).toFixed(0) + ' WAZ', href: '#admin/tokenomics' })
|
|
3485
3967
|
const lowVerifierWarn = alerts.length > 0 ? `
|
|
3486
3968
|
<div style="margin-bottom:14px">
|
|
3487
3969
|
<div style="font-size:11px;color:#6b7280;margin-bottom:4px;font-weight:600">🚨 ${t('需要关注')} (${alerts.length})</div>
|
|
@@ -3514,7 +3996,7 @@ async function renderAdminDashboard(app) {
|
|
|
3514
3996
|
</div>
|
|
3515
3997
|
<div style="font-size:13px;color:#6b7280;margin:16px 0 8px">⚛ ${t('Tokenomics')}</div>
|
|
3516
3998
|
<div style="display:grid;grid-template-columns:repeat(2,1fr);gap:10px;margin-bottom:16px">
|
|
3517
|
-
${quickAction('#admin/tokenomics', '
|
|
3999
|
+
${quickAction('#admin/tokenomics', '⚙', t('协议运营 / 注册门控 / 佣金榜'))}
|
|
3518
4000
|
</div>
|
|
3519
4001
|
<div style="font-size:13px;color:#6b7280;margin:16px 0 8px">🔐 ${t('安全与审计')}</div>
|
|
3520
4002
|
<div style="display:grid;grid-template-columns:repeat(2,1fr);gap:10px;margin-bottom:16px">
|
|
@@ -3542,7 +4024,7 @@ async function renderAdminDashboard(app) {
|
|
|
3542
4024
|
${kpi4}
|
|
3543
4025
|
${sectionTitle('📥', t('卖家配额'), '#d97706')}
|
|
3544
4026
|
${kpi5}
|
|
3545
|
-
${sectionTitle('
|
|
4027
|
+
${sectionTitle('⚙', t('协议运营'), '#9333ea')}
|
|
3546
4028
|
${kpiTokenomics1}
|
|
3547
4029
|
${kpiTokenomics2}
|
|
3548
4030
|
${sectionTitle('⚡', t('快捷操作'), '#4f46e5')}
|
|
@@ -3894,10 +4376,6 @@ async function renderAdminUserDetail(app, userId) {
|
|
|
3894
4376
|
<div style="font-weight:600;margin-bottom:8px">🎭 ${t('角色 & 权限')}</div>
|
|
3895
4377
|
<div style="font-size:13px;margin-bottom:6px"><span style="color:#6b7280">${t('当前激活')}</span>: <strong>${b.role}</strong></div>
|
|
3896
4378
|
<div style="font-size:13px;margin-bottom:8px"><span style="color:#6b7280">${t('全部角色')}</span>: ${(b.roles || []).map(r => `<span style="font-size:11px;background:#f3f4f6;padding:2px 6px;border-radius:8px;margin-right:4px">${r}</span>`).join('')}</div>
|
|
3897
|
-
<div style="font-size:13px;display:flex;justify-content:space-between;align-items:center;padding-top:8px;border-top:1px solid #f3f4f6">
|
|
3898
|
-
<span><span style="color:#6b7280">🎁 ${t('管理津贴资格')}</span>: ${b.mgmt_bonus_eligible ? `<span style="color:#16a34a">✓ ${t('已授予')}</span>` : `<span style="color:#9ca3af">${t('未授予')}</span>`}</span>
|
|
3899
|
-
<button class="btn btn-outline btn-sm" style="font-size:11px;padding:3px 8px" onclick="doMgmtBonusEligibleDetail('${b.id}',${!b.mgmt_bonus_eligible})">${b.mgmt_bonus_eligible ? t('撤销') : t('授予')}</button>
|
|
3900
|
-
</div>
|
|
3901
4379
|
<div style="font-size:13px;padding-top:8px;border-top:1px solid #f3f4f6">
|
|
3902
4380
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">
|
|
3903
4381
|
<span><span style="color:#6b7280">🎯 ${t('L1 分享权限')}</span>: ${b.can_l1_share ? `<span style="color:#16a34a">✓ ${t('可拿分享佣金')}</span>` : `<span style="color:#9ca3af">${t('不可')}</span>`}</span>
|
|
@@ -4837,7 +5315,7 @@ async function renderRewardsMe(app) {
|
|
|
4837
5315
|
`}
|
|
4838
5316
|
|
|
4839
5317
|
<div style="margin-top:20px;font-size:11px;color:#9ca3af;line-height:1.6">
|
|
4840
|
-
${t('协议依据:')} <a href="https://github.com/
|
|
5318
|
+
${t('协议依据:')} <a href="https://github.com/webaz-protocol/webaz/blob/main/docs/rfcs/RFC-002-rewards-opt-in.md" target="_blank" style="color:#6b7280">RFC-002</a>
|
|
4841
5319
|
</div>
|
|
4842
5320
|
`, 'me')
|
|
4843
5321
|
}
|
|
@@ -4904,12 +5382,12 @@ async function renderOnboarding(app, role) {
|
|
|
4904
5382
|
}
|
|
4905
5383
|
|
|
4906
5384
|
const studyDocs = [
|
|
4907
|
-
{ name: 'META-RULES-FULL.md', desc: t('10 元规则,特别是 #5 不偏袒 / #6 不滥用'), link: 'https://github.com/
|
|
4908
|
-
{ name: 'CHARTER.md §3.2 + §6', desc: t('权力边界:多签 + 修改流程'), link: 'https://github.com/
|
|
4909
|
-
{ name: 'SECURITY.md §Iron-Rule', desc: t('真人 Passkey 7 条路径'), link: 'https://github.com/
|
|
4910
|
-
{ name: 'ECONOMIC-MODEL.md §11', desc: t('经济博弈原则 + 关系层估值层'), link: 'https://github.com/
|
|
4911
|
-
{ name: 'ARBITRATION-PLAYBOOK.md', desc: t('案例决策树 + 4 种结算路径') + (role === 'arbitrator' ? ' (' + t('必读') + ')' : ''), link: 'https://github.com/
|
|
4912
|
-
{ name: '
|
|
5385
|
+
{ name: 'META-RULES-FULL.md', desc: t('10 元规则,特别是 #5 不偏袒 / #6 不滥用'), link: 'https://github.com/webaz-protocol/webaz/blob/main/docs/META-RULES-FULL.md' },
|
|
5386
|
+
{ name: 'CHARTER.md §3.2 + §6', desc: t('权力边界:多签 + 修改流程'), link: 'https://github.com/webaz-protocol/webaz/blob/main/docs/CHARTER.md' },
|
|
5387
|
+
{ name: 'SECURITY.md §Iron-Rule', desc: t('真人 Passkey 7 条路径'), link: 'https://github.com/webaz-protocol/webaz/blob/main/SECURITY.md' },
|
|
5388
|
+
{ name: 'ECONOMIC-MODEL.md §11', desc: t('经济博弈原则 + 关系层估值层'), link: 'https://github.com/webaz-protocol/webaz/blob/main/docs/ECONOMIC-MODEL.md' },
|
|
5389
|
+
{ name: 'ARBITRATION-PLAYBOOK.md', desc: t('案例决策树 + 4 种结算路径') + (role === 'arbitrator' ? ' (' + t('必读') + ')' : ''), link: 'https://github.com/webaz-protocol/webaz/blob/main/docs/ARBITRATION-PLAYBOOK.md' },
|
|
5390
|
+
{ name: 'PARTICIPATION-ATTRIBUTION-COMPLIANCE.md', desc: t('合规边界'), link: 'https://github.com/webaz-protocol/webaz/blob/main/docs/PARTICIPATION-ATTRIBUTION-COMPLIANCE.md' },
|
|
4913
5391
|
]
|
|
4914
5392
|
|
|
4915
5393
|
const studySection = studyDocs.map((d, i) => `
|
|
@@ -5809,16 +6287,6 @@ async function renderAdminTokenomics(app) {
|
|
|
5809
6287
|
app.innerHTML = shell(loading$(), 'admin')
|
|
5810
6288
|
const data = await GET('/admin/tokenomics')
|
|
5811
6289
|
if (data.error) { app.innerHTML = shell(alert$('error', data.error), 'admin'); return }
|
|
5812
|
-
const gf = data.global_fund || {}
|
|
5813
|
-
const mb = data.management_bonus_pool || {}
|
|
5814
|
-
|
|
5815
|
-
const tierTable = (data.tier_config || []).map(t => `
|
|
5816
|
-
<tr style="border-bottom:1px solid #f3f4f6">
|
|
5817
|
-
<td style="padding:6px 8px">tier ${t.tier}</td>
|
|
5818
|
-
<td style="padding:6px 8px;text-align:right">${Number(t.pv_threshold).toLocaleString()}</td>
|
|
5819
|
-
<td style="padding:6px 8px;text-align:right;font-weight:600">${t.score_per_hit}</td>
|
|
5820
|
-
<td style="padding:6px 8px;text-align:center">${t.active ? '✓' : '⏸'}</td>
|
|
5821
|
-
</tr>`).join('')
|
|
5822
6290
|
|
|
5823
6291
|
const commRows = (data.top_commission || []).map(r => `
|
|
5824
6292
|
<div style="display:flex;justify-content:space-between;padding:6px 12px;border-bottom:1px solid #f3f4f6;font-size:12px">
|
|
@@ -5826,104 +6294,27 @@ async function renderAdminTokenomics(app) {
|
|
|
5826
6294
|
<div style="font-weight:700;color:#059669">${Number(r.earned).toFixed(2)} WAZ</div>
|
|
5827
6295
|
</div>`).join('') || `<div style="text-align:center;color:#9ca3af;padding:12px;font-size:12px">${t('暂无数据')}</div>`
|
|
5828
6296
|
|
|
5829
|
-
const binaryRows = (data.top_binary || []).map(r => `
|
|
5830
|
-
<div style="display:flex;justify-content:space-between;padding:6px 12px;border-bottom:1px solid #f3f4f6;font-size:12px">
|
|
5831
|
-
<div>${escHtml(r.name || r.user_id)} · ${r.hits}${t('次')} · ${r.score_total}${t('分')}</div>
|
|
5832
|
-
<div style="font-weight:700;color:#0891b2">${Number(r.waz_total).toFixed(2)} WAZ</div>
|
|
5833
|
-
</div>`).join('') || `<div style="text-align:center;color:#9ca3af;padding:12px;font-size:12px">${t('暂无数据')}</div>`
|
|
5834
|
-
|
|
5835
6297
|
app.innerHTML = shell(`
|
|
5836
|
-
<h1 class="page-title"
|
|
6298
|
+
<h1 class="page-title">⚙ ${t('协议运营')}</h1>
|
|
5837
6299
|
<div style="margin-bottom:12px"><button class="btn btn-outline btn-sm" style="width:auto" onclick="navigate('#admin')">${t('返回概览')}</button></div>
|
|
5838
6300
|
<div id="tk-msg"></div>
|
|
5839
6301
|
|
|
5840
|
-
<h2 style="font-size:15px;font-weight:600;margin:16px 0 8px">💰 ${t('全球基金池')}</h2>
|
|
5841
|
-
<div class="card" style="margin-bottom:12px">
|
|
5842
|
-
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;font-size:13px">
|
|
5843
|
-
<div><div style="color:#6b7280;font-size:11px">${t('池子余额')}</div><div style="font-size:18px;font-weight:700">${Number(gf.pool_balance ?? 0).toFixed(2)} WAZ</div></div>
|
|
5844
|
-
<div><div style="color:#6b7280;font-size:11px">${t('待结 Score')}</div><div style="font-size:18px;font-weight:700">${Number(gf.total_scores_pending ?? 0).toFixed(0)}</div></div>
|
|
5845
|
-
<div><div style="color:#6b7280;font-size:11px">${t('上次 N')}</div><div style="font-size:18px;font-weight:700">${Number(gf.current_n ?? 0).toFixed(4)}</div></div>
|
|
5846
|
-
</div>
|
|
5847
|
-
${gf.last_settled_at ? `<div style="font-size:11px;color:#9ca3af;margin-top:6px">${t('上次结算')}: ${fmtTime(gf.last_settled_at)}</div>` : ''}
|
|
5848
|
-
</div>
|
|
5849
|
-
|
|
5850
|
-
<h2 style="font-size:15px;font-weight:600;margin:16px 0 8px">🎁 ${t('管理津贴池')}</h2>
|
|
5851
|
-
<div class="card" style="margin-bottom:12px">
|
|
5852
|
-
<div style="font-size:18px;font-weight:700">${Number(mb.balance ?? 0).toFixed(2)} WAZ</div>
|
|
5853
|
-
<div style="font-size:11px;color:#9ca3af;margin-top:4px">${t('来自协议费 50%,用于大博主匹配团队 10/5/2% 补贴')}</div>
|
|
5854
|
-
<div id="mgmt-bonus-control" style="margin-top:12px;padding:10px;background:#fef9c3;border:1px solid #fde047;border-radius:6px">
|
|
5855
|
-
<div style="font-size:12px;font-weight:600;margin-bottom:6px">⚠️ ${t('津贴门控')}</div>
|
|
5856
|
-
<div id="mgmt-bonus-status" style="font-size:11px;color:#92400e;margin-bottom:8px">${t('加载中...')}</div>
|
|
5857
|
-
<button class="btn btn-outline btn-sm" style="font-size:11px" onclick="loadMgmtBonusStatus()">${t('查看资格用户 + 切换开关')}</button>
|
|
5858
|
-
</div>
|
|
5859
|
-
</div>
|
|
5860
|
-
|
|
5861
6302
|
<h2 style="font-size:15px;font-weight:600;margin:16px 0 8px">🚪 ${t('注册门控')}</h2>
|
|
5862
6303
|
<div class="card" style="margin-bottom:12px">
|
|
5863
6304
|
<div id="require-ref-status" style="font-size:12px;color:#6b7280">${t('加载中...')}</div>
|
|
5864
6305
|
<div style="font-size:11px;color:#9ca3af;margin-top:6px">${t('开启后:无邀请码不能注册(admin/物流/仲裁/审核员 与 region=china 豁免)')}</div>
|
|
5865
6306
|
</div>
|
|
5866
6307
|
|
|
5867
|
-
<h2 style="font-size:15px;font-weight:600;margin:16px 0 8px"
|
|
5868
|
-
<div class="card" style="margin-bottom:12px">
|
|
5869
|
-
<div id="invite-rotation-status" style="font-size:12px;color:#6b7280">${t('加载中...')}</div>
|
|
5870
|
-
<div style="font-size:11px;color:#9ca3af;margin-top:6px">${t('开启后:注册页"获取邀请码"按钮可用,依次轮询 xiaohua / mian / holden / jiayi / qingliang 5 位用户的 permanent_code')}</div>
|
|
5871
|
-
</div>
|
|
5872
|
-
|
|
5873
|
-
<h2 style="font-size:15px;font-weight:600;margin:16px 0 8px">⚙ ${t('Cron 控制 + 紧急操作')}</h2>
|
|
6308
|
+
<h2 style="font-size:15px;font-weight:600;margin:16px 0 8px">📒 ${t('参与记录(PV)流水')}</h2>
|
|
5874
6309
|
<div class="card" style="margin-bottom:12px">
|
|
5875
|
-
<
|
|
5876
|
-
<button class="btn btn-outline btn-sm" style="font-size:11px" onclick="doTkProcess()">${t('处理 PV 流水')}</button>
|
|
5877
|
-
<button class="btn btn-outline btn-sm" style="font-size:11px" onclick="doTkSettle()">${t('触发匹配结算')}</button>
|
|
5878
|
-
<button class="btn btn-outline btn-sm" style="font-size:11px;color:#dc2626;border-color:#dc2626" onclick="if(confirm('${t('确认分发 WAZ?这将清空池子按 N 比例分配')}'))doTkDistribute()">${t('分发 WAZ(清池)')}</button>
|
|
5879
|
-
</div>
|
|
6310
|
+
<button class="btn btn-outline btn-sm" style="font-size:11px;margin-bottom:8px" onclick="doTkProcess()">${t('处理 PV 流水')}</button>
|
|
5880
6311
|
<div style="font-size:12px;color:#6b7280">${t('PV 待处理流水')}: ${data.pv_ledger?.pending ?? 0} ${t('条')} · ${t('待累积 PV')}: ${Number(data.pv_ledger?.pending_pv ?? 0).toLocaleString()}</div>
|
|
5881
6312
|
</div>
|
|
5882
6313
|
|
|
5883
|
-
<h2 style="font-size:15px;font-weight:600;margin:16px 0 8px">📊 ${t('Tier 配置')} <span style="font-size:11px;color:#9ca3af">${t('(admin 可调)')}</span></h2>
|
|
5884
|
-
<div class="card" style="margin-bottom:12px;padding:0">
|
|
5885
|
-
<table style="width:100%;border-collapse:collapse">
|
|
5886
|
-
<tr style="background:#f9fafb;font-size:11px;color:#6b7280">
|
|
5887
|
-
<th style="padding:6px 8px;text-align:left">Tier</th>
|
|
5888
|
-
<th style="padding:6px 8px;text-align:right">${t('门槛 PV')}</th>
|
|
5889
|
-
<th style="padding:6px 8px;text-align:right">Score</th>
|
|
5890
|
-
<th style="padding:6px 8px;text-align:center">${t('启用')}</th>
|
|
5891
|
-
</tr>
|
|
5892
|
-
${tierTable}
|
|
5893
|
-
</table>
|
|
5894
|
-
<div style="padding:8px 12px;font-size:11px;color:#6b7280">${t('调整:POST /api/admin/tokenomics/tier with body {tier, pv_threshold, score_per_hit, active}')}</div>
|
|
5895
|
-
</div>
|
|
5896
|
-
|
|
5897
6314
|
<h2 style="font-size:15px;font-weight:600;margin:16px 0 8px">🏆 ${t('Top 分享佣金')} (Top 10)</h2>
|
|
5898
6315
|
<div class="card" style="padding:0;margin-bottom:12px">${commRows}</div>
|
|
5899
|
-
|
|
5900
|
-
<h2 style="font-size:15px;font-weight:600;margin:16px 0 8px">🏆 ${t('Top 匹配收益')} (Top 10)</h2>
|
|
5901
|
-
<div class="card" style="padding:0;margin-bottom:12px">${binaryRows}</div>
|
|
5902
6316
|
`, 'admin')
|
|
5903
6317
|
setTimeout(loadRequireRefStatus, 100)
|
|
5904
|
-
setTimeout(loadInviteRotationStatus, 100)
|
|
5905
|
-
}
|
|
5906
|
-
|
|
5907
|
-
window.loadInviteRotationStatus = async () => {
|
|
5908
|
-
const f = await GET('/system-flags').catch(() => null)
|
|
5909
|
-
const el = document.getElementById('invite-rotation-status')
|
|
5910
|
-
if (!el) return
|
|
5911
|
-
const enabled = !!f?.invite_rotation_enabled
|
|
5912
|
-
el.innerHTML = `
|
|
5913
|
-
<div style="display:flex;justify-content:space-between;align-items:center">
|
|
5914
|
-
<strong>${t('邀请码轮询')}</strong>: ${enabled ? `<span style="color:#16a34a">🟢 ${t('已开启 — 按钮可用')}</span>` : `<span style="color:#dc2626">🔒 ${t('已关闭 — 按钮置灰')}</span>`}
|
|
5915
|
-
<button class="btn btn-outline btn-sm" style="font-size:11px" onclick="doToggleInviteRotation(${!enabled})">${enabled ? t('关闭') : t('开启')}</button>
|
|
5916
|
-
</div>`
|
|
5917
|
-
}
|
|
5918
|
-
|
|
5919
|
-
window.doToggleInviteRotation = async (enable) => {
|
|
5920
|
-
if (!confirm(enable
|
|
5921
|
-
? t('确认开启邀请码轮询?访客在注册页点按钮可申领下一个 sponsor 邀请码')
|
|
5922
|
-
: t('确认关闭邀请码轮询?按钮将置灰')
|
|
5923
|
-
)) return
|
|
5924
|
-
const res = await POST('/admin/invite-rotation/toggle', { enabled: enable })
|
|
5925
|
-
if (res.error) return alert(res.error)
|
|
5926
|
-
loadInviteRotationStatus()
|
|
5927
6318
|
}
|
|
5928
6319
|
|
|
5929
6320
|
window.loadRequireRefStatus = async () => {
|
|
@@ -5954,46 +6345,6 @@ window.doTkProcess = async () => {
|
|
|
5954
6345
|
setTimeout(() => renderAdminTokenomics(document.getElementById('app')), 800)
|
|
5955
6346
|
}
|
|
5956
6347
|
|
|
5957
|
-
window.doTkSettle = async () => {
|
|
5958
|
-
const res = await POST('/admin/atomic/run-settlement', {})
|
|
5959
|
-
const msg = document.getElementById('tk-msg')
|
|
5960
|
-
if (msg) msg.innerHTML = res.error ? alert$('error', res.error) : alert$('success', `${t('已触发匹配')}: ${res.settled}`)
|
|
5961
|
-
setTimeout(() => renderAdminTokenomics(document.getElementById('app')), 800)
|
|
5962
|
-
}
|
|
5963
|
-
|
|
5964
|
-
window.loadMgmtBonusStatus = async () => {
|
|
5965
|
-
const data = await GET('/admin/tokenomics/mgmt-bonus')
|
|
5966
|
-
if (data.error) return alert(data.error)
|
|
5967
|
-
const el = document.getElementById('mgmt-bonus-status')
|
|
5968
|
-
const userRows = (data.eligible_users || []).map(u => `
|
|
5969
|
-
<div style="display:flex;justify-content:space-between;padding:4px 6px;font-size:11px;border-bottom:1px solid #fef9c3">
|
|
5970
|
-
<div>${escHtml(u.name)} <span style="color:#9ca3af">(L1=${u.l1_count}, 累计佣金 ${Number(u.total_commission).toFixed(2)} WAZ)</span></div>
|
|
5971
|
-
<button style="font-size:10px;color:#dc2626;background:none;border:none;cursor:pointer" onclick="doMgmtBonusEligible('${u.id}',false)">${t('撤销')}</button>
|
|
5972
|
-
</div>`).join('') || `<div style="font-size:11px;color:#9ca3af;text-align:center;padding:8px">${t('暂无资格用户')}</div>`
|
|
5973
|
-
el.innerHTML = `
|
|
5974
|
-
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
|
|
5975
|
-
<span><strong>${t('全局开关')}</strong>: ${data.enabled ? `<span style="color:#16a34a">✓ ${t('已开启')}</span>` : `<span style="color:#dc2626">⊘ ${t('已关闭')}</span>`}</span>
|
|
5976
|
-
<button class="btn btn-outline btn-sm" style="font-size:11px" onclick="doMgmtBonusToggle(${!data.enabled})">${data.enabled ? t('关闭') : t('开启')}</button>
|
|
5977
|
-
</div>
|
|
5978
|
-
<div style="font-size:11px;color:#92400e;margin-bottom:6px">${t('资格用户')} (${data.eligible_count}):</div>
|
|
5979
|
-
<div style="background:#fff;border-radius:4px;max-height:200px;overflow-y:auto">${userRows}</div>
|
|
5980
|
-
<div style="font-size:10px;color:#92400e;margin-top:6px">${t('在用户详情页可授予/撤销该资格')}</div>`
|
|
5981
|
-
}
|
|
5982
|
-
|
|
5983
|
-
window.doMgmtBonusToggle = async (enable) => {
|
|
5984
|
-
if (!confirm(enable ? t('确认开启管理津贴?') : t('确认关闭管理津贴?(已发放的不回收)'))) return
|
|
5985
|
-
const res = await POST('/admin/tokenomics/mgmt-bonus/toggle', { enabled: enable })
|
|
5986
|
-
if (res.error) return alert(res.error)
|
|
5987
|
-
loadMgmtBonusStatus()
|
|
5988
|
-
}
|
|
5989
|
-
|
|
5990
|
-
window.doMgmtBonusEligible = async (userId, eligible) => {
|
|
5991
|
-
if (!confirm(eligible ? t('授予该用户管理津贴资格?') : t('撤销该用户管理津贴资格?'))) return
|
|
5992
|
-
const res = await POST(`/admin/users/${userId}/mgmt-bonus-eligible`, { eligible })
|
|
5993
|
-
if (res.error) return alert(res.error)
|
|
5994
|
-
loadMgmtBonusStatus()
|
|
5995
|
-
}
|
|
5996
|
-
|
|
5997
6348
|
window.doL1ShareOverride = async (userId, value) => {
|
|
5998
6349
|
const labels = { 1: t('强制允许'), 0: t('Auto'), '-1': t('强制禁止') }
|
|
5999
6350
|
if (!confirm(`${t('设置 L1 分享权限为')}: ${labels[value]}?`)) return
|
|
@@ -6002,20 +6353,6 @@ window.doL1ShareOverride = async (userId, value) => {
|
|
|
6002
6353
|
renderAdminUserDetail(document.getElementById('app'), userId)
|
|
6003
6354
|
}
|
|
6004
6355
|
|
|
6005
|
-
window.doMgmtBonusEligibleDetail = async (userId, eligible) => {
|
|
6006
|
-
if (!confirm(eligible ? t('授予该用户管理津贴资格?') : t('撤销该用户管理津贴资格?'))) return
|
|
6007
|
-
const res = await POST(`/admin/users/${userId}/mgmt-bonus-eligible`, { eligible })
|
|
6008
|
-
if (res.error) { showUserOpMsg(alert$('error', res.error)); return }
|
|
6009
|
-
renderAdminUserDetail(document.getElementById('app'), userId)
|
|
6010
|
-
}
|
|
6011
|
-
|
|
6012
|
-
window.doTkDistribute = async () => {
|
|
6013
|
-
const res = await POST('/admin/atomic/distribute', {})
|
|
6014
|
-
const msg = document.getElementById('tk-msg')
|
|
6015
|
-
if (msg) msg.innerHTML = res.error ? alert$('error', res.error) : alert$('success', `${t('已分发 WAZ')}: ${Number(res.distributed||0).toFixed(2)}`)
|
|
6016
|
-
setTimeout(() => renderAdminTokenomics(document.getElementById('app')), 800)
|
|
6017
|
-
}
|
|
6018
|
-
|
|
6019
6356
|
async function renderAdminAudit(app) {
|
|
6020
6357
|
if (!state.user) { renderLogin(); return }
|
|
6021
6358
|
if (!isAdmin()) { app.innerHTML = shell(`<div class="alert alert-info">${t('仅限管理员')}</div>`, 'admin-audit'); return }
|
|
@@ -6517,76 +6854,58 @@ function renderWelcome(app) {
|
|
|
6517
6854
|
<section class="w-section" id="w-join-section">
|
|
6518
6855
|
<h2 style="${H2_STYLE}">${T('成为 webazer', 'Become a webazer')}</h2>
|
|
6519
6856
|
<p style="${SUB_STYLE}">${T('无论你从哪里来,你已经在路上了。', "Wherever you come from, you're already on the way.")}</p>
|
|
6520
|
-
<!--
|
|
6857
|
+
<!-- 三组意图:① 立即开始(注册/贡献) ② 保持联系(邮箱+社区) ③ 有想法(反馈/提任务) — 6 卡精简为 3,功能零丢失 -->
|
|
6521
6858
|
<div style="display:flex;flex-direction:column;gap:14px">
|
|
6522
|
-
|
|
6523
|
-
<div style="margin-bottom:14px">
|
|
6524
|
-
<div class="w-card-title">📧 ${T('申请加入创世团(深度参与)', 'Apply to join the Genesis Cohort (deep dive)')}</div>
|
|
6525
|
-
<div class="w-card-desc">${T('留下邮箱 + 期望身份 · 上线第一时间联系你,我们会主动跟进', "Leave your email + desired role · we'll reach out at launch and follow up")}</div>
|
|
6526
|
-
</div>
|
|
6527
|
-
<div style="display:flex;flex-direction:column;gap:10px">
|
|
6528
|
-
<input id="w-email" class="w-input" type="email" placeholder="your@email.com" autocomplete="email" style="margin-bottom:0">
|
|
6529
|
-
<!-- honeypot -->
|
|
6530
|
-
<input id="w-email-hp" name="website" autocomplete="off" tabindex="-1" style="display:none" aria-hidden="true">
|
|
6531
|
-
<select id="w-role-pref" class="w-input" style="margin-bottom:0">
|
|
6532
|
-
<option value="">${T('我想以什么身份开始?(可选)', 'How would you like to start? (optional)')}</option>
|
|
6533
|
-
<option value="buyer">${T('买家 · Agent 帮我找货', 'Buyer · let agents find me deals')}</option>
|
|
6534
|
-
<option value="seller">${T('卖家 · 上架商品并获取 Earn-Back', 'Seller · list products and earn back')}</option>
|
|
6535
|
-
<option value="creator">${T('分享者 · 测评、笔记、内容归因', 'Sharer · reviews, notes, content attribution')}</option>
|
|
6536
|
-
<option value="verifier">${T('审核员 · 链接 / 内容验证', 'Verifier · link & content verification')}</option>
|
|
6537
|
-
<option value="arbitrator">${T('仲裁员 · 争议裁决', 'Arbitrator · dispute resolution')}</option>
|
|
6538
|
-
<option value="other">${T('其他 / 都看看', 'Other / just curious')}</option>
|
|
6539
|
-
</select>
|
|
6540
|
-
<textarea id="w-note" class="w-input" rows="3" maxlength="500" placeholder="${T('补充说明(可选):你希望我们告诉你什么?有什么期待?', 'Notes (optional): what do you want to hear from us? Any expectations?')}" style="margin-bottom:0;resize:vertical;font-family:inherit"></textarea>
|
|
6541
|
-
<button class="w-btn-full w-btn-primary" onclick="submitWelcomeEmail()" style="padding:12px 20px">${T('申请加入', 'Submit application')}</button>
|
|
6542
|
-
<div id="w-email-msg" style="font-size:12px;text-align:center;min-height:1.5em"></div>
|
|
6543
|
-
</div>
|
|
6544
|
-
</div>
|
|
6859
|
+
<!-- ① 立即开始 — 主转化:注册 + 公开任务板(含贡献记录诚实披露) -->
|
|
6545
6860
|
<div class="w-card w-join-card">
|
|
6546
6861
|
<div class="w-join-card-left">
|
|
6547
|
-
<div class="w-card-title"
|
|
6548
|
-
<div class="w-card-desc">${T('
|
|
6862
|
+
<div class="w-card-title">🚀 ${T('立即开始', 'Start now')}</div>
|
|
6863
|
+
<div class="w-card-desc">${T('准备好了就进入协议;或浏览公开任务板参与建设。建议无需登录,认领与提交需登录。只有 canonical 仓库被合并的 PR(或维护者认可的 issue / task / RFC)才进入贡献记录 —— sandbox、本地草稿、普通购物 / 分享都不是正式贡献。', 'Ready? Step into the protocol — or browse the public task board to help build. Suggesting needs no login; claiming & submitting do. Only a merged PR on the canonical repo (or a maintainer-recognized issue / task / RFC) enters the contribution record — a sandbox run, a local draft, or ordinary shopping / sharing is not formal contribution.')}</div>
|
|
6549
6864
|
</div>
|
|
6550
6865
|
<div class="w-join-card-right">
|
|
6866
|
+
<button class="w-btn-full w-btn-primary" onclick="openAuthSheet('reg')">${T('立即注册', 'Sign Up Now')}</button>
|
|
6551
6867
|
<a class="w-btn-full w-btn-outline" href="#contribute/tasks" style="display:flex;align-items:center;justify-content:center;text-decoration:none">${T('公开任务板', 'Public task board')}</a>
|
|
6552
|
-
<a class="w-btn-full w-btn-outline" href="#contribute/tasks/suggest" style="display:flex;align-items:center;justify-content:center;text-decoration:none">${T('建议新任务', 'Suggest a task')}</a>
|
|
6553
6868
|
</div>
|
|
6554
6869
|
</div>
|
|
6555
|
-
|
|
6556
|
-
|
|
6557
|
-
|
|
6558
|
-
<div class="w-card-
|
|
6559
|
-
|
|
6560
|
-
<div class="w-join-card-right">
|
|
6561
|
-
<button class="w-btn-full w-btn-outline" onclick="openIdeaSheet()">${T('我有建议', 'I have an idea')}</button>
|
|
6562
|
-
</div>
|
|
6563
|
-
</div>
|
|
6564
|
-
<div class="w-card w-join-card">
|
|
6565
|
-
<div class="w-join-card-left">
|
|
6566
|
-
<div class="w-card-title">💬 ${T('加入社区', 'Join the community')}</div>
|
|
6567
|
-
<div class="w-card-desc">${T('找到同路人,开始对话', 'Find peers, start the conversation')}</div>
|
|
6568
|
-
</div>
|
|
6569
|
-
<div class="w-join-card-right">
|
|
6570
|
-
<a class="w-btn-full w-btn-outline" href="#" style="display:flex;align-items:center;justify-content:center;text-decoration:none">${T('加入 Discord', 'Join Discord')}</a>
|
|
6571
|
-
<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>
|
|
6572
|
-
</div>
|
|
6573
|
-
</div>
|
|
6574
|
-
<div class="w-card w-join-card">
|
|
6575
|
-
<div class="w-join-card-left">
|
|
6576
|
-
<div class="w-card-title">📧 ${T('邮件联系', 'Email us')}</div>
|
|
6577
|
-
<div class="w-card-desc">${T('合作 / 反馈 / 合规咨询', 'Partnerships / feedback / compliance')}</div>
|
|
6870
|
+
<!-- ② 保持联系 — 上线通知(邮箱+期望身份) + 社区/邮件;w-email / w-role-pref / w-email-hp / w-email-msg 保留(顶部角色 CTA 依赖) -->
|
|
6871
|
+
<div class="w-card w-join-card" style="flex-direction:column;align-items:stretch">
|
|
6872
|
+
<div style="margin-bottom:14px">
|
|
6873
|
+
<div class="w-card-title">📧 ${T('保持联系(上线通知)', 'Stay in touch (launch notice)')}</div>
|
|
6874
|
+
<div class="w-card-desc">${T('留下邮箱 + 期望身份,上线第一时间通知你;或在社区先聊起来。', "Leave your email + desired role and we'll notify you at launch; or start chatting in the community.")}</div>
|
|
6578
6875
|
</div>
|
|
6579
|
-
<div
|
|
6580
|
-
<
|
|
6876
|
+
<div style="display:flex;flex-direction:column;gap:10px">
|
|
6877
|
+
<div style="display:flex;gap:10px;flex-wrap:wrap">
|
|
6878
|
+
<input id="w-email" class="w-input" type="email" placeholder="your@email.com" autocomplete="email" style="margin-bottom:0;flex:2;min-width:180px">
|
|
6879
|
+
<select id="w-role-pref" class="w-input" style="margin-bottom:0;flex:1;min-width:150px">
|
|
6880
|
+
<option value="">${T('期望身份(可选)', 'Role (optional)')}</option>
|
|
6881
|
+
<option value="buyer">${T('买家 · Agent 帮我找货', 'Buyer · let agents find me deals')}</option>
|
|
6882
|
+
<option value="seller">${T('卖家 · 上架商品并获取 Earn-Back', 'Seller · list products and earn back')}</option>
|
|
6883
|
+
<option value="creator">${T('分享者 · 测评、笔记、内容归因', 'Sharer · reviews, notes, content attribution')}</option>
|
|
6884
|
+
<option value="verifier">${T('审核员 · 链接 / 内容验证', 'Verifier · link & content verification')}</option>
|
|
6885
|
+
<option value="arbitrator">${T('仲裁员 · 争议裁决', 'Arbitrator · dispute resolution')}</option>
|
|
6886
|
+
<option value="other">${T('其他 / 都看看', 'Other / just curious')}</option>
|
|
6887
|
+
</select>
|
|
6888
|
+
</div>
|
|
6889
|
+
<!-- honeypot -->
|
|
6890
|
+
<input id="w-email-hp" name="website" autocomplete="off" tabindex="-1" style="display:none" aria-hidden="true">
|
|
6891
|
+
<button class="w-btn-full w-btn-primary" onclick="submitWelcomeEmail()" style="padding:12px 20px">${T('通知我', 'Notify me')}</button>
|
|
6892
|
+
<div id="w-email-msg" style="font-size:12px;text-align:center;min-height:1.5em"></div>
|
|
6893
|
+
<div style="display:flex;gap:10px;flex-wrap:wrap;margin-top:2px">
|
|
6894
|
+
<a class="w-btn-full w-btn-outline" href="#" style="display:flex;align-items:center;justify-content:center;text-decoration:none;flex:1;min-width:90px">Discord</a>
|
|
6895
|
+
<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;flex:1;min-width:90px">Telegram</a>
|
|
6896
|
+
<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;flex:1;min-width:90px;font-family:ui-monospace,SF Mono,Menlo,monospace;font-size:12px">${T('邮件', 'Email')}</a>
|
|
6897
|
+
</div>
|
|
6581
6898
|
</div>
|
|
6582
6899
|
</div>
|
|
6900
|
+
<!-- ③ 有想法 — 匿名一句话反馈 + 提具体任务 -->
|
|
6583
6901
|
<div class="w-card w-join-card">
|
|
6584
6902
|
<div class="w-join-card-left">
|
|
6585
|
-
<div class="w-card-title"
|
|
6586
|
-
<div class="w-card-desc">${T('
|
|
6903
|
+
<div class="w-card-title">💡 ${T('有想法?', 'Got an idea?')}</div>
|
|
6904
|
+
<div class="w-card-desc">${T('匿名 · 无需邮箱 · 一句话想法 / 痛点 / bug;或提一个具体的新任务。', 'Anonymous · no email · a quick idea / pain point / bug; or propose a concrete new task.')}</div>
|
|
6587
6905
|
</div>
|
|
6588
6906
|
<div class="w-join-card-right">
|
|
6589
|
-
<button class="w-btn-full w-btn-
|
|
6907
|
+
<button class="w-btn-full w-btn-outline" onclick="openIdeaSheet()">${T('我有建议', 'I have an idea')}</button>
|
|
6908
|
+
<a class="w-btn-full w-btn-outline" href="#contribute/tasks/suggest" style="display:flex;align-items:center;justify-content:center;text-decoration:none">${T('建议新任务', 'Suggest a task')}</a>
|
|
6590
6909
|
</div>
|
|
6591
6910
|
</div>
|
|
6592
6911
|
</div>
|
|
@@ -6597,8 +6916,8 @@ function renderWelcome(app) {
|
|
|
6597
6916
|
<div>© 2026 webaz</div>
|
|
6598
6917
|
<div>${T('开放协议 · Agent 原生 · DAO 治理', 'Open Protocol · Agent-Native · DAO Governance')}</div>
|
|
6599
6918
|
<div style="margin-top:10px">
|
|
6600
|
-
<a href="https://github.com/
|
|
6601
|
-
<a href="https://github.com/
|
|
6919
|
+
<a href="https://github.com/webaz-protocol/webaz/blob/main/docs/META-RULES-FULL.md" target="_blank" rel="noopener">${T('完整元规则', 'Full Meta-Rules')}</a>
|
|
6920
|
+
<a href="https://github.com/webaz-protocol/webaz" target="_blank" rel="noopener">GitHub</a>
|
|
6602
6921
|
<a href="${WP_URL}" target="_blank" rel="noopener">${T('协议白皮书', 'Whitepaper')}</a>
|
|
6603
6922
|
<a href="mailto:contact@webaz.xyz">📧 contact@webaz.xyz</a>
|
|
6604
6923
|
</div>
|
|
@@ -6659,7 +6978,7 @@ function renderWelcome(app) {
|
|
|
6659
6978
|
alternateName: 'WebAZ',
|
|
6660
6979
|
url: location.origin,
|
|
6661
6980
|
description: 'Agent-native decentralized commerce protocol. State-machine transactions; explicit sign-offs at each transition; auto-timeout-default. Pre-launch.',
|
|
6662
|
-
sameAs: ['https://github.com/
|
|
6981
|
+
sameAs: ['https://github.com/webaz-protocol/webaz'],
|
|
6663
6982
|
},
|
|
6664
6983
|
{
|
|
6665
6984
|
'@type': 'Service',
|
|
@@ -6736,8 +7055,8 @@ async function renderGovernanceOnboarding(app) {
|
|
|
6736
7055
|
<section style="border-top:1px solid #e4e4e7;padding-top:20px;color:#71717a;font-size:13px;line-height:1.7">
|
|
6737
7056
|
<p style="margin:0 0 8px"><strong>${T('完整规范', 'Full spec')}:</strong></p>
|
|
6738
7057
|
<ul style="margin:0;padding-left:20px">
|
|
6739
|
-
<li><a href="https://github.com/
|
|
6740
|
-
<li><a href="https://github.com/
|
|
7058
|
+
<li><a href="https://github.com/webaz-protocol/webaz/blob/main/docs/GOVERNANCE-ONBOARDING.md" target="_blank" rel="noopener" style="color:#1d4ed8">GOVERNANCE-ONBOARDING.md</a> — ${T('资格 / 流程 / 卸任 / 申诉', 'eligibility / flow / resignation / appeal')}</li>
|
|
7059
|
+
<li><a href="https://github.com/webaz-protocol/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>
|
|
6741
7060
|
<li>${T('机读 JSON 端点', 'Machine-readable JSON endpoint')}: <code style="background:#f4f4f5;padding:2px 6px;border-radius:4px">/api/governance/onboarding-stats</code></li>
|
|
6742
7061
|
</ul>
|
|
6743
7062
|
</section>
|
|
@@ -6955,8 +7274,12 @@ window.contributeSetLang = (lang) => {
|
|
|
6955
7274
|
function contributeLangSwitchHTML(T) {
|
|
6956
7275
|
const en = window._lang === 'en'
|
|
6957
7276
|
const btn = (lang, label, active) => `<button type="button" ${active ? 'disabled' : `onclick="contributeSetLang('${lang}')"`} style="border:0;background:${active ? '#18181B' : 'transparent'};color:${active ? '#fff' : '#71717A'};padding:5px 10px;border-radius:7px;font-size:12px;font-weight:600;cursor:${active ? 'default' : 'pointer'}">${label}</button>`
|
|
7277
|
+
// 返回目标随登录态:已登录回应用首页(roleHome),未登录回欢迎页。
|
|
7278
|
+
// 之前硬编码 #welcome → 登录用户落到预登录营销页,看起来像被退出登录。
|
|
7279
|
+
const home = state.user ? roleHome(state.user.role) : '#welcome'
|
|
7280
|
+
const homeLabel = state.user ? T('返回首页', 'Home') : T('WebAZ 欢迎页', 'WebAZ Welcome')
|
|
6958
7281
|
return `<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;margin:0 0 18px;padding-bottom:10px;border-bottom:1px solid #e4e4e7">
|
|
6959
|
-
<a href="
|
|
7282
|
+
<a href="${home}" style="color:#52525B;text-decoration:none;font-size:13px;font-weight:700">← ${homeLabel}</a>
|
|
6960
7283
|
<div role="group" aria-label="${T('语言切换', 'Language switch')}" style="display:flex;align-items:center;gap:2px;border:1px solid #e4e4e7;border-radius:9px;padding:2px;background:#fff">
|
|
6961
7284
|
${btn('zh', '中文', !en)}${btn('en', 'EN', en)}
|
|
6962
7285
|
</div>
|
|
@@ -7020,7 +7343,7 @@ async function renderContributeTasks(app) {
|
|
|
7020
7343
|
return `<div onclick="location.hash='#contribute/tasks/${_cEsc(task.task_id)}'" style="background:#fff;border:1px solid #e4e4e7;border-radius:10px;padding:14px;margin-bottom:10px;cursor:pointer">
|
|
7021
7344
|
<div style="display:flex;justify-content:space-between;gap:10px;align-items:flex-start">
|
|
7022
7345
|
<div style="font-weight:600;color:#18181B;font-size:15px">${_cEsc(task.title)}</div>
|
|
7023
|
-
${m.auto_claimable ? `<span style="background:#dbeafe;color:#1e40af;padding:1px 8px;border-radius:99px;font-size:11px;font-weight:600;white-space:nowrap">${T('可自动认领', 'auto-claimable')}</span>` : ''}
|
|
7346
|
+
${m.claimability === 'auto_claimable' ? `<span style="background:#dbeafe;color:#1e40af;padding:1px 8px;border-radius:99px;font-size:11px;font-weight:600;white-space:nowrap">${T('可自动认领', 'auto-claimable')}</span>` : ''}
|
|
7024
7347
|
</div>
|
|
7025
7348
|
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-top:8px">
|
|
7026
7349
|
${task.area ? `<span style="font-size:11px;color:#6b7280">📂 ${_cEsc(task.area)}</span>` : ''}
|
|
@@ -7058,7 +7381,7 @@ async function renderContributeTaskDetail(app, id) {
|
|
|
7058
7381
|
CONTRIBUTE_PROMPT_STATE.text = buildContributeAgentPrompt(task, cct, T)
|
|
7059
7382
|
const section = (icon, title, inner) => `<section style="background:#fff;border:1px solid #e4e4e7;border-radius:12px;padding:18px;margin-bottom:14px">
|
|
7060
7383
|
<h3 style="margin:0 0 10px;color:#18181B;font-size:16px">${icon} ${title}</h3>${inner}</section>`
|
|
7061
|
-
const claimBtn = m.auto_claimable
|
|
7384
|
+
const claimBtn = m.claimability === 'auto_claimable'
|
|
7062
7385
|
? (state.apiKey
|
|
7063
7386
|
? `<button onclick="contributeClaim('${_cEsc(task.task_id)}')" style="padding:10px 20px;background:#6366f1;color:#fff;border:none;border-radius:8px;font-size:14px;cursor:pointer">✋ ${T('认领此任务', 'Claim this task')}</button>`
|
|
7064
7387
|
: `<button onclick="contributeClaim('${_cEsc(task.task_id)}')" style="padding:10px 20px;background:#fff;color:#18181B;border:1px solid #6366f1;border-radius:8px;font-size:14px;cursor:pointer">🔑 ${T('登录后认领', 'Log in to claim')}</button><div style="font-size:11px;color:#6b7280;margin-top:6px">${T('认领需要登录(真人 Passkey 账户);浏览器不会自动执行 GitHub 操作。', 'Claiming requires login (a real Passkey account); the browser performs no automatic GitHub action.')}</div>`)
|
|
@@ -7071,7 +7394,7 @@ async function renderContributeTaskDetail(app, id) {
|
|
|
7071
7394
|
${task.area ? `<span style="font-size:12px;color:#6b7280">📂 ${_cEsc(task.area)}</span>` : ''}
|
|
7072
7395
|
${m.task_type ? `<span style="font-size:12px;color:#6b7280">🔖 ${_cEsc(m.task_type)}</span>` : ''}
|
|
7073
7396
|
${_cRiskBadge(m.risk_level, T)} ${_cDuration(m.estimated_duration, T)}
|
|
7074
|
-
${m.auto_claimable ? `<span style="background:#dbeafe;color:#1e40af;padding:1px 8px;border-radius:99px;font-size:11px;font-weight:600">${T('可自动认领', 'auto-claimable')}</span>` : ''}
|
|
7397
|
+
${m.claimability === 'auto_claimable' ? `<span style="background:#dbeafe;color:#1e40af;padding:1px 8px;border-radius:99px;font-size:11px;font-weight:600">${T('可自动认领', 'auto-claimable')}</span>` : ''}
|
|
7075
7398
|
</div>
|
|
7076
7399
|
${_cBoundaryHTML(task.value_boundary || j.value_boundary, T)}
|
|
7077
7400
|
|
|
@@ -7165,7 +7488,7 @@ window.submitWelcomeEmail = async () => {
|
|
|
7165
7488
|
const email = document.getElementById('w-email')?.value?.trim() || ''
|
|
7166
7489
|
const hp = document.getElementById('w-email-hp')?.value || '' // honeypot
|
|
7167
7490
|
const rolePref = document.getElementById('w-role-pref')?.value || ''
|
|
7168
|
-
const note = (document.getElementById('w-note')?.value || '').trim().slice(0, 500)
|
|
7491
|
+
const note = (document.getElementById('w-note')?.value || '').trim().slice(0, 500) // note field retired in welcome consolidation; tolerated if absent
|
|
7169
7492
|
const msg = document.getElementById('w-email-msg')
|
|
7170
7493
|
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { if (msg) msg.innerHTML = `<span style="color:#dc2626">${t('请输入有效邮箱')}</span>`; return }
|
|
7171
7494
|
if (msg) msg.innerHTML = `<span style="color:#6366f1">${t('提交中…')}</span>`
|
|
@@ -7178,8 +7501,8 @@ window.submitWelcomeEmail = async () => {
|
|
|
7178
7501
|
})
|
|
7179
7502
|
if (r?.error) { if (msg) msg.innerHTML = `<span style="color:#dc2626">${r.error}</span>`; return }
|
|
7180
7503
|
if (msg) msg.innerHTML = `<span style="color:#16a34a">✓ ${t('已收到,上线时第一时间通知你')}</span>`
|
|
7181
|
-
document.getElementById('w-email').value = ''
|
|
7182
|
-
document.getElementById('w-note').value = ''
|
|
7504
|
+
const emailEl = document.getElementById('w-email'); if (emailEl) emailEl.value = ''
|
|
7505
|
+
const noteEl = document.getElementById('w-note'); if (noteEl) noteEl.value = '' // retired field — guard so clear never throws
|
|
7183
7506
|
// role-pref 保留(用户已表达过的偏好,刷新页面前都留着)
|
|
7184
7507
|
}
|
|
7185
7508
|
|
|
@@ -7213,13 +7536,21 @@ window.openParticipateSheet = (defaultTab) => {
|
|
|
7213
7536
|
<button onclick="(closeSheet(),openAuthSheet('${defaultTab||'login'}'))" class="btn btn-primary" style="width:100%;padding:14px;font-size:15px;font-weight:600;border-radius:10px;margin-bottom:10px;display:flex;align-items:center;justify-content:space-between">
|
|
7214
7537
|
<span>🔑 ${t('注册 / 登录')}</span><span style="font-size:13px;opacity:0.6">›</span>
|
|
7215
7538
|
</button>
|
|
7216
|
-
|
|
7217
|
-
|
|
7218
|
-
|
|
7219
|
-
|
|
7220
|
-
|
|
7221
|
-
|
|
7222
|
-
|
|
7539
|
+
<!-- 次级探索路径(浏览任务板 / 提建议 / 了解协议)收进可展开「了解更多」,突出主 CTA -->
|
|
7540
|
+
<details style="border:1px solid #e5e7eb;border-radius:10px;overflow:hidden">
|
|
7541
|
+
<summary style="padding:13px 14px;font-size:14px;font-weight:600;color:#4338ca;cursor:pointer;list-style:none;display:flex;align-items:center;justify-content:space-between">
|
|
7542
|
+
<span>📖 ${t('了解更多')}</span><span style="font-size:13px;opacity:0.6">▾</span>
|
|
7543
|
+
</summary>
|
|
7544
|
+
<div style="padding:2px 10px 12px;display:flex;flex-direction:column;gap:8px">
|
|
7545
|
+
<button onclick="(closeSheet(),navigate('#contribute/tasks'))" class="btn btn-outline" style="width:100%;padding:12px;font-size:14px;font-weight:600;border-radius:9px;display:flex;align-items:center;justify-content:space-between;border:1.5px solid #c7d2fe;color:#4338ca">
|
|
7546
|
+
<span>🛠 ${t('浏览公开任务板')}</span><span style="font-size:13px;opacity:0.6">›</span>
|
|
7547
|
+
</button>
|
|
7548
|
+
<button onclick="(closeSheet(),navigate('#contribute/tasks/suggest'))" class="btn btn-outline" style="width:100%;padding:12px;font-size:14px;font-weight:600;border-radius:9px;display:flex;align-items:center;justify-content:space-between;border:1.5px solid #c7d2fe;color:#4338ca">
|
|
7549
|
+
<span>💡 ${t('提建议(无需登录)')}</span><span style="font-size:13px;opacity:0.6">›</span>
|
|
7550
|
+
</button>
|
|
7551
|
+
<a href="#welcome" onclick="closeSheet()" style="text-align:center;font-size:12px;color:#9ca3af;text-decoration:none;padding:6px 4px 2px;line-height:1.5">${t('了解协议设计 · 多种角色 · 元规则')} ›</a>
|
|
7552
|
+
</div>
|
|
7553
|
+
</details>
|
|
7223
7554
|
</div>
|
|
7224
7555
|
`, { maxWidth: 460 })
|
|
7225
7556
|
}
|
|
@@ -7312,10 +7643,7 @@ window.openAuthSheet = (defaultTab) => {
|
|
|
7312
7643
|
</div>
|
|
7313
7644
|
<div class="form-group">
|
|
7314
7645
|
<label class="form-label">${t('邀请码')} <span style="color:#dc2626">*</span></label>
|
|
7315
|
-
<
|
|
7316
|
-
<input class="form-control" id="inp-sponsor" placeholder="${t('陆续开放中,请期待')}" style="font-family:monospace;font-size:13px;flex:1">
|
|
7317
|
-
<button id="btn-fetch-ref" type="button" disabled title="${t('该功能默认关闭,由管理员开启后可用')}" onclick="doFetchInviteCode()" style="white-space:nowrap;padding:0 12px;background:#e5e7eb;color:#9ca3af;border:none;border-radius:6px;font-size:12px;font-weight:600;cursor:not-allowed">${t('获取邀请码')}</button>
|
|
7318
|
-
</div>
|
|
7646
|
+
<input class="form-control" id="inp-sponsor" placeholder="${t('陆续开放中,请期待')}" style="font-family:monospace;font-size:13px;width:100%">
|
|
7319
7647
|
<div style="font-size:11px;color:#6b7280;margin-top:4px" id="sponsor-hint-msg">${t('邀请码为 6-7 位永久码;没有就联系老用户拿邀请链接')}</div>
|
|
7320
7648
|
</div>
|
|
7321
7649
|
<div class="form-group">
|
|
@@ -7390,32 +7718,23 @@ async function renderPromoter(app) {
|
|
|
7390
7718
|
const leftPv = Number(atomic.total_left_pv || 0)
|
|
7391
7719
|
const rightPv = Number(atomic.total_right_pv || 0)
|
|
7392
7720
|
const weak = Math.min(leftPv, rightPv)
|
|
7393
|
-
const tiersArr = atomic.tier_config || []
|
|
7394
|
-
const nextTier = tiersArr.find(x => x.pv_threshold > weak) || tiersArr[tiersArr.length - 1]
|
|
7395
|
-
const nextProgress = nextTier ? Math.min(100, (weak / nextTier.pv_threshold) * 100) : 0
|
|
7396
7721
|
|
|
7397
|
-
// ─── ① 顶部 KPI —
|
|
7398
|
-
const totalEarnings = data.earnings.grand_total
|
|
7399
|
-
const last30 = Number(data.projection?.last_30_commission || 0)
|
|
7722
|
+
// ─── ① 顶部 KPI — 紧张地区改为时间线(合规:不诱导金钱大字,仅按时间显示已发生事实) ───
|
|
7723
|
+
const totalEarnings = data.earnings.grand_total
|
|
7724
|
+
const last30 = Number(data.projection?.last_30_commission || 0)
|
|
7400
7725
|
const growth = data.projection?.growth_rate
|
|
7401
|
-
const pending = atomic.score?.pending_score || 0
|
|
7402
7726
|
const _mlmMax = Number(state.user?.region_max_levels ?? 1)
|
|
7403
7727
|
const _kpiRestricted = _mlmMax <= 1
|
|
7404
|
-
//
|
|
7405
|
-
//
|
|
7406
|
-
|
|
7407
|
-
// commission 层级(max_levels) 与 PV 系统(pv_enabled) 分离:可单独开某辖区到 L2/L3 而 PV 仍关。
|
|
7408
|
-
const _pvAllowed = Number(state.user?.region_pv_enabled ?? 0) === 1
|
|
7728
|
+
// 匹配奖励引擎已切除(#401):只展示中性参与记录(PV),不再有 Score/档位等奖励视图。
|
|
7729
|
+
// _recordingOn:中性参与记录(PV ledger/聚合)是否开启 —— data.gates.participation_recording_active(默认 true)。
|
|
7730
|
+
const _recordingOn = data.gates?.participation_recording_active !== false
|
|
7409
7731
|
// 紧张地区:拼装最近奖励时间线(commission + 匹配 binary 混合,按时间倒序取 5 条)
|
|
7410
7732
|
let kpiBar = ''
|
|
7411
7733
|
if (_kpiRestricted) {
|
|
7412
7734
|
const _cm = (data.recent || []).slice(0, 10).map(r => ({
|
|
7413
7735
|
ts: r.created_at, label: `L${r.level} ${t('佣金')}`, amount: Number(r.amount || 0),
|
|
7414
7736
|
}))
|
|
7415
|
-
const
|
|
7416
|
-
ts: r.created_at, label: `tier ${r.tier} ${t('发展奖')}`, amount: Number(r.waz_amount || 0),
|
|
7417
|
-
}))
|
|
7418
|
-
const _merged = [..._cm, ..._bn].sort((a, b) => (b.ts || '').localeCompare(a.ts || '')).slice(0, 5)
|
|
7737
|
+
const _merged = _cm.sort((a, b) => (b.ts || '').localeCompare(a.ts || '')).slice(0, 5)
|
|
7419
7738
|
kpiBar = `
|
|
7420
7739
|
<div class="card" style="padding:12px 14px;margin-bottom:16px">
|
|
7421
7740
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
|
|
@@ -7447,14 +7766,14 @@ async function renderPromoter(app) {
|
|
|
7447
7766
|
<div style="font-size:10px;color:${growth == null ? '#9ca3af' : (growth >= 0 ? '#16a34a' : '#dc2626')}">${growth == null ? t('新用户期') : (growth >= 0 ? '↑' : '↓') + ' ' + Math.abs(growth*100).toFixed(0) + '%'}</div>
|
|
7448
7767
|
</div>
|
|
7449
7768
|
<div class="card" style="text-align:center;padding:10px 6px">
|
|
7450
|
-
<div style="font-size:11px;color:#6b7280"
|
|
7451
|
-
<div style="font-size:17px;font-weight:700;color:#
|
|
7452
|
-
<div style="font-size:10px;color:#9ca3af">
|
|
7769
|
+
<div style="font-size:11px;color:#6b7280">📒 ${t('参与积分')}</div>
|
|
7770
|
+
<div style="font-size:17px;font-weight:700;color:#4338ca;margin-top:2px">${(leftPv + rightPv).toLocaleString()}</div>
|
|
7771
|
+
<div style="font-size:10px;color:#9ca3af">PV</div>
|
|
7453
7772
|
</div>
|
|
7454
7773
|
<div class="card" style="text-align:center;padding:10px 6px">
|
|
7455
|
-
<div style="font-size:11px;color:#6b7280"
|
|
7456
|
-
<div style="font-size:17px;font-weight:700;color:#
|
|
7457
|
-
<div style="font-size:10px;color:#9ca3af">${
|
|
7774
|
+
<div style="font-size:11px;color:#6b7280">👥 ${t('直推')}</div>
|
|
7775
|
+
<div style="font-size:17px;font-weight:700;color:#0891b2;margin-top:2px">${Number(data.team?.l1 || 0)}</div>
|
|
7776
|
+
<div style="font-size:10px;color:#9ca3af">${t('人')}</div>
|
|
7458
7777
|
</div>
|
|
7459
7778
|
</div>`
|
|
7460
7779
|
}
|
|
@@ -7497,23 +7816,9 @@ async function renderPromoter(app) {
|
|
|
7497
7816
|
? `<span style="color:#16a34a">✅ ${t('已开通分享分润资格')}</span>` + (data.permissions.l1_share_override === 1 ? ` · <span style="color:#7c3aed">${t('Admin 强制授予')}</span>` : '')
|
|
7498
7817
|
: `<span style="color:#d97706">⏳ ${t('分享分润待开通')}</span> · <span>${t('完成首笔购买即可')}</span>`}
|
|
7499
7818
|
${data.my_sponsor ? `<br>${t('邀请人')}: <strong>${escHtml(t(data.my_sponsor.name))}</strong>` : ''}
|
|
7500
|
-
${_pvAllowed && atomic.my_placement ? ` · ${t('挂靠')}: <strong>${escHtml(t(atomic.my_placement.name))}</strong> ${atomic.my_placement.side === 'left' ? '🔵' : '🟢'}` : ''}
|
|
7501
7819
|
· ${t('所在地区')}: ${regionLabel(data.region || 'global')}
|
|
7502
7820
|
</div>
|
|
7503
7821
|
|
|
7504
|
-
${!_pvAllowed ? '' : `
|
|
7505
|
-
<details style="margin-top:4px">
|
|
7506
|
-
<summary style="font-size:11px;color:#6366f1;cursor:pointer;padding:6px 0">⚙ ${t('自动放置设置')}</summary>
|
|
7507
|
-
<div style="padding:8px 0">
|
|
7508
|
-
<div style="font-size:11px;color:#9ca3af;margin-bottom:8px">${t('新人通过你的推荐码注册后,系统自动安排积分树位置(无需选择左右)。')}</div>
|
|
7509
|
-
<div style="font-size:11px;color:#6b7280;margin-bottom:4px">${t('自动放置依据')}</div>
|
|
7510
|
-
<select id="placement-pref-select" class="form-control" style="font-size:12px" onchange="doSetPlacementPref()">
|
|
7511
|
-
<option value="team_count">${t('推荐少的一边(默认)')}</option>
|
|
7512
|
-
<option value="pv_count">${t('近 90 天积分少的一边')}</option>
|
|
7513
|
-
</select>
|
|
7514
|
-
<div id="placement-pref-msg" style="font-size:11px;color:#6b7280;margin-top:4px"></div>
|
|
7515
|
-
</div>
|
|
7516
|
-
</details>`}
|
|
7517
7822
|
<div style="margin-top:12px;display:flex;justify-content:flex-end">
|
|
7518
7823
|
<button class="btn btn-outline btn-sm" onclick="closeModal()">${t('关闭')}</button>
|
|
7519
7824
|
</div>`
|
|
@@ -7719,47 +8024,24 @@ async function renderPromoter(app) {
|
|
|
7719
8024
|
</div>
|
|
7720
8025
|
</details>`
|
|
7721
8026
|
|
|
7722
|
-
// ─── ⑥
|
|
7723
|
-
// 合规:max_levels ≤ 1 的地区(GCC/越南/印尼/菲律宾 + 未审计地区)
|
|
7724
|
-
// 只显示"累计推广 N 人"客观数字,全部金钱/tier/匹配话术隐藏
|
|
7725
|
-
// 复用顶部 _kpiRestricted(同一含义,避免重复定义)
|
|
8027
|
+
// ─── ⑥ 参与记录(PV)───
|
|
7726
8028
|
const _totalRecruits = Number(data.team?.l1 || 0) + Number(data.team?.l2 || 0) + Number(data.team?.l3 || 0)
|
|
7727
|
-
// 最右侧地区显示(所有模式都加)
|
|
7728
8029
|
const _userRegion = state.user?.region || 'global'
|
|
7729
8030
|
const _regionChip = `<span style="font-size:11px;color:#6b7280;white-space:nowrap;font-weight:400">${regionLabel(_userRegion)}</span>`
|
|
7730
|
-
// PV
|
|
7731
|
-
|
|
7732
|
-
const atomicSection = atomic.left_invite_url
|
|
7733
|
-
? (!_pvAllowed
|
|
8031
|
+
// 匹配奖励引擎已切除(#401):只展示中性参与记录(PV = 参与/贡献的记录,非收益、不可兑付、无权益)。
|
|
8032
|
+
const atomicSection = (_recordingOn
|
|
7734
8033
|
? `<div style="margin-bottom:12px;background:#fff;border:1px solid #e5e7eb;border-radius:8px;padding:14px">
|
|
7735
8034
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;gap:8px">
|
|
7736
|
-
<span style="font-size:14px;font-weight:600"
|
|
7737
|
-
|
|
7738
|
-
<span style="font-size:11px;color:#9ca3af">${t('协议级荣誉')}</span>
|
|
7739
|
-
${_regionChip}
|
|
7740
|
-
</div>
|
|
8035
|
+
<span style="font-size:14px;font-weight:600">📒 ${t('参与记录')}</span>
|
|
8036
|
+
${_regionChip}
|
|
7741
8037
|
</div>
|
|
7742
8038
|
<div style="display:flex;align-items:baseline;gap:8px;margin-top:6px">
|
|
7743
|
-
<span style="font-size:24px;font-weight:800;color:#
|
|
7744
|
-
<span style="font-size:12px;color:#6b7280">${t('
|
|
8039
|
+
<span style="font-size:24px;font-weight:800;color:#4338ca">${(leftPv + rightPv).toLocaleString()}</span>
|
|
8040
|
+
<span style="font-size:12px;color:#6b7280">${t('累计参与积分(PV)')}</span>
|
|
7745
8041
|
</div>
|
|
7746
|
-
<div style="font-size:11px;color:#9ca3af;margin-top:6px;line-height:1.5">${t('
|
|
8042
|
+
<div style="font-size:11px;color:#9ca3af;margin-top:6px;line-height:1.5">${t('PV 是参与 / 贡献的记录,不是收益、不可兑付、不构成任何奖励权益。匹配奖励当前未启用。')}</div>
|
|
7747
8043
|
</div>`
|
|
7748
|
-
:
|
|
7749
|
-
<summary style="padding:12px;font-size:14px;font-weight:600;cursor:pointer;list-style:none;display:flex;justify-content:space-between;align-items:center;gap:8px">
|
|
7750
|
-
<span>
|
|
7751
|
-
🌟 ${t('WebAZ 发展奖')}
|
|
7752
|
-
<span style="font-weight:400;font-size:12px;color:#6b7280;margin-left:6px">
|
|
7753
|
-
${t('弱侧')} ${weak.toLocaleString()} PV · ${t('累计')} ${(atomic.score?.settled_waz || 0).toFixed(2)} WAZ
|
|
7754
|
-
</span>
|
|
7755
|
-
</span>
|
|
7756
|
-
${_regionChip}
|
|
7757
|
-
</summary>
|
|
7758
|
-
<div style="padding:0 12px 12px">
|
|
7759
|
-
${renderAtomicInner(atomic, leftPv, rightPv, weak, nextTier, nextProgress)}
|
|
7760
|
-
</div>
|
|
7761
|
-
</details>`)
|
|
7762
|
-
: ''
|
|
8044
|
+
: '')
|
|
7763
8045
|
|
|
7764
8046
|
// ─── ⑦ 实时流水(默认折叠)───
|
|
7765
8047
|
const bulldozerEvents = (data.recent || []).map(r => ({
|
|
@@ -7767,12 +8049,7 @@ async function renderPromoter(app) {
|
|
|
7767
8049
|
label: `🚜 L${r.level} ${t('佣金')} · ${escHtml(r.source_buyer_name || '—')}`,
|
|
7768
8050
|
rate: r.rate,
|
|
7769
8051
|
}))
|
|
7770
|
-
const
|
|
7771
|
-
kind: 'binary', ts: r.created_at, level: 0, amount: Number(r.waz_amount || 0),
|
|
7772
|
-
label: `⚛ tier ${r.tier} · Score ${r.score}${r.settled_at ? '' : ' (' + t('待结') + ')'}`,
|
|
7773
|
-
settled: !!r.settled_at,
|
|
7774
|
-
}))
|
|
7775
|
-
const merged = [...bulldozerEvents, ...atomicEvents]
|
|
8052
|
+
const merged = [...bulldozerEvents]
|
|
7776
8053
|
.sort((a, b) => (b.ts || '').localeCompare(a.ts || ''))
|
|
7777
8054
|
.slice(0, 30)
|
|
7778
8055
|
const cutoff = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
|
|
@@ -7936,87 +8213,6 @@ window.setBoughtKw = (kw) => {
|
|
|
7936
8213
|
}, 200)
|
|
7937
8214
|
}
|
|
7938
8215
|
|
|
7939
|
-
// 积分匹配内部内容(折叠展开后渲染,从 renderAtomicSection 简化抽出)
|
|
7940
|
-
function renderAtomicInner(a, leftPv, rightPv, weak, nextTier, nextProgress) {
|
|
7941
|
-
const tiers = a.tier_config || []
|
|
7942
|
-
const tierTable = tiers.map(x => `
|
|
7943
|
-
<tr style="border-bottom:1px solid #f3f4f6">
|
|
7944
|
-
<td style="padding:5px 8px;font-size:12px">tier ${x.tier}</td>
|
|
7945
|
-
<td style="padding:5px 8px;font-size:12px;text-align:right">${Number(x.pv_threshold).toLocaleString()}</td>
|
|
7946
|
-
<td style="padding:5px 8px;font-size:12px;text-align:right;font-weight:600">${x.score_per_hit}</td>
|
|
7947
|
-
</tr>`).join('')
|
|
7948
|
-
|
|
7949
|
-
// 本月匹配次数 + WAZ
|
|
7950
|
-
const now = new Date()
|
|
7951
|
-
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1).toISOString().slice(0, 10)
|
|
7952
|
-
const monthBinary = (a.recent_binary || []).filter(r => (r.created_at || '').slice(0, 10) >= monthStart)
|
|
7953
|
-
const monthHits = monthBinary.length
|
|
7954
|
-
const monthWaz = monthBinary.reduce((s, r) => s + Number(r.waz_amount || 0), 0)
|
|
7955
|
-
|
|
7956
|
-
// 总 hits / 总下线(直挂左右子节点)
|
|
7957
|
-
const score = a.score || {}
|
|
7958
|
-
const totalHits = score.total_hits || 0
|
|
7959
|
-
|
|
7960
|
-
const recentRows = (a.recent_binary || []).length
|
|
7961
|
-
? a.recent_binary.map(r => `
|
|
7962
|
-
<div style="display:flex;justify-content:space-between;padding:6px 0;border-bottom:1px solid #f3f4f6;font-size:12px">
|
|
7963
|
-
<div>tier ${r.tier} · Score <strong>${r.score}</strong> · ${r.settled_at ? `<span style="color:#16a34a">${t('已结')} ${Number(r.waz_amount).toFixed(2)}</span>` : `<span style="color:#d97706">${t('待分配')}</span>`}</div>
|
|
7964
|
-
<div style="color:#9ca3af">${fmtTime(r.created_at)}</div>
|
|
7965
|
-
</div>`).join('')
|
|
7966
|
-
: `<div style="text-align:center;color:#9ca3af;padding:10px;font-size:12px">${t('暂无匹配记录')}</div>`
|
|
7967
|
-
|
|
7968
|
-
return `
|
|
7969
|
-
<!-- 我的位置 -->
|
|
7970
|
-
${a.my_placement ? `
|
|
7971
|
-
<div style="background:#f5f3ff;border:1px solid #e9d5ff;border-radius:6px;padding:8px 10px;font-size:12px;color:#6b21a8;margin-bottom:10px">
|
|
7972
|
-
📍 ${t('我挂位置')}: <strong>${escHtml(a.my_placement.name)}</strong> 的 ${a.my_placement.side === 'left' ? '🔵 左侧' : '🟢 右侧'}
|
|
7973
|
-
</div>` : `
|
|
7974
|
-
<div style="background:#f9fafb;border:1px solid #e5e7eb;border-radius:6px;padding:8px 10px;font-size:12px;color:#9ca3af;margin-bottom:10px">
|
|
7975
|
-
${t('你还没加入任何上级的积分树(独立根节点)')}
|
|
7976
|
-
</div>`}
|
|
7977
|
-
|
|
7978
|
-
<!-- 左/右 PV -->
|
|
7979
|
-
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px">
|
|
7980
|
-
<div style="text-align:center;background:#eff6ff;border-radius:6px;padding:10px">
|
|
7981
|
-
<div style="font-size:11px;color:#6b7280">🔵 ${t('左区 PV')}</div>
|
|
7982
|
-
<div style="font-size:18px;font-weight:700;color:#1e40af">${leftPv.toLocaleString()}</div>
|
|
7983
|
-
<div style="font-size:10px;color:#9ca3af">${t('直挂')}: ${a.left_child ? escHtml(a.left_child.name) : '—'}</div>
|
|
7984
|
-
</div>
|
|
7985
|
-
<div style="text-align:center;background:#f0fdf4;border-radius:6px;padding:10px">
|
|
7986
|
-
<div style="font-size:11px;color:#6b7280">🟢 ${t('右区 PV')}</div>
|
|
7987
|
-
<div style="font-size:18px;font-weight:700;color:#15803d">${rightPv.toLocaleString()}</div>
|
|
7988
|
-
<div style="font-size:10px;color:#9ca3af">${t('直挂')}: ${a.right_child ? escHtml(a.right_child.name) : '—'}</div>
|
|
7989
|
-
</div>
|
|
7990
|
-
</div>
|
|
7991
|
-
|
|
7992
|
-
<!-- 本月匹配统计 + 总匹配 -->
|
|
7993
|
-
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:6px;margin-bottom:10px">
|
|
7994
|
-
<div style="text-align:center;background:#fef3c7;border-radius:6px;padding:8px 4px">
|
|
7995
|
-
<div style="font-size:10px;color:#92400e">${t('本月匹配次数')}</div>
|
|
7996
|
-
<div style="font-size:14px;font-weight:700;color:#78350f">${monthHits}</div>
|
|
7997
|
-
</div>
|
|
7998
|
-
<div style="text-align:center;background:#dcfce7;border-radius:6px;padding:8px 4px">
|
|
7999
|
-
<div style="font-size:10px;color:#166534">${t('本月匹配 WAZ')}</div>
|
|
8000
|
-
<div style="font-size:14px;font-weight:700;color:#14532d">${monthWaz.toFixed(2)}</div>
|
|
8001
|
-
</div>
|
|
8002
|
-
<div style="text-align:center;background:#eef2ff;border-radius:6px;padding:8px 4px">
|
|
8003
|
-
<div style="font-size:10px;color:#4338ca">${t('累计匹配次数')}</div>
|
|
8004
|
-
<div style="font-size:14px;font-weight:700;color:#3730a3">${totalHits}</div>
|
|
8005
|
-
</div>
|
|
8006
|
-
</div>
|
|
8007
|
-
|
|
8008
|
-
${renderBinaryTree(a.binary_tree)}
|
|
8009
|
-
|
|
8010
|
-
<!-- pre-public de-MLM: 弱腿/对碰 tier 进度 + Score/次 档位表已下线;PV/位置仅为参与记录,非收益路径 -->
|
|
8011
|
-
<div style="background:#f9fafb;border-radius:6px;padding:8px 10px;font-size:11px;color:#9ca3af;line-height:1.6;margin-bottom:10px">
|
|
8012
|
-
${window._lang === 'en'
|
|
8013
|
-
? 'PV tiers / pairing are pre-launch and currently disabled. PV / placement is a participation record only — not an earning path or payout promise.'
|
|
8014
|
-
: 'PV 档位 / 对碰为 pre-launch 阶段、当前未启用。PV / 位置仅为参与记录,非收益路径或兑付承诺。'}
|
|
8015
|
-
</div>
|
|
8016
|
-
<h4 style="font-size:12px;font-weight:600;margin:6px 0">📊 ${t('最近匹配')}</h4>
|
|
8017
|
-
${recentRows}`
|
|
8018
|
-
}
|
|
8019
|
-
|
|
8020
8216
|
// ─── 🎯 成长任务 UI(替换 insights,主线:分享达人养成)───
|
|
8021
8217
|
function renderGrowthTasksSection(tasks, summary) {
|
|
8022
8218
|
if (!tasks || tasks.length === 0) return ''
|
|
@@ -8151,200 +8347,6 @@ window.growthTaskAction = (action, id) => {
|
|
|
8151
8347
|
}
|
|
8152
8348
|
}
|
|
8153
8349
|
|
|
8154
|
-
function renderAtomicSection(a) {
|
|
8155
|
-
if (!a || !a.left_invite_url) return ''
|
|
8156
|
-
const leftPv = Number(a.total_left_pv || 0)
|
|
8157
|
-
const rightPv = Number(a.total_right_pv || 0)
|
|
8158
|
-
const pair = Math.min(leftPv, rightPv)
|
|
8159
|
-
const tiers = a.tier_config || []
|
|
8160
|
-
// 下一档进度
|
|
8161
|
-
const nextTier = tiers.find(t => t.pv_threshold > pair) || tiers[tiers.length - 1]
|
|
8162
|
-
const nextProgress = nextTier ? Math.min(100, (pair / nextTier.pv_threshold) * 100) : 0
|
|
8163
|
-
|
|
8164
|
-
const tierTable = tiers.map(t => `
|
|
8165
|
-
<tr style="border-bottom:1px solid #f3f4f6">
|
|
8166
|
-
<td style="padding:6px 8px;font-size:12px">tier ${t.tier}</td>
|
|
8167
|
-
<td style="padding:6px 8px;font-size:12px;text-align:right">${Number(t.pv_threshold).toLocaleString()}</td>
|
|
8168
|
-
<td style="padding:6px 8px;font-size:12px;text-align:right;font-weight:600">${t.score_per_hit}</td>
|
|
8169
|
-
</tr>`).join('')
|
|
8170
|
-
|
|
8171
|
-
const recentRows = (a.recent_binary || []).length
|
|
8172
|
-
? a.recent_binary.map(r => `
|
|
8173
|
-
<div style="display:flex;justify-content:space-between;padding:6px 12px;border-bottom:1px solid #f3f4f6;font-size:12px">
|
|
8174
|
-
<div>tier ${r.tier} · Score <strong>${r.score}</strong> · ${r.settled_at ? `<span style="color:#16a34a">已结 ${Number(r.waz_amount).toFixed(2)} WAZ</span>` : `<span style="color:#d97706">待分配</span>`}</div>
|
|
8175
|
-
<div style="color:#9ca3af">${fmtTime(r.created_at)}</div>
|
|
8176
|
-
</div>`).join('')
|
|
8177
|
-
: `<div style="text-align:center;color:#9ca3af;padding:16px;font-size:12px">${t('暂无匹配记录')}</div>`
|
|
8178
|
-
|
|
8179
|
-
return `
|
|
8180
|
-
<h2 style="font-size:15px;font-weight:600;margin:24px 0 8px">⚛ ${t('积分 — 积分匹配')}</h2>
|
|
8181
|
-
|
|
8182
|
-
<div class="card" style="margin-bottom:12px;background:linear-gradient(135deg,#dbeafe,#f0fdf4)">
|
|
8183
|
-
<div style="font-size:13px;color:#6b7280;margin-bottom:6px">🔗 ${t('我的推荐码')}</div>
|
|
8184
|
-
<div style="font-size:11px;color:#6b7280;margin-bottom:10px">${t('复制你的推荐链接分享给新人;新人注册后由系统自动安排积分树位置(无需选择左右)。')}</div>
|
|
8185
|
-
${(() => {
|
|
8186
|
-
// pre-public 去左右码:只用唯一的推荐码(/i/CODE,不带 -L/-R);缺失则提示不可用
|
|
8187
|
-
const myCode = state.user?.permanent_code || null
|
|
8188
|
-
const origin = location.origin
|
|
8189
|
-
if (!myCode) return `<div style="background:#fef2f2;border:1px solid #fca5a5;border-radius:10px;padding:10px;font-size:12px;color:#991b1b">⚠️ ${t('邀请码暂不可用,请刷新或联系支持')}</div>`
|
|
8190
|
-
const link = `${origin}/i/${myCode}`
|
|
8191
|
-
const esc = (s) => s.replace(/'/g, "\\'")
|
|
8192
|
-
return `
|
|
8193
|
-
<div onclick="copyRefLink('${esc(link)}')" style="cursor:pointer;background:#eef2ff;border:2px solid #6366f1;border-radius:10px;padding:10px;transition:all 0.15s" onmouseover="this.style.background='#e0e7ff'" onmouseout="this.style.background='#eef2ff'">
|
|
8194
|
-
<div style="font-size:13px;font-weight:700;color:#4338ca;margin-bottom:6px">${t('推荐链接')}</div>
|
|
8195
|
-
<div style="font-size:10px;color:#4338ca;word-break:break-all;line-height:1.4;background:#fff;padding:6px 8px;border-radius:6px;font-family:monospace">${escHtml(link)}</div>
|
|
8196
|
-
<div style="font-size:11px;color:#6366f1;margin-top:6px;text-align:center">${t('点击复制 →')}</div>
|
|
8197
|
-
</div>`
|
|
8198
|
-
})()}
|
|
8199
|
-
<details style="margin-top:10px">
|
|
8200
|
-
<summary style="font-size:11px;color:#6366f1;cursor:pointer">⚙ ${t('自动放置依据')}</summary>
|
|
8201
|
-
<div style="padding:8px 0">
|
|
8202
|
-
<select id="placement-pref-select" class="form-control" style="font-size:12px" onchange="doSetPlacementPref()">
|
|
8203
|
-
<option value="team_count">${t('推荐少的一边(默认)')}</option>
|
|
8204
|
-
<option value="pv_count">${t('近 90 天积分少的一边')}</option>
|
|
8205
|
-
</select>
|
|
8206
|
-
<div id="placement-pref-msg" style="font-size:11px;color:#6b7280;margin-top:4px"></div>
|
|
8207
|
-
</div>
|
|
8208
|
-
</details>
|
|
8209
|
-
${a.my_placement ? `<div style="font-size:11px;color:#6b7280;margin-top:8px">${t('我挂靠位置')}: <strong>${escHtml(a.my_placement.name)}</strong> 的 ${a.my_placement.side === 'left' ? '🔵 左区' : '🟢 右区'}</div>` : ''}
|
|
8210
|
-
</div>
|
|
8211
|
-
|
|
8212
|
-
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:12px">
|
|
8213
|
-
<div class="card" style="text-align:center;background:#eff6ff">
|
|
8214
|
-
<div style="font-size:11px;color:#6b7280">🔵 ${t('左区 PV')}</div>
|
|
8215
|
-
<div style="font-size:20px;font-weight:700;color:#1e40af">${leftPv.toLocaleString()}</div>
|
|
8216
|
-
<div style="font-size:10px;color:#9ca3af">${t('直挂')}: ${a.left_child ? escHtml(a.left_child.name) : '—'}</div>
|
|
8217
|
-
</div>
|
|
8218
|
-
<div class="card" style="text-align:center;background:#f0fdf4">
|
|
8219
|
-
<div style="font-size:11px;color:#6b7280">🟢 ${t('右区 PV')}</div>
|
|
8220
|
-
<div style="font-size:20px;font-weight:700;color:#15803d">${rightPv.toLocaleString()}</div>
|
|
8221
|
-
<div style="font-size:10px;color:#9ca3af">${t('直挂')}: ${a.right_child ? escHtml(a.right_child.name) : '—'}</div>
|
|
8222
|
-
</div>
|
|
8223
|
-
</div>
|
|
8224
|
-
|
|
8225
|
-
${renderBinaryTree(a.binary_tree)}
|
|
8226
|
-
|
|
8227
|
-
<!-- pre-public de-MLM: 弱腿/对碰 tier 进度已下线;PV/位置仅为参与记录,非收益路径 -->
|
|
8228
|
-
<div class="card" style="margin-bottom:12px">
|
|
8229
|
-
<div style="font-size:11px;color:#9ca3af;line-height:1.6">
|
|
8230
|
-
${window._lang === 'en'
|
|
8231
|
-
? 'PV tiers / pairing are pre-launch and currently disabled. PV / placement is a participation record only — not an earning path or payout promise.'
|
|
8232
|
-
: 'PV 档位 / 对碰为 pre-launch 阶段、当前未启用。PV / 位置仅为参与记录,非收益路径或兑付承诺。'}
|
|
8233
|
-
</div>
|
|
8234
|
-
</div>
|
|
8235
|
-
|
|
8236
|
-
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:12px">
|
|
8237
|
-
<div class="card" style="text-align:center">
|
|
8238
|
-
<div style="font-size:11px;color:#6b7280">${t('待结算 Score')}</div>
|
|
8239
|
-
<div style="font-size:18px;font-weight:700;color:#d97706">${(a.score?.pending_score || 0).toFixed(0)}</div>
|
|
8240
|
-
</div>
|
|
8241
|
-
<div class="card" style="text-align:center">
|
|
8242
|
-
<div style="font-size:11px;color:#6b7280">${t('累计获 WAZ')}</div>
|
|
8243
|
-
<div style="font-size:18px;font-weight:700;color:#059669">${(a.score?.settled_waz || 0).toFixed(2)}</div>
|
|
8244
|
-
</div>
|
|
8245
|
-
</div>
|
|
8246
|
-
|
|
8247
|
-
<h3 style="font-size:13px;font-weight:600;margin:8px 0">📊 ${t('最近匹配')}</h3>
|
|
8248
|
-
<div class="card" style="padding:0">
|
|
8249
|
-
${recentRows}
|
|
8250
|
-
</div>`
|
|
8251
|
-
}
|
|
8252
|
-
|
|
8253
|
-
// P12: 三层积分树(你 + 左右 + 各自左右)
|
|
8254
|
-
function renderBinaryTree(tree) {
|
|
8255
|
-
if (!tree || !tree.me) return ''
|
|
8256
|
-
const node = (n, bg, fg, label) => n
|
|
8257
|
-
? `<div style="padding:6px 4px;background:${bg};border-radius:6px;font-size:10px;line-height:1.3;color:${fg};text-align:center;min-height:42px;display:flex;flex-direction:column;justify-content:center;cursor:pointer" onclick="showNodePvModal('${n.id}')" title="${t('点击查看 PV KPI')}">
|
|
8258
|
-
<div style="font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${escHtml(n.name || '—')}</div>
|
|
8259
|
-
<div style="font-size:9px;opacity:0.8">L${Math.round(n.lpv)}/R${Math.round(n.rpv)}</div>
|
|
8260
|
-
</div>`
|
|
8261
|
-
: `<div style="padding:6px 4px;background:#f9fafb;border-radius:6px;font-size:10px;color:#d1d5db;text-align:center;min-height:42px;display:flex;flex-direction:column;justify-content:center">${label || '—'}</div>`
|
|
8262
|
-
|
|
8263
|
-
return `
|
|
8264
|
-
<details style="margin-bottom:12px" open>
|
|
8265
|
-
<summary style="font-size:12px;color:#6366f1;cursor:pointer;margin-bottom:6px">🌳 ${t('团队组织图(3 层)')} <span style="color:#9ca3af;font-size:10px">${t('点击节点查看 PV')}</span></summary>
|
|
8266
|
-
<div style="padding:8px;background:#fff;border:1px solid #e5e7eb;border-radius:8px">
|
|
8267
|
-
<!-- L0: 你 -->
|
|
8268
|
-
<div style="margin-bottom:6px">
|
|
8269
|
-
${node(tree.me, '#e0e7ff', '#3730a3', '')}
|
|
8270
|
-
</div>
|
|
8271
|
-
<div style="text-align:center;color:#d1d5db;font-size:12px;margin:-2px 0">┌────┴────┐</div>
|
|
8272
|
-
<!-- L1: 左 / 右 -->
|
|
8273
|
-
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:4px">
|
|
8274
|
-
${node(tree.left, '#dbeafe', '#1e40af', t('空左'))}
|
|
8275
|
-
${node(tree.right, '#dcfce7', '#15803d', t('空右'))}
|
|
8276
|
-
</div>
|
|
8277
|
-
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;color:#d1d5db;font-size:11px;text-align:center;margin:-2px 0">
|
|
8278
|
-
<span>┌──┴──┐</span><span>┌──┴──┐</span>
|
|
8279
|
-
</div>
|
|
8280
|
-
<!-- L2 -->
|
|
8281
|
-
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:4px">
|
|
8282
|
-
${node(tree.ll, '#eff6ff', '#1e40af', '·')}
|
|
8283
|
-
${node(tree.lr, '#eff6ff', '#1e40af', '·')}
|
|
8284
|
-
${node(tree.rl, '#f0fdf4', '#15803d', '·')}
|
|
8285
|
-
${node(tree.rr, '#f0fdf4', '#15803d', '·')}
|
|
8286
|
-
</div>
|
|
8287
|
-
<div style="font-size:10px;color:#9ca3af;margin-top:6px;text-align:center">${t('每节点显示 L=左累计 PV / R=右累计 PV · 点击节点看详情')}</div>
|
|
8288
|
-
</div>
|
|
8289
|
-
</details>`
|
|
8290
|
-
}
|
|
8291
|
-
|
|
8292
|
-
// 节点 PV KPI modal — 点组织图任一节点弹出
|
|
8293
|
-
window.showNodePvModal = async (userId) => {
|
|
8294
|
-
if (!userId) return
|
|
8295
|
-
_openModal(`
|
|
8296
|
-
<h2 style="font-size:16px;font-weight:600;margin-bottom:12px">📊 ${t('节点 PV 详情')}</h2>
|
|
8297
|
-
<div id="pv-modal-body" style="text-align:center;padding:20px;color:#9ca3af;font-size:13px">${t('加载中...')}</div>
|
|
8298
|
-
<div style="display:flex;gap:8px;margin-top:8px">
|
|
8299
|
-
<button class="btn btn-outline" style="flex:1" onclick="closeModal()">${t('关闭')}</button>
|
|
8300
|
-
</div>
|
|
8301
|
-
`)
|
|
8302
|
-
const data = await GET(`/users/${userId}/pv-summary`)
|
|
8303
|
-
const body = document.getElementById('pv-modal-body')
|
|
8304
|
-
if (!body) return
|
|
8305
|
-
if (data?.error) {
|
|
8306
|
-
body.innerHTML = alert$('error', data.error)
|
|
8307
|
-
return
|
|
8308
|
-
}
|
|
8309
|
-
// pre-public de-MLM: 不再展示弱腿/对碰收益(weak-leg / pairing earnings)。PV / 位置仅为参与记录。
|
|
8310
|
-
body.innerHTML = `
|
|
8311
|
-
<div style="text-align:left">
|
|
8312
|
-
<div style="display:flex;align-items:center;gap:10px;margin-bottom:14px;padding-bottom:10px;border-bottom:1px solid #f3f4f6">
|
|
8313
|
-
<div style="width:42px;height:42px;border-radius:50%;background:#eef2ff;display:flex;align-items:center;justify-content:center;font-size:20px">👤</div>
|
|
8314
|
-
<div style="flex:1;min-width:0">
|
|
8315
|
-
<div style="font-size:15px;font-weight:600">${escHtml(data.name)}</div>
|
|
8316
|
-
<div style="font-size:11px;color:#9ca3af">${data.permanent_code || data.id} ${data.handle ? '· @' + data.handle : ''}</div>
|
|
8317
|
-
</div>
|
|
8318
|
-
</div>
|
|
8319
|
-
${data.placement ? `
|
|
8320
|
-
<div style="background:#f5f3ff;border-radius:6px;padding:8px 10px;font-size:12px;color:#6b21a8;margin-bottom:10px">
|
|
8321
|
-
📍 ${t('挂位置')}: ${escHtml(data.placement.name || data.placement.id)} ${data.placement.side === 'left' ? '🔵 左侧' : '🟢 右侧'} · ${t('深度')} ${data.placement.depth}
|
|
8322
|
-
</div>` : `<div style="font-size:11px;color:#9ca3af;margin-bottom:10px">${t('独立根节点(无上级)')}</div>`}
|
|
8323
|
-
|
|
8324
|
-
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:10px">
|
|
8325
|
-
<div style="text-align:center;background:#eff6ff;border-radius:6px;padding:8px">
|
|
8326
|
-
<div style="font-size:10px;color:#6b7280">🔵 ${t('左区 PV')}</div>
|
|
8327
|
-
<div style="font-size:16px;font-weight:700;color:#1e40af">${Number(data.total_left_pv).toLocaleString()}</div>
|
|
8328
|
-
<div style="font-size:9px;color:#9ca3af">${data.left_child ? escHtml(data.left_child.name) : t('空')}</div>
|
|
8329
|
-
</div>
|
|
8330
|
-
<div style="text-align:center;background:#f0fdf4;border-radius:6px;padding:8px">
|
|
8331
|
-
<div style="font-size:10px;color:#6b7280">🟢 ${t('右区 PV')}</div>
|
|
8332
|
-
<div style="font-size:16px;font-weight:700;color:#15803d">${Number(data.total_right_pv).toLocaleString()}</div>
|
|
8333
|
-
<div style="font-size:9px;color:#9ca3af">${data.right_child ? escHtml(data.right_child.name) : t('空')}</div>
|
|
8334
|
-
</div>
|
|
8335
|
-
</div>
|
|
8336
|
-
|
|
8337
|
-
<div style="background:#f9fafb;border-radius:6px;padding:8px 10px;font-size:11px;color:#9ca3af;line-height:1.6;margin-bottom:10px">
|
|
8338
|
-
${window._lang === 'en'
|
|
8339
|
-
? 'PV pairing is pre-launch and currently disabled. PV / placement is a participation record only — not an earning path or payout promise.'
|
|
8340
|
-
: 'PV 对碰为 pre-launch 阶段、当前未启用。PV / 位置仅为参与记录,非收益路径或兑付承诺。'}
|
|
8341
|
-
</div>
|
|
8342
|
-
|
|
8343
|
-
<a href="#u/${data.id}" onclick="closeModal()" style="display:block;text-align:center;font-size:12px;color:#4f46e5;text-decoration:none;padding:8px">→ ${t('查看 TA 的主页')}</a>
|
|
8344
|
-
</div>
|
|
8345
|
-
`
|
|
8346
|
-
}
|
|
8347
|
-
|
|
8348
8350
|
// 健壮复制:先 clipboard API,失败回退 execCommand(不依赖 focus/activation)
|
|
8349
8351
|
// 任何 async 后调用都安全。window.copyText 暴露给 inline onclick handlers
|
|
8350
8352
|
async function copyText(text) {
|
|
@@ -10433,6 +10435,8 @@ async function renderMyContributions(app) {
|
|
|
10433
10435
|
const gid = await GET('/contribution-identity/github/me').catch(() => null)
|
|
10434
10436
|
// F10 — 自动发现可认领的 GitHub 贡献(只读;失败优雅降级)
|
|
10435
10437
|
const claimable = await GET('/contribution-identity/github/claimable').catch(() => null)
|
|
10438
|
+
// Contribution read-out V1 — 已归属到本账号的贡献事实(GitHub + 管理协调;只读;失败优雅降级)
|
|
10439
|
+
const cf = await GET('/contribution-facts/me').catch(() => null)
|
|
10436
10440
|
const lang = window._lang === 'zh' ? 'zh' : 'en'
|
|
10437
10441
|
const tier = p.tier || {}
|
|
10438
10442
|
const k = p.kpi || {}
|
|
@@ -10470,6 +10474,7 @@ async function renderMyContributions(app) {
|
|
|
10470
10474
|
${provRows ? `<div class="card" style="padding:10px;margin-bottom:12px"><div style="font-size:11px;color:#6b7280;margin-bottom:4px">${t('署名构成(自报)')}</div>${provRows}</div>` : ''}
|
|
10471
10475
|
${!p.passkey_anchor_present ? `<div class="card" style="padding:10px;margin-bottom:12px;border-left:3px solid #d97706"><div style="font-size:12px;color:#92400e">${t('未绑 Passkey:贡献可受理致谢,但需绑定真人锚点才记入建设信誉。')}</div></div>` : ''}
|
|
10472
10476
|
${ghClaimSectionHtml(gid, claimable, lang)}
|
|
10477
|
+
${contributionFactsSectionHtml(cf, lang)}
|
|
10473
10478
|
<div class="card" onclick="location.hash='#contribute/tasks'" style="padding:12px 14px;margin-bottom:12px;cursor:pointer;display:flex;align-items:center;gap:10px">
|
|
10474
10479
|
<div style="font-size:20px;flex-shrink:0">📋</div>
|
|
10475
10480
|
<div style="flex:1;min-width:0">
|
|
@@ -10510,6 +10515,56 @@ function ghClaimErrText(code, fallback) {
|
|
|
10510
10515
|
return m[code] || fallback || t('操作失败')
|
|
10511
10516
|
}
|
|
10512
10517
|
|
|
10518
|
+
// Contribution read-out V1 — 「贡献事实记录」只读区块。展示已归属到本账号的 contribution facts
|
|
10519
|
+
// (GitHub + 管理协调),按来源分组。这是事实与归属记录,不是奖励/付款/兑现权利。不暴露 admin audit detail。
|
|
10520
|
+
function contributionFactsSectionHtml(cf, lang) {
|
|
10521
|
+
const ok = cf && !cf.error
|
|
10522
|
+
const groups = ok ? (cf.groups || {}) : {}
|
|
10523
|
+
const gh = ok ? (groups.github || []) : []
|
|
10524
|
+
const ac = ok ? (groups.admin_coordination || []) : []
|
|
10525
|
+
const total = ok ? (cf.total || 0) : 0
|
|
10526
|
+
const notice = ok && cf.value_boundary ? (lang === 'zh' ? cf.value_boundary.notice_zh : cf.value_boundary.notice_en) : ''
|
|
10527
|
+
if (!ok) {
|
|
10528
|
+
return `<div class="card" style="padding:14px;margin-bottom:12px">
|
|
10529
|
+
<div style="font-size:13px;font-weight:600;margin-bottom:4px">📜 ${_qT('贡献事实记录', 'Contribution evidence')}</div>
|
|
10530
|
+
<div style="font-size:12px;color:#9ca3af">${_qT('暂不可用,稍后再试', 'Unavailable, try again later')}</div>
|
|
10531
|
+
</div>`
|
|
10532
|
+
}
|
|
10533
|
+
const statusLabel = (s) => ({ active: _qT('有效', 'active'), superseded: _qT('被替代', 'superseded'), reverted: _qT('已撤销', 'reverted'), void: _qT('作废', 'void'), forfeited: _qT('已没收', 'forfeited') }[String(s)] || escHtml(String(s)))
|
|
10534
|
+
const factRow = (f) => {
|
|
10535
|
+
const label = lang === 'zh' ? (f.display_source_label || '') : (f.display_source_label_en || f.display_source_label || '')
|
|
10536
|
+
const middle = (f.evidence_ref && f.evidence_ref.source_type) ? f.evidence_ref.source_type : (f.type || f.source || '')
|
|
10537
|
+
const date = String(f.occurred_at || '').slice(0, 10)
|
|
10538
|
+
return `<div style="font-size:11px;color:#374151;padding:6px 8px;background:#f9fafb;border-radius:6px;margin-bottom:4px">
|
|
10539
|
+
<div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap">
|
|
10540
|
+
<span style="background:#eef2ff;color:#4338ca;border-radius:10px;padding:1px 8px;font-size:10px;font-weight:600">${escHtml(label)}</span>
|
|
10541
|
+
<b style="word-break:break-all">${escHtml(String(middle))}</b>
|
|
10542
|
+
${date ? `<span style="color:#6b7280">· ${escHtml(date)}</span>` : ''}
|
|
10543
|
+
<span style="color:#9ca3af">· ${statusLabel(f.status)}</span>
|
|
10544
|
+
</div>
|
|
10545
|
+
<div style="color:#9ca3af;font-size:10px;margin-top:2px">fact <code>${escHtml(String(f.fact_id).slice(0, 18))}…</code></div>
|
|
10546
|
+
</div>`
|
|
10547
|
+
}
|
|
10548
|
+
const group = (titleZh, titleEn, items, emptyZh, emptyEn) => `
|
|
10549
|
+
<div style="margin-top:10px">
|
|
10550
|
+
<div style="font-size:12px;font-weight:600;color:#374151;margin-bottom:4px">${_qT(titleZh, titleEn)} <span style="color:#9ca3af;font-weight:400">(${items.length})</span></div>
|
|
10551
|
+
${items.length ? items.map(factRow).join('') : `<div style="font-size:11px;color:#9ca3af">${_qT(emptyZh, emptyEn)}</div>`}
|
|
10552
|
+
</div>`
|
|
10553
|
+
return `
|
|
10554
|
+
<div class="card" style="padding:14px;margin-bottom:12px">
|
|
10555
|
+
<div style="font-size:13px;font-weight:600;margin-bottom:4px">📜 ${_qT('贡献事实记录', 'Contribution evidence')} <span style="color:#9ca3af;font-weight:400">· ${total}</span></div>
|
|
10556
|
+
<div style="font-size:11px;color:#92400e;background:#fffbeb;border:1px solid #fde68a;border-radius:6px;padding:6px 8px;margin-bottom:8px">⚠️ ${_qT('这里是贡献事实与归属记录,不是奖励、不是付款、不是兑现权利。', 'These are contribution facts and attribution records only — not a payment, and they confer no economic or redemption right.')}</div>
|
|
10557
|
+
${total === 0 ? `<div style="font-size:12px;color:#9ca3af">${_qT('暂无已归属到你的贡献事实。', 'No contribution facts attributed to you yet.')}</div>` : `
|
|
10558
|
+
${group('GitHub', 'GitHub', gh, '暂无 GitHub 贡献事实', 'No GitHub facts')}
|
|
10559
|
+
${group('管理协调', 'Admin coordination', ac, '暂无管理协调贡献事实', 'No admin-coordination facts')}
|
|
10560
|
+
<div style="margin-top:10px">
|
|
10561
|
+
<div style="font-size:12px;font-weight:600;color:#9ca3af;margin-bottom:4px">${_qT('Agent 授权执行', 'Agent-authorized execution')} <span style="font-weight:400">(${_qT('未来', 'future')})</span></div>
|
|
10562
|
+
<div style="font-size:11px;color:#9ca3af">${_qT('暂无', 'None yet')}</div>
|
|
10563
|
+
</div>`}
|
|
10564
|
+
${notice ? `<div style="font-size:10px;color:#9ca3af;background:#f4f4f5;border-radius:6px;padding:6px 8px;margin-top:10px">🔒 ${escHtml(notice)}</div>` : ''}
|
|
10565
|
+
</div>`
|
|
10566
|
+
}
|
|
10567
|
+
|
|
10513
10568
|
function ghClaimSectionHtml(gid, claimable, lang) {
|
|
10514
10569
|
const ok = gid && !gid.error
|
|
10515
10570
|
const bindings = ok ? (gid.bindings || []) : []
|
|
@@ -12480,19 +12535,6 @@ window.switchLoginTab = (tab) => {
|
|
|
12480
12535
|
async function checkRegGate() {
|
|
12481
12536
|
try {
|
|
12482
12537
|
const f = await GET('/system-flags')
|
|
12483
|
-
// 邀请码轮询按钮开关 — 默认禁用,admin 开启后变可用
|
|
12484
|
-
const btn = document.getElementById('btn-fetch-ref')
|
|
12485
|
-
if (btn) {
|
|
12486
|
-
if (f?.invite_rotation_enabled) {
|
|
12487
|
-
btn.disabled = false
|
|
12488
|
-
btn.title = t('点击向系统申领一个邀请码')
|
|
12489
|
-
btn.style.cssText = 'white-space:nowrap;padding:0 12px;background:#4f46e5;color:#fff;border:none;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer'
|
|
12490
|
-
} else {
|
|
12491
|
-
btn.disabled = true
|
|
12492
|
-
btn.title = t('该功能默认关闭,由管理员开启后可用')
|
|
12493
|
-
btn.style.cssText = 'white-space:nowrap;padding:0 12px;background:#e5e7eb;color:#9ca3af;border:none;border-radius:6px;font-size:12px;font-weight:600;cursor:not-allowed'
|
|
12494
|
-
}
|
|
12495
|
-
}
|
|
12496
12538
|
if (!f?.require_ref_to_register) return
|
|
12497
12539
|
const hint = readShareHint()
|
|
12498
12540
|
const el = document.getElementById('reg-gate-hint')
|
|
@@ -12510,14 +12552,6 @@ async function checkRegGate() {
|
|
|
12510
12552
|
} catch {}
|
|
12511
12553
|
}
|
|
12512
12554
|
|
|
12513
|
-
window.doFetchInviteCode = async () => {
|
|
12514
|
-
const r = await POST('/invite/rotate', {})
|
|
12515
|
-
if (r.error) { alert(r.error); return }
|
|
12516
|
-
// 仅自动填入推荐码 — 不展示推荐人信息(轮询是被动分配,非主动分享)
|
|
12517
|
-
const inp = document.getElementById('inp-sponsor')
|
|
12518
|
-
if (inp) inp.value = r.code
|
|
12519
|
-
}
|
|
12520
|
-
|
|
12521
12555
|
window.doLogin = async () => {
|
|
12522
12556
|
const key = document.getElementById('inp-key').value.trim()
|
|
12523
12557
|
if (!key) return showMsg('error', t('请粘贴 api_key'))
|
|
@@ -14548,13 +14582,13 @@ async function renderUserProfile(app, userId) {
|
|
|
14548
14582
|
|
|
14549
14583
|
<div style="margin-bottom:14px">
|
|
14550
14584
|
<div style="font-size:13px;color:#374151;margin-bottom:6px">${t('一句话简介')} <span style="color:#9ca3af;font-size:11px">(${t('≤ 120 字')})</span></div>
|
|
14551
|
-
<input class="form-control" id="bio-inp" placeholder="${t('
|
|
14585
|
+
<input class="form-control" id="bio-inp" placeholder="${t('例如:一句话介绍你自己')}" style="font-size:13px" value="${escHtml(data.bio || '')}" maxlength="120">
|
|
14552
14586
|
</div>
|
|
14553
14587
|
|
|
14554
14588
|
<div style="margin-bottom:14px">
|
|
14555
14589
|
<div style="font-size:13px;color:#374151;margin-bottom:6px">🔍 ${t('流量口令')} <span style="color:#9ca3af;font-size:11px">(${t('≤ 40 字,字母/数字/汉字/-_.')})</span></div>
|
|
14556
14590
|
<div style="display:flex;gap:8px">
|
|
14557
|
-
<input class="form-control" id="anchor-inp" placeholder="${t('
|
|
14591
|
+
<input class="form-control" id="anchor-inp" placeholder="${t('例如:好记的字母或数字组合')}" style="font-size:13px;flex:1" value="${escHtml(data.search_anchor || '')}" maxlength="40">
|
|
14558
14592
|
<button class="btn btn-primary btn-sm" style="white-space:nowrap" onclick="saveSocialProfile()">${t('保存')}</button>
|
|
14559
14593
|
</div>
|
|
14560
14594
|
<p style="font-size:11px;color:#9ca3af;margin-top:4px">${t('在 TikTok / 小红书 口播这个口令,粉丝在 WebAZ 搜它就能找到你')}</p>
|
|
@@ -15237,7 +15271,7 @@ const AI_TOOLS = [
|
|
|
15237
15271
|
},
|
|
15238
15272
|
{
|
|
15239
15273
|
name: 'search_by_anchor',
|
|
15240
|
-
description: '按创作者"流量口令"
|
|
15274
|
+
description: '按创作者"流量口令"查找评测内容(如某创作者的口令)。返回外链评测(YouTube/TikTok/etc)+ 原生 P2P 评测。',
|
|
15241
15275
|
input_schema: {
|
|
15242
15276
|
type: 'object',
|
|
15243
15277
|
properties: { anchor: { type: 'string', description: '创作者的口令字符串' } },
|
|
@@ -15789,7 +15823,7 @@ function renderAIMessages(messages) {
|
|
|
15789
15823
|
${t('试试问:')}<br>
|
|
15790
15824
|
• ${t('"帮我找适合送 60 岁妈妈的礼物 ≤ 500 元"')}<br>
|
|
15791
15825
|
• ${t('"附近最近有人买什么?"')}<br>
|
|
15792
|
-
• ${t('"
|
|
15826
|
+
• ${t('"某个口令的创作者发了啥?"')}
|
|
15793
15827
|
</div>
|
|
15794
15828
|
<a href="#ai-demo" style="display:inline-block;font-size:12px;color:#007aff;text-decoration:none;background:#eff6ff;border:0.5px solid #bfdbfe;padding:6px 14px;border-radius:99px">🎬 ${t('看看预设演示 →')}</a>
|
|
15795
15829
|
</div>`
|
|
@@ -17947,9 +17981,11 @@ async function renderBuyPage(app, productId) {
|
|
|
17947
17981
|
</button>`
|
|
17948
17982
|
// 三柱 chip — 跳对应区域(在同页内 scrollIntoView)
|
|
17949
17983
|
const reviewChip = chip('📝', t('评测'), '#7c3aed', '#f3e8ff', `document.getElementById('reviews-block-${p.id}')?.scrollIntoView({behavior:'smooth',block:'start'})`, reviewsCount > 0 ? reviewsCount : null)
|
|
17984
|
+
// 发起验证只对可发起者显示(登录且非本商品卖家);点击展开商品声明区的现成表单(替代旧的死路由 #claim-task/new)
|
|
17985
|
+
const _canClaim = !!state.user && state.user.id !== p.seller_id
|
|
17950
17986
|
const claimChip = claimsResolved + claimsOpen > 0
|
|
17951
17987
|
? chip('✓', t('已验证'), '#059669', '#d1fae5', `document.getElementById('claims-block-${p.id}')?.scrollIntoView({behavior:'smooth',block:'start'})`, claimsResolved)
|
|
17952
|
-
: chip('+', t('发起验证'), '#6366f1', '#eef2ff', `
|
|
17988
|
+
: (_canClaim ? chip('+', t('发起验证'), '#6366f1', '#eef2ff', `openProductClaimForm('${p.id}')`) : '')
|
|
17953
17989
|
const disputeChip = sellerDisputes > 0
|
|
17954
17990
|
? chip('⚖', t('仲裁'), sellerOpenDisputes > 0 ? '#dc2626' : '#92400e', sellerOpenDisputes > 0 ? '#fee2e2' : '#fef3c7', `navigate('#shop/${p.seller_id}?tab=disputes')`, sellerDisputes)
|
|
17955
17991
|
: ''
|
|
@@ -18370,7 +18406,6 @@ async function loadDisputeCaseDetail(caseId, container) {
|
|
|
18370
18406
|
if (cm.role === 'verifier' || cm.role === 'arbitrator') tags.push('<span style="background:#ede9fe;color:#5b21b6;font-size:9px;padding:1px 6px;border-radius:99px;font-weight:600">👨⚖️ ' + (cm.role === 'arbitrator' ? t('仲裁员') : t('验证员')) + '</span>')
|
|
18371
18407
|
else if (cm.role === 'staff') tags.push('<span style="background:#ede9fe;color:#5b21b6;font-size:9px;padding:1px 6px;border-radius:99px;font-weight:600">👨⚖️ ' + t('平台角色') + '</span>')
|
|
18372
18408
|
if (tags.length === 0) tags.push('<span style="background:#f3f4f6;color:#6b7280;font-size:9px;padding:1px 6px;border-radius:99px">👀 ' + t('围观') + '</span>')
|
|
18373
|
-
if (Number(cm.lifetime_score) >= 1000) tags.push('<span style="background:#fff7ed;color:#9a3412;font-size:9px;padding:1px 6px;border-radius:99px;font-weight:600">⭐ ' + t('资深') + '</span>')
|
|
18374
18409
|
// 身份显示:公开 → @handle 可点跳主页 ; 脱敏 → 匿名用户 灰字不可点
|
|
18375
18410
|
const isAnon = !!cm.anonymous
|
|
18376
18411
|
const nameHtml = isAnon
|
|
@@ -18915,6 +18950,19 @@ window.voteProductClaim = async (claimId, vote) => {
|
|
|
18915
18950
|
setTimeout(() => renderClaimVerify(document.getElementById('app')), 1500)
|
|
18916
18951
|
}
|
|
18917
18952
|
|
|
18953
|
+
// "+发起验证" chip → open the existing product-claim form in the claims block (scroll + expand collapsed
|
|
18954
|
+
// <details> + focus the target select). Replaces the dead #claim-task/new route. If the form isn't present
|
|
18955
|
+
// (not eligible), the scroll is a graceful no-op.
|
|
18956
|
+
window.openProductClaimForm = (productId) => {
|
|
18957
|
+
const block = document.getElementById(`claims-block-${productId}`)
|
|
18958
|
+
if (block) {
|
|
18959
|
+
block.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
18960
|
+
block.querySelectorAll('details').forEach(d => { d.open = true })
|
|
18961
|
+
}
|
|
18962
|
+
const sel = document.getElementById(`pcl-target-${productId}`)
|
|
18963
|
+
if (sel) setTimeout(() => sel.focus(), 350)
|
|
18964
|
+
}
|
|
18965
|
+
|
|
18918
18966
|
window.submitProductClaim = async (productId) => {
|
|
18919
18967
|
const target = document.getElementById(`pcl-target-${productId}`)?.value
|
|
18920
18968
|
const text = document.getElementById(`pcl-text-${productId}`)?.value?.trim()
|
|
@@ -25980,7 +26028,7 @@ async function renderWallet(app) {
|
|
|
25980
26028
|
if (!state.user) { renderLogin(); return }
|
|
25981
26029
|
app.innerHTML = shell(loading$(), 'wallet')
|
|
25982
26030
|
const userRegion = state.user?.region || 'global'
|
|
25983
|
-
const [wallet, income, deposits, withdrawals, whitelistRes, trust,
|
|
26031
|
+
const [wallet, income, deposits, withdrawals, whitelistRes, trust, rateRes, regionPmRes] = await Promise.all([
|
|
25984
26032
|
// 2026-06-01 fix(BUG-PWA-WALLET): 加 catch 防 #wallet 加载死循环
|
|
25985
26033
|
// 原 GET('/wallet') 无 fallback,一旦该 endpoint 错(网络 / 401 / 后端 500),
|
|
25986
26034
|
// 整个 Promise.all reject,renderWallet 早退,"加载中..." 永驻
|
|
@@ -25990,7 +26038,6 @@ async function renderWallet(app) {
|
|
|
25990
26038
|
GET('/wallet/withdrawals').catch(() => []),
|
|
25991
26039
|
GET('/wallet/whitelist').catch(() => ({ whitelist: [] })),
|
|
25992
26040
|
GET('/agents/me/reputation').catch(() => null),
|
|
25993
|
-
GET('/tokenomics/status').catch(() => null),
|
|
25994
26041
|
GET('/wallet/rate').catch(() => null),
|
|
25995
26042
|
GET('/payment-methods/for-region?region=' + encodeURIComponent(userRegion)).catch(() => ({ items: [] })),
|
|
25996
26043
|
])
|
|
@@ -26029,7 +26076,7 @@ async function renderWallet(app) {
|
|
|
26029
26076
|
const earned = Number(wallet.earned || 0)
|
|
26030
26077
|
const totalAssets = balance + escrowed + staked
|
|
26031
26078
|
|
|
26032
|
-
const inc = income || { commissions: { l1:{count:0,total:0}, l2:{count:0,total:0}, l3:{count:0,total:0} },
|
|
26079
|
+
const inc = income || { commissions: { l1:{count:0,total:0}, l2:{count:0,total:0}, l3:{count:0,total:0} }, sales: { count:0, total:0 }, total_income: 0 }
|
|
26033
26080
|
const dep = (deposits || []).slice(0, 5)
|
|
26034
26081
|
const wdr = (withdrawals || []).slice(0, 5)
|
|
26035
26082
|
const pendingWdr = wdr.filter(w => w.status === 'pending')
|
|
@@ -26050,48 +26097,11 @@ async function renderWallet(app) {
|
|
|
26050
26097
|
? `<button class="btn btn-outline btn-sm" style="font-size:10px;padding:2px 8px;color:#dc2626;border-color:#fecaca;margin-left:6px" onclick="cancelWithdrawal('${w.id}')">${t('取消')}</button>`
|
|
26051
26098
|
: ''
|
|
26052
26099
|
|
|
26053
|
-
//
|
|
26054
|
-
const userLevel = state.user?.user_level || { level: 0, name: t('游客'), nextThreshold: 1 }
|
|
26055
|
-
const lifetimeScore = Number(state.user?.lifetime_score || 0)
|
|
26056
|
-
const distCap = Number(tokenomics?.distribution_cap ?? 1.0)
|
|
26057
|
-
const healthLevel = tokenomics?.health_level || 'cold_start'
|
|
26058
|
-
const pausedRecent = tokenomics?.paused_recent
|
|
26059
|
-
// P2 #7:R11 硬停 banner(仅在最近 7 天触发过时显示)
|
|
26060
|
-
const pausedBanner = pausedRecent ? `
|
|
26061
|
-
<div class="card" style="margin-bottom:12px;background:#fef3c7;border-color:#fde68a;padding:10px 14px">
|
|
26062
|
-
<div style="font-size:13px;color:#92400e;font-weight:600;margin-bottom:2px">⚠️ ${t('结算暂停保护中')}</div>
|
|
26063
|
-
<div style="font-size:11px;color:#78350f">${t('基金池水位保护已触发,本期 pending 奖金已保留,将在水位恢复后自动结算。')}</div>
|
|
26064
|
-
</div>` : ''
|
|
26065
|
-
const healthColor = {
|
|
26066
|
-
healthy: { bg: '#dcfce7', fg: '#166534', label: t('健康') },
|
|
26067
|
-
normal: { bg: '#dbeafe', fg: '#1e40af', label: t('正常') },
|
|
26068
|
-
strained: { bg: '#fef3c7', fg: '#92400e', label: t('紧张') },
|
|
26069
|
-
critical: { bg: '#fee2e2', fg: '#991b1b', label: t('告急') },
|
|
26070
|
-
cold_start: { bg: '#f3f4f6', fg: '#6b7280', label: t('启动期') },
|
|
26071
|
-
}[healthLevel] || { bg: '#f3f4f6', fg: '#6b7280', label: '—' }
|
|
26072
|
-
const levelCard = `
|
|
26073
|
-
<div class="card" style="margin-bottom:12px;background:linear-gradient(135deg,#fff7ed 0%,#fef3c7 100%);border-color:#fde68a">
|
|
26074
|
-
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
|
|
26075
|
-
<div>
|
|
26076
|
-
<div style="font-size:11px;color:#92400e;font-weight:600;margin-bottom:4px">🏅 ${t('我的等级')}</div>
|
|
26077
|
-
<div style="font-size:18px;font-weight:800;color:#78350f">L${userLevel.level} ${escHtml(userLevel.name)}</div>
|
|
26078
|
-
<div style="font-size:10px;color:#a16207;margin-top:2px">${t('累计')}:¥${lifetimeScore.toFixed(0)}${userLevel.nextThreshold ? ` · ${t('下一档')} ¥${Number(userLevel.nextThreshold).toLocaleString()}` : ''}</div>
|
|
26079
|
-
</div>
|
|
26080
|
-
<div>
|
|
26081
|
-
<div style="font-size:11px;color:#92400e;font-weight:600;margin-bottom:4px">💎 ${t('本期分配率')}</div>
|
|
26082
|
-
<div style="font-size:18px;font-weight:800;color:#78350f">${(distCap * 100).toFixed(0)}%</div>
|
|
26083
|
-
<div style="font-size:10px;margin-top:2px"><span style="background:${healthColor.bg};color:${healthColor.fg};padding:1px 6px;border-radius:99px;font-weight:600">${healthColor.label}</span> ${t('池子健康度')}</div>
|
|
26084
|
-
</div>
|
|
26085
|
-
</div>
|
|
26086
|
-
</div>`
|
|
26100
|
+
// 奖励等级 / 分配率 / 池子健康度卡已移除(匹配奖励引擎已切除 #401 / #PR-D);钱包只展示中性资产 + 参与记录。
|
|
26087
26101
|
|
|
26088
26102
|
app.innerHTML = shell(`
|
|
26089
26103
|
<h1 class="page-title">💰 ${t('我的钱包')}</h1>
|
|
26090
26104
|
|
|
26091
|
-
${pausedBanner}
|
|
26092
|
-
|
|
26093
|
-
${levelCard}
|
|
26094
|
-
|
|
26095
26105
|
${renderTrustCard(trust)}
|
|
26096
26106
|
|
|
26097
26107
|
<!-- ① 资产总览 -->
|
|
@@ -26116,7 +26126,7 @@ async function renderWallet(app) {
|
|
|
26116
26126
|
|
|
26117
26127
|
<!-- 卖家收入分类速览(仅 seller 显示)- 2026-05-24 匹配列只在 PV 允许地区显示 -->
|
|
26118
26128
|
${state.user?.role === 'seller' ? (() => {
|
|
26119
|
-
const _pvOK =
|
|
26129
|
+
const _pvOK = inc.gates?.matching_rewards_active === true // Category C:匹配已结列读奖励闸门(默认关),不再读 region_pv_enabled
|
|
26120
26130
|
const cols = _pvOK ? 4 : 3
|
|
26121
26131
|
return `
|
|
26122
26132
|
<div class="card" style="margin-bottom:12px;padding:14px">
|
|
@@ -26132,11 +26142,6 @@ async function renderWallet(app) {
|
|
|
26132
26142
|
<div style="font-size:14px;font-weight:700;color:#059669;margin-top:2px">${((inc.commissions?.l1?.total||0)+(inc.commissions?.l2?.total||0)+(inc.commissions?.l3?.total||0)).toFixed(2)}</div>
|
|
26133
26143
|
<div style="font-size:9px;color:#9ca3af">${((inc.commissions?.l1?.count||0)+(inc.commissions?.l2?.count||0)+(inc.commissions?.l3?.count||0))} ${t('单')}</div>
|
|
26134
26144
|
</div>
|
|
26135
|
-
${_pvOK ? `<div style="background:#fefce8;border-radius:6px;padding:8px">
|
|
26136
|
-
<div style="font-size:9px;color:#854d0e">⚛ ${t('匹配已结')}</div>
|
|
26137
|
-
<div style="font-size:14px;font-weight:700;color:#a16207;margin-top:2px">${(inc.binary?.settled_waz||0).toFixed(2)}</div>
|
|
26138
|
-
<div style="font-size:9px;color:#9ca3af">${inc.binary?.settled_count||0} ${t('次')}</div>
|
|
26139
|
-
</div>` : ''}
|
|
26140
26145
|
<div style="background:#fef3c7;border-radius:6px;padding:8px">
|
|
26141
26146
|
<div style="font-size:9px;color:#92400e">⏳ ${t('待结算')}</div>
|
|
26142
26147
|
<div style="font-size:14px;font-weight:700;color:#d97706;margin-top:2px">${escrowed.toFixed(2)}</div>
|
|
@@ -26167,11 +26172,6 @@ async function renderWallet(app) {
|
|
|
26167
26172
|
<div style="text-align:center;background:#ecfeff;border-radius:6px;padding:10px"><div style="font-size:10px;color:#155e75">L2</div><div style="font-size:15px;font-weight:700;color:#0891b2">${inc.commissions.l2.total.toFixed(2)}</div><div style="font-size:9px;color:#9ca3af">${inc.commissions.l2.count} ${t('单')}</div></div>
|
|
26168
26173
|
</div>`
|
|
26169
26174
|
})()}
|
|
26170
|
-
${getMaxLevels() >= 3 ? `
|
|
26171
|
-
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:10px">
|
|
26172
|
-
<div style="background:#fefce8;border-radius:6px;padding:10px"><div style="font-size:10px;color:#854d0e">⚛ ${t('积分已结算')}</div><div style="font-size:15px;font-weight:700;color:#a16207">${inc.binary.settled_waz.toFixed(2)} WAZ</div><div style="font-size:9px;color:#9ca3af">${inc.binary.settled_count} ${t('次')}</div></div>
|
|
26173
|
-
<div style="background:#fef3c7;border-radius:6px;padding:10px"><div style="font-size:10px;color:#92400e">⏳ ${t('待结 Score')}</div><div style="font-size:15px;font-weight:700;color:#d97706">${inc.binary.pending_score.toFixed(1)}</div><div style="font-size:9px;color:#9ca3af">${t('每月结算')}</div></div>
|
|
26174
|
-
</div>` : ''}
|
|
26175
26175
|
${inc.sales.count > 0 ? `
|
|
26176
26176
|
<div style="background:#f0fdf4;border-radius:6px;padding:10px"><div style="font-size:10px;color:#166534">🏪 ${t('销售收入')}</div><div style="font-size:15px;font-weight:700;color:#15803d">${inc.sales.total.toFixed(2)} WAZ</div><div style="font-size:9px;color:#9ca3af">${inc.sales.count} ${t('单')}</div></div>
|
|
26177
26177
|
` : ''}
|
|
@@ -27426,7 +27426,8 @@ window.setClaimVerifyTab = (k) => {
|
|
|
27426
27426
|
}
|
|
27427
27427
|
|
|
27428
27428
|
async function renderClaimTaskDetail(app, taskId) {
|
|
27429
|
-
|
|
27429
|
+
// 'new' 是历史死路由(无创建处理器);商品声明验证从商品页声明区的现成表单发起 → 回验证中心,避免报错页
|
|
27430
|
+
if (!taskId || taskId === 'new') { navigate('#verify'); return }
|
|
27430
27431
|
if (!state.user) { renderLogin(); return }
|
|
27431
27432
|
app.innerHTML = shell(loading$(), 'verify')
|
|
27432
27433
|
const data = await GET(`/claim-tasks/${taskId}`)
|
|
@@ -31784,15 +31785,11 @@ async function renderAdminProtocol(app) {
|
|
|
31784
31785
|
<!-- 协议金库摘要 -->
|
|
31785
31786
|
<div class="card" style="margin-bottom:14px;background:linear-gradient(135deg,#eef2ff,#faf5ff);border-color:#c7d2fe;padding:16px">
|
|
31786
31787
|
<div style="font-size:11px;color:#3730a3;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;margin-bottom:10px">💎 ${t('协议金库摘要')}</div>
|
|
31787
|
-
<div style="display:grid;grid-template-columns:repeat(
|
|
31788
|
+
<div style="display:grid;grid-template-columns:repeat(2,1fr);gap:10px;font-size:12px">
|
|
31788
31789
|
<div style="background:rgba(255,255,255,0.7);border-radius:8px;padding:10px;text-align:center">
|
|
31789
31790
|
<div style="font-size:18px;font-weight:800;color:#4338ca">${Number(tk.pool_balance || 0).toFixed(2)}</div>
|
|
31790
31791
|
<div style="color:#6b7280;margin-top:2px">${t('全球基金池')} WAZ</div>
|
|
31791
31792
|
</div>
|
|
31792
|
-
<div style="background:rgba(255,255,255,0.7);border-radius:8px;padding:10px;text-align:center">
|
|
31793
|
-
<div style="font-size:18px;font-weight:800;color:#7c3aed">${Number(tk.management_bonus || 0).toFixed(2)}</div>
|
|
31794
|
-
<div style="color:#6b7280;margin-top:2px">${t('管理津贴池')} WAZ</div>
|
|
31795
|
-
</div>
|
|
31796
31793
|
<div style="background:rgba(255,255,255,0.7);border-radius:8px;padding:10px;text-align:center">
|
|
31797
31794
|
<div style="font-size:18px;font-weight:800;color:#dc2626">${Number(fund.balance || 0).toFixed(2)}</div>
|
|
31798
31795
|
<div style="color:#6b7280;margin-top:2px">${t('慈善基金')} WAZ</div>
|
|
@@ -31807,6 +31804,9 @@ async function renderAdminProtocol(app) {
|
|
|
31807
31804
|
${adminLinkCard('🛑', t('错误监控'), t('24h 趋势 + burst 告警'), '#admin/errors')}
|
|
31808
31805
|
${adminLinkCard('📨', t('Welcome 提交'), t('#welcome 留下的邮箱订阅 + 建议'), '#admin/public-ideas')}
|
|
31809
31806
|
${adminLinkCard('🛠️', t('任务建议收件箱'), t('陌生人 / agent 提交的共建任务建议;审阅 → 转正式任务'), '#admin/task-proposals')}
|
|
31807
|
+
${((state.user && state.user.admin_type || 'root') === 'root') ? adminLinkCard('🎟️', t('建任务额度审核'), t('非根管理员的建任务扩容申请;批准 = 限时计数授权(仅 root)'), '#admin/quota-requests') : ''}
|
|
31808
|
+
${adminLinkCard('🔗', t('关联个人贡献账号'), t('把本管理席位的协调贡献归属到你的真实个人账号(需对方确认 + root 审批)'), '#me/operator-claims')}
|
|
31809
|
+
${((state.user && state.user.admin_type || 'root') === 'root') ? adminLinkCard('🪪', t('操作席位关联审批'), t('管理席位→个人贡献账号的关联申请;确认 + 审批 / 撤销(仅 root)'), '#admin/operator-claims') : ''}
|
|
31810
31810
|
</div>
|
|
31811
31811
|
`, 'admin-protocol')
|
|
31812
31812
|
}
|