@seasonkoh/webaz 0.1.25 → 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 -328
- 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 +164 -177
- 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 +33 -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 +58 -8
- 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/identity-claim-discovery.js +55 -0
- package/dist/layer2-business/L2-9-contribution/task-proposal-ai-store.js +99 -0
- package/dist/layer2-business/L2-9-contribution/task-proposal-draft.js +360 -0
- 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/admin-bearer-auth.js +21 -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/email-delivery.js +127 -0
- 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 +1607 -912
- package/dist/pwa/public/i18n.js +284 -68
- package/dist/pwa/public/index.html +1 -1
- package/dist/pwa/public/openapi.json +4760 -2769
- package/dist/pwa/public/whitepaper/en/index.html +153 -0
- package/dist/pwa/public/whitepaper/zh-CN/index.html +153 -0
- 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 +7 -14
- package/dist/pwa/routes/admin-moderation.js +25 -1
- package/dist/pwa/routes/admin-operator-claims.js +280 -0
- package/dist/pwa/routes/admin-ops.js +13 -2
- 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 +35 -2
- package/dist/pwa/routes/admin-wallet-ops.js +26 -3
- package/dist/pwa/routes/auction.js +4 -2
- package/dist/pwa/routes/auth-read.js +11 -6
- package/dist/pwa/routes/auth-register.js +84 -24
- 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/contribution-identity.js +17 -0
- package/dist/pwa/routes/dispute-cases.js +5 -4
- package/dist/pwa/routes/growth.js +4 -4
- package/dist/pwa/routes/orders-action.js +46 -23
- package/dist/pwa/routes/orders-create.js +1 -1
- package/dist/pwa/routes/products-meta.js +19 -6
- package/dist/pwa/routes/profile-credentials.js +7 -4
- package/dist/pwa/routes/profile-placement.js +8 -9
- package/dist/pwa/routes/promoter.js +11 -44
- package/dist/pwa/routes/public-build-tasks.js +5 -1
- package/dist/pwa/routes/public-utils.js +9 -12
- package/dist/pwa/routes/ratings.js +64 -4
- package/dist/pwa/routes/recover-key.js +58 -19
- package/dist/pwa/routes/referral.js +9 -50
- package/dist/pwa/routes/rewards-apply.js +3 -2
- package/dist/pwa/routes/share-redirects.js +5 -4
- package/dist/pwa/routes/shareables-interactions.js +2 -1
- package/dist/pwa/routes/shop-referral.js +6 -5
- package/dist/pwa/routes/shops.js +5 -2
- package/dist/pwa/routes/task-proposals.js +159 -7
- package/dist/pwa/routes/trial.js +4 -2
- package/dist/pwa/routes/users-public.js +1 -14
- package/dist/pwa/routes/wallet-read.js +3 -15
- package/dist/pwa/routes/webauthn.js +1 -1
- package/dist/pwa/server.js +223 -478
- package/dist/settlement-math.js +3 -3
- package/dist/version.js +6 -4
- package/package.json +62 -8
- 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])
|
|
@@ -693,6 +695,7 @@ async function render(page, params) {
|
|
|
693
695
|
if (params[0] === 'users' && params[1]) return renderAdminUserDetail(app, params[1])
|
|
694
696
|
if (params[0] === 'users') return renderAdminUsers(app)
|
|
695
697
|
if (params[0] === 'audit') return renderAdminAudit(app)
|
|
698
|
+
if (params[0] === 'security') return renderAdminSecurity(app)
|
|
696
699
|
if (params[0] === 'products') return renderAdminProducts(app)
|
|
697
700
|
if (params[0] === 'orders') return renderAdminOrders(app)
|
|
698
701
|
if (params[0] === 'disputes') return renderAdminDisputes(app)
|
|
@@ -712,6 +715,8 @@ async function render(page, params) {
|
|
|
712
715
|
// 2026-05-24 #welcome 公开 ideas/邮箱订阅查看
|
|
713
716
|
if (params[0] === 'public-ideas') return renderAdminPublicIdeas(app)
|
|
714
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)
|
|
715
720
|
if (params[0] === 'params') return renderAdminParams(app)
|
|
716
721
|
if (params[0] === 'timeline' && params[1]) return renderAdminUserTimeline(app, params[1])
|
|
717
722
|
if (params[0] === 'timeline') return renderAdminUserTimelinePicker(app)
|
|
@@ -1012,6 +1017,40 @@ function preLaunchBannerHTML() {
|
|
|
1012
1017
|
</div>`
|
|
1013
1018
|
}
|
|
1014
1019
|
|
|
1020
|
+
// 账户无任何恢复方式 → 首页顶部持续红色风险横幅。
|
|
1021
|
+
// 恢复方式 = 密码 OR 已验证邮箱。Passkey【不算】恢复方式:它是敏感操作的"真人在场"门,
|
|
1022
|
+
// 没有 Passkey 登录 / 找回路径,丢 key 换设备后 Passkey 救不回账号(与弹窗"增强不替代恢复邮箱"一致)。
|
|
1023
|
+
function recoveryBannerHTML() {
|
|
1024
|
+
const u = state.user
|
|
1025
|
+
if (!u || u.role === 'admin') return ''
|
|
1026
|
+
const hasRecovery = u.has_password || u.email_verified
|
|
1027
|
+
if (hasRecovery) return ''
|
|
1028
|
+
return `<div style="background:#fef2f2;border:1px solid #fca5a5;border-radius:8px;padding:10px 14px;font-size:12px;color:#991b1b;line-height:1.6;margin:8px 12px 12px">
|
|
1029
|
+
🚨 <strong>${t('账户还没有恢复方式')}</strong> — ${t('闪退或换设备清缓存后可能永久无法登录。')}
|
|
1030
|
+
<div style="margin-top:8px;display:flex;gap:8px;flex-wrap:wrap">
|
|
1031
|
+
<button class="btn btn-sm" style="font-size:11px;padding:4px 10px;background:#dc2626;color:#fff;border:none;border-radius:6px;cursor:pointer" onclick="navigate('#me/settings')">${t('立即设置密码 / 绑定邮箱')} →</button>
|
|
1032
|
+
</div>
|
|
1033
|
+
</div>`
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// 卖家后台安全提醒(P1):卖家涉及商品/履约/钱包,恢复方式不齐尤其危险。
|
|
1037
|
+
// 全局红横幅(零恢复)已在 shell 顶部覆盖;此处补"有邮箱但还缺密码"等部分缺口的黄色软提醒,避免与红横幅叠加。
|
|
1038
|
+
function sellerRecoveryReminderHTML() {
|
|
1039
|
+
const u = state.user
|
|
1040
|
+
if (!u) return ''
|
|
1041
|
+
// 全局红横幅判定一致:无密码且无邮箱 → 红横幅已覆盖,这里不再叠加(Passkey 不算恢复方式)
|
|
1042
|
+
const globalRedShowing = !u.has_password && !u.email_verified
|
|
1043
|
+
if (globalRedShowing) return ''
|
|
1044
|
+
const gaps = []
|
|
1045
|
+
if (!u.has_password) gaps.push(t('未设置登录密码'))
|
|
1046
|
+
if (!u.email_verified) gaps.push(t('未绑定找回邮箱'))
|
|
1047
|
+
if (gaps.length === 0) return ''
|
|
1048
|
+
return `<div class="alert" style="background:#fffbeb;border:1px solid #fde68a;color:#92400e;font-size:12px;line-height:1.6;margin-bottom:12px">
|
|
1049
|
+
🛡 <strong>${t('建议补全账户恢复方式')}</strong>:${gaps.join(' · ')}。${t('卖家涉及商品/履约/钱包,闪退或换设备清缓存后可能无法登录。')}
|
|
1050
|
+
<button class="btn btn-sm" style="font-size:11px;padding:3px 10px;margin-left:6px;background:#d97706;color:#fff;border:none;border-radius:6px;cursor:pointer" onclick="navigate('#me/settings')">${t('去设置')} →</button>
|
|
1051
|
+
</div>`
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1015
1054
|
function shell(content, activeTab, opts) {
|
|
1016
1055
|
// opts = { hideTabbar?: bool, bottomBar?: html }(第三参数可选,向后兼容)
|
|
1017
1056
|
const _opts = opts || {}
|
|
@@ -1116,7 +1155,7 @@ function shell(content, activeTab, opts) {
|
|
|
1116
1155
|
: `<button class="btn btn-primary btn-sm" onclick="navigate('#login')">${t('登录')}</button>`}
|
|
1117
1156
|
</div>
|
|
1118
1157
|
</nav>
|
|
1119
|
-
<main class="main">${content}</main>
|
|
1158
|
+
<main class="main">${recoveryBannerHTML()}${content}</main>
|
|
1120
1159
|
${state.user?.role === 'buyer' ? `
|
|
1121
1160
|
<button id="compare-fab" onclick="openCompare()" title="${t('对比商品')}"
|
|
1122
1161
|
style="position:fixed;bottom:136px;right:14px;background:#4f46e5;color:#fff;border:none;cursor:pointer;font-size:12px;font-weight:600;padding:8px 14px;border-radius:99px;box-shadow:0 4px 12px rgba(79,70,229,0.3);z-index:98;display:none;align-items:center;gap:4px">
|
|
@@ -1250,14 +1289,17 @@ async function renderMyAdvanced(app) {
|
|
|
1250
1289
|
app.innerHTML = shell(loading$(), 'me')
|
|
1251
1290
|
const role = state.user.role
|
|
1252
1291
|
const isTrusted = ['admin', 'verifier', 'logistics', 'arbitrator'].includes(role)
|
|
1253
|
-
const [agentRes, skillsRes] = await Promise.all([
|
|
1292
|
+
const [agentRes, skillsRes, ocRes] = await Promise.all([
|
|
1254
1293
|
GET('/agents/me/reputation').catch(() => null),
|
|
1255
1294
|
GET('/skills/mine').catch(() => []),
|
|
1295
|
+
GET('/me/operator-claims').catch(() => null),
|
|
1256
1296
|
])
|
|
1257
1297
|
const trustScore = Math.round(agentRes?.trust_score || 0)
|
|
1258
1298
|
const level = agentRes?.level || 'new'
|
|
1259
1299
|
const lvlColor = { legend: '#dc2626', quality: '#9333ea', trusted: '#4f46e5', new: '#9ca3af' }[level] || '#6b7280'
|
|
1260
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)
|
|
1261
1303
|
|
|
1262
1304
|
const card = (icon, label, sub, hash) => `
|
|
1263
1305
|
<div class="card" onclick="location.hash='${hash}'" style="padding:14px;cursor:pointer;display:flex;align-items:center;gap:10px;min-height:64px">
|
|
@@ -1299,6 +1341,7 @@ async function renderMyAdvanced(app) {
|
|
|
1299
1341
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px">
|
|
1300
1342
|
${card('📜', t('Timeline'), t('全部事件按时间排列'), '#me/timeline')}
|
|
1301
1343
|
${card('📡', t('Webhook'), t('订阅事件 push 到外部端点'), '#me/webhooks')}
|
|
1344
|
+
${(role === 'admin' || hasOperatorClaim) ? card('🪪', t('贡献归属'), t('待确认的 admin 关联 / 关联记录'), '#me/operator-claims') : ''}
|
|
1302
1345
|
</div>
|
|
1303
1346
|
|
|
1304
1347
|
<div style="font-size:12px;color:#6b7280;font-weight:600;margin:14px 0 6px">🧠 ${t('技能市场')}</div>
|
|
@@ -1311,7 +1354,9 @@ async function renderMyAdvanced(app) {
|
|
|
1311
1354
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px">
|
|
1312
1355
|
${card('🏛', t('协议治理'), t('参数公开 · 变更可追溯'), '#governance')}
|
|
1313
1356
|
${card('⚖', t('判例库'), t('争议判决公开'), '#judgments')}
|
|
1314
|
-
${card('
|
|
1357
|
+
${card('🛠', t('我的共建'), t('贡献 / GitHub 认领 / 建设信誉 — 无购买门槛'), '#my-contributions')}
|
|
1358
|
+
${card('📋', t('公开共建任务'), t('浏览可认领任务、提交建议、参与共建'), '#contribute/tasks')}
|
|
1359
|
+
${card('🎁', t('分享分润管理'), t('分享佣金 / PV / escrow · 经济关系登记'), '#rewards-me')}
|
|
1315
1360
|
</div>
|
|
1316
1361
|
|
|
1317
1362
|
<div style="font-size:12px;color:#6b7280;font-weight:600;margin:14px 0 6px">📧 ${t('联系我们')}</div>
|
|
@@ -1719,7 +1764,7 @@ async function renderProfile(app) {
|
|
|
1719
1764
|
</div>
|
|
1720
1765
|
<div style="display:flex;justify-content:space-between;align-items:center;padding:6px 0;border-bottom:1px solid #f3f4f6;font-size:13px">
|
|
1721
1766
|
<span style="color:#374151">${t('协议')}</span>
|
|
1722
|
-
<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>
|
|
1723
1768
|
</div>
|
|
1724
1769
|
<div style="display:flex;justify-content:space-between;align-items:center;padding:6px 0;border-bottom:1px solid #f3f4f6;font-size:13px">
|
|
1725
1770
|
<span style="color:#374151">🔔 ${t('推送通知')}</span>
|
|
@@ -3113,9 +3158,14 @@ async function renderAdminTaskProposals(app) {
|
|
|
3113
3158
|
const T = (zh, e) => en && e ? e : zh
|
|
3114
3159
|
app.innerHTML = shell(loading$(), 'admin')
|
|
3115
3160
|
const sf = state._proposalStatus || '' // '' | new | needs_info | rejected | converted
|
|
3116
|
-
const r = await
|
|
3161
|
+
const [r, dr] = await Promise.all([
|
|
3162
|
+
GET('/admin/task-proposals' + (sf ? '?status=' + encodeURIComponent(sf) : '')),
|
|
3163
|
+
GET('/admin/build-task-drafts'),
|
|
3164
|
+
])
|
|
3117
3165
|
if (r.error) { app.innerHTML = shell(alert$('error', r.error), 'admin'); return }
|
|
3118
3166
|
const proposals = r.proposals || []
|
|
3167
|
+
const drafts = (dr && dr.drafts) || []
|
|
3168
|
+
const draftedIds = new Set(drafts.map((d) => d.source_proposal_id).filter(Boolean)) // proposals that already have an unpublished draft
|
|
3119
3169
|
const notice = en ? (r.value_boundary?.notice_en || '') : (r.value_boundary?.notice_zh || '')
|
|
3120
3170
|
const STATUS = {
|
|
3121
3171
|
new: { bg: '#fef3c7', fg: '#92400e', label: T('待审', 'New') },
|
|
@@ -3126,12 +3176,33 @@ async function renderAdminTaskProposals(app) {
|
|
|
3126
3176
|
const badge = (s) => { const c = STATUS[s] || { bg: '#f3f4f6', fg: '#6b7280', label: s }; return `<span style="font-size:10px;background:${c.bg};color:${c.fg};padding:2px 8px;border-radius:99px;font-weight:600">${c.label}</span>` }
|
|
3127
3177
|
const chip = (val, label) => `<button onclick="setProposalStatusFilter('${val}')" style="padding:5px 12px;border-radius:99px;font-size:11px;cursor:pointer;border:1px solid ${sf === val ? '#6366f1' : '#e5e7eb'};background:${sf === val ? '#eef2ff' : '#fff'};color:${sf === val ? '#4338ca' : '#6b7280'};font-weight:600">${label}</button>`
|
|
3128
3178
|
const field = (label, val) => val ? `<div style="font-size:12px;color:#374151;margin-top:4px"><b>${label}:</b> ${escHtml(String(val))}</div>` : ''
|
|
3179
|
+
// inline "create formal task draft" form (prefilled from the proposal; AI can also prefill it). All list
|
|
3180
|
+
// fields are newline-separated. These are the agent-handoff fields the formal task model requires.
|
|
3181
|
+
const ta = (id, ph, val, h) => `<textarea id="${id}" placeholder="${ph}" style="width:100%;box-sizing:border-box;min-height:${h || 38}px;padding:6px 8px;border:1px solid #d4d4d8;border-radius:6px;font-size:12px;margin-top:6px">${val ? escHtml(String(val)) : ''}</textarea>`
|
|
3182
|
+
const draftForm = (p) => `<div id="df-${escHtml(p.id)}" style="display:none;margin-top:10px;border:1px dashed #c7d2fe;background:#f5f7ff;border-radius:8px;padding:10px">
|
|
3183
|
+
<div style="font-size:11px;color:#4338ca;font-weight:600;margin-bottom:2px">${T('建正式任务草稿(未发布)', 'Create formal task draft (unpublished)')}</div>
|
|
3184
|
+
<div style="font-size:10px;color:#6b7280;margin-bottom:4px">${T('草稿默认隐藏不可认领;填齐 agent 交接字段后由人工显式「发布」才进任务板。', 'A draft is hidden + unclaimable; only an explicit human “Publish” (after the agent-handoff fields are filled) puts it on the board.')}</div>
|
|
3185
|
+
${ta('df-title-' + escHtml(p.id), T('标题', 'Title'), p.title)}
|
|
3186
|
+
${ta('df-area-' + escHtml(p.id), T('领域(可选)', 'Area (optional)'), p.suggested_area, 30)}
|
|
3187
|
+
${ta('df-source-' + escHtml(p.id), T('来源引用(文件 / RFC / issue,可选)', 'Source ref (file / RFC / issue, optional)'), p.source_ref, 30)}
|
|
3188
|
+
${ta('df-desc-' + escHtml(p.id), T('说明 / 原因', 'Summary / reason'), p.summary, 48)}
|
|
3189
|
+
${ta('df-allowed-' + escHtml(p.id), T('允许路径(每行一条)', 'Allowed paths (one per line)'), '')}
|
|
3190
|
+
${ta('df-fpaths-' + escHtml(p.id), T('禁止路径(每行一条)', 'Forbidden paths (one per line)'), '')}
|
|
3191
|
+
${ta('df-forbidden-' + escHtml(p.id), T('禁止动作(每行一条)', 'Forbidden actions (one per line)'), '')}
|
|
3192
|
+
${ta('df-accept-' + escHtml(p.id), T('验收标准(每行一条)', 'Acceptance criteria (one per line)'), p.expected_outcome)}
|
|
3193
|
+
${ta('df-verify-' + escHtml(p.id), T('验证命令(每行一条)', 'Verification commands (one per line)'), '')}
|
|
3194
|
+
${ta('df-deliver-' + escHtml(p.id), T('交付物(每行一条)', 'Deliverables (one per line)'), '')}
|
|
3195
|
+
${ta('df-dod-' + escHtml(p.id), T('完成定义', 'Definition of done'), '')}
|
|
3196
|
+
${ta('df-expect-' + escHtml(p.id), T('预期结果(留空则用说明)', 'Expected results (blank = use summary)'), '')}
|
|
3197
|
+
<button onclick="createTaskDraft('${escHtml(p.id)}')" style="margin-top:8px;padding:7px 14px;border:none;background:#4338ca;color:#fff;border-radius:6px;font-size:12px;cursor:pointer;font-weight:600">${T('保存草稿', 'Save draft')}</button>
|
|
3198
|
+
</div>`
|
|
3129
3199
|
const row = (p) => {
|
|
3130
3200
|
const terminal = p.status === 'rejected' || p.status === 'converted'
|
|
3131
3201
|
return `<div class="card" style="padding:14px;margin-bottom:10px">
|
|
3132
3202
|
<div style="display:flex;justify-content:space-between;gap:8px;align-items:flex-start">
|
|
3133
3203
|
<div style="font-weight:600;font-size:14px">${escHtml(p.title)}</div>${badge(p.status)}
|
|
3134
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>
|
|
3135
3206
|
<div style="font-size:13px;color:#52525B;line-height:1.5;margin-top:6px;white-space:pre-wrap">${escHtml(p.summary)}</div>
|
|
3136
3207
|
${field(T('建议领域', 'Area'), p.suggested_area)}
|
|
3137
3208
|
${field(T('预期结果', 'Outcome'), p.expected_outcome)}
|
|
@@ -3146,13 +3217,33 @@ async function renderAdminTaskProposals(app) {
|
|
|
3146
3217
|
<textarea id="pr-note-${escHtml(p.id)}" placeholder="${T('审阅备注(可选)', 'Review note (optional)')}" style="width:100%;box-sizing:border-box;min-height:44px;padding:6px 8px;border:1px solid #d4d4d8;border-radius:6px;font-size:12px"></textarea>
|
|
3147
3218
|
<input id="pr-ref-${escHtml(p.id)}" placeholder="${T('转任务时:关联正式 task / PR / release(可选)', 'On convert: link the real task / PR / release (optional)')}" style="width:100%;box-sizing:border-box;margin-top:6px;padding:6px 8px;border:1px solid #d4d4d8;border-radius:6px;font-size:12px">
|
|
3148
3219
|
<div style="display:flex;gap:6px;margin-top:8px;flex-wrap:wrap">
|
|
3220
|
+
<button onclick="aiAssistProposal('${escHtml(p.id)}')" style="padding:6px 12px;border:1px solid #8b5cf6;background:#fff;color:#6d28d9;border-radius:6px;font-size:12px;cursor:pointer">🤖 ${T('AI 建议', 'AI suggest')}</button>
|
|
3221
|
+
${draftedIds.has(p.id)
|
|
3222
|
+
? `<span style="padding:6px 10px;font-size:11px;color:#4338ca;background:#eef2ff;border-radius:6px">📝 ${T('已建草稿(在上方草稿区发布;发布即接受)', 'Draft created — publish it in the drafts panel above (publish = accept)')}</span>`
|
|
3223
|
+
: `<button onclick="toggleDraftForm('${escHtml(p.id)}')" style="padding:6px 12px;border:1px solid #6366f1;background:#fff;color:#4338ca;border-radius:6px;font-size:12px;cursor:pointer">${T('建任务草稿', 'Create task draft')}</button>
|
|
3149
3224
|
<button onclick="reviewProposal('${escHtml(p.id)}','needs_info')" style="padding:6px 12px;border:1px solid #3b82f6;background:#fff;color:#1e40af;border-radius:6px;font-size:12px;cursor:pointer">${T('需补充', 'Needs info')}</button>
|
|
3150
3225
|
<button onclick="reviewProposal('${escHtml(p.id)}','rejected')" style="padding:6px 12px;border:1px solid #ef4444;background:#fff;color:#991b1b;border-radius:6px;font-size:12px;cursor:pointer">${T('拒绝', 'Reject')}</button>
|
|
3151
|
-
<button onclick="reviewProposal('${escHtml(p.id)}','converted')" style="padding:6px 12px;border:
|
|
3226
|
+
<button onclick="reviewProposal('${escHtml(p.id)}','converted')" style="padding:6px 12px;border:1px solid #16a34a;background:#fff;color:#166534;border-radius:6px;font-size:12px;cursor:pointer">${T('仅记审阅决定', 'Mark reviewed')}</button>`}
|
|
3152
3227
|
</div>
|
|
3228
|
+
<div id="ai-${escHtml(p.id)}"></div>
|
|
3229
|
+
${draftedIds.has(p.id) ? '' : draftForm(p)}
|
|
3153
3230
|
</div>`}
|
|
3154
3231
|
</div>`
|
|
3155
3232
|
}
|
|
3233
|
+
const draftRow = (d) => `<div class="card" style="padding:12px;margin-bottom:8px;border-left:3px solid #6366f1">
|
|
3234
|
+
<div style="display:flex;justify-content:space-between;gap:8px;align-items:flex-start">
|
|
3235
|
+
<div style="font-weight:600;font-size:13px">${escHtml(d.title)}</div>
|
|
3236
|
+
<span style="font-size:10px;background:#eef2ff;color:#4338ca;padding:2px 8px;border-radius:99px;font-weight:600">${T('未发布草稿', 'Unpublished draft')}</span>
|
|
3237
|
+
</div>
|
|
3238
|
+
${field(T('风险', 'Risk'), d.risk_level)}${field(T('可自助认领', 'Auto-claimable'), d.auto_claimable === 1 || d.auto_claimable === true ? T('是', 'yes') : T('否(需真人)', 'no (human)'))}
|
|
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>
|
|
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>
|
|
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>
|
|
3246
|
+
</div>`
|
|
3156
3247
|
app.innerHTML = shell(`
|
|
3157
3248
|
<div style="padding:14px;max-width:920px;margin:0 auto">
|
|
3158
3249
|
<h1 class="page-title">🛠️ ${T('任务建议收件箱', 'Task Proposal Inbox')}</h1>
|
|
@@ -3160,6 +3251,10 @@ async function renderAdminTaskProposals(app) {
|
|
|
3160
3251
|
${T('建议是陌生人 / agent 提交的想法,不是贡献事实 / 奖励 / 正式参与。「转为正式任务」只记录评审决定与证据链(proposer → reviewer → 关联引用),不会自动创建 build_task。', 'A proposal is a stranger / agent suggestion — NOT a contribution fact / reward / participation. “Convert” only records the review decision + the proposer → reviewer → ref evidence chain; it does NOT auto-create a build_task.')}
|
|
3161
3252
|
${notice ? `<br>${escHtml(notice)}` : ''}
|
|
3162
3253
|
</div>
|
|
3254
|
+
${drafts.length ? `<div style="margin-bottom:14px">
|
|
3255
|
+
<div style="font-size:12px;font-weight:700;color:#4338ca;margin-bottom:6px">📝 ${T('未发布任务草稿', 'Unpublished task drafts')} (${drafts.length})</div>
|
|
3256
|
+
${drafts.map(draftRow).join('')}
|
|
3257
|
+
</div>` : ''}
|
|
3163
3258
|
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:12px">
|
|
3164
3259
|
${chip('', T('全部', 'All'))}${chip('new', STATUS.new.label)}${chip('needs_info', STATUS.needs_info.label)}${chip('rejected', STATUS.rejected.label)}${chip('converted', STATUS.converted.label)}
|
|
3165
3260
|
</div>
|
|
@@ -3179,6 +3274,530 @@ window.reviewProposal = async (id, status) => {
|
|
|
3179
3274
|
toast$(window._lang === 'en' ? 'Updated' : '已更新')
|
|
3180
3275
|
renderAdminTaskProposals(document.getElementById('app'))
|
|
3181
3276
|
}
|
|
3277
|
+
window.toggleDraftForm = (id) => { const el = document.getElementById('df-' + id); if (el) el.style.display = el.style.display === 'none' ? 'block' : 'none' }
|
|
3278
|
+
// AI-assist is ASSISTANT-ONLY: it renders a suggestion + prefills the draft form; it never publishes/decides.
|
|
3279
|
+
window.aiAssistProposal = async (id) => {
|
|
3280
|
+
const en = window._lang === 'en'
|
|
3281
|
+
const box = document.getElementById('ai-' + id); if (box) box.innerHTML = `<div style="font-size:11px;color:#8b5cf6;margin-top:8px">🤖 ${en ? 'thinking…' : '分析中…'}</div>`
|
|
3282
|
+
const r = await POST('/admin/task-proposals/' + encodeURIComponent(id) + '/ai-assist', {})
|
|
3283
|
+
if (r.error) { if (box) box.innerHTML = ''; toast$(r.error || 'failed'); return }
|
|
3284
|
+
const s = r.ai_suggestion || {}
|
|
3285
|
+
window._aiSuggest = window._aiSuggest || {}; window._aiSuggest[id] = s.suggested || {}
|
|
3286
|
+
const list = (arr) => (arr || []).map(x => '• ' + escHtml(String(x))).join('<br>')
|
|
3287
|
+
if (box) box.innerHTML = `<div style="margin-top:8px;border:1px solid #ddd6fe;background:#faf5ff;border-radius:8px;padding:10px">
|
|
3288
|
+
<div style="font-size:11px;font-weight:700;color:#6d28d9">🤖 ${en ? 'AI suggestion' : 'AI 建议'} <span style="font-weight:400;color:#9ca3af">(${escHtml(String(r.model || ''))})</span></div>
|
|
3289
|
+
<div style="font-size:10px;color:#b45309;margin:2px 0 6px">${escHtml(String(r.ai_notice || ''))}</div>
|
|
3290
|
+
<div style="font-size:12px;color:#374151;line-height:1.6">
|
|
3291
|
+
<b>${en ? 'Category' : '分类'}:</b> ${escHtml(String(s.category || ''))} · <b>${en ? 'Risk' : '风险'}:</b> ${escHtml(String(s.risk || ''))} · <b>${en ? 'Effort' : '工作量'}:</b> ${escHtml(String(s.effort || ''))} · <b>${en ? 'Duplicate' : '疑似重复'}:</b> ${escHtml(String(s.duplicate_likelihood || ''))}
|
|
3292
|
+
${(s.missing_info && s.missing_info.length) ? `<div style="margin-top:4px"><b>${en ? 'Missing info' : '缺失信息'}:</b><br>${list(s.missing_info)}</div>` : ''}
|
|
3293
|
+
</div>
|
|
3294
|
+
<button onclick="applyAiToDraft('${id}')" style="margin-top:8px;padding:5px 12px;border:1px solid #8b5cf6;background:#fff;color:#6d28d9;border-radius:6px;font-size:11px;cursor:pointer">${en ? 'Fill draft form with this' : '用此填充草稿表单'}</button>
|
|
3295
|
+
</div>`
|
|
3296
|
+
}
|
|
3297
|
+
window.applyAiToDraft = (id) => {
|
|
3298
|
+
const suggested = (window._aiSuggest && window._aiSuggest[id]) || {}
|
|
3299
|
+
document.getElementById('df-' + id).style.display = 'block'
|
|
3300
|
+
const set = (sfx, v) => { const el = document.getElementById('df-' + sfx + '-' + id); if (el && v != null && v !== '') el.value = v }
|
|
3301
|
+
set('title', suggested.title); set('area', suggested.area); set('desc', suggested.description)
|
|
3302
|
+
set('accept', (suggested.acceptance_criteria || []).join('\n')); set('verify', (suggested.verification_commands || []).join('\n'))
|
|
3303
|
+
toast$(window._lang === 'en' ? 'Draft prefilled (review before saving)' : '已填充草稿(保存前请人工核对)')
|
|
3304
|
+
}
|
|
3305
|
+
window.createTaskDraft = async (id) => {
|
|
3306
|
+
const en = window._lang === 'en'
|
|
3307
|
+
const v = (sfx) => (document.getElementById('df-' + sfx + '-' + id)?.value || '').trim()
|
|
3308
|
+
const lines = (sfx) => v(sfx).split('\n').map(x => x.trim()).filter(Boolean)
|
|
3309
|
+
const body = {
|
|
3310
|
+
title: v('title'), area: v('area') || null, source_ref: v('source') || null, description: v('desc'),
|
|
3311
|
+
allowed_paths: lines('allowed'), forbidden_paths: lines('fpaths'), forbidden_actions: lines('forbidden'),
|
|
3312
|
+
acceptance_criteria: lines('accept'), verification_commands: lines('verify'), deliverables: lines('deliver'),
|
|
3313
|
+
definition_of_done: v('dod'), expected_results: v('expect'),
|
|
3314
|
+
}
|
|
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 }
|
|
3317
|
+
if (r.error) { toast$((r.missing && r.missing.length) ? ((en ? 'Missing: ' : '缺少:') + r.missing.join(', ')) : (r.error || 'failed')); return }
|
|
3318
|
+
toast$(en ? 'Draft saved (unpublished)' : '草稿已保存(未发布)')
|
|
3319
|
+
renderAdminTaskProposals(document.getElementById('app'))
|
|
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
|
+
|
|
3351
|
+
window.publishDraft = async (taskId) => {
|
|
3352
|
+
const en = window._lang === 'en'
|
|
3353
|
+
const r = await POST('/admin/build-task-drafts/' + encodeURIComponent(taskId) + '/publish', {})
|
|
3354
|
+
if (r.error) { toast$((r.missing && r.missing.length) ? ((en ? 'Fill before publish: ' : '发布前请填齐:') + r.missing.join(', ')) : (r.error || 'failed')); return }
|
|
3355
|
+
toast$(en ? 'Published to task board' : '已发布到任务板')
|
|
3356
|
+
renderAdminTaskProposals(document.getElementById('app'))
|
|
3357
|
+
}
|
|
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
|
+
}
|
|
3182
3801
|
|
|
3183
3802
|
async function renderAdminKPI(app) {
|
|
3184
3803
|
if (!state.user) { renderLogin(); return }
|
|
@@ -3334,22 +3953,17 @@ async function renderAdminDashboard(app) {
|
|
|
3334
3953
|
])
|
|
3335
3954
|
const tk = data.tokenomics || {}
|
|
3336
3955
|
const kpiTokenomics1 = kpiGrid([
|
|
3337
|
-
{ label: t('全球基金池'), value: Number(tk.pool_balance || 0).toFixed(2), unit: 'WAZ' },
|
|
3338
|
-
{ label: t('待结 Score'), value: Number(tk.scores_pending || 0).toFixed(0) },
|
|
3339
|
-
{ label: t('管理津贴池'), value: Number(tk.management_bonus || 0).toFixed(2), unit: 'WAZ' },
|
|
3340
|
-
])
|
|
3341
|
-
const kpiTokenomics2 = kpiGrid([
|
|
3342
3956
|
{ label: t('累计分享分润'),value: Number(tk.commission_total || 0).toFixed(2), unit: 'WAZ' },
|
|
3343
|
-
{ label: t('累计匹配发放'),value: Number(tk.binary_waz_total || 0).toFixed(2), unit: 'WAZ' },
|
|
3344
3957
|
{ label: t('PV 待处理'), value: tk.ledger_pending ?? 0 },
|
|
3958
|
+
{ label: t('参与记录用户'), value: tk.dirty_users ?? 0 },
|
|
3345
3959
|
])
|
|
3960
|
+
const kpiTokenomics2 = ''
|
|
3346
3961
|
// 异常告警 banner — 多条件聚合
|
|
3347
3962
|
const alerts = []
|
|
3348
3963
|
if ((data.active_verifiers ?? 0) < 5) alerts.push({ icon: '⚠️', color: '#dc2626', text: t('活跃审核员不足 5 人 — 请尽快批准申请'), href: '#admin/verifier-applications' })
|
|
3349
3964
|
if ((data.disputes_open ?? 0) > 10) alerts.push({ icon: '⚖️', color: '#dc2626', text: t('待处理争议') + ' > 10:' + data.disputes_open, href: '#admin/disputes' })
|
|
3350
3965
|
if ((data.verifier_apps_pending ?? 0) > 5) alerts.push({ icon: '📥', color: '#d97706', text: t('待审申请积压') + ': ' + data.verifier_apps_pending, href: '#admin/verifier-applications' })
|
|
3351
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' })
|
|
3352
|
-
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' })
|
|
3353
3967
|
const lowVerifierWarn = alerts.length > 0 ? `
|
|
3354
3968
|
<div style="margin-bottom:14px">
|
|
3355
3969
|
<div style="font-size:11px;color:#6b7280;margin-bottom:4px;font-weight:600">🚨 ${t('需要关注')} (${alerts.length})</div>
|
|
@@ -3382,7 +3996,12 @@ async function renderAdminDashboard(app) {
|
|
|
3382
3996
|
</div>
|
|
3383
3997
|
<div style="font-size:13px;color:#6b7280;margin:16px 0 8px">⚛ ${t('Tokenomics')}</div>
|
|
3384
3998
|
<div style="display:grid;grid-template-columns:repeat(2,1fr);gap:10px;margin-bottom:16px">
|
|
3385
|
-
${quickAction('#admin/tokenomics', '
|
|
3999
|
+
${quickAction('#admin/tokenomics', '⚙', t('协议运营 / 注册门控 / 佣金榜'))}
|
|
4000
|
+
</div>
|
|
4001
|
+
<div style="font-size:13px;color:#6b7280;margin:16px 0 8px">🔐 ${t('安全与审计')}</div>
|
|
4002
|
+
<div style="display:grid;grid-template-columns:repeat(2,1fr);gap:10px;margin-bottom:16px">
|
|
4003
|
+
${quickAction('#admin/security', '🪪', t('我的管理身份与权限'))}
|
|
4004
|
+
${quickAction('#admin/audit', '📜', t('审计日志'))}
|
|
3386
4005
|
</div>`
|
|
3387
4006
|
|
|
3388
4007
|
// A5 重设:渐变标题 + 分区标题 + 颜色块分组
|
|
@@ -3405,7 +4024,7 @@ async function renderAdminDashboard(app) {
|
|
|
3405
4024
|
${kpi4}
|
|
3406
4025
|
${sectionTitle('📥', t('卖家配额'), '#d97706')}
|
|
3407
4026
|
${kpi5}
|
|
3408
|
-
${sectionTitle('
|
|
4027
|
+
${sectionTitle('⚙', t('协议运营'), '#9333ea')}
|
|
3409
4028
|
${kpiTokenomics1}
|
|
3410
4029
|
${kpiTokenomics2}
|
|
3411
4030
|
${sectionTitle('⚡', t('快捷操作'), '#4f46e5')}
|
|
@@ -3437,6 +4056,90 @@ function renderTagBadges(tags, max = 3) {
|
|
|
3437
4056
|
}).join('') + (more > 0 ? `<span style="font-size:11px;color:#6b7280">+${more}</span>` : '')
|
|
3438
4057
|
}
|
|
3439
4058
|
|
|
4059
|
+
// 管理身份与权限自查面板(只读)。回答"我正在以什么身份/级别/权限操作?",
|
|
4060
|
+
// Passkey 责任绑定状态 + GitHub 关联 + 普通 admin vs root/破玻璃 + 经济操作审计须知。
|
|
4061
|
+
// 纯前端:数据来自 /me(state.user)+ 只读 /contribution-identity/github/me;无新后端、无经济动作。
|
|
4062
|
+
async function renderAdminSecurity(app) {
|
|
4063
|
+
if (!state.user) { renderLogin(); return }
|
|
4064
|
+
if (!isAdmin()) { app.innerHTML = shell(`<div class="alert alert-info">${t('仅限管理员')}</div>`, 'admin'); return }
|
|
4065
|
+
app.innerHTML = shell(loading$(), 'admin')
|
|
4066
|
+
const u = state.user
|
|
4067
|
+
const gid = await GET('/contribution-identity/github/me').catch(() => null)
|
|
4068
|
+
const bindings = (gid && !gid.error && Array.isArray(gid.bindings)) ? gid.bindings : []
|
|
4069
|
+
|
|
4070
|
+
const adminType = u.admin_type || 'root'
|
|
4071
|
+
const isRoot = adminType === 'root'
|
|
4072
|
+
const scope = u.admin_scope || 'global'
|
|
4073
|
+
let perms = []
|
|
4074
|
+
try { perms = isRoot ? ['all'] : JSON.parse(u.admin_permissions || '[]') } catch { perms = [] }
|
|
4075
|
+
const hasPasskey = !!u.has_passkey
|
|
4076
|
+
|
|
4077
|
+
const PERM_LABEL = () => ({ all: t('全部'), users: t('用户'), content: t('内容'), arbitration: t('仲裁'), protocol: t('协议 / 经济'), verifier_mgmt: t('审核员管理'), support: t('支持') })
|
|
4078
|
+
const permChips = (perms.length === 0)
|
|
4079
|
+
? `<span style="font-size:12px;color:#dc2626">${t('无任何权限(请联系 root 配置)')}</span>`
|
|
4080
|
+
: perms.map(p => `<span style="display:inline-block;background:#eef2ff;color:#3730a3;font-size:11px;padding:2px 8px;border-radius:99px;margin:0 4px 4px 0">${PERM_LABEL()[p] || p}</span>`).join('')
|
|
4081
|
+
|
|
4082
|
+
const row = (label, value) => `<div style="display:flex;justify-content:space-between;gap:10px;padding:7px 0;border-bottom:1px solid #f3f4f6"><span style="font-size:12px;color:#6b7280">${label}</span><span style="font-size:12px;color:#111827;text-align:right;word-break:break-all">${value}</span></div>`
|
|
4083
|
+
|
|
4084
|
+
const passkeyRow = hasPasskey
|
|
4085
|
+
? `<span style="color:#16a34a;font-weight:600">✓ ${t('已绑定')}</span>`
|
|
4086
|
+
: `<span style="color:#dc2626;font-weight:600">⚠ ${t('未绑定')}</span> <a href="#me/settings" style="color:#6366f1;font-size:11px">${t('去绑定')} →</a>`
|
|
4087
|
+
const githubRow = bindings.length > 0
|
|
4088
|
+
? bindings.map(b => `<code style="font-size:11px">github:${escHtml(String(b.github_actor_id))}</code>`).join(' ')
|
|
4089
|
+
: `<span style="color:#9ca3af">${t('未关联')}</span> <a href="#my-contributions" style="color:#6366f1;font-size:11px">${t('去认领')} →</a>`
|
|
4090
|
+
|
|
4091
|
+
app.innerHTML = shell(`
|
|
4092
|
+
${adminPageHeader('🪪', t('我的管理身份与权限'), t('你正在以此身份操作 · 只读自查'))}
|
|
4093
|
+
|
|
4094
|
+
${isRoot ? `
|
|
4095
|
+
<div class="card" style="padding:12px;background:#fffbeb;border:1px solid #fcd34d;margin-bottom:10px">
|
|
4096
|
+
<div style="font-size:13px;font-weight:700;color:#92400e">🚧 ${t('创始人 / 引导管理员(Founder Admin · Bootstrap Operator)')}</div>
|
|
4097
|
+
<div style="font-size:12px;color:#78350f;margin-top:4px;line-height:1.6">${t('这是 pre-launch 引导期的【临时治理模式】:更广的只读可见性 + 有限的应急写权限 —— 不是日常全能账号。')}</div>
|
|
4098
|
+
<div style="font-size:11px;color:#78350f;margin-top:6px;line-height:1.6">${t('设计目标:launch 后把创始人权力拆成更窄的角色 —— maintainer / support operator / arbitrator / finance reviewer / security admin(用 regional admin + 权限位逐步收窄)。')}</div>
|
|
4099
|
+
</div>` : ''}
|
|
4100
|
+
|
|
4101
|
+
<div class="card" style="padding:14px">
|
|
4102
|
+
<div style="font-size:13px;font-weight:700;margin-bottom:8px">👤 ${t('账户')}</div>
|
|
4103
|
+
${row(t('名称'), escHtml(u.name || ''))}
|
|
4104
|
+
${row(t('用户名'), '@' + escHtml(u.handle || ''))}
|
|
4105
|
+
${row(t('账户 ID'), `<code style="font-size:11px">${escHtml(u.id || '')}</code>`)}
|
|
4106
|
+
</div>
|
|
4107
|
+
|
|
4108
|
+
<div class="card" style="padding:14px">
|
|
4109
|
+
<div style="font-size:13px;font-weight:700;margin-bottom:8px">🛡 ${t('角色与级别')}</div>
|
|
4110
|
+
${row(t('角色'), t('管理员'))}
|
|
4111
|
+
${row(t('级别'), isRoot
|
|
4112
|
+
? `<span style="color:#b91c1c;font-weight:700">ROOT</span> · <span style="font-size:11px;color:#6b7280">${t('破玻璃 / 系统操作员')}</span>`
|
|
4113
|
+
: `<span style="color:#0369a1;font-weight:700">REGIONAL</span>`)}
|
|
4114
|
+
${row(t('范围'), `<code style="font-size:11px">${escHtml(scope)}</code>`)}
|
|
4115
|
+
<div style="font-size:12px;color:#6b7280;margin-top:8px;margin-bottom:4px">${t('有效权限')}</div>
|
|
4116
|
+
<div>${permChips}</div>
|
|
4117
|
+
</div>
|
|
4118
|
+
|
|
4119
|
+
<div class="card" style="padding:14px">
|
|
4120
|
+
<div style="font-size:13px;font-weight:700;margin-bottom:8px">🔐 ${t('问责绑定')}</div>
|
|
4121
|
+
${row('Passkey', passkeyRow)}
|
|
4122
|
+
${row(t('GitHub 关联'), githubRow)}
|
|
4123
|
+
<div style="font-size:11px;color:#92400e;background:#fffbeb;border:1px solid #fde68a;border-radius:6px;padding:8px 10px;margin-top:8px;line-height:1.6">
|
|
4124
|
+
${t('管理身份应绑定 Passkey(真人问责)。个人 GitHub 账号用于提交 PR;仓库所有权 / 设置由组织/管理身份治理 —— 独立审阅不应由同一人用另一账号假冒。')}
|
|
4125
|
+
</div>
|
|
4126
|
+
</div>
|
|
4127
|
+
|
|
4128
|
+
<div class="card" style="padding:14px">
|
|
4129
|
+
<div style="font-size:13px;font-weight:700;margin-bottom:8px">⚠️ ${t('操作安全须知')}</div>
|
|
4130
|
+
<div style="font-size:12px;color:#374151;line-height:1.8">
|
|
4131
|
+
• ${t('普通 admin 与 root / 破玻璃 不同:经济 / 协议级操作需 protocol 权限;按治理铁律须记入审计日志 —— 部分手动结算 / 评估入口的审计仍在补齐中。')}<br>
|
|
4132
|
+
• ${t('危险操作(封禁 / 角色 / 资金 / 协议参数)须带原因,且不可绕过争议 / 仲裁规则。')}<br>
|
|
4133
|
+
• ${t('不要在公共设备暴露 API Key;管理操作均可追溯到你的账户。')}
|
|
4134
|
+
</div>
|
|
4135
|
+
<div style="display:flex;gap:8px;margin-top:10px">
|
|
4136
|
+
<a href="#admin/audit" style="text-decoration:none"><button class="btn btn-outline btn-sm" style="font-size:12px">📜 ${t('查看审计日志')}</button></a>
|
|
4137
|
+
${isRoot ? `<a href="#admin/manage-admins" style="text-decoration:none"><button class="btn btn-outline btn-sm" style="font-size:12px">👥 ${t('管理管理员')}</button></a>` : ''}
|
|
4138
|
+
</div>
|
|
4139
|
+
</div>
|
|
4140
|
+
`, 'admin')
|
|
4141
|
+
}
|
|
4142
|
+
|
|
3440
4143
|
async function renderAdminUsers(app, opts = {}) {
|
|
3441
4144
|
if (!state.user) { renderLogin(); return }
|
|
3442
4145
|
if (!isAdmin()) { app.innerHTML = shell(`<div class="alert alert-info">${t('仅限管理员')}</div>`, 'admin-users'); return }
|
|
@@ -3673,10 +4376,6 @@ async function renderAdminUserDetail(app, userId) {
|
|
|
3673
4376
|
<div style="font-weight:600;margin-bottom:8px">🎭 ${t('角色 & 权限')}</div>
|
|
3674
4377
|
<div style="font-size:13px;margin-bottom:6px"><span style="color:#6b7280">${t('当前激活')}</span>: <strong>${b.role}</strong></div>
|
|
3675
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>
|
|
3676
|
-
<div style="font-size:13px;display:flex;justify-content:space-between;align-items:center;padding-top:8px;border-top:1px solid #f3f4f6">
|
|
3677
|
-
<span><span style="color:#6b7280">🎁 ${t('管理津贴资格')}</span>: ${b.mgmt_bonus_eligible ? `<span style="color:#16a34a">✓ ${t('已授予')}</span>` : `<span style="color:#9ca3af">${t('未授予')}</span>`}</span>
|
|
3678
|
-
<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>
|
|
3679
|
-
</div>
|
|
3680
4379
|
<div style="font-size:13px;padding-top:8px;border-top:1px solid #f3f4f6">
|
|
3681
4380
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">
|
|
3682
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>
|
|
@@ -4434,20 +5133,25 @@ async function renderApplyRewards(app) {
|
|
|
4434
5133
|
<h1 class="page-title">🎁 ${t('申请分享分润')}</h1>
|
|
4435
5134
|
|
|
4436
5135
|
<div class="card" style="margin-bottom:16px;background:#fef2f2;border-color:#fca5a5">
|
|
4437
|
-
<div style="font-weight:600;color:#991b1b;margin-bottom:6px;font-size:14px">⚠️ ${t('
|
|
5136
|
+
<div style="font-weight:600;color:#991b1b;margin-bottom:6px;font-size:14px">⚠️ ${t('本流程不是购物流程;下方"已完成订单"门槛只是分享分润的反女巫要求')}</div>
|
|
4438
5137
|
<div style="font-size:12px;color:#7f1d1d;line-height:1.6">
|
|
4439
|
-
${t('
|
|
4440
|
-
<span style="opacity:0.85">This flow is
|
|
5138
|
+
${t('你可以随时退出,不影响任何已下单或未来订单。本流程是分享分润的经济关系登记(佣金 / PV / escrow 结算规则;层级按地区配置),请仔细阅读全部条款。')}<br>
|
|
5139
|
+
<span style="opacity:0.85">This is not a shopping flow; the completed-order threshold below is only the anti-sybil requirement for share-commission. You can leave anytime without affecting any orders. This registers the share-commission economic relationship (commission / PV / escrow settlement rules; levels per region config) — please read all terms.</span>
|
|
4441
5140
|
</div>
|
|
4442
5141
|
<div style="font-size:11px;color:#7f1d1d;line-height:1.6;margin-top:8px;padding-top:8px;border-top:1px solid #fca5a5;opacity:0.9">
|
|
4443
5142
|
${t('注:本「分享分润 / rewards opt-in」仅为 commission / PV / escrow 经济关系登记,不是贡献资格,与贡献任务(#contribute/tasks)/ 我的共建 无关。RFC-002 同意书可能沿用更早措辞,以本 UI 含义为准。')}<br>
|
|
4444
5143
|
<span style="opacity:0.85">Note: this “share-commission / rewards opt-in” is only a commission / PV / escrow economic-relationship registration — NOT contribution eligibility, and unrelated to the contribution funnel (#contribute/tasks) / My contributions. RFC-002’s consent text may retain older wording; this note governs the UI meaning.</span>
|
|
4445
5144
|
</div>
|
|
5145
|
+
<div style="font-size:11px;color:#7f1d1d;line-height:1.6;margin-top:8px;padding-top:8px;border-top:1px solid #fca5a5;opacity:0.9">
|
|
5146
|
+
${t('现实性说明:佣金层级按地区合规配置生效;当前预发布期全局上限为 1 级(仅 L1)。"三级(7:2:1)"是协议最大设计,不构成对未来层级的承诺。')}<br>
|
|
5147
|
+
<span style="opacity:0.85">Reality note: commission levels follow per-region compliance config; during pre-launch a global cap of 1 level (L1 only) applies. “Three tiers (7:2:1)” is the protocol’s maximum design — not a promise of future levels.</span>
|
|
5148
|
+
</div>
|
|
4446
5149
|
</div>
|
|
4447
5150
|
|
|
4448
5151
|
<div class="card" style="margin-bottom:16px">
|
|
4449
|
-
<div style="font-size:13px;color:#6b7280;margin-bottom:10px">${t('
|
|
5152
|
+
<div style="font-size:13px;color:#6b7280;margin-bottom:10px">${t('分享分润开通门槛(只适用于分润,不适用于贡献)')}:</div>
|
|
4450
5153
|
${checklist}
|
|
5154
|
+
<div style="font-size:11px;color:#6b7280;margin-top:8px;padding-top:8px;border-top:1px solid #f3f4f6">${t('此购买门槛只适用于分享分润(经济关系登记),不适用于贡献任务或 GitHub 贡献认领 — 贡献无需购买。')}</div>
|
|
4451
5155
|
</div>
|
|
4452
5156
|
|
|
4453
5157
|
<div class="card" style="margin-bottom:16px;background:#fafafa">
|
|
@@ -4611,7 +5315,7 @@ async function renderRewardsMe(app) {
|
|
|
4611
5315
|
`}
|
|
4612
5316
|
|
|
4613
5317
|
<div style="margin-top:20px;font-size:11px;color:#9ca3af;line-height:1.6">
|
|
4614
|
-
${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>
|
|
4615
5319
|
</div>
|
|
4616
5320
|
`, 'me')
|
|
4617
5321
|
}
|
|
@@ -4678,12 +5382,12 @@ async function renderOnboarding(app, role) {
|
|
|
4678
5382
|
}
|
|
4679
5383
|
|
|
4680
5384
|
const studyDocs = [
|
|
4681
|
-
{ name: 'META-RULES-FULL.md', desc: t('10 元规则,特别是 #5 不偏袒 / #6 不滥用'), link: 'https://github.com/
|
|
4682
|
-
{ name: 'CHARTER.md §3.2 + §6', desc: t('权力边界:多签 + 修改流程'), link: 'https://github.com/
|
|
4683
|
-
{ name: 'SECURITY.md §Iron-Rule', desc: t('真人 Passkey 7 条路径'), link: 'https://github.com/
|
|
4684
|
-
{ name: 'ECONOMIC-MODEL.md §11', desc: t('经济博弈原则 + 关系层估值层'), link: 'https://github.com/
|
|
4685
|
-
{ name: 'ARBITRATION-PLAYBOOK.md', desc: t('案例决策树 + 4 种结算路径') + (role === 'arbitrator' ? ' (' + t('必读') + ')' : ''), link: 'https://github.com/
|
|
4686
|
-
{ 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' },
|
|
4687
5391
|
]
|
|
4688
5392
|
|
|
4689
5393
|
const studySection = studyDocs.map((d, i) => `
|
|
@@ -5583,16 +6287,6 @@ async function renderAdminTokenomics(app) {
|
|
|
5583
6287
|
app.innerHTML = shell(loading$(), 'admin')
|
|
5584
6288
|
const data = await GET('/admin/tokenomics')
|
|
5585
6289
|
if (data.error) { app.innerHTML = shell(alert$('error', data.error), 'admin'); return }
|
|
5586
|
-
const gf = data.global_fund || {}
|
|
5587
|
-
const mb = data.management_bonus_pool || {}
|
|
5588
|
-
|
|
5589
|
-
const tierTable = (data.tier_config || []).map(t => `
|
|
5590
|
-
<tr style="border-bottom:1px solid #f3f4f6">
|
|
5591
|
-
<td style="padding:6px 8px">tier ${t.tier}</td>
|
|
5592
|
-
<td style="padding:6px 8px;text-align:right">${Number(t.pv_threshold).toLocaleString()}</td>
|
|
5593
|
-
<td style="padding:6px 8px;text-align:right;font-weight:600">${t.score_per_hit}</td>
|
|
5594
|
-
<td style="padding:6px 8px;text-align:center">${t.active ? '✓' : '⏸'}</td>
|
|
5595
|
-
</tr>`).join('')
|
|
5596
6290
|
|
|
5597
6291
|
const commRows = (data.top_commission || []).map(r => `
|
|
5598
6292
|
<div style="display:flex;justify-content:space-between;padding:6px 12px;border-bottom:1px solid #f3f4f6;font-size:12px">
|
|
@@ -5600,104 +6294,27 @@ async function renderAdminTokenomics(app) {
|
|
|
5600
6294
|
<div style="font-weight:700;color:#059669">${Number(r.earned).toFixed(2)} WAZ</div>
|
|
5601
6295
|
</div>`).join('') || `<div style="text-align:center;color:#9ca3af;padding:12px;font-size:12px">${t('暂无数据')}</div>`
|
|
5602
6296
|
|
|
5603
|
-
const binaryRows = (data.top_binary || []).map(r => `
|
|
5604
|
-
<div style="display:flex;justify-content:space-between;padding:6px 12px;border-bottom:1px solid #f3f4f6;font-size:12px">
|
|
5605
|
-
<div>${escHtml(r.name || r.user_id)} · ${r.hits}${t('次')} · ${r.score_total}${t('分')}</div>
|
|
5606
|
-
<div style="font-weight:700;color:#0891b2">${Number(r.waz_total).toFixed(2)} WAZ</div>
|
|
5607
|
-
</div>`).join('') || `<div style="text-align:center;color:#9ca3af;padding:12px;font-size:12px">${t('暂无数据')}</div>`
|
|
5608
|
-
|
|
5609
6297
|
app.innerHTML = shell(`
|
|
5610
|
-
<h1 class="page-title"
|
|
6298
|
+
<h1 class="page-title">⚙ ${t('协议运营')}</h1>
|
|
5611
6299
|
<div style="margin-bottom:12px"><button class="btn btn-outline btn-sm" style="width:auto" onclick="navigate('#admin')">${t('返回概览')}</button></div>
|
|
5612
6300
|
<div id="tk-msg"></div>
|
|
5613
6301
|
|
|
5614
|
-
<h2 style="font-size:15px;font-weight:600;margin:16px 0 8px">💰 ${t('全球基金池')}</h2>
|
|
5615
|
-
<div class="card" style="margin-bottom:12px">
|
|
5616
|
-
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;font-size:13px">
|
|
5617
|
-
<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>
|
|
5618
|
-
<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>
|
|
5619
|
-
<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>
|
|
5620
|
-
</div>
|
|
5621
|
-
${gf.last_settled_at ? `<div style="font-size:11px;color:#9ca3af;margin-top:6px">${t('上次结算')}: ${fmtTime(gf.last_settled_at)}</div>` : ''}
|
|
5622
|
-
</div>
|
|
5623
|
-
|
|
5624
|
-
<h2 style="font-size:15px;font-weight:600;margin:16px 0 8px">🎁 ${t('管理津贴池')}</h2>
|
|
5625
|
-
<div class="card" style="margin-bottom:12px">
|
|
5626
|
-
<div style="font-size:18px;font-weight:700">${Number(mb.balance ?? 0).toFixed(2)} WAZ</div>
|
|
5627
|
-
<div style="font-size:11px;color:#9ca3af;margin-top:4px">${t('来自协议费 50%,用于大博主匹配团队 10/5/2% 补贴')}</div>
|
|
5628
|
-
<div id="mgmt-bonus-control" style="margin-top:12px;padding:10px;background:#fef9c3;border:1px solid #fde047;border-radius:6px">
|
|
5629
|
-
<div style="font-size:12px;font-weight:600;margin-bottom:6px">⚠️ ${t('津贴门控')}</div>
|
|
5630
|
-
<div id="mgmt-bonus-status" style="font-size:11px;color:#92400e;margin-bottom:8px">${t('加载中...')}</div>
|
|
5631
|
-
<button class="btn btn-outline btn-sm" style="font-size:11px" onclick="loadMgmtBonusStatus()">${t('查看资格用户 + 切换开关')}</button>
|
|
5632
|
-
</div>
|
|
5633
|
-
</div>
|
|
5634
|
-
|
|
5635
6302
|
<h2 style="font-size:15px;font-weight:600;margin:16px 0 8px">🚪 ${t('注册门控')}</h2>
|
|
5636
6303
|
<div class="card" style="margin-bottom:12px">
|
|
5637
6304
|
<div id="require-ref-status" style="font-size:12px;color:#6b7280">${t('加载中...')}</div>
|
|
5638
6305
|
<div style="font-size:11px;color:#9ca3af;margin-top:6px">${t('开启后:无邀请码不能注册(admin/物流/仲裁/审核员 与 region=china 豁免)')}</div>
|
|
5639
6306
|
</div>
|
|
5640
6307
|
|
|
5641
|
-
<h2 style="font-size:15px;font-weight:600;margin:16px 0 8px"
|
|
5642
|
-
<div class="card" style="margin-bottom:12px">
|
|
5643
|
-
<div id="invite-rotation-status" style="font-size:12px;color:#6b7280">${t('加载中...')}</div>
|
|
5644
|
-
<div style="font-size:11px;color:#9ca3af;margin-top:6px">${t('开启后:注册页"获取邀请码"按钮可用,依次轮询 xiaohua / mian / holden / jiayi / qingliang 5 位用户的 permanent_code')}</div>
|
|
5645
|
-
</div>
|
|
5646
|
-
|
|
5647
|
-
<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>
|
|
5648
6309
|
<div class="card" style="margin-bottom:12px">
|
|
5649
|
-
<
|
|
5650
|
-
<button class="btn btn-outline btn-sm" style="font-size:11px" onclick="doTkProcess()">${t('处理 PV 流水')}</button>
|
|
5651
|
-
<button class="btn btn-outline btn-sm" style="font-size:11px" onclick="doTkSettle()">${t('触发匹配结算')}</button>
|
|
5652
|
-
<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>
|
|
5653
|
-
</div>
|
|
6310
|
+
<button class="btn btn-outline btn-sm" style="font-size:11px;margin-bottom:8px" onclick="doTkProcess()">${t('处理 PV 流水')}</button>
|
|
5654
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>
|
|
5655
6312
|
</div>
|
|
5656
6313
|
|
|
5657
|
-
<h2 style="font-size:15px;font-weight:600;margin:16px 0 8px"
|
|
5658
|
-
<div class="card" style="margin-bottom:12px;padding:0">
|
|
5659
|
-
<table style="width:100%;border-collapse:collapse">
|
|
5660
|
-
<tr style="background:#f9fafb;font-size:11px;color:#6b7280">
|
|
5661
|
-
<th style="padding:6px 8px;text-align:left">Tier</th>
|
|
5662
|
-
<th style="padding:6px 8px;text-align:right">${t('门槛 PV')}</th>
|
|
5663
|
-
<th style="padding:6px 8px;text-align:right">Score</th>
|
|
5664
|
-
<th style="padding:6px 8px;text-align:center">${t('启用')}</th>
|
|
5665
|
-
</tr>
|
|
5666
|
-
${tierTable}
|
|
5667
|
-
</table>
|
|
5668
|
-
<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>
|
|
5669
|
-
</div>
|
|
5670
|
-
|
|
5671
|
-
<h2 style="font-size:15px;font-weight:600;margin:16px 0 8px">🏆 ${t('Top 三级佣金')} (Top 10)</h2>
|
|
6314
|
+
<h2 style="font-size:15px;font-weight:600;margin:16px 0 8px">🏆 ${t('Top 分享佣金')} (Top 10)</h2>
|
|
5672
6315
|
<div class="card" style="padding:0;margin-bottom:12px">${commRows}</div>
|
|
5673
|
-
|
|
5674
|
-
<h2 style="font-size:15px;font-weight:600;margin:16px 0 8px">🏆 ${t('Top 匹配收益')} (Top 10)</h2>
|
|
5675
|
-
<div class="card" style="padding:0;margin-bottom:12px">${binaryRows}</div>
|
|
5676
6316
|
`, 'admin')
|
|
5677
6317
|
setTimeout(loadRequireRefStatus, 100)
|
|
5678
|
-
setTimeout(loadInviteRotationStatus, 100)
|
|
5679
|
-
}
|
|
5680
|
-
|
|
5681
|
-
window.loadInviteRotationStatus = async () => {
|
|
5682
|
-
const f = await GET('/system-flags').catch(() => null)
|
|
5683
|
-
const el = document.getElementById('invite-rotation-status')
|
|
5684
|
-
if (!el) return
|
|
5685
|
-
const enabled = !!f?.invite_rotation_enabled
|
|
5686
|
-
el.innerHTML = `
|
|
5687
|
-
<div style="display:flex;justify-content:space-between;align-items:center">
|
|
5688
|
-
<strong>${t('邀请码轮询')}</strong>: ${enabled ? `<span style="color:#16a34a">🟢 ${t('已开启 — 按钮可用')}</span>` : `<span style="color:#dc2626">🔒 ${t('已关闭 — 按钮置灰')}</span>`}
|
|
5689
|
-
<button class="btn btn-outline btn-sm" style="font-size:11px" onclick="doToggleInviteRotation(${!enabled})">${enabled ? t('关闭') : t('开启')}</button>
|
|
5690
|
-
</div>`
|
|
5691
|
-
}
|
|
5692
|
-
|
|
5693
|
-
window.doToggleInviteRotation = async (enable) => {
|
|
5694
|
-
if (!confirm(enable
|
|
5695
|
-
? t('确认开启邀请码轮询?访客在注册页点按钮可申领下一个 sponsor 邀请码')
|
|
5696
|
-
: t('确认关闭邀请码轮询?按钮将置灰')
|
|
5697
|
-
)) return
|
|
5698
|
-
const res = await POST('/admin/invite-rotation/toggle', { enabled: enable })
|
|
5699
|
-
if (res.error) return alert(res.error)
|
|
5700
|
-
loadInviteRotationStatus()
|
|
5701
6318
|
}
|
|
5702
6319
|
|
|
5703
6320
|
window.loadRequireRefStatus = async () => {
|
|
@@ -5728,46 +6345,6 @@ window.doTkProcess = async () => {
|
|
|
5728
6345
|
setTimeout(() => renderAdminTokenomics(document.getElementById('app')), 800)
|
|
5729
6346
|
}
|
|
5730
6347
|
|
|
5731
|
-
window.doTkSettle = async () => {
|
|
5732
|
-
const res = await POST('/admin/atomic/run-settlement', {})
|
|
5733
|
-
const msg = document.getElementById('tk-msg')
|
|
5734
|
-
if (msg) msg.innerHTML = res.error ? alert$('error', res.error) : alert$('success', `${t('已触发匹配')}: ${res.settled}`)
|
|
5735
|
-
setTimeout(() => renderAdminTokenomics(document.getElementById('app')), 800)
|
|
5736
|
-
}
|
|
5737
|
-
|
|
5738
|
-
window.loadMgmtBonusStatus = async () => {
|
|
5739
|
-
const data = await GET('/admin/tokenomics/mgmt-bonus')
|
|
5740
|
-
if (data.error) return alert(data.error)
|
|
5741
|
-
const el = document.getElementById('mgmt-bonus-status')
|
|
5742
|
-
const userRows = (data.eligible_users || []).map(u => `
|
|
5743
|
-
<div style="display:flex;justify-content:space-between;padding:4px 6px;font-size:11px;border-bottom:1px solid #fef9c3">
|
|
5744
|
-
<div>${escHtml(u.name)} <span style="color:#9ca3af">(L1=${u.l1_count}, 累计佣金 ${Number(u.total_commission).toFixed(2)} WAZ)</span></div>
|
|
5745
|
-
<button style="font-size:10px;color:#dc2626;background:none;border:none;cursor:pointer" onclick="doMgmtBonusEligible('${u.id}',false)">${t('撤销')}</button>
|
|
5746
|
-
</div>`).join('') || `<div style="font-size:11px;color:#9ca3af;text-align:center;padding:8px">${t('暂无资格用户')}</div>`
|
|
5747
|
-
el.innerHTML = `
|
|
5748
|
-
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
|
|
5749
|
-
<span><strong>${t('全局开关')}</strong>: ${data.enabled ? `<span style="color:#16a34a">✓ ${t('已开启')}</span>` : `<span style="color:#dc2626">⊘ ${t('已关闭')}</span>`}</span>
|
|
5750
|
-
<button class="btn btn-outline btn-sm" style="font-size:11px" onclick="doMgmtBonusToggle(${!data.enabled})">${data.enabled ? t('关闭') : t('开启')}</button>
|
|
5751
|
-
</div>
|
|
5752
|
-
<div style="font-size:11px;color:#92400e;margin-bottom:6px">${t('资格用户')} (${data.eligible_count}):</div>
|
|
5753
|
-
<div style="background:#fff;border-radius:4px;max-height:200px;overflow-y:auto">${userRows}</div>
|
|
5754
|
-
<div style="font-size:10px;color:#92400e;margin-top:6px">${t('在用户详情页可授予/撤销该资格')}</div>`
|
|
5755
|
-
}
|
|
5756
|
-
|
|
5757
|
-
window.doMgmtBonusToggle = async (enable) => {
|
|
5758
|
-
if (!confirm(enable ? t('确认开启管理津贴?') : t('确认关闭管理津贴?(已发放的不回收)'))) return
|
|
5759
|
-
const res = await POST('/admin/tokenomics/mgmt-bonus/toggle', { enabled: enable })
|
|
5760
|
-
if (res.error) return alert(res.error)
|
|
5761
|
-
loadMgmtBonusStatus()
|
|
5762
|
-
}
|
|
5763
|
-
|
|
5764
|
-
window.doMgmtBonusEligible = async (userId, eligible) => {
|
|
5765
|
-
if (!confirm(eligible ? t('授予该用户管理津贴资格?') : t('撤销该用户管理津贴资格?'))) return
|
|
5766
|
-
const res = await POST(`/admin/users/${userId}/mgmt-bonus-eligible`, { eligible })
|
|
5767
|
-
if (res.error) return alert(res.error)
|
|
5768
|
-
loadMgmtBonusStatus()
|
|
5769
|
-
}
|
|
5770
|
-
|
|
5771
6348
|
window.doL1ShareOverride = async (userId, value) => {
|
|
5772
6349
|
const labels = { 1: t('强制允许'), 0: t('Auto'), '-1': t('强制禁止') }
|
|
5773
6350
|
if (!confirm(`${t('设置 L1 分享权限为')}: ${labels[value]}?`)) return
|
|
@@ -5776,20 +6353,6 @@ window.doL1ShareOverride = async (userId, value) => {
|
|
|
5776
6353
|
renderAdminUserDetail(document.getElementById('app'), userId)
|
|
5777
6354
|
}
|
|
5778
6355
|
|
|
5779
|
-
window.doMgmtBonusEligibleDetail = async (userId, eligible) => {
|
|
5780
|
-
if (!confirm(eligible ? t('授予该用户管理津贴资格?') : t('撤销该用户管理津贴资格?'))) return
|
|
5781
|
-
const res = await POST(`/admin/users/${userId}/mgmt-bonus-eligible`, { eligible })
|
|
5782
|
-
if (res.error) { showUserOpMsg(alert$('error', res.error)); return }
|
|
5783
|
-
renderAdminUserDetail(document.getElementById('app'), userId)
|
|
5784
|
-
}
|
|
5785
|
-
|
|
5786
|
-
window.doTkDistribute = async () => {
|
|
5787
|
-
const res = await POST('/admin/atomic/distribute', {})
|
|
5788
|
-
const msg = document.getElementById('tk-msg')
|
|
5789
|
-
if (msg) msg.innerHTML = res.error ? alert$('error', res.error) : alert$('success', `${t('已分发 WAZ')}: ${Number(res.distributed||0).toFixed(2)}`)
|
|
5790
|
-
setTimeout(() => renderAdminTokenomics(document.getElementById('app')), 800)
|
|
5791
|
-
}
|
|
5792
|
-
|
|
5793
6356
|
async function renderAdminAudit(app) {
|
|
5794
6357
|
if (!state.user) { renderLogin(); return }
|
|
5795
6358
|
if (!isAdmin()) { app.innerHTML = shell(`<div class="alert alert-info">${t('仅限管理员')}</div>`, 'admin-audit'); return }
|
|
@@ -5961,6 +6524,9 @@ function renderLogin() {
|
|
|
5961
6524
|
function renderWelcome(app) {
|
|
5962
6525
|
const en = window._lang === 'en'
|
|
5963
6526
|
const T = (zh, e) => en ? e : zh
|
|
6527
|
+
// 创始白皮书(canonical founding doc)— 跟随界面语言指向 webaz.xyz 公开静态页(匿名可达,不走私有 GitHub)
|
|
6528
|
+
// 这两个路径由 scripts/build-whitepaper-html.ts 从 docs/WHITEPAPER*.md 生成,经 express.static 公开服务
|
|
6529
|
+
const WP_URL = en ? '/whitepaper/en' : '/whitepaper/zh-CN'
|
|
5964
6530
|
// 2026-05-26 排版品控 — 区块标题 / 副标题 改为 inline style,避免 CSS 级联 / @media 覆写
|
|
5965
6531
|
// 最低线:h2 ≥ 24px(手机) / 32px(桌面);sub ≥ 16px
|
|
5966
6532
|
// ⚠️ 不用 calc(),避免 +/- 两侧空格被压掉导致整条 font-size 失效
|
|
@@ -6129,6 +6695,7 @@ function renderWelcome(app) {
|
|
|
6129
6695
|
<p style="font-size:clamp(17px,2.6vh,20px);color:#71717A;line-height:${en ? '1.6' : '1.75'};margin:0;max-width:65ch;margin-left:auto;margin-right:auto">
|
|
6130
6696
|
${T('买家、卖家、创作者三位一体的去中心化商业协议', 'A decentralized commerce protocol where buyers, sellers, and creators are one.')}
|
|
6131
6697
|
</p>
|
|
6698
|
+
<div style="margin-top:28px"><a href="${WP_URL}" target="_blank" rel="noopener" style="font-size:15px;color:#6366f1;text-decoration:none;font-weight:500">${T('📖 阅读创始白皮书:我参与,故我被看见 →', '📖 Read the founding whitepaper: I participate, therefore I am seen →')}</a></div>
|
|
6132
6699
|
</section>
|
|
6133
6700
|
|
|
6134
6701
|
<!-- 区块 1: Buyers -->
|
|
@@ -6287,76 +6854,58 @@ function renderWelcome(app) {
|
|
|
6287
6854
|
<section class="w-section" id="w-join-section">
|
|
6288
6855
|
<h2 style="${H2_STYLE}">${T('成为 webazer', 'Become a webazer')}</h2>
|
|
6289
6856
|
<p style="${SUB_STYLE}">${T('无论你从哪里来,你已经在路上了。', "Wherever you come from, you're already on the way.")}</p>
|
|
6290
|
-
<!--
|
|
6857
|
+
<!-- 三组意图:① 立即开始(注册/贡献) ② 保持联系(邮箱+社区) ③ 有想法(反馈/提任务) — 6 卡精简为 3,功能零丢失 -->
|
|
6291
6858
|
<div style="display:flex;flex-direction:column;gap:14px">
|
|
6292
|
-
|
|
6293
|
-
<div style="margin-bottom:14px">
|
|
6294
|
-
<div class="w-card-title">📧 ${T('申请加入创世团(深度参与)', 'Apply to join the Genesis Cohort (deep dive)')}</div>
|
|
6295
|
-
<div class="w-card-desc">${T('留下邮箱 + 期望身份 · 上线第一时间联系你,我们会主动跟进', "Leave your email + desired role · we'll reach out at launch and follow up")}</div>
|
|
6296
|
-
</div>
|
|
6297
|
-
<div style="display:flex;flex-direction:column;gap:10px">
|
|
6298
|
-
<input id="w-email" class="w-input" type="email" placeholder="your@email.com" autocomplete="email" style="margin-bottom:0">
|
|
6299
|
-
<!-- honeypot -->
|
|
6300
|
-
<input id="w-email-hp" name="website" autocomplete="off" tabindex="-1" style="display:none" aria-hidden="true">
|
|
6301
|
-
<select id="w-role-pref" class="w-input" style="margin-bottom:0">
|
|
6302
|
-
<option value="">${T('我想以什么身份开始?(可选)', 'How would you like to start? (optional)')}</option>
|
|
6303
|
-
<option value="buyer">${T('买家 · Agent 帮我找货', 'Buyer · let agents find me deals')}</option>
|
|
6304
|
-
<option value="seller">${T('卖家 · 上架商品并获取 Earn-Back', 'Seller · list products and earn back')}</option>
|
|
6305
|
-
<option value="creator">${T('分享者 · 测评、笔记、内容归因', 'Sharer · reviews, notes, content attribution')}</option>
|
|
6306
|
-
<option value="verifier">${T('审核员 · 链接 / 内容验证', 'Verifier · link & content verification')}</option>
|
|
6307
|
-
<option value="arbitrator">${T('仲裁员 · 争议裁决', 'Arbitrator · dispute resolution')}</option>
|
|
6308
|
-
<option value="other">${T('其他 / 都看看', 'Other / just curious')}</option>
|
|
6309
|
-
</select>
|
|
6310
|
-
<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>
|
|
6311
|
-
<button class="w-btn-full w-btn-primary" onclick="submitWelcomeEmail()" style="padding:12px 20px">${T('申请加入', 'Submit application')}</button>
|
|
6312
|
-
<div id="w-email-msg" style="font-size:12px;text-align:center;min-height:1.5em"></div>
|
|
6313
|
-
</div>
|
|
6314
|
-
</div>
|
|
6859
|
+
<!-- ① 立即开始 — 主转化:注册 + 公开任务板(含贡献记录诚实披露) -->
|
|
6315
6860
|
<div class="w-card w-join-card">
|
|
6316
6861
|
<div class="w-join-card-left">
|
|
6317
|
-
<div class="w-card-title"
|
|
6318
|
-
<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>
|
|
6319
6864
|
</div>
|
|
6320
6865
|
<div class="w-join-card-right">
|
|
6866
|
+
<button class="w-btn-full w-btn-primary" onclick="openAuthSheet('reg')">${T('立即注册', 'Sign Up Now')}</button>
|
|
6321
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>
|
|
6322
|
-
<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>
|
|
6323
6868
|
</div>
|
|
6324
6869
|
</div>
|
|
6325
|
-
|
|
6326
|
-
|
|
6327
|
-
|
|
6328
|
-
<div class="w-card-
|
|
6329
|
-
|
|
6330
|
-
<div class="w-join-card-right">
|
|
6331
|
-
<button class="w-btn-full w-btn-outline" onclick="openIdeaSheet()">${T('我有建议', 'I have an idea')}</button>
|
|
6332
|
-
</div>
|
|
6333
|
-
</div>
|
|
6334
|
-
<div class="w-card w-join-card">
|
|
6335
|
-
<div class="w-join-card-left">
|
|
6336
|
-
<div class="w-card-title">💬 ${T('加入社区', 'Join the community')}</div>
|
|
6337
|
-
<div class="w-card-desc">${T('找到同路人,开始对话', 'Find peers, start the conversation')}</div>
|
|
6338
|
-
</div>
|
|
6339
|
-
<div class="w-join-card-right">
|
|
6340
|
-
<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>
|
|
6341
|
-
<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>
|
|
6342
|
-
</div>
|
|
6343
|
-
</div>
|
|
6344
|
-
<div class="w-card w-join-card">
|
|
6345
|
-
<div class="w-join-card-left">
|
|
6346
|
-
<div class="w-card-title">📧 ${T('邮件联系', 'Email us')}</div>
|
|
6347
|
-
<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>
|
|
6348
6875
|
</div>
|
|
6349
|
-
<div
|
|
6350
|
-
<
|
|
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>
|
|
6351
6898
|
</div>
|
|
6352
6899
|
</div>
|
|
6900
|
+
<!-- ③ 有想法 — 匿名一句话反馈 + 提具体任务 -->
|
|
6353
6901
|
<div class="w-card w-join-card">
|
|
6354
6902
|
<div class="w-join-card-left">
|
|
6355
|
-
<div class="w-card-title"
|
|
6356
|
-
<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>
|
|
6357
6905
|
</div>
|
|
6358
6906
|
<div class="w-join-card-right">
|
|
6359
|
-
<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>
|
|
6360
6909
|
</div>
|
|
6361
6910
|
</div>
|
|
6362
6911
|
</div>
|
|
@@ -6367,9 +6916,9 @@ function renderWelcome(app) {
|
|
|
6367
6916
|
<div>© 2026 webaz</div>
|
|
6368
6917
|
<div>${T('开放协议 · Agent 原生 · DAO 治理', 'Open Protocol · Agent-Native · DAO Governance')}</div>
|
|
6369
6918
|
<div style="margin-top:10px">
|
|
6370
|
-
<a href="https://github.com/
|
|
6371
|
-
<a href="https://github.com/
|
|
6372
|
-
<a href="
|
|
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>
|
|
6921
|
+
<a href="${WP_URL}" target="_blank" rel="noopener">${T('协议白皮书', 'Whitepaper')}</a>
|
|
6373
6922
|
<a href="mailto:contact@webaz.xyz">📧 contact@webaz.xyz</a>
|
|
6374
6923
|
</div>
|
|
6375
6924
|
</footer>
|
|
@@ -6429,7 +6978,7 @@ function renderWelcome(app) {
|
|
|
6429
6978
|
alternateName: 'WebAZ',
|
|
6430
6979
|
url: location.origin,
|
|
6431
6980
|
description: 'Agent-native decentralized commerce protocol. State-machine transactions; explicit sign-offs at each transition; auto-timeout-default. Pre-launch.',
|
|
6432
|
-
sameAs: ['https://github.com/
|
|
6981
|
+
sameAs: ['https://github.com/webaz-protocol/webaz'],
|
|
6433
6982
|
},
|
|
6434
6983
|
{
|
|
6435
6984
|
'@type': 'Service',
|
|
@@ -6506,8 +7055,8 @@ async function renderGovernanceOnboarding(app) {
|
|
|
6506
7055
|
<section style="border-top:1px solid #e4e4e7;padding-top:20px;color:#71717a;font-size:13px;line-height:1.7">
|
|
6507
7056
|
<p style="margin:0 0 8px"><strong>${T('完整规范', 'Full spec')}:</strong></p>
|
|
6508
7057
|
<ul style="margin:0;padding-left:20px">
|
|
6509
|
-
<li><a href="https://github.com/
|
|
6510
|
-
<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>
|
|
6511
7060
|
<li>${T('机读 JSON 端点', 'Machine-readable JSON endpoint')}: <code style="background:#f4f4f5;padding:2px 6px;border-radius:4px">/api/governance/onboarding-stats</code></li>
|
|
6512
7061
|
</ul>
|
|
6513
7062
|
</section>
|
|
@@ -6715,13 +7264,37 @@ window.submitContributeProposal = async () => {
|
|
|
6715
7264
|
|
|
6716
7265
|
const CONTRIBUTE_PAGE_STYLE = `max-width:880px;margin:0 auto;padding:24px 16px 80px;font-family:-apple-system,BlinkMacSystemFont,'SF Pro Text','PingFang SC','Microsoft YaHei UI',sans-serif`
|
|
6717
7266
|
|
|
7267
|
+
window.contributeSetLang = (lang) => {
|
|
7268
|
+
if (lang !== 'zh' && lang !== 'en') return
|
|
7269
|
+
setLang(lang)
|
|
7270
|
+
document.getElementById('html-root')?.setAttribute('lang', lang === 'en' ? 'en' : 'zh-CN')
|
|
7271
|
+
route(true)
|
|
7272
|
+
}
|
|
7273
|
+
|
|
7274
|
+
function contributeLangSwitchHTML(T) {
|
|
7275
|
+
const en = window._lang === 'en'
|
|
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')
|
|
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">
|
|
7282
|
+
<a href="${home}" style="color:#52525B;text-decoration:none;font-size:13px;font-weight:700">← ${homeLabel}</a>
|
|
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">
|
|
7284
|
+
${btn('zh', '中文', !en)}${btn('en', 'EN', en)}
|
|
7285
|
+
</div>
|
|
7286
|
+
</div>`
|
|
7287
|
+
}
|
|
7288
|
+
|
|
7289
|
+
function contributePageShell(T, inner) {
|
|
7290
|
+
return `${preLaunchBannerHTML()}<div style="${CONTRIBUTE_PAGE_STYLE}">${contributeLangSwitchHTML(T)}${inner}</div>`
|
|
7291
|
+
}
|
|
7292
|
+
|
|
6718
7293
|
async function renderContributeTasks(app) {
|
|
6719
7294
|
const en = window._lang === 'en'
|
|
6720
7295
|
const T = (zh, e) => en && e ? e : zh
|
|
6721
7296
|
const q = state._urlQuery || {}
|
|
6722
|
-
app.innerHTML = `
|
|
6723
|
-
${preLaunchBannerHTML()}
|
|
6724
|
-
<div style="${CONTRIBUTE_PAGE_STYLE}">
|
|
7297
|
+
app.innerHTML = contributePageShell(T, `
|
|
6725
7298
|
<header style="margin-bottom:20px">
|
|
6726
7299
|
<h1 style="font-size:clamp(24px,5vw,32px);margin:0 0 8px;color:#18181B">🛠️ ${T('公开任务板', 'Open Task Board')}</h1>
|
|
6727
7300
|
<p style="color:#52525B;font-size:15px;margin:0;line-height:1.6">${T('面向任何人和他们的 agent 的公开共建任务。只读取公开任务;认领需登录。', 'Open contribution tasks for anyone and their agents. Read-only here; claiming requires login.')}</p>
|
|
@@ -6752,7 +7325,7 @@ async function renderContributeTasks(app) {
|
|
|
6752
7325
|
<button onclick="location.hash='#contribute/tasks/suggest'" style="padding:8px 16px;background:#fff;color:#18181B;border:1px solid #d4d4d8;border-radius:8px;font-size:13px;cursor:pointer">💡 ${T('建议新任务', 'Suggest a task')}</button>
|
|
6753
7326
|
</div>
|
|
6754
7327
|
<div id="ct-list"><div style="color:#a1a1aa;text-align:center;padding:30px">${T('加载中…', 'Loading…')}</div></div>
|
|
6755
|
-
|
|
7328
|
+
`)
|
|
6756
7329
|
|
|
6757
7330
|
const params = new URLSearchParams()
|
|
6758
7331
|
for (const k of ['area', 'risk_level', 'auto_claimable', 'agent_capabilities', 'max_duration_minutes', 'estimated_context_size', 'estimated_agent_budget']) if (q[k]) params.set(k, q[k])
|
|
@@ -6770,7 +7343,7 @@ async function renderContributeTasks(app) {
|
|
|
6770
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">
|
|
6771
7344
|
<div style="display:flex;justify-content:space-between;gap:10px;align-items:flex-start">
|
|
6772
7345
|
<div style="font-weight:600;color:#18181B;font-size:15px">${_cEsc(task.title)}</div>
|
|
6773
|
-
${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>` : ''}
|
|
6774
7347
|
</div>
|
|
6775
7348
|
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-top:8px">
|
|
6776
7349
|
${task.area ? `<span style="font-size:11px;color:#6b7280">📂 ${_cEsc(task.area)}</span>` : ''}
|
|
@@ -6789,18 +7362,18 @@ async function renderContributeTasks(app) {
|
|
|
6789
7362
|
async function renderContributeTaskDetail(app, id) {
|
|
6790
7363
|
const en = window._lang === 'en'
|
|
6791
7364
|
const T = (zh, e) => en && e ? e : zh
|
|
6792
|
-
app.innerHTML =
|
|
7365
|
+
app.innerHTML = contributePageShell(T, `<div style="color:#a1a1aa;text-align:center;padding:40px">${T('加载中…', 'Loading…')}</div>`)
|
|
6793
7366
|
let j, res
|
|
6794
7367
|
try {
|
|
6795
7368
|
res = await fetch('/api/public/build-tasks/' + encodeURIComponent(id), { signal: AbortSignal.timeout(10000) })
|
|
6796
7369
|
j = await res.json().catch(() => ({}))
|
|
6797
7370
|
} catch (e) {
|
|
6798
|
-
app.innerHTML =
|
|
7371
|
+
app.innerHTML = contributePageShell(T, `<div style="color:#dc2626;text-align:center;padding:30px">${T('加载失败', 'Load failed')}: ${_cEsc(e.message || 'unknown')}</div>`); return
|
|
6799
7372
|
}
|
|
6800
7373
|
if (!res.ok || !j.task) {
|
|
6801
|
-
app.innerHTML =
|
|
7374
|
+
app.innerHTML = contributePageShell(T, `
|
|
6802
7375
|
<div style="color:#52525B;text-align:center;padding:30px">${T('任务不存在或非公开。', 'Task not found or not public.')}</div>
|
|
6803
|
-
<div style="text-align:center"><button onclick="location.hash='#contribute/tasks'" style="padding:8px 16px;background:#fff;border:1px solid #d4d4d8;border-radius:8px;cursor:pointer">← ${T('返回任务板', 'Back to board')}</button></div
|
|
7376
|
+
<div style="text-align:center"><button onclick="location.hash='#contribute/tasks'" style="padding:8px 16px;background:#fff;border:1px solid #d4d4d8;border-radius:8px;cursor:pointer">← ${T('返回任务板', 'Back to board')}</button></div>`); return
|
|
6804
7377
|
}
|
|
6805
7378
|
const task = j.task
|
|
6806
7379
|
const cct = j.canonical_contribution_target || {}
|
|
@@ -6808,20 +7381,20 @@ async function renderContributeTaskDetail(app, id) {
|
|
|
6808
7381
|
CONTRIBUTE_PROMPT_STATE.text = buildContributeAgentPrompt(task, cct, T)
|
|
6809
7382
|
const section = (icon, title, inner) => `<section style="background:#fff;border:1px solid #e4e4e7;border-radius:12px;padding:18px;margin-bottom:14px">
|
|
6810
7383
|
<h3 style="margin:0 0 10px;color:#18181B;font-size:16px">${icon} ${title}</h3>${inner}</section>`
|
|
6811
|
-
const claimBtn = m.auto_claimable
|
|
7384
|
+
const claimBtn = m.claimability === 'auto_claimable'
|
|
6812
7385
|
? (state.apiKey
|
|
6813
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>`
|
|
6814
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>`)
|
|
6815
7388
|
: `<div style="font-size:12px;color:#6b7280">${T('此任务需人工认领流程,不可自动认领。', 'This task uses a manual claim flow; it is not auto-claimable.')}</div>`
|
|
6816
7389
|
|
|
6817
|
-
app.innerHTML =
|
|
7390
|
+
app.innerHTML = contributePageShell(T, `
|
|
6818
7391
|
<button onclick="location.hash='#contribute/tasks'" style="background:none;border:none;color:#6366f1;cursor:pointer;font-size:13px;padding:0;margin-bottom:12px">← ${T('返回任务板', 'Back to board')}</button>
|
|
6819
7392
|
<h1 style="font-size:clamp(22px,4.5vw,28px);margin:0 0 8px;color:#18181B">${_cEsc(task.title)}</h1>
|
|
6820
7393
|
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:6px">
|
|
6821
7394
|
${task.area ? `<span style="font-size:12px;color:#6b7280">📂 ${_cEsc(task.area)}</span>` : ''}
|
|
6822
7395
|
${m.task_type ? `<span style="font-size:12px;color:#6b7280">🔖 ${_cEsc(m.task_type)}</span>` : ''}
|
|
6823
7396
|
${_cRiskBadge(m.risk_level, T)} ${_cDuration(m.estimated_duration, T)}
|
|
6824
|
-
${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>` : ''}
|
|
6825
7398
|
</div>
|
|
6826
7399
|
${_cBoundaryHTML(task.value_boundary || j.value_boundary, T)}
|
|
6827
7400
|
|
|
@@ -6857,7 +7430,7 @@ async function renderContributeTaskDetail(app, id) {
|
|
|
6857
7430
|
${section('✋', T('认领', 'Claim'), claimBtn)}
|
|
6858
7431
|
|
|
6859
7432
|
<div style="font-size:11px;color:#9ca3af;margin-top:8px;line-height:1.6">${T('提示:sandbox 运行或本地草稿不是正式参与,也不是贡献。只有在 canonical 仓库被合并的 PR(或被认可的 issue / task / RFC)才进入贡献记录。', 'Note: a sandbox run or local draft is not participation and is not a contribution. Only a merged PR (or recognized issue / task / RFC) on the canonical repo enters the contribution record.')}</div>
|
|
6860
|
-
|
|
7433
|
+
`)
|
|
6861
7434
|
}
|
|
6862
7435
|
|
|
6863
7436
|
async function renderContributeSuggest(app) {
|
|
@@ -6868,7 +7441,7 @@ async function renderContributeSuggest(app) {
|
|
|
6868
7441
|
${ta
|
|
6869
7442
|
? `<textarea id="${id}" placeholder="${_cEsc(ph)}" style="width:100%;box-sizing:border-box;min-height:90px;padding:8px 10px;border:1px solid #d4d4d8;border-radius:8px;font-size:13px;font-family:inherit"></textarea>`
|
|
6870
7443
|
: `<input id="${id}" placeholder="${_cEsc(ph)}" style="width:100%;box-sizing:border-box;padding:8px 10px;border:1px solid #d4d4d8;border-radius:8px;font-size:13px">`}`
|
|
6871
|
-
app.innerHTML =
|
|
7444
|
+
app.innerHTML = contributePageShell(T, `
|
|
6872
7445
|
<button onclick="location.hash='#contribute/tasks'" style="background:none;border:none;color:#6366f1;cursor:pointer;font-size:13px;padding:0;margin-bottom:12px">← ${T('返回任务板', 'Back to board')}</button>
|
|
6873
7446
|
<h1 style="font-size:clamp(22px,4.5vw,28px);margin:0 0 8px;color:#18181B">💡 ${T('建议一个任务', 'Suggest a task')}</h1>
|
|
6874
7447
|
<p style="color:#52525B;font-size:14px;margin:0 0 8px;line-height:1.6">${T('任何人都可以提建议(无需登录)。建议进入维护者收件箱待审,绝不会自动变成正式任务或出现在公开任务板。', 'Anyone can suggest (no login needed). Suggestions enter the maintainer review inbox and never auto-become tasks or appear on the public board.')}</p>
|
|
@@ -6881,7 +7454,7 @@ async function renderContributeSuggest(app) {
|
|
|
6881
7454
|
${field('cs-login', T('你的 GitHub 用户名(可选)', 'Your GitHub login (optional)'), 'octocat')}
|
|
6882
7455
|
<button onclick="submitContributeProposal()" style="margin-top:16px;padding:10px 22px;background:#6366f1;color:#fff;border:none;border-radius:8px;font-size:14px;cursor:pointer">${T('提交建议', 'Submit suggestion')}</button>
|
|
6883
7456
|
<div id="ct-suggest-result" style="margin-top:14px"></div>
|
|
6884
|
-
|
|
7457
|
+
`)
|
|
6885
7458
|
}
|
|
6886
7459
|
|
|
6887
7460
|
// The boundary constant is server-authoritative; the client mirrors the SAME frozen stance for pages that
|
|
@@ -6915,7 +7488,7 @@ window.submitWelcomeEmail = async () => {
|
|
|
6915
7488
|
const email = document.getElementById('w-email')?.value?.trim() || ''
|
|
6916
7489
|
const hp = document.getElementById('w-email-hp')?.value || '' // honeypot
|
|
6917
7490
|
const rolePref = document.getElementById('w-role-pref')?.value || ''
|
|
6918
|
-
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
|
|
6919
7492
|
const msg = document.getElementById('w-email-msg')
|
|
6920
7493
|
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { if (msg) msg.innerHTML = `<span style="color:#dc2626">${t('请输入有效邮箱')}</span>`; return }
|
|
6921
7494
|
if (msg) msg.innerHTML = `<span style="color:#6366f1">${t('提交中…')}</span>`
|
|
@@ -6928,8 +7501,8 @@ window.submitWelcomeEmail = async () => {
|
|
|
6928
7501
|
})
|
|
6929
7502
|
if (r?.error) { if (msg) msg.innerHTML = `<span style="color:#dc2626">${r.error}</span>`; return }
|
|
6930
7503
|
if (msg) msg.innerHTML = `<span style="color:#16a34a">✓ ${t('已收到,上线时第一时间通知你')}</span>`
|
|
6931
|
-
document.getElementById('w-email').value = ''
|
|
6932
|
-
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
|
|
6933
7506
|
// role-pref 保留(用户已表达过的偏好,刷新页面前都留着)
|
|
6934
7507
|
}
|
|
6935
7508
|
|
|
@@ -6963,13 +7536,21 @@ window.openParticipateSheet = (defaultTab) => {
|
|
|
6963
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">
|
|
6964
7537
|
<span>🔑 ${t('注册 / 登录')}</span><span style="font-size:13px;opacity:0.6">›</span>
|
|
6965
7538
|
</button>
|
|
6966
|
-
|
|
6967
|
-
|
|
6968
|
-
|
|
6969
|
-
|
|
6970
|
-
|
|
6971
|
-
|
|
6972
|
-
|
|
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>
|
|
6973
7554
|
</div>
|
|
6974
7555
|
`, { maxWidth: 460 })
|
|
6975
7556
|
}
|
|
@@ -7043,19 +7624,27 @@ window.openAuthSheet = (defaultTab) => {
|
|
|
7043
7624
|
</div>
|
|
7044
7625
|
|
|
7045
7626
|
<div style="margin-top:14px;text-align:center">
|
|
7046
|
-
<a href="#recover" onclick="closeSheet()" style="font-size:13px;color:#6366f1;text-decoration:none">🔑 ${t('
|
|
7627
|
+
<a href="#recover" onclick="closeSheet()" style="font-size:13px;color:#6366f1;text-decoration:none">🔑 ${t('忘记 API Key / 密码?邮箱找回并重置 →')}</a>
|
|
7047
7628
|
</div>
|
|
7048
7629
|
</div>
|
|
7049
7630
|
|
|
7050
7631
|
<div id="panel-reg" style="${defaultTab==='reg'?'':'display:none'}">
|
|
7051
7632
|
<div id="reg-gate-hint" style="display:none;background:#fef3c7;border:1px solid #fde68a;padding:10px 12px;border-radius:8px;margin-bottom:12px;font-size:13px;color:#92400e"></div>
|
|
7052
7633
|
<div class="form-group">
|
|
7053
|
-
<label class="form-label">${t('
|
|
7634
|
+
<label class="form-label">${t('找回邮箱')} <span style="color:#dc2626">*</span> <span style="font-size:11px;color:#9ca3af;font-weight:400">${t('(先验证邮箱,丢号才能找回)')}</span></label>
|
|
7054
7635
|
<div style="display:flex;gap:6px;align-items:stretch">
|
|
7055
|
-
<input class="form-control" id="inp-
|
|
7056
|
-
<button id="btn-
|
|
7636
|
+
<input class="form-control" id="inp-reg-email" type="email" placeholder="your@example.com" style="flex:1" oninput="window._onRegEmailInput && window._onRegEmailInput()">
|
|
7637
|
+
<button id="btn-reg-sendcode" type="button" onclick="doRegSendCode()" style="white-space:nowrap;padding:0 12px;background:#6366f1;color:#fff;border:none;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer">${t('发送验证码')}</button>
|
|
7638
|
+
</div>
|
|
7639
|
+
<div id="reg-code-row" class="form-group" style="display:none;margin-top:8px;margin-bottom:0">
|
|
7640
|
+
<input class="form-control" id="inp-reg-code" inputmode="numeric" maxlength="6" placeholder="${t('输入 6 位邮箱验证码')}">
|
|
7641
|
+
<div style="font-size:11px;color:#16a34a;margin-top:4px">${t('验证码已发送,请查收(含垃圾箱),10 分钟内有效')}</div>
|
|
7057
7642
|
</div>
|
|
7058
|
-
|
|
7643
|
+
</div>
|
|
7644
|
+
<div class="form-group">
|
|
7645
|
+
<label class="form-label">${t('邀请码')} <span style="color:#dc2626">*</span></label>
|
|
7646
|
+
<input class="form-control" id="inp-sponsor" placeholder="${t('陆续开放中,请期待')}" style="font-family:monospace;font-size:13px;width:100%">
|
|
7647
|
+
<div style="font-size:11px;color:#6b7280;margin-top:4px" id="sponsor-hint-msg">${t('邀请码为 6-7 位永久码;没有就联系老用户拿邀请链接')}</div>
|
|
7059
7648
|
</div>
|
|
7060
7649
|
<div class="form-group">
|
|
7061
7650
|
<label class="form-label">${t('名称 / 店铺名')}</label>
|
|
@@ -7129,32 +7718,23 @@ async function renderPromoter(app) {
|
|
|
7129
7718
|
const leftPv = Number(atomic.total_left_pv || 0)
|
|
7130
7719
|
const rightPv = Number(atomic.total_right_pv || 0)
|
|
7131
7720
|
const weak = Math.min(leftPv, rightPv)
|
|
7132
|
-
const tiersArr = atomic.tier_config || []
|
|
7133
|
-
const nextTier = tiersArr.find(x => x.pv_threshold > weak) || tiersArr[tiersArr.length - 1]
|
|
7134
|
-
const nextProgress = nextTier ? Math.min(100, (weak / nextTier.pv_threshold) * 100) : 0
|
|
7135
7721
|
|
|
7136
|
-
// ─── ① 顶部 KPI —
|
|
7137
|
-
const totalEarnings = data.earnings.grand_total
|
|
7138
|
-
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)
|
|
7139
7725
|
const growth = data.projection?.growth_rate
|
|
7140
|
-
const pending = atomic.score?.pending_score || 0
|
|
7141
7726
|
const _mlmMax = Number(state.user?.region_max_levels ?? 1)
|
|
7142
7727
|
const _kpiRestricted = _mlmMax <= 1
|
|
7143
|
-
//
|
|
7144
|
-
//
|
|
7145
|
-
|
|
7146
|
-
// commission 层级(max_levels) 与 PV 系统(pv_enabled) 分离:可单独开某辖区到 L2/L3 而 PV 仍关。
|
|
7147
|
-
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
|
|
7148
7731
|
// 紧张地区:拼装最近奖励时间线(commission + 匹配 binary 混合,按时间倒序取 5 条)
|
|
7149
7732
|
let kpiBar = ''
|
|
7150
7733
|
if (_kpiRestricted) {
|
|
7151
7734
|
const _cm = (data.recent || []).slice(0, 10).map(r => ({
|
|
7152
7735
|
ts: r.created_at, label: `L${r.level} ${t('佣金')}`, amount: Number(r.amount || 0),
|
|
7153
7736
|
}))
|
|
7154
|
-
const
|
|
7155
|
-
ts: r.created_at, label: `tier ${r.tier} ${t('发展奖')}`, amount: Number(r.waz_amount || 0),
|
|
7156
|
-
}))
|
|
7157
|
-
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)
|
|
7158
7738
|
kpiBar = `
|
|
7159
7739
|
<div class="card" style="padding:12px 14px;margin-bottom:16px">
|
|
7160
7740
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
|
|
@@ -7186,14 +7766,14 @@ async function renderPromoter(app) {
|
|
|
7186
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>
|
|
7187
7767
|
</div>
|
|
7188
7768
|
<div class="card" style="text-align:center;padding:10px 6px">
|
|
7189
|
-
<div style="font-size:11px;color:#6b7280"
|
|
7190
|
-
<div style="font-size:17px;font-weight:700;color:#
|
|
7191
|
-
<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>
|
|
7192
7772
|
</div>
|
|
7193
7773
|
<div class="card" style="text-align:center;padding:10px 6px">
|
|
7194
|
-
<div style="font-size:11px;color:#6b7280"
|
|
7195
|
-
<div style="font-size:17px;font-weight:700;color:#
|
|
7196
|
-
<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>
|
|
7197
7777
|
</div>
|
|
7198
7778
|
</div>`
|
|
7199
7779
|
}
|
|
@@ -7207,8 +7787,7 @@ async function renderPromoter(app) {
|
|
|
7207
7787
|
// 邀请短链只用 permanent_code,绝不兜底 usr_xxx;缺失时显示"暂不可用"。
|
|
7208
7788
|
const code = data.permanent_code || null
|
|
7209
7789
|
const refLinkShort = code ? `${origin}/i/${code}` : ''
|
|
7210
|
-
|
|
7211
|
-
const rightLink = code ? `${origin}/i/${code}-R` : ''
|
|
7790
|
+
// pre-public 去左右码:不再生成 -L/-R 侧链,只用唯一推荐码 refLinkShort
|
|
7212
7791
|
const inviteUnavailableHtml = `<div style="background:#fef2f2;border:1px solid #fca5a5;border-radius:10px;padding:12px;margin-bottom:12px;font-size:12px;color:#991b1b">⚠️ ${t('邀请码暂不可用,请刷新或联系支持')}</div>`
|
|
7213
7792
|
const esc = (s) => s.replace(/'/g, "\\'").replace(/"/g, '"')
|
|
7214
7793
|
|
|
@@ -7234,44 +7813,12 @@ async function renderPromoter(app) {
|
|
|
7234
7813
|
|
|
7235
7814
|
<div style="font-size:11px;line-height:1.6;color:#6b7280;margin-bottom:8px">
|
|
7236
7815
|
${data.permissions?.can_l1_share
|
|
7237
|
-
? `<span style="color:#16a34a">✅ ${t('
|
|
7238
|
-
: `<span style="color:#d97706">⏳ ${t('
|
|
7816
|
+
? `<span style="color:#16a34a">✅ ${t('已开通分享分润资格')}</span>` + (data.permissions.l1_share_override === 1 ? ` · <span style="color:#7c3aed">${t('Admin 强制授予')}</span>` : '')
|
|
7817
|
+
: `<span style="color:#d97706">⏳ ${t('分享分润待开通')}</span> · <span>${t('完成首笔购买即可')}</span>`}
|
|
7239
7818
|
${data.my_sponsor ? `<br>${t('邀请人')}: <strong>${escHtml(t(data.my_sponsor.name))}</strong>` : ''}
|
|
7240
|
-
${_pvAllowed && atomic.my_placement ? ` · ${t('挂靠')}: <strong>${escHtml(t(atomic.my_placement.name))}</strong> ${atomic.my_placement.side === 'left' ? '🔵' : '🟢'}` : ''}
|
|
7241
7819
|
· ${t('所在地区')}: ${regionLabel(data.region || 'global')}
|
|
7242
7820
|
</div>
|
|
7243
7821
|
|
|
7244
|
-
${!_pvAllowed ? '' : `
|
|
7245
|
-
<details style="margin-top:4px">
|
|
7246
|
-
<summary style="font-size:11px;color:#6366f1;cursor:pointer;padding:6px 0">⚙ ${t('左右区设置')}</summary>
|
|
7247
|
-
<div style="padding:8px 0">
|
|
7248
|
-
<div style="font-size:11px;color:#6b7280;margin-bottom:6px">${t('指定左/右轨')}</div>
|
|
7249
|
-
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:14px">
|
|
7250
|
-
<div style="background:#eff6ff;border:1px solid #bfdbfe;border-radius:8px;padding:10px">
|
|
7251
|
-
<div style="font-size:12px;font-weight:600;color:#1e40af;text-align:center;margin-bottom:8px">🔵 ${t('左区码')}</div>
|
|
7252
|
-
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px">
|
|
7253
|
-
<button onclick="copyPlacementLink('${esc(leftLink)}', 'left', this)" style="display:flex;align-items:center;justify-content:center;gap:3px;padding:7px 4px;background:#3b82f6;color:#fff;border:none;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer">📋 ${t('复制')}</button>
|
|
7254
|
-
<button onclick="showQRModal('${esc(leftLink)}','🔵 ${t('左区码')}')" style="display:flex;align-items:center;justify-content:center;gap:3px;padding:7px 4px;background:#fff;color:#3b82f6;border:1.5px solid #3b82f6;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer">📱 ${t('二维码')}</button>
|
|
7255
|
-
</div>
|
|
7256
|
-
</div>
|
|
7257
|
-
<div style="background:#f0fdf4;border:1px solid #bbf7d0;border-radius:8px;padding:10px">
|
|
7258
|
-
<div style="font-size:12px;font-weight:600;color:#166534;text-align:center;margin-bottom:8px">🟢 ${t('右区码')}</div>
|
|
7259
|
-
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px">
|
|
7260
|
-
<button onclick="copyPlacementLink('${esc(rightLink)}', 'right', this)" style="display:flex;align-items:center;justify-content:center;gap:3px;padding:7px 4px;background:#16a34a;color:#fff;border:none;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer">📋 ${t('复制')}</button>
|
|
7261
|
-
<button onclick="showQRModal('${esc(rightLink)}','🟢 ${t('右区码')}')" style="display:flex;align-items:center;justify-content:center;gap:3px;padding:7px 4px;background:#fff;color:#16a34a;border:1.5px solid #16a34a;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer">📱 ${t('二维码')}</button>
|
|
7262
|
-
</div>
|
|
7263
|
-
</div>
|
|
7264
|
-
</div>
|
|
7265
|
-
|
|
7266
|
-
<div style="font-size:11px;color:#6b7280;margin-bottom:4px">${t('自动选边偏好')}</div>
|
|
7267
|
-
<select id="placement-pref-select" class="form-control" style="font-size:12px" onchange="doSetPlacementPref()">
|
|
7268
|
-
<option value="team_count">${t('推荐少的一边(默认)')}</option>
|
|
7269
|
-
<option value="pv_count">${t('近 90 天积分少的一边')}</option>
|
|
7270
|
-
</select>
|
|
7271
|
-
<p style="font-size:11px;color:#9ca3af;margin-top:6px;line-height:1.5">${t('💡 想为这一次分享强制走左/右轨?复制上方「🔵 左区推荐码 / 🟢 右区推荐码」直接发,仅当次有效,不会改长期偏好。')}</p>
|
|
7272
|
-
<div id="placement-pref-msg" style="font-size:11px;color:#6b7280;margin-top:4px"></div>
|
|
7273
|
-
</div>
|
|
7274
|
-
</details>`}
|
|
7275
7822
|
<div style="margin-top:12px;display:flex;justify-content:flex-end">
|
|
7276
7823
|
<button class="btn btn-outline btn-sm" onclick="closeModal()">${t('关闭')}</button>
|
|
7277
7824
|
</div>`
|
|
@@ -7477,47 +8024,24 @@ async function renderPromoter(app) {
|
|
|
7477
8024
|
</div>
|
|
7478
8025
|
</details>`
|
|
7479
8026
|
|
|
7480
|
-
// ─── ⑥
|
|
7481
|
-
// 合规:max_levels ≤ 1 的地区(GCC/越南/印尼/菲律宾 + 未审计地区)
|
|
7482
|
-
// 只显示"累计推广 N 人"客观数字,全部金钱/tier/匹配话术隐藏
|
|
7483
|
-
// 复用顶部 _kpiRestricted(同一含义,避免重复定义)
|
|
8027
|
+
// ─── ⑥ 参与记录(PV)───
|
|
7484
8028
|
const _totalRecruits = Number(data.team?.l1 || 0) + Number(data.team?.l2 || 0) + Number(data.team?.l3 || 0)
|
|
7485
|
-
// 最右侧地区显示(所有模式都加)
|
|
7486
8029
|
const _userRegion = state.user?.region || 'global'
|
|
7487
8030
|
const _regionChip = `<span style="font-size:11px;color:#6b7280;white-space:nowrap;font-weight:400">${regionLabel(_userRegion)}</span>`
|
|
7488
|
-
// PV
|
|
7489
|
-
|
|
7490
|
-
const atomicSection = atomic.left_invite_url
|
|
7491
|
-
? (!_pvAllowed
|
|
8031
|
+
// 匹配奖励引擎已切除(#401):只展示中性参与记录(PV = 参与/贡献的记录,非收益、不可兑付、无权益)。
|
|
8032
|
+
const atomicSection = (_recordingOn
|
|
7492
8033
|
? `<div style="margin-bottom:12px;background:#fff;border:1px solid #e5e7eb;border-radius:8px;padding:14px">
|
|
7493
8034
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;gap:8px">
|
|
7494
|
-
<span style="font-size:14px;font-weight:600"
|
|
7495
|
-
|
|
7496
|
-
<span style="font-size:11px;color:#9ca3af">${t('协议级荣誉')}</span>
|
|
7497
|
-
${_regionChip}
|
|
7498
|
-
</div>
|
|
8035
|
+
<span style="font-size:14px;font-weight:600">📒 ${t('参与记录')}</span>
|
|
8036
|
+
${_regionChip}
|
|
7499
8037
|
</div>
|
|
7500
8038
|
<div style="display:flex;align-items:baseline;gap:8px;margin-top:6px">
|
|
7501
|
-
<span style="font-size:24px;font-weight:800;color:#
|
|
7502
|
-
<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>
|
|
7503
8041
|
</div>
|
|
7504
|
-
<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>
|
|
7505
8043
|
</div>`
|
|
7506
|
-
:
|
|
7507
|
-
<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">
|
|
7508
|
-
<span>
|
|
7509
|
-
🌟 ${t('WebAZ 发展奖')}
|
|
7510
|
-
<span style="font-weight:400;font-size:12px;color:#6b7280;margin-left:6px">
|
|
7511
|
-
${t('弱侧')} ${weak.toLocaleString()} PV · ${t('累计')} ${(atomic.score?.settled_waz || 0).toFixed(2)} WAZ
|
|
7512
|
-
</span>
|
|
7513
|
-
</span>
|
|
7514
|
-
${_regionChip}
|
|
7515
|
-
</summary>
|
|
7516
|
-
<div style="padding:0 12px 12px">
|
|
7517
|
-
${renderAtomicInner(atomic, leftPv, rightPv, weak, nextTier, nextProgress)}
|
|
7518
|
-
</div>
|
|
7519
|
-
</details>`)
|
|
7520
|
-
: ''
|
|
8044
|
+
: '')
|
|
7521
8045
|
|
|
7522
8046
|
// ─── ⑦ 实时流水(默认折叠)───
|
|
7523
8047
|
const bulldozerEvents = (data.recent || []).map(r => ({
|
|
@@ -7525,12 +8049,7 @@ async function renderPromoter(app) {
|
|
|
7525
8049
|
label: `🚜 L${r.level} ${t('佣金')} · ${escHtml(r.source_buyer_name || '—')}`,
|
|
7526
8050
|
rate: r.rate,
|
|
7527
8051
|
}))
|
|
7528
|
-
const
|
|
7529
|
-
kind: 'binary', ts: r.created_at, level: 0, amount: Number(r.waz_amount || 0),
|
|
7530
|
-
label: `⚛ tier ${r.tier} · Score ${r.score}${r.settled_at ? '' : ' (' + t('待结') + ')'}`,
|
|
7531
|
-
settled: !!r.settled_at,
|
|
7532
|
-
}))
|
|
7533
|
-
const merged = [...bulldozerEvents, ...atomicEvents]
|
|
8052
|
+
const merged = [...bulldozerEvents]
|
|
7534
8053
|
.sort((a, b) => (b.ts || '').localeCompare(a.ts || ''))
|
|
7535
8054
|
.slice(0, 30)
|
|
7536
8055
|
const cutoff = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
|
|
@@ -7670,121 +8189,28 @@ window.viewProductShares = async (productId, filter, orderId) => {
|
|
|
7670
8189
|
<p style="font-size:11px;color:#6b7280;margin-bottom:14px">${t('共')} ${items.length} ${t('条 · 点击下方按钮复制 / 打开')}</p>
|
|
7671
8190
|
${rowsHtml}
|
|
7672
8191
|
<div style="margin-top:8px;display:flex;justify-content:flex-end">
|
|
7673
|
-
<button class="btn btn-outline btn-sm" onclick="closeModal()">${t('关闭')}</button>
|
|
7674
|
-
</div>
|
|
7675
|
-
`)
|
|
7676
|
-
}
|
|
7677
|
-
window.setBoughtFilter = (val) => {
|
|
7678
|
-
state._boughtFilter = val
|
|
7679
|
-
renderPromoter(document.getElementById('app'))
|
|
7680
|
-
}
|
|
7681
|
-
window.setBoughtKw = (kw) => {
|
|
7682
|
-
state._boughtKw = kw
|
|
7683
|
-
// 只重渲染列表部分而不整页刷新(保持 input focus)
|
|
7684
|
-
clearTimeout(window._boughtKwTimer)
|
|
7685
|
-
window._boughtKwTimer = setTimeout(() => {
|
|
7686
|
-
const el = document.getElementById('bought-search')
|
|
7687
|
-
const focus = el === document.activeElement
|
|
7688
|
-
const pos = el?.selectionStart
|
|
7689
|
-
renderPromoter(document.getElementById('app'))
|
|
7690
|
-
if (focus) {
|
|
7691
|
-
const newEl = document.getElementById('bought-search')
|
|
7692
|
-
if (newEl) { newEl.focus(); if (pos != null) newEl.setSelectionRange(pos, pos) }
|
|
7693
|
-
}
|
|
7694
|
-
}, 200)
|
|
7695
|
-
}
|
|
7696
|
-
|
|
7697
|
-
// 积分匹配内部内容(折叠展开后渲染,从 renderAtomicSection 简化抽出)
|
|
7698
|
-
function renderAtomicInner(a, leftPv, rightPv, weak, nextTier, nextProgress) {
|
|
7699
|
-
const tiers = a.tier_config || []
|
|
7700
|
-
const tierTable = tiers.map(x => `
|
|
7701
|
-
<tr style="border-bottom:1px solid #f3f4f6">
|
|
7702
|
-
<td style="padding:5px 8px;font-size:12px">tier ${x.tier}</td>
|
|
7703
|
-
<td style="padding:5px 8px;font-size:12px;text-align:right">${Number(x.pv_threshold).toLocaleString()}</td>
|
|
7704
|
-
<td style="padding:5px 8px;font-size:12px;text-align:right;font-weight:600">${x.score_per_hit}</td>
|
|
7705
|
-
</tr>`).join('')
|
|
7706
|
-
|
|
7707
|
-
// 本月匹配次数 + WAZ
|
|
7708
|
-
const now = new Date()
|
|
7709
|
-
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1).toISOString().slice(0, 10)
|
|
7710
|
-
const monthBinary = (a.recent_binary || []).filter(r => (r.created_at || '').slice(0, 10) >= monthStart)
|
|
7711
|
-
const monthHits = monthBinary.length
|
|
7712
|
-
const monthWaz = monthBinary.reduce((s, r) => s + Number(r.waz_amount || 0), 0)
|
|
7713
|
-
|
|
7714
|
-
// 总 hits / 总下线(直挂左右子节点)
|
|
7715
|
-
const score = a.score || {}
|
|
7716
|
-
const totalHits = score.total_hits || 0
|
|
7717
|
-
|
|
7718
|
-
const recentRows = (a.recent_binary || []).length
|
|
7719
|
-
? a.recent_binary.map(r => `
|
|
7720
|
-
<div style="display:flex;justify-content:space-between;padding:6px 0;border-bottom:1px solid #f3f4f6;font-size:12px">
|
|
7721
|
-
<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>
|
|
7722
|
-
<div style="color:#9ca3af">${fmtTime(r.created_at)}</div>
|
|
7723
|
-
</div>`).join('')
|
|
7724
|
-
: `<div style="text-align:center;color:#9ca3af;padding:10px;font-size:12px">${t('暂无匹配记录')}</div>`
|
|
7725
|
-
|
|
7726
|
-
return `
|
|
7727
|
-
<!-- 我的位置 -->
|
|
7728
|
-
${a.my_placement ? `
|
|
7729
|
-
<div style="background:#f5f3ff;border:1px solid #e9d5ff;border-radius:6px;padding:8px 10px;font-size:12px;color:#6b21a8;margin-bottom:10px">
|
|
7730
|
-
📍 ${t('我挂位置')}: <strong>${escHtml(a.my_placement.name)}</strong> 的 ${a.my_placement.side === 'left' ? '🔵 左侧' : '🟢 右侧'}
|
|
7731
|
-
</div>` : `
|
|
7732
|
-
<div style="background:#f9fafb;border:1px solid #e5e7eb;border-radius:6px;padding:8px 10px;font-size:12px;color:#9ca3af;margin-bottom:10px">
|
|
7733
|
-
${t('你还没加入任何上级的积分树(独立根节点)')}
|
|
7734
|
-
</div>`}
|
|
7735
|
-
|
|
7736
|
-
<!-- 左/右 PV -->
|
|
7737
|
-
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px">
|
|
7738
|
-
<div style="text-align:center;background:#eff6ff;border-radius:6px;padding:10px">
|
|
7739
|
-
<div style="font-size:11px;color:#6b7280">🔵 ${t('左区 PV')}</div>
|
|
7740
|
-
<div style="font-size:18px;font-weight:700;color:#1e40af">${leftPv.toLocaleString()}</div>
|
|
7741
|
-
<div style="font-size:10px;color:#9ca3af">${t('直挂')}: ${a.left_child ? escHtml(a.left_child.name) : '—'}</div>
|
|
7742
|
-
</div>
|
|
7743
|
-
<div style="text-align:center;background:#f0fdf4;border-radius:6px;padding:10px">
|
|
7744
|
-
<div style="font-size:11px;color:#6b7280">🟢 ${t('右区 PV')}</div>
|
|
7745
|
-
<div style="font-size:18px;font-weight:700;color:#15803d">${rightPv.toLocaleString()}</div>
|
|
7746
|
-
<div style="font-size:10px;color:#9ca3af">${t('直挂')}: ${a.right_child ? escHtml(a.right_child.name) : '—'}</div>
|
|
7747
|
-
</div>
|
|
7748
|
-
</div>
|
|
7749
|
-
|
|
7750
|
-
<!-- 本月匹配统计 + 总匹配 -->
|
|
7751
|
-
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:6px;margin-bottom:10px">
|
|
7752
|
-
<div style="text-align:center;background:#fef3c7;border-radius:6px;padding:8px 4px">
|
|
7753
|
-
<div style="font-size:10px;color:#92400e">${t('本月匹配次数')}</div>
|
|
7754
|
-
<div style="font-size:14px;font-weight:700;color:#78350f">${monthHits}</div>
|
|
7755
|
-
</div>
|
|
7756
|
-
<div style="text-align:center;background:#dcfce7;border-radius:6px;padding:8px 4px">
|
|
7757
|
-
<div style="font-size:10px;color:#166534">${t('本月匹配 WAZ')}</div>
|
|
7758
|
-
<div style="font-size:14px;font-weight:700;color:#14532d">${monthWaz.toFixed(2)}</div>
|
|
7759
|
-
</div>
|
|
7760
|
-
<div style="text-align:center;background:#eef2ff;border-radius:6px;padding:8px 4px">
|
|
7761
|
-
<div style="font-size:10px;color:#4338ca">${t('累计匹配次数')}</div>
|
|
7762
|
-
<div style="font-size:14px;font-weight:700;color:#3730a3">${totalHits}</div>
|
|
7763
|
-
</div>
|
|
7764
|
-
</div>
|
|
7765
|
-
|
|
7766
|
-
${renderBinaryTree(a.binary_tree)}
|
|
7767
|
-
|
|
7768
|
-
<div style="font-size:12px;color:#6b7280;margin-bottom:6px">${t('弱侧匹配量')}: <strong>${weak.toLocaleString()}</strong> PV</div>
|
|
7769
|
-
${nextTier ? `
|
|
7770
|
-
<div style="font-size:11px;color:#6b7280;margin-bottom:4px">${t('距离 tier')} ${nextTier.tier} ${t('门槛')} ${Number(nextTier.pv_threshold).toLocaleString()} PV (+${nextTier.score_per_hit} Score)</div>
|
|
7771
|
-
<div style="background:#f3f4f6;height:6px;border-radius:3px;overflow:hidden;margin-bottom:10px">
|
|
7772
|
-
<div style="background:#6366f1;height:6px;width:${nextProgress.toFixed(1)}%"></div>
|
|
7773
|
-
</div>` : ''}
|
|
7774
|
-
|
|
7775
|
-
<details style="margin-bottom:10px">
|
|
7776
|
-
<summary style="font-size:12px;color:#6366f1;cursor:pointer">${t('匹配档位表')}</summary>
|
|
7777
|
-
<table style="width:100%;margin-top:6px;border-collapse:collapse">
|
|
7778
|
-
<tr style="background:#f9fafb;font-size:11px;color:#6b7280">
|
|
7779
|
-
<th style="padding:5px 8px;text-align:left">Tier</th>
|
|
7780
|
-
<th style="padding:5px 8px;text-align:right">${t('门槛 PV')}</th>
|
|
7781
|
-
<th style="padding:5px 8px;text-align:right">${t('Score / 次')}</th>
|
|
7782
|
-
</tr>
|
|
7783
|
-
${tierTable}
|
|
7784
|
-
</table>
|
|
7785
|
-
</details>
|
|
7786
|
-
<h4 style="font-size:12px;font-weight:600;margin:6px 0">📊 ${t('最近匹配')}</h4>
|
|
7787
|
-
${recentRows}`
|
|
8192
|
+
<button class="btn btn-outline btn-sm" onclick="closeModal()">${t('关闭')}</button>
|
|
8193
|
+
</div>
|
|
8194
|
+
`)
|
|
8195
|
+
}
|
|
8196
|
+
window.setBoughtFilter = (val) => {
|
|
8197
|
+
state._boughtFilter = val
|
|
8198
|
+
renderPromoter(document.getElementById('app'))
|
|
8199
|
+
}
|
|
8200
|
+
window.setBoughtKw = (kw) => {
|
|
8201
|
+
state._boughtKw = kw
|
|
8202
|
+
// 只重渲染列表部分而不整页刷新(保持 input focus)
|
|
8203
|
+
clearTimeout(window._boughtKwTimer)
|
|
8204
|
+
window._boughtKwTimer = setTimeout(() => {
|
|
8205
|
+
const el = document.getElementById('bought-search')
|
|
8206
|
+
const focus = el === document.activeElement
|
|
8207
|
+
const pos = el?.selectionStart
|
|
8208
|
+
renderPromoter(document.getElementById('app'))
|
|
8209
|
+
if (focus) {
|
|
8210
|
+
const newEl = document.getElementById('bought-search')
|
|
8211
|
+
if (newEl) { newEl.focus(); if (pos != null) newEl.setSelectionRange(pos, pos) }
|
|
8212
|
+
}
|
|
8213
|
+
}, 200)
|
|
7788
8214
|
}
|
|
7789
8215
|
|
|
7790
8216
|
// ─── 🎯 成长任务 UI(替换 insights,主线:分享达人养成)───
|
|
@@ -7921,224 +8347,6 @@ window.growthTaskAction = (action, id) => {
|
|
|
7921
8347
|
}
|
|
7922
8348
|
}
|
|
7923
8349
|
|
|
7924
|
-
function renderAtomicSection(a) {
|
|
7925
|
-
if (!a || !a.left_invite_url) return ''
|
|
7926
|
-
const leftPv = Number(a.total_left_pv || 0)
|
|
7927
|
-
const rightPv = Number(a.total_right_pv || 0)
|
|
7928
|
-
const pair = Math.min(leftPv, rightPv)
|
|
7929
|
-
const tiers = a.tier_config || []
|
|
7930
|
-
// 下一档进度
|
|
7931
|
-
const nextTier = tiers.find(t => t.pv_threshold > pair) || tiers[tiers.length - 1]
|
|
7932
|
-
const nextProgress = nextTier ? Math.min(100, (pair / nextTier.pv_threshold) * 100) : 0
|
|
7933
|
-
|
|
7934
|
-
const tierTable = tiers.map(t => `
|
|
7935
|
-
<tr style="border-bottom:1px solid #f3f4f6">
|
|
7936
|
-
<td style="padding:6px 8px;font-size:12px">tier ${t.tier}</td>
|
|
7937
|
-
<td style="padding:6px 8px;font-size:12px;text-align:right">${Number(t.pv_threshold).toLocaleString()}</td>
|
|
7938
|
-
<td style="padding:6px 8px;font-size:12px;text-align:right;font-weight:600">${t.score_per_hit}</td>
|
|
7939
|
-
</tr>`).join('')
|
|
7940
|
-
|
|
7941
|
-
const recentRows = (a.recent_binary || []).length
|
|
7942
|
-
? a.recent_binary.map(r => `
|
|
7943
|
-
<div style="display:flex;justify-content:space-between;padding:6px 12px;border-bottom:1px solid #f3f4f6;font-size:12px">
|
|
7944
|
-
<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>
|
|
7945
|
-
<div style="color:#9ca3af">${fmtTime(r.created_at)}</div>
|
|
7946
|
-
</div>`).join('')
|
|
7947
|
-
: `<div style="text-align:center;color:#9ca3af;padding:16px;font-size:12px">${t('暂无匹配记录')}</div>`
|
|
7948
|
-
|
|
7949
|
-
return `
|
|
7950
|
-
<h2 style="font-size:15px;font-weight:600;margin:24px 0 8px">⚛ ${t('积分 — 积分匹配')}</h2>
|
|
7951
|
-
|
|
7952
|
-
<div class="card" style="margin-bottom:12px;background:linear-gradient(135deg,#dbeafe,#f0fdf4)">
|
|
7953
|
-
<div style="font-size:13px;color:#6b7280;margin-bottom:6px">🔗 ${t('左右码(完全对称 / 末端垂直挂靠)')}</div>
|
|
7954
|
-
<div style="font-size:11px;color:#6b7280;margin-bottom:10px">${t('点击下方任一卡片复制整条推荐链接。新人通过左/右链接注册会挂到对应区。')}</div>
|
|
7955
|
-
${(() => {
|
|
7956
|
-
// placement 短链只用 permanent_code(/i/CODE-L / -R),绝不用 usr_xxx;缺失则提示不可用
|
|
7957
|
-
const myCode = state.user?.permanent_code || null
|
|
7958
|
-
const origin = location.origin
|
|
7959
|
-
if (!myCode) return `<div style="background:#fef2f2;border:1px solid #fca5a5;border-radius:10px;padding:10px;font-size:12px;color:#991b1b">⚠️ ${t('邀请码暂不可用,请刷新或联系支持')}</div>`
|
|
7960
|
-
const leftLink = `${origin}/i/${myCode}-L`
|
|
7961
|
-
const rightLink = `${origin}/i/${myCode}-R`
|
|
7962
|
-
const esc = (s) => s.replace(/'/g, "\\'").replace(/"/g, '"')
|
|
7963
|
-
return `
|
|
7964
|
-
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
|
|
7965
|
-
<div onclick="copyPlacementLink('${esc(leftLink)}', 'left', this)" style="cursor:pointer;background:#eff6ff;border:2px solid #3b82f6;border-radius:10px;padding:10px;transition:all 0.15s" onmouseover="this.style.background='#dbeafe'" onmouseout="this.style.background='#eff6ff'">
|
|
7966
|
-
<div style="font-size:13px;font-weight:700;color:#1e40af;margin-bottom:6px">🔵 ${t('左区推荐码')}</div>
|
|
7967
|
-
<div style="font-size:10px;color:#1e40af;word-break:break-all;line-height:1.4;background:#fff;padding:6px 8px;border-radius:6px;font-family:monospace">${escHtml(leftLink)}</div>
|
|
7968
|
-
<div style="font-size:11px;color:#3b82f6;margin-top:6px;text-align:center">${t('点击复制 →')}</div>
|
|
7969
|
-
</div>
|
|
7970
|
-
<div onclick="copyPlacementLink('${esc(rightLink)}', 'right', this)" style="cursor:pointer;background:#f0fdf4;border:2px solid #16a34a;border-radius:10px;padding:10px;transition:all 0.15s" onmouseover="this.style.background='#dcfce7'" onmouseout="this.style.background='#f0fdf4'">
|
|
7971
|
-
<div style="font-size:13px;font-weight:700;color:#166534;margin-bottom:6px">🟢 ${t('右区推荐码')}</div>
|
|
7972
|
-
<div style="font-size:10px;color:#166534;word-break:break-all;line-height:1.4;background:#fff;padding:6px 8px;border-radius:6px;font-family:monospace">${escHtml(rightLink)}</div>
|
|
7973
|
-
<div style="font-size:11px;color:#16a34a;margin-top:6px;text-align:center">${t('点击复制 →')}</div>
|
|
7974
|
-
</div>
|
|
7975
|
-
</div>`
|
|
7976
|
-
})()}
|
|
7977
|
-
<details style="margin-top:10px">
|
|
7978
|
-
<summary style="font-size:11px;color:#6366f1;cursor:pointer">⚙ ${t('自动选边偏好(无 side 的链接 / 商品分享适用)')}</summary>
|
|
7979
|
-
<div style="padding:8px 0">
|
|
7980
|
-
<select id="placement-pref-select" class="form-control" style="font-size:12px" onchange="doSetPlacementPref()">
|
|
7981
|
-
<option value="team_count">${t('推荐少的一边(默认)')}</option>
|
|
7982
|
-
<option value="pv_count">${t('近 90 天积分少的一边')}</option>
|
|
7983
|
-
</select>
|
|
7984
|
-
<p style="font-size:11px;color:#9ca3af;margin-top:6px;line-height:1.5">${t('💡 想为这一次分享强制走左/右轨?复制上方「🔵 左区推荐码 / 🟢 右区推荐码」直接发,仅当次有效,不会改长期偏好。')}</p>
|
|
7985
|
-
<div id="placement-pref-msg" style="font-size:11px;color:#6b7280;margin-top:4px"></div>
|
|
7986
|
-
</div>
|
|
7987
|
-
</details>
|
|
7988
|
-
${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>` : ''}
|
|
7989
|
-
</div>
|
|
7990
|
-
|
|
7991
|
-
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:12px">
|
|
7992
|
-
<div class="card" style="text-align:center;background:#eff6ff">
|
|
7993
|
-
<div style="font-size:11px;color:#6b7280">🔵 ${t('左区 PV')}</div>
|
|
7994
|
-
<div style="font-size:20px;font-weight:700;color:#1e40af">${leftPv.toLocaleString()}</div>
|
|
7995
|
-
<div style="font-size:10px;color:#9ca3af">${t('直挂')}: ${a.left_child ? escHtml(a.left_child.name) : '—'}</div>
|
|
7996
|
-
</div>
|
|
7997
|
-
<div class="card" style="text-align:center;background:#f0fdf4">
|
|
7998
|
-
<div style="font-size:11px;color:#6b7280">🟢 ${t('右区 PV')}</div>
|
|
7999
|
-
<div style="font-size:20px;font-weight:700;color:#15803d">${rightPv.toLocaleString()}</div>
|
|
8000
|
-
<div style="font-size:10px;color:#9ca3af">${t('直挂')}: ${a.right_child ? escHtml(a.right_child.name) : '—'}</div>
|
|
8001
|
-
</div>
|
|
8002
|
-
</div>
|
|
8003
|
-
|
|
8004
|
-
${renderBinaryTree(a.binary_tree)}
|
|
8005
|
-
|
|
8006
|
-
<div class="card" style="margin-bottom:12px">
|
|
8007
|
-
<div style="font-size:13px;color:#6b7280;margin-bottom:6px">${t('弱侧匹配量')}: <strong>${pair.toLocaleString()}</strong> PV</div>
|
|
8008
|
-
${nextTier ? `
|
|
8009
|
-
<div style="font-size:11px;color:#6b7280;margin-bottom:4px">${t('距离 tier')} ${nextTier.tier} ${t('门槛')} ${Number(nextTier.pv_threshold).toLocaleString()} PV (${nextTier.score_per_hit} Score)</div>
|
|
8010
|
-
<div style="background:#f3f4f6;height:6px;border-radius:3px;overflow:hidden">
|
|
8011
|
-
<div style="background:#6366f1;height:6px;width:${nextProgress.toFixed(1)}%"></div>
|
|
8012
|
-
</div>` : ''}
|
|
8013
|
-
</div>
|
|
8014
|
-
|
|
8015
|
-
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:12px">
|
|
8016
|
-
<div class="card" style="text-align:center">
|
|
8017
|
-
<div style="font-size:11px;color:#6b7280">${t('待结算 Score')}</div>
|
|
8018
|
-
<div style="font-size:18px;font-weight:700;color:#d97706">${(a.score?.pending_score || 0).toFixed(0)}</div>
|
|
8019
|
-
</div>
|
|
8020
|
-
<div class="card" style="text-align:center">
|
|
8021
|
-
<div style="font-size:11px;color:#6b7280">${t('累计获 WAZ')}</div>
|
|
8022
|
-
<div style="font-size:18px;font-weight:700;color:#059669">${(a.score?.settled_waz || 0).toFixed(2)}</div>
|
|
8023
|
-
</div>
|
|
8024
|
-
</div>
|
|
8025
|
-
|
|
8026
|
-
<details style="margin-bottom:12px">
|
|
8027
|
-
<summary style="font-size:12px;color:#6366f1;cursor:pointer">${t('匹配档位表')}</summary>
|
|
8028
|
-
<table style="width:100%;margin-top:8px;border-collapse:collapse">
|
|
8029
|
-
<tr style="background:#f9fafb;font-size:11px;color:#6b7280">
|
|
8030
|
-
<th style="padding:6px 8px;text-align:left">Tier</th>
|
|
8031
|
-
<th style="padding:6px 8px;text-align:right">${t('门槛 PV')}</th>
|
|
8032
|
-
<th style="padding:6px 8px;text-align:right">${t('Score / 次')}</th>
|
|
8033
|
-
</tr>
|
|
8034
|
-
${tierTable}
|
|
8035
|
-
</table>
|
|
8036
|
-
</details>
|
|
8037
|
-
|
|
8038
|
-
<h3 style="font-size:13px;font-weight:600;margin:8px 0">📊 ${t('最近匹配')}</h3>
|
|
8039
|
-
<div class="card" style="padding:0">
|
|
8040
|
-
${recentRows}
|
|
8041
|
-
</div>`
|
|
8042
|
-
}
|
|
8043
|
-
|
|
8044
|
-
// P12: 三层积分树(你 + 左右 + 各自左右)
|
|
8045
|
-
function renderBinaryTree(tree) {
|
|
8046
|
-
if (!tree || !tree.me) return ''
|
|
8047
|
-
const node = (n, bg, fg, label) => n
|
|
8048
|
-
? `<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')}">
|
|
8049
|
-
<div style="font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${escHtml(n.name || '—')}</div>
|
|
8050
|
-
<div style="font-size:9px;opacity:0.8">L${Math.round(n.lpv)}/R${Math.round(n.rpv)}</div>
|
|
8051
|
-
</div>`
|
|
8052
|
-
: `<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>`
|
|
8053
|
-
|
|
8054
|
-
return `
|
|
8055
|
-
<details style="margin-bottom:12px" open>
|
|
8056
|
-
<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>
|
|
8057
|
-
<div style="padding:8px;background:#fff;border:1px solid #e5e7eb;border-radius:8px">
|
|
8058
|
-
<!-- L0: 你 -->
|
|
8059
|
-
<div style="margin-bottom:6px">
|
|
8060
|
-
${node(tree.me, '#e0e7ff', '#3730a3', '')}
|
|
8061
|
-
</div>
|
|
8062
|
-
<div style="text-align:center;color:#d1d5db;font-size:12px;margin:-2px 0">┌────┴────┐</div>
|
|
8063
|
-
<!-- L1: 左 / 右 -->
|
|
8064
|
-
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:4px">
|
|
8065
|
-
${node(tree.left, '#dbeafe', '#1e40af', t('空左'))}
|
|
8066
|
-
${node(tree.right, '#dcfce7', '#15803d', t('空右'))}
|
|
8067
|
-
</div>
|
|
8068
|
-
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;color:#d1d5db;font-size:11px;text-align:center;margin:-2px 0">
|
|
8069
|
-
<span>┌──┴──┐</span><span>┌──┴──┐</span>
|
|
8070
|
-
</div>
|
|
8071
|
-
<!-- L2 -->
|
|
8072
|
-
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:4px">
|
|
8073
|
-
${node(tree.ll, '#eff6ff', '#1e40af', '·')}
|
|
8074
|
-
${node(tree.lr, '#eff6ff', '#1e40af', '·')}
|
|
8075
|
-
${node(tree.rl, '#f0fdf4', '#15803d', '·')}
|
|
8076
|
-
${node(tree.rr, '#f0fdf4', '#15803d', '·')}
|
|
8077
|
-
</div>
|
|
8078
|
-
<div style="font-size:10px;color:#9ca3af;margin-top:6px;text-align:center">${t('每节点显示 L=左累计 PV / R=右累计 PV · 点击节点看详情')}</div>
|
|
8079
|
-
</div>
|
|
8080
|
-
</details>`
|
|
8081
|
-
}
|
|
8082
|
-
|
|
8083
|
-
// 节点 PV KPI modal — 点组织图任一节点弹出
|
|
8084
|
-
window.showNodePvModal = async (userId) => {
|
|
8085
|
-
if (!userId) return
|
|
8086
|
-
_openModal(`
|
|
8087
|
-
<h2 style="font-size:16px;font-weight:600;margin-bottom:12px">📊 ${t('节点 PV 详情')}</h2>
|
|
8088
|
-
<div id="pv-modal-body" style="text-align:center;padding:20px;color:#9ca3af;font-size:13px">${t('加载中...')}</div>
|
|
8089
|
-
<div style="display:flex;gap:8px;margin-top:8px">
|
|
8090
|
-
<button class="btn btn-outline" style="flex:1" onclick="closeModal()">${t('关闭')}</button>
|
|
8091
|
-
</div>
|
|
8092
|
-
`)
|
|
8093
|
-
const data = await GET(`/users/${userId}/pv-summary`)
|
|
8094
|
-
const body = document.getElementById('pv-modal-body')
|
|
8095
|
-
if (!body) return
|
|
8096
|
-
if (data?.error) {
|
|
8097
|
-
body.innerHTML = alert$('error', data.error)
|
|
8098
|
-
return
|
|
8099
|
-
}
|
|
8100
|
-
const weak = data.weak_leg_pv || 0
|
|
8101
|
-
const ratio = (data.total_left_pv + data.total_right_pv) > 0
|
|
8102
|
-
? (weak / Math.max(data.total_left_pv, data.total_right_pv) * 100).toFixed(0)
|
|
8103
|
-
: '0'
|
|
8104
|
-
body.innerHTML = `
|
|
8105
|
-
<div style="text-align:left">
|
|
8106
|
-
<div style="display:flex;align-items:center;gap:10px;margin-bottom:14px;padding-bottom:10px;border-bottom:1px solid #f3f4f6">
|
|
8107
|
-
<div style="width:42px;height:42px;border-radius:50%;background:#eef2ff;display:flex;align-items:center;justify-content:center;font-size:20px">👤</div>
|
|
8108
|
-
<div style="flex:1;min-width:0">
|
|
8109
|
-
<div style="font-size:15px;font-weight:600">${escHtml(data.name)}</div>
|
|
8110
|
-
<div style="font-size:11px;color:#9ca3af">${data.permanent_code || data.id} ${data.handle ? '· @' + data.handle : ''}</div>
|
|
8111
|
-
</div>
|
|
8112
|
-
</div>
|
|
8113
|
-
${data.placement ? `
|
|
8114
|
-
<div style="background:#f5f3ff;border-radius:6px;padding:8px 10px;font-size:12px;color:#6b21a8;margin-bottom:10px">
|
|
8115
|
-
📍 ${t('挂位置')}: ${escHtml(data.placement.name || data.placement.id)} ${data.placement.side === 'left' ? '🔵 左侧' : '🟢 右侧'} · ${t('深度')} ${data.placement.depth}
|
|
8116
|
-
</div>` : `<div style="font-size:11px;color:#9ca3af;margin-bottom:10px">${t('独立根节点(无上级)')}</div>`}
|
|
8117
|
-
|
|
8118
|
-
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:10px">
|
|
8119
|
-
<div style="text-align:center;background:#eff6ff;border-radius:6px;padding:8px">
|
|
8120
|
-
<div style="font-size:10px;color:#6b7280">🔵 ${t('左区 PV')}</div>
|
|
8121
|
-
<div style="font-size:16px;font-weight:700;color:#1e40af">${Number(data.total_left_pv).toLocaleString()}</div>
|
|
8122
|
-
<div style="font-size:9px;color:#9ca3af">${data.left_child ? escHtml(data.left_child.name) : t('空')}</div>
|
|
8123
|
-
</div>
|
|
8124
|
-
<div style="text-align:center;background:#f0fdf4;border-radius:6px;padding:8px">
|
|
8125
|
-
<div style="font-size:10px;color:#6b7280">🟢 ${t('右区 PV')}</div>
|
|
8126
|
-
<div style="font-size:16px;font-weight:700;color:#15803d">${Number(data.total_right_pv).toLocaleString()}</div>
|
|
8127
|
-
<div style="font-size:9px;color:#9ca3af">${data.right_child ? escHtml(data.right_child.name) : t('空')}</div>
|
|
8128
|
-
</div>
|
|
8129
|
-
</div>
|
|
8130
|
-
|
|
8131
|
-
<div style="background:#f9fafb;border-radius:6px;padding:8px 10px;font-size:11px;color:#6b7280;line-height:1.7;margin-bottom:10px">
|
|
8132
|
-
${t('弱侧匹配量')}: <strong style="color:#374151">${weak.toLocaleString()}</strong> PV · ${t('双腿均衡')} ${ratio}%<br>
|
|
8133
|
-
${t('累计匹配')}: <strong style="color:#374151">${data.total_hits || 0}</strong> ${t('次')} · ${t('累计获 WAZ')}: <strong style="color:#16a34a">${Number(data.settled_waz || 0).toFixed(2)}</strong><br>
|
|
8134
|
-
${t('待结 Score')}: <strong style="color:#d97706">${Number(data.pending_score || 0).toFixed(1)}</strong>
|
|
8135
|
-
</div>
|
|
8136
|
-
|
|
8137
|
-
<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>
|
|
8138
|
-
</div>
|
|
8139
|
-
`
|
|
8140
|
-
}
|
|
8141
|
-
|
|
8142
8350
|
// 健壮复制:先 clipboard API,失败回退 execCommand(不依赖 focus/activation)
|
|
8143
8351
|
// 任何 async 后调用都安全。window.copyText 暴露给 inline onclick handlers
|
|
8144
8352
|
async function copyText(text) {
|
|
@@ -8187,7 +8395,7 @@ async function webShareOrCopy(opts) {
|
|
|
8187
8395
|
|
|
8188
8396
|
window.copyRefLink = async (link) => {
|
|
8189
8397
|
const meName = state.user?.name || t('一位老用户')
|
|
8190
|
-
const text = t('我在 WebAZ 用 AI
|
|
8398
|
+
const text = t('我在 WebAZ 用 AI 比价下单,体验不错,推荐你也试试。分享链接仅作参与 / 归因记录,不构成收益承诺。') + '\n— ' + meName
|
|
8191
8399
|
const r = await webShareOrCopy({ title: 'WebAZ', text, url: link })
|
|
8192
8400
|
if (r === 'shared') toast$(t('已分享'))
|
|
8193
8401
|
else if (r === 'copied') toast$(t('已复制(含邀请文案)'))
|
|
@@ -8265,9 +8473,9 @@ async function maybePromptPlacementBind() {
|
|
|
8265
8473
|
const st = await GET('/profile/placement-status')
|
|
8266
8474
|
if (!st.can_bind) return // 已有 placement 或 有下线 → 不弹
|
|
8267
8475
|
setTimeout(() => {
|
|
8268
|
-
const ok = confirm(`📍 ${t('系统检测到邀请链接')}\n\n${t('inviter')}: ${inviter}\n\n${t('是否加入对方的积分配对?')}\n${t('
|
|
8476
|
+
const ok = confirm(`📍 ${t('系统检测到邀请链接')}\n\n${t('inviter')}: ${inviter}\n\n${t('是否加入对方的积分配对?')}\n${t('系统将自动安排积分树位置。一旦加入永久不变。')}`)
|
|
8269
8477
|
if (ok) {
|
|
8270
|
-
POST('/profile/bind-placement', { inviter_id: inviter
|
|
8478
|
+
POST('/profile/bind-placement', { inviter_id: inviter }).then(res => {
|
|
8271
8479
|
if (res.error) alert(`✗ ${res.error}`)
|
|
8272
8480
|
else alert(`✓ ${t('已加入积分树')}\n${t('侧')}: ${res.side}\n${t('深度')}: ${res.depth}`)
|
|
8273
8481
|
})
|
|
@@ -10223,6 +10431,12 @@ async function renderMyContributions(app) {
|
|
|
10223
10431
|
app.innerHTML = shell(loading$(), 'me')
|
|
10224
10432
|
const p = await GET('/build-reputation/me')
|
|
10225
10433
|
if (!p || p.error) { app.innerHTML = shell(`<div class="card" style="padding:16px;color:#b91c1c">${escHtml(p?.error || t('加载失败'))}</div>`, 'me'); return }
|
|
10434
|
+
// F9 — GitHub 身份归属面(只读自己的 bindings + attributable facts;失败时优雅降级,不挡整页)
|
|
10435
|
+
const gid = await GET('/contribution-identity/github/me').catch(() => null)
|
|
10436
|
+
// F10 — 自动发现可认领的 GitHub 贡献(只读;失败优雅降级)
|
|
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)
|
|
10226
10440
|
const lang = window._lang === 'zh' ? 'zh' : 'en'
|
|
10227
10441
|
const tier = p.tier || {}
|
|
10228
10442
|
const k = p.kpi || {}
|
|
@@ -10259,12 +10473,226 @@ async function renderMyContributions(app) {
|
|
|
10259
10473
|
</div>
|
|
10260
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>` : ''}
|
|
10261
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>` : ''}
|
|
10476
|
+
${ghClaimSectionHtml(gid, claimable, lang)}
|
|
10477
|
+
${contributionFactsSectionHtml(cf, lang)}
|
|
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">
|
|
10479
|
+
<div style="font-size:20px;flex-shrink:0">📋</div>
|
|
10480
|
+
<div style="flex:1;min-width:0">
|
|
10481
|
+
<div style="font-weight:600;font-size:13px">${t('查看公开共建任务')}</div>
|
|
10482
|
+
<div style="font-size:11px;color:#9ca3af;margin-top:2px">${t('可浏览 / 可认领的公共任务板(独立于我的贡献记录)')}</div>
|
|
10483
|
+
</div>
|
|
10484
|
+
<div style="color:#9ca3af">›</div>
|
|
10485
|
+
</div>
|
|
10262
10486
|
<div style="font-size:13px;font-weight:600;margin:14px 0 6px">${t('限制与申诉')}</div>
|
|
10263
10487
|
${restrHtml}
|
|
10264
10488
|
<div style="font-size:11px;color:#9ca3af;margin-top:14px">${t('看板仅自己可见,不做公开排行。')}</div>
|
|
10265
10489
|
`, 'me')
|
|
10266
10490
|
}
|
|
10267
10491
|
|
|
10492
|
+
// ─── F9 — GitHub 贡献认领 UI(身份/归属认领;不是奖励、付款或提现)──────────────────────
|
|
10493
|
+
// 服务端三步契约:claim-challenge(签发挑战+proof_marker)→ 用户把 marker 发到自己 GitHub 账号的
|
|
10494
|
+
// public gist → requestPasskeyGate('identity_claim', {github_actor_id, source_event_key, challenge_id})
|
|
10495
|
+
// 拿一次性 webauthn_token → claim-complete。accountId 恒为 session 用户(前端绝不传 account_id);
|
|
10496
|
+
// GitHub read token 只在服务端(前端不收不传)。F10 discovery 已接入(GET .../github/claimable →
|
|
10497
|
+
// 「可认领的 GitHub 贡献」列表,点「认领此贡献」预填并发起);手动输入仅作找不到时的 fallback。
|
|
10498
|
+
let _ghClaimCtx = null // { challenge_id, expires_at, proof_marker, actor, sek }
|
|
10499
|
+
|
|
10500
|
+
// typed error code → 可读提示(认领失败绝不伪装成功)
|
|
10501
|
+
function ghClaimErrText(code, fallback) {
|
|
10502
|
+
const m = {
|
|
10503
|
+
GITHUB_READ_NOT_CONFIGURED: t('身份认领暂不可用(服务端未配置 GitHub 读取凭证),请稍后再试或联系 maintainer'),
|
|
10504
|
+
FACT_NOT_CLAIMABLE: t('没有可认领的、经凭证背书的 GitHub 贡献记录 — 请检查 source_event_key 是否正确'),
|
|
10505
|
+
ACTOR_MISMATCH: t('该贡献记录的执行者与所填 GitHub 身份不符'),
|
|
10506
|
+
ALREADY_BOUND: t('该 GitHub 身份已被其他账号认领'),
|
|
10507
|
+
CHALLENGE_EXPIRED: t('认领挑战已过期,请重新生成'),
|
|
10508
|
+
CHALLENGE_ALREADY_USED: t('认领挑战已被使用,请重新生成'),
|
|
10509
|
+
CHALLENGE_NOT_FOUND: t('认领挑战不存在或不属于当前账号'),
|
|
10510
|
+
PROOF_REJECTED: t('gist 证明未通过 — 请确认 gist 公开、归属于该 GitHub 账号、且内容为完整 proof_marker'),
|
|
10511
|
+
HUMAN_PRESENCE_REQUIRED: t('此操作需要真人 Passkey 验证'),
|
|
10512
|
+
AGENT_SCOPE_UNDECLARED: t('写操作需问责锚点 — 请先在「安全」页绑定 Passkey'),
|
|
10513
|
+
INVALID_REQUEST: t('请求参数无效 — 请检查两个输入框'),
|
|
10514
|
+
}
|
|
10515
|
+
return m[code] || fallback || t('操作失败')
|
|
10516
|
+
}
|
|
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
|
+
|
|
10568
|
+
function ghClaimSectionHtml(gid, claimable, lang) {
|
|
10569
|
+
const ok = gid && !gid.error
|
|
10570
|
+
const bindings = ok ? (gid.bindings || []) : []
|
|
10571
|
+
const facts = ok ? (gid.attributable_facts || []) : []
|
|
10572
|
+
const notice = ok && gid.value_boundary ? (lang === 'zh' ? gid.value_boundary.notice_zh : gid.value_boundary.notice_en) : ''
|
|
10573
|
+
const bindRows = bindings.length === 0
|
|
10574
|
+
? `<div style="font-size:12px;color:#9ca3af">${t('尚未绑定 GitHub 身份')}</div>`
|
|
10575
|
+
: bindings.map(b => `<div style="font-size:12px;color:#374151">🔗 <code>github:${escHtml(String(b.github_actor_id))}</code> · ${escHtml(String(b.visibility))} · ${t('绑定于')} ${escHtml(String(b.bound_at || ''))}</div>`).join('')
|
|
10576
|
+
const factRows = facts.length === 0
|
|
10577
|
+
? `<div style="font-size:12px;color:#9ca3af">${t('暂无可归属的贡献事实')}</div>`
|
|
10578
|
+
: facts.map(f => `<div style="font-size:11px;color:#374151;padding:6px 8px;background:#f9fafb;border-radius:6px;margin-bottom:4px">
|
|
10579
|
+
<div><b>${escHtml(String(f.type || f.source))}</b> · ${escHtml(String(f.status))} · <code style="font-size:10px">${escHtml(String(f.fact_id).slice(0, 18))}…</code></div>
|
|
10580
|
+
<div style="color:#6b7280;word-break:break-all">${escHtml(String(f.source_event_key))}</div>
|
|
10581
|
+
</div>`).join('')
|
|
10582
|
+
// F10 — 自动发现的可认领贡献(actor 未被任何账号绑定的 credential-backed facts)
|
|
10583
|
+
const cl = (claimable && !claimable.error) ? (claimable.claimable_facts || []) : null
|
|
10584
|
+
const claimRows = cl === null
|
|
10585
|
+
? `<div style="font-size:12px;color:#9ca3af">${t('可认领列表暂不可用,稍后再试')}</div>`
|
|
10586
|
+
: cl.length === 0
|
|
10587
|
+
? `<div style="font-size:12px;color:#9ca3af">${t('暂无自动发现的可认领贡献')}</div>`
|
|
10588
|
+
: cl.map(r => `<div style="font-size:11px;color:#374151;padding:6px 8px;background:#eff6ff;border-radius:6px;margin-bottom:4px">
|
|
10589
|
+
<div><b>PR #${escHtml(String(r.pr_number))}</b> · <code>github:${escHtml(String(r.github_actor_id))}</code> · ${escHtml(String(r.merged_at || r.created_at || ''))}</div>
|
|
10590
|
+
<div style="color:#6b7280;word-break:break-all">${escHtml(String(r.source_event_key))}</div>
|
|
10591
|
+
<div style="color:#9ca3af;font-size:10px">fact <code>${escHtml(String(r.fact_id).slice(0, 18))}…</code> · ${escHtml(String(r.merge_commit_sha || '').slice(0, 10))}</div>
|
|
10592
|
+
<button class="btn btn-outline btn-sm" style="width:auto;margin-top:4px;font-size:11px" onclick="ghClaimFromRow('${escHtml(String(r.github_actor_id))}','${escHtml(String(r.source_event_key))}')">${t('认领此贡献')}</button>
|
|
10593
|
+
</div>`).join('')
|
|
10594
|
+
const ctx = _ghClaimCtx
|
|
10595
|
+
return `
|
|
10596
|
+
<div class="card" style="padding:14px;margin-bottom:12px" id="gh-claim-card">
|
|
10597
|
+
<div style="font-size:13px;font-weight:600;margin-bottom:4px">🔗 ${t('GitHub 贡献认领(身份归属)')}</div>
|
|
10598
|
+
<div style="font-size:11px;color:#6b7280;margin-bottom:4px">${t('把"以 GitHub 身份完成、已被凭证背书"的贡献事实归属到本账号。这只是身份与归属的认领 — 不是奖励、不是付款。')}</div>
|
|
10599
|
+
<div style="font-size:11px;color:#166534;background:#f0fdf4;border-radius:6px;padding:6px 8px;margin-bottom:8px">✓ ${t('GitHub 贡献认领不需要先购买,也不需要开通分享分润。')}</div>
|
|
10600
|
+
${notice ? `<div style="font-size:10px;color:#9ca3af;background:#f4f4f5;border-radius:6px;padding:6px 8px;margin-bottom:8px">🔒 ${escHtml(notice)}</div>` : ''}
|
|
10601
|
+
${ok ? `
|
|
10602
|
+
<div style="font-size:12px;font-weight:600;margin:8px 0 4px">${t('已绑定身份')}</div>${bindRows}
|
|
10603
|
+
<div style="font-size:12px;font-weight:600;margin:10px 0 4px">${t('可归属的贡献事实')}</div>${factRows}
|
|
10604
|
+
<div style="font-size:12px;font-weight:600;margin:10px 0 4px">${t('可认领的 GitHub 贡献(自动发现)')}</div>${claimRows}
|
|
10605
|
+
<details style="margin-top:10px" ${ctx ? 'open' : ''} id="gh-claim-details">
|
|
10606
|
+
<summary style="font-size:12px;font-weight:600;cursor:pointer">${t('手动认领一条贡献(找不到时的备用入口)')}</summary>
|
|
10607
|
+
<div style="font-size:11px;color:#92400e;background:#fef3c7;border-radius:6px;padding:6px 8px;margin:8px 0">${t('上方列表找不到时,可手动输入 source_event_key 与 github_actor_id。')}</div>
|
|
10608
|
+
<input id="gh-claim-actor" placeholder="github_actor_id (${t('如')} 262558625)" value="${escHtml(ctx?.actor || '')}" style="width:100%;box-sizing:border-box;padding:8px 10px;border:1px solid #d4d4d8;border-radius:8px;font-size:12px;margin-bottom:6px">
|
|
10609
|
+
<input id="gh-claim-sek" placeholder="source_event_key (github:<repo>:<pr>:merged)" value="${escHtml(ctx?.sek || '')}" style="width:100%;box-sizing:border-box;padding:8px 10px;border:1px solid #d4d4d8;border-radius:8px;font-size:12px;margin-bottom:6px">
|
|
10610
|
+
<button class="btn btn-outline btn-sm" style="width:auto" onclick="ghClaimIssue()">1️⃣ ${t('生成认领挑战')}</button>
|
|
10611
|
+
<div id="gh-claim-step2" style="margin-top:8px">${ctx ? ghClaimStep2Html(ctx) : ''}</div>
|
|
10612
|
+
<div id="gh-claim-msg" style="font-size:12px;margin-top:6px"></div>
|
|
10613
|
+
</details>` : `<div style="font-size:12px;color:#9ca3af">${t('身份归属面暂不可用,稍后再试')}</div>`}
|
|
10614
|
+
</div>`
|
|
10615
|
+
}
|
|
10616
|
+
|
|
10617
|
+
function ghClaimStep2Html(ctx) {
|
|
10618
|
+
return `
|
|
10619
|
+
<div style="background:#eef2ff;border-radius:8px;padding:10px;margin-top:4px">
|
|
10620
|
+
<div style="font-size:11px;color:#3730a3;margin-bottom:2px">🔒 ${t('本挑战锁定于')}: <code>github:${escHtml(ctx.actor)}</code> · <code style="word-break:break-all">${escHtml(ctx.sek)}</code></div>
|
|
10621
|
+
<div style="font-size:11px;color:#3730a3;margin-bottom:4px">challenge_id: <code>${escHtml(ctx.challenge_id)}</code> · ${t('过期于')} ${escHtml(ctx.expires_at || '')}</div>
|
|
10622
|
+
<div style="font-size:11px;color:#374151;margin-bottom:4px">2️⃣ ${t('把下方 proof_marker 原样发布到【该 GitHub 账号拥有的 public gist】,然后回来填 gist_id:')}</div>
|
|
10623
|
+
<div style="font-family:ui-monospace,Menlo,monospace;font-size:10px;background:#fff;border:1px solid #e0e7ff;border-radius:6px;padding:6px 8px;word-break:break-all" id="gh-claim-marker">${escHtml(ctx.proof_marker)}</div>
|
|
10624
|
+
<button class="btn btn-outline btn-sm" style="width:auto;margin-top:6px" onclick="copyText(document.getElementById('gh-claim-marker').textContent).then(ok=>toast$(ok?t('已复制'):t('复制失败,请手动复制'),ok?'success':'error'))">📋 ${t('复制 proof_marker')}</button>
|
|
10625
|
+
<input id="gh-claim-gist" placeholder="gist_id" style="width:100%;box-sizing:border-box;padding:8px 10px;border:1px solid #d4d4d8;border-radius:8px;font-size:12px;margin-top:8px">
|
|
10626
|
+
<button class="btn btn-primary btn-sm" style="width:auto;margin-top:6px" onclick="ghClaimComplete()">3️⃣ 🔑 ${t('用 Passkey 完成认领')}</button>
|
|
10627
|
+
</div>`
|
|
10628
|
+
}
|
|
10629
|
+
|
|
10630
|
+
// F10:点「认领此贡献」→ 预填 actor + source_event_key,展开手动区,直接走 F9 既有三步流程(签发挑战)
|
|
10631
|
+
window.ghClaimFromRow = (actor, sek) => {
|
|
10632
|
+
const d = document.getElementById('gh-claim-details'); if (d) d.open = true
|
|
10633
|
+
const ia = document.getElementById('gh-claim-actor'); if (ia) ia.value = actor
|
|
10634
|
+
const is = document.getElementById('gh-claim-sek'); if (is) is.value = sek
|
|
10635
|
+
ghClaimIssue()
|
|
10636
|
+
}
|
|
10637
|
+
|
|
10638
|
+
window.ghClaimIssue = async () => {
|
|
10639
|
+
const msg = document.getElementById('gh-claim-msg')
|
|
10640
|
+
const actor = (document.getElementById('gh-claim-actor')?.value || '').trim()
|
|
10641
|
+
const sek = (document.getElementById('gh-claim-sek')?.value || '').trim()
|
|
10642
|
+
if (!actor || !sek) { if (msg) msg.innerHTML = `<span style="color:#dc2626">${t('请先填写 github_actor_id 与 source_event_key')}</span>`; return }
|
|
10643
|
+
if (msg) msg.innerHTML = `<span style="color:#6366f1">${t('签发中…')}</span>`
|
|
10644
|
+
const r = await POST('/contribution-identity/github/claim-challenge', { source_event_key: sek, github_actor_id: actor })
|
|
10645
|
+
if (r?.status === 'issued') {
|
|
10646
|
+
_ghClaimCtx = { challenge_id: r.challenge_id, expires_at: r.expires_at, proof_marker: r.proof_marker, actor, sek }
|
|
10647
|
+
const s2 = document.getElementById('gh-claim-step2'); if (s2) s2.innerHTML = ghClaimStep2Html(_ghClaimCtx)
|
|
10648
|
+
if (msg) msg.innerHTML = `<span style="color:#16a34a">✓ ${t('挑战已签发 — 按第 2 步发布 gist')}</span>`
|
|
10649
|
+
return
|
|
10650
|
+
}
|
|
10651
|
+
if (r?.status === 'already_bound_self') { if (msg) msg.innerHTML = `<span style="color:#16a34a">✓ ${t('该 GitHub 身份已绑定到本账号,无需重复认领')}</span>`; return }
|
|
10652
|
+
// 签发失败 → 清空旧挑战上下文(防止旧 challenge 被误用于新输入)
|
|
10653
|
+
_ghClaimCtx = null
|
|
10654
|
+
const s2f = document.getElementById('gh-claim-step2'); if (s2f) s2f.innerHTML = ''
|
|
10655
|
+
if (msg) msg.innerHTML = `<span style="color:#dc2626">${escHtml(ghClaimErrText(r?.error_code, r?.error))}${r?.error_code ? ` <code style="font-size:10px">[${escHtml(r.error_code)}]</code>` : ''}</span>`
|
|
10656
|
+
}
|
|
10657
|
+
|
|
10658
|
+
window.ghClaimComplete = async () => {
|
|
10659
|
+
const msg = document.getElementById('gh-claim-msg')
|
|
10660
|
+
const ctx = _ghClaimCtx
|
|
10661
|
+
if (!ctx) { if (msg) msg.innerHTML = `<span style="color:#dc2626">${t('请先生成认领挑战')}</span>`; return }
|
|
10662
|
+
// 输入漂移守卫:签发后若用户改了 actor/source 输入框,旧挑战不再适用 — 清空并要求重新生成,
|
|
10663
|
+
// 防止"以为在认领当前输入的那条"的错觉(服务端本就只认 challenge 绑定的三元组)。
|
|
10664
|
+
const curActor = (document.getElementById('gh-claim-actor')?.value || '').trim()
|
|
10665
|
+
const curSek = (document.getElementById('gh-claim-sek')?.value || '').trim()
|
|
10666
|
+
if (curActor !== ctx.actor || curSek !== ctx.sek) {
|
|
10667
|
+
_ghClaimCtx = null
|
|
10668
|
+
const s2 = document.getElementById('gh-claim-step2'); if (s2) s2.innerHTML = ''
|
|
10669
|
+
if (msg) msg.innerHTML = `<span style="color:#dc2626">${t('输入已更改,与已签发的挑战不一致 — 请重新生成认领挑战')}</span>`
|
|
10670
|
+
return
|
|
10671
|
+
}
|
|
10672
|
+
const gistId = (document.getElementById('gh-claim-gist')?.value || '').trim()
|
|
10673
|
+
if (!gistId) { if (msg) msg.innerHTML = `<span style="color:#dc2626">${t('请填写 gist_id')}</span>`; return }
|
|
10674
|
+
let token
|
|
10675
|
+
try {
|
|
10676
|
+
// purpose_data 必须与服务端校验完全一致:{github_actor_id, source_event_key, challenge_id}
|
|
10677
|
+
token = await requestPasskeyGate('identity_claim', { github_actor_id: ctx.actor, source_event_key: ctx.sek, challenge_id: ctx.challenge_id })
|
|
10678
|
+
} catch (e) {
|
|
10679
|
+
if (msg) msg.innerHTML = `<span style="color:#dc2626">${t('Passkey 验证未完成')}: ${escHtml(e?.message || '')}</span>`
|
|
10680
|
+
return
|
|
10681
|
+
}
|
|
10682
|
+
if (msg) msg.innerHTML = `<span style="color:#6366f1">${t('提交认领…')}</span>`
|
|
10683
|
+
const r = await POST('/contribution-identity/github/claim-complete', {
|
|
10684
|
+
source_event_key: ctx.sek, github_actor_id: ctx.actor,
|
|
10685
|
+
challenge_id: ctx.challenge_id, gist_id: gistId, webauthn_token: token,
|
|
10686
|
+
})
|
|
10687
|
+
if (r?.status === 'claimed' || r?.status === 'already_bound_self') {
|
|
10688
|
+
_ghClaimCtx = null
|
|
10689
|
+
toast$(t('✓ 认领成功 — 贡献事实已归属到本账号'), 'success')
|
|
10690
|
+
renderMyContributions(document.getElementById('app')) // 刷新归属面 + 建设信誉面板
|
|
10691
|
+
return
|
|
10692
|
+
}
|
|
10693
|
+
if (msg) msg.innerHTML = `<span style="color:#dc2626">${escHtml(ghClaimErrText(r?.error_code, r?.error))}${r?.error_code ? ` <code style="font-size:10px">[${escHtml(r.error_code)}]</code>` : ''}</span>`
|
|
10694
|
+
}
|
|
10695
|
+
|
|
10268
10696
|
// W7 客服 ticket-thread 视图
|
|
10269
10697
|
const TICKET_TYPE_META = {
|
|
10270
10698
|
created: { icon: '🛟', title: '新建工单', border: '#d97706' },
|
|
@@ -11187,7 +11615,63 @@ async function renderSellerAnalytics(app) {
|
|
|
11187
11615
|
<div style="font-size:10px;color:#9ca3af">${t('退款')}</div>
|
|
11188
11616
|
</div>
|
|
11189
11617
|
</div>
|
|
11618
|
+
|
|
11619
|
+
<div class="card" style="padding:14px;margin-bottom:10px">
|
|
11620
|
+
<div style="font-size:13px;font-weight:600;margin-bottom:8px">⭐ ${t('店铺评价')} <span style="font-size:11px;color:#9ca3af;font-weight:400">${t('(买家评价 · 每条可回应一次)')}</span></div>
|
|
11621
|
+
<div id="seller-reviews-area" style="font-size:12px;color:#6b7280">${loading$()}</div>
|
|
11622
|
+
</div>
|
|
11190
11623
|
`, 'me')
|
|
11624
|
+
hydrateSellerReviews()
|
|
11625
|
+
}
|
|
11626
|
+
|
|
11627
|
+
// 店铺评价汇总 + 逐条回应(P2)。复用既有 POST /orders/:order_id/rating/reply(卖家一回一限);
|
|
11628
|
+
// 读 /sellers/me/ratings(authed,含 order_id)。不改评价 / 资金逻辑。
|
|
11629
|
+
async function hydrateSellerReviews() {
|
|
11630
|
+
const area = document.getElementById('seller-reviews-area')
|
|
11631
|
+
if (!area) return
|
|
11632
|
+
const r = await GET('/sellers/me/ratings?limit=50').catch(() => null)
|
|
11633
|
+
const items = Array.isArray(r?.items) ? r.items : []
|
|
11634
|
+
if (items.length === 0) { area.innerHTML = `<div style="color:#9ca3af;text-align:center;padding:12px">${t('暂无评价')}</div>`; return }
|
|
11635
|
+
const unreplied = Number(r?.agg?.unreplied || 0)
|
|
11636
|
+
const starStr = (n) => '★'.repeat(Math.max(0, Math.min(5, Number(n) || 0))) + '☆'.repeat(5 - Math.max(0, Math.min(5, Number(n) || 0)))
|
|
11637
|
+
area.innerHTML = `
|
|
11638
|
+
${unreplied > 0 ? `<div style="font-size:11px;color:#92400e;background:#fffbeb;border:1px solid #fde68a;border-radius:6px;padding:6px 8px;margin-bottom:8px">📝 ${unreplied} ${t('条评价待回应')}</div>` : ''}
|
|
11639
|
+
${items.map(it => it.masked ? `
|
|
11640
|
+
<div style="border:1px solid #f3f4f6;border-radius:8px;padding:10px;margin-bottom:8px;background:#fafafa">
|
|
11641
|
+
<div style="display:flex;justify-content:space-between;align-items:center;gap:6px;margin-bottom:4px">
|
|
11642
|
+
<span style="font-size:12px;color:#6b7280">🔒 ${t('评价双盲遮蔽中')}</span>
|
|
11643
|
+
<span style="font-size:10px;color:#9ca3af">${fmtTime(it.created_at)}</span>
|
|
11644
|
+
</div>
|
|
11645
|
+
<div style="font-size:11px;color:#9ca3af;margin-bottom:4px">📦 ${escHtml(it.product_title || '')}</div>
|
|
11646
|
+
<div style="font-size:11px;color:#92400e;background:#fffbeb;border:1px solid #fde68a;border-radius:6px;padding:6px 8px">${t('买家已评价,但需你先评价买家,或盲评期结束后才能查看与回应(防互相影响打分)。')}</div>
|
|
11647
|
+
</div>` : `
|
|
11648
|
+
<div style="border:1px solid #f3f4f6;border-radius:8px;padding:10px;margin-bottom:8px">
|
|
11649
|
+
<div style="display:flex;justify-content:space-between;align-items:center;gap:6px;margin-bottom:4px">
|
|
11650
|
+
<span style="color:#f59e0b;font-size:13px">${starStr(it.stars)}</span>
|
|
11651
|
+
<span style="font-size:10px;color:#9ca3af">${fmtTime(it.created_at)}</span>
|
|
11652
|
+
</div>
|
|
11653
|
+
<div style="font-size:11px;color:#6b7280;margin-bottom:4px">@${escHtml(it.buyer_handle || it.buyer_name || '')} · 📦 ${escHtml(it.product_title || '')}</div>
|
|
11654
|
+
${it.comment ? `<div style="font-size:12px;color:#374151;margin-bottom:6px">${escHtml(it.comment)}</div>` : `<div style="font-size:12px;color:#9ca3af;margin-bottom:6px">${t('买家未留言')}</div>`}
|
|
11655
|
+
${it.reply ? `<div style="background:#f0f9ff;border-radius:6px;padding:6px 8px;font-size:12px;color:#0369a1"><strong>${t('你的回应')}:</strong>${escHtml(it.reply)}</div>${it.buyer_followup ? `<div style="background:#fafafa;border-radius:6px;padding:6px 8px;font-size:12px;color:#374151;margin-top:4px"><strong>${t('买家追问')}:</strong>${escHtml(it.buyer_followup)}</div>` : ''}` : `
|
|
11656
|
+
<div style="display:flex;gap:6px;align-items:flex-end">
|
|
11657
|
+
<textarea id="rev-reply-${it.order_id}" rows="1" maxlength="500" placeholder="${t('回应这条评价(最多 500 字 · 仅一次)')}" style="flex:1;padding:6px 8px;border:1px solid #d1d5db;border-radius:6px;font-size:12px;resize:none"></textarea>
|
|
11658
|
+
<button class="btn btn-primary btn-sm" style="padding:6px 12px;font-size:11px" onclick="submitSellerReviewReply('${it.order_id}')">${t('回应')}</button>
|
|
11659
|
+
</div>
|
|
11660
|
+
<div id="rev-reply-err-${it.order_id}" style="font-size:11px;color:#dc2626;margin-top:4px"></div>`}
|
|
11661
|
+
</div>`).join('')}
|
|
11662
|
+
`
|
|
11663
|
+
}
|
|
11664
|
+
|
|
11665
|
+
window.submitSellerReviewReply = async (orderId) => {
|
|
11666
|
+
const ta = document.getElementById('rev-reply-' + orderId)
|
|
11667
|
+
const errEl = document.getElementById('rev-reply-err-' + orderId)
|
|
11668
|
+
const reply = (ta?.value || '').trim()
|
|
11669
|
+
if (errEl) errEl.textContent = ''
|
|
11670
|
+
if (!reply) { if (errEl) errEl.textContent = t('回应不能为空'); return }
|
|
11671
|
+
const res = await POST(`/orders/${orderId}/rating/reply`, { reply })
|
|
11672
|
+
if (res.error) { if (errEl) errEl.textContent = res.error; return }
|
|
11673
|
+
toast$(t('已回应'))
|
|
11674
|
+
hydrateSellerReviews()
|
|
11191
11675
|
}
|
|
11192
11676
|
|
|
11193
11677
|
window.switchAnalyticsWindow = (days) => {
|
|
@@ -11263,15 +11747,17 @@ async function renderReturnsCenter(app) {
|
|
|
11263
11747
|
}
|
|
11264
11748
|
|
|
11265
11749
|
// L3 Phase 2:卖家确认收到退货 → 触发退款
|
|
11266
|
-
|
|
11750
|
+
// orderId 可选:从订单详情内联调用时传入 → 处理完回到订单详情(否则回退货中心)
|
|
11751
|
+
window.confirmReturnReceived = async (id, orderId) => {
|
|
11267
11752
|
if (!confirm(t('确认已收到退货商品?此操作将触发退款(不可撤销)'))) return
|
|
11268
11753
|
const r = await POST(`/return-requests/${id}/received`, {})
|
|
11269
11754
|
if (r.error) { toast$(r.error, 'error'); return }
|
|
11270
11755
|
toast$(t('已退款 · 退货完成'))
|
|
11271
|
-
|
|
11756
|
+
if (orderId) renderOrderDetail(document.getElementById('app'), orderId)
|
|
11757
|
+
else renderReturnsCenter(document.getElementById('app'))
|
|
11272
11758
|
}
|
|
11273
11759
|
|
|
11274
|
-
window.decideReturn = (id, decision) => {
|
|
11760
|
+
window.decideReturn = (id, decision, orderId) => {
|
|
11275
11761
|
const isAccept = decision === 'accept'
|
|
11276
11762
|
const title = isAccept ? `✓ ${t('确认接受退款')}` : `✗ ${t('拒绝退货')}`
|
|
11277
11763
|
const hint = isAccept
|
|
@@ -11290,7 +11776,7 @@ window.decideReturn = (id, decision) => {
|
|
|
11290
11776
|
<div id="ret-decide-msg" style="margin:8px 0"></div>
|
|
11291
11777
|
<div style="display:flex;gap:8px;margin-top:12px">
|
|
11292
11778
|
<button class="btn btn-gray" style="flex:1" onclick="this.closest('[style*=position]').remove()">${t('取消')}</button>
|
|
11293
|
-
<button class="btn ${isAccept ? 'btn-success' : 'btn-primary'}" style="flex:1" onclick="confirmDecideReturn('${id}','${decision}')">${isAccept ? t('接受退款') : t('拒绝')}</button>
|
|
11779
|
+
<button class="btn ${isAccept ? 'btn-success' : 'btn-primary'}" style="flex:1" onclick="confirmDecideReturn('${id}','${decision}','${orderId || ''}')">${isAccept ? t('接受退款') : t('拒绝')}</button>
|
|
11294
11780
|
</div>
|
|
11295
11781
|
</div>
|
|
11296
11782
|
</div>
|
|
@@ -11300,7 +11786,7 @@ window.decideReturn = (id, decision) => {
|
|
|
11300
11786
|
document.body.appendChild(div.firstElementChild)
|
|
11301
11787
|
}
|
|
11302
11788
|
|
|
11303
|
-
window.confirmDecideReturn = async (id, decision) => {
|
|
11789
|
+
window.confirmDecideReturn = async (id, decision, orderId) => {
|
|
11304
11790
|
const response = document.getElementById('ret-decide-text').value.trim()
|
|
11305
11791
|
const msg = document.getElementById('ret-decide-msg')
|
|
11306
11792
|
if (decision === 'reject' && !response) {
|
|
@@ -11311,7 +11797,8 @@ window.confirmDecideReturn = async (id, decision) => {
|
|
|
11311
11797
|
if (res.error) { msg.innerHTML = alert$('error', res.error); return }
|
|
11312
11798
|
document.querySelector('.js-modal')?.remove()
|
|
11313
11799
|
toast$(t('已处理'))
|
|
11314
|
-
|
|
11800
|
+
if (orderId) renderOrderDetail(document.getElementById('app'), orderId)
|
|
11801
|
+
else renderReturnsCenter(document.getElementById('app'))
|
|
11315
11802
|
}
|
|
11316
11803
|
|
|
11317
11804
|
window.sharePromoLink = async (productId, title) => {
|
|
@@ -11328,7 +11815,7 @@ window.sharePromoLink = async (productId, title) => {
|
|
|
11328
11815
|
}
|
|
11329
11816
|
const link = `${location.origin}${res.short_url}`
|
|
11330
11817
|
const meName = state.user.name || ''
|
|
11331
|
-
const text = `${t('我在 WebAZ 看上「')}${title}${t('
|
|
11818
|
+
const text = `${t('我在 WebAZ 看上「')}${title}${t('」,用 AI 比价,体验不错')}\n— ${meName}`
|
|
11332
11819
|
const r = await webShareOrCopy({ title: 'WebAZ — ' + title, text, url: link })
|
|
11333
11820
|
if (r === 'shared') toast$(t('已分享'))
|
|
11334
11821
|
else if (r === 'copied') toast$(t('已复制(含商品文案 + 链接)') + (res.reused ? '' : ' · ' + t('已自动加入「我的分享」')))
|
|
@@ -11942,32 +12429,8 @@ async function syncPlacementPref() {
|
|
|
11942
12429
|
} catch {}
|
|
11943
12430
|
}
|
|
11944
12431
|
|
|
11945
|
-
//
|
|
11946
|
-
//
|
|
11947
|
-
window.copyPlacementLink = async (link, side, el) => {
|
|
11948
|
-
const ok = await copyText(link)
|
|
11949
|
-
if (ok) {
|
|
11950
|
-
toast$((side === 'left' ? '🔵 ' : '🟢 ') + t('推荐码已复制'))
|
|
11951
|
-
if (el) {
|
|
11952
|
-
const orig = el.style.background
|
|
11953
|
-
el.style.background = side === 'left' ? '#bfdbfe' : '#bbf7d0'
|
|
11954
|
-
setTimeout(() => { el.style.background = orig }, 600)
|
|
11955
|
-
}
|
|
11956
|
-
} else {
|
|
11957
|
-
prompt(t('请手动复制'), link)
|
|
11958
|
-
}
|
|
11959
|
-
}
|
|
11960
|
-
|
|
11961
|
-
// 兼容:旧调用点保留
|
|
11962
|
-
window.sharePlatformLink = (side) => {
|
|
11963
|
-
if (!state.user?.id) return alert(t('请先登录'))
|
|
11964
|
-
if (side !== 'left' && side !== 'right') return
|
|
11965
|
-
// 用 permanent_code 短链(/i/CODE-L|R),绝不用 usr_xxx;缺码时拒绝
|
|
11966
|
-
const code = state.user?.permanent_code
|
|
11967
|
-
if (!code) return alert(t('邀请码暂不可用,请刷新或联系支持'))
|
|
11968
|
-
const link = `${location.origin}/i/${code}-${side === 'left' ? 'L' : 'R'}`
|
|
11969
|
-
copyPlacementLink(link, side, null)
|
|
11970
|
-
}
|
|
12432
|
+
// pre-public 去左右码:copyPlacementLink / sharePlatformLink(生成 /i/CODE-L|R 侧链)已移除 —
|
|
12433
|
+
// 统一用唯一推荐码 copyRefLink('/i/CODE'),放置侧别由系统自动决定。
|
|
11971
12434
|
|
|
11972
12435
|
function renderRecover(app) {
|
|
11973
12436
|
app.innerHTML = `
|
|
@@ -11979,8 +12442,8 @@ function renderRecover(app) {
|
|
|
11979
12442
|
|
|
11980
12443
|
<div id="rec-step1">
|
|
11981
12444
|
<div class="form-group">
|
|
11982
|
-
<label class="form-label">${t('
|
|
11983
|
-
<input class="form-control" id="rec-name" placeholder="${t('例:陈小明')}">
|
|
12445
|
+
<label class="form-label">${t('名称或 @用户名')}</label>
|
|
12446
|
+
<input class="form-control" id="rec-name" placeholder="${t('例:陈小明 或 @chenxiaoming')}">
|
|
11984
12447
|
</div>
|
|
11985
12448
|
<div class="form-group">
|
|
11986
12449
|
<label class="form-label">${t('绑定的邮箱')}</label>
|
|
@@ -11994,7 +12457,11 @@ function renderRecover(app) {
|
|
|
11994
12457
|
<div class="form-group">
|
|
11995
12458
|
<input class="form-control" id="rec-code" placeholder="${t('6 位验证码')}" maxlength="6">
|
|
11996
12459
|
</div>
|
|
11997
|
-
<
|
|
12460
|
+
<div class="form-group">
|
|
12461
|
+
<label class="form-label">${t('设置新登录密码')} <span style="font-size:11px;color:#9ca3af;font-weight:400">${t('(可选 · 至少 8 位 · 留空则只找回 API Key)')}</span></label>
|
|
12462
|
+
<input class="form-control" id="rec-newpw" type="password" placeholder="${t('新密码(至少 8 位)')}" autocomplete="new-password" maxlength="200">
|
|
12463
|
+
</div>
|
|
12464
|
+
<button class="btn btn-primary" onclick="doRecoverConfirm()">${t('验证并找回')}</button>
|
|
11998
12465
|
<button class="btn btn-outline" onclick="recoverBackToStep1()" style="margin-left:8px">${t('重新开始')}</button>
|
|
11999
12466
|
</div>
|
|
12000
12467
|
|
|
@@ -12034,13 +12501,15 @@ window.doRecoverConfirm = async () => {
|
|
|
12034
12501
|
const name = codeInp?.dataset?.name
|
|
12035
12502
|
const email = codeInp?.dataset?.email
|
|
12036
12503
|
const code = codeInp?.value?.trim()
|
|
12504
|
+
const newpw = document.getElementById('rec-newpw')?.value || ''
|
|
12037
12505
|
const result = document.getElementById('rec-result')
|
|
12038
12506
|
if (!code) { result.innerHTML = alert$('error', t('请填写验证码')); return }
|
|
12507
|
+
if (newpw && newpw.length < 8) { result.innerHTML = alert$('error', t('新密码至少 8 字符')); return }
|
|
12039
12508
|
result.innerHTML = loading$()
|
|
12040
|
-
const res = await api('POST', '/recover-key/confirm', { name, email, code })
|
|
12509
|
+
const res = await api('POST', '/recover-key/confirm', { name, email, code, ...(newpw ? { new_password: newpw } : {}) })
|
|
12041
12510
|
if (res.error) { result.innerHTML = alert$('error', res.error); return }
|
|
12042
12511
|
result.innerHTML = `
|
|
12043
|
-
<div class="alert alert-success" style="font-size:13px">${t('✓ 找回成功')}</div>
|
|
12512
|
+
<div class="alert alert-success" style="font-size:13px">${res.password_reset ? t('✓ 找回成功 · 新密码已设置') : t('✓ 找回成功')}</div>
|
|
12044
12513
|
<div style="background:#f3f4f6;border-radius:8px;padding:12px;margin-top:10px">
|
|
12045
12514
|
<div style="font-size:12px;color:#6b7280;margin-bottom:6px">${t('你的 API Key')}</div>
|
|
12046
12515
|
<code style="font-size:13px;word-break:break-all">${res.api_key}</code>
|
|
@@ -12066,19 +12535,6 @@ window.switchLoginTab = (tab) => {
|
|
|
12066
12535
|
async function checkRegGate() {
|
|
12067
12536
|
try {
|
|
12068
12537
|
const f = await GET('/system-flags')
|
|
12069
|
-
// 邀请码轮询按钮开关 — 默认禁用,admin 开启后变可用
|
|
12070
|
-
const btn = document.getElementById('btn-fetch-ref')
|
|
12071
|
-
if (btn) {
|
|
12072
|
-
if (f?.invite_rotation_enabled) {
|
|
12073
|
-
btn.disabled = false
|
|
12074
|
-
btn.title = t('点击向系统申领一个邀请码')
|
|
12075
|
-
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'
|
|
12076
|
-
} else {
|
|
12077
|
-
btn.disabled = true
|
|
12078
|
-
btn.title = t('该功能默认关闭,由管理员开启后可用')
|
|
12079
|
-
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'
|
|
12080
|
-
}
|
|
12081
|
-
}
|
|
12082
12538
|
if (!f?.require_ref_to_register) return
|
|
12083
12539
|
const hint = readShareHint()
|
|
12084
12540
|
const el = document.getElementById('reg-gate-hint')
|
|
@@ -12096,14 +12552,6 @@ async function checkRegGate() {
|
|
|
12096
12552
|
} catch {}
|
|
12097
12553
|
}
|
|
12098
12554
|
|
|
12099
|
-
window.doFetchInviteCode = async () => {
|
|
12100
|
-
const r = await POST('/invite/rotate', {})
|
|
12101
|
-
if (r.error) { alert(r.error); return }
|
|
12102
|
-
// 仅自动填入推荐码 — 不展示推荐人信息(轮询是被动分配,非主动分享)
|
|
12103
|
-
const inp = document.getElementById('inp-sponsor')
|
|
12104
|
-
if (inp) inp.value = r.code
|
|
12105
|
-
}
|
|
12106
|
-
|
|
12107
12555
|
window.doLogin = async () => {
|
|
12108
12556
|
const key = document.getElementById('inp-key').value.trim()
|
|
12109
12557
|
if (!key) return showMsg('error', t('请粘贴 api_key'))
|
|
@@ -12149,36 +12597,27 @@ window.doLoginByPassword = async () => {
|
|
|
12149
12597
|
|
|
12150
12598
|
|
|
12151
12599
|
// 从 URL 解析分享 hint(30 天 cookie 持久化)
|
|
12152
|
-
// ?ref=
|
|
12153
|
-
// ?placement=
|
|
12154
|
-
//
|
|
12155
|
-
// ?ref=xxx&placement=xxx&side=... → 显式两轨道
|
|
12600
|
+
// ?ref=CODE → 三级佣金 sponsor(同时作 placement inviter)
|
|
12601
|
+
// ?placement=CODE → 仅积分挂靠
|
|
12602
|
+
// pre-public 去左右码:不再解析 side / -L/-R 侧别,放置侧别由注册时系统自动决定。
|
|
12156
12603
|
function readShareHint() {
|
|
12157
12604
|
const params = new URLSearchParams(location.search)
|
|
12158
12605
|
let urlRef = params.get('ref')
|
|
12159
12606
|
let urlPlace = params.get('placement')
|
|
12160
|
-
|
|
12161
|
-
//
|
|
12162
|
-
const stripSide = (v) =>
|
|
12163
|
-
|
|
12164
|
-
|
|
12165
|
-
|
|
12166
|
-
}
|
|
12167
|
-
let extraSide = null
|
|
12168
|
-
;[urlRef, extraSide] = stripSide(urlRef)
|
|
12169
|
-
if (!extraSide) [urlPlace, extraSide] = stripSide(urlPlace)
|
|
12170
|
-
if (!urlSide && extraSide) urlSide = extraSide
|
|
12171
|
-
// ref / placement 仅接受邀请码(6-7 位永久码,可带 -L/-R)。usr_xxx / @handle / 裸 handle 不再作为邀请引用 —
|
|
12607
|
+
// pre-public 去左右码:兼容旧的 -L/-R 后缀但只做归一化(剥离回基础码),不再提取/存储 side。
|
|
12608
|
+
// 放置侧别由注册时系统自动决定。
|
|
12609
|
+
const stripSide = (v) => (v ? v.replace(/-[lLrR]$/, '') : v)
|
|
12610
|
+
urlRef = stripSide(urlRef)
|
|
12611
|
+
urlPlace = stripSide(urlPlace)
|
|
12612
|
+
// ref / placement 仅接受邀请码(6-7 位永久码)。usr_xxx / @handle / 裸 handle 不再作为邀请引用 —
|
|
12172
12613
|
// 收窄公开邀请面,消除歧义(权威由服务端 resolveInviteCodeRef 二次校验)。
|
|
12173
12614
|
const refPattern = /^[A-Za-z0-9]{6,7}$/
|
|
12174
12615
|
const validRef = urlRef && refPattern.test(urlRef)
|
|
12175
12616
|
const validPlace= urlPlace && refPattern.test(urlPlace)
|
|
12176
12617
|
if (validRef || validPlace) {
|
|
12177
|
-
const side = (urlSide === 'left' || urlSide === 'right') ? urlSide : null
|
|
12178
12618
|
const obj = {
|
|
12179
12619
|
sponsor_id: validRef ? urlRef : null,
|
|
12180
|
-
placement_inviter_id: validPlace ? urlPlace :
|
|
12181
|
-
placement_side: side,
|
|
12620
|
+
placement_inviter_id: validPlace ? urlPlace : null,
|
|
12182
12621
|
expiry: Date.now() + 30 * 86400_000,
|
|
12183
12622
|
}
|
|
12184
12623
|
localStorage.setItem('webaz_share_hint', JSON.stringify(obj))
|
|
@@ -12211,8 +12650,7 @@ function readShareHint() {
|
|
|
12211
12650
|
const o = JSON.parse(old)
|
|
12212
12651
|
if (o.expiry > Date.now() && okCode(o.ref)) return {
|
|
12213
12652
|
sponsor_id: o.ref || null,
|
|
12214
|
-
placement_inviter_id:
|
|
12215
|
-
placement_side: o.side || null,
|
|
12653
|
+
placement_inviter_id: null,
|
|
12216
12654
|
expiry: o.expiry,
|
|
12217
12655
|
}
|
|
12218
12656
|
localStorage.removeItem('webaz_ref') // expired or non-code → drop
|
|
@@ -12273,7 +12711,7 @@ async function maybeClaimPendingShopReferral() {
|
|
|
12273
12711
|
writeShareCtx({ pending_shop_referral: null }); return
|
|
12274
12712
|
}
|
|
12275
12713
|
try {
|
|
12276
|
-
await POST('/shop-referral/touch', { seller_identifier: p.seller_identifier, ref_code: p.ref_code
|
|
12714
|
+
await POST('/shop-referral/touch', { seller_identifier: p.seller_identifier, ref_code: p.ref_code })
|
|
12277
12715
|
// 任何已送达的响应(成功 / self-skip / already_locked / typed 错误)都清;仅网络异常保留待重试
|
|
12278
12716
|
writeShareCtx({ pending_shop_referral: null })
|
|
12279
12717
|
} catch {}
|
|
@@ -12402,7 +12840,6 @@ async function initShareCtx() {
|
|
|
12402
12840
|
if (hint?.sponsor_id && !ctx?.sponsor_id) patch.sponsor_id = hint.sponsor_id
|
|
12403
12841
|
if (hint?.placement_inviter_id && !ctx?.placement_inviter_id) {
|
|
12404
12842
|
patch.placement_inviter_id = hint.placement_inviter_id
|
|
12405
|
-
patch.placement_side = hint.placement_side
|
|
12406
12843
|
}
|
|
12407
12844
|
if (fromHash && !ctx?.target_hash) patch.target_hash = fromHash
|
|
12408
12845
|
|
|
@@ -12413,11 +12850,11 @@ async function initShareCtx() {
|
|
|
12413
12850
|
}
|
|
12414
12851
|
|
|
12415
12852
|
// 店铺推荐:?ref=CODE + #shop/<seller> → 暂存待登录后 touch(只锚定推荐关系,非全店佣金权)。
|
|
12416
|
-
// hint.sponsor_id 已由 readShareHint 收窄为邀请码(-L/-R
|
|
12853
|
+
// hint.sponsor_id 已由 readShareHint 收窄为邀请码(-L/-R 已归一化为基础码),usr_xxx/@handle 进不来。
|
|
12417
12854
|
if (hint?.sponsor_id && fromHash && fromHash.startsWith('#shop/') && !ctx?.pending_shop_referral) {
|
|
12418
12855
|
const sellerIdent = fromHash.slice('#shop/'.length).split('?')[0].trim()
|
|
12419
12856
|
if (sellerIdent && sellerIdent !== 'agent') {
|
|
12420
|
-
patch.pending_shop_referral = { seller_identifier: sellerIdent, ref_code: hint.sponsor_id
|
|
12857
|
+
patch.pending_shop_referral = { seller_identifier: sellerIdent, ref_code: hint.sponsor_id }
|
|
12421
12858
|
}
|
|
12422
12859
|
}
|
|
12423
12860
|
|
|
@@ -12507,11 +12944,47 @@ window._mountTurnstileIfEnabled = async () => {
|
|
|
12507
12944
|
} catch { /* 加载失败不阻塞 dev */ }
|
|
12508
12945
|
}
|
|
12509
12946
|
|
|
12947
|
+
// 注册邮箱验证:发码。成功后展开验证码输入框。
|
|
12948
|
+
window._regCodeSent = false
|
|
12949
|
+
window._onRegEmailInput = () => {
|
|
12950
|
+
// 邮箱改了就要求重新发码(旧码对不上新邮箱)
|
|
12951
|
+
window._regCodeSent = false
|
|
12952
|
+
const row = document.getElementById('reg-code-row')
|
|
12953
|
+
if (row) row.style.display = 'none'
|
|
12954
|
+
}
|
|
12955
|
+
window.doRegSendCode = async () => {
|
|
12956
|
+
const email = document.getElementById('inp-reg-email')?.value?.trim()
|
|
12957
|
+
const btn = document.getElementById('btn-reg-sendcode')
|
|
12958
|
+
if (!email || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) return showMsg('error', t('请填写有效邮箱'))
|
|
12959
|
+
if (btn) { btn.disabled = true; btn.textContent = t('发送中...') }
|
|
12960
|
+
const res = await POST('/register/send-code', { email })
|
|
12961
|
+
if (res.error) {
|
|
12962
|
+
if (btn) { btn.disabled = false; btn.textContent = t('发送验证码') }
|
|
12963
|
+
return showMsg('error', res.error)
|
|
12964
|
+
}
|
|
12965
|
+
window._regCodeSent = true
|
|
12966
|
+
const row = document.getElementById('reg-code-row')
|
|
12967
|
+
if (row) row.style.display = ''
|
|
12968
|
+
// 60s 冷却
|
|
12969
|
+
let left = 60
|
|
12970
|
+
if (btn) {
|
|
12971
|
+
btn.disabled = true
|
|
12972
|
+
const tick = () => { if (left <= 0) { btn.disabled = false; btn.textContent = t('重新发送'); return } btn.textContent = left + 's'; left--; setTimeout(tick, 1000) }
|
|
12973
|
+
tick()
|
|
12974
|
+
}
|
|
12975
|
+
showMsg('success', t('验证码已发送至邮箱,请查收'))
|
|
12976
|
+
}
|
|
12977
|
+
|
|
12510
12978
|
window.doRegister = async () => {
|
|
12511
12979
|
const name = document.getElementById('inp-name').value.trim()
|
|
12512
12980
|
const role = document.getElementById('inp-role').value
|
|
12513
12981
|
const region = document.getElementById('inp-region')?.value || ''
|
|
12514
12982
|
const sponsorInput = document.getElementById('inp-sponsor')?.value?.trim() || ''
|
|
12983
|
+
const email = document.getElementById('inp-reg-email')?.value?.trim() || ''
|
|
12984
|
+
const code = document.getElementById('inp-reg-code')?.value?.trim() || ''
|
|
12985
|
+
if (!email) return showMsg('error', t('请填写找回邮箱'))
|
|
12986
|
+
if (!window._regCodeSent) return showMsg('error', t('请先点"发送验证码"并验证邮箱'))
|
|
12987
|
+
if (!code) return showMsg('error', t('请输入邮箱验证码'))
|
|
12515
12988
|
if (!name) return showMsg('error', t('请填写名称'))
|
|
12516
12989
|
if (!region) return showMsg('error', t('请选择国家 / 地区'))
|
|
12517
12990
|
// A3 软 gate:sponsor_id 视觉必填但允许空(后端按 require_ref_to_register 系统设置裁决)
|
|
@@ -12519,13 +12992,12 @@ window.doRegister = async () => {
|
|
|
12519
12992
|
if (!confirm(t('没有填写邀请码 — 你将绑定到平台公库(推荐找老用户拿邀请链接)\n\n确认继续?'))) return
|
|
12520
12993
|
}
|
|
12521
12994
|
const hint = readShareHint()
|
|
12522
|
-
const body = { name, role, region }
|
|
12995
|
+
const body = { name, role, region, email, code }
|
|
12523
12996
|
// 优先 input 值,回退 URL hint
|
|
12524
12997
|
const sponsorFinal = sponsorInput || hint?.sponsor_id
|
|
12525
12998
|
if (sponsorFinal) body.sponsor_id = sponsorFinal
|
|
12526
|
-
if (hint?.placement_inviter_id
|
|
12999
|
+
if (hint?.placement_inviter_id) {
|
|
12527
13000
|
body.placement_inviter_id = hint.placement_inviter_id
|
|
12528
|
-
body.placement_side = hint.placement_side
|
|
12529
13001
|
}
|
|
12530
13002
|
// #1049 Turnstile token(若启用)
|
|
12531
13003
|
if (window._turnstileToken) body.turnstile_token = window._turnstileToken
|
|
@@ -12568,41 +13040,91 @@ function showRegisterSuccessModal(res) {
|
|
|
12568
13040
|
// P1 (QA 轮 14.a): 区分"邀请人"(referral sponsor) 与"积分树挂靠"(binary placement)
|
|
12569
13041
|
// 旧版挤一行 "已绑定邀请人 X · 左区 depth N" → 用户误以为 depth 是相对邀请人
|
|
12570
13042
|
const placementLine = res.placement
|
|
12571
|
-
? `<div style="font-size:11px;color:#9ca3af;margin-bottom:14px">${t('积分树挂靠')}: ${res.placement.side === 'left' ? '🔵 ' + t('左区') : '🟢 ' + t('右区')} · ${t('深度')} ${res.placement.depth}<br><span style="font-size:10px">${t('
|
|
13043
|
+
? `<div style="font-size:11px;color:#9ca3af;margin-bottom:14px">${t('积分树挂靠')}: ${res.placement.side === 'left' ? '🔵 ' + t('左区') : '🟢 ' + t('右区')} · ${t('深度')} ${res.placement.depth}<br><span style="font-size:10px">${t('积分树挂靠为系统内部记录,不代表收益权或兑付承诺。')}</span></div>`
|
|
12572
13044
|
: ''
|
|
12573
13045
|
_openModal(`
|
|
12574
13046
|
<h2 style="font-size:18px;font-weight:600;margin-bottom:8px;color:#16a34a">🎉 ${t('注册成功!')}</h2>
|
|
12575
13047
|
${res.sponsor_id && ctx?.sponsor_name ? `<div style="font-size:12px;color:#6366f1;margin-bottom:6px">${t('邀请人')}: <strong>${escHtml(ctx.sponsor_name)}</strong></div>` : ''}
|
|
12576
13048
|
${placementLine}
|
|
12577
13049
|
|
|
12578
|
-
<div style="background:#fef3c7;border:1px solid #fde68a;border-radius:8px;padding:10px 12px;margin-bottom:
|
|
12579
|
-
<div style="font-size:12px;color:#92400e;font-weight:600;margin-bottom:6px">⚠️ ${t('立即保存你的 API Key')}</div>
|
|
12580
|
-
<div style="font-size:11px;color:#78350f;margin-bottom:6px">${t('这是你的唯一登录凭证。丢了只能通过绑定邮箱找回。')}</div>
|
|
13050
|
+
<div style="background:#fef3c7;border:1px solid #fde68a;border-radius:8px;padding:10px 12px;margin-bottom:12px">
|
|
13051
|
+
<div style="font-size:12px;color:#92400e;font-weight:600;margin-bottom:6px">⚠️ ${t('立即保存你的 API Key(登录凭证)')}</div>
|
|
12581
13052
|
<div style="display:flex;gap:6px;align-items:center">
|
|
12582
13053
|
<code id="reg-apikey-display" style="font-size:12px;background:#fff;padding:6px 10px;border-radius:6px;flex:1;word-break:break-all;border:1px solid #fde68a">${res.api_key}</code>
|
|
12583
|
-
|
|
13054
|
+
</div>
|
|
13055
|
+
<div style="display:flex;gap:6px;margin-top:8px">
|
|
13056
|
+
<button class="btn btn-outline btn-sm" style="font-size:11px;padding:6px 10px;flex:1" onclick="window._regMarkSaved();copyText('${res.api_key}').then(ok=>toast$(ok?'${t('已复制')}':'${t('复制失败,请手动复制')}',ok?'success':'error'))">📋 ${t('复制 Key')}</button>
|
|
13057
|
+
<button class="btn btn-outline btn-sm" style="font-size:11px;padding:6px 10px;flex:1" onclick="window._downloadCredBackup()">⬇️ ${t('下载备份 .txt')}</button>
|
|
12584
13058
|
</div>
|
|
12585
13059
|
</div>
|
|
12586
13060
|
|
|
12587
|
-
<div style="font-size:13px;color:#374151;font-weight:600;margin-bottom:6px">${t('
|
|
12588
|
-
<div style="background:#f9fafb;border-radius:8px;padding:
|
|
12589
|
-
|
|
12590
|
-
|
|
12591
|
-
|
|
12592
|
-
|
|
13061
|
+
<div style="font-size:13px;color:#374151;font-weight:600;margin-bottom:6px">${t('保存凭证检查清单')}</div>
|
|
13062
|
+
<div style="background:#f9fafb;border-radius:8px;padding:8px 10px;margin-bottom:8px;font-size:12px;line-height:1.9">
|
|
13063
|
+
<div id="reg-chk-save">☐ ${t('复制 API Key 或下载备份文件')}</div>
|
|
13064
|
+
<div>✅ ${t('已验证找回邮箱')}:${escHtml(res.email || '')}</div>
|
|
13065
|
+
<div>☐ ${t('设置登录密码(备用凭证)')}</div>
|
|
13066
|
+
<div>☐ ${t('🔐 绑定 Passkey — 大额提现自动启用,防账号被盗')}</div>
|
|
13067
|
+
</div>
|
|
13068
|
+
<div style="font-size:11px;color:#6b7280;line-height:1.6;margin-bottom:12px">
|
|
13069
|
+
${t('邮箱已验证:可用于找回账号或重置登录密码。API Key 仍是主要身份凭证,强烈建议保存;Passkey 是增强保护,不替代恢复邮箱。')}
|
|
12593
13070
|
</div>
|
|
12594
13071
|
|
|
12595
13072
|
${targetBtn}
|
|
12596
13073
|
<button class="btn btn-primary" style="width:100%;margin-bottom:8px;background:linear-gradient(135deg,#7c3aed,#6d28d9);border-color:transparent" onclick="window._closeRegModal('passkey')">🔐 ${t('立即绑定 Passkey(强烈推荐)')}</button>
|
|
12597
|
-
<button class="btn btn-outline" style="width:100%;margin-bottom:8px" onclick="window._closeRegModal('
|
|
12598
|
-
<button class="btn btn-outline" style="width:100
|
|
13074
|
+
<button class="btn btn-outline" style="width:100%;margin-bottom:8px" onclick="window._closeRegModal('password')">🔑 ${t('设置登录密码')}</button>
|
|
13075
|
+
<button id="reg-skip-btn" class="btn btn-outline btn-sm" style="width:100%;color:#9ca3af;font-size:12px" onclick="window._closeRegModal(false)">${t('稍后再说,先逛逛')}</button>
|
|
12599
13076
|
`)
|
|
13077
|
+
// 凭证保存状态:复制或下载前,"稍后"按钮弱化 + 强提示。
|
|
13078
|
+
window._regSaved = false
|
|
13079
|
+
window._regMarkSaved = () => {
|
|
13080
|
+
window._regSaved = true
|
|
13081
|
+
const chk = document.getElementById('reg-chk-save')
|
|
13082
|
+
if (chk) { chk.innerHTML = '✅ ' + t('复制 API Key 或下载备份文件'); chk.style.color = '#16a34a' }
|
|
13083
|
+
const skip = document.getElementById('reg-skip-btn')
|
|
13084
|
+
if (skip) skip.style.color = ''
|
|
13085
|
+
}
|
|
13086
|
+
window._downloadCredBackup = () => {
|
|
13087
|
+
try {
|
|
13088
|
+
const lines = [
|
|
13089
|
+
'WebAZ 账户备份 / Account Backup',
|
|
13090
|
+
'================================',
|
|
13091
|
+
`${t('名称')} / Name: ${res.name || ''}`,
|
|
13092
|
+
`Handle: @${res.handle || ''}`,
|
|
13093
|
+
`${t('角色')} / Role: ${res.role || ''}`,
|
|
13094
|
+
`${t('找回邮箱')} / Recovery email: ${res.email || ''}`,
|
|
13095
|
+
`${t('注册时间')} / Registered: ${new Date().toISOString()}`,
|
|
13096
|
+
'--------------------------------',
|
|
13097
|
+
'API Key (登录凭证 / login credential):',
|
|
13098
|
+
res.api_key || '',
|
|
13099
|
+
'--------------------------------',
|
|
13100
|
+
'⚠️ 这是你的登录凭证,等同密码。妥善保管,不要截图分享或发给任何人。',
|
|
13101
|
+
'⚠️ This is your login credential (equivalent to a password). Keep it private — do not screenshot, forward, or share it.',
|
|
13102
|
+
'丢失 API Key 可用上面的找回邮箱在 https://webaz.xyz/#recover 重置(也可重置登录密码)。',
|
|
13103
|
+
'Lost your API Key? Reset it (and your password) with the recovery email above at https://webaz.xyz/#recover',
|
|
13104
|
+
]
|
|
13105
|
+
const blob = new Blob([lines.join('\n')], { type: 'text/plain;charset=utf-8' })
|
|
13106
|
+
const url = URL.createObjectURL(blob)
|
|
13107
|
+
const a = document.createElement('a')
|
|
13108
|
+
a.href = url
|
|
13109
|
+
a.download = `webaz-backup-${(res.handle || res.user_id || 'account')}.txt`
|
|
13110
|
+
document.body.appendChild(a); a.click(); a.remove()
|
|
13111
|
+
setTimeout(() => URL.revokeObjectURL(url), 2000)
|
|
13112
|
+
window._regMarkSaved()
|
|
13113
|
+
toast$(t('备份已下载'), 'success')
|
|
13114
|
+
} catch (e) {
|
|
13115
|
+
toast$(t('下载失败,请改用复制'), 'error')
|
|
13116
|
+
}
|
|
13117
|
+
}
|
|
12600
13118
|
window._closeRegModal = (action) => {
|
|
13119
|
+
// 复制/下载前点"稍后" → 强提示(已绑邮箱可找回,但仍建议先存 key)
|
|
13120
|
+
if (action === false && !window._regSaved) {
|
|
13121
|
+
if (!confirm(t('你还没复制 API Key 或下载备份。\n虽然已绑定找回邮箱(丢号可重置),仍强烈建议先保存 Key。\n\n确定先逛逛?'))) return
|
|
13122
|
+
}
|
|
12601
13123
|
closeModal()
|
|
12602
13124
|
if (action === true) {
|
|
12603
13125
|
// 跳 intended_hash
|
|
12604
13126
|
navigateIntended(roleHome(res.role))
|
|
12605
|
-
} else if (action === 'profile') {
|
|
13127
|
+
} else if (action === 'password' || action === 'profile') {
|
|
12606
13128
|
sessionStorage.removeItem('webaz_intended_hash')
|
|
12607
13129
|
location.hash = '#me/settings'
|
|
12608
13130
|
} else if (action === 'passkey') {
|
|
@@ -13732,8 +14254,8 @@ async function renderFeedView() {
|
|
|
13732
14254
|
const amount = Number(extra.amount || 0).toFixed(2)
|
|
13733
14255
|
body = `${actor} ${t('因推广')} <a href="#order-product/${e.product_id}" style="color:#111">${escHtml(e.product_title)}</a> ${t('获得 L')}${extra.level} ${t('佣金')} <strong style="color:#059669">+${amount} WAZ</strong>`
|
|
13734
14256
|
} else if (e.kind === 'join_binary') {
|
|
13735
|
-
|
|
13736
|
-
body = `${actor} ${t('加入了')} ${escHtml(extra.placement_name || '—')} ${t('
|
|
14257
|
+
// pre-public 去左右码:活动流不再广播左/右区,只显示加入了某人的积分树
|
|
14258
|
+
body = `${actor} ${t('加入了')} ${escHtml(extra.placement_name || '—')} ${t('的积分树')}`
|
|
13737
14259
|
}
|
|
13738
14260
|
const icon = e.kind === 'purchase' ? '🛒' : e.kind === 'commission' ? '💰' : '⚛'
|
|
13739
14261
|
return `<div class="card" style="margin-bottom:8px;padding:10px 12px;display:flex;gap:10px;align-items:flex-start">
|
|
@@ -14060,13 +14582,13 @@ async function renderUserProfile(app, userId) {
|
|
|
14060
14582
|
|
|
14061
14583
|
<div style="margin-bottom:14px">
|
|
14062
14584
|
<div style="font-size:13px;color:#374151;margin-bottom:6px">${t('一句话简介')} <span style="color:#9ca3af;font-size:11px">(${t('≤ 120 字')})</span></div>
|
|
14063
|
-
<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">
|
|
14064
14586
|
</div>
|
|
14065
14587
|
|
|
14066
14588
|
<div style="margin-bottom:14px">
|
|
14067
14589
|
<div style="font-size:13px;color:#374151;margin-bottom:6px">🔍 ${t('流量口令')} <span style="color:#9ca3af;font-size:11px">(${t('≤ 40 字,字母/数字/汉字/-_.')})</span></div>
|
|
14068
14590
|
<div style="display:flex;gap:8px">
|
|
14069
|
-
<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">
|
|
14070
14592
|
<button class="btn btn-primary btn-sm" style="white-space:nowrap" onclick="saveSocialProfile()">${t('保存')}</button>
|
|
14071
14593
|
</div>
|
|
14072
14594
|
<p style="font-size:11px;color:#9ca3af;margin-top:4px">${t('在 TikTok / 小红书 口播这个口令,粉丝在 WebAZ 搜它就能找到你')}</p>
|
|
@@ -14749,7 +15271,7 @@ const AI_TOOLS = [
|
|
|
14749
15271
|
},
|
|
14750
15272
|
{
|
|
14751
15273
|
name: 'search_by_anchor',
|
|
14752
|
-
description: '按创作者"流量口令"
|
|
15274
|
+
description: '按创作者"流量口令"查找评测内容(如某创作者的口令)。返回外链评测(YouTube/TikTok/etc)+ 原生 P2P 评测。',
|
|
14753
15275
|
input_schema: {
|
|
14754
15276
|
type: 'object',
|
|
14755
15277
|
properties: { anchor: { type: 'string', description: '创作者的口令字符串' } },
|
|
@@ -15301,7 +15823,7 @@ function renderAIMessages(messages) {
|
|
|
15301
15823
|
${t('试试问:')}<br>
|
|
15302
15824
|
• ${t('"帮我找适合送 60 岁妈妈的礼物 ≤ 500 元"')}<br>
|
|
15303
15825
|
• ${t('"附近最近有人买什么?"')}<br>
|
|
15304
|
-
• ${t('"
|
|
15826
|
+
• ${t('"某个口令的创作者发了啥?"')}
|
|
15305
15827
|
</div>
|
|
15306
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>
|
|
15307
15829
|
</div>`
|
|
@@ -17459,9 +17981,11 @@ async function renderBuyPage(app, productId) {
|
|
|
17459
17981
|
</button>`
|
|
17460
17982
|
// 三柱 chip — 跳对应区域(在同页内 scrollIntoView)
|
|
17461
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
|
|
17462
17986
|
const claimChip = claimsResolved + claimsOpen > 0
|
|
17463
17987
|
? chip('✓', t('已验证'), '#059669', '#d1fae5', `document.getElementById('claims-block-${p.id}')?.scrollIntoView({behavior:'smooth',block:'start'})`, claimsResolved)
|
|
17464
|
-
: chip('+', t('发起验证'), '#6366f1', '#eef2ff', `
|
|
17988
|
+
: (_canClaim ? chip('+', t('发起验证'), '#6366f1', '#eef2ff', `openProductClaimForm('${p.id}')`) : '')
|
|
17465
17989
|
const disputeChip = sellerDisputes > 0
|
|
17466
17990
|
? chip('⚖', t('仲裁'), sellerOpenDisputes > 0 ? '#dc2626' : '#92400e', sellerOpenDisputes > 0 ? '#fee2e2' : '#fef3c7', `navigate('#shop/${p.seller_id}?tab=disputes')`, sellerDisputes)
|
|
17467
17991
|
: ''
|
|
@@ -17882,7 +18406,6 @@ async function loadDisputeCaseDetail(caseId, container) {
|
|
|
17882
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>')
|
|
17883
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>')
|
|
17884
18408
|
if (tags.length === 0) tags.push('<span style="background:#f3f4f6;color:#6b7280;font-size:9px;padding:1px 6px;border-radius:99px">👀 ' + t('围观') + '</span>')
|
|
17885
|
-
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>')
|
|
17886
18409
|
// 身份显示:公开 → @handle 可点跳主页 ; 脱敏 → 匿名用户 灰字不可点
|
|
17887
18410
|
const isAnon = !!cm.anonymous
|
|
17888
18411
|
const nameHtml = isAnon
|
|
@@ -18427,6 +18950,19 @@ window.voteProductClaim = async (claimId, vote) => {
|
|
|
18427
18950
|
setTimeout(() => renderClaimVerify(document.getElementById('app')), 1500)
|
|
18428
18951
|
}
|
|
18429
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
|
+
|
|
18430
18966
|
window.submitProductClaim = async (productId) => {
|
|
18431
18967
|
const target = document.getElementById(`pcl-target-${productId}`)?.value
|
|
18432
18968
|
const text = document.getElementById(`pcl-text-${productId}`)?.value?.trim()
|
|
@@ -18831,20 +19367,21 @@ window.setSellerSubTab = (k) => {
|
|
|
18831
19367
|
// C-4: 批量发货 modal
|
|
18832
19368
|
window.openBatchShipModal = async (ids) => {
|
|
18833
19369
|
if (!Array.isArray(ids) || ids.length === 0) return
|
|
18834
|
-
// 拉物流公司
|
|
19370
|
+
// 拉物流公司(可空:自发货不需要物流方)
|
|
18835
19371
|
const lc = await GET('/logistics/companies').catch(() => [])
|
|
18836
19372
|
const companies = Array.isArray(lc) ? lc : []
|
|
18837
|
-
if (companies.length === 0) { alert(t('暂无可用物流公司')); return }
|
|
18838
19373
|
const html = `
|
|
18839
19374
|
<div class="js-modal" style="background:rgba(0,0,0,0.6);position:fixed;inset:0;z-index:1000;display:flex;align-items:flex-end;justify-content:center" onclick="this.remove()">
|
|
18840
19375
|
<div style="background:#fff;width:100%;max-width:560px;border-radius:16px 16px 0 0;padding:20px;max-height:80vh;overflow-y:auto" onclick="event.stopPropagation()">
|
|
18841
19376
|
<h2 style="font-size:16px;font-weight:700;margin-bottom:8px">📦 ${t('批量发货')}</h2>
|
|
18842
19377
|
<div style="font-size:12px;color:#6b7280;margin-bottom:12px">${t('将')} <strong>${ids.length}</strong> ${t('个订单一次性标记为已发货')}</div>
|
|
18843
19378
|
<div class="form-group">
|
|
18844
|
-
<label class="form-label">${t('
|
|
19379
|
+
<label class="form-label">${t('发货方式')}</label>
|
|
18845
19380
|
<select class="form-control" id="bs-logistics">
|
|
19381
|
+
<option value="self">${t('📦 我自己发货(自提自送)')}</option>
|
|
18846
19382
|
${companies.map(c => `<option value="${escHtml(c.id)}">${escHtml(c.name)}</option>`).join('')}
|
|
18847
19383
|
</select>
|
|
19384
|
+
<div style="font-size:11px;color:#92400e;margin-top:6px;line-height:1.5">${t('自己发货:你负责揽收 / 运输 / 送达,超时或虚假发货仍按卖家责任处理。选物流公司则由物流方流转。')}</div>
|
|
18848
19385
|
</div>
|
|
18849
19386
|
<div style="font-size:11px;color:#9ca3af;margin-bottom:8px">${t('快递单号可发货后单独补填,或先填部分')}</div>
|
|
18850
19387
|
<div id="bs-msg" style="margin:8px 0"></div>
|
|
@@ -18861,10 +19398,12 @@ window.openBatchShipModal = async (ids) => {
|
|
|
18861
19398
|
}
|
|
18862
19399
|
|
|
18863
19400
|
window.submitBatchShip = async (ids) => {
|
|
18864
|
-
const
|
|
19401
|
+
const choice = document.getElementById('bs-logistics').value
|
|
18865
19402
|
const msg = document.getElementById('bs-msg')
|
|
18866
19403
|
msg.innerHTML = loading$()
|
|
18867
|
-
|
|
19404
|
+
// 自发货:不传 logistics_company_id(后端保持 logistics_id 空 → seller self-fulfill)
|
|
19405
|
+
const body = (choice && choice !== 'self') ? { order_ids: ids, logistics_company_id: choice } : { order_ids: ids }
|
|
19406
|
+
const res = await POST('/orders/batch-ship', body)
|
|
18868
19407
|
if (res.error) { msg.innerHTML = alert$('error', res.error); return }
|
|
18869
19408
|
document.querySelector('.js-modal')?.remove()
|
|
18870
19409
|
toast$(`${t('已发货')} ${res.shipped} / ${ids.length}`)
|
|
@@ -19794,6 +20333,7 @@ async function renderOrderDetail(app, orderId) {
|
|
|
19794
20333
|
|
|
19795
20334
|
<div id="action-area">
|
|
19796
20335
|
${actions ? renderActions(orderId, actions, order, logisticsCompanies) : ''}
|
|
20336
|
+
${sellerDeclineContestPanel(order, orderId, isSeller)}
|
|
19797
20337
|
</div>
|
|
19798
20338
|
|
|
19799
20339
|
${(isBuyer && order.status === 'completed' && product?.id) ? `
|
|
@@ -19815,6 +20355,12 @@ async function renderOrderDetail(app, orderId) {
|
|
|
19815
20355
|
<div id="ret-area-${order.id}" style="font-size:12px;color:#6b7280">${loading$()}</div>
|
|
19816
20356
|
</div>` : ''}
|
|
19817
20357
|
|
|
20358
|
+
${(isSeller && order.status === 'completed') ? `
|
|
20359
|
+
<div class="card" id="ret-card-${order.id}">
|
|
20360
|
+
<div style="font-size:14px;font-weight:600;margin-bottom:6px">↩ ${t('退货处理')}</div>
|
|
20361
|
+
<div id="ret-area-${order.id}" style="font-size:12px;color:#6b7280">${loading$()}</div>
|
|
20362
|
+
</div>` : ''}
|
|
20363
|
+
|
|
19818
20364
|
${((isBuyer || isSeller) && order.status === 'completed') ? `
|
|
19819
20365
|
<div class="card" id="rate-card-${order.id}">
|
|
19820
20366
|
<div style="font-size:14px;font-weight:600;margin-bottom:6px">⭐ ${t('交易评价')}</div>
|
|
@@ -19839,7 +20385,8 @@ async function renderOrderDetail(app, orderId) {
|
|
|
19839
20385
|
`, 'orders')
|
|
19840
20386
|
|
|
19841
20387
|
// Wave B-3: 退货 widget — 异步加载(仅 completed 订单可退)
|
|
19842
|
-
|
|
20388
|
+
// 买家:有退货窗口可申请/查看;卖家:有退货申请时内联查看+处理(accept/reject/received),无申请则隐藏卡
|
|
20389
|
+
if (((isBuyer && Number(product?.return_days || 0) > 0) || isSeller) && order.status === 'completed') {
|
|
19843
20390
|
try { await renderReturnWidgetForOrder(order, product) } catch (e) { console.error(e) }
|
|
19844
20391
|
}
|
|
19845
20392
|
// Wave C-3: 评价 widget
|
|
@@ -20285,7 +20832,13 @@ async function renderReturnWidgetForOrder(order, product) {
|
|
|
20285
20832
|
// P1-5: 订单级直查(取代拉全量列表过滤)
|
|
20286
20833
|
const r = await GET(`/orders/${order.id}/return-request`).catch(() => ({ item: null }))
|
|
20287
20834
|
const mine = r?.item || null
|
|
20288
|
-
const
|
|
20835
|
+
const isSellerView = state.user && state.user.id === order.seller_id
|
|
20836
|
+
// 卖家视角:无退货申请则隐藏整张卡(卖家不申请退货,只在有申请时处理)
|
|
20837
|
+
if (isSellerView && !mine) {
|
|
20838
|
+
const card = area.closest('.card'); if (card) card.style.display = 'none'
|
|
20839
|
+
return
|
|
20840
|
+
}
|
|
20841
|
+
const returnDays = Number(product?.return_days || 0)
|
|
20289
20842
|
const baseTime = order.updated_at || order.created_at
|
|
20290
20843
|
const deadline = new Date(baseTime).getTime() + returnDays * 86400 * 1000
|
|
20291
20844
|
const remainMs = deadline - Date.now()
|
|
@@ -20315,6 +20868,14 @@ async function renderReturnWidgetForOrder(order, product) {
|
|
|
20315
20868
|
${item.status === 'pending' && isBuyer ? `<button class="btn btn-outline btn-sm" style="font-size:11px;padding:4px 10px" onclick="cancelReturnRequest('${item.id}', '${order.id}')">${t('取消申请')}</button>` : ''}
|
|
20316
20869
|
</div>
|
|
20317
20870
|
|
|
20871
|
+
${isSellerView && item.status === 'pending' ? `
|
|
20872
|
+
<div style="display:flex;gap:8px;margin-bottom:8px">
|
|
20873
|
+
<button class="btn btn-success btn-sm" style="flex:1;font-size:12px" onclick="decideReturn('${item.id}','accept','${order.id}')">${t('接受退款')}</button>
|
|
20874
|
+
<button class="btn btn-outline btn-sm" style="flex:1;font-size:12px;color:#dc2626;border-color:#fecaca" onclick="decideReturn('${item.id}','reject','${order.id}')">${t('拒绝退货')}</button>
|
|
20875
|
+
</div>` : ''}
|
|
20876
|
+
${isSellerView && item.status === 'picked_up' ? `
|
|
20877
|
+
<button class="btn btn-success btn-sm" style="width:100%;font-size:12px;margin-bottom:8px" onclick="confirmReturnReceived('${item.id}','${order.id}')">${t('✓ 已收到退货 · 触发退款')}</button>` : ''}
|
|
20878
|
+
|
|
20318
20879
|
<div style="padding:10px;background:#fafafa;border-radius:8px;margin-bottom:8px">
|
|
20319
20880
|
<div style="font-size:11px;font-weight:600;color:#6b7280;margin-bottom:8px">🧾 ${t('协商时间线')} · ${events.length} ${t('条')}</div>
|
|
20320
20881
|
${events.map(ev => buildReturnTimelineEvent(ev, isBuyer)).join('')}
|
|
@@ -20651,26 +21212,32 @@ window.cancelReturnRequest = async (id, orderId) => {
|
|
|
20651
21212
|
function getActions(order, isBuyer, isSeller, isLogistic) {
|
|
20652
21213
|
const s = order.status
|
|
20653
21214
|
const isInPerson = order.fulfillment_mode === 'in_person'
|
|
21215
|
+
const isSelfFulfillSeller = isSeller && !order.logistics_id
|
|
20654
21216
|
// M8 面交订单:买家在 paid / accepted 都可"面交完成"直接结算
|
|
20655
21217
|
if (isInPerson && isBuyer && (s === 'paid' || s === 'accepted')) {
|
|
20656
21218
|
return [{ action: 'confirm_in_person', label: '🤝 面交完成 / 确认收货', style: 'success' }]
|
|
20657
21219
|
}
|
|
20658
21220
|
if (isSeller && s === 'paid')
|
|
20659
|
-
return [
|
|
21221
|
+
return [
|
|
21222
|
+
{ action: 'accept', label: '接单', style: 'success' },
|
|
21223
|
+
{ action: 'decline', label: '拒绝接单', style: 'danger', custom: 'decline' },
|
|
21224
|
+
]
|
|
20660
21225
|
if (isSeller && s === 'accepted' && !isInPerson)
|
|
20661
21226
|
return [{ action: 'ship', label: '确认发货', style: 'success', logisticsSelector: true,
|
|
20662
21227
|
trackingInput: true,
|
|
20663
21228
|
evidencePlaceholder: '包装状态描述 / 货物说明(可选)' }]
|
|
20664
21229
|
if (isSeller && s === 'accepted' && isInPerson)
|
|
20665
21230
|
return [{ action: 'noop_in_person', label: '🤝 面交中(等待买家确认)', style: 'secondary', disabled: true }]
|
|
20666
|
-
if (isLogistic && s === 'shipped')
|
|
21231
|
+
if ((isLogistic || isSelfFulfillSeller) && s === 'shipped')
|
|
20667
21232
|
return [{ action: 'pickup', label: '✅ 确认揽收', style: 'success', needsEvidence: true,
|
|
20668
|
-
noteLabel: '快递单号 *', evidencePlaceholder: '如:SF1234567890'
|
|
20669
|
-
|
|
21233
|
+
noteLabel: '快递单号 *', evidencePlaceholder: '如:SF1234567890',
|
|
21234
|
+
helperText: isSelfFulfillSeller ? '自履约订单:你负责回传揽收/单号,超时仍按卖家责任处理。' : '' }]
|
|
21235
|
+
if ((isLogistic || isSelfFulfillSeller) && s === 'picked_up')
|
|
20670
21236
|
return [{ action: 'transit', label: '🚛 开始运输', style: 'primary' }]
|
|
20671
|
-
if (isLogistic && s === 'in_transit')
|
|
21237
|
+
if ((isLogistic || isSelfFulfillSeller) && s === 'in_transit')
|
|
20672
21238
|
return [{ action: 'deliver', label: '📬 确认投递', style: 'success', needsEvidence: true,
|
|
20673
|
-
noteLabel: '投递凭证', evidencePlaceholder: '门牌照片描述 / 收件人姓名 / 签收时间'
|
|
21239
|
+
noteLabel: '投递凭证', evidencePlaceholder: '门牌照片描述 / 收件人姓名 / 签收时间',
|
|
21240
|
+
helperText: isSelfFulfillSeller ? '自履约投递需留存签收/门牌/交付说明,买家确认后才结算。' : '' }]
|
|
20674
21241
|
if (isBuyer && s === 'delivered')
|
|
20675
21242
|
return [
|
|
20676
21243
|
{ action: 'confirm', label: '确认收货', style: 'success' },
|
|
@@ -20683,19 +21250,22 @@ function getActions(order, isBuyer, isSeller, isLogistic) {
|
|
|
20683
21250
|
function renderActions(orderId, actions, order, logisticsCompanies = []) {
|
|
20684
21251
|
return `
|
|
20685
21252
|
<div class="action-area">
|
|
20686
|
-
<div class="action-title"
|
|
21253
|
+
<div class="action-title">${t('我的操作')}</div>
|
|
20687
21254
|
<div id="action-msg"></div>
|
|
20688
21255
|
${actions.map((a, i) => `
|
|
20689
|
-
${a.
|
|
21256
|
+
${a.custom === 'decline' ? `
|
|
21257
|
+
<button class="btn btn-${a.style}" style="margin-bottom:8px;background:#fff;color:#dc2626;border:1px solid #dc2626"
|
|
21258
|
+
onclick="openDeclineModal('${orderId}')">
|
|
21259
|
+
${t(a.label)}
|
|
21260
|
+
</button>` :
|
|
21261
|
+
a.logisticsSelector ? `
|
|
20690
21262
|
<div class="form-group">
|
|
20691
|
-
<label class="form-label">${t('
|
|
21263
|
+
<label class="form-label">${t('发货方式')}</label>
|
|
20692
21264
|
<select class="form-control" id="logi-select-${i}">
|
|
20693
|
-
<option value="">${t('
|
|
21265
|
+
<option value="self">${t('📦 我自己发货(自提自送)')}</option>
|
|
20694
21266
|
${logisticsCompanies.map(c => `<option value="${escHtml(c.id)}">${escHtml(c.name)}</option>`).join('')}
|
|
20695
21267
|
</select>
|
|
20696
|
-
|
|
20697
|
-
? `<div class="alert alert-warning" style="margin-top:6px;font-size:13px">${t('暂无已注册的物流公司,请先让物流方注册账号')}</div>`
|
|
20698
|
-
: ''}
|
|
21268
|
+
<div style="font-size:11px;color:#92400e;background:#fffbeb;border:1px solid #fde68a;border-radius:6px;padding:6px 8px;margin-top:6px;line-height:1.5">${t('自己发货:你负责揽收 / 运输 / 送达,超时或虚假发货仍按卖家责任处理。选物流公司则由物流方流转。')}</div>
|
|
20699
21269
|
</div>
|
|
20700
21270
|
${a.trackingInput ? `
|
|
20701
21271
|
<div class="form-group">
|
|
@@ -20704,22 +21274,23 @@ function renderActions(orderId, actions, order, logisticsCompanies = []) {
|
|
|
20704
21274
|
</div>` : ''}
|
|
20705
21275
|
<button class="btn btn-${a.style}" style="margin-bottom:8px"
|
|
20706
21276
|
onclick="handleAction('${orderId}','${a.action}',${i},false,true)">
|
|
20707
|
-
${a.label}
|
|
21277
|
+
${t(a.label)}
|
|
20708
21278
|
</button>` :
|
|
20709
21279
|
a.needsEvidence ? `
|
|
20710
21280
|
<div class="form-group">
|
|
20711
|
-
<label class="form-label">${a.noteLabel || '证据说明'}</label>
|
|
21281
|
+
<label class="form-label">${t(a.noteLabel || '证据说明')}</label>
|
|
20712
21282
|
${a.action === 'pickup'
|
|
20713
|
-
? `<input type="text" class="form-control" id="evid-${i}" placeholder="${a.evidencePlaceholder || '快递单号'}">`
|
|
20714
|
-
: `<textarea class="form-control" id="evid-${i}" placeholder="${a.evidencePlaceholder || ''}"></textarea>`}
|
|
21283
|
+
? `<input type="text" class="form-control" id="evid-${i}" placeholder="${t(a.evidencePlaceholder || '快递单号')}">`
|
|
21284
|
+
: `<textarea class="form-control" id="evid-${i}" placeholder="${t(a.evidencePlaceholder || '')}"></textarea>`}
|
|
21285
|
+
${a.helperText ? `<div style="font-size:11px;color:#92400e;background:#fffbeb;border:1px solid #fde68a;border-radius:6px;padding:6px 8px;margin-top:6px">${t(a.helperText)}</div>` : ''}
|
|
20715
21286
|
</div>
|
|
20716
21287
|
<button class="btn btn-${a.style}" style="margin-bottom:8px"
|
|
20717
21288
|
onclick="handleAction('${orderId}','${a.action}',${i},true,false)">
|
|
20718
|
-
${a.label}
|
|
21289
|
+
${t(a.label)}
|
|
20719
21290
|
</button>` : `
|
|
20720
21291
|
<button class="btn btn-${a.style}" style="margin-bottom:8px"
|
|
20721
21292
|
onclick="handleAction('${orderId}','${a.action}',${i},false,false)">
|
|
20722
|
-
${a.label}
|
|
21293
|
+
${t(a.label)}
|
|
20723
21294
|
</button>`}
|
|
20724
21295
|
`).join('')}
|
|
20725
21296
|
</div>`
|
|
@@ -20740,6 +21311,13 @@ window.handleAction = async (orderId, action, idx, needsEvidence, hasLogisticsSe
|
|
|
20740
21311
|
return
|
|
20741
21312
|
}
|
|
20742
21313
|
if (action === 'noop_in_person') return
|
|
21314
|
+
const confirmText = {
|
|
21315
|
+
ship: t('确认已经发货?发货后买家将看到物流信息,超时/虚假发货可能进入争议或判责。'),
|
|
21316
|
+
pickup: t('确认已揽收并回传凭证?请确保单号或揽收说明真实可追踪。'),
|
|
21317
|
+
deliver: t('确认已投递?请确保投递凭证真实,买家仍需确认收货后才结算。'),
|
|
21318
|
+
confirm: t('确认收货?escrow 将进入结算流程,无法撤销。'),
|
|
21319
|
+
}[action]
|
|
21320
|
+
if (confirmText && !confirm(confirmText)) return
|
|
20743
21321
|
|
|
20744
21322
|
let evidDesc = (needsEvidence || hasLogisticsSelector)
|
|
20745
21323
|
? (document.getElementById(`evid-${idx}`)?.value?.trim() || '') : ''
|
|
@@ -20758,15 +21336,21 @@ window.handleAction = async (orderId, action, idx, needsEvidence, hasLogisticsSe
|
|
|
20758
21336
|
let logisticsCompanyId = ''
|
|
20759
21337
|
if (hasLogisticsSelector) {
|
|
20760
21338
|
const sel = document.getElementById(`logi-select-${idx}`)
|
|
20761
|
-
|
|
20762
|
-
if (!logisticsCompanyId) { msgEl.innerHTML = alert$('error', t('请选择物流公司')); return }
|
|
20763
|
-
const companyName = sel.options[sel.selectedIndex]?.text || logisticsCompanyId
|
|
21339
|
+
const choice = sel?.value || 'self'
|
|
20764
21340
|
const trackingInp = document.getElementById(`tracking-${idx}`)
|
|
20765
21341
|
const trackingNo = trackingInp?.value?.trim() || ''
|
|
20766
|
-
if (
|
|
20767
|
-
|
|
21342
|
+
if (choice === 'self') {
|
|
21343
|
+
// 自发货:不绑定物流公司(logistics_id 留空 → seller self-fulfill 流转)
|
|
21344
|
+
logisticsCompanyId = ''
|
|
21345
|
+
evidDesc = trackingNo
|
|
21346
|
+
? `${t('快递单号:')}${trackingNo}${t('(自己发货)')}`
|
|
21347
|
+
: t('卖家自己发货(自提自送)—— 后续由你揽收 / 运输 / 送达')
|
|
20768
21348
|
} else {
|
|
20769
|
-
|
|
21349
|
+
logisticsCompanyId = choice
|
|
21350
|
+
const companyName = sel.options[sel.selectedIndex]?.text || logisticsCompanyId
|
|
21351
|
+
evidDesc = trackingNo
|
|
21352
|
+
? `${t('快递单号:')}${trackingNo} · ${companyName}`
|
|
21353
|
+
: `${t('已交付物流公司:')}${companyName}${t(',快递单号待物流揽收后回传')}`
|
|
20770
21354
|
}
|
|
20771
21355
|
}
|
|
20772
21356
|
|
|
@@ -20788,6 +21372,107 @@ window.handleAction = async (orderId, action, idx, needsEvidence, hasLogisticsSe
|
|
|
20788
21372
|
}
|
|
20789
21373
|
}
|
|
20790
21374
|
|
|
21375
|
+
// ─── 卖家拒单(decline) + 临时判责举证(contest_decline)─── RFC-007 stage 2/3/5 ───────────────
|
|
21376
|
+
// reason code 元数据(与后端 orders-action.ts DECLINE_REASON_CODES / OBJECTIVE_DECLINE_REASONS 对齐)。
|
|
21377
|
+
// objective(客观声称)→ 临时判责,需在举证窗口内发起仲裁,不是自动免责;subjective(主观)→ 立即按卖家违约、买家退款。
|
|
21378
|
+
const DECLINE_REASONS = [
|
|
21379
|
+
{ code: 'stock_consumed_concurrent', objective: true, zh: '并发售罄(库存被同时下单耗尽)', en: 'Stock consumed by a concurrent order' },
|
|
21380
|
+
{ code: 'stale_price_snapshot', objective: true, zh: '价格快照过期(下单价已失效)', en: 'Stale price snapshot' },
|
|
21381
|
+
{ code: 'force_majeure', objective: true, zh: '不可抗力', en: 'Force majeure' },
|
|
21382
|
+
{ code: 'price_regret', objective: false, zh: '价格反悔(不想按此价卖)', en: 'Price regret' },
|
|
21383
|
+
{ code: 'cherry_pick', objective: false, zh: '挑单(选择性拒单)', en: 'Cherry-picking' },
|
|
21384
|
+
{ code: 'other', objective: false, zh: '其他(主观)', en: 'Other (subjective)' },
|
|
21385
|
+
]
|
|
21386
|
+
function declineReasonOptions() {
|
|
21387
|
+
const en = window._lang === 'en'
|
|
21388
|
+
return DECLINE_REASONS.map(r => `<option value="${r.code}" data-objective="${r.objective ? '1' : '0'}">${en ? r.en : r.zh}${r.objective ? ` · ${t('客观')}` : ` · ${t('主观')}`}</option>`).join('')
|
|
21389
|
+
}
|
|
21390
|
+
// 选中 reason 后的诚实后果提示(主观 vs 客观,绝不把客观拒单说成自动免责)
|
|
21391
|
+
window.onDeclineReasonChange = () => {
|
|
21392
|
+
const sel = document.getElementById('decline-reason')
|
|
21393
|
+
const note = document.getElementById('decline-consequence')
|
|
21394
|
+
if (!sel || !note) return
|
|
21395
|
+
const objective = sel.selectedOptions[0]?.dataset?.objective === '1'
|
|
21396
|
+
if (!sel.value) { note.innerHTML = ''; return }
|
|
21397
|
+
note.innerHTML = objective
|
|
21398
|
+
? `<div style="background:#fffbeb;border:1px solid #fcd34d;color:#92400e;border-radius:8px;padding:10px;font-size:12px;line-height:1.6">⚠️ ${t('客观理由:订单将先转入【临时判责·卖家违约】并冻结结算。你需在举证窗口内发起仲裁举证翻案 —— 这不是自动免责;窗口过期未举证将按违约终结、买家退款。')}</div>`
|
|
21399
|
+
: `<div style="background:#fef2f2;border:1px solid #fca5a5;color:#991b1b;border-radius:8px;padding:10px;font-size:12px;line-height:1.6">⚠️ ${t('主观理由:将立即按【卖家违约】处理,买家全额退款。此操作不可撤销。')}</div>`
|
|
21400
|
+
}
|
|
21401
|
+
window.openDeclineModal = (orderId) => {
|
|
21402
|
+
const en = window._lang === 'en'
|
|
21403
|
+
_openModal(`
|
|
21404
|
+
<div style="max-width:440px">
|
|
21405
|
+
<h3 style="font-size:16px;font-weight:700;margin-bottom:6px">${t('拒绝接单')}</h3>
|
|
21406
|
+
<div style="font-size:12px;color:#6b7280;margin-bottom:12px">${t('拒单会影响买家。请如实选择理由 —— 系统按理由区分【主观违约】与【客观临时判责】,后者需举证。')}</div>
|
|
21407
|
+
<div class="form-group">
|
|
21408
|
+
<label class="form-label">${t('拒单理由')} <span style="color:#dc2626">*</span></label>
|
|
21409
|
+
<select class="form-control" id="decline-reason" onchange="onDeclineReasonChange()">
|
|
21410
|
+
<option value="">${en ? '— select a reason —' : '— 请选择理由 —'}</option>
|
|
21411
|
+
${declineReasonOptions()}
|
|
21412
|
+
</select>
|
|
21413
|
+
</div>
|
|
21414
|
+
<div id="decline-consequence" style="margin-bottom:10px"></div>
|
|
21415
|
+
<div class="form-group">
|
|
21416
|
+
<label class="form-label">${t('说明')} <span style="font-size:11px;color:#9ca3af;font-weight:400">${t('(可选)')}</span></label>
|
|
21417
|
+
<textarea class="form-control" id="decline-notes" rows="2" placeholder="${t('补充说明(客观理由建议写清,便于后续举证)')}"></textarea>
|
|
21418
|
+
</div>
|
|
21419
|
+
<div id="decline-msg"></div>
|
|
21420
|
+
<div style="display:flex;gap:8px;margin-top:8px">
|
|
21421
|
+
<button class="btn btn-outline" style="flex:1" onclick="closeModal()">${t('取消')}</button>
|
|
21422
|
+
<button class="btn btn-danger" style="flex:1;background:#dc2626;border-color:#dc2626;color:#fff" onclick="submitDecline('${orderId}')">${t('确认拒单')}</button>
|
|
21423
|
+
</div>
|
|
21424
|
+
</div>`)
|
|
21425
|
+
}
|
|
21426
|
+
window.submitDecline = async (orderId) => {
|
|
21427
|
+
const code = document.getElementById('decline-reason')?.value || ''
|
|
21428
|
+
const notes = document.getElementById('decline-notes')?.value?.trim() || ''
|
|
21429
|
+
const msg = document.getElementById('decline-msg')
|
|
21430
|
+
if (!code) { if (msg) msg.innerHTML = alert$('error', t('请选择拒单理由')); return }
|
|
21431
|
+
if (msg) msg.innerHTML = `<div class="alert alert-info"><span class="spinner"></span>${t('处理中...')}</div>`
|
|
21432
|
+
const res = await POST(`/orders/${orderId}/action`, { action: 'decline', decline_reason_code: code, notes })
|
|
21433
|
+
if (res.error) { if (msg) msg.innerHTML = alert$('error', res.error); return }
|
|
21434
|
+
closeModal()
|
|
21435
|
+
toast$(res.outcome === 'fault_seller_provisional'
|
|
21436
|
+
? t('已拒单 — 临时判责,请在举证窗口内发起仲裁')
|
|
21437
|
+
: t('已拒单 — 已按卖家违约处理,买家将获退款'), 'success')
|
|
21438
|
+
setTimeout(() => renderOrderDetail(document.getElementById('app'), orderId), 1000)
|
|
21439
|
+
}
|
|
21440
|
+
// 临时判责举证面板:仅卖家、订单 fault_seller + decline_objective_pending=1 + 未结算时显示
|
|
21441
|
+
function sellerDeclineContestPanel(order, orderId, isSeller) {
|
|
21442
|
+
if (!isSeller || order.status !== 'fault_seller' || Number(order.decline_objective_pending) !== 1 || order.settled_fault_at) return ''
|
|
21443
|
+
const contested = Number(order.decline_contested) === 1
|
|
21444
|
+
const deadline = order.decline_contest_deadline ? fmtTime(order.decline_contest_deadline) : ''
|
|
21445
|
+
if (contested) {
|
|
21446
|
+
return `<div class="card" style="padding:14px;margin-top:10px;border-left:3px solid #4f46e5;background:#eef2ff">
|
|
21447
|
+
<div style="font-size:13px;font-weight:600;color:#3730a3;margin-bottom:4px">⚖️ ${t('已发起仲裁举证')}</div>
|
|
21448
|
+
<div style="font-size:12px;color:#374151;line-height:1.6">${t('自动终结已暂停,等待仲裁员裁决:维持→免责全退+退质押;驳回→按违约结算。')}</div>
|
|
21449
|
+
</div>`
|
|
21450
|
+
}
|
|
21451
|
+
return `<div class="card" style="padding:14px;margin-top:10px;border-left:3px solid #d97706;background:#fffbeb">
|
|
21452
|
+
<div style="font-size:13px;font-weight:600;color:#92400e;margin-bottom:4px">⏳ ${t('临时判责 — 举证窗口开放')}</div>
|
|
21453
|
+
<div style="font-size:12px;color:#7f1d1d;line-height:1.6;margin-bottom:8px">
|
|
21454
|
+
${t('你以客观理由拒单,订单暂判【卖家违约】但尚未结算。你可在窗口内发起人工仲裁举证翻案 —— 这不是自动免责。')}
|
|
21455
|
+
${deadline ? `<br><strong>${t('举证截止')}:</strong> ${escHtml(deadline)} · ${t('过期未举证将按违约终结、买家退款。')}` : ''}
|
|
21456
|
+
</div>
|
|
21457
|
+
<div class="form-group">
|
|
21458
|
+
<label class="form-label">${t('举证说明')} <span style="color:#dc2626">*</span></label>
|
|
21459
|
+
<textarea class="form-control" id="contest-evidence" rows="3" placeholder="${t('客观说明拒单理由的证据(如并发订单号、价格变更时间、不可抗力凭证)')}"></textarea>
|
|
21460
|
+
</div>
|
|
21461
|
+
<div id="contest-msg"></div>
|
|
21462
|
+
<button class="btn btn-primary" style="width:100%" onclick="submitContestDecline('${orderId}')">${t('提交举证 / 发起仲裁')}</button>
|
|
21463
|
+
</div>`
|
|
21464
|
+
}
|
|
21465
|
+
window.submitContestDecline = async (orderId) => {
|
|
21466
|
+
const evid = document.getElementById('contest-evidence')?.value?.trim() || ''
|
|
21467
|
+
const msg = document.getElementById('contest-msg')
|
|
21468
|
+
if (!evid) { if (msg) msg.innerHTML = alert$('error', t('请填写举证说明')); return }
|
|
21469
|
+
if (msg) msg.innerHTML = `<div class="alert alert-info"><span class="spinner"></span>${t('提交中...')}</div>`
|
|
21470
|
+
const res = await POST(`/orders/${orderId}/action`, { action: 'contest_decline', evidence_description: evid })
|
|
21471
|
+
if (res.error) { if (msg) msg.innerHTML = alert$('error', res.error); return }
|
|
21472
|
+
toast$(t('已发起仲裁举证 — 等待仲裁员裁决'), 'success')
|
|
21473
|
+
setTimeout(() => renderOrderDetail(document.getElementById('app'), orderId), 1000)
|
|
21474
|
+
}
|
|
21475
|
+
|
|
20791
21476
|
// S8: 检查买家是否完成首单,若是 + ShareCtx 还有 sponsor → 一次性致谢 toast + 清 ctx
|
|
20792
21477
|
async function maybeThankSponsorAndClear() {
|
|
20793
21478
|
const ctx = readShareCtx()
|
|
@@ -22271,48 +22956,61 @@ async function renderSeller(app) {
|
|
|
22271
22956
|
}
|
|
22272
22957
|
|
|
22273
22958
|
app.innerHTML = shell(loading$(), 'seller')
|
|
22274
|
-
const [productsRaw, ordersRaw, mySkillsRaw, quotaRaw, insightsRaw] = await Promise.all([
|
|
22959
|
+
const [productsRaw, ordersRaw, mySkillsRaw, quotaRaw, insightsRaw, returnsRaw] = await Promise.all([
|
|
22275
22960
|
GET('/my-products'),
|
|
22276
22961
|
GET('/orders'),
|
|
22277
22962
|
GET('/skills/mine'),
|
|
22278
22963
|
GET('/seller/quota-status'),
|
|
22279
22964
|
GET('/seller/insights').catch(() => null),
|
|
22965
|
+
GET('/return-requests?role=seller').catch(() => ({ items: [] })),
|
|
22280
22966
|
])
|
|
22281
22967
|
const mySkills = Array.isArray(mySkillsRaw) ? mySkillsRaw : []
|
|
22282
22968
|
const orders = Array.isArray(ordersRaw) ? ordersRaw : []
|
|
22283
22969
|
const products = Array.isArray(productsRaw) ? productsRaw : []
|
|
22284
22970
|
const quota = (quotaRaw && !quotaRaw.error) ? quotaRaw : null
|
|
22285
22971
|
const insights = (insightsRaw && !insightsRaw.error) ? insightsRaw : null
|
|
22972
|
+
const returns = Array.isArray(returnsRaw?.items) ? returnsRaw.items : []
|
|
22286
22973
|
|
|
22287
22974
|
const pendingOrders = orders.filter(o => ['paid', 'accepted'].includes(o.status) && o.seller_id === state.user.id)
|
|
22975
|
+
const paidOrders = orders.filter(o => o.status === 'paid' && o.seller_id === state.user.id)
|
|
22976
|
+
const acceptedOrders = orders.filter(o => o.status === 'accepted' && o.seller_id === state.user.id)
|
|
22288
22977
|
const myProducts = products
|
|
22289
22978
|
|
|
22290
22979
|
// KPI(基于最近 50 单的派生统计)+ 今日维度
|
|
22291
22980
|
const mySoldOrders = orders.filter(o => o.seller_id === state.user.id)
|
|
22292
22981
|
const today = new Date().toISOString().slice(0, 10)
|
|
22293
22982
|
const todayOrders = mySoldOrders.filter(o => (o.created_at || '').startsWith(today))
|
|
22294
|
-
const
|
|
22983
|
+
const kpiPaid = paidOrders.length
|
|
22984
|
+
const kpiAccepted = acceptedOrders.length
|
|
22295
22985
|
const kpiInTransit = mySoldOrders.filter(o => ['shipped','picked_up','in_transit','delivered'].includes(o.status)).length
|
|
22986
|
+
const kpiDisputes = mySoldOrders.filter(o => o.status === 'disputed').length
|
|
22987
|
+
const kpiProvisionalDeclines = mySoldOrders.filter(o => o.status === 'fault_seller' && Number(o.decline_objective_pending) === 1 && !o.settled_fault_at).length
|
|
22988
|
+
const kpiReturnExceptions = returns.filter(r => ['pending','picked_up','rejected','accepted_pickup_pending'].includes(r.status)).length
|
|
22989
|
+
const kpiExceptions = kpiDisputes + kpiProvisionalDeclines + kpiReturnExceptions
|
|
22296
22990
|
const kpiSales = mySoldOrders.filter(o => o.status === 'completed').reduce((s,o) => s + Number(o.total_amount || 0), 0)
|
|
22297
22991
|
const kpiTodayCount = todayOrders.length
|
|
22298
22992
|
const kpiTodaySales = todayOrders.filter(o => o.status === 'completed').reduce((s,o) => s + Number(o.total_amount || 0), 0)
|
|
22299
22993
|
const sellerKpis = `
|
|
22300
|
-
<div style="display:grid;grid-template-columns:repeat(
|
|
22994
|
+
<div style="display:grid;grid-template-columns:repeat(5,1fr);gap:6px;margin-bottom:14px">
|
|
22301
22995
|
<div class="card" style="padding:10px;text-align:center;background:linear-gradient(135deg,#eff6ff,#dbeafe);border-color:#bfdbfe">
|
|
22302
22996
|
<div style="font-size:18px;font-weight:800;color:#1d4ed8">${kpiTodayCount}</div>
|
|
22303
22997
|
<div style="font-size:10px;color:#6b7280;margin-top:2px">${t('今日订单')}</div>
|
|
22304
22998
|
</div>
|
|
22305
|
-
<div class="card" style="padding:10px;text-align:center;background:${
|
|
22306
|
-
<div style="font-size:18px;font-weight:800;color:${
|
|
22999
|
+
<div class="card" style="padding:10px;text-align:center;background:${kpiPaid > 0 ? 'linear-gradient(135deg,#fef3c7,#fde68a)' : '#f9fafb'};border-color:${kpiPaid > 0 ? '#fcd34d' : '#e5e7eb'}">
|
|
23000
|
+
<div style="font-size:18px;font-weight:800;color:${kpiPaid > 0 ? '#b45309' : '#9ca3af'}">${kpiPaid}</div>
|
|
23001
|
+
<div style="font-size:10px;color:#6b7280;margin-top:2px">${t('待接单')}</div>
|
|
23002
|
+
</div>
|
|
23003
|
+
<div class="card" style="padding:10px;text-align:center;background:${kpiAccepted > 0 ? 'linear-gradient(135deg,#fff7ed,#fed7aa)' : '#f9fafb'};border-color:${kpiAccepted > 0 ? '#fdba74' : '#e5e7eb'}">
|
|
23004
|
+
<div style="font-size:18px;font-weight:800;color:${kpiAccepted > 0 ? '#c2410c' : '#9ca3af'}">${kpiAccepted}</div>
|
|
22307
23005
|
<div style="font-size:10px;color:#6b7280;margin-top:2px">${t('待发货')}</div>
|
|
22308
23006
|
</div>
|
|
22309
23007
|
<div class="card" style="padding:10px;text-align:center">
|
|
22310
23008
|
<div style="font-size:18px;font-weight:800;color:#374151">${kpiInTransit}</div>
|
|
22311
23009
|
<div style="font-size:10px;color:#6b7280;margin-top:2px">${t('在途')}</div>
|
|
22312
23010
|
</div>
|
|
22313
|
-
<div class="card" style="padding:10px;text-align:center;background
|
|
22314
|
-
<div style="font-size:
|
|
22315
|
-
<div style="font-size:10px;color:#6b7280;margin-top:2px">${t('
|
|
23011
|
+
<div class="card" style="padding:10px;text-align:center;background:${kpiExceptions > 0 ? 'linear-gradient(135deg,#fef2f2,#fee2e2)' : '#f9fafb'};border-color:${kpiExceptions > 0 ? '#fca5a5' : '#e5e7eb'}">
|
|
23012
|
+
<div style="font-size:18px;font-weight:800;color:${kpiExceptions > 0 ? '#dc2626' : '#9ca3af'}">${kpiExceptions}</div>
|
|
23013
|
+
<div style="font-size:10px;color:#6b7280;margin-top:2px">${t('异常')}</div>
|
|
22316
23014
|
</div>
|
|
22317
23015
|
</div>
|
|
22318
23016
|
`
|
|
@@ -22394,9 +23092,7 @@ async function renderSeller(app) {
|
|
|
22394
23092
|
</div>
|
|
22395
23093
|
` : ''
|
|
22396
23094
|
|
|
22397
|
-
const
|
|
22398
|
-
? `<div class="empty" style="padding:24px"><div class="empty-icon">✅</div><div class="empty-text">${t('暂无待处理订单')}</div></div>`
|
|
22399
|
-
: batchAcceptBar + batchShipBar + pendingOrders.map(o => `
|
|
23095
|
+
const pendingOrderRows = (list) => list.map(o => `
|
|
22400
23096
|
<div class="card" onclick="navigate('#order/${o.id}')" style="cursor:pointer">
|
|
22401
23097
|
<div class="order-item">
|
|
22402
23098
|
<div class="order-icon">📦</div>
|
|
@@ -22408,6 +23104,39 @@ async function renderSeller(app) {
|
|
|
22408
23104
|
<div class="order-amount">${o.total_amount} WAZ</div>
|
|
22409
23105
|
</div>
|
|
22410
23106
|
</div>`).join('')
|
|
23107
|
+
const acceptHtml = paidOrders.length === 0
|
|
23108
|
+
? `<div class="empty" style="padding:24px"><div class="empty-icon">✅</div><div class="empty-text">${t('暂无待处理订单')}</div></div>`
|
|
23109
|
+
: batchAcceptBar + pendingOrderRows(paidOrders)
|
|
23110
|
+
const shipHtml = acceptedOrders.length === 0
|
|
23111
|
+
? `<div class="empty" style="padding:18px"><div class="empty-icon">📦</div><div class="empty-text">${t('暂无待发货订单')}</div></div>`
|
|
23112
|
+
: batchShipBar + pendingOrderRows(acceptedOrders)
|
|
23113
|
+
const exceptionOrders = mySoldOrders.filter(o => o.status === 'disputed' || (o.status === 'fault_seller' && Number(o.decline_objective_pending) === 1 && !o.settled_fault_at))
|
|
23114
|
+
const exceptionReturns = returns.filter(r => ['pending','picked_up','rejected','accepted_pickup_pending'].includes(r.status))
|
|
23115
|
+
const exceptionsHtml = (exceptionOrders.length + exceptionReturns.length) === 0
|
|
23116
|
+
? `<div class="empty" style="padding:18px"><div class="empty-icon">✅</div><div class="empty-text">${t('暂无异常待处理')}</div></div>`
|
|
23117
|
+
: `
|
|
23118
|
+
${exceptionOrders.map(o => `
|
|
23119
|
+
<div class="card" onclick="navigate('#order/${o.id}')" style="cursor:pointer;border-left:3px solid ${o.status === 'disputed' ? '#dc2626' : '#f59e0b'};padding:10px 12px;margin-bottom:8px">
|
|
23120
|
+
<div style="display:flex;justify-content:space-between;gap:10px;align-items:center">
|
|
23121
|
+
<div style="min-width:0">
|
|
23122
|
+
<div style="font-size:13px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${escHtml(o.product_title)}</div>
|
|
23123
|
+
<div style="font-size:11px;color:#6b7280;margin-top:2px">${o.status === 'disputed' ? t('争议订单') : t('临时判责拒单')} · ${fmtTime(o.updated_at || o.created_at)}</div>
|
|
23124
|
+
</div>
|
|
23125
|
+
<span style="font-size:11px;color:#dc2626;font-weight:600;white-space:nowrap">${t('查看处理')} →</span>
|
|
23126
|
+
</div>
|
|
23127
|
+
</div>`).join('')}
|
|
23128
|
+
${exceptionReturns.map(r => `
|
|
23129
|
+
<div class="card" onclick="navigate('#order/${r.order_id}')" style="cursor:pointer;border-left:3px solid #0891b2;padding:10px 12px;margin-bottom:8px">
|
|
23130
|
+
<div style="display:flex;justify-content:space-between;gap:10px;align-items:center">
|
|
23131
|
+
<div style="min-width:0">
|
|
23132
|
+
<div style="font-size:13px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${escHtml(r.product_title)}</div>
|
|
23133
|
+
<div style="font-size:11px;color:#6b7280;margin-top:2px">${t('退货处理')} · ${r.refund_amount} WAZ · ${fmtTime(r.created_at)}</div>
|
|
23134
|
+
</div>
|
|
23135
|
+
<span style="font-size:11px;color:#0891b2;font-weight:600;white-space:nowrap">${t('查看订单')} →</span>
|
|
23136
|
+
</div>
|
|
23137
|
+
</div>`).join('')}
|
|
23138
|
+
<button class="btn btn-outline btn-sm" style="width:auto;font-size:12px" onclick="navigate('#returns')">↩ ${t('退货管理')}</button>
|
|
23139
|
+
`
|
|
22411
23140
|
|
|
22412
23141
|
const activeProducts = products.filter(p => p.status === 'active')
|
|
22413
23142
|
const warehouseProducts = products.filter(p => p.status !== 'active' && p.status !== 'deleted')
|
|
@@ -22587,11 +23316,24 @@ async function renderSeller(app) {
|
|
|
22587
23316
|
// 各 sub-tab 内容
|
|
22588
23317
|
const dashboardSection = sellerSubTab === 'dashboard' ? `
|
|
22589
23318
|
${sellerKpis}
|
|
23319
|
+
${sellerRecoveryReminderHTML()}
|
|
22590
23320
|
${stockAlertBanner}
|
|
22591
23321
|
${quotaBanner}
|
|
22592
23322
|
${pendingOrders.length > 0 ? `<div class="alert alert-warning">📬 ${t('你有')} ${pendingOrders.length} ${t('个订单需要处理')}</div>` : ''}
|
|
22593
|
-
<div style="
|
|
22594
|
-
|
|
23323
|
+
<div style="display:grid;grid-template-columns:1fr;gap:10px;margin-bottom:12px">
|
|
23324
|
+
<section>
|
|
23325
|
+
<div style="font-weight:700;margin-bottom:8px">📬 ${t('待接单')}</div>
|
|
23326
|
+
${acceptHtml}
|
|
23327
|
+
</section>
|
|
23328
|
+
<section>
|
|
23329
|
+
<div style="font-weight:700;margin-bottom:8px">📦 ${t('待发货')}</div>
|
|
23330
|
+
${shipHtml}
|
|
23331
|
+
</section>
|
|
23332
|
+
<section>
|
|
23333
|
+
<div style="font-weight:700;margin-bottom:8px">⚠ ${t('退货 · 争议 · 异常')}</div>
|
|
23334
|
+
${exceptionsHtml}
|
|
23335
|
+
</section>
|
|
23336
|
+
</div>
|
|
22595
23337
|
${insightsBlock}
|
|
22596
23338
|
` : ''
|
|
22597
23339
|
|
|
@@ -23564,6 +24306,7 @@ function skillCard(s, context) {
|
|
|
23564
24306
|
<span class="badge badge-green">${t('运行中')}</span>
|
|
23565
24307
|
</div>
|
|
23566
24308
|
<div style="font-size:13px;color:#6b7280;margin-top:8px">${s.description}</div>
|
|
24309
|
+
${s.skill_type === 'auto_accept' ? `<div style="font-size:11px;color:#92400e;background:#fffbeb;border:1px solid #fde68a;border-radius:6px;padding:6px 8px;margin-top:8px;line-height:1.5">⚠️ ${t('自动接单仍受约束:接单后须按时发货,超时按卖家违约判责;跳过「拒绝接单」窗口;不校验库存。')}</div>` : ''}
|
|
23567
24310
|
</div>`
|
|
23568
24311
|
}
|
|
23569
24312
|
// buyer context
|
|
@@ -23584,7 +24327,7 @@ function skillCard(s, context) {
|
|
|
23584
24327
|
|
|
23585
24328
|
const SKILL_CONFIG_HINTS = {
|
|
23586
24329
|
catalog_sync: '目录同步:订阅此 Skill 的买家在搜索时会优先看到你的商品。成交后协议自动给你 0.5% 推荐佣金。',
|
|
23587
|
-
auto_accept: '
|
|
24330
|
+
auto_accept: '⚠️ 自动接单:买家下单后系统自动接受(省去手动「接单」)。注意责任不变:①接单后你仍须按时发货,超时/不发货仍按卖家违约判责;②会跳过「拒绝接单」窗口,订单自动进入已接单后无法再拒单;③不校验库存,售罄商品也会被自动接单,你仍要履约或担责。建议用金额范围/每日上限控制风险。',
|
|
23588
24331
|
price_negotiation: '价格协商:允许买家 Agent 在你设定的折扣范围内自动议价,减少沟通成本。',
|
|
23589
24332
|
quality_guarantee: '质量承诺:额外质押 WAZ 作为品质担保,增强买家信任,适合高客单价商品。',
|
|
23590
24333
|
instant_ship: '极速发货:承诺接单后 24h 内发货,违约自动赔付。适合有充足现货的卖家。',
|
|
@@ -25285,7 +26028,7 @@ async function renderWallet(app) {
|
|
|
25285
26028
|
if (!state.user) { renderLogin(); return }
|
|
25286
26029
|
app.innerHTML = shell(loading$(), 'wallet')
|
|
25287
26030
|
const userRegion = state.user?.region || 'global'
|
|
25288
|
-
const [wallet, income, deposits, withdrawals, whitelistRes, trust,
|
|
26031
|
+
const [wallet, income, deposits, withdrawals, whitelistRes, trust, rateRes, regionPmRes] = await Promise.all([
|
|
25289
26032
|
// 2026-06-01 fix(BUG-PWA-WALLET): 加 catch 防 #wallet 加载死循环
|
|
25290
26033
|
// 原 GET('/wallet') 无 fallback,一旦该 endpoint 错(网络 / 401 / 后端 500),
|
|
25291
26034
|
// 整个 Promise.all reject,renderWallet 早退,"加载中..." 永驻
|
|
@@ -25295,7 +26038,6 @@ async function renderWallet(app) {
|
|
|
25295
26038
|
GET('/wallet/withdrawals').catch(() => []),
|
|
25296
26039
|
GET('/wallet/whitelist').catch(() => ({ whitelist: [] })),
|
|
25297
26040
|
GET('/agents/me/reputation').catch(() => null),
|
|
25298
|
-
GET('/tokenomics/status').catch(() => null),
|
|
25299
26041
|
GET('/wallet/rate').catch(() => null),
|
|
25300
26042
|
GET('/payment-methods/for-region?region=' + encodeURIComponent(userRegion)).catch(() => ({ items: [] })),
|
|
25301
26043
|
])
|
|
@@ -25334,7 +26076,7 @@ async function renderWallet(app) {
|
|
|
25334
26076
|
const earned = Number(wallet.earned || 0)
|
|
25335
26077
|
const totalAssets = balance + escrowed + staked
|
|
25336
26078
|
|
|
25337
|
-
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 }
|
|
25338
26080
|
const dep = (deposits || []).slice(0, 5)
|
|
25339
26081
|
const wdr = (withdrawals || []).slice(0, 5)
|
|
25340
26082
|
const pendingWdr = wdr.filter(w => w.status === 'pending')
|
|
@@ -25355,48 +26097,11 @@ async function renderWallet(app) {
|
|
|
25355
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>`
|
|
25356
26098
|
: ''
|
|
25357
26099
|
|
|
25358
|
-
//
|
|
25359
|
-
const userLevel = state.user?.user_level || { level: 0, name: t('游客'), nextThreshold: 1 }
|
|
25360
|
-
const lifetimeScore = Number(state.user?.lifetime_score || 0)
|
|
25361
|
-
const distCap = Number(tokenomics?.distribution_cap ?? 1.0)
|
|
25362
|
-
const healthLevel = tokenomics?.health_level || 'cold_start'
|
|
25363
|
-
const pausedRecent = tokenomics?.paused_recent
|
|
25364
|
-
// P2 #7:R11 硬停 banner(仅在最近 7 天触发过时显示)
|
|
25365
|
-
const pausedBanner = pausedRecent ? `
|
|
25366
|
-
<div class="card" style="margin-bottom:12px;background:#fef3c7;border-color:#fde68a;padding:10px 14px">
|
|
25367
|
-
<div style="font-size:13px;color:#92400e;font-weight:600;margin-bottom:2px">⚠️ ${t('结算暂停保护中')}</div>
|
|
25368
|
-
<div style="font-size:11px;color:#78350f">${t('基金池水位保护已触发,本期 pending 奖金已保留,将在水位恢复后自动结算。')}</div>
|
|
25369
|
-
</div>` : ''
|
|
25370
|
-
const healthColor = {
|
|
25371
|
-
healthy: { bg: '#dcfce7', fg: '#166534', label: t('健康') },
|
|
25372
|
-
normal: { bg: '#dbeafe', fg: '#1e40af', label: t('正常') },
|
|
25373
|
-
strained: { bg: '#fef3c7', fg: '#92400e', label: t('紧张') },
|
|
25374
|
-
critical: { bg: '#fee2e2', fg: '#991b1b', label: t('告急') },
|
|
25375
|
-
cold_start: { bg: '#f3f4f6', fg: '#6b7280', label: t('启动期') },
|
|
25376
|
-
}[healthLevel] || { bg: '#f3f4f6', fg: '#6b7280', label: '—' }
|
|
25377
|
-
const levelCard = `
|
|
25378
|
-
<div class="card" style="margin-bottom:12px;background:linear-gradient(135deg,#fff7ed 0%,#fef3c7 100%);border-color:#fde68a">
|
|
25379
|
-
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
|
|
25380
|
-
<div>
|
|
25381
|
-
<div style="font-size:11px;color:#92400e;font-weight:600;margin-bottom:4px">🏅 ${t('我的等级')}</div>
|
|
25382
|
-
<div style="font-size:18px;font-weight:800;color:#78350f">L${userLevel.level} ${escHtml(userLevel.name)}</div>
|
|
25383
|
-
<div style="font-size:10px;color:#a16207;margin-top:2px">${t('累计')}:¥${lifetimeScore.toFixed(0)}${userLevel.nextThreshold ? ` · ${t('下一档')} ¥${Number(userLevel.nextThreshold).toLocaleString()}` : ''}</div>
|
|
25384
|
-
</div>
|
|
25385
|
-
<div>
|
|
25386
|
-
<div style="font-size:11px;color:#92400e;font-weight:600;margin-bottom:4px">💎 ${t('本期分配率')}</div>
|
|
25387
|
-
<div style="font-size:18px;font-weight:800;color:#78350f">${(distCap * 100).toFixed(0)}%</div>
|
|
25388
|
-
<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>
|
|
25389
|
-
</div>
|
|
25390
|
-
</div>
|
|
25391
|
-
</div>`
|
|
26100
|
+
// 奖励等级 / 分配率 / 池子健康度卡已移除(匹配奖励引擎已切除 #401 / #PR-D);钱包只展示中性资产 + 参与记录。
|
|
25392
26101
|
|
|
25393
26102
|
app.innerHTML = shell(`
|
|
25394
26103
|
<h1 class="page-title">💰 ${t('我的钱包')}</h1>
|
|
25395
26104
|
|
|
25396
|
-
${pausedBanner}
|
|
25397
|
-
|
|
25398
|
-
${levelCard}
|
|
25399
|
-
|
|
25400
26105
|
${renderTrustCard(trust)}
|
|
25401
26106
|
|
|
25402
26107
|
<!-- ① 资产总览 -->
|
|
@@ -25421,7 +26126,7 @@ async function renderWallet(app) {
|
|
|
25421
26126
|
|
|
25422
26127
|
<!-- 卖家收入分类速览(仅 seller 显示)- 2026-05-24 匹配列只在 PV 允许地区显示 -->
|
|
25423
26128
|
${state.user?.role === 'seller' ? (() => {
|
|
25424
|
-
const _pvOK =
|
|
26129
|
+
const _pvOK = inc.gates?.matching_rewards_active === true // Category C:匹配已结列读奖励闸门(默认关),不再读 region_pv_enabled
|
|
25425
26130
|
const cols = _pvOK ? 4 : 3
|
|
25426
26131
|
return `
|
|
25427
26132
|
<div class="card" style="margin-bottom:12px;padding:14px">
|
|
@@ -25437,11 +26142,6 @@ async function renderWallet(app) {
|
|
|
25437
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>
|
|
25438
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>
|
|
25439
26144
|
</div>
|
|
25440
|
-
${_pvOK ? `<div style="background:#fefce8;border-radius:6px;padding:8px">
|
|
25441
|
-
<div style="font-size:9px;color:#854d0e">⚛ ${t('匹配已结')}</div>
|
|
25442
|
-
<div style="font-size:14px;font-weight:700;color:#a16207;margin-top:2px">${(inc.binary?.settled_waz||0).toFixed(2)}</div>
|
|
25443
|
-
<div style="font-size:9px;color:#9ca3af">${inc.binary?.settled_count||0} ${t('次')}</div>
|
|
25444
|
-
</div>` : ''}
|
|
25445
26145
|
<div style="background:#fef3c7;border-radius:6px;padding:8px">
|
|
25446
26146
|
<div style="font-size:9px;color:#92400e">⏳ ${t('待结算')}</div>
|
|
25447
26147
|
<div style="font-size:14px;font-weight:700;color:#d97706;margin-top:2px">${escrowed.toFixed(2)}</div>
|
|
@@ -25472,11 +26172,6 @@ async function renderWallet(app) {
|
|
|
25472
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>
|
|
25473
26173
|
</div>`
|
|
25474
26174
|
})()}
|
|
25475
|
-
${getMaxLevels() >= 3 ? `
|
|
25476
|
-
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:10px">
|
|
25477
|
-
<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>
|
|
25478
|
-
<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>
|
|
25479
|
-
</div>` : ''}
|
|
25480
26175
|
${inc.sales.count > 0 ? `
|
|
25481
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>
|
|
25482
26177
|
` : ''}
|
|
@@ -26731,7 +27426,8 @@ window.setClaimVerifyTab = (k) => {
|
|
|
26731
27426
|
}
|
|
26732
27427
|
|
|
26733
27428
|
async function renderClaimTaskDetail(app, taskId) {
|
|
26734
|
-
|
|
27429
|
+
// 'new' 是历史死路由(无创建处理器);商品声明验证从商品页声明区的现成表单发起 → 回验证中心,避免报错页
|
|
27430
|
+
if (!taskId || taskId === 'new') { navigate('#verify'); return }
|
|
26735
27431
|
if (!state.user) { renderLogin(); return }
|
|
26736
27432
|
app.innerHTML = shell(loading$(), 'verify')
|
|
26737
27433
|
const data = await GET(`/claim-tasks/${taskId}`)
|
|
@@ -31089,15 +31785,11 @@ async function renderAdminProtocol(app) {
|
|
|
31089
31785
|
<!-- 协议金库摘要 -->
|
|
31090
31786
|
<div class="card" style="margin-bottom:14px;background:linear-gradient(135deg,#eef2ff,#faf5ff);border-color:#c7d2fe;padding:16px">
|
|
31091
31787
|
<div style="font-size:11px;color:#3730a3;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;margin-bottom:10px">💎 ${t('协议金库摘要')}</div>
|
|
31092
|
-
<div style="display:grid;grid-template-columns:repeat(
|
|
31788
|
+
<div style="display:grid;grid-template-columns:repeat(2,1fr);gap:10px;font-size:12px">
|
|
31093
31789
|
<div style="background:rgba(255,255,255,0.7);border-radius:8px;padding:10px;text-align:center">
|
|
31094
31790
|
<div style="font-size:18px;font-weight:800;color:#4338ca">${Number(tk.pool_balance || 0).toFixed(2)}</div>
|
|
31095
31791
|
<div style="color:#6b7280;margin-top:2px">${t('全球基金池')} WAZ</div>
|
|
31096
31792
|
</div>
|
|
31097
|
-
<div style="background:rgba(255,255,255,0.7);border-radius:8px;padding:10px;text-align:center">
|
|
31098
|
-
<div style="font-size:18px;font-weight:800;color:#7c3aed">${Number(tk.management_bonus || 0).toFixed(2)}</div>
|
|
31099
|
-
<div style="color:#6b7280;margin-top:2px">${t('管理津贴池')} WAZ</div>
|
|
31100
|
-
</div>
|
|
31101
31793
|
<div style="background:rgba(255,255,255,0.7);border-radius:8px;padding:10px;text-align:center">
|
|
31102
31794
|
<div style="font-size:18px;font-weight:800;color:#dc2626">${Number(fund.balance || 0).toFixed(2)}</div>
|
|
31103
31795
|
<div style="color:#6b7280;margin-top:2px">${t('慈善基金')} WAZ</div>
|
|
@@ -31112,6 +31804,9 @@ async function renderAdminProtocol(app) {
|
|
|
31112
31804
|
${adminLinkCard('🛑', t('错误监控'), t('24h 趋势 + burst 告警'), '#admin/errors')}
|
|
31113
31805
|
${adminLinkCard('📨', t('Welcome 提交'), t('#welcome 留下的邮箱订阅 + 建议'), '#admin/public-ideas')}
|
|
31114
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') : ''}
|
|
31115
31810
|
</div>
|
|
31116
31811
|
`, 'admin-protocol')
|
|
31117
31812
|
}
|
|
@@ -33528,7 +34223,7 @@ async function renderMyNotes(app) {
|
|
|
33528
34223
|
<h2 style="font-size:18px;font-weight:700">📝 ${t('我的笔记')}</h2>
|
|
33529
34224
|
${draftCount > 0 ? `<button class="btn btn-outline btn-sm" style="font-size:11px;padding:5px 10px" onclick="openDraftsModal()">📋 ${t('草稿')} ${draftCount}</button>` : ''}
|
|
33530
34225
|
</div>
|
|
33531
|
-
<div style="font-size:11px;color:#6b7280;margin-bottom:14px">${state.user?.mlm_ui_visible !== false ? t('
|
|
34226
|
+
<div style="font-size:11px;color:#6b7280;margin-bottom:14px">${state.user?.mlm_ui_visible !== false ? t('协议按地区层级自动拆分分享返佣(当前预发布期仅 L1)') : t('你所在地区不分多级返佣 — 产生的佣金已自动捐入公益基金')}</div>
|
|
33532
34227
|
<div class="card" style="padding:14px;margin-bottom:14px;background:linear-gradient(135deg,#fef3c7,#fff7ed)">
|
|
33533
34228
|
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:8px;text-align:center">
|
|
33534
34229
|
<div><div style="font-size:18px;font-weight:800;color:#92400e">${notes.length}</div><div style="font-size:10px;color:#9ca3af">${t('笔记数')}</div></div>
|