@seasonkoh/webaz 0.1.24 → 0.1.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/layer0-foundation/L0-1-database/db-backends/pg-backend.js +51 -0
- package/dist/layer0-foundation/L0-1-database/db-backends/sql-dialect-datetime.js +437 -0
- package/dist/layer0-foundation/L0-1-database/db-backends/sql-placeholders.js +98 -0
- package/dist/layer0-foundation/L0-1-database/db.js +65 -0
- package/dist/layer0-foundation/L0-2-state-machine/order-chain.js +13 -11
- package/dist/layer0-foundation/L0-2-state-machine/transitions.js +1 -1
- package/dist/layer0-foundation/L0-5-manifest/manifest.js +13 -11
- package/dist/layer1-agent/L1-1-mcp-server/server.js +288 -208
- package/dist/layer1-agent/L1-2-external-anchor/anchor-engine.js +14 -12
- package/dist/layer2-business/L2-6-notifications/notification-engine.js +8 -5
- package/dist/layer2-business/L2-7-snf/snf-engine.js +16 -14
- package/dist/layer2-business/L2-8-feedback/build-feedback-engine.js +18 -10
- package/dist/layer2-business/L2-9-contribution/build-reputation-engine.js +37 -23
- package/dist/layer2-business/L2-9-contribution/build-task-agent-metadata-store.js +182 -0
- package/dist/layer2-business/L2-9-contribution/build-task-participation.js +47 -0
- package/dist/layer2-business/L2-9-contribution/build-task-read.js +222 -0
- package/dist/layer2-business/L2-9-contribution/build-tasks-engine.js +11 -3
- package/dist/layer2-business/L2-9-contribution/canonical-contribution-target.js +16 -0
- package/dist/layer2-business/L2-9-contribution/contribution-display-envelope.js +40 -0
- package/dist/layer2-business/L2-9-contribution/contribution-score-contract.js +36 -0
- package/dist/layer2-business/L2-9-contribution/contribution-score-evidence.js +61 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/canonical.js +60 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/github-credential.schema.js +140 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/github-fetch-adapter.js +437 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/self-consistency.js +38 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/verifier.js +231 -0
- package/dist/layer2-business/L2-9-contribution/github-credential-ingestion-engine.js +145 -0
- package/dist/layer2-business/L2-9-contribution/github-credential-store.js +115 -0
- package/dist/layer2-business/L2-9-contribution/identity-binding-engine.js +134 -0
- package/dist/layer2-business/L2-9-contribution/identity-binding-store.js +101 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-challenge-engine.js +126 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-challenge-store.js +30 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-discovery.js +55 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-engine.js +109 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-fact-precondition.js +22 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-proof-verifier.js +97 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-read.js +59 -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 +191 -0
- package/dist/layer2-business/L2-9-contribution/task-proposal-store.js +129 -0
- package/dist/layer2-business/L2-notes/note-photo-storage.js +4 -2
- package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +17 -15
- package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +11 -8
- package/dist/layer4-economics/L4-3-reputation/reputation-engine.js +9 -8
- package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +11 -8
- package/dist/layer4-economics/L4-4-skill-market/skill-listing-engine.js +22 -16
- package/dist/pwa/acp-feed.js +13 -1
- package/dist/pwa/admin-bearer-auth.js +21 -0
- package/dist/pwa/contract-fingerprint.js +2 -0
- package/dist/pwa/email-delivery.js +127 -0
- package/dist/pwa/endpoint-actions.js +5 -1
- package/dist/pwa/goal-index.js +8 -8
- package/dist/pwa/human-presence.js +62 -0
- package/dist/pwa/public/app.js +1485 -283
- package/dist/pwa/public/i18n.js +297 -59
- package/dist/pwa/public/index.html +1 -0
- package/dist/pwa/public/openapi.json +5 -5
- package/dist/pwa/public/whitepaper/en/index.html +153 -0
- package/dist/pwa/public/whitepaper/zh-CN/index.html +153 -0
- package/dist/pwa/rate-limit.js +22 -0
- package/dist/pwa/routes/account-deletion.js +15 -13
- package/dist/pwa/routes/addresses.js +10 -9
- package/dist/pwa/routes/admin-admins.js +13 -14
- package/dist/pwa/routes/admin-analytics.js +109 -69
- package/dist/pwa/routes/admin-atomic.js +10 -4
- package/dist/pwa/routes/admin-catalog.js +13 -11
- package/dist/pwa/routes/admin-editor-picks.js +15 -10
- package/dist/pwa/routes/admin-events.js +5 -3
- package/dist/pwa/routes/admin-health.js +2 -1
- package/dist/pwa/routes/admin-moderation.js +50 -29
- package/dist/pwa/routes/admin-ops.js +35 -23
- package/dist/pwa/routes/admin-protocol-params.js +16 -19
- package/dist/pwa/routes/admin-reports.js +23 -21
- package/dist/pwa/routes/admin-tokenomics.js +26 -25
- package/dist/pwa/routes/admin-users-lifecycle.js +37 -40
- package/dist/pwa/routes/admin-users-query.js +65 -53
- package/dist/pwa/routes/admin-verifier-flow.js +82 -41
- package/dist/pwa/routes/admin-verifier-whitelist.js +55 -27
- package/dist/pwa/routes/admin-wallet-ops.js +32 -7
- package/dist/pwa/routes/agent-buy.js +46 -22
- package/dist/pwa/routes/agent-governance.js +52 -56
- package/dist/pwa/routes/ai.js +7 -5
- package/dist/pwa/routes/analytics.js +43 -41
- package/dist/pwa/routes/anchors.js +19 -20
- package/dist/pwa/routes/announcements.js +13 -13
- package/dist/pwa/routes/arbitrator.js +97 -31
- package/dist/pwa/routes/auction.js +157 -116
- package/dist/pwa/routes/auth-login.js +6 -4
- package/dist/pwa/routes/auth-read.js +21 -10
- package/dist/pwa/routes/auth-register.js +111 -26
- package/dist/pwa/routes/auth-sessions.js +12 -11
- package/dist/pwa/routes/blocklist.js +16 -15
- package/dist/pwa/routes/build-feedback.js +10 -9
- package/dist/pwa/routes/build-reputation.js +6 -2
- package/dist/pwa/routes/build-tasks.js +45 -13
- package/dist/pwa/routes/buyer-feeds.js +27 -25
- package/dist/pwa/routes/cart.js +16 -15
- package/dist/pwa/routes/charity.js +212 -150
- package/dist/pwa/routes/chat.js +42 -43
- package/dist/pwa/routes/checkin-tasks.js +10 -9
- package/dist/pwa/routes/checkout-helpers.js +12 -10
- package/dist/pwa/routes/claim-initiators.js +34 -14
- package/dist/pwa/routes/claim-verify.js +86 -53
- package/dist/pwa/routes/claim-voting.js +43 -18
- package/dist/pwa/routes/contribution-identity.js +164 -0
- package/dist/pwa/routes/contribution-score.js +19 -0
- package/dist/pwa/routes/coupons.js +19 -16
- package/dist/pwa/routes/dashboards.js +18 -16
- package/dist/pwa/routes/dispute-cases.js +25 -24
- package/dist/pwa/routes/disputes-read.js +45 -51
- package/dist/pwa/routes/disputes-write.js +124 -61
- package/dist/pwa/routes/evidence.js +9 -9
- package/dist/pwa/routes/external-anchors.js +13 -12
- package/dist/pwa/routes/feedback.js +29 -33
- package/dist/pwa/routes/flash-sales.js +18 -16
- package/dist/pwa/routes/follows.js +25 -24
- package/dist/pwa/routes/governance-auto-deactivate.js +21 -9
- package/dist/pwa/routes/governance-onboarding.js +70 -59
- package/dist/pwa/routes/group-buys.js +22 -22
- package/dist/pwa/routes/growth.js +34 -31
- package/dist/pwa/routes/import-product.js +12 -10
- package/dist/pwa/routes/kyc.js +9 -8
- package/dist/pwa/routes/leaderboard.js +20 -18
- package/dist/pwa/routes/listings.js +23 -22
- package/dist/pwa/routes/logistics.js +10 -8
- package/dist/pwa/routes/manifests.js +27 -27
- package/dist/pwa/routes/me-data.js +23 -21
- package/dist/pwa/routes/notifications.js +7 -6
- package/dist/pwa/routes/offers.js +30 -12
- package/dist/pwa/routes/orders-action.js +51 -29
- package/dist/pwa/routes/orders-create.js +75 -20
- package/dist/pwa/routes/orders-read.js +21 -20
- package/dist/pwa/routes/p2p-products.js +30 -18
- package/dist/pwa/routes/payments-governance.js +61 -56
- package/dist/pwa/routes/peers.js +9 -8
- package/dist/pwa/routes/pin-receipts.js +13 -13
- package/dist/pwa/routes/products-aliases.js +12 -10
- package/dist/pwa/routes/products-claims.js +36 -17
- package/dist/pwa/routes/products-create.js +53 -38
- package/dist/pwa/routes/products-crud.js +17 -16
- package/dist/pwa/routes/products-links.js +49 -26
- package/dist/pwa/routes/products-list.js +6 -4
- package/dist/pwa/routes/products-meta.js +40 -39
- package/dist/pwa/routes/products-update.js +19 -5
- package/dist/pwa/routes/profile-credentials.js +20 -19
- package/dist/pwa/routes/profile-identity.js +14 -13
- package/dist/pwa/routes/profile-location.js +7 -6
- package/dist/pwa/routes/profile-placement.js +20 -19
- package/dist/pwa/routes/profile-prefs.js +11 -11
- package/dist/pwa/routes/promoter.js +58 -66
- package/dist/pwa/routes/public-build-tasks.js +19 -0
- package/dist/pwa/routes/public-utils.js +108 -46
- package/dist/pwa/routes/push.js +16 -15
- package/dist/pwa/routes/ratings.js +92 -32
- package/dist/pwa/routes/recover-key.js +66 -26
- package/dist/pwa/routes/referral.js +37 -52
- package/dist/pwa/routes/reputation.js +3 -2
- package/dist/pwa/routes/returns.js +76 -73
- package/dist/pwa/routes/reviews.js +41 -18
- package/dist/pwa/routes/rewards-apply.js +16 -15
- package/dist/pwa/routes/rewards-auto-downgrade.js +9 -7
- package/dist/pwa/routes/rewards-escrow-expire.js +7 -5
- package/dist/pwa/routes/rfqs.js +163 -85
- package/dist/pwa/routes/search.js +16 -14
- package/dist/pwa/routes/secondhand.js +25 -22
- package/dist/pwa/routes/seller-quota.js +24 -26
- package/dist/pwa/routes/share-redirects.js +60 -55
- package/dist/pwa/routes/shareables-interactions.js +34 -35
- package/dist/pwa/routes/shareables.js +55 -51
- package/dist/pwa/routes/shop-referral.js +58 -0
- package/dist/pwa/routes/shops.js +25 -20
- package/dist/pwa/routes/signaling.js +10 -9
- package/dist/pwa/routes/skill-market.js +16 -16
- package/dist/pwa/routes/skills.js +15 -14
- package/dist/pwa/routes/snf.js +14 -13
- package/dist/pwa/routes/tags.js +10 -9
- package/dist/pwa/routes/task-proposals.js +121 -0
- package/dist/pwa/routes/trial.js +72 -52
- package/dist/pwa/routes/trusted-kpi.js +20 -18
- package/dist/pwa/routes/url-claim.js +67 -28
- package/dist/pwa/routes/users-public.js +62 -70
- package/dist/pwa/routes/variants.js +12 -13
- package/dist/pwa/routes/verifier-user.js +61 -21
- package/dist/pwa/routes/verify-tasks.js +49 -25
- package/dist/pwa/routes/waitlist.js +16 -15
- package/dist/pwa/routes/wallet-read.js +75 -37
- package/dist/pwa/routes/wallet-write.js +12 -9
- package/dist/pwa/routes/webauthn.js +25 -26
- package/dist/pwa/routes/webhooks.js +26 -26
- package/dist/pwa/routes/welcome.js +45 -50
- package/dist/pwa/routes/wishlist-qa.js +29 -32
- package/dist/pwa/server.js +304 -90
- package/dist/version.js +1 -1
- package/package.json +76 -3
package/dist/pwa/public/app.js
CHANGED
|
@@ -549,7 +549,7 @@ async function render(page, params) {
|
|
|
549
549
|
try { await initShareCtx() } catch (e) { console.warn('[ShareCtx]', e) }
|
|
550
550
|
|
|
551
551
|
// 未登录时只允许看登录页、找回密钥页、商品、welcome 预发布页、governance-onboarding 公开页
|
|
552
|
-
if (!state.apiKey && page !== 'login' && page !== 'shop' && page !== 'recover' && page !== 'welcome' && page !== 'governance-onboarding' && page !== '') {
|
|
552
|
+
if (!state.apiKey && page !== 'login' && page !== 'shop' && page !== 'recover' && page !== 'welcome' && page !== 'governance-onboarding' && page !== 'contribute' && page !== '') {
|
|
553
553
|
// 保存目标 hash 以便登录/注册后跳回
|
|
554
554
|
if (location.hash && !['#login', '#recover'].includes(location.hash)) {
|
|
555
555
|
sessionStorage.setItem('webaz_intended_hash', location.hash)
|
|
@@ -562,7 +562,7 @@ async function render(page, params) {
|
|
|
562
562
|
const { status, body } = await apiWithStatus('GET', '/me')
|
|
563
563
|
if (status === 200 && body && !body.error) {
|
|
564
564
|
state.user = body
|
|
565
|
-
connectSSE(); maybePromptPlacementBind(); refreshCartBadge(); maybeShowInstallBanner(); maybeClaimPendingProductShare(); setTimeout(refreshCompareBadge, 0); refreshChatsBadge(); refreshFeedbackBadge(); refreshSnfBadge(); setTimeout(ensureSnfPull, 800)
|
|
565
|
+
connectSSE(); maybePromptPlacementBind(); refreshCartBadge(); maybeShowInstallBanner(); maybeClaimPendingProductShare(); maybeClaimPendingShopReferral(); setTimeout(refreshCompareBadge, 0); refreshChatsBadge(); refreshFeedbackBadge(); refreshSnfBadge(); setTimeout(ensureSnfPull, 800)
|
|
566
566
|
try { p2pStart() } catch (e) { console.warn('[P2P] start err', e) }
|
|
567
567
|
} else if (status === 401 || status === 403) {
|
|
568
568
|
// 真·鉴权失败:清掉两层存储
|
|
@@ -632,6 +632,12 @@ async function render(page, params) {
|
|
|
632
632
|
case 'welcome': return renderWelcome(app)
|
|
633
633
|
// 2026-06-02 W3.5-B:#governance-onboarding 治理岗位申请页(公开页)
|
|
634
634
|
case 'governance-onboarding': return renderGovernanceOnboarding(app)
|
|
635
|
+
// PR9E-1 公开共建页(无需登录):任务板 / 详情 + agent 提示 / 建议任务
|
|
636
|
+
case 'contribute':
|
|
637
|
+
if (params[0] === 'tasks' && params[1] === 'suggest') return renderContributeSuggest(app)
|
|
638
|
+
if (params[0] === 'tasks' && params[1]) return renderContributeTaskDetail(app, params[1])
|
|
639
|
+
if (params[0] === 'suggest') return renderContributeSuggest(app)
|
|
640
|
+
return renderContributeTasks(app)
|
|
635
641
|
case 'seller':
|
|
636
642
|
if (state.user?.role === 'logistics') return renderLogistics(app)
|
|
637
643
|
if (state.user?.role === 'arbitrator') return renderDisputeList(app)
|
|
@@ -687,6 +693,7 @@ async function render(page, params) {
|
|
|
687
693
|
if (params[0] === 'users' && params[1]) return renderAdminUserDetail(app, params[1])
|
|
688
694
|
if (params[0] === 'users') return renderAdminUsers(app)
|
|
689
695
|
if (params[0] === 'audit') return renderAdminAudit(app)
|
|
696
|
+
if (params[0] === 'security') return renderAdminSecurity(app)
|
|
690
697
|
if (params[0] === 'products') return renderAdminProducts(app)
|
|
691
698
|
if (params[0] === 'orders') return renderAdminOrders(app)
|
|
692
699
|
if (params[0] === 'disputes') return renderAdminDisputes(app)
|
|
@@ -705,6 +712,7 @@ async function render(page, params) {
|
|
|
705
712
|
if (params[0] === 'kpi') return renderAdminKPI(app)
|
|
706
713
|
// 2026-05-24 #welcome 公开 ideas/邮箱订阅查看
|
|
707
714
|
if (params[0] === 'public-ideas') return renderAdminPublicIdeas(app)
|
|
715
|
+
if (params[0] === 'task-proposals') return renderAdminTaskProposals(app)
|
|
708
716
|
if (params[0] === 'params') return renderAdminParams(app)
|
|
709
717
|
if (params[0] === 'timeline' && params[1]) return renderAdminUserTimeline(app, params[1])
|
|
710
718
|
if (params[0] === 'timeline') return renderAdminUserTimelinePicker(app)
|
|
@@ -1005,6 +1013,40 @@ function preLaunchBannerHTML() {
|
|
|
1005
1013
|
</div>`
|
|
1006
1014
|
}
|
|
1007
1015
|
|
|
1016
|
+
// 账户无任何恢复方式 → 首页顶部持续红色风险横幅。
|
|
1017
|
+
// 恢复方式 = 密码 OR 已验证邮箱。Passkey【不算】恢复方式:它是敏感操作的"真人在场"门,
|
|
1018
|
+
// 没有 Passkey 登录 / 找回路径,丢 key 换设备后 Passkey 救不回账号(与弹窗"增强不替代恢复邮箱"一致)。
|
|
1019
|
+
function recoveryBannerHTML() {
|
|
1020
|
+
const u = state.user
|
|
1021
|
+
if (!u || u.role === 'admin') return ''
|
|
1022
|
+
const hasRecovery = u.has_password || u.email_verified
|
|
1023
|
+
if (hasRecovery) return ''
|
|
1024
|
+
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">
|
|
1025
|
+
🚨 <strong>${t('账户还没有恢复方式')}</strong> — ${t('闪退或换设备清缓存后可能永久无法登录。')}
|
|
1026
|
+
<div style="margin-top:8px;display:flex;gap:8px;flex-wrap:wrap">
|
|
1027
|
+
<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>
|
|
1028
|
+
</div>
|
|
1029
|
+
</div>`
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// 卖家后台安全提醒(P1):卖家涉及商品/履约/钱包,恢复方式不齐尤其危险。
|
|
1033
|
+
// 全局红横幅(零恢复)已在 shell 顶部覆盖;此处补"有邮箱但还缺密码"等部分缺口的黄色软提醒,避免与红横幅叠加。
|
|
1034
|
+
function sellerRecoveryReminderHTML() {
|
|
1035
|
+
const u = state.user
|
|
1036
|
+
if (!u) return ''
|
|
1037
|
+
// 全局红横幅判定一致:无密码且无邮箱 → 红横幅已覆盖,这里不再叠加(Passkey 不算恢复方式)
|
|
1038
|
+
const globalRedShowing = !u.has_password && !u.email_verified
|
|
1039
|
+
if (globalRedShowing) return ''
|
|
1040
|
+
const gaps = []
|
|
1041
|
+
if (!u.has_password) gaps.push(t('未设置登录密码'))
|
|
1042
|
+
if (!u.email_verified) gaps.push(t('未绑定找回邮箱'))
|
|
1043
|
+
if (gaps.length === 0) return ''
|
|
1044
|
+
return `<div class="alert" style="background:#fffbeb;border:1px solid #fde68a;color:#92400e;font-size:12px;line-height:1.6;margin-bottom:12px">
|
|
1045
|
+
🛡 <strong>${t('建议补全账户恢复方式')}</strong>:${gaps.join(' · ')}。${t('卖家涉及商品/履约/钱包,闪退或换设备清缓存后可能无法登录。')}
|
|
1046
|
+
<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>
|
|
1047
|
+
</div>`
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1008
1050
|
function shell(content, activeTab, opts) {
|
|
1009
1051
|
// opts = { hideTabbar?: bool, bottomBar?: html }(第三参数可选,向后兼容)
|
|
1010
1052
|
const _opts = opts || {}
|
|
@@ -1109,7 +1151,7 @@ function shell(content, activeTab, opts) {
|
|
|
1109
1151
|
: `<button class="btn btn-primary btn-sm" onclick="navigate('#login')">${t('登录')}</button>`}
|
|
1110
1152
|
</div>
|
|
1111
1153
|
</nav>
|
|
1112
|
-
<main class="main">${content}</main>
|
|
1154
|
+
<main class="main">${recoveryBannerHTML()}${content}</main>
|
|
1113
1155
|
${state.user?.role === 'buyer' ? `
|
|
1114
1156
|
<button id="compare-fab" onclick="openCompare()" title="${t('对比商品')}"
|
|
1115
1157
|
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">
|
|
@@ -1304,7 +1346,9 @@ async function renderMyAdvanced(app) {
|
|
|
1304
1346
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px">
|
|
1305
1347
|
${card('🏛', t('协议治理'), t('参数公开 · 变更可追溯'), '#governance')}
|
|
1306
1348
|
${card('⚖', t('判例库'), t('争议判决公开'), '#judgments')}
|
|
1307
|
-
${card('
|
|
1349
|
+
${card('🛠', t('我的共建'), t('贡献 / GitHub 认领 / 建设信誉 — 无购买门槛'), '#my-contributions')}
|
|
1350
|
+
${card('📋', t('公开共建任务'), t('浏览可认领任务、提交建议、参与共建'), '#contribute/tasks')}
|
|
1351
|
+
${card('🎁', t('分享分润管理'), t('分享佣金 / PV / escrow · 经济关系登记'), '#rewards-me')}
|
|
1308
1352
|
</div>
|
|
1309
1353
|
|
|
1310
1354
|
<div style="font-size:12px;color:#6b7280;font-weight:600;margin:14px 0 6px">📧 ${t('联系我们')}</div>
|
|
@@ -3096,6 +3140,178 @@ window.setAdminIdeaStatus = async (id, status) => {
|
|
|
3096
3140
|
setTimeout(() => renderAdminPublicIdeas(document.getElementById('app')), 300)
|
|
3097
3141
|
}
|
|
3098
3142
|
|
|
3143
|
+
// PR9I — Task Proposal Inbox admin review (maintainer-only). Calls the #331 admin endpoints. A proposal is
|
|
3144
|
+
// a SUGGESTION, never a contribution fact / reward / participation; "Convert" only records the review
|
|
3145
|
+
// decision + the proposer→reviewer→ref evidence chain — it does NOT auto-create a build_task.
|
|
3146
|
+
async function renderAdminTaskProposals(app) {
|
|
3147
|
+
if (!state.user) { renderLogin(); return }
|
|
3148
|
+
if (!isAdmin()) { app.innerHTML = shell(`<div class="alert alert-info">${t('仅限管理员')}</div>`, 'admin'); return }
|
|
3149
|
+
const en = window._lang === 'en'
|
|
3150
|
+
const T = (zh, e) => en && e ? e : zh
|
|
3151
|
+
app.innerHTML = shell(loading$(), 'admin')
|
|
3152
|
+
const sf = state._proposalStatus || '' // '' | new | needs_info | rejected | converted
|
|
3153
|
+
const [r, dr] = await Promise.all([
|
|
3154
|
+
GET('/admin/task-proposals' + (sf ? '?status=' + encodeURIComponent(sf) : '')),
|
|
3155
|
+
GET('/admin/build-task-drafts'),
|
|
3156
|
+
])
|
|
3157
|
+
if (r.error) { app.innerHTML = shell(alert$('error', r.error), 'admin'); return }
|
|
3158
|
+
const proposals = r.proposals || []
|
|
3159
|
+
const drafts = (dr && dr.drafts) || []
|
|
3160
|
+
const draftedIds = new Set(drafts.map((d) => d.source_proposal_id).filter(Boolean)) // proposals that already have an unpublished draft
|
|
3161
|
+
const notice = en ? (r.value_boundary?.notice_en || '') : (r.value_boundary?.notice_zh || '')
|
|
3162
|
+
const STATUS = {
|
|
3163
|
+
new: { bg: '#fef3c7', fg: '#92400e', label: T('待审', 'New') },
|
|
3164
|
+
needs_info: { bg: '#dbeafe', fg: '#1e40af', label: T('待补充', 'Needs info') },
|
|
3165
|
+
rejected: { bg: '#fee2e2', fg: '#991b1b', label: T('已拒绝', 'Rejected') },
|
|
3166
|
+
converted: { bg: '#dcfce7', fg: '#166534', label: T('已转任务', 'Converted') },
|
|
3167
|
+
}
|
|
3168
|
+
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>` }
|
|
3169
|
+
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>`
|
|
3170
|
+
const field = (label, val) => val ? `<div style="font-size:12px;color:#374151;margin-top:4px"><b>${label}:</b> ${escHtml(String(val))}</div>` : ''
|
|
3171
|
+
// inline "create formal task draft" form (prefilled from the proposal; AI can also prefill it). All list
|
|
3172
|
+
// fields are newline-separated. These are the agent-handoff fields the formal task model requires.
|
|
3173
|
+
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>`
|
|
3174
|
+
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">
|
|
3175
|
+
<div style="font-size:11px;color:#4338ca;font-weight:600;margin-bottom:2px">${T('建正式任务草稿(未发布)', 'Create formal task draft (unpublished)')}</div>
|
|
3176
|
+
<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>
|
|
3177
|
+
${ta('df-title-' + escHtml(p.id), T('标题', 'Title'), p.title)}
|
|
3178
|
+
${ta('df-area-' + escHtml(p.id), T('领域(可选)', 'Area (optional)'), p.suggested_area, 30)}
|
|
3179
|
+
${ta('df-source-' + escHtml(p.id), T('来源引用(文件 / RFC / issue,可选)', 'Source ref (file / RFC / issue, optional)'), p.source_ref, 30)}
|
|
3180
|
+
${ta('df-desc-' + escHtml(p.id), T('说明 / 原因', 'Summary / reason'), p.summary, 48)}
|
|
3181
|
+
${ta('df-allowed-' + escHtml(p.id), T('允许路径(每行一条)', 'Allowed paths (one per line)'), '')}
|
|
3182
|
+
${ta('df-fpaths-' + escHtml(p.id), T('禁止路径(每行一条)', 'Forbidden paths (one per line)'), '')}
|
|
3183
|
+
${ta('df-forbidden-' + escHtml(p.id), T('禁止动作(每行一条)', 'Forbidden actions (one per line)'), '')}
|
|
3184
|
+
${ta('df-accept-' + escHtml(p.id), T('验收标准(每行一条)', 'Acceptance criteria (one per line)'), p.expected_outcome)}
|
|
3185
|
+
${ta('df-verify-' + escHtml(p.id), T('验证命令(每行一条)', 'Verification commands (one per line)'), '')}
|
|
3186
|
+
${ta('df-deliver-' + escHtml(p.id), T('交付物(每行一条)', 'Deliverables (one per line)'), '')}
|
|
3187
|
+
${ta('df-dod-' + escHtml(p.id), T('完成定义', 'Definition of done'), '')}
|
|
3188
|
+
${ta('df-expect-' + escHtml(p.id), T('预期结果(留空则用说明)', 'Expected results (blank = use summary)'), '')}
|
|
3189
|
+
<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>
|
|
3190
|
+
</div>`
|
|
3191
|
+
const row = (p) => {
|
|
3192
|
+
const terminal = p.status === 'rejected' || p.status === 'converted'
|
|
3193
|
+
return `<div class="card" style="padding:14px;margin-bottom:10px">
|
|
3194
|
+
<div style="display:flex;justify-content:space-between;gap:8px;align-items:flex-start">
|
|
3195
|
+
<div style="font-weight:600;font-size:14px">${escHtml(p.title)}</div>${badge(p.status)}
|
|
3196
|
+
</div>
|
|
3197
|
+
<div style="font-size:13px;color:#52525B;line-height:1.5;margin-top:6px;white-space:pre-wrap">${escHtml(p.summary)}</div>
|
|
3198
|
+
${field(T('建议领域', 'Area'), p.suggested_area)}
|
|
3199
|
+
${field(T('预期结果', 'Outcome'), p.expected_outcome)}
|
|
3200
|
+
${field(T('参考', 'Source ref'), p.source_ref)}
|
|
3201
|
+
${field('GitHub', p.proposer_github_login)}
|
|
3202
|
+
${field(T('提交时间', 'Created'), p.created_at)}
|
|
3203
|
+
${field(T('审阅备注', 'Review note'), p.review_note)}
|
|
3204
|
+
${field(T('已关联', 'Converted ref'), p.converted_ref)}
|
|
3205
|
+
${terminal
|
|
3206
|
+
? `<div style="font-size:11px;color:#9ca3af;margin-top:8px">${T('终态,不可再审', 'Terminal — locked')}${p.reviewer_id ? ' · ' + escHtml(String(p.reviewer_id)) : ''}</div>`
|
|
3207
|
+
: `<div style="margin-top:10px;border-top:1px solid #f1f1f4;padding-top:10px">
|
|
3208
|
+
<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>
|
|
3209
|
+
<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">
|
|
3210
|
+
<div style="display:flex;gap:6px;margin-top:8px;flex-wrap:wrap">
|
|
3211
|
+
<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>
|
|
3212
|
+
${draftedIds.has(p.id)
|
|
3213
|
+
? `<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>`
|
|
3214
|
+
: `<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>
|
|
3215
|
+
<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>
|
|
3216
|
+
<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>
|
|
3217
|
+
<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>`}
|
|
3218
|
+
</div>
|
|
3219
|
+
<div id="ai-${escHtml(p.id)}"></div>
|
|
3220
|
+
${draftedIds.has(p.id) ? '' : draftForm(p)}
|
|
3221
|
+
</div>`}
|
|
3222
|
+
</div>`
|
|
3223
|
+
}
|
|
3224
|
+
const draftRow = (d) => `<div class="card" style="padding:12px;margin-bottom:8px;border-left:3px solid #6366f1">
|
|
3225
|
+
<div style="display:flex;justify-content:space-between;gap:8px;align-items:flex-start">
|
|
3226
|
+
<div style="font-weight:600;font-size:13px">${escHtml(d.title)}</div>
|
|
3227
|
+
<span style="font-size:10px;background:#eef2ff;color:#4338ca;padding:2px 8px;border-radius:99px;font-weight:600">${T('未发布草稿', 'Unpublished draft')}</span>
|
|
3228
|
+
</div>
|
|
3229
|
+
${field(T('风险', 'Risk'), d.risk_level)}${field(T('可自助认领', 'Auto-claimable'), d.auto_claimable === 1 || d.auto_claimable === true ? T('是', 'yes') : T('否(需真人)', 'no (human)'))}
|
|
3230
|
+
${field(T('来源建议', 'Source proposal'), d.source_proposal_id)}${field(T('创建人', 'Created by'), d.created_by)}
|
|
3231
|
+
<div style="font-size:10px;color:#9ca3af;margin-top:6px">${T('发布前会校验交接字段;发布后进入正常任务板,可被参与者 agent 发现 / 认领 / 提交 PR。', 'Publish validates the handoff fields; once published it enters the normal task board — discoverable / claimable / PR-submittable by participant agents.')}</div>
|
|
3232
|
+
<button onclick="publishDraft('${escHtml(d.id)}')" style="margin-top:8px;padding:6px 14px;border:none;background:#16a34a;color:#fff;border-radius:6px;font-size:12px;cursor:pointer;font-weight:600">${T('发布到任务板', 'Publish to board')}</button>
|
|
3233
|
+
</div>`
|
|
3234
|
+
app.innerHTML = shell(`
|
|
3235
|
+
<div style="padding:14px;max-width:920px;margin:0 auto">
|
|
3236
|
+
<h1 class="page-title">🛠️ ${T('任务建议收件箱', 'Task Proposal Inbox')}</h1>
|
|
3237
|
+
<div style="background:#f4f4f5;border:1px solid #e4e4e7;border-radius:8px;padding:10px;font-size:11px;color:#52525B;line-height:1.6;margin-bottom:12px">
|
|
3238
|
+
${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.')}
|
|
3239
|
+
${notice ? `<br>${escHtml(notice)}` : ''}
|
|
3240
|
+
</div>
|
|
3241
|
+
${drafts.length ? `<div style="margin-bottom:14px">
|
|
3242
|
+
<div style="font-size:12px;font-weight:700;color:#4338ca;margin-bottom:6px">📝 ${T('未发布任务草稿', 'Unpublished task drafts')} (${drafts.length})</div>
|
|
3243
|
+
${drafts.map(draftRow).join('')}
|
|
3244
|
+
</div>` : ''}
|
|
3245
|
+
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:12px">
|
|
3246
|
+
${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)}
|
|
3247
|
+
</div>
|
|
3248
|
+
${proposals.length === 0 ? `<div style="color:#a1a1aa;text-align:center;padding:30px;font-size:14px">${T('收件箱为空', 'Inbox is empty')}</div>` : proposals.map(row).join('')}
|
|
3249
|
+
</div>
|
|
3250
|
+
`, 'admin')
|
|
3251
|
+
}
|
|
3252
|
+
window.setProposalStatusFilter = (s) => { state._proposalStatus = s; renderAdminTaskProposals(document.getElementById('app')) }
|
|
3253
|
+
window.reviewProposal = async (id, status) => {
|
|
3254
|
+
const note = (document.getElementById('pr-note-' + id)?.value || '').trim()
|
|
3255
|
+
const ref = (document.getElementById('pr-ref-' + id)?.value || '').trim()
|
|
3256
|
+
const body = { status }
|
|
3257
|
+
if (note) body.note = note
|
|
3258
|
+
if (status === 'converted' && ref) body.converted_ref = ref
|
|
3259
|
+
const r = await POST('/admin/task-proposals/' + encodeURIComponent(id) + '/review', body)
|
|
3260
|
+
if (r.error) { toast$(r.error || r.error_code || (window._lang === 'en' ? 'failed' : '操作失败')); return }
|
|
3261
|
+
toast$(window._lang === 'en' ? 'Updated' : '已更新')
|
|
3262
|
+
renderAdminTaskProposals(document.getElementById('app'))
|
|
3263
|
+
}
|
|
3264
|
+
window.toggleDraftForm = (id) => { const el = document.getElementById('df-' + id); if (el) el.style.display = el.style.display === 'none' ? 'block' : 'none' }
|
|
3265
|
+
// AI-assist is ASSISTANT-ONLY: it renders a suggestion + prefills the draft form; it never publishes/decides.
|
|
3266
|
+
window.aiAssistProposal = async (id) => {
|
|
3267
|
+
const en = window._lang === 'en'
|
|
3268
|
+
const box = document.getElementById('ai-' + id); if (box) box.innerHTML = `<div style="font-size:11px;color:#8b5cf6;margin-top:8px">🤖 ${en ? 'thinking…' : '分析中…'}</div>`
|
|
3269
|
+
const r = await POST('/admin/task-proposals/' + encodeURIComponent(id) + '/ai-assist', {})
|
|
3270
|
+
if (r.error) { if (box) box.innerHTML = ''; toast$(r.error || 'failed'); return }
|
|
3271
|
+
const s = r.ai_suggestion || {}
|
|
3272
|
+
window._aiSuggest = window._aiSuggest || {}; window._aiSuggest[id] = s.suggested || {}
|
|
3273
|
+
const list = (arr) => (arr || []).map(x => '• ' + escHtml(String(x))).join('<br>')
|
|
3274
|
+
if (box) box.innerHTML = `<div style="margin-top:8px;border:1px solid #ddd6fe;background:#faf5ff;border-radius:8px;padding:10px">
|
|
3275
|
+
<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>
|
|
3276
|
+
<div style="font-size:10px;color:#b45309;margin:2px 0 6px">${escHtml(String(r.ai_notice || ''))}</div>
|
|
3277
|
+
<div style="font-size:12px;color:#374151;line-height:1.6">
|
|
3278
|
+
<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 || ''))}
|
|
3279
|
+
${(s.missing_info && s.missing_info.length) ? `<div style="margin-top:4px"><b>${en ? 'Missing info' : '缺失信息'}:</b><br>${list(s.missing_info)}</div>` : ''}
|
|
3280
|
+
</div>
|
|
3281
|
+
<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>
|
|
3282
|
+
</div>`
|
|
3283
|
+
}
|
|
3284
|
+
window.applyAiToDraft = (id) => {
|
|
3285
|
+
const suggested = (window._aiSuggest && window._aiSuggest[id]) || {}
|
|
3286
|
+
document.getElementById('df-' + id).style.display = 'block'
|
|
3287
|
+
const set = (sfx, v) => { const el = document.getElementById('df-' + sfx + '-' + id); if (el && v != null && v !== '') el.value = v }
|
|
3288
|
+
set('title', suggested.title); set('area', suggested.area); set('desc', suggested.description)
|
|
3289
|
+
set('accept', (suggested.acceptance_criteria || []).join('\n')); set('verify', (suggested.verification_commands || []).join('\n'))
|
|
3290
|
+
toast$(window._lang === 'en' ? 'Draft prefilled (review before saving)' : '已填充草稿(保存前请人工核对)')
|
|
3291
|
+
}
|
|
3292
|
+
window.createTaskDraft = async (id) => {
|
|
3293
|
+
const en = window._lang === 'en'
|
|
3294
|
+
const v = (sfx) => (document.getElementById('df-' + sfx + '-' + id)?.value || '').trim()
|
|
3295
|
+
const lines = (sfx) => v(sfx).split('\n').map(x => x.trim()).filter(Boolean)
|
|
3296
|
+
const body = {
|
|
3297
|
+
title: v('title'), area: v('area') || null, source_ref: v('source') || null, description: v('desc'),
|
|
3298
|
+
allowed_paths: lines('allowed'), forbidden_paths: lines('fpaths'), forbidden_actions: lines('forbidden'),
|
|
3299
|
+
acceptance_criteria: lines('accept'), verification_commands: lines('verify'), deliverables: lines('deliver'),
|
|
3300
|
+
definition_of_done: v('dod'), expected_results: v('expect'),
|
|
3301
|
+
}
|
|
3302
|
+
const r = await POST('/admin/task-proposals/' + encodeURIComponent(id) + '/create-task-draft', body)
|
|
3303
|
+
if (r.error) { toast$((r.missing && r.missing.length) ? ((en ? 'Missing: ' : '缺少:') + r.missing.join(', ')) : (r.error || 'failed')); return }
|
|
3304
|
+
toast$(en ? 'Draft saved (unpublished)' : '草稿已保存(未发布)')
|
|
3305
|
+
renderAdminTaskProposals(document.getElementById('app'))
|
|
3306
|
+
}
|
|
3307
|
+
window.publishDraft = async (taskId) => {
|
|
3308
|
+
const en = window._lang === 'en'
|
|
3309
|
+
const r = await POST('/admin/build-task-drafts/' + encodeURIComponent(taskId) + '/publish', {})
|
|
3310
|
+
if (r.error) { toast$((r.missing && r.missing.length) ? ((en ? 'Fill before publish: ' : '发布前请填齐:') + r.missing.join(', ')) : (r.error || 'failed')); return }
|
|
3311
|
+
toast$(en ? 'Published to task board' : '已发布到任务板')
|
|
3312
|
+
renderAdminTaskProposals(document.getElementById('app'))
|
|
3313
|
+
}
|
|
3314
|
+
|
|
3099
3315
|
async function renderAdminKPI(app) {
|
|
3100
3316
|
if (!state.user) { renderLogin(); return }
|
|
3101
3317
|
if (!isAdmin()) { app.innerHTML = shell(`<div class="alert alert-info">${t('仅限管理员')}</div>`, 'admin'); return }
|
|
@@ -3299,6 +3515,11 @@ async function renderAdminDashboard(app) {
|
|
|
3299
3515
|
<div style="font-size:13px;color:#6b7280;margin:16px 0 8px">⚛ ${t('Tokenomics')}</div>
|
|
3300
3516
|
<div style="display:grid;grid-template-columns:repeat(2,1fr);gap:10px;margin-bottom:16px">
|
|
3301
3517
|
${quickAction('#admin/tokenomics', '⚛', t('积分基金 / Tier 配置 / 高额榜'))}
|
|
3518
|
+
</div>
|
|
3519
|
+
<div style="font-size:13px;color:#6b7280;margin:16px 0 8px">🔐 ${t('安全与审计')}</div>
|
|
3520
|
+
<div style="display:grid;grid-template-columns:repeat(2,1fr);gap:10px;margin-bottom:16px">
|
|
3521
|
+
${quickAction('#admin/security', '🪪', t('我的管理身份与权限'))}
|
|
3522
|
+
${quickAction('#admin/audit', '📜', t('审计日志'))}
|
|
3302
3523
|
</div>`
|
|
3303
3524
|
|
|
3304
3525
|
// A5 重设:渐变标题 + 分区标题 + 颜色块分组
|
|
@@ -3353,6 +3574,90 @@ function renderTagBadges(tags, max = 3) {
|
|
|
3353
3574
|
}).join('') + (more > 0 ? `<span style="font-size:11px;color:#6b7280">+${more}</span>` : '')
|
|
3354
3575
|
}
|
|
3355
3576
|
|
|
3577
|
+
// 管理身份与权限自查面板(只读)。回答"我正在以什么身份/级别/权限操作?",
|
|
3578
|
+
// Passkey 责任绑定状态 + GitHub 关联 + 普通 admin vs root/破玻璃 + 经济操作审计须知。
|
|
3579
|
+
// 纯前端:数据来自 /me(state.user)+ 只读 /contribution-identity/github/me;无新后端、无经济动作。
|
|
3580
|
+
async function renderAdminSecurity(app) {
|
|
3581
|
+
if (!state.user) { renderLogin(); return }
|
|
3582
|
+
if (!isAdmin()) { app.innerHTML = shell(`<div class="alert alert-info">${t('仅限管理员')}</div>`, 'admin'); return }
|
|
3583
|
+
app.innerHTML = shell(loading$(), 'admin')
|
|
3584
|
+
const u = state.user
|
|
3585
|
+
const gid = await GET('/contribution-identity/github/me').catch(() => null)
|
|
3586
|
+
const bindings = (gid && !gid.error && Array.isArray(gid.bindings)) ? gid.bindings : []
|
|
3587
|
+
|
|
3588
|
+
const adminType = u.admin_type || 'root'
|
|
3589
|
+
const isRoot = adminType === 'root'
|
|
3590
|
+
const scope = u.admin_scope || 'global'
|
|
3591
|
+
let perms = []
|
|
3592
|
+
try { perms = isRoot ? ['all'] : JSON.parse(u.admin_permissions || '[]') } catch { perms = [] }
|
|
3593
|
+
const hasPasskey = !!u.has_passkey
|
|
3594
|
+
|
|
3595
|
+
const PERM_LABEL = () => ({ all: t('全部'), users: t('用户'), content: t('内容'), arbitration: t('仲裁'), protocol: t('协议 / 经济'), verifier_mgmt: t('审核员管理'), support: t('支持') })
|
|
3596
|
+
const permChips = (perms.length === 0)
|
|
3597
|
+
? `<span style="font-size:12px;color:#dc2626">${t('无任何权限(请联系 root 配置)')}</span>`
|
|
3598
|
+
: 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('')
|
|
3599
|
+
|
|
3600
|
+
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>`
|
|
3601
|
+
|
|
3602
|
+
const passkeyRow = hasPasskey
|
|
3603
|
+
? `<span style="color:#16a34a;font-weight:600">✓ ${t('已绑定')}</span>`
|
|
3604
|
+
: `<span style="color:#dc2626;font-weight:600">⚠ ${t('未绑定')}</span> <a href="#me/settings" style="color:#6366f1;font-size:11px">${t('去绑定')} →</a>`
|
|
3605
|
+
const githubRow = bindings.length > 0
|
|
3606
|
+
? bindings.map(b => `<code style="font-size:11px">github:${escHtml(String(b.github_actor_id))}</code>`).join(' ')
|
|
3607
|
+
: `<span style="color:#9ca3af">${t('未关联')}</span> <a href="#my-contributions" style="color:#6366f1;font-size:11px">${t('去认领')} →</a>`
|
|
3608
|
+
|
|
3609
|
+
app.innerHTML = shell(`
|
|
3610
|
+
${adminPageHeader('🪪', t('我的管理身份与权限'), t('你正在以此身份操作 · 只读自查'))}
|
|
3611
|
+
|
|
3612
|
+
${isRoot ? `
|
|
3613
|
+
<div class="card" style="padding:12px;background:#fffbeb;border:1px solid #fcd34d;margin-bottom:10px">
|
|
3614
|
+
<div style="font-size:13px;font-weight:700;color:#92400e">🚧 ${t('创始人 / 引导管理员(Founder Admin · Bootstrap Operator)')}</div>
|
|
3615
|
+
<div style="font-size:12px;color:#78350f;margin-top:4px;line-height:1.6">${t('这是 pre-launch 引导期的【临时治理模式】:更广的只读可见性 + 有限的应急写权限 —— 不是日常全能账号。')}</div>
|
|
3616
|
+
<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>
|
|
3617
|
+
</div>` : ''}
|
|
3618
|
+
|
|
3619
|
+
<div class="card" style="padding:14px">
|
|
3620
|
+
<div style="font-size:13px;font-weight:700;margin-bottom:8px">👤 ${t('账户')}</div>
|
|
3621
|
+
${row(t('名称'), escHtml(u.name || ''))}
|
|
3622
|
+
${row(t('用户名'), '@' + escHtml(u.handle || ''))}
|
|
3623
|
+
${row(t('账户 ID'), `<code style="font-size:11px">${escHtml(u.id || '')}</code>`)}
|
|
3624
|
+
</div>
|
|
3625
|
+
|
|
3626
|
+
<div class="card" style="padding:14px">
|
|
3627
|
+
<div style="font-size:13px;font-weight:700;margin-bottom:8px">🛡 ${t('角色与级别')}</div>
|
|
3628
|
+
${row(t('角色'), t('管理员'))}
|
|
3629
|
+
${row(t('级别'), isRoot
|
|
3630
|
+
? `<span style="color:#b91c1c;font-weight:700">ROOT</span> · <span style="font-size:11px;color:#6b7280">${t('破玻璃 / 系统操作员')}</span>`
|
|
3631
|
+
: `<span style="color:#0369a1;font-weight:700">REGIONAL</span>`)}
|
|
3632
|
+
${row(t('范围'), `<code style="font-size:11px">${escHtml(scope)}</code>`)}
|
|
3633
|
+
<div style="font-size:12px;color:#6b7280;margin-top:8px;margin-bottom:4px">${t('有效权限')}</div>
|
|
3634
|
+
<div>${permChips}</div>
|
|
3635
|
+
</div>
|
|
3636
|
+
|
|
3637
|
+
<div class="card" style="padding:14px">
|
|
3638
|
+
<div style="font-size:13px;font-weight:700;margin-bottom:8px">🔐 ${t('问责绑定')}</div>
|
|
3639
|
+
${row('Passkey', passkeyRow)}
|
|
3640
|
+
${row(t('GitHub 关联'), githubRow)}
|
|
3641
|
+
<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">
|
|
3642
|
+
${t('管理身份应绑定 Passkey(真人问责)。个人 GitHub 账号用于提交 PR;仓库所有权 / 设置由组织/管理身份治理 —— 独立审阅不应由同一人用另一账号假冒。')}
|
|
3643
|
+
</div>
|
|
3644
|
+
</div>
|
|
3645
|
+
|
|
3646
|
+
<div class="card" style="padding:14px">
|
|
3647
|
+
<div style="font-size:13px;font-weight:700;margin-bottom:8px">⚠️ ${t('操作安全须知')}</div>
|
|
3648
|
+
<div style="font-size:12px;color:#374151;line-height:1.8">
|
|
3649
|
+
• ${t('普通 admin 与 root / 破玻璃 不同:经济 / 协议级操作需 protocol 权限;按治理铁律须记入审计日志 —— 部分手动结算 / 评估入口的审计仍在补齐中。')}<br>
|
|
3650
|
+
• ${t('危险操作(封禁 / 角色 / 资金 / 协议参数)须带原因,且不可绕过争议 / 仲裁规则。')}<br>
|
|
3651
|
+
• ${t('不要在公共设备暴露 API Key;管理操作均可追溯到你的账户。')}
|
|
3652
|
+
</div>
|
|
3653
|
+
<div style="display:flex;gap:8px;margin-top:10px">
|
|
3654
|
+
<a href="#admin/audit" style="text-decoration:none"><button class="btn btn-outline btn-sm" style="font-size:12px">📜 ${t('查看审计日志')}</button></a>
|
|
3655
|
+
${isRoot ? `<a href="#admin/manage-admins" style="text-decoration:none"><button class="btn btn-outline btn-sm" style="font-size:12px">👥 ${t('管理管理员')}</button></a>` : ''}
|
|
3656
|
+
</div>
|
|
3657
|
+
</div>
|
|
3658
|
+
`, 'admin')
|
|
3659
|
+
}
|
|
3660
|
+
|
|
3356
3661
|
async function renderAdminUsers(app, opts = {}) {
|
|
3357
3662
|
if (!state.user) { renderLogin(); return }
|
|
3358
3663
|
if (!isAdmin()) { app.innerHTML = shell(`<div class="alert alert-info">${t('仅限管理员')}</div>`, 'admin-users'); return }
|
|
@@ -4315,10 +4620,10 @@ async function renderApplyRewards(app) {
|
|
|
4315
4620
|
|
|
4316
4621
|
if (status.opted_in) {
|
|
4317
4622
|
app.innerHTML = shell(`
|
|
4318
|
-
<h1 class="page-title">${t('
|
|
4623
|
+
<h1 class="page-title">${t('已开通分享分润')}</h1>
|
|
4319
4624
|
<div class="card" style="background:#ecfdf5;border-color:#86efac;padding:12px;font-size:13px;color:#064e3b">
|
|
4320
4625
|
${t('你已 opted-in。如需查看状态或退出,前往 ')}
|
|
4321
|
-
<a href="#rewards-me" style="color:#16a34a;font-weight:600">${t('
|
|
4626
|
+
<a href="#rewards-me" style="color:#16a34a;font-weight:600">${t('分享分润管理')}</a>
|
|
4322
4627
|
</div>
|
|
4323
4628
|
`, returnNav)
|
|
4324
4629
|
return
|
|
@@ -4347,19 +4652,28 @@ async function renderApplyRewards(app) {
|
|
|
4347
4652
|
</div>`
|
|
4348
4653
|
|
|
4349
4654
|
app.innerHTML = shell(`
|
|
4350
|
-
<h1 class="page-title">🎁 ${t('
|
|
4655
|
+
<h1 class="page-title">🎁 ${t('申请分享分润')}</h1>
|
|
4351
4656
|
|
|
4352
4657
|
<div class="card" style="margin-bottom:16px;background:#fef2f2;border-color:#fca5a5">
|
|
4353
|
-
<div style="font-weight:600;color:#991b1b;margin-bottom:6px;font-size:14px">⚠️ ${t('
|
|
4658
|
+
<div style="font-weight:600;color:#991b1b;margin-bottom:6px;font-size:14px">⚠️ ${t('本流程不是购物流程;下方"已完成订单"门槛只是分享分润的反女巫要求')}</div>
|
|
4354
4659
|
<div style="font-size:12px;color:#7f1d1d;line-height:1.6">
|
|
4355
|
-
${t('
|
|
4356
|
-
<span style="opacity:0.85">This flow is
|
|
4660
|
+
${t('你可以随时退出,不影响任何已下单或未来订单。本流程是分享分润的经济关系登记(佣金 / PV / escrow 结算规则;层级按地区配置),请仔细阅读全部条款。')}<br>
|
|
4661
|
+
<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>
|
|
4662
|
+
</div>
|
|
4663
|
+
<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">
|
|
4664
|
+
${t('注:本「分享分润 / rewards opt-in」仅为 commission / PV / escrow 经济关系登记,不是贡献资格,与贡献任务(#contribute/tasks)/ 我的共建 无关。RFC-002 同意书可能沿用更早措辞,以本 UI 含义为准。')}<br>
|
|
4665
|
+
<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>
|
|
4666
|
+
</div>
|
|
4667
|
+
<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">
|
|
4668
|
+
${t('现实性说明:佣金层级按地区合规配置生效;当前预发布期全局上限为 1 级(仅 L1)。"三级(7:2:1)"是协议最大设计,不构成对未来层级的承诺。')}<br>
|
|
4669
|
+
<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>
|
|
4357
4670
|
</div>
|
|
4358
4671
|
</div>
|
|
4359
4672
|
|
|
4360
4673
|
<div class="card" style="margin-bottom:16px">
|
|
4361
|
-
<div style="font-size:13px;color:#6b7280;margin-bottom:10px">${t('
|
|
4674
|
+
<div style="font-size:13px;color:#6b7280;margin-bottom:10px">${t('分享分润开通门槛(只适用于分润,不适用于贡献)')}:</div>
|
|
4362
4675
|
${checklist}
|
|
4676
|
+
<div style="font-size:11px;color:#6b7280;margin-top:8px;padding-top:8px;border-top:1px solid #f3f4f6">${t('此购买门槛只适用于分享分润(经济关系登记),不适用于贡献任务或 GitHub 贡献认领 — 贡献无需购买。')}</div>
|
|
4363
4677
|
</div>
|
|
4364
4678
|
|
|
4365
4679
|
<div class="card" style="margin-bottom:16px;background:#fafafa">
|
|
@@ -4463,7 +4777,7 @@ window.doSubmitRewardsApply = async (consentVersion, pageLoadedAt) => {
|
|
|
4463
4777
|
const drainNote = drained.count > 0
|
|
4464
4778
|
? '<br>' + t('已从 escrow 拨回:') + ' ' + drained.total + ' WAZ (' + drained.count + ' ' + t('笔') + ')'
|
|
4465
4779
|
: ''
|
|
4466
|
-
msg.innerHTML = alert$('success', t('✅
|
|
4780
|
+
msg.innerHTML = alert$('success', t('✅ 分享分润开通成功') + drainNote)
|
|
4467
4781
|
setTimeout(() => { location.hash = '#rewards-me' }, 2000)
|
|
4468
4782
|
}
|
|
4469
4783
|
|
|
@@ -4474,7 +4788,7 @@ async function renderRewardsMe(app) {
|
|
|
4474
4788
|
if (status.error) { app.innerHTML = shell(alert$('error', status.error), 'me'); return }
|
|
4475
4789
|
|
|
4476
4790
|
const stateLabel = {
|
|
4477
|
-
opted_in: { icon: '✅', color: '#16a34a', label: t('
|
|
4791
|
+
opted_in: { icon: '✅', color: '#16a34a', label: t('分享分润已开通') },
|
|
4478
4792
|
never_activated: { icon: '⚪', color: '#6b7280', label: t('未激活') },
|
|
4479
4793
|
auto_downgraded: { icon: '⚠️', color: '#f59e0b', label: t('已自动降级(consent 未确认)') },
|
|
4480
4794
|
deactivated: { icon: '🔒', color: '#9ca3af', label: t('已退出') },
|
|
@@ -4484,7 +4798,7 @@ async function renderRewardsMe(app) {
|
|
|
4484
4798
|
const expired = status.expired_to_charity || { count: 0, total_amount: 0 }
|
|
4485
4799
|
|
|
4486
4800
|
app.innerHTML = shell(`
|
|
4487
|
-
<h1 class="page-title">🎁 ${t('
|
|
4801
|
+
<h1 class="page-title">🎁 ${t('分享分润管理')}</h1>
|
|
4488
4802
|
|
|
4489
4803
|
<div class="card" style="margin-bottom:16px;border-left:4px solid ${stateLabel.color};padding:14px">
|
|
4490
4804
|
<div style="display:flex;align-items:center;gap:10px;margin-bottom:6px">
|
|
@@ -4498,27 +4812,27 @@ async function renderRewardsMe(app) {
|
|
|
4498
4812
|
<div class="card" style="margin-bottom:16px;background:#fef3c7;border-color:#fde68a;padding:12px">
|
|
4499
4813
|
<div style="font-weight:600;color:#92400e;font-size:13px;margin-bottom:4px">💰 ${t('待领 escrow')}</div>
|
|
4500
4814
|
<div style="font-size:13px;color:#7c2d12">${escrow.count} ${t('笔')} · ${escrow.total_amount} WAZ</div>
|
|
4501
|
-
<div style="font-size:11px;color:#a16207;margin-top:6px">${t('opt-in 后这笔钱会拨回钱包。30
|
|
4815
|
+
<div style="font-size:11px;color:#a16207;margin-top:6px">${t('opt-in 后这笔钱会拨回钱包。30 天未领过期入协议公池。')}</div>
|
|
4502
4816
|
</div>
|
|
4503
4817
|
` : ''}
|
|
4504
4818
|
|
|
4505
4819
|
${expired.count > 0 ? `
|
|
4506
4820
|
<div class="card" style="margin-bottom:16px;background:#f3f4f6;padding:10px;font-size:12px;color:#6b7280">
|
|
4507
|
-
${t('
|
|
4821
|
+
${t('历史过期入协议公池')}: ${expired.count} ${t('笔')} · ${expired.total_amount} WAZ
|
|
4508
4822
|
</div>
|
|
4509
4823
|
` : ''}
|
|
4510
4824
|
|
|
4511
4825
|
${status.state === 'opted_in' ? `
|
|
4512
4826
|
<div class="card" style="margin-bottom:16px;padding:14px">
|
|
4513
|
-
<div style="font-size:13px;color:#374151;margin-bottom:10px">${t('
|
|
4827
|
+
<div style="font-size:13px;color:#374151;margin-bottom:10px">${t('退出分享分润后,未来 commission 进入协议公池(commission_reserve),不是慈善基金。可随时重新开通。')}</div>
|
|
4514
4828
|
<button class="btn" style="background:#fee2e2;color:#991b1b;border:1px solid #fca5a5" onclick="doDeactivateRewards()">
|
|
4515
|
-
🔒 ${t('
|
|
4829
|
+
🔒 ${t('退出分享分润')}
|
|
4516
4830
|
</button>
|
|
4517
4831
|
<div id="rwd-deact-msg" style="margin-top:10px"></div>
|
|
4518
4832
|
</div>
|
|
4519
4833
|
` : `
|
|
4520
4834
|
<button class="btn btn-primary" onclick="location.hash='#apply-rewards'">
|
|
4521
|
-
🎁 ${status.state === 'never_activated' ? t('
|
|
4835
|
+
🎁 ${status.state === 'never_activated' ? t('申请分享分润') : t('重新申请')}
|
|
4522
4836
|
</button>
|
|
4523
4837
|
`}
|
|
4524
4838
|
|
|
@@ -4531,7 +4845,7 @@ async function renderRewardsMe(app) {
|
|
|
4531
4845
|
window.doDeactivateRewards = async () => {
|
|
4532
4846
|
const msg = document.getElementById('rwd-deact-msg')
|
|
4533
4847
|
if (!msg) return
|
|
4534
|
-
if (!confirm(t('
|
|
4848
|
+
if (!confirm(t('确认退出分享分润?未来 commission 将进入协议公池(commission_reserve,非慈善基金),可随时重新开通。'))) return
|
|
4535
4849
|
|
|
4536
4850
|
msg.innerHTML = `<div class="alert alert-info"><span class="spinner"></span>${t('请通过 Passkey / 生物识别确认...')}</div>`
|
|
4537
4851
|
let passkeyToken
|
|
@@ -4548,7 +4862,7 @@ window.doDeactivateRewards = async () => {
|
|
|
4548
4862
|
msg.innerHTML = alert$('error', res.error)
|
|
4549
4863
|
return
|
|
4550
4864
|
}
|
|
4551
|
-
msg.innerHTML = alert$('success', t('✅
|
|
4865
|
+
msg.innerHTML = alert$('success', t('✅ 已退出分享分润'))
|
|
4552
4866
|
setTimeout(() => renderRewardsMe(document.getElementById('app')), 1200)
|
|
4553
4867
|
}
|
|
4554
4868
|
|
|
@@ -5580,7 +5894,7 @@ async function renderAdminTokenomics(app) {
|
|
|
5580
5894
|
<div style="padding:8px 12px;font-size:11px;color:#6b7280">${t('调整:POST /api/admin/tokenomics/tier with body {tier, pv_threshold, score_per_hit, active}')}</div>
|
|
5581
5895
|
</div>
|
|
5582
5896
|
|
|
5583
|
-
<h2 style="font-size:15px;font-weight:600;margin:16px 0 8px">🏆 ${t('Top
|
|
5897
|
+
<h2 style="font-size:15px;font-weight:600;margin:16px 0 8px">🏆 ${t('Top 分享佣金')} (Top 10)</h2>
|
|
5584
5898
|
<div class="card" style="padding:0;margin-bottom:12px">${commRows}</div>
|
|
5585
5899
|
|
|
5586
5900
|
<h2 style="font-size:15px;font-weight:600;margin:16px 0 8px">🏆 ${t('Top 匹配收益')} (Top 10)</h2>
|
|
@@ -5815,7 +6129,7 @@ function renderLogin() {
|
|
|
5815
6129
|
<!-- ② 主 CTA:参与共建 -->
|
|
5816
6130
|
<div style="text-align:center;margin-top:clamp(20px,3.6vh,34px)">
|
|
5817
6131
|
<button onclick="openParticipateSheet('${defaultTab}')" style="padding:13px 48px;font-size:16px;font-weight:600;width:auto;min-width:220px;border-radius:99px;border:none;cursor:pointer;background:linear-gradient(135deg,#6366f1 0%,#8b5cf6 100%);color:#fff;box-shadow:0 12px 28px rgba(99,102,241,0.32),0 4px 8px rgba(99,102,241,0.18);letter-spacing:0.2px;transition:all 0.2s ease;font-family:inherit" onmouseover="this.style.transform='translateY(-1px)';this.style.boxShadow='0 16px 36px rgba(99,102,241,0.38),0 6px 12px rgba(99,102,241,0.22)'" onmouseout="this.style.transform='';this.style.boxShadow='0 12px 28px rgba(99,102,241,0.32),0 4px 8px rgba(99,102,241,0.18)'">
|
|
5818
|
-
${window._lang === 'en' ? '
|
|
6132
|
+
${window._lang === 'en' ? 'Get started' : '开始使用'}
|
|
5819
6133
|
</button>
|
|
5820
6134
|
</div>
|
|
5821
6135
|
|
|
@@ -5873,6 +6187,9 @@ function renderLogin() {
|
|
|
5873
6187
|
function renderWelcome(app) {
|
|
5874
6188
|
const en = window._lang === 'en'
|
|
5875
6189
|
const T = (zh, e) => en ? e : zh
|
|
6190
|
+
// 创始白皮书(canonical founding doc)— 跟随界面语言指向 webaz.xyz 公开静态页(匿名可达,不走私有 GitHub)
|
|
6191
|
+
// 这两个路径由 scripts/build-whitepaper-html.ts 从 docs/WHITEPAPER*.md 生成,经 express.static 公开服务
|
|
6192
|
+
const WP_URL = en ? '/whitepaper/en' : '/whitepaper/zh-CN'
|
|
5876
6193
|
// 2026-05-26 排版品控 — 区块标题 / 副标题 改为 inline style,避免 CSS 级联 / @media 覆写
|
|
5877
6194
|
// 最低线:h2 ≥ 24px(手机) / 32px(桌面);sub ≥ 16px
|
|
5878
6195
|
// ⚠️ 不用 calc(),避免 +/- 两侧空格被压掉导致整条 font-size 失效
|
|
@@ -6041,20 +6358,21 @@ function renderWelcome(app) {
|
|
|
6041
6358
|
<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">
|
|
6042
6359
|
${T('买家、卖家、创作者三位一体的去中心化商业协议', 'A decentralized commerce protocol where buyers, sellers, and creators are one.')}
|
|
6043
6360
|
</p>
|
|
6361
|
+
<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>
|
|
6044
6362
|
</section>
|
|
6045
6363
|
|
|
6046
6364
|
<!-- 区块 1: Buyers -->
|
|
6047
6365
|
<section class="w-section">
|
|
6048
6366
|
<h2 style="${H2_STYLE}">${T('作为买家<br>你不只是买家', "As a buyer<br>you're more than a buyer")}</h2>
|
|
6049
|
-
<p style="${SUB_STYLE}">${T('
|
|
6367
|
+
<p style="${SUB_STYLE}">${T('真实使用,留下可验证的记录', 'Use for real — leave a verifiable record.')}</p>
|
|
6050
6368
|
<div class="w-cards">
|
|
6051
6369
|
<div class="w-card">
|
|
6052
|
-
<div class="w-card-title"
|
|
6370
|
+
<div class="w-card-title">🛍 ${T('真实使用,真实记录', 'Real use, real record')}</div>
|
|
6053
6371
|
<ul class="w-card-list">
|
|
6054
6372
|
<li>${T('真实购买,真实体验', 'Buy real. Use real. Review real.')}</li>
|
|
6055
|
-
<li>${T('
|
|
6056
|
-
<li>${T('
|
|
6057
|
-
<li>${T('
|
|
6373
|
+
<li>${T('你的体验可以帮助后来者判断', 'Your experience helps others decide')}</li>
|
|
6374
|
+
<li>${T('购物、评价、分享是使用记录', 'Shopping, reviews, sharing are usage records')}</li>
|
|
6375
|
+
<li>${T('想正式贡献?走公开任务板 #contribute/tasks', 'Want to contribute? Use the public task board #contribute/tasks')}</li>
|
|
6058
6376
|
</ul>
|
|
6059
6377
|
</div>
|
|
6060
6378
|
<div class="w-card">
|
|
@@ -6105,18 +6423,18 @@ function renderWelcome(app) {
|
|
|
6105
6423
|
<div class="w-card">
|
|
6106
6424
|
<div class="w-card-title">🎯 ${T('跨平台引流,数据归你', 'Cross-platform traffic, your data')}</div>
|
|
6107
6425
|
<ul class="w-card-list">
|
|
6108
|
-
<li>${T('在小红书、抖音、B
|
|
6426
|
+
<li>${T('在小红书、抖音、B 站、视频号发内容', 'Post on TikTok, Instagram, YouTube, Reddit, or Pinterest')}</li>
|
|
6109
6427
|
<li>${T('用 webaz 口令引流', 'Drive traffic with webaz codes')}</li>
|
|
6110
6428
|
<li>${T('真实访问、真实转化都看得见', 'See real visits and conversions')}</li>
|
|
6111
6429
|
<li>${T("数据是你的,不是平台的", "Data is yours, not the platform's")}</li>
|
|
6112
6430
|
</ul>
|
|
6113
6431
|
</div>
|
|
6114
6432
|
<div class="w-card">
|
|
6115
|
-
<div class="w-card-title"
|
|
6433
|
+
<div class="w-card-title">🔗 ${T('分享与归因', 'Sharing & attribution')}</div>
|
|
6116
6434
|
<ul class="w-card-list">
|
|
6117
|
-
<li>${T('
|
|
6118
|
-
<li>${T('
|
|
6119
|
-
<li>${T('
|
|
6435
|
+
<li>${T('分享商品 / 内容,留下可追溯的归因记录', 'Share products / content — leave a traceable attribution record')}</li>
|
|
6436
|
+
<li>${T('符合条件并完成「分享分润」opt-in 后,协议按规则处理分润', 'If eligible and after you opt in to share-commission, the protocol settles commission per the rules')}</li>
|
|
6437
|
+
<li>${T('不承诺收益;内容发布不等同于正式贡献', 'No promised earnings; publishing content is not formal contribution')}</li>
|
|
6120
6438
|
</ul>
|
|
6121
6439
|
</div>
|
|
6122
6440
|
</div>
|
|
@@ -6177,10 +6495,10 @@ function renderWelcome(app) {
|
|
|
6177
6495
|
)}
|
|
6178
6496
|
${renderTL(
|
|
6179
6497
|
'19:00',
|
|
6180
|
-
T('
|
|
6181
|
-
T('小红书发护手霜测评 · 带 webaz 口令', 'Post the review on
|
|
6182
|
-
T('使用 +
|
|
6183
|
-
T('别人凭口令购买 →
|
|
6498
|
+
T('你作为分享者', 'You as a sharer'),
|
|
6499
|
+
T('小红书发护手霜测评 · 带 webaz 口令', 'Post the review on TikTok · with a webaz code'),
|
|
6500
|
+
T('使用 + 分享', 'Use + Share'),
|
|
6501
|
+
T('别人凭口令购买 → 留下归因记录(分润需单独 opt-in)', 'Others buy via your code → an attribution record (commission needs a separate opt-in)')
|
|
6184
6502
|
)}
|
|
6185
6503
|
${renderTL(
|
|
6186
6504
|
'22:00',
|
|
@@ -6214,7 +6532,7 @@ function renderWelcome(app) {
|
|
|
6214
6532
|
<option value="">${T('我想以什么身份开始?(可选)', 'How would you like to start? (optional)')}</option>
|
|
6215
6533
|
<option value="buyer">${T('买家 · Agent 帮我找货', 'Buyer · let agents find me deals')}</option>
|
|
6216
6534
|
<option value="seller">${T('卖家 · 上架商品并获取 Earn-Back', 'Seller · list products and earn back')}</option>
|
|
6217
|
-
<option value="creator">${T('
|
|
6535
|
+
<option value="creator">${T('分享者 · 测评、笔记、内容归因', 'Sharer · reviews, notes, content attribution')}</option>
|
|
6218
6536
|
<option value="verifier">${T('审核员 · 链接 / 内容验证', 'Verifier · link & content verification')}</option>
|
|
6219
6537
|
<option value="arbitrator">${T('仲裁员 · 争议裁决', 'Arbitrator · dispute resolution')}</option>
|
|
6220
6538
|
<option value="other">${T('其他 / 都看看', 'Other / just curious')}</option>
|
|
@@ -6224,6 +6542,16 @@ function renderWelcome(app) {
|
|
|
6224
6542
|
<div id="w-email-msg" style="font-size:12px;text-align:center;min-height:1.5em"></div>
|
|
6225
6543
|
</div>
|
|
6226
6544
|
</div>
|
|
6545
|
+
<div class="w-card w-join-card">
|
|
6546
|
+
<div class="w-join-card-left">
|
|
6547
|
+
<div class="w-card-title">🛠 ${T('想直接改进 WebAZ?', 'Want to improve WebAZ directly?')}</div>
|
|
6548
|
+
<div class="w-card-desc">${T('公开任务板:浏览 / 认领可做的任务,或提个新任务。建议无需登录;认领与提交需登录。只有 canonical 仓库被合并的 PR(或维护者认可的 issue / task / RFC)才进入贡献记录 —— sandbox、本地草稿、普通购物 / 分享都不是正式贡献。', 'Public task board: browse / claim tasks, or propose a new one. Suggesting needs no login; claiming & submitting need login. Only a merged PR on the canonical repo (or a maintainer-recognized issue / task / RFC) enters the contribution record — a sandbox run, a local draft, or ordinary shopping / sharing is not formal contribution.')}</div>
|
|
6549
|
+
</div>
|
|
6550
|
+
<div class="w-join-card-right">
|
|
6551
|
+
<a class="w-btn-full w-btn-outline" href="#contribute/tasks" style="display:flex;align-items:center;justify-content:center;text-decoration:none">${T('公开任务板', 'Public task board')}</a>
|
|
6552
|
+
<a class="w-btn-full w-btn-outline" href="#contribute/tasks/suggest" style="display:flex;align-items:center;justify-content:center;text-decoration:none">${T('建议新任务', 'Suggest a task')}</a>
|
|
6553
|
+
</div>
|
|
6554
|
+
</div>
|
|
6227
6555
|
<div class="w-card w-join-card">
|
|
6228
6556
|
<div class="w-join-card-left">
|
|
6229
6557
|
<div class="w-card-title">💡 ${T('随便逛逛?提个建议', 'Just browsing? Drop an idea')}</div>
|
|
@@ -6258,7 +6586,7 @@ function renderWelcome(app) {
|
|
|
6258
6586
|
<div class="w-card-desc">${T('已经准备好了?直接进入协议。', 'Ready already? Step into the protocol.')}</div>
|
|
6259
6587
|
</div>
|
|
6260
6588
|
<div class="w-join-card-right">
|
|
6261
|
-
<button class="w-btn-full w-btn-primary" onclick="
|
|
6589
|
+
<button class="w-btn-full w-btn-primary" onclick="openAuthSheet('reg')">${T('立即注册', 'Sign Up Now')}</button>
|
|
6262
6590
|
</div>
|
|
6263
6591
|
</div>
|
|
6264
6592
|
</div>
|
|
@@ -6271,7 +6599,7 @@ function renderWelcome(app) {
|
|
|
6271
6599
|
<div style="margin-top:10px">
|
|
6272
6600
|
<a href="https://github.com/seasonsagents-art/webaz/blob/main/docs/META-RULES-FULL.md" target="_blank" rel="noopener">${T('完整元规则', 'Full Meta-Rules')}</a>
|
|
6273
6601
|
<a href="https://github.com/seasonsagents-art/webaz" target="_blank" rel="noopener">GitHub</a>
|
|
6274
|
-
<a href="
|
|
6602
|
+
<a href="${WP_URL}" target="_blank" rel="noopener">${T('协议白皮书', 'Whitepaper')}</a>
|
|
6275
6603
|
<a href="mailto:contact@webaz.xyz">📧 contact@webaz.xyz</a>
|
|
6276
6604
|
</div>
|
|
6277
6605
|
</footer>
|
|
@@ -6389,7 +6717,7 @@ async function renderGovernanceOnboarding(app) {
|
|
|
6389
6717
|
<ul style="margin:0;padding-left:20px;color:#1e3a8a;line-height:1.8;font-size:14px">
|
|
6390
6718
|
<li><strong>arbitrator</strong> — ${T('仲裁纠纷 dispute(Iron-Rule 真人 Passkey 必备)', 'Arbitrate disputes (Iron-Rule requires real human Passkey)')}</li>
|
|
6391
6719
|
<li><strong>verifier</strong> — ${T('验证 claim(同 Iron-Rule)', 'Verify claims (same Iron-Rule)')}</li>
|
|
6392
|
-
<li>${T('卸任 30 天冷却
|
|
6720
|
+
<li>${T('卸任 30 天冷却 · outlier 是风险信号,经复核确认错误(confirmed-wrong)才触发 auto-deactivate · 有申诉路径', 'Resignation 30d cooldown · an outlier is a risk signal — auto-deactivate only triggers when review confirms it wrong (confirmed-wrong) · with an appeal path')}</li>
|
|
6393
6721
|
</ul>
|
|
6394
6722
|
</section>
|
|
6395
6723
|
|
|
@@ -6460,6 +6788,8 @@ async function renderGovernanceOnboarding(app) {
|
|
|
6460
6788
|
<div style="font-size:13px;line-height:1.9;color:#3f3f46">
|
|
6461
6789
|
📅 ${T('注册', 'Registered')} ≥ <strong>${esc(ver.registration_days ?? '?')}</strong> ${T('天', 'd')}<br>
|
|
6462
6790
|
📦 ${T('完成订单', 'Completed orders')} ≥ <strong>${esc(ver.completed_orders ?? '?')}</strong><br>
|
|
6791
|
+
⭐ ${T('信誉', 'Reputation')} ≥ <strong>${esc(ver.reputation ?? '?')}</strong><br>
|
|
6792
|
+
💰 ${T('钱包余额', 'Balance')} ≥ <strong>${esc(ver.balance_waz ?? '?')}</strong> WAZ<br>
|
|
6463
6793
|
✉️ ${T('邮箱验证', 'Email verified')} · 🚫 ${T('零仲裁判输', 'Zero disputes lost')} · ✅ ${T('未曾暂停', 'Never suspended')}
|
|
6464
6794
|
</div>
|
|
6465
6795
|
</div>
|
|
@@ -6471,6 +6801,349 @@ async function renderGovernanceOnboarding(app) {
|
|
|
6471
6801
|
}
|
|
6472
6802
|
}
|
|
6473
6803
|
|
|
6804
|
+
// ─── PR9E-1 Public Contribution Pages (#contribute/tasks · /:id · /tasks/suggest) ────────────────────
|
|
6805
|
+
// Public, no-auth PWA surface over the #329/#330/#331 APIs so a stranger + their agent can participate at
|
|
6806
|
+
// low friction. READ + SUGGEST only. Hard boundaries (mirror the server contract):
|
|
6807
|
+
// · Restricted/internal tasks NEVER reach these pages — the list/detail call ONLY the public-scope
|
|
6808
|
+
// endpoints (/api/public/build-tasks[/:id]); the server hides everything but audience=public+open.
|
|
6809
|
+
// · The agent prompt instructs PRs to the CANONICAL repo only (from the response, trusted config); a
|
|
6810
|
+
// sandbox run / local draft is explicitly NOT participation; the agent is only an executor (DCO).
|
|
6811
|
+
// · No economic field is ever rendered (no reward/payout/amount/score) — value stays uncommitted
|
|
6812
|
+
// (RFC-017 I-12). No auto GitHub op, no merge, no PR submission from the browser. Claim needs login.
|
|
6813
|
+
const CONTRIBUTE_PROMPT_STATE = { text: '' } // last-built copy-ready prompt, for the copy button
|
|
6814
|
+
function _cEsc(s) { return escHtml(String(s ?? '')) }
|
|
6815
|
+
function _cArrList(arr, empty) {
|
|
6816
|
+
if (!Array.isArray(arr) || arr.length === 0) return `<div style="color:#a1a1aa;font-size:13px">${_cEsc(empty)}</div>`
|
|
6817
|
+
return '<ul style="margin:6px 0 0;padding-left:18px;line-height:1.8">' + arr.map(x => `<li><code style="font-size:12px;word-break:break-all">${_cEsc(x)}</code></li>`).join('') + '</ul>'
|
|
6818
|
+
}
|
|
6819
|
+
function _cBoundaryHTML(vb, T) {
|
|
6820
|
+
if (!vb) return ''
|
|
6821
|
+
const notice = window._lang === 'en' ? (vb.notice_en || '') : (vb.notice_zh || '')
|
|
6822
|
+
return `<div style="background:#f4f4f5;border:1px solid #e4e4e7;border-radius:8px;padding:12px;font-size:12px;color:#52525B;line-height:1.6;margin:12px 0">
|
|
6823
|
+
🔒 ${T('价值边界', 'Value boundary')}: <strong>${_cEsc(vb.value_state || 'uncommitted')}</strong> · ${_cEsc(vb.boundary_ref || 'RFC-017 I-12')}<br>${_cEsc(notice)}</div>`
|
|
6824
|
+
}
|
|
6825
|
+
function _cRiskBadge(level, T) {
|
|
6826
|
+
if (!level) return ''
|
|
6827
|
+
const c = { low: '#16a34a', medium: '#d97706', high: '#dc2626', critical: '#7f1d1d' }[level] || '#6b7280'
|
|
6828
|
+
return `<span style="background:${c}1a;color:${c};padding:1px 8px;border-radius:99px;font-size:11px;font-weight:600">${T('风险', 'risk')}:${_cEsc(level)}</span>`
|
|
6829
|
+
}
|
|
6830
|
+
function _cDuration(d, T) {
|
|
6831
|
+
if (!d || (d.min_minutes == null && d.max_minutes == null)) return ''
|
|
6832
|
+
const lo = d.min_minutes, hi = d.max_minutes
|
|
6833
|
+
const span = lo != null && hi != null ? `${lo}–${hi}` : (lo ?? hi)
|
|
6834
|
+
return `<span style="font-size:11px;color:#6b7280">⏱ ${T('预计', 'est.')} ${_cEsc(span)} ${T('分钟', 'min')}</span>`
|
|
6835
|
+
}
|
|
6836
|
+
|
|
6837
|
+
// Build a copy-ready agent prompt from the (trusted) public detail payload. Names the boundary, forbidden
|
|
6838
|
+
// paths, prohibited actions, verification commands, the canonical-repo PR requirement, and that a
|
|
6839
|
+
// sandbox/local draft is NOT participation. It states the contribution confers no economic right (it does
|
|
6840
|
+
// NOT restate any reward/payout/amount/score — value stays uncommitted).
|
|
6841
|
+
function buildContributeAgentPrompt(task, cct, T) {
|
|
6842
|
+
const m = task.agent_metadata || {}
|
|
6843
|
+
const join = (a, sep) => (Array.isArray(a) && a.length ? a.join(sep) : null)
|
|
6844
|
+
const L = []
|
|
6845
|
+
L.push(`# WebAZ contribution task: ${task.title || task.task_id || ''}`)
|
|
6846
|
+
L.push(`Task id: ${task.task_id || ''}`)
|
|
6847
|
+
if (task.area) L.push(`Area: ${task.area}`)
|
|
6848
|
+
if (m.task_type) L.push(`Type: ${m.task_type}`)
|
|
6849
|
+
L.push('')
|
|
6850
|
+
L.push('## Boundary — stay strictly inside this')
|
|
6851
|
+
L.push('Allowed paths: ' + (join(m.allowed_paths, ', ') || '(none specified — ask the maintainer before editing)'))
|
|
6852
|
+
L.push('Forbidden paths (NEVER touch): ' + (join(m.forbidden_paths, ', ') || '(none listed)'))
|
|
6853
|
+
L.push('Prohibited actions (NEVER do): ' + (join(m.prohibited_actions, '; ') || '(none listed)'))
|
|
6854
|
+
if (join(m.required_capabilities, ', ')) L.push('Required capabilities: ' + join(m.required_capabilities, ', '))
|
|
6855
|
+
L.push('')
|
|
6856
|
+
L.push('## Acceptance & verification')
|
|
6857
|
+
L.push('Acceptance criteria: ' + (join(m.acceptance_criteria, '; ') || '(see task)'))
|
|
6858
|
+
L.push('Verification commands (must pass): ' + (join(m.verification_commands, ' && ') || '(see task)'))
|
|
6859
|
+
if (m.expected_results) L.push('Expected results: ' + m.expected_results)
|
|
6860
|
+
L.push('Deliverables: ' + (join(m.deliverables, '; ') || '(see task)'))
|
|
6861
|
+
if (m.definition_of_done) L.push('Definition of done: ' + m.definition_of_done)
|
|
6862
|
+
L.push('')
|
|
6863
|
+
L.push('## Submitting — canonical repository only')
|
|
6864
|
+
L.push(`Open a Pull Request whose BASE repository is the canonical WebAZ repo: ${cct.expected_pr_base_repo || ''} (${cct.canonical_github_url || ''}), base branch ${cct.base_branch || 'main'}.`)
|
|
6865
|
+
L.push('If any target repo differs from this canonical repo, STOP and ask the human to confirm — never contribute to a non-canonical repository.')
|
|
6866
|
+
L.push('Sign every commit (DCO): `git commit -s`. A real human/org is the accountable party; the agent is only an executor.')
|
|
6867
|
+
L.push('')
|
|
6868
|
+
L.push('## Not participation')
|
|
6869
|
+
L.push('A sandbox run or a local-only 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.')
|
|
6870
|
+
L.push('This task confers no economic or redemption right; contribution value is uncommitted (RFC-017 I-12).')
|
|
6871
|
+
return L.join('\n')
|
|
6872
|
+
}
|
|
6873
|
+
|
|
6874
|
+
window.contributeApplyFilters = () => {
|
|
6875
|
+
const area = (document.getElementById('ct-f-area')?.value || '').trim()
|
|
6876
|
+
const risk = document.getElementById('ct-f-risk')?.value || ''
|
|
6877
|
+
const ac = document.getElementById('ct-f-ac')?.value || ''
|
|
6878
|
+
const agentcaps = (document.getElementById('ct-f-agentcaps')?.value || '').trim()
|
|
6879
|
+
const maxdur = (document.getElementById('ct-f-maxdur')?.value || '').trim()
|
|
6880
|
+
const ctx = document.getElementById('ct-f-ctx')?.value || ''
|
|
6881
|
+
const budget = document.getElementById('ct-f-budget')?.value || ''
|
|
6882
|
+
const p = new URLSearchParams()
|
|
6883
|
+
if (area) p.set('area', area)
|
|
6884
|
+
if (risk) p.set('risk_level', risk)
|
|
6885
|
+
if (ac) p.set('auto_claimable', ac)
|
|
6886
|
+
if (agentcaps) p.set('agent_capabilities', agentcaps)
|
|
6887
|
+
if (maxdur) p.set('max_duration_minutes', maxdur)
|
|
6888
|
+
if (ctx) p.set('estimated_context_size', ctx)
|
|
6889
|
+
if (budget) p.set('estimated_agent_budget', budget)
|
|
6890
|
+
const qs = p.toString()
|
|
6891
|
+
location.hash = '#contribute/tasks' + (qs ? '?' + qs : '')
|
|
6892
|
+
}
|
|
6893
|
+
|
|
6894
|
+
window.contributeCopyPrompt = async () => {
|
|
6895
|
+
const msg = document.getElementById('ct-prompt-msg')
|
|
6896
|
+
try {
|
|
6897
|
+
await navigator.clipboard.writeText(CONTRIBUTE_PROMPT_STATE.text || '')
|
|
6898
|
+
if (msg) msg.textContent = window._lang === 'en' ? '✓ Copied' : '✓ 已复制'
|
|
6899
|
+
} catch {
|
|
6900
|
+
// clipboard unavailable (insecure context / permission) — select the textarea so the user can copy
|
|
6901
|
+
const ta = document.getElementById('ct-prompt'); if (ta) { ta.focus(); ta.select() }
|
|
6902
|
+
if (msg) msg.textContent = window._lang === 'en' ? 'Select the text above and copy manually' : '请选中上方文本手动复制'
|
|
6903
|
+
}
|
|
6904
|
+
}
|
|
6905
|
+
|
|
6906
|
+
window.contributeClaim = async (id) => {
|
|
6907
|
+
const T = (zh, e) => window._lang === 'en' && e ? e : zh
|
|
6908
|
+
if (!state.apiKey) { // not logged in → save intended task and go to login
|
|
6909
|
+
try { sessionStorage.setItem('webaz_intended_hash', '#contribute/tasks/' + id) } catch {}
|
|
6910
|
+
navigate('#login'); return
|
|
6911
|
+
}
|
|
6912
|
+
const { status, body } = await apiWithStatus('POST', '/build-tasks/' + id + '/claim', { provenance: 'unspecified' })
|
|
6913
|
+
if (status === 200 && body && !body.error) { toast$(T('已认领 — 去「我的共建」查看', 'Claimed — see “My contributions”')); route(true); return }
|
|
6914
|
+
toast$((body && (body.error || body.error_code)) || T('认领失败', 'Claim failed'))
|
|
6915
|
+
}
|
|
6916
|
+
|
|
6917
|
+
window.submitContributeProposal = async () => {
|
|
6918
|
+
const T = (zh, e) => window._lang === 'en' && e ? e : zh
|
|
6919
|
+
const val = (id) => (document.getElementById(id)?.value || '').trim()
|
|
6920
|
+
const out = document.getElementById('ct-suggest-result')
|
|
6921
|
+
const body = {
|
|
6922
|
+
title: val('cs-title'), summary: val('cs-summary'),
|
|
6923
|
+
suggested_area: val('cs-area') || undefined, expected_outcome: val('cs-outcome') || undefined,
|
|
6924
|
+
source_ref: val('cs-source') || undefined, proposer_github_login: val('cs-login') || undefined,
|
|
6925
|
+
}
|
|
6926
|
+
if (out) out.innerHTML = `<div style="color:#a1a1aa;font-size:13px">${T('提交中…', 'Submitting…')}</div>`
|
|
6927
|
+
const { status, body: r } = await apiWithStatus('POST', '/public/task-proposals', body)
|
|
6928
|
+
if (status === 200 && r && r.proposal) {
|
|
6929
|
+
const notice = window._lang === 'en' ? (r.value_boundary?.notice_en || '') : (r.value_boundary?.notice_zh || '')
|
|
6930
|
+
if (out) out.innerHTML = `<div style="background:#dcfce7;border:1px solid #16a34a;border-radius:8px;padding:14px">
|
|
6931
|
+
<div style="font-weight:600;color:#166534">✓ ${T('已收到你的建议', 'Your suggestion was received')}</div>
|
|
6932
|
+
<div style="font-size:13px;color:#15803d;margin-top:6px">${T('建议编号', 'Proposal id')}: <code>${_cEsc(r.proposal.id)}</code></div>
|
|
6933
|
+
<div style="font-size:12px;color:#15803d;margin-top:8px">${T('这是一条建议,不是贡献事实 / 奖励 / 正式参与。它进入维护者收件箱待审,绝不会自动变成正式任务或出现在公开任务板。', 'This is a SUGGESTION — not a contribution fact, not a reward, not formal participation. It enters the maintainer review inbox and never auto-becomes a task or appears on the public board.')}</div>
|
|
6934
|
+
<div style="font-size:11px;color:#52525B;margin-top:8px">${_cEsc(notice)}</div></div>`
|
|
6935
|
+
return
|
|
6936
|
+
}
|
|
6937
|
+
// typed errors — make RATE_LIMITED / DUPLICATE_PROPOSAL / validation legible
|
|
6938
|
+
const code = (r && r.error_code) || ''
|
|
6939
|
+
let human
|
|
6940
|
+
if (status === 429 || code === 'RATE_LIMITED') human = T('提交太频繁,请稍后再试(每小时有上限)。', 'Too many submissions — please try again later (there is an hourly limit).')
|
|
6941
|
+
else if (status === 409 || code === 'DUPLICATE_PROPOSAL') human = T('相同建议已在收件箱中,请勿重复提交。', 'An identical suggestion is already in the inbox — no need to submit it again.') + (r && r.existing_id ? ` (#${_cEsc(r.existing_id)})` : '')
|
|
6942
|
+
else human = ((r && r.error) || T('提交失败,请检查输入。', 'Submission failed — please check your input.')) + (code ? ` [${_cEsc(code)}]` : '')
|
|
6943
|
+
if (out) out.innerHTML = `<div style="background:#fee2e2;border:1px solid #ef4444;border-radius:8px;padding:14px;color:#991b1b;font-size:13px">⚠️ ${human}</div>`
|
|
6944
|
+
}
|
|
6945
|
+
|
|
6946
|
+
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`
|
|
6947
|
+
|
|
6948
|
+
window.contributeSetLang = (lang) => {
|
|
6949
|
+
if (lang !== 'zh' && lang !== 'en') return
|
|
6950
|
+
setLang(lang)
|
|
6951
|
+
document.getElementById('html-root')?.setAttribute('lang', lang === 'en' ? 'en' : 'zh-CN')
|
|
6952
|
+
route(true)
|
|
6953
|
+
}
|
|
6954
|
+
|
|
6955
|
+
function contributeLangSwitchHTML(T) {
|
|
6956
|
+
const en = window._lang === 'en'
|
|
6957
|
+
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>`
|
|
6958
|
+
return `<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;margin:0 0 18px;padding-bottom:10px;border-bottom:1px solid #e4e4e7">
|
|
6959
|
+
<a href="#welcome" style="color:#52525B;text-decoration:none;font-size:13px;font-weight:700">← ${T('WebAZ 欢迎页', 'WebAZ Welcome')}</a>
|
|
6960
|
+
<div role="group" aria-label="${T('语言切换', 'Language switch')}" style="display:flex;align-items:center;gap:2px;border:1px solid #e4e4e7;border-radius:9px;padding:2px;background:#fff">
|
|
6961
|
+
${btn('zh', '中文', !en)}${btn('en', 'EN', en)}
|
|
6962
|
+
</div>
|
|
6963
|
+
</div>`
|
|
6964
|
+
}
|
|
6965
|
+
|
|
6966
|
+
function contributePageShell(T, inner) {
|
|
6967
|
+
return `${preLaunchBannerHTML()}<div style="${CONTRIBUTE_PAGE_STYLE}">${contributeLangSwitchHTML(T)}${inner}</div>`
|
|
6968
|
+
}
|
|
6969
|
+
|
|
6970
|
+
async function renderContributeTasks(app) {
|
|
6971
|
+
const en = window._lang === 'en'
|
|
6972
|
+
const T = (zh, e) => en && e ? e : zh
|
|
6973
|
+
const q = state._urlQuery || {}
|
|
6974
|
+
app.innerHTML = contributePageShell(T, `
|
|
6975
|
+
<header style="margin-bottom:20px">
|
|
6976
|
+
<h1 style="font-size:clamp(24px,5vw,32px);margin:0 0 8px;color:#18181B">🛠️ ${T('公开任务板', 'Open Task Board')}</h1>
|
|
6977
|
+
<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>
|
|
6978
|
+
</header>
|
|
6979
|
+
${_cBoundaryHTML(UNCOMMITTED_VALUE_BOUNDARY_HINT(), T)}
|
|
6980
|
+
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:14px">
|
|
6981
|
+
<input id="ct-f-area" placeholder="${T('领域 area', 'area')}" value="${_cEsc(q.area || '')}" style="flex:1;min-width:120px;padding:8px 10px;border:1px solid #d4d4d8;border-radius:8px;font-size:13px">
|
|
6982
|
+
<select id="ct-f-risk" style="padding:8px 10px;border:1px solid #d4d4d8;border-radius:8px;font-size:13px">
|
|
6983
|
+
<option value="">${T('风险(全部)', 'risk (all)')}</option>
|
|
6984
|
+
${['low','medium','high','critical'].map(r => `<option value="${r}" ${q.risk_level === r ? 'selected' : ''}>${r}</option>`).join('')}
|
|
6985
|
+
</select>
|
|
6986
|
+
<select id="ct-f-ac" style="padding:8px 10px;border:1px solid #d4d4d8;border-radius:8px;font-size:13px">
|
|
6987
|
+
<option value="">${T('可自动认领(全部)', 'auto-claimable (all)')}</option>
|
|
6988
|
+
<option value="true" ${q.auto_claimable === 'true' ? 'selected' : ''}>${T('仅可自动认领', 'auto-claimable only')}</option>
|
|
6989
|
+
<option value="false" ${q.auto_claimable === 'false' ? 'selected' : ''}>${T('仅需人工认领', 'manual-claim only')}</option>
|
|
6990
|
+
</select>
|
|
6991
|
+
<input id="ct-f-agentcaps" placeholder="${T('你的能力(逗号分隔)→ 你能做的任务', 'your capabilities (comma-sep) → tasks you can do')}" value="${_cEsc(q.agent_capabilities || '')}" title="${T('只显示所需能力都在你能力集内的任务', 'shows only tasks whose required capabilities are all within yours')}" style="flex:1;min-width:160px;padding:8px 10px;border:1px solid #d4d4d8;border-radius:8px;font-size:13px">
|
|
6992
|
+
<input id="ct-f-maxdur" type="number" min="1" placeholder="${T('≤ 分钟', '≤ min')}" value="${_cEsc(q.max_duration_minutes || '')}" title="${T('只显示预计耗时不超过该分钟数的任务', 'tasks whose estimated max duration fits within this many minutes')}" style="width:96px;padding:8px 10px;border:1px solid #d4d4d8;border-radius:8px;font-size:13px">
|
|
6993
|
+
<select id="ct-f-ctx" style="padding:8px 10px;border:1px solid #d4d4d8;border-radius:8px;font-size:13px">
|
|
6994
|
+
<option value="">${T('上下文(全部)', 'context (all)')}</option>
|
|
6995
|
+
${['small','medium','large'].map(c => `<option value="${c}" ${q.estimated_context_size === c ? 'selected' : ''}>${c}</option>`).join('')}
|
|
6996
|
+
</select>
|
|
6997
|
+
<select id="ct-f-budget" title="${T('按 agent 预计工作量筛选(不是费用 / 报酬)', 'filter by estimated agent effort (not a cost / payment)')}" style="padding:8px 10px;border:1px solid #d4d4d8;border-radius:8px;font-size:13px">
|
|
6998
|
+
<option value="">${T('agent 工作量(全部)', 'agent effort (all)')}</option>
|
|
6999
|
+
${['minimal','small','moderate','large','xlarge'].map(b => `<option value="${b}" ${q.estimated_agent_budget === b ? 'selected' : ''}>${b}</option>`).join('')}
|
|
7000
|
+
</select>
|
|
7001
|
+
<button onclick="contributeApplyFilters()" style="padding:8px 16px;background:#6366f1;color:#fff;border:none;border-radius:8px;font-size:13px;cursor:pointer">${T('筛选', 'Filter')}</button>
|
|
7002
|
+
<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>
|
|
7003
|
+
</div>
|
|
7004
|
+
<div id="ct-list"><div style="color:#a1a1aa;text-align:center;padding:30px">${T('加载中…', 'Loading…')}</div></div>
|
|
7005
|
+
`)
|
|
7006
|
+
|
|
7007
|
+
const params = new URLSearchParams()
|
|
7008
|
+
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])
|
|
7009
|
+
const list = document.getElementById('ct-list')
|
|
7010
|
+
try {
|
|
7011
|
+
const res = await fetch('/api/public/build-tasks' + (params.toString() ? '?' + params : ''), { signal: AbortSignal.timeout(10000) })
|
|
7012
|
+
const j = await res.json().catch(() => ({}))
|
|
7013
|
+
if (!res.ok) { list.innerHTML = `<div style="background:#fee2e2;border:1px solid #ef4444;border-radius:8px;padding:14px;color:#991b1b;font-size:13px">⚠️ ${_cEsc(j.error || T('筛选无效', 'Invalid filter'))}${j.error_code ? ` [${_cEsc(j.error_code)}]` : ''}</div>`; return }
|
|
7014
|
+
const tasks = j.tasks || []
|
|
7015
|
+
if (tasks.length === 0) { list.innerHTML = `<div style="color:#a1a1aa;text-align:center;padding:30px;font-size:14px">${T('暂无符合条件的公开任务。', 'No open tasks match the filters.')}</div>`; return }
|
|
7016
|
+
list.innerHTML = tasks.map(task => {
|
|
7017
|
+
const m = task.agent_metadata || {}
|
|
7018
|
+
const caps = Array.isArray(m.required_capabilities) && m.required_capabilities.length
|
|
7019
|
+
? `<div style="font-size:11px;color:#6b7280;margin-top:6px">${T('所需能力', 'capabilities')}: ${m.required_capabilities.map(c => `<code style="font-size:11px">${_cEsc(c)}</code>`).join(' ')}</div>` : ''
|
|
7020
|
+
return `<div onclick="location.hash='#contribute/tasks/${_cEsc(task.task_id)}'" style="background:#fff;border:1px solid #e4e4e7;border-radius:10px;padding:14px;margin-bottom:10px;cursor:pointer">
|
|
7021
|
+
<div style="display:flex;justify-content:space-between;gap:10px;align-items:flex-start">
|
|
7022
|
+
<div style="font-weight:600;color:#18181B;font-size:15px">${_cEsc(task.title)}</div>
|
|
7023
|
+
${m.auto_claimable ? `<span style="background:#dbeafe;color:#1e40af;padding:1px 8px;border-radius:99px;font-size:11px;font-weight:600;white-space:nowrap">${T('可自动认领', 'auto-claimable')}</span>` : ''}
|
|
7024
|
+
</div>
|
|
7025
|
+
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-top:8px">
|
|
7026
|
+
${task.area ? `<span style="font-size:11px;color:#6b7280">📂 ${_cEsc(task.area)}</span>` : ''}
|
|
7027
|
+
${_cRiskBadge(m.risk_level, T)}
|
|
7028
|
+
${_cDuration(m.estimated_duration, T)}
|
|
7029
|
+
</div>
|
|
7030
|
+
${caps}
|
|
7031
|
+
<div style="font-size:12px;color:#6366f1;margin-top:8px">${T('查看详情 + 复制 agent 提示', 'View detail + copy agent prompt')} →</div>
|
|
7032
|
+
</div>`
|
|
7033
|
+
}).join('')
|
|
7034
|
+
} catch (e) {
|
|
7035
|
+
list.innerHTML = `<div style="color:#dc2626;text-align:center;padding:20px;font-size:14px">${T('加载失败', 'Load failed')}: ${_cEsc(e.message || 'unknown')}</div>`
|
|
7036
|
+
}
|
|
7037
|
+
}
|
|
7038
|
+
|
|
7039
|
+
async function renderContributeTaskDetail(app, id) {
|
|
7040
|
+
const en = window._lang === 'en'
|
|
7041
|
+
const T = (zh, e) => en && e ? e : zh
|
|
7042
|
+
app.innerHTML = contributePageShell(T, `<div style="color:#a1a1aa;text-align:center;padding:40px">${T('加载中…', 'Loading…')}</div>`)
|
|
7043
|
+
let j, res
|
|
7044
|
+
try {
|
|
7045
|
+
res = await fetch('/api/public/build-tasks/' + encodeURIComponent(id), { signal: AbortSignal.timeout(10000) })
|
|
7046
|
+
j = await res.json().catch(() => ({}))
|
|
7047
|
+
} catch (e) {
|
|
7048
|
+
app.innerHTML = contributePageShell(T, `<div style="color:#dc2626;text-align:center;padding:30px">${T('加载失败', 'Load failed')}: ${_cEsc(e.message || 'unknown')}</div>`); return
|
|
7049
|
+
}
|
|
7050
|
+
if (!res.ok || !j.task) {
|
|
7051
|
+
app.innerHTML = contributePageShell(T, `
|
|
7052
|
+
<div style="color:#52525B;text-align:center;padding:30px">${T('任务不存在或非公开。', 'Task not found or not public.')}</div>
|
|
7053
|
+
<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
|
|
7054
|
+
}
|
|
7055
|
+
const task = j.task
|
|
7056
|
+
const cct = j.canonical_contribution_target || {}
|
|
7057
|
+
const m = task.agent_metadata || {}
|
|
7058
|
+
CONTRIBUTE_PROMPT_STATE.text = buildContributeAgentPrompt(task, cct, T)
|
|
7059
|
+
const section = (icon, title, inner) => `<section style="background:#fff;border:1px solid #e4e4e7;border-radius:12px;padding:18px;margin-bottom:14px">
|
|
7060
|
+
<h3 style="margin:0 0 10px;color:#18181B;font-size:16px">${icon} ${title}</h3>${inner}</section>`
|
|
7061
|
+
const claimBtn = m.auto_claimable
|
|
7062
|
+
? (state.apiKey
|
|
7063
|
+
? `<button onclick="contributeClaim('${_cEsc(task.task_id)}')" style="padding:10px 20px;background:#6366f1;color:#fff;border:none;border-radius:8px;font-size:14px;cursor:pointer">✋ ${T('认领此任务', 'Claim this task')}</button>`
|
|
7064
|
+
: `<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>`)
|
|
7065
|
+
: `<div style="font-size:12px;color:#6b7280">${T('此任务需人工认领流程,不可自动认领。', 'This task uses a manual claim flow; it is not auto-claimable.')}</div>`
|
|
7066
|
+
|
|
7067
|
+
app.innerHTML = contributePageShell(T, `
|
|
7068
|
+
<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>
|
|
7069
|
+
<h1 style="font-size:clamp(22px,4.5vw,28px);margin:0 0 8px;color:#18181B">${_cEsc(task.title)}</h1>
|
|
7070
|
+
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:6px">
|
|
7071
|
+
${task.area ? `<span style="font-size:12px;color:#6b7280">📂 ${_cEsc(task.area)}</span>` : ''}
|
|
7072
|
+
${m.task_type ? `<span style="font-size:12px;color:#6b7280">🔖 ${_cEsc(m.task_type)}</span>` : ''}
|
|
7073
|
+
${_cRiskBadge(m.risk_level, T)} ${_cDuration(m.estimated_duration, T)}
|
|
7074
|
+
${m.auto_claimable ? `<span style="background:#dbeafe;color:#1e40af;padding:1px 8px;border-radius:99px;font-size:11px;font-weight:600">${T('可自动认领', 'auto-claimable')}</span>` : ''}
|
|
7075
|
+
</div>
|
|
7076
|
+
${_cBoundaryHTML(task.value_boundary || j.value_boundary, T)}
|
|
7077
|
+
|
|
7078
|
+
${section('🧭', T('执行边界', 'Execution boundary'), `
|
|
7079
|
+
<div style="font-size:13px;color:#374151;font-weight:600">${T('允许修改的路径', 'Allowed paths')}</div>${_cArrList(m.allowed_paths, T('未指定 — 请先向维护者确认', 'Not specified — confirm with the maintainer'))}
|
|
7080
|
+
<div style="font-size:13px;color:#991b1b;font-weight:600;margin-top:10px">${T('禁止触碰的路径', 'Forbidden paths')}</div>${_cArrList(m.forbidden_paths, T('无', 'none'))}
|
|
7081
|
+
<div style="font-size:13px;color:#991b1b;font-weight:600;margin-top:10px">${T('禁止的动作', 'Prohibited actions')}</div>${_cArrList(m.prohibited_actions, T('无', 'none'))}
|
|
7082
|
+
${Array.isArray(m.required_capabilities) && m.required_capabilities.length ? `<div style="font-size:13px;color:#374151;font-weight:600;margin-top:10px">${T('所需能力', 'Required capabilities')}</div>${_cArrList(m.required_capabilities, '')}` : ''}`)}
|
|
7083
|
+
|
|
7084
|
+
${section('✅', T('验收与验证', 'Acceptance & verification'), `
|
|
7085
|
+
<div style="font-size:13px;color:#374151;font-weight:600">${T('验收标准', 'Acceptance criteria')}</div>${_cArrList(m.acceptance_criteria, T('见任务描述', 'see task'))}
|
|
7086
|
+
<div style="font-size:13px;color:#374151;font-weight:600;margin-top:10px">${T('验证命令(须通过)', 'Verification commands (must pass)')}</div>${_cArrList(m.verification_commands, T('见任务描述', 'see task'))}
|
|
7087
|
+
${m.expected_results ? `<div style="font-size:13px;color:#374151;font-weight:600;margin-top:10px">${T('预期结果', 'Expected results')}</div><div style="font-size:13px;color:#52525B;line-height:1.6;white-space:pre-wrap">${_cEsc(m.expected_results)}</div>` : ''}
|
|
7088
|
+
<div style="font-size:13px;color:#374151;font-weight:600;margin-top:10px">${T('交付物', 'Deliverables')}</div>${_cArrList(m.deliverables, T('见任务描述', 'see task'))}
|
|
7089
|
+
${m.definition_of_done ? `<div style="font-size:13px;color:#374151;font-weight:600;margin-top:10px">${T('完成定义', 'Definition of done')}</div><div style="font-size:13px;color:#52525B;line-height:1.6;white-space:pre-wrap">${_cEsc(m.definition_of_done)}</div>` : ''}`)}
|
|
7090
|
+
|
|
7091
|
+
${section('🎯', T('提交目标(必须是 canonical 仓库)', 'Submit to the canonical repository'), `
|
|
7092
|
+
<div style="font-size:13px;color:#52525B;line-height:1.7">
|
|
7093
|
+
${T('PR 的 base 仓库必须是', 'A PR\'s base repository MUST be')} <code>${_cEsc(cct.expected_pr_base_repo || cct.canonical_repository_full_name || '')}</code>
|
|
7094
|
+
${cct.canonical_github_url ? ` · <a href="${_cEsc(cct.canonical_github_url)}" target="_blank" rel="noopener" style="color:#1d4ed8">${_cEsc(cct.canonical_github_url)}</a>` : ''}
|
|
7095
|
+
${cct.base_branch ? ` · base <code>${_cEsc(cct.base_branch)}</code>` : ''}
|
|
7096
|
+
</div>
|
|
7097
|
+
<div style="font-size:12px;color:#92400e;background:#fef3c7;border-radius:8px;padding:10px;margin-top:10px;line-height:1.6">⚠️ ${T('若目标仓库与上述 canonical 仓库不一致,请停止并向真人确认 —— 不要向非 canonical 仓库提交。提交需 DCO 签名(git commit -s),问责主体是真人/组织,agent 只是执行者。', 'If a target repo differs from this canonical repo, STOP and ask a human to confirm — never contribute to a non-canonical repository. Commits need DCO sign-off (git commit -s); the accountable party is a real human/org, the agent is only an executor.')}</div>`)}
|
|
7098
|
+
|
|
7099
|
+
${section('📋', T('复制给你的 agent', 'Copy-ready agent prompt'), `
|
|
7100
|
+
<div style="font-size:12px;color:#6b7280;margin-bottom:8px">${T('包含边界 / 禁止动作 / 验证命令 / 提交到 canonical 仓库的要求,并明确 sandbox / 本地草稿不算正式参与。', 'Includes the boundary / prohibited actions / verification commands / the canonical-repo PR requirement, and states that a sandbox / local draft is not participation.')}</div>
|
|
7101
|
+
<textarea id="ct-prompt" readonly style="width:100%;box-sizing:border-box;height:200px;font-family:ui-monospace,'SF Mono',Menlo,monospace;font-size:11px;line-height:1.5;border:1px solid #d4d4d8;border-radius:8px;padding:10px;color:#374151">${_cEsc(CONTRIBUTE_PROMPT_STATE.text)}</textarea>
|
|
7102
|
+
<div style="display:flex;gap:10px;align-items:center;margin-top:8px">
|
|
7103
|
+
<button onclick="contributeCopyPrompt()" style="padding:8px 16px;background:#6366f1;color:#fff;border:none;border-radius:8px;font-size:13px;cursor:pointer">📋 ${T('复制提示', 'Copy prompt')}</button>
|
|
7104
|
+
<span id="ct-prompt-msg" style="font-size:12px;color:#16a34a"></span>
|
|
7105
|
+
</div>`)}
|
|
7106
|
+
|
|
7107
|
+
${section('✋', T('认领', 'Claim'), claimBtn)}
|
|
7108
|
+
|
|
7109
|
+
<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>
|
|
7110
|
+
`)
|
|
7111
|
+
}
|
|
7112
|
+
|
|
7113
|
+
async function renderContributeSuggest(app) {
|
|
7114
|
+
const en = window._lang === 'en'
|
|
7115
|
+
const T = (zh, e) => en && e ? e : zh
|
|
7116
|
+
const field = (id, label, ph, ta) => `
|
|
7117
|
+
<label style="display:block;font-size:13px;color:#374151;font-weight:600;margin:12px 0 4px">${label}</label>
|
|
7118
|
+
${ta
|
|
7119
|
+
? `<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>`
|
|
7120
|
+
: `<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">`}`
|
|
7121
|
+
app.innerHTML = contributePageShell(T, `
|
|
7122
|
+
<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>
|
|
7123
|
+
<h1 style="font-size:clamp(22px,4.5vw,28px);margin:0 0 8px;color:#18181B">💡 ${T('建议一个任务', 'Suggest a task')}</h1>
|
|
7124
|
+
<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>
|
|
7125
|
+
${_cBoundaryHTML(UNCOMMITTED_VALUE_BOUNDARY_HINT(), T)}
|
|
7126
|
+
${field('cs-title', T('标题', 'Title') + ' *', T('简短描述这个任务', 'Short description of the task'))}
|
|
7127
|
+
${field('cs-summary', T('原因 / 说明', 'Reason / summary') + ' *', T('为什么值得做?要解决什么?', 'Why is it worth doing? What does it solve?'), true)}
|
|
7128
|
+
${field('cs-area', T('建议领域(可选)', 'Suggested area (optional)'), 'docs / api / pwa …')}
|
|
7129
|
+
${field('cs-outcome', T('预期结果(可选)', 'Expected outcome (optional)'), T('完成后应达成什么', 'What should be true when done'), true)}
|
|
7130
|
+
${field('cs-source', T('参考链接(可选)', 'Source reference (optional)'), 'https://… ' + T('(仅作参考,不决定提交目标仓库)', '(reference only; does not set the target repo)'))}
|
|
7131
|
+
${field('cs-login', T('你的 GitHub 用户名(可选)', 'Your GitHub login (optional)'), 'octocat')}
|
|
7132
|
+
<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>
|
|
7133
|
+
<div id="ct-suggest-result" style="margin-top:14px"></div>
|
|
7134
|
+
`)
|
|
7135
|
+
}
|
|
7136
|
+
|
|
7137
|
+
// The boundary constant is server-authoritative; the client mirrors the SAME frozen stance for pages that
|
|
7138
|
+
// render before any task payload arrives (list/suggest). It restates NO economic value (RFC-017 I-12).
|
|
7139
|
+
function UNCOMMITTED_VALUE_BOUNDARY_HINT() {
|
|
7140
|
+
return {
|
|
7141
|
+
value_state: 'uncommitted', boundary_ref: 'RFC-017 I-12',
|
|
7142
|
+
notice_zh: '仅为贡献事实与归属的信息性记录,不是金融工具,不授予任何经济或兑现权利,此处不作任何承诺或保证(RFC-017 I-12 / §7)。',
|
|
7143
|
+
notice_en: 'Informational record of contribution facts and attribution only. It is not a financial instrument and confers no economic or redemption right; nothing here is promised or guaranteed (RFC-017 I-12 / §7).',
|
|
7144
|
+
}
|
|
7145
|
+
}
|
|
7146
|
+
|
|
6474
7147
|
function renderRule(num, text) {
|
|
6475
7148
|
const [zh, en] = text.includes(' / ') ? text.split(' / ') : [text, '']
|
|
6476
7149
|
return `<div class="w-rule w-rule-item">
|
|
@@ -6534,16 +7207,19 @@ window.openParticipateSheet = (defaultTab) => {
|
|
|
6534
7207
|
<div style="padding:18px 16px 28px">
|
|
6535
7208
|
<div style="text-align:center;margin-bottom:18px">
|
|
6536
7209
|
<div style="font-size:22px;margin-bottom:4px">🚀</div>
|
|
6537
|
-
<div style="font-size:15px;font-weight:700;color:#1f2937">${t('
|
|
7210
|
+
<div style="font-size:15px;font-weight:700;color:#1f2937">${t('开始使用 WebAZ')}</div>
|
|
6538
7211
|
<div style="font-size:11px;color:#9ca3af;margin-top:2px">${t('选一个开始的方式')}</div>
|
|
6539
7212
|
</div>
|
|
6540
|
-
<button onclick="(
|
|
6541
|
-
<span>🔑 ${t('
|
|
7213
|
+
<button onclick="(closeSheet(),openAuthSheet('${defaultTab||'login'}'))" class="btn btn-primary" style="width:100%;padding:14px;font-size:15px;font-weight:600;border-radius:10px;margin-bottom:10px;display:flex;align-items:center;justify-content:space-between">
|
|
7214
|
+
<span>🔑 ${t('注册 / 登录')}</span><span style="font-size:13px;opacity:0.6">›</span>
|
|
6542
7215
|
</button>
|
|
6543
|
-
<button onclick="(
|
|
6544
|
-
<span
|
|
7216
|
+
<button onclick="(closeSheet(),navigate('#contribute/tasks'))" class="btn btn-outline" 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;border:1.5px solid #6366f1;color:#4338ca">
|
|
7217
|
+
<span>🛠 ${t('浏览公开任务板')}</span><span style="font-size:13px;opacity:0.6">›</span>
|
|
6545
7218
|
</button>
|
|
6546
|
-
<
|
|
7219
|
+
<button onclick="(closeSheet(),navigate('#contribute/tasks/suggest'))" class="btn btn-outline" style="width:100%;padding:14px;font-size:15px;font-weight:600;border-radius:10px;display:flex;align-items:center;justify-content:space-between;border:1.5px solid #6366f1;color:#4338ca">
|
|
7220
|
+
<span>💡 ${t('提建议(无需登录)')}</span><span style="font-size:13px;opacity:0.6">›</span>
|
|
7221
|
+
</button>
|
|
7222
|
+
<div style="font-size:10px;color:#9ca3af;text-align:center;margin-top:14px;line-height:1.5"><a href="#welcome" onclick="closeSheet()" style="color:#9ca3af;text-decoration:none">${t('了解协议设计 · 多种角色 · 元规则')}</a></div>
|
|
6547
7223
|
</div>
|
|
6548
7224
|
`, { maxWidth: 460 })
|
|
6549
7225
|
}
|
|
@@ -6617,19 +7293,30 @@ window.openAuthSheet = (defaultTab) => {
|
|
|
6617
7293
|
</div>
|
|
6618
7294
|
|
|
6619
7295
|
<div style="margin-top:14px;text-align:center">
|
|
6620
|
-
<a href="#recover" onclick="closeSheet()" style="font-size:13px;color:#6366f1;text-decoration:none">🔑 ${t('
|
|
7296
|
+
<a href="#recover" onclick="closeSheet()" style="font-size:13px;color:#6366f1;text-decoration:none">🔑 ${t('忘记 API Key / 密码?邮箱找回并重置 →')}</a>
|
|
6621
7297
|
</div>
|
|
6622
7298
|
</div>
|
|
6623
7299
|
|
|
6624
7300
|
<div id="panel-reg" style="${defaultTab==='reg'?'':'display:none'}">
|
|
6625
7301
|
<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>
|
|
7302
|
+
<div class="form-group">
|
|
7303
|
+
<label class="form-label">${t('找回邮箱')} <span style="color:#dc2626">*</span> <span style="font-size:11px;color:#9ca3af;font-weight:400">${t('(先验证邮箱,丢号才能找回)')}</span></label>
|
|
7304
|
+
<div style="display:flex;gap:6px;align-items:stretch">
|
|
7305
|
+
<input class="form-control" id="inp-reg-email" type="email" placeholder="your@example.com" style="flex:1" oninput="window._onRegEmailInput && window._onRegEmailInput()">
|
|
7306
|
+
<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>
|
|
7307
|
+
</div>
|
|
7308
|
+
<div id="reg-code-row" class="form-group" style="display:none;margin-top:8px;margin-bottom:0">
|
|
7309
|
+
<input class="form-control" id="inp-reg-code" inputmode="numeric" maxlength="6" placeholder="${t('输入 6 位邮箱验证码')}">
|
|
7310
|
+
<div style="font-size:11px;color:#16a34a;margin-top:4px">${t('验证码已发送,请查收(含垃圾箱),10 分钟内有效')}</div>
|
|
7311
|
+
</div>
|
|
7312
|
+
</div>
|
|
6626
7313
|
<div class="form-group">
|
|
6627
7314
|
<label class="form-label">${t('邀请码')} <span style="color:#dc2626">*</span></label>
|
|
6628
7315
|
<div style="display:flex;gap:6px;align-items:stretch">
|
|
6629
7316
|
<input class="form-control" id="inp-sponsor" placeholder="${t('陆续开放中,请期待')}" style="font-family:monospace;font-size:13px;flex:1">
|
|
6630
7317
|
<button id="btn-fetch-ref" type="button" disabled title="${t('该功能默认关闭,由管理员开启后可用')}" onclick="doFetchInviteCode()" style="white-space:nowrap;padding:0 12px;background:#e5e7eb;color:#9ca3af;border:none;border-radius:6px;font-size:12px;font-weight:600;cursor:not-allowed">${t('获取邀请码')}</button>
|
|
6631
7318
|
</div>
|
|
6632
|
-
<div style="font-size:11px;color:#6b7280;margin-top:4px" id="sponsor-hint-msg">${t('
|
|
7319
|
+
<div style="font-size:11px;color:#6b7280;margin-top:4px" id="sponsor-hint-msg">${t('邀请码为 6-7 位永久码;没有就联系老用户拿邀请链接')}</div>
|
|
6633
7320
|
</div>
|
|
6634
7321
|
<div class="form-group">
|
|
6635
7322
|
<label class="form-label">${t('名称 / 店铺名')}</label>
|
|
@@ -6778,16 +7465,17 @@ async function renderPromoter(app) {
|
|
|
6778
7465
|
// ─── ③ 我要分享 ───
|
|
6779
7466
|
const myId = state.user?.id || ''
|
|
6780
7467
|
const origin = location.origin
|
|
6781
|
-
//
|
|
6782
|
-
const code = data.permanent_code ||
|
|
6783
|
-
const refLinkShort = `${origin}/i/${code}`
|
|
6784
|
-
|
|
6785
|
-
const
|
|
7468
|
+
// 邀请短链只用 permanent_code,绝不兜底 usr_xxx;缺失时显示"暂不可用"。
|
|
7469
|
+
const code = data.permanent_code || null
|
|
7470
|
+
const refLinkShort = code ? `${origin}/i/${code}` : ''
|
|
7471
|
+
// pre-public 去左右码:不再生成 -L/-R 侧链,只用唯一推荐码 refLinkShort
|
|
7472
|
+
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>`
|
|
6786
7473
|
const esc = (s) => s.replace(/'/g, "\\'").replace(/"/g, '"')
|
|
6787
7474
|
|
|
6788
7475
|
const toolboxInner = `
|
|
6789
7476
|
<h2 style="font-size:16px;font-weight:600;margin-bottom:12px">🔗 ${t('我要分享')}</h2>
|
|
6790
7477
|
|
|
7478
|
+
${!code ? inviteUnavailableHtml : `
|
|
6791
7479
|
<div style="background:linear-gradient(135deg,#eef2ff 0%,#e0e7ff 100%);border:1px solid #c7d2fe;border-radius:10px;padding:12px;margin-bottom:12px">
|
|
6792
7480
|
<div style="display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px">
|
|
6793
7481
|
<div style="font-size:13px;font-weight:600;color:#3730a3">📎 ${t('我的邀请链接')}</div>
|
|
@@ -6795,19 +7483,19 @@ async function renderPromoter(app) {
|
|
|
6795
7483
|
</div>
|
|
6796
7484
|
<div style="font-family:ui-monospace,Consolas,monospace;font-size:11px;color:#3730a3;background:#fff;padding:7px 10px;border-radius:6px;margin-bottom:10px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;border:1px solid #e0e7ff">${escHtml(refLinkShort)}</div>
|
|
6797
7485
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
|
|
6798
|
-
<button onclick="copyRefLink('${esc(
|
|
7486
|
+
<button onclick="copyRefLink('${esc(refLinkShort)}')" style="display:flex;align-items:center;justify-content:center;gap:6px;padding:10px 8px;background:#4f46e5;color:#fff;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;box-shadow:0 1px 2px rgba(79,70,229,0.2)">
|
|
6799
7487
|
<span style="font-size:15px">📋</span><span>${t('复制链接')}</span>
|
|
6800
7488
|
</button>
|
|
6801
|
-
<button onclick="showQRModal('${esc(
|
|
7489
|
+
<button onclick="showQRModal('${esc(refLinkShort)}','${t('我的邀请链接')}')" style="display:flex;align-items:center;justify-content:center;gap:6px;padding:10px 8px;background:#fff;color:#4f46e5;border:2px solid #4f46e5;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer">
|
|
6802
7490
|
<span style="font-size:15px">📱</span><span>${t('二维码')}</span>
|
|
6803
7491
|
</button>
|
|
6804
7492
|
</div>
|
|
6805
|
-
</div
|
|
7493
|
+
</div>`}
|
|
6806
7494
|
|
|
6807
7495
|
<div style="font-size:11px;line-height:1.6;color:#6b7280;margin-bottom:8px">
|
|
6808
7496
|
${data.permissions?.can_l1_share
|
|
6809
|
-
? `<span style="color:#16a34a">✅ ${t('
|
|
6810
|
-
: `<span style="color:#d97706">⏳ ${t('
|
|
7497
|
+
? `<span style="color:#16a34a">✅ ${t('已开通分享分润资格')}</span>` + (data.permissions.l1_share_override === 1 ? ` · <span style="color:#7c3aed">${t('Admin 强制授予')}</span>` : '')
|
|
7498
|
+
: `<span style="color:#d97706">⏳ ${t('分享分润待开通')}</span> · <span>${t('完成首笔购买即可')}</span>`}
|
|
6811
7499
|
${data.my_sponsor ? `<br>${t('邀请人')}: <strong>${escHtml(t(data.my_sponsor.name))}</strong>` : ''}
|
|
6812
7500
|
${_pvAllowed && atomic.my_placement ? ` · ${t('挂靠')}: <strong>${escHtml(t(atomic.my_placement.name))}</strong> ${atomic.my_placement.side === 'left' ? '🔵' : '🟢'}` : ''}
|
|
6813
7501
|
· ${t('所在地区')}: ${regionLabel(data.region || 'global')}
|
|
@@ -6815,32 +7503,14 @@ async function renderPromoter(app) {
|
|
|
6815
7503
|
|
|
6816
7504
|
${!_pvAllowed ? '' : `
|
|
6817
7505
|
<details style="margin-top:4px">
|
|
6818
|
-
<summary style="font-size:11px;color:#6366f1;cursor:pointer;padding:6px 0">⚙ ${t('
|
|
7506
|
+
<summary style="font-size:11px;color:#6366f1;cursor:pointer;padding:6px 0">⚙ ${t('自动放置设置')}</summary>
|
|
6819
7507
|
<div style="padding:8px 0">
|
|
6820
|
-
<div style="font-size:11px;color:#
|
|
6821
|
-
<div style="
|
|
6822
|
-
<div style="background:#eff6ff;border:1px solid #bfdbfe;border-radius:8px;padding:10px">
|
|
6823
|
-
<div style="font-size:12px;font-weight:600;color:#1e40af;text-align:center;margin-bottom:8px">🔵 ${t('左区码')}</div>
|
|
6824
|
-
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px">
|
|
6825
|
-
<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>
|
|
6826
|
-
<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>
|
|
6827
|
-
</div>
|
|
6828
|
-
</div>
|
|
6829
|
-
<div style="background:#f0fdf4;border:1px solid #bbf7d0;border-radius:8px;padding:10px">
|
|
6830
|
-
<div style="font-size:12px;font-weight:600;color:#166534;text-align:center;margin-bottom:8px">🟢 ${t('右区码')}</div>
|
|
6831
|
-
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px">
|
|
6832
|
-
<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>
|
|
6833
|
-
<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>
|
|
6834
|
-
</div>
|
|
6835
|
-
</div>
|
|
6836
|
-
</div>
|
|
6837
|
-
|
|
6838
|
-
<div style="font-size:11px;color:#6b7280;margin-bottom:4px">${t('自动选边偏好')}</div>
|
|
7508
|
+
<div style="font-size:11px;color:#9ca3af;margin-bottom:8px">${t('新人通过你的推荐码注册后,系统自动安排积分树位置(无需选择左右)。')}</div>
|
|
7509
|
+
<div style="font-size:11px;color:#6b7280;margin-bottom:4px">${t('自动放置依据')}</div>
|
|
6839
7510
|
<select id="placement-pref-select" class="form-control" style="font-size:12px" onchange="doSetPlacementPref()">
|
|
6840
7511
|
<option value="team_count">${t('推荐少的一边(默认)')}</option>
|
|
6841
7512
|
<option value="pv_count">${t('近 90 天积分少的一边')}</option>
|
|
6842
7513
|
</select>
|
|
6843
|
-
<p style="font-size:11px;color:#9ca3af;margin-top:6px;line-height:1.5">${t('💡 想为这一次分享强制走左/右轨?复制上方「🔵 左区推荐码 / 🟢 右区推荐码」直接发,仅当次有效,不会改长期偏好。')}</p>
|
|
6844
7514
|
<div id="placement-pref-msg" style="font-size:11px;color:#6b7280;margin-top:4px"></div>
|
|
6845
7515
|
</div>
|
|
6846
7516
|
</details>`}
|
|
@@ -7337,24 +8007,12 @@ function renderAtomicInner(a, leftPv, rightPv, weak, nextTier, nextProgress) {
|
|
|
7337
8007
|
|
|
7338
8008
|
${renderBinaryTree(a.binary_tree)}
|
|
7339
8009
|
|
|
7340
|
-
|
|
7341
|
-
|
|
7342
|
-
|
|
7343
|
-
|
|
7344
|
-
|
|
7345
|
-
|
|
7346
|
-
|
|
7347
|
-
<details style="margin-bottom:10px">
|
|
7348
|
-
<summary style="font-size:12px;color:#6366f1;cursor:pointer">${t('匹配档位表')}</summary>
|
|
7349
|
-
<table style="width:100%;margin-top:6px;border-collapse:collapse">
|
|
7350
|
-
<tr style="background:#f9fafb;font-size:11px;color:#6b7280">
|
|
7351
|
-
<th style="padding:5px 8px;text-align:left">Tier</th>
|
|
7352
|
-
<th style="padding:5px 8px;text-align:right">${t('门槛 PV')}</th>
|
|
7353
|
-
<th style="padding:5px 8px;text-align:right">${t('Score / 次')}</th>
|
|
7354
|
-
</tr>
|
|
7355
|
-
${tierTable}
|
|
7356
|
-
</table>
|
|
7357
|
-
</details>
|
|
8010
|
+
<!-- pre-public de-MLM: 弱腿/对碰 tier 进度 + Score/次 档位表已下线;PV/位置仅为参与记录,非收益路径 -->
|
|
8011
|
+
<div style="background:#f9fafb;border-radius:6px;padding:8px 10px;font-size:11px;color:#9ca3af;line-height:1.6;margin-bottom:10px">
|
|
8012
|
+
${window._lang === 'en'
|
|
8013
|
+
? 'PV tiers / pairing are pre-launch and currently disabled. PV / placement is a participation record only — not an earning path or payout promise.'
|
|
8014
|
+
: 'PV 档位 / 对碰为 pre-launch 阶段、当前未启用。PV / 位置仅为参与记录,非收益路径或兑付承诺。'}
|
|
8015
|
+
</div>
|
|
7358
8016
|
<h4 style="font-size:12px;font-weight:600;margin:6px 0">📊 ${t('最近匹配')}</h4>
|
|
7359
8017
|
${recentRows}`
|
|
7360
8018
|
}
|
|
@@ -7439,7 +8097,13 @@ function renderGrowthTasksSection(tasks, summary) {
|
|
|
7439
8097
|
<details ${claimed.length ? '' : 'open'} style="margin-top:${claimed.length?'10px':'0'}">
|
|
7440
8098
|
<summary style="font-size:11px;color:#374151;font-weight:600;cursor:pointer;padding:4px 0;list-style:none">▸ ${t('推荐领取')} (${available.length}) ▾</summary>
|
|
7441
8099
|
<div style="padding-top:4px">
|
|
7442
|
-
${available.map(taskRow).join('')}
|
|
8100
|
+
${available.slice(0, 3).map(taskRow).join('')}
|
|
8101
|
+
${available.length > 3 ? `
|
|
8102
|
+
<details style="margin-top:2px">
|
|
8103
|
+
<summary style="font-size:11px;color:#6b7280;cursor:pointer;padding:4px 0;list-style:none">▸ ${t('查看更多推荐')} (${available.length - 3}) ▾</summary>
|
|
8104
|
+
<div style="padding-top:4px">${available.slice(3).map(taskRow).join('')}</div>
|
|
8105
|
+
</details>
|
|
8106
|
+
` : ''}
|
|
7443
8107
|
</div>
|
|
7444
8108
|
</details>
|
|
7445
8109
|
` : ''}
|
|
@@ -7516,36 +8180,29 @@ function renderAtomicSection(a) {
|
|
|
7516
8180
|
<h2 style="font-size:15px;font-weight:600;margin:24px 0 8px">⚛ ${t('积分 — 积分匹配')}</h2>
|
|
7517
8181
|
|
|
7518
8182
|
<div class="card" style="margin-bottom:12px;background:linear-gradient(135deg,#dbeafe,#f0fdf4)">
|
|
7519
|
-
<div style="font-size:13px;color:#6b7280;margin-bottom:6px">🔗 ${t('
|
|
7520
|
-
<div style="font-size:11px;color:#6b7280;margin-bottom:10px">${t('
|
|
8183
|
+
<div style="font-size:13px;color:#6b7280;margin-bottom:6px">🔗 ${t('我的推荐码')}</div>
|
|
8184
|
+
<div style="font-size:11px;color:#6b7280;margin-bottom:10px">${t('复制你的推荐链接分享给新人;新人注册后由系统自动安排积分树位置(无需选择左右)。')}</div>
|
|
7521
8185
|
${(() => {
|
|
7522
|
-
|
|
8186
|
+
// pre-public 去左右码:只用唯一的推荐码(/i/CODE,不带 -L/-R);缺失则提示不可用
|
|
8187
|
+
const myCode = state.user?.permanent_code || null
|
|
7523
8188
|
const origin = location.origin
|
|
7524
|
-
|
|
7525
|
-
const
|
|
7526
|
-
const esc = (s) => s.replace(/'/g, "\\'")
|
|
8189
|
+
if (!myCode) return `<div style="background:#fef2f2;border:1px solid #fca5a5;border-radius:10px;padding:10px;font-size:12px;color:#991b1b">⚠️ ${t('邀请码暂不可用,请刷新或联系支持')}</div>`
|
|
8190
|
+
const link = `${origin}/i/${myCode}`
|
|
8191
|
+
const esc = (s) => s.replace(/'/g, "\\'")
|
|
7527
8192
|
return `
|
|
7528
|
-
<div style="
|
|
7529
|
-
<div
|
|
7530
|
-
|
|
7531
|
-
|
|
7532
|
-
<div style="font-size:11px;color:#3b82f6;margin-top:6px;text-align:center">${t('点击复制 →')}</div>
|
|
7533
|
-
</div>
|
|
7534
|
-
<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'">
|
|
7535
|
-
<div style="font-size:13px;font-weight:700;color:#166534;margin-bottom:6px">🟢 ${t('右区推荐码')}</div>
|
|
7536
|
-
<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>
|
|
7537
|
-
<div style="font-size:11px;color:#16a34a;margin-top:6px;text-align:center">${t('点击复制 →')}</div>
|
|
7538
|
-
</div>
|
|
8193
|
+
<div onclick="copyRefLink('${esc(link)}')" style="cursor:pointer;background:#eef2ff;border:2px solid #6366f1;border-radius:10px;padding:10px;transition:all 0.15s" onmouseover="this.style.background='#e0e7ff'" onmouseout="this.style.background='#eef2ff'">
|
|
8194
|
+
<div style="font-size:13px;font-weight:700;color:#4338ca;margin-bottom:6px">${t('推荐链接')}</div>
|
|
8195
|
+
<div style="font-size:10px;color:#4338ca;word-break:break-all;line-height:1.4;background:#fff;padding:6px 8px;border-radius:6px;font-family:monospace">${escHtml(link)}</div>
|
|
8196
|
+
<div style="font-size:11px;color:#6366f1;margin-top:6px;text-align:center">${t('点击复制 →')}</div>
|
|
7539
8197
|
</div>`
|
|
7540
8198
|
})()}
|
|
7541
8199
|
<details style="margin-top:10px">
|
|
7542
|
-
<summary style="font-size:11px;color:#6366f1;cursor:pointer">⚙ ${t('
|
|
8200
|
+
<summary style="font-size:11px;color:#6366f1;cursor:pointer">⚙ ${t('自动放置依据')}</summary>
|
|
7543
8201
|
<div style="padding:8px 0">
|
|
7544
8202
|
<select id="placement-pref-select" class="form-control" style="font-size:12px" onchange="doSetPlacementPref()">
|
|
7545
8203
|
<option value="team_count">${t('推荐少的一边(默认)')}</option>
|
|
7546
8204
|
<option value="pv_count">${t('近 90 天积分少的一边')}</option>
|
|
7547
8205
|
</select>
|
|
7548
|
-
<p style="font-size:11px;color:#9ca3af;margin-top:6px;line-height:1.5">${t('💡 想为这一次分享强制走左/右轨?复制上方「🔵 左区推荐码 / 🟢 右区推荐码」直接发,仅当次有效,不会改长期偏好。')}</p>
|
|
7549
8206
|
<div id="placement-pref-msg" style="font-size:11px;color:#6b7280;margin-top:4px"></div>
|
|
7550
8207
|
</div>
|
|
7551
8208
|
</details>
|
|
@@ -7567,13 +8224,13 @@ function renderAtomicSection(a) {
|
|
|
7567
8224
|
|
|
7568
8225
|
${renderBinaryTree(a.binary_tree)}
|
|
7569
8226
|
|
|
8227
|
+
<!-- pre-public de-MLM: 弱腿/对碰 tier 进度已下线;PV/位置仅为参与记录,非收益路径 -->
|
|
7570
8228
|
<div class="card" style="margin-bottom:12px">
|
|
7571
|
-
<div style="font-size:
|
|
7572
|
-
|
|
7573
|
-
|
|
7574
|
-
|
|
7575
|
-
|
|
7576
|
-
</div>` : ''}
|
|
8229
|
+
<div style="font-size:11px;color:#9ca3af;line-height:1.6">
|
|
8230
|
+
${window._lang === 'en'
|
|
8231
|
+
? 'PV tiers / pairing are pre-launch and currently disabled. PV / placement is a participation record only — not an earning path or payout promise.'
|
|
8232
|
+
: 'PV 档位 / 对碰为 pre-launch 阶段、当前未启用。PV / 位置仅为参与记录,非收益路径或兑付承诺。'}
|
|
8233
|
+
</div>
|
|
7577
8234
|
</div>
|
|
7578
8235
|
|
|
7579
8236
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:12px">
|
|
@@ -7587,18 +8244,6 @@ function renderAtomicSection(a) {
|
|
|
7587
8244
|
</div>
|
|
7588
8245
|
</div>
|
|
7589
8246
|
|
|
7590
|
-
<details style="margin-bottom:12px">
|
|
7591
|
-
<summary style="font-size:12px;color:#6366f1;cursor:pointer">${t('匹配档位表')}</summary>
|
|
7592
|
-
<table style="width:100%;margin-top:8px;border-collapse:collapse">
|
|
7593
|
-
<tr style="background:#f9fafb;font-size:11px;color:#6b7280">
|
|
7594
|
-
<th style="padding:6px 8px;text-align:left">Tier</th>
|
|
7595
|
-
<th style="padding:6px 8px;text-align:right">${t('门槛 PV')}</th>
|
|
7596
|
-
<th style="padding:6px 8px;text-align:right">${t('Score / 次')}</th>
|
|
7597
|
-
</tr>
|
|
7598
|
-
${tierTable}
|
|
7599
|
-
</table>
|
|
7600
|
-
</details>
|
|
7601
|
-
|
|
7602
8247
|
<h3 style="font-size:13px;font-weight:600;margin:8px 0">📊 ${t('最近匹配')}</h3>
|
|
7603
8248
|
<div class="card" style="padding:0">
|
|
7604
8249
|
${recentRows}
|
|
@@ -7661,10 +8306,7 @@ window.showNodePvModal = async (userId) => {
|
|
|
7661
8306
|
body.innerHTML = alert$('error', data.error)
|
|
7662
8307
|
return
|
|
7663
8308
|
}
|
|
7664
|
-
|
|
7665
|
-
const ratio = (data.total_left_pv + data.total_right_pv) > 0
|
|
7666
|
-
? (weak / Math.max(data.total_left_pv, data.total_right_pv) * 100).toFixed(0)
|
|
7667
|
-
: '0'
|
|
8309
|
+
// pre-public de-MLM: 不再展示弱腿/对碰收益(weak-leg / pairing earnings)。PV / 位置仅为参与记录。
|
|
7668
8310
|
body.innerHTML = `
|
|
7669
8311
|
<div style="text-align:left">
|
|
7670
8312
|
<div style="display:flex;align-items:center;gap:10px;margin-bottom:14px;padding-bottom:10px;border-bottom:1px solid #f3f4f6">
|
|
@@ -7692,10 +8334,10 @@ window.showNodePvModal = async (userId) => {
|
|
|
7692
8334
|
</div>
|
|
7693
8335
|
</div>
|
|
7694
8336
|
|
|
7695
|
-
<div style="background:#f9fafb;border-radius:6px;padding:8px 10px;font-size:11px;color:#
|
|
7696
|
-
${
|
|
7697
|
-
|
|
7698
|
-
|
|
8337
|
+
<div style="background:#f9fafb;border-radius:6px;padding:8px 10px;font-size:11px;color:#9ca3af;line-height:1.6;margin-bottom:10px">
|
|
8338
|
+
${window._lang === 'en'
|
|
8339
|
+
? 'PV pairing is pre-launch and currently disabled. PV / placement is a participation record only — not an earning path or payout promise.'
|
|
8340
|
+
: 'PV 对碰为 pre-launch 阶段、当前未启用。PV / 位置仅为参与记录,非收益路径或兑付承诺。'}
|
|
7699
8341
|
</div>
|
|
7700
8342
|
|
|
7701
8343
|
<a href="#u/${data.id}" onclick="closeModal()" style="display:block;text-align:center;font-size:12px;color:#4f46e5;text-decoration:none;padding:8px">→ ${t('查看 TA 的主页')}</a>
|
|
@@ -7751,7 +8393,7 @@ async function webShareOrCopy(opts) {
|
|
|
7751
8393
|
|
|
7752
8394
|
window.copyRefLink = async (link) => {
|
|
7753
8395
|
const meName = state.user?.name || t('一位老用户')
|
|
7754
|
-
const text = t('我在 WebAZ 用 AI
|
|
8396
|
+
const text = t('我在 WebAZ 用 AI 比价下单,体验不错,推荐你也试试。分享链接仅作参与 / 归因记录,不构成收益承诺。') + '\n— ' + meName
|
|
7755
8397
|
const r = await webShareOrCopy({ title: 'WebAZ', text, url: link })
|
|
7756
8398
|
if (r === 'shared') toast$(t('已分享'))
|
|
7757
8399
|
else if (r === 'copied') toast$(t('已复制(含邀请文案)'))
|
|
@@ -7829,9 +8471,9 @@ async function maybePromptPlacementBind() {
|
|
|
7829
8471
|
const st = await GET('/profile/placement-status')
|
|
7830
8472
|
if (!st.can_bind) return // 已有 placement 或 有下线 → 不弹
|
|
7831
8473
|
setTimeout(() => {
|
|
7832
|
-
const ok = confirm(`📍 ${t('系统检测到邀请链接')}\n\n${t('inviter')}: ${inviter}\n\n${t('是否加入对方的积分配对?')}\n${t('
|
|
8474
|
+
const ok = confirm(`📍 ${t('系统检测到邀请链接')}\n\n${t('inviter')}: ${inviter}\n\n${t('是否加入对方的积分配对?')}\n${t('系统将自动安排积分树位置。一旦加入永久不变。')}`)
|
|
7833
8475
|
if (ok) {
|
|
7834
|
-
POST('/profile/bind-placement', { inviter_id: inviter
|
|
8476
|
+
POST('/profile/bind-placement', { inviter_id: inviter }).then(res => {
|
|
7835
8477
|
if (res.error) alert(`✗ ${res.error}`)
|
|
7836
8478
|
else alert(`✓ ${t('已加入积分树')}\n${t('侧')}: ${res.side}\n${t('深度')}: ${res.depth}`)
|
|
7837
8479
|
})
|
|
@@ -8704,6 +9346,10 @@ async function renderShopPage(app, identifier) {
|
|
|
8704
9346
|
const followBtn = state.user && !isOwnShop && state.user.role === 'buyer'
|
|
8705
9347
|
? `<button class="btn btn-${is_following ? 'gray' : 'primary'} btn-sm" style="width:auto" id="shop-follow-btn" data-following="${is_following ? '1' : '0'}" onclick="toggleShopFollow('${seller.id}', this)">${is_following ? '✓ ' + t('已关注') : '+ ' + t('关注')}</button>`
|
|
8706
9348
|
: ''
|
|
9349
|
+
// 推荐店铺:只锚定推荐关系/二叉树位置/店铺来源 —— 不是全店佣金权;商品分润仍要求推荐人真实成交过同款
|
|
9350
|
+
const shopReferralBtn = state.user?.permanent_code
|
|
9351
|
+
? `<button class="btn btn-outline btn-sm" style="width:auto;font-size:11px" title="${t('店铺推荐只锚定推荐关系;只有你真实成交过的同款商品,后续成交才可能形成商品推荐关系')}" onclick="copyShopReferralLink('${seller.id}')">🔗 ${t('推荐店铺')}</button>`
|
|
9352
|
+
: ''
|
|
8707
9353
|
const productCards = products.length === 0
|
|
8708
9354
|
? `<div style="text-align:center;padding:30px;color:#9ca3af;font-size:13px">${t('该卖家暂无商品')}</div>`
|
|
8709
9355
|
: products.map(p => {
|
|
@@ -8749,6 +9395,7 @@ async function renderShopPage(app, identifier) {
|
|
|
8749
9395
|
</div>
|
|
8750
9396
|
<div style="display:flex;flex-direction:column;gap:6px;align-items:flex-end">
|
|
8751
9397
|
${followBtn}
|
|
9398
|
+
${shopReferralBtn}
|
|
8752
9399
|
${isOwnShop ? `<a href="#shop-edit" style="font-size:11px;color:#6366f1">${t('编辑店铺')} →</a>` : ''}
|
|
8753
9400
|
</div>
|
|
8754
9401
|
</div>
|
|
@@ -8765,6 +9412,17 @@ async function renderShopPage(app, identifier) {
|
|
|
8765
9412
|
`, 'discover')
|
|
8766
9413
|
}
|
|
8767
9414
|
|
|
9415
|
+
// 复制店铺推荐链接 — /?ref=CODE#shop/<seller>(target URL 形态:ref 在 query,目标页在 hash,服务端可见 ref)。
|
|
9416
|
+
// 只用 permanent_code,绝不用 usr_xxx;诚实文案:不暗示"分享店铺即可获得全店佣金"。
|
|
9417
|
+
window.copyShopReferralLink = (sellerId) => {
|
|
9418
|
+
const code = state.user?.permanent_code
|
|
9419
|
+
if (!code) return alert(t('邀请码暂不可用,请刷新或联系支持'))
|
|
9420
|
+
const link = `${location.origin}/?ref=${code}#shop/${sellerId}`
|
|
9421
|
+
copyText(link).then(ok => toast$(ok
|
|
9422
|
+
? t('店铺推荐链接已复制 — 商品分润仍需你真实成交过同款并 opt-in')
|
|
9423
|
+
: t('复制失败,请手动复制'), ok ? 'success' : 'error'))
|
|
9424
|
+
}
|
|
9425
|
+
|
|
8768
9426
|
window.toggleShopFollow = async (sellerId, btn) => {
|
|
8769
9427
|
const following = btn.dataset.following === '1'
|
|
8770
9428
|
btn.disabled = true
|
|
@@ -9688,7 +10346,7 @@ window.openBuildFeedback = () => {
|
|
|
9688
10346
|
if (!state.user) { toast$(t('请先登录')); navigate('#login'); return }
|
|
9689
10347
|
const page = (location.hash || '#/').split('?')[0] // 非 PII:只取当前页面路由作上下文
|
|
9690
10348
|
const html = `
|
|
9691
|
-
<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()">
|
|
10349
|
+
<div class="js-modal" data-page="${escHtml(page)}" 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()">
|
|
9692
10350
|
<div style="background:#fff;width:100%;max-width:560px;border-radius:16px 16px 0 0;padding:20px;max-height:82vh;overflow-y:auto" onclick="event.stopPropagation()">
|
|
9693
10351
|
<h2 style="font-size:16px;font-weight:700;margin-bottom:4px">💬 ${t('反馈 / 建议')}</h2>
|
|
9694
10352
|
<div style="font-size:11px;color:#9ca3af;margin-bottom:12px">${t('帮我们改进 WebAZ。会自动附上当前页面')} <code style="background:#f3f4f6;padding:1px 5px;border-radius:4px">${escHtml(page)}</code> ${t('作为上下文(不含个人信息)')}</div>
|
|
@@ -9707,7 +10365,7 @@ window.openBuildFeedback = () => {
|
|
|
9707
10365
|
<div id="bfb-msg" style="margin:8px 0"></div>
|
|
9708
10366
|
<div style="display:flex;gap:8px">
|
|
9709
10367
|
<button class="btn btn-gray" style="flex:1" onclick="this.closest('.js-modal').remove()">${t('取消')}</button>
|
|
9710
|
-
<button class="btn btn-primary" style="flex:1" onclick="
|
|
10368
|
+
<button class="btn btn-primary" style="flex:1" onclick="submitBuildFeedbackFromModal(this)">${t('提交')}</button>
|
|
9711
10369
|
</div>
|
|
9712
10370
|
<div style="text-align:center;margin-top:12px">
|
|
9713
10371
|
<a onclick="this.closest('.js-modal').remove();navigate('#build-feedback')" style="font-size:12px;color:#4f46e5;cursor:pointer">${t('查看我的反馈进度 →')}</a>
|
|
@@ -9717,7 +10375,10 @@ window.openBuildFeedback = () => {
|
|
|
9717
10375
|
const div = document.createElement('div'); div.innerHTML = html; document.body.appendChild(div.firstElementChild)
|
|
9718
10376
|
}
|
|
9719
10377
|
|
|
9720
|
-
window.
|
|
10378
|
+
window.submitBuildFeedbackFromModal = async (btn) => {
|
|
10379
|
+
// Codex #93:page 从 modal dataset 读,绝不把 location.hash 拼进内联 onclick 的 JS 字符串
|
|
10380
|
+
// (escHtml 是 HTML escape,放进 JS 字符串上下文会被 HTML attribute 解码 → hash 注入/XSS)。
|
|
10381
|
+
const page = btn.closest('.js-modal')?.dataset.page || '#/'
|
|
9721
10382
|
const type = document.getElementById('bfb-type').value
|
|
9722
10383
|
const text = document.getElementById('bfb-text').value.trim()
|
|
9723
10384
|
const msg = document.getElementById('bfb-msg')
|
|
@@ -9750,8 +10411,8 @@ async function renderMyBuildFeedback(app) {
|
|
|
9750
10411
|
${Number(f.credited_points) > 0 ? `<div style="font-size:11px;color:#16a34a;margin-top:4px">🏅 +${f.credited_points} ${t('共建信誉')}</div>` : ''}
|
|
9751
10412
|
${(f.credit_pending_anchor && !(Number(f.credited_points) > 0)) ? `
|
|
9752
10413
|
<div style="font-size:11px;color:#854d0e;background:#fef3c7;border-radius:6px;padding:8px;margin-top:6px">
|
|
9753
|
-
🔐 ${t('这条贡献已被采纳 —— 绑定 Passkey
|
|
9754
|
-
<button class="btn btn-sm" style="margin-top:6px;font-size:11px;padding:4px 10px;background:#7c3aed;color:#fff;border-color:transparent" onclick="(async()=>{ if(await doRegisterPasskey('')){ alert(t('
|
|
10414
|
+
🔐 ${t('这条贡献已被采纳 —— 绑定 Passkey 即可记入建设信誉(建设信誉需锚定可问责真人)')}
|
|
10415
|
+
<button class="btn btn-sm" style="margin-top:6px;font-size:11px;padding:4px 10px;background:#7c3aed;color:#fff;border-color:transparent" onclick="(async()=>{ if(await doRegisterPasskey('')){ alert(t('已绑定,建设信誉已记入')); navigate('#build-feedback') } })()">${t('绑定 Passkey 记入建设信誉')}</button>
|
|
9755
10416
|
</div>` : ''}
|
|
9756
10417
|
</div>`).join('')
|
|
9757
10418
|
app.innerHTML = shell(`
|
|
@@ -9768,6 +10429,10 @@ async function renderMyContributions(app) {
|
|
|
9768
10429
|
app.innerHTML = shell(loading$(), 'me')
|
|
9769
10430
|
const p = await GET('/build-reputation/me')
|
|
9770
10431
|
if (!p || p.error) { app.innerHTML = shell(`<div class="card" style="padding:16px;color:#b91c1c">${escHtml(p?.error || t('加载失败'))}</div>`, 'me'); return }
|
|
10432
|
+
// F9 — GitHub 身份归属面(只读自己的 bindings + attributable facts;失败时优雅降级,不挡整页)
|
|
10433
|
+
const gid = await GET('/contribution-identity/github/me').catch(() => null)
|
|
10434
|
+
// F10 — 自动发现可认领的 GitHub 贡献(只读;失败优雅降级)
|
|
10435
|
+
const claimable = await GET('/contribution-identity/github/claimable').catch(() => null)
|
|
9771
10436
|
const lang = window._lang === 'zh' ? 'zh' : 'en'
|
|
9772
10437
|
const tier = p.tier || {}
|
|
9773
10438
|
const k = p.kpi || {}
|
|
@@ -9803,13 +10468,176 @@ async function renderMyContributions(app) {
|
|
|
9803
10468
|
${stat(t('提任务'), k.tasks_created)}
|
|
9804
10469
|
</div>
|
|
9805
10470
|
${provRows ? `<div class="card" style="padding:10px;margin-bottom:12px"><div style="font-size:11px;color:#6b7280;margin-bottom:4px">${t('署名构成(自报)')}</div>${provRows}</div>` : ''}
|
|
9806
|
-
${!p.
|
|
10471
|
+
${!p.passkey_anchor_present ? `<div class="card" style="padding:10px;margin-bottom:12px;border-left:3px solid #d97706"><div style="font-size:12px;color:#92400e">${t('未绑 Passkey:贡献可受理致谢,但需绑定真人锚点才记入建设信誉。')}</div></div>` : ''}
|
|
10472
|
+
${ghClaimSectionHtml(gid, claimable, lang)}
|
|
10473
|
+
<div class="card" onclick="location.hash='#contribute/tasks'" style="padding:12px 14px;margin-bottom:12px;cursor:pointer;display:flex;align-items:center;gap:10px">
|
|
10474
|
+
<div style="font-size:20px;flex-shrink:0">📋</div>
|
|
10475
|
+
<div style="flex:1;min-width:0">
|
|
10476
|
+
<div style="font-weight:600;font-size:13px">${t('查看公开共建任务')}</div>
|
|
10477
|
+
<div style="font-size:11px;color:#9ca3af;margin-top:2px">${t('可浏览 / 可认领的公共任务板(独立于我的贡献记录)')}</div>
|
|
10478
|
+
</div>
|
|
10479
|
+
<div style="color:#9ca3af">›</div>
|
|
10480
|
+
</div>
|
|
9807
10481
|
<div style="font-size:13px;font-weight:600;margin:14px 0 6px">${t('限制与申诉')}</div>
|
|
9808
10482
|
${restrHtml}
|
|
9809
10483
|
<div style="font-size:11px;color:#9ca3af;margin-top:14px">${t('看板仅自己可见,不做公开排行。')}</div>
|
|
9810
10484
|
`, 'me')
|
|
9811
10485
|
}
|
|
9812
10486
|
|
|
10487
|
+
// ─── F9 — GitHub 贡献认领 UI(身份/归属认领;不是奖励、付款或提现)──────────────────────
|
|
10488
|
+
// 服务端三步契约:claim-challenge(签发挑战+proof_marker)→ 用户把 marker 发到自己 GitHub 账号的
|
|
10489
|
+
// public gist → requestPasskeyGate('identity_claim', {github_actor_id, source_event_key, challenge_id})
|
|
10490
|
+
// 拿一次性 webauthn_token → claim-complete。accountId 恒为 session 用户(前端绝不传 account_id);
|
|
10491
|
+
// GitHub read token 只在服务端(前端不收不传)。F10 discovery 已接入(GET .../github/claimable →
|
|
10492
|
+
// 「可认领的 GitHub 贡献」列表,点「认领此贡献」预填并发起);手动输入仅作找不到时的 fallback。
|
|
10493
|
+
let _ghClaimCtx = null // { challenge_id, expires_at, proof_marker, actor, sek }
|
|
10494
|
+
|
|
10495
|
+
// typed error code → 可读提示(认领失败绝不伪装成功)
|
|
10496
|
+
function ghClaimErrText(code, fallback) {
|
|
10497
|
+
const m = {
|
|
10498
|
+
GITHUB_READ_NOT_CONFIGURED: t('身份认领暂不可用(服务端未配置 GitHub 读取凭证),请稍后再试或联系 maintainer'),
|
|
10499
|
+
FACT_NOT_CLAIMABLE: t('没有可认领的、经凭证背书的 GitHub 贡献记录 — 请检查 source_event_key 是否正确'),
|
|
10500
|
+
ACTOR_MISMATCH: t('该贡献记录的执行者与所填 GitHub 身份不符'),
|
|
10501
|
+
ALREADY_BOUND: t('该 GitHub 身份已被其他账号认领'),
|
|
10502
|
+
CHALLENGE_EXPIRED: t('认领挑战已过期,请重新生成'),
|
|
10503
|
+
CHALLENGE_ALREADY_USED: t('认领挑战已被使用,请重新生成'),
|
|
10504
|
+
CHALLENGE_NOT_FOUND: t('认领挑战不存在或不属于当前账号'),
|
|
10505
|
+
PROOF_REJECTED: t('gist 证明未通过 — 请确认 gist 公开、归属于该 GitHub 账号、且内容为完整 proof_marker'),
|
|
10506
|
+
HUMAN_PRESENCE_REQUIRED: t('此操作需要真人 Passkey 验证'),
|
|
10507
|
+
AGENT_SCOPE_UNDECLARED: t('写操作需问责锚点 — 请先在「安全」页绑定 Passkey'),
|
|
10508
|
+
INVALID_REQUEST: t('请求参数无效 — 请检查两个输入框'),
|
|
10509
|
+
}
|
|
10510
|
+
return m[code] || fallback || t('操作失败')
|
|
10511
|
+
}
|
|
10512
|
+
|
|
10513
|
+
function ghClaimSectionHtml(gid, claimable, lang) {
|
|
10514
|
+
const ok = gid && !gid.error
|
|
10515
|
+
const bindings = ok ? (gid.bindings || []) : []
|
|
10516
|
+
const facts = ok ? (gid.attributable_facts || []) : []
|
|
10517
|
+
const notice = ok && gid.value_boundary ? (lang === 'zh' ? gid.value_boundary.notice_zh : gid.value_boundary.notice_en) : ''
|
|
10518
|
+
const bindRows = bindings.length === 0
|
|
10519
|
+
? `<div style="font-size:12px;color:#9ca3af">${t('尚未绑定 GitHub 身份')}</div>`
|
|
10520
|
+
: 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('')
|
|
10521
|
+
const factRows = facts.length === 0
|
|
10522
|
+
? `<div style="font-size:12px;color:#9ca3af">${t('暂无可归属的贡献事实')}</div>`
|
|
10523
|
+
: facts.map(f => `<div style="font-size:11px;color:#374151;padding:6px 8px;background:#f9fafb;border-radius:6px;margin-bottom:4px">
|
|
10524
|
+
<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>
|
|
10525
|
+
<div style="color:#6b7280;word-break:break-all">${escHtml(String(f.source_event_key))}</div>
|
|
10526
|
+
</div>`).join('')
|
|
10527
|
+
// F10 — 自动发现的可认领贡献(actor 未被任何账号绑定的 credential-backed facts)
|
|
10528
|
+
const cl = (claimable && !claimable.error) ? (claimable.claimable_facts || []) : null
|
|
10529
|
+
const claimRows = cl === null
|
|
10530
|
+
? `<div style="font-size:12px;color:#9ca3af">${t('可认领列表暂不可用,稍后再试')}</div>`
|
|
10531
|
+
: cl.length === 0
|
|
10532
|
+
? `<div style="font-size:12px;color:#9ca3af">${t('暂无自动发现的可认领贡献')}</div>`
|
|
10533
|
+
: cl.map(r => `<div style="font-size:11px;color:#374151;padding:6px 8px;background:#eff6ff;border-radius:6px;margin-bottom:4px">
|
|
10534
|
+
<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>
|
|
10535
|
+
<div style="color:#6b7280;word-break:break-all">${escHtml(String(r.source_event_key))}</div>
|
|
10536
|
+
<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>
|
|
10537
|
+
<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>
|
|
10538
|
+
</div>`).join('')
|
|
10539
|
+
const ctx = _ghClaimCtx
|
|
10540
|
+
return `
|
|
10541
|
+
<div class="card" style="padding:14px;margin-bottom:12px" id="gh-claim-card">
|
|
10542
|
+
<div style="font-size:13px;font-weight:600;margin-bottom:4px">🔗 ${t('GitHub 贡献认领(身份归属)')}</div>
|
|
10543
|
+
<div style="font-size:11px;color:#6b7280;margin-bottom:4px">${t('把"以 GitHub 身份完成、已被凭证背书"的贡献事实归属到本账号。这只是身份与归属的认领 — 不是奖励、不是付款。')}</div>
|
|
10544
|
+
<div style="font-size:11px;color:#166534;background:#f0fdf4;border-radius:6px;padding:6px 8px;margin-bottom:8px">✓ ${t('GitHub 贡献认领不需要先购买,也不需要开通分享分润。')}</div>
|
|
10545
|
+
${notice ? `<div style="font-size:10px;color:#9ca3af;background:#f4f4f5;border-radius:6px;padding:6px 8px;margin-bottom:8px">🔒 ${escHtml(notice)}</div>` : ''}
|
|
10546
|
+
${ok ? `
|
|
10547
|
+
<div style="font-size:12px;font-weight:600;margin:8px 0 4px">${t('已绑定身份')}</div>${bindRows}
|
|
10548
|
+
<div style="font-size:12px;font-weight:600;margin:10px 0 4px">${t('可归属的贡献事实')}</div>${factRows}
|
|
10549
|
+
<div style="font-size:12px;font-weight:600;margin:10px 0 4px">${t('可认领的 GitHub 贡献(自动发现)')}</div>${claimRows}
|
|
10550
|
+
<details style="margin-top:10px" ${ctx ? 'open' : ''} id="gh-claim-details">
|
|
10551
|
+
<summary style="font-size:12px;font-weight:600;cursor:pointer">${t('手动认领一条贡献(找不到时的备用入口)')}</summary>
|
|
10552
|
+
<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>
|
|
10553
|
+
<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">
|
|
10554
|
+
<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">
|
|
10555
|
+
<button class="btn btn-outline btn-sm" style="width:auto" onclick="ghClaimIssue()">1️⃣ ${t('生成认领挑战')}</button>
|
|
10556
|
+
<div id="gh-claim-step2" style="margin-top:8px">${ctx ? ghClaimStep2Html(ctx) : ''}</div>
|
|
10557
|
+
<div id="gh-claim-msg" style="font-size:12px;margin-top:6px"></div>
|
|
10558
|
+
</details>` : `<div style="font-size:12px;color:#9ca3af">${t('身份归属面暂不可用,稍后再试')}</div>`}
|
|
10559
|
+
</div>`
|
|
10560
|
+
}
|
|
10561
|
+
|
|
10562
|
+
function ghClaimStep2Html(ctx) {
|
|
10563
|
+
return `
|
|
10564
|
+
<div style="background:#eef2ff;border-radius:8px;padding:10px;margin-top:4px">
|
|
10565
|
+
<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>
|
|
10566
|
+
<div style="font-size:11px;color:#3730a3;margin-bottom:4px">challenge_id: <code>${escHtml(ctx.challenge_id)}</code> · ${t('过期于')} ${escHtml(ctx.expires_at || '')}</div>
|
|
10567
|
+
<div style="font-size:11px;color:#374151;margin-bottom:4px">2️⃣ ${t('把下方 proof_marker 原样发布到【该 GitHub 账号拥有的 public gist】,然后回来填 gist_id:')}</div>
|
|
10568
|
+
<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>
|
|
10569
|
+
<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>
|
|
10570
|
+
<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">
|
|
10571
|
+
<button class="btn btn-primary btn-sm" style="width:auto;margin-top:6px" onclick="ghClaimComplete()">3️⃣ 🔑 ${t('用 Passkey 完成认领')}</button>
|
|
10572
|
+
</div>`
|
|
10573
|
+
}
|
|
10574
|
+
|
|
10575
|
+
// F10:点「认领此贡献」→ 预填 actor + source_event_key,展开手动区,直接走 F9 既有三步流程(签发挑战)
|
|
10576
|
+
window.ghClaimFromRow = (actor, sek) => {
|
|
10577
|
+
const d = document.getElementById('gh-claim-details'); if (d) d.open = true
|
|
10578
|
+
const ia = document.getElementById('gh-claim-actor'); if (ia) ia.value = actor
|
|
10579
|
+
const is = document.getElementById('gh-claim-sek'); if (is) is.value = sek
|
|
10580
|
+
ghClaimIssue()
|
|
10581
|
+
}
|
|
10582
|
+
|
|
10583
|
+
window.ghClaimIssue = async () => {
|
|
10584
|
+
const msg = document.getElementById('gh-claim-msg')
|
|
10585
|
+
const actor = (document.getElementById('gh-claim-actor')?.value || '').trim()
|
|
10586
|
+
const sek = (document.getElementById('gh-claim-sek')?.value || '').trim()
|
|
10587
|
+
if (!actor || !sek) { if (msg) msg.innerHTML = `<span style="color:#dc2626">${t('请先填写 github_actor_id 与 source_event_key')}</span>`; return }
|
|
10588
|
+
if (msg) msg.innerHTML = `<span style="color:#6366f1">${t('签发中…')}</span>`
|
|
10589
|
+
const r = await POST('/contribution-identity/github/claim-challenge', { source_event_key: sek, github_actor_id: actor })
|
|
10590
|
+
if (r?.status === 'issued') {
|
|
10591
|
+
_ghClaimCtx = { challenge_id: r.challenge_id, expires_at: r.expires_at, proof_marker: r.proof_marker, actor, sek }
|
|
10592
|
+
const s2 = document.getElementById('gh-claim-step2'); if (s2) s2.innerHTML = ghClaimStep2Html(_ghClaimCtx)
|
|
10593
|
+
if (msg) msg.innerHTML = `<span style="color:#16a34a">✓ ${t('挑战已签发 — 按第 2 步发布 gist')}</span>`
|
|
10594
|
+
return
|
|
10595
|
+
}
|
|
10596
|
+
if (r?.status === 'already_bound_self') { if (msg) msg.innerHTML = `<span style="color:#16a34a">✓ ${t('该 GitHub 身份已绑定到本账号,无需重复认领')}</span>`; return }
|
|
10597
|
+
// 签发失败 → 清空旧挑战上下文(防止旧 challenge 被误用于新输入)
|
|
10598
|
+
_ghClaimCtx = null
|
|
10599
|
+
const s2f = document.getElementById('gh-claim-step2'); if (s2f) s2f.innerHTML = ''
|
|
10600
|
+
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>`
|
|
10601
|
+
}
|
|
10602
|
+
|
|
10603
|
+
window.ghClaimComplete = async () => {
|
|
10604
|
+
const msg = document.getElementById('gh-claim-msg')
|
|
10605
|
+
const ctx = _ghClaimCtx
|
|
10606
|
+
if (!ctx) { if (msg) msg.innerHTML = `<span style="color:#dc2626">${t('请先生成认领挑战')}</span>`; return }
|
|
10607
|
+
// 输入漂移守卫:签发后若用户改了 actor/source 输入框,旧挑战不再适用 — 清空并要求重新生成,
|
|
10608
|
+
// 防止"以为在认领当前输入的那条"的错觉(服务端本就只认 challenge 绑定的三元组)。
|
|
10609
|
+
const curActor = (document.getElementById('gh-claim-actor')?.value || '').trim()
|
|
10610
|
+
const curSek = (document.getElementById('gh-claim-sek')?.value || '').trim()
|
|
10611
|
+
if (curActor !== ctx.actor || curSek !== ctx.sek) {
|
|
10612
|
+
_ghClaimCtx = null
|
|
10613
|
+
const s2 = document.getElementById('gh-claim-step2'); if (s2) s2.innerHTML = ''
|
|
10614
|
+
if (msg) msg.innerHTML = `<span style="color:#dc2626">${t('输入已更改,与已签发的挑战不一致 — 请重新生成认领挑战')}</span>`
|
|
10615
|
+
return
|
|
10616
|
+
}
|
|
10617
|
+
const gistId = (document.getElementById('gh-claim-gist')?.value || '').trim()
|
|
10618
|
+
if (!gistId) { if (msg) msg.innerHTML = `<span style="color:#dc2626">${t('请填写 gist_id')}</span>`; return }
|
|
10619
|
+
let token
|
|
10620
|
+
try {
|
|
10621
|
+
// purpose_data 必须与服务端校验完全一致:{github_actor_id, source_event_key, challenge_id}
|
|
10622
|
+
token = await requestPasskeyGate('identity_claim', { github_actor_id: ctx.actor, source_event_key: ctx.sek, challenge_id: ctx.challenge_id })
|
|
10623
|
+
} catch (e) {
|
|
10624
|
+
if (msg) msg.innerHTML = `<span style="color:#dc2626">${t('Passkey 验证未完成')}: ${escHtml(e?.message || '')}</span>`
|
|
10625
|
+
return
|
|
10626
|
+
}
|
|
10627
|
+
if (msg) msg.innerHTML = `<span style="color:#6366f1">${t('提交认领…')}</span>`
|
|
10628
|
+
const r = await POST('/contribution-identity/github/claim-complete', {
|
|
10629
|
+
source_event_key: ctx.sek, github_actor_id: ctx.actor,
|
|
10630
|
+
challenge_id: ctx.challenge_id, gist_id: gistId, webauthn_token: token,
|
|
10631
|
+
})
|
|
10632
|
+
if (r?.status === 'claimed' || r?.status === 'already_bound_self') {
|
|
10633
|
+
_ghClaimCtx = null
|
|
10634
|
+
toast$(t('✓ 认领成功 — 贡献事实已归属到本账号'), 'success')
|
|
10635
|
+
renderMyContributions(document.getElementById('app')) // 刷新归属面 + 建设信誉面板
|
|
10636
|
+
return
|
|
10637
|
+
}
|
|
10638
|
+
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>`
|
|
10639
|
+
}
|
|
10640
|
+
|
|
9813
10641
|
// W7 客服 ticket-thread 视图
|
|
9814
10642
|
const TICKET_TYPE_META = {
|
|
9815
10643
|
created: { icon: '🛟', title: '新建工单', border: '#d97706' },
|
|
@@ -10732,7 +11560,63 @@ async function renderSellerAnalytics(app) {
|
|
|
10732
11560
|
<div style="font-size:10px;color:#9ca3af">${t('退款')}</div>
|
|
10733
11561
|
</div>
|
|
10734
11562
|
</div>
|
|
11563
|
+
|
|
11564
|
+
<div class="card" style="padding:14px;margin-bottom:10px">
|
|
11565
|
+
<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>
|
|
11566
|
+
<div id="seller-reviews-area" style="font-size:12px;color:#6b7280">${loading$()}</div>
|
|
11567
|
+
</div>
|
|
10735
11568
|
`, 'me')
|
|
11569
|
+
hydrateSellerReviews()
|
|
11570
|
+
}
|
|
11571
|
+
|
|
11572
|
+
// 店铺评价汇总 + 逐条回应(P2)。复用既有 POST /orders/:order_id/rating/reply(卖家一回一限);
|
|
11573
|
+
// 读 /sellers/me/ratings(authed,含 order_id)。不改评价 / 资金逻辑。
|
|
11574
|
+
async function hydrateSellerReviews() {
|
|
11575
|
+
const area = document.getElementById('seller-reviews-area')
|
|
11576
|
+
if (!area) return
|
|
11577
|
+
const r = await GET('/sellers/me/ratings?limit=50').catch(() => null)
|
|
11578
|
+
const items = Array.isArray(r?.items) ? r.items : []
|
|
11579
|
+
if (items.length === 0) { area.innerHTML = `<div style="color:#9ca3af;text-align:center;padding:12px">${t('暂无评价')}</div>`; return }
|
|
11580
|
+
const unreplied = Number(r?.agg?.unreplied || 0)
|
|
11581
|
+
const starStr = (n) => '★'.repeat(Math.max(0, Math.min(5, Number(n) || 0))) + '☆'.repeat(5 - Math.max(0, Math.min(5, Number(n) || 0)))
|
|
11582
|
+
area.innerHTML = `
|
|
11583
|
+
${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>` : ''}
|
|
11584
|
+
${items.map(it => it.masked ? `
|
|
11585
|
+
<div style="border:1px solid #f3f4f6;border-radius:8px;padding:10px;margin-bottom:8px;background:#fafafa">
|
|
11586
|
+
<div style="display:flex;justify-content:space-between;align-items:center;gap:6px;margin-bottom:4px">
|
|
11587
|
+
<span style="font-size:12px;color:#6b7280">🔒 ${t('评价双盲遮蔽中')}</span>
|
|
11588
|
+
<span style="font-size:10px;color:#9ca3af">${fmtTime(it.created_at)}</span>
|
|
11589
|
+
</div>
|
|
11590
|
+
<div style="font-size:11px;color:#9ca3af;margin-bottom:4px">📦 ${escHtml(it.product_title || '')}</div>
|
|
11591
|
+
<div style="font-size:11px;color:#92400e;background:#fffbeb;border:1px solid #fde68a;border-radius:6px;padding:6px 8px">${t('买家已评价,但需你先评价买家,或盲评期结束后才能查看与回应(防互相影响打分)。')}</div>
|
|
11592
|
+
</div>` : `
|
|
11593
|
+
<div style="border:1px solid #f3f4f6;border-radius:8px;padding:10px;margin-bottom:8px">
|
|
11594
|
+
<div style="display:flex;justify-content:space-between;align-items:center;gap:6px;margin-bottom:4px">
|
|
11595
|
+
<span style="color:#f59e0b;font-size:13px">${starStr(it.stars)}</span>
|
|
11596
|
+
<span style="font-size:10px;color:#9ca3af">${fmtTime(it.created_at)}</span>
|
|
11597
|
+
</div>
|
|
11598
|
+
<div style="font-size:11px;color:#6b7280;margin-bottom:4px">@${escHtml(it.buyer_handle || it.buyer_name || '')} · 📦 ${escHtml(it.product_title || '')}</div>
|
|
11599
|
+
${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>`}
|
|
11600
|
+
${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>` : ''}` : `
|
|
11601
|
+
<div style="display:flex;gap:6px;align-items:flex-end">
|
|
11602
|
+
<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>
|
|
11603
|
+
<button class="btn btn-primary btn-sm" style="padding:6px 12px;font-size:11px" onclick="submitSellerReviewReply('${it.order_id}')">${t('回应')}</button>
|
|
11604
|
+
</div>
|
|
11605
|
+
<div id="rev-reply-err-${it.order_id}" style="font-size:11px;color:#dc2626;margin-top:4px"></div>`}
|
|
11606
|
+
</div>`).join('')}
|
|
11607
|
+
`
|
|
11608
|
+
}
|
|
11609
|
+
|
|
11610
|
+
window.submitSellerReviewReply = async (orderId) => {
|
|
11611
|
+
const ta = document.getElementById('rev-reply-' + orderId)
|
|
11612
|
+
const errEl = document.getElementById('rev-reply-err-' + orderId)
|
|
11613
|
+
const reply = (ta?.value || '').trim()
|
|
11614
|
+
if (errEl) errEl.textContent = ''
|
|
11615
|
+
if (!reply) { if (errEl) errEl.textContent = t('回应不能为空'); return }
|
|
11616
|
+
const res = await POST(`/orders/${orderId}/rating/reply`, { reply })
|
|
11617
|
+
if (res.error) { if (errEl) errEl.textContent = res.error; return }
|
|
11618
|
+
toast$(t('已回应'))
|
|
11619
|
+
hydrateSellerReviews()
|
|
10736
11620
|
}
|
|
10737
11621
|
|
|
10738
11622
|
window.switchAnalyticsWindow = (days) => {
|
|
@@ -10808,15 +11692,17 @@ async function renderReturnsCenter(app) {
|
|
|
10808
11692
|
}
|
|
10809
11693
|
|
|
10810
11694
|
// L3 Phase 2:卖家确认收到退货 → 触发退款
|
|
10811
|
-
|
|
11695
|
+
// orderId 可选:从订单详情内联调用时传入 → 处理完回到订单详情(否则回退货中心)
|
|
11696
|
+
window.confirmReturnReceived = async (id, orderId) => {
|
|
10812
11697
|
if (!confirm(t('确认已收到退货商品?此操作将触发退款(不可撤销)'))) return
|
|
10813
11698
|
const r = await POST(`/return-requests/${id}/received`, {})
|
|
10814
11699
|
if (r.error) { toast$(r.error, 'error'); return }
|
|
10815
11700
|
toast$(t('已退款 · 退货完成'))
|
|
10816
|
-
|
|
11701
|
+
if (orderId) renderOrderDetail(document.getElementById('app'), orderId)
|
|
11702
|
+
else renderReturnsCenter(document.getElementById('app'))
|
|
10817
11703
|
}
|
|
10818
11704
|
|
|
10819
|
-
window.decideReturn = (id, decision) => {
|
|
11705
|
+
window.decideReturn = (id, decision, orderId) => {
|
|
10820
11706
|
const isAccept = decision === 'accept'
|
|
10821
11707
|
const title = isAccept ? `✓ ${t('确认接受退款')}` : `✗ ${t('拒绝退货')}`
|
|
10822
11708
|
const hint = isAccept
|
|
@@ -10835,7 +11721,7 @@ window.decideReturn = (id, decision) => {
|
|
|
10835
11721
|
<div id="ret-decide-msg" style="margin:8px 0"></div>
|
|
10836
11722
|
<div style="display:flex;gap:8px;margin-top:12px">
|
|
10837
11723
|
<button class="btn btn-gray" style="flex:1" onclick="this.closest('[style*=position]').remove()">${t('取消')}</button>
|
|
10838
|
-
<button class="btn ${isAccept ? 'btn-success' : 'btn-primary'}" style="flex:1" onclick="confirmDecideReturn('${id}','${decision}')">${isAccept ? t('接受退款') : t('拒绝')}</button>
|
|
11724
|
+
<button class="btn ${isAccept ? 'btn-success' : 'btn-primary'}" style="flex:1" onclick="confirmDecideReturn('${id}','${decision}','${orderId || ''}')">${isAccept ? t('接受退款') : t('拒绝')}</button>
|
|
10839
11725
|
</div>
|
|
10840
11726
|
</div>
|
|
10841
11727
|
</div>
|
|
@@ -10845,7 +11731,7 @@ window.decideReturn = (id, decision) => {
|
|
|
10845
11731
|
document.body.appendChild(div.firstElementChild)
|
|
10846
11732
|
}
|
|
10847
11733
|
|
|
10848
|
-
window.confirmDecideReturn = async (id, decision) => {
|
|
11734
|
+
window.confirmDecideReturn = async (id, decision, orderId) => {
|
|
10849
11735
|
const response = document.getElementById('ret-decide-text').value.trim()
|
|
10850
11736
|
const msg = document.getElementById('ret-decide-msg')
|
|
10851
11737
|
if (decision === 'reject' && !response) {
|
|
@@ -10856,7 +11742,8 @@ window.confirmDecideReturn = async (id, decision) => {
|
|
|
10856
11742
|
if (res.error) { msg.innerHTML = alert$('error', res.error); return }
|
|
10857
11743
|
document.querySelector('.js-modal')?.remove()
|
|
10858
11744
|
toast$(t('已处理'))
|
|
10859
|
-
|
|
11745
|
+
if (orderId) renderOrderDetail(document.getElementById('app'), orderId)
|
|
11746
|
+
else renderReturnsCenter(document.getElementById('app'))
|
|
10860
11747
|
}
|
|
10861
11748
|
|
|
10862
11749
|
window.sharePromoLink = async (productId, title) => {
|
|
@@ -10867,13 +11754,13 @@ window.sharePromoLink = async (productId, title) => {
|
|
|
10867
11754
|
if (res.error === 'rewards_opt_in_required') {
|
|
10868
11755
|
const msg = (window._lang === 'en' ? res.message_en : res.message_zh) || res.message_zh || res.error
|
|
10869
11756
|
const missing = Array.isArray(res.missing_requirements) ? `\n\n${t('待补')}: ${res.missing_requirements.join(', ')}` : ''
|
|
10870
|
-
return alert(`⚠ ${msg}${missing}\n\n${t('前往 #me
|
|
11757
|
+
return alert(`⚠ ${msg}${missing}\n\n${t('前往 #me 申请分享分润')}`)
|
|
10871
11758
|
}
|
|
10872
11759
|
return alert(`⚠ ${res.error}${res.completed_orders === 0 ? '\n' + t('完成该商品的购买后再分享') : ''}`)
|
|
10873
11760
|
}
|
|
10874
11761
|
const link = `${location.origin}${res.short_url}`
|
|
10875
11762
|
const meName = state.user.name || ''
|
|
10876
|
-
const text = `${t('我在 WebAZ 看上「')}${title}${t('
|
|
11763
|
+
const text = `${t('我在 WebAZ 看上「')}${title}${t('」,用 AI 比价,体验不错')}\n— ${meName}`
|
|
10877
11764
|
const r = await webShareOrCopy({ title: 'WebAZ — ' + title, text, url: link })
|
|
10878
11765
|
if (r === 'shared') toast$(t('已分享'))
|
|
10879
11766
|
else if (r === 'copied') toast$(t('已复制(含商品文案 + 链接)') + (res.reused ? '' : ' · ' + t('已自动加入「我的分享」')))
|
|
@@ -11487,29 +12374,8 @@ async function syncPlacementPref() {
|
|
|
11487
12374
|
} catch {}
|
|
11488
12375
|
}
|
|
11489
12376
|
|
|
11490
|
-
//
|
|
11491
|
-
//
|
|
11492
|
-
window.copyPlacementLink = async (link, side, el) => {
|
|
11493
|
-
const ok = await copyText(link)
|
|
11494
|
-
if (ok) {
|
|
11495
|
-
toast$((side === 'left' ? '🔵 ' : '🟢 ') + t('推荐码已复制'))
|
|
11496
|
-
if (el) {
|
|
11497
|
-
const orig = el.style.background
|
|
11498
|
-
el.style.background = side === 'left' ? '#bfdbfe' : '#bbf7d0'
|
|
11499
|
-
setTimeout(() => { el.style.background = orig }, 600)
|
|
11500
|
-
}
|
|
11501
|
-
} else {
|
|
11502
|
-
prompt(t('请手动复制'), link)
|
|
11503
|
-
}
|
|
11504
|
-
}
|
|
11505
|
-
|
|
11506
|
-
// 兼容:旧调用点保留
|
|
11507
|
-
window.sharePlatformLink = (side) => {
|
|
11508
|
-
if (!state.user?.id) return alert(t('请先登录'))
|
|
11509
|
-
if (side !== 'left' && side !== 'right') return
|
|
11510
|
-
const link = `${location.origin}/?placement=${state.user.id}&side=${side}`
|
|
11511
|
-
copyPlacementLink(link, side, null)
|
|
11512
|
-
}
|
|
12377
|
+
// pre-public 去左右码:copyPlacementLink / sharePlatformLink(生成 /i/CODE-L|R 侧链)已移除 —
|
|
12378
|
+
// 统一用唯一推荐码 copyRefLink('/i/CODE'),放置侧别由系统自动决定。
|
|
11513
12379
|
|
|
11514
12380
|
function renderRecover(app) {
|
|
11515
12381
|
app.innerHTML = `
|
|
@@ -11521,8 +12387,8 @@ function renderRecover(app) {
|
|
|
11521
12387
|
|
|
11522
12388
|
<div id="rec-step1">
|
|
11523
12389
|
<div class="form-group">
|
|
11524
|
-
<label class="form-label">${t('
|
|
11525
|
-
<input class="form-control" id="rec-name" placeholder="${t('例:陈小明')}">
|
|
12390
|
+
<label class="form-label">${t('名称或 @用户名')}</label>
|
|
12391
|
+
<input class="form-control" id="rec-name" placeholder="${t('例:陈小明 或 @chenxiaoming')}">
|
|
11526
12392
|
</div>
|
|
11527
12393
|
<div class="form-group">
|
|
11528
12394
|
<label class="form-label">${t('绑定的邮箱')}</label>
|
|
@@ -11536,7 +12402,11 @@ function renderRecover(app) {
|
|
|
11536
12402
|
<div class="form-group">
|
|
11537
12403
|
<input class="form-control" id="rec-code" placeholder="${t('6 位验证码')}" maxlength="6">
|
|
11538
12404
|
</div>
|
|
11539
|
-
<
|
|
12405
|
+
<div class="form-group">
|
|
12406
|
+
<label class="form-label">${t('设置新登录密码')} <span style="font-size:11px;color:#9ca3af;font-weight:400">${t('(可选 · 至少 8 位 · 留空则只找回 API Key)')}</span></label>
|
|
12407
|
+
<input class="form-control" id="rec-newpw" type="password" placeholder="${t('新密码(至少 8 位)')}" autocomplete="new-password" maxlength="200">
|
|
12408
|
+
</div>
|
|
12409
|
+
<button class="btn btn-primary" onclick="doRecoverConfirm()">${t('验证并找回')}</button>
|
|
11540
12410
|
<button class="btn btn-outline" onclick="recoverBackToStep1()" style="margin-left:8px">${t('重新开始')}</button>
|
|
11541
12411
|
</div>
|
|
11542
12412
|
|
|
@@ -11576,13 +12446,15 @@ window.doRecoverConfirm = async () => {
|
|
|
11576
12446
|
const name = codeInp?.dataset?.name
|
|
11577
12447
|
const email = codeInp?.dataset?.email
|
|
11578
12448
|
const code = codeInp?.value?.trim()
|
|
12449
|
+
const newpw = document.getElementById('rec-newpw')?.value || ''
|
|
11579
12450
|
const result = document.getElementById('rec-result')
|
|
11580
12451
|
if (!code) { result.innerHTML = alert$('error', t('请填写验证码')); return }
|
|
12452
|
+
if (newpw && newpw.length < 8) { result.innerHTML = alert$('error', t('新密码至少 8 字符')); return }
|
|
11581
12453
|
result.innerHTML = loading$()
|
|
11582
|
-
const res = await api('POST', '/recover-key/confirm', { name, email, code })
|
|
12454
|
+
const res = await api('POST', '/recover-key/confirm', { name, email, code, ...(newpw ? { new_password: newpw } : {}) })
|
|
11583
12455
|
if (res.error) { result.innerHTML = alert$('error', res.error); return }
|
|
11584
12456
|
result.innerHTML = `
|
|
11585
|
-
<div class="alert alert-success" style="font-size:13px">${t('✓ 找回成功')}</div>
|
|
12457
|
+
<div class="alert alert-success" style="font-size:13px">${res.password_reset ? t('✓ 找回成功 · 新密码已设置') : t('✓ 找回成功')}</div>
|
|
11586
12458
|
<div style="background:#f3f4f6;border-radius:8px;padding:12px;margin-top:10px">
|
|
11587
12459
|
<div style="font-size:12px;color:#6b7280;margin-bottom:6px">${t('你的 API Key')}</div>
|
|
11588
12460
|
<code style="font-size:13px;word-break:break-all">${res.api_key}</code>
|
|
@@ -11590,6 +12462,7 @@ window.doRecoverConfirm = async () => {
|
|
|
11590
12462
|
<button class="btn btn-outline btn-sm" onclick="copyApiKey('${res.api_key}')">${t('复制')}</button>
|
|
11591
12463
|
<button class="btn btn-primary btn-sm" onclick="useKey('${res.api_key}')">${t('立即登录')}</button>
|
|
11592
12464
|
</div>
|
|
12465
|
+
<div style="font-size:11px;color:#b91c1c;margin-top:10px;line-height:1.5">⚠️ ${t('仅在私密设备打开;不要截图、转发或发送给任何人。')}</div>
|
|
11593
12466
|
</div>`
|
|
11594
12467
|
}
|
|
11595
12468
|
|
|
@@ -11655,6 +12528,7 @@ window.doLogin = async () => {
|
|
|
11655
12528
|
await persistApiKey(key)
|
|
11656
12529
|
connectSSE()
|
|
11657
12530
|
maybeClaimPendingProductShare()
|
|
12531
|
+
maybeClaimPendingShopReferral()
|
|
11658
12532
|
// 2026-05-28 修:登录 sheet 是 .js-sheet(openSheet 建),之前只删 .js-modal → 登录成功后窗口不消失
|
|
11659
12533
|
try { document.querySelectorAll('.js-modal, .js-sheet').forEach(m => m.remove()) } catch {}
|
|
11660
12534
|
navigateIntended(roleHome(user.role))
|
|
@@ -11681,6 +12555,7 @@ window.doLoginByPassword = async () => {
|
|
|
11681
12555
|
else state.user = { id: res.user_id, name: res.name, role: res.role }
|
|
11682
12556
|
connectSSE()
|
|
11683
12557
|
maybeClaimPendingProductShare()
|
|
12558
|
+
maybeClaimPendingShopReferral()
|
|
11684
12559
|
// 2026-05-28 修:同 doLogin — 关 .js-sheet 登录窗口
|
|
11685
12560
|
try { document.querySelectorAll('.js-modal, .js-sheet').forEach(m => m.remove()) } catch {}
|
|
11686
12561
|
navigateIntended(roleHome(res.role))
|
|
@@ -11688,46 +12563,50 @@ window.doLoginByPassword = async () => {
|
|
|
11688
12563
|
|
|
11689
12564
|
|
|
11690
12565
|
// 从 URL 解析分享 hint(30 天 cookie 持久化)
|
|
11691
|
-
// ?ref=
|
|
11692
|
-
// ?placement=
|
|
11693
|
-
//
|
|
11694
|
-
// ?ref=xxx&placement=xxx&side=... → 显式两轨道
|
|
12566
|
+
// ?ref=CODE → 三级佣金 sponsor(同时作 placement inviter)
|
|
12567
|
+
// ?placement=CODE → 仅积分挂靠
|
|
12568
|
+
// pre-public 去左右码:不再解析 side / -L/-R 侧别,放置侧别由注册时系统自动决定。
|
|
11695
12569
|
function readShareHint() {
|
|
11696
12570
|
const params = new URLSearchParams(location.search)
|
|
11697
12571
|
let urlRef = params.get('ref')
|
|
11698
12572
|
let urlPlace = params.get('placement')
|
|
11699
|
-
|
|
11700
|
-
//
|
|
11701
|
-
const stripSide = (v) =>
|
|
11702
|
-
|
|
11703
|
-
|
|
11704
|
-
|
|
11705
|
-
|
|
11706
|
-
|
|
11707
|
-
;[urlRef, extraSide] = stripSide(urlRef)
|
|
11708
|
-
if (!extraSide) [urlPlace, extraSide] = stripSide(urlPlace)
|
|
11709
|
-
if (!urlSide && extraSide) urlSide = extraSide
|
|
11710
|
-
// ref / placement 接受三态:usr_xxx / VKSF9P (permanent_code 6-7 大写字母数字) / @handle 或 handle
|
|
11711
|
-
const refPattern = /^(usr_[A-Za-z0-9_]+|[A-Z0-9]{6,7}|@?[a-z0-9._]{3,20})$/
|
|
12573
|
+
// pre-public 去左右码:兼容旧的 -L/-R 后缀但只做归一化(剥离回基础码),不再提取/存储 side。
|
|
12574
|
+
// 放置侧别由注册时系统自动决定。
|
|
12575
|
+
const stripSide = (v) => (v ? v.replace(/-[lLrR]$/, '') : v)
|
|
12576
|
+
urlRef = stripSide(urlRef)
|
|
12577
|
+
urlPlace = stripSide(urlPlace)
|
|
12578
|
+
// ref / placement 仅接受邀请码(6-7 位永久码)。usr_xxx / @handle / 裸 handle 不再作为邀请引用 —
|
|
12579
|
+
// 收窄公开邀请面,消除歧义(权威由服务端 resolveInviteCodeRef 二次校验)。
|
|
12580
|
+
const refPattern = /^[A-Za-z0-9]{6,7}$/
|
|
11712
12581
|
const validRef = urlRef && refPattern.test(urlRef)
|
|
11713
12582
|
const validPlace= urlPlace && refPattern.test(urlPlace)
|
|
11714
12583
|
if (validRef || validPlace) {
|
|
11715
|
-
const side = (urlSide === 'left' || urlSide === 'right') ? urlSide : null
|
|
11716
12584
|
const obj = {
|
|
11717
12585
|
sponsor_id: validRef ? urlRef : null,
|
|
11718
|
-
placement_inviter_id: validPlace ? urlPlace :
|
|
11719
|
-
placement_side: side,
|
|
12586
|
+
placement_inviter_id: validPlace ? urlPlace : null,
|
|
11720
12587
|
expiry: Date.now() + 30 * 86400_000,
|
|
11721
12588
|
}
|
|
11722
12589
|
localStorage.setItem('webaz_share_hint', JSON.stringify(obj))
|
|
11723
12590
|
return obj
|
|
11724
12591
|
}
|
|
12592
|
+
// 旧 localStorage hint 里的 sponsor_id / placement_inviter_id 若是 usr_xxx / @handle / 裸 handle(非邀请码),
|
|
12593
|
+
// 注册时不再使用 —— 清掉非码引用,只保留合法邀请码。
|
|
12594
|
+
const okCode = (v) => typeof v === 'string' && /^[A-Za-z0-9]{6,7}$/.test(v)
|
|
12595
|
+
const sanitizeHint = (o) => {
|
|
12596
|
+
if (!o || typeof o !== 'object') return null
|
|
12597
|
+
if (o.sponsor_id && !okCode(o.sponsor_id)) o.sponsor_id = null
|
|
12598
|
+
if (o.placement_inviter_id && !okCode(o.placement_inviter_id)) o.placement_inviter_id = null
|
|
12599
|
+
return (o.sponsor_id || o.placement_inviter_id) ? o : null
|
|
12600
|
+
}
|
|
11725
12601
|
try {
|
|
11726
12602
|
const raw = localStorage.getItem('webaz_share_hint')
|
|
11727
12603
|
if (raw) {
|
|
11728
12604
|
const obj = JSON.parse(raw)
|
|
11729
|
-
if (obj.expiry > Date.now())
|
|
11730
|
-
|
|
12605
|
+
if (obj.expiry > Date.now()) {
|
|
12606
|
+
const clean = sanitizeHint(obj)
|
|
12607
|
+
if (clean) return clean
|
|
12608
|
+
}
|
|
12609
|
+
localStorage.removeItem('webaz_share_hint') // expired or non-code → drop
|
|
11731
12610
|
}
|
|
11732
12611
|
} catch {}
|
|
11733
12612
|
// 向后兼容:旧 'webaz_ref' 格式
|
|
@@ -11735,13 +12614,12 @@ function readShareHint() {
|
|
|
11735
12614
|
const old = localStorage.getItem('webaz_ref')
|
|
11736
12615
|
if (old) {
|
|
11737
12616
|
const o = JSON.parse(old)
|
|
11738
|
-
if (o.expiry > Date.now()) return {
|
|
12617
|
+
if (o.expiry > Date.now() && okCode(o.ref)) return {
|
|
11739
12618
|
sponsor_id: o.ref || null,
|
|
11740
|
-
placement_inviter_id:
|
|
11741
|
-
placement_side: o.side || null,
|
|
12619
|
+
placement_inviter_id: null,
|
|
11742
12620
|
expiry: o.expiry,
|
|
11743
12621
|
}
|
|
11744
|
-
localStorage.removeItem('webaz_ref')
|
|
12622
|
+
localStorage.removeItem('webaz_ref') // expired or non-code → drop
|
|
11745
12623
|
}
|
|
11746
12624
|
} catch {}
|
|
11747
12625
|
return null
|
|
@@ -11787,6 +12665,24 @@ async function maybeClaimPendingProductShare() {
|
|
|
11787
12665
|
writeShareCtx({ pending_share_id: null })
|
|
11788
12666
|
}
|
|
11789
12667
|
|
|
12668
|
+
// 店铺推荐锚定:从 ?ref=CODE + #shop/<seller> 进来时 ShareCtx 暂存 pending_shop_referral,
|
|
12669
|
+
// 登录/注册后调一次 touch 落库(first-touch 30 天锁)。只锚定推荐关系,不是全店佣金权。
|
|
12670
|
+
async function maybeClaimPendingShopReferral() {
|
|
12671
|
+
if (!state.user?.id) return
|
|
12672
|
+
const ctx = readShareCtx()
|
|
12673
|
+
const p = ctx?.pending_shop_referral
|
|
12674
|
+
if (!p) return
|
|
12675
|
+
// 只接受邀请码形态;旧 localStorage 里的 usr_xxx / @handle 残留 → 直接清理不发送
|
|
12676
|
+
if (!p.ref_code || !/^[A-Za-z0-9]{6,7}$/.test(p.ref_code) || !p.seller_identifier) {
|
|
12677
|
+
writeShareCtx({ pending_shop_referral: null }); return
|
|
12678
|
+
}
|
|
12679
|
+
try {
|
|
12680
|
+
await POST('/shop-referral/touch', { seller_identifier: p.seller_identifier, ref_code: p.ref_code })
|
|
12681
|
+
// 任何已送达的响应(成功 / self-skip / already_locked / typed 错误)都清;仅网络异常保留待重试
|
|
12682
|
+
writeShareCtx({ pending_shop_referral: null })
|
|
12683
|
+
} catch {}
|
|
12684
|
+
}
|
|
12685
|
+
|
|
11790
12686
|
// 还原意图 hash(注册/登录后调用,没意图就走 fallback)
|
|
11791
12687
|
function navigateIntended(fallback) {
|
|
11792
12688
|
const intended = sessionStorage.getItem('webaz_intended_hash')
|
|
@@ -11825,7 +12721,7 @@ function renderShareBanner(variant) {
|
|
|
11825
12721
|
<div id="share-banner-hero" style="background:linear-gradient(135deg,#eef2ff,#fce7f3);border:1px solid #c7d2fe;border-radius:12px;padding:14px 16px;margin-bottom:14px">
|
|
11826
12722
|
<div style="font-size:13px;color:#3730a3;font-weight:600;margin-bottom:4px">👋 ${t('你被')} <strong>${escHtml(name)}</strong> ${t('邀请来 WebAZ')}</div>
|
|
11827
12723
|
${tgtLine ? `<div style="font-size:12px;color:#374151;line-height:1.6;margin-top:6px">${tgtLine}</div>` : ''}
|
|
11828
|
-
<div style="font-size:11px;color:#6366f1;margin-top:8px">${t('
|
|
12724
|
+
<div style="font-size:11px;color:#6366f1;margin-top:8px">${t('通过邀请进入会记录归因;分享分润需登录后单独申请。')}</div>
|
|
11829
12725
|
</div>`
|
|
11830
12726
|
}
|
|
11831
12727
|
// compact
|
|
@@ -11910,7 +12806,6 @@ async function initShareCtx() {
|
|
|
11910
12806
|
if (hint?.sponsor_id && !ctx?.sponsor_id) patch.sponsor_id = hint.sponsor_id
|
|
11911
12807
|
if (hint?.placement_inviter_id && !ctx?.placement_inviter_id) {
|
|
11912
12808
|
patch.placement_inviter_id = hint.placement_inviter_id
|
|
11913
|
-
patch.placement_side = hint.placement_side
|
|
11914
12809
|
}
|
|
11915
12810
|
if (fromHash && !ctx?.target_hash) patch.target_hash = fromHash
|
|
11916
12811
|
|
|
@@ -11920,6 +12815,15 @@ async function initShareCtx() {
|
|
|
11920
12815
|
patch.pending_share_id = urlShareId
|
|
11921
12816
|
}
|
|
11922
12817
|
|
|
12818
|
+
// 店铺推荐:?ref=CODE + #shop/<seller> → 暂存待登录后 touch(只锚定推荐关系,非全店佣金权)。
|
|
12819
|
+
// hint.sponsor_id 已由 readShareHint 收窄为邀请码(-L/-R 已归一化为基础码),usr_xxx/@handle 进不来。
|
|
12820
|
+
if (hint?.sponsor_id && fromHash && fromHash.startsWith('#shop/') && !ctx?.pending_shop_referral) {
|
|
12821
|
+
const sellerIdent = fromHash.slice('#shop/'.length).split('?')[0].trim()
|
|
12822
|
+
if (sellerIdent && sellerIdent !== 'agent') {
|
|
12823
|
+
patch.pending_shop_referral = { seller_identifier: sellerIdent, ref_code: hint.sponsor_id }
|
|
12824
|
+
}
|
|
12825
|
+
}
|
|
12826
|
+
|
|
11923
12827
|
if (Object.keys(patch).length > 0 || !ctx) {
|
|
11924
12828
|
ctx = writeShareCtx(patch)
|
|
11925
12829
|
}
|
|
@@ -11933,6 +12837,10 @@ async function initShareCtx() {
|
|
|
11933
12837
|
// 不论成功失败都清掉 pending(避免重复尝试)
|
|
11934
12838
|
writeShareCtx({ pending_share_id: null })
|
|
11935
12839
|
}
|
|
12840
|
+
// 已登录 + 有待锚定店铺推荐 → 立即 touch(网络失败保留待下次)
|
|
12841
|
+
if (state.user?.id && ctx.pending_shop_referral) {
|
|
12842
|
+
await maybeClaimPendingShopReferral()
|
|
12843
|
+
}
|
|
11936
12844
|
|
|
11937
12845
|
// Enrich:并发 fetch sponsor 公开卡 + target 预览(阻塞返回,确保 banner 渲染时数据齐)
|
|
11938
12846
|
// 2026-05-31 修:每个 fetch 加 AbortSignal.timeout(5000),防 boot 阶段任一 fetch hang
|
|
@@ -12002,11 +12910,47 @@ window._mountTurnstileIfEnabled = async () => {
|
|
|
12002
12910
|
} catch { /* 加载失败不阻塞 dev */ }
|
|
12003
12911
|
}
|
|
12004
12912
|
|
|
12913
|
+
// 注册邮箱验证:发码。成功后展开验证码输入框。
|
|
12914
|
+
window._regCodeSent = false
|
|
12915
|
+
window._onRegEmailInput = () => {
|
|
12916
|
+
// 邮箱改了就要求重新发码(旧码对不上新邮箱)
|
|
12917
|
+
window._regCodeSent = false
|
|
12918
|
+
const row = document.getElementById('reg-code-row')
|
|
12919
|
+
if (row) row.style.display = 'none'
|
|
12920
|
+
}
|
|
12921
|
+
window.doRegSendCode = async () => {
|
|
12922
|
+
const email = document.getElementById('inp-reg-email')?.value?.trim()
|
|
12923
|
+
const btn = document.getElementById('btn-reg-sendcode')
|
|
12924
|
+
if (!email || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) return showMsg('error', t('请填写有效邮箱'))
|
|
12925
|
+
if (btn) { btn.disabled = true; btn.textContent = t('发送中...') }
|
|
12926
|
+
const res = await POST('/register/send-code', { email })
|
|
12927
|
+
if (res.error) {
|
|
12928
|
+
if (btn) { btn.disabled = false; btn.textContent = t('发送验证码') }
|
|
12929
|
+
return showMsg('error', res.error)
|
|
12930
|
+
}
|
|
12931
|
+
window._regCodeSent = true
|
|
12932
|
+
const row = document.getElementById('reg-code-row')
|
|
12933
|
+
if (row) row.style.display = ''
|
|
12934
|
+
// 60s 冷却
|
|
12935
|
+
let left = 60
|
|
12936
|
+
if (btn) {
|
|
12937
|
+
btn.disabled = true
|
|
12938
|
+
const tick = () => { if (left <= 0) { btn.disabled = false; btn.textContent = t('重新发送'); return } btn.textContent = left + 's'; left--; setTimeout(tick, 1000) }
|
|
12939
|
+
tick()
|
|
12940
|
+
}
|
|
12941
|
+
showMsg('success', t('验证码已发送至邮箱,请查收'))
|
|
12942
|
+
}
|
|
12943
|
+
|
|
12005
12944
|
window.doRegister = async () => {
|
|
12006
12945
|
const name = document.getElementById('inp-name').value.trim()
|
|
12007
12946
|
const role = document.getElementById('inp-role').value
|
|
12008
12947
|
const region = document.getElementById('inp-region')?.value || ''
|
|
12009
12948
|
const sponsorInput = document.getElementById('inp-sponsor')?.value?.trim() || ''
|
|
12949
|
+
const email = document.getElementById('inp-reg-email')?.value?.trim() || ''
|
|
12950
|
+
const code = document.getElementById('inp-reg-code')?.value?.trim() || ''
|
|
12951
|
+
if (!email) return showMsg('error', t('请填写找回邮箱'))
|
|
12952
|
+
if (!window._regCodeSent) return showMsg('error', t('请先点"发送验证码"并验证邮箱'))
|
|
12953
|
+
if (!code) return showMsg('error', t('请输入邮箱验证码'))
|
|
12010
12954
|
if (!name) return showMsg('error', t('请填写名称'))
|
|
12011
12955
|
if (!region) return showMsg('error', t('请选择国家 / 地区'))
|
|
12012
12956
|
// A3 软 gate:sponsor_id 视觉必填但允许空(后端按 require_ref_to_register 系统设置裁决)
|
|
@@ -12014,13 +12958,12 @@ window.doRegister = async () => {
|
|
|
12014
12958
|
if (!confirm(t('没有填写邀请码 — 你将绑定到平台公库(推荐找老用户拿邀请链接)\n\n确认继续?'))) return
|
|
12015
12959
|
}
|
|
12016
12960
|
const hint = readShareHint()
|
|
12017
|
-
const body = { name, role, region }
|
|
12961
|
+
const body = { name, role, region, email, code }
|
|
12018
12962
|
// 优先 input 值,回退 URL hint
|
|
12019
12963
|
const sponsorFinal = sponsorInput || hint?.sponsor_id
|
|
12020
12964
|
if (sponsorFinal) body.sponsor_id = sponsorFinal
|
|
12021
|
-
if (hint?.placement_inviter_id
|
|
12965
|
+
if (hint?.placement_inviter_id) {
|
|
12022
12966
|
body.placement_inviter_id = hint.placement_inviter_id
|
|
12023
|
-
body.placement_side = hint.placement_side
|
|
12024
12967
|
}
|
|
12025
12968
|
// #1049 Turnstile token(若启用)
|
|
12026
12969
|
if (window._turnstileToken) body.turnstile_token = window._turnstileToken
|
|
@@ -12045,6 +12988,7 @@ window.doRegister = async () => {
|
|
|
12045
12988
|
await persistApiKey(res.api_key)
|
|
12046
12989
|
connectSSE()
|
|
12047
12990
|
maybeClaimPendingProductShare()
|
|
12991
|
+
maybeClaimPendingShopReferral()
|
|
12048
12992
|
// 2026-05-24 修:关闭 auth sheet,再开成功 modal(避免堆叠)
|
|
12049
12993
|
// 2026-05-28 修:auth sheet 是 .js-sheet(openSheet 建),需一并删除
|
|
12050
12994
|
try { document.querySelectorAll('.js-modal, .js-sheet').forEach(m => m.remove()) } catch {}
|
|
@@ -12062,41 +13006,91 @@ function showRegisterSuccessModal(res) {
|
|
|
12062
13006
|
// P1 (QA 轮 14.a): 区分"邀请人"(referral sponsor) 与"积分树挂靠"(binary placement)
|
|
12063
13007
|
// 旧版挤一行 "已绑定邀请人 X · 左区 depth N" → 用户误以为 depth 是相对邀请人
|
|
12064
13008
|
const placementLine = res.placement
|
|
12065
|
-
? `<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('
|
|
13009
|
+
? `<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>`
|
|
12066
13010
|
: ''
|
|
12067
13011
|
_openModal(`
|
|
12068
13012
|
<h2 style="font-size:18px;font-weight:600;margin-bottom:8px;color:#16a34a">🎉 ${t('注册成功!')}</h2>
|
|
12069
13013
|
${res.sponsor_id && ctx?.sponsor_name ? `<div style="font-size:12px;color:#6366f1;margin-bottom:6px">${t('邀请人')}: <strong>${escHtml(ctx.sponsor_name)}</strong></div>` : ''}
|
|
12070
13014
|
${placementLine}
|
|
12071
13015
|
|
|
12072
|
-
<div style="background:#fef3c7;border:1px solid #fde68a;border-radius:8px;padding:10px 12px;margin-bottom:
|
|
12073
|
-
<div style="font-size:12px;color:#92400e;font-weight:600;margin-bottom:6px">⚠️ ${t('立即保存你的 API Key')}</div>
|
|
12074
|
-
<div style="font-size:11px;color:#78350f;margin-bottom:6px">${t('这是你的唯一登录凭证。丢了只能通过绑定邮箱找回。')}</div>
|
|
13016
|
+
<div style="background:#fef3c7;border:1px solid #fde68a;border-radius:8px;padding:10px 12px;margin-bottom:12px">
|
|
13017
|
+
<div style="font-size:12px;color:#92400e;font-weight:600;margin-bottom:6px">⚠️ ${t('立即保存你的 API Key(登录凭证)')}</div>
|
|
12075
13018
|
<div style="display:flex;gap:6px;align-items:center">
|
|
12076
13019
|
<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>
|
|
12077
|
-
|
|
13020
|
+
</div>
|
|
13021
|
+
<div style="display:flex;gap:6px;margin-top:8px">
|
|
13022
|
+
<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>
|
|
13023
|
+
<button class="btn btn-outline btn-sm" style="font-size:11px;padding:6px 10px;flex:1" onclick="window._downloadCredBackup()">⬇️ ${t('下载备份 .txt')}</button>
|
|
12078
13024
|
</div>
|
|
12079
13025
|
</div>
|
|
12080
13026
|
|
|
12081
|
-
<div style="font-size:13px;color:#374151;font-weight:600;margin-bottom:6px">${t('
|
|
12082
|
-
<div style="background:#f9fafb;border-radius:8px;padding:
|
|
12083
|
-
|
|
12084
|
-
|
|
12085
|
-
|
|
12086
|
-
|
|
13027
|
+
<div style="font-size:13px;color:#374151;font-weight:600;margin-bottom:6px">${t('保存凭证检查清单')}</div>
|
|
13028
|
+
<div style="background:#f9fafb;border-radius:8px;padding:8px 10px;margin-bottom:8px;font-size:12px;line-height:1.9">
|
|
13029
|
+
<div id="reg-chk-save">☐ ${t('复制 API Key 或下载备份文件')}</div>
|
|
13030
|
+
<div>✅ ${t('已验证找回邮箱')}:${escHtml(res.email || '')}</div>
|
|
13031
|
+
<div>☐ ${t('设置登录密码(备用凭证)')}</div>
|
|
13032
|
+
<div>☐ ${t('🔐 绑定 Passkey — 大额提现自动启用,防账号被盗')}</div>
|
|
13033
|
+
</div>
|
|
13034
|
+
<div style="font-size:11px;color:#6b7280;line-height:1.6;margin-bottom:12px">
|
|
13035
|
+
${t('邮箱已验证:可用于找回账号或重置登录密码。API Key 仍是主要身份凭证,强烈建议保存;Passkey 是增强保护,不替代恢复邮箱。')}
|
|
12087
13036
|
</div>
|
|
12088
13037
|
|
|
12089
13038
|
${targetBtn}
|
|
12090
13039
|
<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>
|
|
12091
|
-
<button class="btn btn-outline" style="width:100%;margin-bottom:8px" onclick="window._closeRegModal('
|
|
12092
|
-
<button class="btn btn-outline" style="width:100
|
|
13040
|
+
<button class="btn btn-outline" style="width:100%;margin-bottom:8px" onclick="window._closeRegModal('password')">🔑 ${t('设置登录密码')}</button>
|
|
13041
|
+
<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>
|
|
12093
13042
|
`)
|
|
13043
|
+
// 凭证保存状态:复制或下载前,"稍后"按钮弱化 + 强提示。
|
|
13044
|
+
window._regSaved = false
|
|
13045
|
+
window._regMarkSaved = () => {
|
|
13046
|
+
window._regSaved = true
|
|
13047
|
+
const chk = document.getElementById('reg-chk-save')
|
|
13048
|
+
if (chk) { chk.innerHTML = '✅ ' + t('复制 API Key 或下载备份文件'); chk.style.color = '#16a34a' }
|
|
13049
|
+
const skip = document.getElementById('reg-skip-btn')
|
|
13050
|
+
if (skip) skip.style.color = ''
|
|
13051
|
+
}
|
|
13052
|
+
window._downloadCredBackup = () => {
|
|
13053
|
+
try {
|
|
13054
|
+
const lines = [
|
|
13055
|
+
'WebAZ 账户备份 / Account Backup',
|
|
13056
|
+
'================================',
|
|
13057
|
+
`${t('名称')} / Name: ${res.name || ''}`,
|
|
13058
|
+
`Handle: @${res.handle || ''}`,
|
|
13059
|
+
`${t('角色')} / Role: ${res.role || ''}`,
|
|
13060
|
+
`${t('找回邮箱')} / Recovery email: ${res.email || ''}`,
|
|
13061
|
+
`${t('注册时间')} / Registered: ${new Date().toISOString()}`,
|
|
13062
|
+
'--------------------------------',
|
|
13063
|
+
'API Key (登录凭证 / login credential):',
|
|
13064
|
+
res.api_key || '',
|
|
13065
|
+
'--------------------------------',
|
|
13066
|
+
'⚠️ 这是你的登录凭证,等同密码。妥善保管,不要截图分享或发给任何人。',
|
|
13067
|
+
'⚠️ This is your login credential (equivalent to a password). Keep it private — do not screenshot, forward, or share it.',
|
|
13068
|
+
'丢失 API Key 可用上面的找回邮箱在 https://webaz.xyz/#recover 重置(也可重置登录密码)。',
|
|
13069
|
+
'Lost your API Key? Reset it (and your password) with the recovery email above at https://webaz.xyz/#recover',
|
|
13070
|
+
]
|
|
13071
|
+
const blob = new Blob([lines.join('\n')], { type: 'text/plain;charset=utf-8' })
|
|
13072
|
+
const url = URL.createObjectURL(blob)
|
|
13073
|
+
const a = document.createElement('a')
|
|
13074
|
+
a.href = url
|
|
13075
|
+
a.download = `webaz-backup-${(res.handle || res.user_id || 'account')}.txt`
|
|
13076
|
+
document.body.appendChild(a); a.click(); a.remove()
|
|
13077
|
+
setTimeout(() => URL.revokeObjectURL(url), 2000)
|
|
13078
|
+
window._regMarkSaved()
|
|
13079
|
+
toast$(t('备份已下载'), 'success')
|
|
13080
|
+
} catch (e) {
|
|
13081
|
+
toast$(t('下载失败,请改用复制'), 'error')
|
|
13082
|
+
}
|
|
13083
|
+
}
|
|
12094
13084
|
window._closeRegModal = (action) => {
|
|
13085
|
+
// 复制/下载前点"稍后" → 强提示(已绑邮箱可找回,但仍建议先存 key)
|
|
13086
|
+
if (action === false && !window._regSaved) {
|
|
13087
|
+
if (!confirm(t('你还没复制 API Key 或下载备份。\n虽然已绑定找回邮箱(丢号可重置),仍强烈建议先保存 Key。\n\n确定先逛逛?'))) return
|
|
13088
|
+
}
|
|
12095
13089
|
closeModal()
|
|
12096
13090
|
if (action === true) {
|
|
12097
13091
|
// 跳 intended_hash
|
|
12098
13092
|
navigateIntended(roleHome(res.role))
|
|
12099
|
-
} else if (action === 'profile') {
|
|
13093
|
+
} else if (action === 'password' || action === 'profile') {
|
|
12100
13094
|
sessionStorage.removeItem('webaz_intended_hash')
|
|
12101
13095
|
location.hash = '#me/settings'
|
|
12102
13096
|
} else if (action === 'passkey') {
|
|
@@ -13226,8 +14220,8 @@ async function renderFeedView() {
|
|
|
13226
14220
|
const amount = Number(extra.amount || 0).toFixed(2)
|
|
13227
14221
|
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>`
|
|
13228
14222
|
} else if (e.kind === 'join_binary') {
|
|
13229
|
-
|
|
13230
|
-
body = `${actor} ${t('加入了')} ${escHtml(extra.placement_name || '—')} ${t('
|
|
14223
|
+
// pre-public 去左右码:活动流不再广播左/右区,只显示加入了某人的积分树
|
|
14224
|
+
body = `${actor} ${t('加入了')} ${escHtml(extra.placement_name || '—')} ${t('的积分树')}`
|
|
13231
14225
|
}
|
|
13232
14226
|
const icon = e.kind === 'purchase' ? '🛒' : e.kind === 'commission' ? '💰' : '⚛'
|
|
13233
14227
|
return `<div class="card" style="margin-bottom:8px;padding:10px 12px;display:flex;gap:10px;align-items:flex-start">
|
|
@@ -18325,20 +19319,21 @@ window.setSellerSubTab = (k) => {
|
|
|
18325
19319
|
// C-4: 批量发货 modal
|
|
18326
19320
|
window.openBatchShipModal = async (ids) => {
|
|
18327
19321
|
if (!Array.isArray(ids) || ids.length === 0) return
|
|
18328
|
-
// 拉物流公司
|
|
19322
|
+
// 拉物流公司(可空:自发货不需要物流方)
|
|
18329
19323
|
const lc = await GET('/logistics/companies').catch(() => [])
|
|
18330
19324
|
const companies = Array.isArray(lc) ? lc : []
|
|
18331
|
-
if (companies.length === 0) { alert(t('暂无可用物流公司')); return }
|
|
18332
19325
|
const html = `
|
|
18333
19326
|
<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()">
|
|
18334
19327
|
<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()">
|
|
18335
19328
|
<h2 style="font-size:16px;font-weight:700;margin-bottom:8px">📦 ${t('批量发货')}</h2>
|
|
18336
19329
|
<div style="font-size:12px;color:#6b7280;margin-bottom:12px">${t('将')} <strong>${ids.length}</strong> ${t('个订单一次性标记为已发货')}</div>
|
|
18337
19330
|
<div class="form-group">
|
|
18338
|
-
<label class="form-label">${t('
|
|
19331
|
+
<label class="form-label">${t('发货方式')}</label>
|
|
18339
19332
|
<select class="form-control" id="bs-logistics">
|
|
19333
|
+
<option value="self">${t('📦 我自己发货(自提自送)')}</option>
|
|
18340
19334
|
${companies.map(c => `<option value="${escHtml(c.id)}">${escHtml(c.name)}</option>`).join('')}
|
|
18341
19335
|
</select>
|
|
19336
|
+
<div style="font-size:11px;color:#92400e;margin-top:6px;line-height:1.5">${t('自己发货:你负责揽收 / 运输 / 送达,超时或虚假发货仍按卖家责任处理。选物流公司则由物流方流转。')}</div>
|
|
18342
19337
|
</div>
|
|
18343
19338
|
<div style="font-size:11px;color:#9ca3af;margin-bottom:8px">${t('快递单号可发货后单独补填,或先填部分')}</div>
|
|
18344
19339
|
<div id="bs-msg" style="margin:8px 0"></div>
|
|
@@ -18355,10 +19350,12 @@ window.openBatchShipModal = async (ids) => {
|
|
|
18355
19350
|
}
|
|
18356
19351
|
|
|
18357
19352
|
window.submitBatchShip = async (ids) => {
|
|
18358
|
-
const
|
|
19353
|
+
const choice = document.getElementById('bs-logistics').value
|
|
18359
19354
|
const msg = document.getElementById('bs-msg')
|
|
18360
19355
|
msg.innerHTML = loading$()
|
|
18361
|
-
|
|
19356
|
+
// 自发货:不传 logistics_company_id(后端保持 logistics_id 空 → seller self-fulfill)
|
|
19357
|
+
const body = (choice && choice !== 'self') ? { order_ids: ids, logistics_company_id: choice } : { order_ids: ids }
|
|
19358
|
+
const res = await POST('/orders/batch-ship', body)
|
|
18362
19359
|
if (res.error) { msg.innerHTML = alert$('error', res.error); return }
|
|
18363
19360
|
document.querySelector('.js-modal')?.remove()
|
|
18364
19361
|
toast$(`${t('已发货')} ${res.shipped} / ${ids.length}`)
|
|
@@ -19288,6 +20285,7 @@ async function renderOrderDetail(app, orderId) {
|
|
|
19288
20285
|
|
|
19289
20286
|
<div id="action-area">
|
|
19290
20287
|
${actions ? renderActions(orderId, actions, order, logisticsCompanies) : ''}
|
|
20288
|
+
${sellerDeclineContestPanel(order, orderId, isSeller)}
|
|
19291
20289
|
</div>
|
|
19292
20290
|
|
|
19293
20291
|
${(isBuyer && order.status === 'completed' && product?.id) ? `
|
|
@@ -19309,6 +20307,12 @@ async function renderOrderDetail(app, orderId) {
|
|
|
19309
20307
|
<div id="ret-area-${order.id}" style="font-size:12px;color:#6b7280">${loading$()}</div>
|
|
19310
20308
|
</div>` : ''}
|
|
19311
20309
|
|
|
20310
|
+
${(isSeller && order.status === 'completed') ? `
|
|
20311
|
+
<div class="card" id="ret-card-${order.id}">
|
|
20312
|
+
<div style="font-size:14px;font-weight:600;margin-bottom:6px">↩ ${t('退货处理')}</div>
|
|
20313
|
+
<div id="ret-area-${order.id}" style="font-size:12px;color:#6b7280">${loading$()}</div>
|
|
20314
|
+
</div>` : ''}
|
|
20315
|
+
|
|
19312
20316
|
${((isBuyer || isSeller) && order.status === 'completed') ? `
|
|
19313
20317
|
<div class="card" id="rate-card-${order.id}">
|
|
19314
20318
|
<div style="font-size:14px;font-weight:600;margin-bottom:6px">⭐ ${t('交易评价')}</div>
|
|
@@ -19333,7 +20337,8 @@ async function renderOrderDetail(app, orderId) {
|
|
|
19333
20337
|
`, 'orders')
|
|
19334
20338
|
|
|
19335
20339
|
// Wave B-3: 退货 widget — 异步加载(仅 completed 订单可退)
|
|
19336
|
-
|
|
20340
|
+
// 买家:有退货窗口可申请/查看;卖家:有退货申请时内联查看+处理(accept/reject/received),无申请则隐藏卡
|
|
20341
|
+
if (((isBuyer && Number(product?.return_days || 0) > 0) || isSeller) && order.status === 'completed') {
|
|
19337
20342
|
try { await renderReturnWidgetForOrder(order, product) } catch (e) { console.error(e) }
|
|
19338
20343
|
}
|
|
19339
20344
|
// Wave C-3: 评价 widget
|
|
@@ -19779,7 +20784,13 @@ async function renderReturnWidgetForOrder(order, product) {
|
|
|
19779
20784
|
// P1-5: 订单级直查(取代拉全量列表过滤)
|
|
19780
20785
|
const r = await GET(`/orders/${order.id}/return-request`).catch(() => ({ item: null }))
|
|
19781
20786
|
const mine = r?.item || null
|
|
19782
|
-
const
|
|
20787
|
+
const isSellerView = state.user && state.user.id === order.seller_id
|
|
20788
|
+
// 卖家视角:无退货申请则隐藏整张卡(卖家不申请退货,只在有申请时处理)
|
|
20789
|
+
if (isSellerView && !mine) {
|
|
20790
|
+
const card = area.closest('.card'); if (card) card.style.display = 'none'
|
|
20791
|
+
return
|
|
20792
|
+
}
|
|
20793
|
+
const returnDays = Number(product?.return_days || 0)
|
|
19783
20794
|
const baseTime = order.updated_at || order.created_at
|
|
19784
20795
|
const deadline = new Date(baseTime).getTime() + returnDays * 86400 * 1000
|
|
19785
20796
|
const remainMs = deadline - Date.now()
|
|
@@ -19809,6 +20820,14 @@ async function renderReturnWidgetForOrder(order, product) {
|
|
|
19809
20820
|
${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>` : ''}
|
|
19810
20821
|
</div>
|
|
19811
20822
|
|
|
20823
|
+
${isSellerView && item.status === 'pending' ? `
|
|
20824
|
+
<div style="display:flex;gap:8px;margin-bottom:8px">
|
|
20825
|
+
<button class="btn btn-success btn-sm" style="flex:1;font-size:12px" onclick="decideReturn('${item.id}','accept','${order.id}')">${t('接受退款')}</button>
|
|
20826
|
+
<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>
|
|
20827
|
+
</div>` : ''}
|
|
20828
|
+
${isSellerView && item.status === 'picked_up' ? `
|
|
20829
|
+
<button class="btn btn-success btn-sm" style="width:100%;font-size:12px;margin-bottom:8px" onclick="confirmReturnReceived('${item.id}','${order.id}')">${t('✓ 已收到退货 · 触发退款')}</button>` : ''}
|
|
20830
|
+
|
|
19812
20831
|
<div style="padding:10px;background:#fafafa;border-radius:8px;margin-bottom:8px">
|
|
19813
20832
|
<div style="font-size:11px;font-weight:600;color:#6b7280;margin-bottom:8px">🧾 ${t('协商时间线')} · ${events.length} ${t('条')}</div>
|
|
19814
20833
|
${events.map(ev => buildReturnTimelineEvent(ev, isBuyer)).join('')}
|
|
@@ -20145,26 +21164,32 @@ window.cancelReturnRequest = async (id, orderId) => {
|
|
|
20145
21164
|
function getActions(order, isBuyer, isSeller, isLogistic) {
|
|
20146
21165
|
const s = order.status
|
|
20147
21166
|
const isInPerson = order.fulfillment_mode === 'in_person'
|
|
21167
|
+
const isSelfFulfillSeller = isSeller && !order.logistics_id
|
|
20148
21168
|
// M8 面交订单:买家在 paid / accepted 都可"面交完成"直接结算
|
|
20149
21169
|
if (isInPerson && isBuyer && (s === 'paid' || s === 'accepted')) {
|
|
20150
21170
|
return [{ action: 'confirm_in_person', label: '🤝 面交完成 / 确认收货', style: 'success' }]
|
|
20151
21171
|
}
|
|
20152
21172
|
if (isSeller && s === 'paid')
|
|
20153
|
-
return [
|
|
21173
|
+
return [
|
|
21174
|
+
{ action: 'accept', label: '接单', style: 'success' },
|
|
21175
|
+
{ action: 'decline', label: '拒绝接单', style: 'danger', custom: 'decline' },
|
|
21176
|
+
]
|
|
20154
21177
|
if (isSeller && s === 'accepted' && !isInPerson)
|
|
20155
21178
|
return [{ action: 'ship', label: '确认发货', style: 'success', logisticsSelector: true,
|
|
20156
21179
|
trackingInput: true,
|
|
20157
21180
|
evidencePlaceholder: '包装状态描述 / 货物说明(可选)' }]
|
|
20158
21181
|
if (isSeller && s === 'accepted' && isInPerson)
|
|
20159
21182
|
return [{ action: 'noop_in_person', label: '🤝 面交中(等待买家确认)', style: 'secondary', disabled: true }]
|
|
20160
|
-
if (isLogistic && s === 'shipped')
|
|
21183
|
+
if ((isLogistic || isSelfFulfillSeller) && s === 'shipped')
|
|
20161
21184
|
return [{ action: 'pickup', label: '✅ 确认揽收', style: 'success', needsEvidence: true,
|
|
20162
|
-
noteLabel: '快递单号 *', evidencePlaceholder: '如:SF1234567890'
|
|
20163
|
-
|
|
21185
|
+
noteLabel: '快递单号 *', evidencePlaceholder: '如:SF1234567890',
|
|
21186
|
+
helperText: isSelfFulfillSeller ? '自履约订单:你负责回传揽收/单号,超时仍按卖家责任处理。' : '' }]
|
|
21187
|
+
if ((isLogistic || isSelfFulfillSeller) && s === 'picked_up')
|
|
20164
21188
|
return [{ action: 'transit', label: '🚛 开始运输', style: 'primary' }]
|
|
20165
|
-
if (isLogistic && s === 'in_transit')
|
|
21189
|
+
if ((isLogistic || isSelfFulfillSeller) && s === 'in_transit')
|
|
20166
21190
|
return [{ action: 'deliver', label: '📬 确认投递', style: 'success', needsEvidence: true,
|
|
20167
|
-
noteLabel: '投递凭证', evidencePlaceholder: '门牌照片描述 / 收件人姓名 / 签收时间'
|
|
21191
|
+
noteLabel: '投递凭证', evidencePlaceholder: '门牌照片描述 / 收件人姓名 / 签收时间',
|
|
21192
|
+
helperText: isSelfFulfillSeller ? '自履约投递需留存签收/门牌/交付说明,买家确认后才结算。' : '' }]
|
|
20168
21193
|
if (isBuyer && s === 'delivered')
|
|
20169
21194
|
return [
|
|
20170
21195
|
{ action: 'confirm', label: '确认收货', style: 'success' },
|
|
@@ -20177,19 +21202,22 @@ function getActions(order, isBuyer, isSeller, isLogistic) {
|
|
|
20177
21202
|
function renderActions(orderId, actions, order, logisticsCompanies = []) {
|
|
20178
21203
|
return `
|
|
20179
21204
|
<div class="action-area">
|
|
20180
|
-
<div class="action-title"
|
|
21205
|
+
<div class="action-title">${t('我的操作')}</div>
|
|
20181
21206
|
<div id="action-msg"></div>
|
|
20182
21207
|
${actions.map((a, i) => `
|
|
20183
|
-
${a.
|
|
21208
|
+
${a.custom === 'decline' ? `
|
|
21209
|
+
<button class="btn btn-${a.style}" style="margin-bottom:8px;background:#fff;color:#dc2626;border:1px solid #dc2626"
|
|
21210
|
+
onclick="openDeclineModal('${orderId}')">
|
|
21211
|
+
${t(a.label)}
|
|
21212
|
+
</button>` :
|
|
21213
|
+
a.logisticsSelector ? `
|
|
20184
21214
|
<div class="form-group">
|
|
20185
|
-
<label class="form-label">${t('
|
|
21215
|
+
<label class="form-label">${t('发货方式')}</label>
|
|
20186
21216
|
<select class="form-control" id="logi-select-${i}">
|
|
20187
|
-
<option value="">${t('
|
|
21217
|
+
<option value="self">${t('📦 我自己发货(自提自送)')}</option>
|
|
20188
21218
|
${logisticsCompanies.map(c => `<option value="${escHtml(c.id)}">${escHtml(c.name)}</option>`).join('')}
|
|
20189
21219
|
</select>
|
|
20190
|
-
|
|
20191
|
-
? `<div class="alert alert-warning" style="margin-top:6px;font-size:13px">${t('暂无已注册的物流公司,请先让物流方注册账号')}</div>`
|
|
20192
|
-
: ''}
|
|
21220
|
+
<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>
|
|
20193
21221
|
</div>
|
|
20194
21222
|
${a.trackingInput ? `
|
|
20195
21223
|
<div class="form-group">
|
|
@@ -20198,22 +21226,23 @@ function renderActions(orderId, actions, order, logisticsCompanies = []) {
|
|
|
20198
21226
|
</div>` : ''}
|
|
20199
21227
|
<button class="btn btn-${a.style}" style="margin-bottom:8px"
|
|
20200
21228
|
onclick="handleAction('${orderId}','${a.action}',${i},false,true)">
|
|
20201
|
-
${a.label}
|
|
21229
|
+
${t(a.label)}
|
|
20202
21230
|
</button>` :
|
|
20203
21231
|
a.needsEvidence ? `
|
|
20204
21232
|
<div class="form-group">
|
|
20205
|
-
<label class="form-label">${a.noteLabel || '证据说明'}</label>
|
|
21233
|
+
<label class="form-label">${t(a.noteLabel || '证据说明')}</label>
|
|
20206
21234
|
${a.action === 'pickup'
|
|
20207
|
-
? `<input type="text" class="form-control" id="evid-${i}" placeholder="${a.evidencePlaceholder || '快递单号'}">`
|
|
20208
|
-
: `<textarea class="form-control" id="evid-${i}" placeholder="${a.evidencePlaceholder || ''}"></textarea>`}
|
|
21235
|
+
? `<input type="text" class="form-control" id="evid-${i}" placeholder="${t(a.evidencePlaceholder || '快递单号')}">`
|
|
21236
|
+
: `<textarea class="form-control" id="evid-${i}" placeholder="${t(a.evidencePlaceholder || '')}"></textarea>`}
|
|
21237
|
+
${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>` : ''}
|
|
20209
21238
|
</div>
|
|
20210
21239
|
<button class="btn btn-${a.style}" style="margin-bottom:8px"
|
|
20211
21240
|
onclick="handleAction('${orderId}','${a.action}',${i},true,false)">
|
|
20212
|
-
${a.label}
|
|
21241
|
+
${t(a.label)}
|
|
20213
21242
|
</button>` : `
|
|
20214
21243
|
<button class="btn btn-${a.style}" style="margin-bottom:8px"
|
|
20215
21244
|
onclick="handleAction('${orderId}','${a.action}',${i},false,false)">
|
|
20216
|
-
${a.label}
|
|
21245
|
+
${t(a.label)}
|
|
20217
21246
|
</button>`}
|
|
20218
21247
|
`).join('')}
|
|
20219
21248
|
</div>`
|
|
@@ -20234,6 +21263,13 @@ window.handleAction = async (orderId, action, idx, needsEvidence, hasLogisticsSe
|
|
|
20234
21263
|
return
|
|
20235
21264
|
}
|
|
20236
21265
|
if (action === 'noop_in_person') return
|
|
21266
|
+
const confirmText = {
|
|
21267
|
+
ship: t('确认已经发货?发货后买家将看到物流信息,超时/虚假发货可能进入争议或判责。'),
|
|
21268
|
+
pickup: t('确认已揽收并回传凭证?请确保单号或揽收说明真实可追踪。'),
|
|
21269
|
+
deliver: t('确认已投递?请确保投递凭证真实,买家仍需确认收货后才结算。'),
|
|
21270
|
+
confirm: t('确认收货?escrow 将进入结算流程,无法撤销。'),
|
|
21271
|
+
}[action]
|
|
21272
|
+
if (confirmText && !confirm(confirmText)) return
|
|
20237
21273
|
|
|
20238
21274
|
let evidDesc = (needsEvidence || hasLogisticsSelector)
|
|
20239
21275
|
? (document.getElementById(`evid-${idx}`)?.value?.trim() || '') : ''
|
|
@@ -20252,15 +21288,21 @@ window.handleAction = async (orderId, action, idx, needsEvidence, hasLogisticsSe
|
|
|
20252
21288
|
let logisticsCompanyId = ''
|
|
20253
21289
|
if (hasLogisticsSelector) {
|
|
20254
21290
|
const sel = document.getElementById(`logi-select-${idx}`)
|
|
20255
|
-
|
|
20256
|
-
if (!logisticsCompanyId) { msgEl.innerHTML = alert$('error', t('请选择物流公司')); return }
|
|
20257
|
-
const companyName = sel.options[sel.selectedIndex]?.text || logisticsCompanyId
|
|
21291
|
+
const choice = sel?.value || 'self'
|
|
20258
21292
|
const trackingInp = document.getElementById(`tracking-${idx}`)
|
|
20259
21293
|
const trackingNo = trackingInp?.value?.trim() || ''
|
|
20260
|
-
if (
|
|
20261
|
-
|
|
21294
|
+
if (choice === 'self') {
|
|
21295
|
+
// 自发货:不绑定物流公司(logistics_id 留空 → seller self-fulfill 流转)
|
|
21296
|
+
logisticsCompanyId = ''
|
|
21297
|
+
evidDesc = trackingNo
|
|
21298
|
+
? `${t('快递单号:')}${trackingNo}${t('(自己发货)')}`
|
|
21299
|
+
: t('卖家自己发货(自提自送)—— 后续由你揽收 / 运输 / 送达')
|
|
20262
21300
|
} else {
|
|
20263
|
-
|
|
21301
|
+
logisticsCompanyId = choice
|
|
21302
|
+
const companyName = sel.options[sel.selectedIndex]?.text || logisticsCompanyId
|
|
21303
|
+
evidDesc = trackingNo
|
|
21304
|
+
? `${t('快递单号:')}${trackingNo} · ${companyName}`
|
|
21305
|
+
: `${t('已交付物流公司:')}${companyName}${t(',快递单号待物流揽收后回传')}`
|
|
20264
21306
|
}
|
|
20265
21307
|
}
|
|
20266
21308
|
|
|
@@ -20282,6 +21324,107 @@ window.handleAction = async (orderId, action, idx, needsEvidence, hasLogisticsSe
|
|
|
20282
21324
|
}
|
|
20283
21325
|
}
|
|
20284
21326
|
|
|
21327
|
+
// ─── 卖家拒单(decline) + 临时判责举证(contest_decline)─── RFC-007 stage 2/3/5 ───────────────
|
|
21328
|
+
// reason code 元数据(与后端 orders-action.ts DECLINE_REASON_CODES / OBJECTIVE_DECLINE_REASONS 对齐)。
|
|
21329
|
+
// objective(客观声称)→ 临时判责,需在举证窗口内发起仲裁,不是自动免责;subjective(主观)→ 立即按卖家违约、买家退款。
|
|
21330
|
+
const DECLINE_REASONS = [
|
|
21331
|
+
{ code: 'stock_consumed_concurrent', objective: true, zh: '并发售罄(库存被同时下单耗尽)', en: 'Stock consumed by a concurrent order' },
|
|
21332
|
+
{ code: 'stale_price_snapshot', objective: true, zh: '价格快照过期(下单价已失效)', en: 'Stale price snapshot' },
|
|
21333
|
+
{ code: 'force_majeure', objective: true, zh: '不可抗力', en: 'Force majeure' },
|
|
21334
|
+
{ code: 'price_regret', objective: false, zh: '价格反悔(不想按此价卖)', en: 'Price regret' },
|
|
21335
|
+
{ code: 'cherry_pick', objective: false, zh: '挑单(选择性拒单)', en: 'Cherry-picking' },
|
|
21336
|
+
{ code: 'other', objective: false, zh: '其他(主观)', en: 'Other (subjective)' },
|
|
21337
|
+
]
|
|
21338
|
+
function declineReasonOptions() {
|
|
21339
|
+
const en = window._lang === 'en'
|
|
21340
|
+
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('')
|
|
21341
|
+
}
|
|
21342
|
+
// 选中 reason 后的诚实后果提示(主观 vs 客观,绝不把客观拒单说成自动免责)
|
|
21343
|
+
window.onDeclineReasonChange = () => {
|
|
21344
|
+
const sel = document.getElementById('decline-reason')
|
|
21345
|
+
const note = document.getElementById('decline-consequence')
|
|
21346
|
+
if (!sel || !note) return
|
|
21347
|
+
const objective = sel.selectedOptions[0]?.dataset?.objective === '1'
|
|
21348
|
+
if (!sel.value) { note.innerHTML = ''; return }
|
|
21349
|
+
note.innerHTML = objective
|
|
21350
|
+
? `<div style="background:#fffbeb;border:1px solid #fcd34d;color:#92400e;border-radius:8px;padding:10px;font-size:12px;line-height:1.6">⚠️ ${t('客观理由:订单将先转入【临时判责·卖家违约】并冻结结算。你需在举证窗口内发起仲裁举证翻案 —— 这不是自动免责;窗口过期未举证将按违约终结、买家退款。')}</div>`
|
|
21351
|
+
: `<div style="background:#fef2f2;border:1px solid #fca5a5;color:#991b1b;border-radius:8px;padding:10px;font-size:12px;line-height:1.6">⚠️ ${t('主观理由:将立即按【卖家违约】处理,买家全额退款。此操作不可撤销。')}</div>`
|
|
21352
|
+
}
|
|
21353
|
+
window.openDeclineModal = (orderId) => {
|
|
21354
|
+
const en = window._lang === 'en'
|
|
21355
|
+
_openModal(`
|
|
21356
|
+
<div style="max-width:440px">
|
|
21357
|
+
<h3 style="font-size:16px;font-weight:700;margin-bottom:6px">${t('拒绝接单')}</h3>
|
|
21358
|
+
<div style="font-size:12px;color:#6b7280;margin-bottom:12px">${t('拒单会影响买家。请如实选择理由 —— 系统按理由区分【主观违约】与【客观临时判责】,后者需举证。')}</div>
|
|
21359
|
+
<div class="form-group">
|
|
21360
|
+
<label class="form-label">${t('拒单理由')} <span style="color:#dc2626">*</span></label>
|
|
21361
|
+
<select class="form-control" id="decline-reason" onchange="onDeclineReasonChange()">
|
|
21362
|
+
<option value="">${en ? '— select a reason —' : '— 请选择理由 —'}</option>
|
|
21363
|
+
${declineReasonOptions()}
|
|
21364
|
+
</select>
|
|
21365
|
+
</div>
|
|
21366
|
+
<div id="decline-consequence" style="margin-bottom:10px"></div>
|
|
21367
|
+
<div class="form-group">
|
|
21368
|
+
<label class="form-label">${t('说明')} <span style="font-size:11px;color:#9ca3af;font-weight:400">${t('(可选)')}</span></label>
|
|
21369
|
+
<textarea class="form-control" id="decline-notes" rows="2" placeholder="${t('补充说明(客观理由建议写清,便于后续举证)')}"></textarea>
|
|
21370
|
+
</div>
|
|
21371
|
+
<div id="decline-msg"></div>
|
|
21372
|
+
<div style="display:flex;gap:8px;margin-top:8px">
|
|
21373
|
+
<button class="btn btn-outline" style="flex:1" onclick="closeModal()">${t('取消')}</button>
|
|
21374
|
+
<button class="btn btn-danger" style="flex:1;background:#dc2626;border-color:#dc2626;color:#fff" onclick="submitDecline('${orderId}')">${t('确认拒单')}</button>
|
|
21375
|
+
</div>
|
|
21376
|
+
</div>`)
|
|
21377
|
+
}
|
|
21378
|
+
window.submitDecline = async (orderId) => {
|
|
21379
|
+
const code = document.getElementById('decline-reason')?.value || ''
|
|
21380
|
+
const notes = document.getElementById('decline-notes')?.value?.trim() || ''
|
|
21381
|
+
const msg = document.getElementById('decline-msg')
|
|
21382
|
+
if (!code) { if (msg) msg.innerHTML = alert$('error', t('请选择拒单理由')); return }
|
|
21383
|
+
if (msg) msg.innerHTML = `<div class="alert alert-info"><span class="spinner"></span>${t('处理中...')}</div>`
|
|
21384
|
+
const res = await POST(`/orders/${orderId}/action`, { action: 'decline', decline_reason_code: code, notes })
|
|
21385
|
+
if (res.error) { if (msg) msg.innerHTML = alert$('error', res.error); return }
|
|
21386
|
+
closeModal()
|
|
21387
|
+
toast$(res.outcome === 'fault_seller_provisional'
|
|
21388
|
+
? t('已拒单 — 临时判责,请在举证窗口内发起仲裁')
|
|
21389
|
+
: t('已拒单 — 已按卖家违约处理,买家将获退款'), 'success')
|
|
21390
|
+
setTimeout(() => renderOrderDetail(document.getElementById('app'), orderId), 1000)
|
|
21391
|
+
}
|
|
21392
|
+
// 临时判责举证面板:仅卖家、订单 fault_seller + decline_objective_pending=1 + 未结算时显示
|
|
21393
|
+
function sellerDeclineContestPanel(order, orderId, isSeller) {
|
|
21394
|
+
if (!isSeller || order.status !== 'fault_seller' || Number(order.decline_objective_pending) !== 1 || order.settled_fault_at) return ''
|
|
21395
|
+
const contested = Number(order.decline_contested) === 1
|
|
21396
|
+
const deadline = order.decline_contest_deadline ? fmtTime(order.decline_contest_deadline) : ''
|
|
21397
|
+
if (contested) {
|
|
21398
|
+
return `<div class="card" style="padding:14px;margin-top:10px;border-left:3px solid #4f46e5;background:#eef2ff">
|
|
21399
|
+
<div style="font-size:13px;font-weight:600;color:#3730a3;margin-bottom:4px">⚖️ ${t('已发起仲裁举证')}</div>
|
|
21400
|
+
<div style="font-size:12px;color:#374151;line-height:1.6">${t('自动终结已暂停,等待仲裁员裁决:维持→免责全退+退质押;驳回→按违约结算。')}</div>
|
|
21401
|
+
</div>`
|
|
21402
|
+
}
|
|
21403
|
+
return `<div class="card" style="padding:14px;margin-top:10px;border-left:3px solid #d97706;background:#fffbeb">
|
|
21404
|
+
<div style="font-size:13px;font-weight:600;color:#92400e;margin-bottom:4px">⏳ ${t('临时判责 — 举证窗口开放')}</div>
|
|
21405
|
+
<div style="font-size:12px;color:#7f1d1d;line-height:1.6;margin-bottom:8px">
|
|
21406
|
+
${t('你以客观理由拒单,订单暂判【卖家违约】但尚未结算。你可在窗口内发起人工仲裁举证翻案 —— 这不是自动免责。')}
|
|
21407
|
+
${deadline ? `<br><strong>${t('举证截止')}:</strong> ${escHtml(deadline)} · ${t('过期未举证将按违约终结、买家退款。')}` : ''}
|
|
21408
|
+
</div>
|
|
21409
|
+
<div class="form-group">
|
|
21410
|
+
<label class="form-label">${t('举证说明')} <span style="color:#dc2626">*</span></label>
|
|
21411
|
+
<textarea class="form-control" id="contest-evidence" rows="3" placeholder="${t('客观说明拒单理由的证据(如并发订单号、价格变更时间、不可抗力凭证)')}"></textarea>
|
|
21412
|
+
</div>
|
|
21413
|
+
<div id="contest-msg"></div>
|
|
21414
|
+
<button class="btn btn-primary" style="width:100%" onclick="submitContestDecline('${orderId}')">${t('提交举证 / 发起仲裁')}</button>
|
|
21415
|
+
</div>`
|
|
21416
|
+
}
|
|
21417
|
+
window.submitContestDecline = async (orderId) => {
|
|
21418
|
+
const evid = document.getElementById('contest-evidence')?.value?.trim() || ''
|
|
21419
|
+
const msg = document.getElementById('contest-msg')
|
|
21420
|
+
if (!evid) { if (msg) msg.innerHTML = alert$('error', t('请填写举证说明')); return }
|
|
21421
|
+
if (msg) msg.innerHTML = `<div class="alert alert-info"><span class="spinner"></span>${t('提交中...')}</div>`
|
|
21422
|
+
const res = await POST(`/orders/${orderId}/action`, { action: 'contest_decline', evidence_description: evid })
|
|
21423
|
+
if (res.error) { if (msg) msg.innerHTML = alert$('error', res.error); return }
|
|
21424
|
+
toast$(t('已发起仲裁举证 — 等待仲裁员裁决'), 'success')
|
|
21425
|
+
setTimeout(() => renderOrderDetail(document.getElementById('app'), orderId), 1000)
|
|
21426
|
+
}
|
|
21427
|
+
|
|
20285
21428
|
// S8: 检查买家是否完成首单,若是 + ShareCtx 还有 sponsor → 一次性致谢 toast + 清 ctx
|
|
20286
21429
|
async function maybeThankSponsorAndClear() {
|
|
20287
21430
|
const ctx = readShareCtx()
|
|
@@ -21765,48 +22908,61 @@ async function renderSeller(app) {
|
|
|
21765
22908
|
}
|
|
21766
22909
|
|
|
21767
22910
|
app.innerHTML = shell(loading$(), 'seller')
|
|
21768
|
-
const [productsRaw, ordersRaw, mySkillsRaw, quotaRaw, insightsRaw] = await Promise.all([
|
|
22911
|
+
const [productsRaw, ordersRaw, mySkillsRaw, quotaRaw, insightsRaw, returnsRaw] = await Promise.all([
|
|
21769
22912
|
GET('/my-products'),
|
|
21770
22913
|
GET('/orders'),
|
|
21771
22914
|
GET('/skills/mine'),
|
|
21772
22915
|
GET('/seller/quota-status'),
|
|
21773
22916
|
GET('/seller/insights').catch(() => null),
|
|
22917
|
+
GET('/return-requests?role=seller').catch(() => ({ items: [] })),
|
|
21774
22918
|
])
|
|
21775
22919
|
const mySkills = Array.isArray(mySkillsRaw) ? mySkillsRaw : []
|
|
21776
22920
|
const orders = Array.isArray(ordersRaw) ? ordersRaw : []
|
|
21777
22921
|
const products = Array.isArray(productsRaw) ? productsRaw : []
|
|
21778
22922
|
const quota = (quotaRaw && !quotaRaw.error) ? quotaRaw : null
|
|
21779
22923
|
const insights = (insightsRaw && !insightsRaw.error) ? insightsRaw : null
|
|
22924
|
+
const returns = Array.isArray(returnsRaw?.items) ? returnsRaw.items : []
|
|
21780
22925
|
|
|
21781
22926
|
const pendingOrders = orders.filter(o => ['paid', 'accepted'].includes(o.status) && o.seller_id === state.user.id)
|
|
22927
|
+
const paidOrders = orders.filter(o => o.status === 'paid' && o.seller_id === state.user.id)
|
|
22928
|
+
const acceptedOrders = orders.filter(o => o.status === 'accepted' && o.seller_id === state.user.id)
|
|
21782
22929
|
const myProducts = products
|
|
21783
22930
|
|
|
21784
22931
|
// KPI(基于最近 50 单的派生统计)+ 今日维度
|
|
21785
22932
|
const mySoldOrders = orders.filter(o => o.seller_id === state.user.id)
|
|
21786
22933
|
const today = new Date().toISOString().slice(0, 10)
|
|
21787
22934
|
const todayOrders = mySoldOrders.filter(o => (o.created_at || '').startsWith(today))
|
|
21788
|
-
const
|
|
22935
|
+
const kpiPaid = paidOrders.length
|
|
22936
|
+
const kpiAccepted = acceptedOrders.length
|
|
21789
22937
|
const kpiInTransit = mySoldOrders.filter(o => ['shipped','picked_up','in_transit','delivered'].includes(o.status)).length
|
|
22938
|
+
const kpiDisputes = mySoldOrders.filter(o => o.status === 'disputed').length
|
|
22939
|
+
const kpiProvisionalDeclines = mySoldOrders.filter(o => o.status === 'fault_seller' && Number(o.decline_objective_pending) === 1 && !o.settled_fault_at).length
|
|
22940
|
+
const kpiReturnExceptions = returns.filter(r => ['pending','picked_up','rejected','accepted_pickup_pending'].includes(r.status)).length
|
|
22941
|
+
const kpiExceptions = kpiDisputes + kpiProvisionalDeclines + kpiReturnExceptions
|
|
21790
22942
|
const kpiSales = mySoldOrders.filter(o => o.status === 'completed').reduce((s,o) => s + Number(o.total_amount || 0), 0)
|
|
21791
22943
|
const kpiTodayCount = todayOrders.length
|
|
21792
22944
|
const kpiTodaySales = todayOrders.filter(o => o.status === 'completed').reduce((s,o) => s + Number(o.total_amount || 0), 0)
|
|
21793
22945
|
const sellerKpis = `
|
|
21794
|
-
<div style="display:grid;grid-template-columns:repeat(
|
|
22946
|
+
<div style="display:grid;grid-template-columns:repeat(5,1fr);gap:6px;margin-bottom:14px">
|
|
21795
22947
|
<div class="card" style="padding:10px;text-align:center;background:linear-gradient(135deg,#eff6ff,#dbeafe);border-color:#bfdbfe">
|
|
21796
22948
|
<div style="font-size:18px;font-weight:800;color:#1d4ed8">${kpiTodayCount}</div>
|
|
21797
22949
|
<div style="font-size:10px;color:#6b7280;margin-top:2px">${t('今日订单')}</div>
|
|
21798
22950
|
</div>
|
|
21799
|
-
<div class="card" style="padding:10px;text-align:center;background:${
|
|
21800
|
-
<div style="font-size:18px;font-weight:800;color:${
|
|
22951
|
+
<div class="card" style="padding:10px;text-align:center;background:${kpiPaid > 0 ? 'linear-gradient(135deg,#fef3c7,#fde68a)' : '#f9fafb'};border-color:${kpiPaid > 0 ? '#fcd34d' : '#e5e7eb'}">
|
|
22952
|
+
<div style="font-size:18px;font-weight:800;color:${kpiPaid > 0 ? '#b45309' : '#9ca3af'}">${kpiPaid}</div>
|
|
22953
|
+
<div style="font-size:10px;color:#6b7280;margin-top:2px">${t('待接单')}</div>
|
|
22954
|
+
</div>
|
|
22955
|
+
<div class="card" style="padding:10px;text-align:center;background:${kpiAccepted > 0 ? 'linear-gradient(135deg,#fff7ed,#fed7aa)' : '#f9fafb'};border-color:${kpiAccepted > 0 ? '#fdba74' : '#e5e7eb'}">
|
|
22956
|
+
<div style="font-size:18px;font-weight:800;color:${kpiAccepted > 0 ? '#c2410c' : '#9ca3af'}">${kpiAccepted}</div>
|
|
21801
22957
|
<div style="font-size:10px;color:#6b7280;margin-top:2px">${t('待发货')}</div>
|
|
21802
22958
|
</div>
|
|
21803
22959
|
<div class="card" style="padding:10px;text-align:center">
|
|
21804
22960
|
<div style="font-size:18px;font-weight:800;color:#374151">${kpiInTransit}</div>
|
|
21805
22961
|
<div style="font-size:10px;color:#6b7280;margin-top:2px">${t('在途')}</div>
|
|
21806
22962
|
</div>
|
|
21807
|
-
<div class="card" style="padding:10px;text-align:center;background
|
|
21808
|
-
<div style="font-size:
|
|
21809
|
-
<div style="font-size:10px;color:#6b7280;margin-top:2px">${t('
|
|
22963
|
+
<div class="card" style="padding:10px;text-align:center;background:${kpiExceptions > 0 ? 'linear-gradient(135deg,#fef2f2,#fee2e2)' : '#f9fafb'};border-color:${kpiExceptions > 0 ? '#fca5a5' : '#e5e7eb'}">
|
|
22964
|
+
<div style="font-size:18px;font-weight:800;color:${kpiExceptions > 0 ? '#dc2626' : '#9ca3af'}">${kpiExceptions}</div>
|
|
22965
|
+
<div style="font-size:10px;color:#6b7280;margin-top:2px">${t('异常')}</div>
|
|
21810
22966
|
</div>
|
|
21811
22967
|
</div>
|
|
21812
22968
|
`
|
|
@@ -21888,9 +23044,7 @@ async function renderSeller(app) {
|
|
|
21888
23044
|
</div>
|
|
21889
23045
|
` : ''
|
|
21890
23046
|
|
|
21891
|
-
const
|
|
21892
|
-
? `<div class="empty" style="padding:24px"><div class="empty-icon">✅</div><div class="empty-text">${t('暂无待处理订单')}</div></div>`
|
|
21893
|
-
: batchAcceptBar + batchShipBar + pendingOrders.map(o => `
|
|
23047
|
+
const pendingOrderRows = (list) => list.map(o => `
|
|
21894
23048
|
<div class="card" onclick="navigate('#order/${o.id}')" style="cursor:pointer">
|
|
21895
23049
|
<div class="order-item">
|
|
21896
23050
|
<div class="order-icon">📦</div>
|
|
@@ -21902,6 +23056,39 @@ async function renderSeller(app) {
|
|
|
21902
23056
|
<div class="order-amount">${o.total_amount} WAZ</div>
|
|
21903
23057
|
</div>
|
|
21904
23058
|
</div>`).join('')
|
|
23059
|
+
const acceptHtml = paidOrders.length === 0
|
|
23060
|
+
? `<div class="empty" style="padding:24px"><div class="empty-icon">✅</div><div class="empty-text">${t('暂无待处理订单')}</div></div>`
|
|
23061
|
+
: batchAcceptBar + pendingOrderRows(paidOrders)
|
|
23062
|
+
const shipHtml = acceptedOrders.length === 0
|
|
23063
|
+
? `<div class="empty" style="padding:18px"><div class="empty-icon">📦</div><div class="empty-text">${t('暂无待发货订单')}</div></div>`
|
|
23064
|
+
: batchShipBar + pendingOrderRows(acceptedOrders)
|
|
23065
|
+
const exceptionOrders = mySoldOrders.filter(o => o.status === 'disputed' || (o.status === 'fault_seller' && Number(o.decline_objective_pending) === 1 && !o.settled_fault_at))
|
|
23066
|
+
const exceptionReturns = returns.filter(r => ['pending','picked_up','rejected','accepted_pickup_pending'].includes(r.status))
|
|
23067
|
+
const exceptionsHtml = (exceptionOrders.length + exceptionReturns.length) === 0
|
|
23068
|
+
? `<div class="empty" style="padding:18px"><div class="empty-icon">✅</div><div class="empty-text">${t('暂无异常待处理')}</div></div>`
|
|
23069
|
+
: `
|
|
23070
|
+
${exceptionOrders.map(o => `
|
|
23071
|
+
<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">
|
|
23072
|
+
<div style="display:flex;justify-content:space-between;gap:10px;align-items:center">
|
|
23073
|
+
<div style="min-width:0">
|
|
23074
|
+
<div style="font-size:13px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${escHtml(o.product_title)}</div>
|
|
23075
|
+
<div style="font-size:11px;color:#6b7280;margin-top:2px">${o.status === 'disputed' ? t('争议订单') : t('临时判责拒单')} · ${fmtTime(o.updated_at || o.created_at)}</div>
|
|
23076
|
+
</div>
|
|
23077
|
+
<span style="font-size:11px;color:#dc2626;font-weight:600;white-space:nowrap">${t('查看处理')} →</span>
|
|
23078
|
+
</div>
|
|
23079
|
+
</div>`).join('')}
|
|
23080
|
+
${exceptionReturns.map(r => `
|
|
23081
|
+
<div class="card" onclick="navigate('#order/${r.order_id}')" style="cursor:pointer;border-left:3px solid #0891b2;padding:10px 12px;margin-bottom:8px">
|
|
23082
|
+
<div style="display:flex;justify-content:space-between;gap:10px;align-items:center">
|
|
23083
|
+
<div style="min-width:0">
|
|
23084
|
+
<div style="font-size:13px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${escHtml(r.product_title)}</div>
|
|
23085
|
+
<div style="font-size:11px;color:#6b7280;margin-top:2px">${t('退货处理')} · ${r.refund_amount} WAZ · ${fmtTime(r.created_at)}</div>
|
|
23086
|
+
</div>
|
|
23087
|
+
<span style="font-size:11px;color:#0891b2;font-weight:600;white-space:nowrap">${t('查看订单')} →</span>
|
|
23088
|
+
</div>
|
|
23089
|
+
</div>`).join('')}
|
|
23090
|
+
<button class="btn btn-outline btn-sm" style="width:auto;font-size:12px" onclick="navigate('#returns')">↩ ${t('退货管理')}</button>
|
|
23091
|
+
`
|
|
21905
23092
|
|
|
21906
23093
|
const activeProducts = products.filter(p => p.status === 'active')
|
|
21907
23094
|
const warehouseProducts = products.filter(p => p.status !== 'active' && p.status !== 'deleted')
|
|
@@ -22081,11 +23268,24 @@ async function renderSeller(app) {
|
|
|
22081
23268
|
// 各 sub-tab 内容
|
|
22082
23269
|
const dashboardSection = sellerSubTab === 'dashboard' ? `
|
|
22083
23270
|
${sellerKpis}
|
|
23271
|
+
${sellerRecoveryReminderHTML()}
|
|
22084
23272
|
${stockAlertBanner}
|
|
22085
23273
|
${quotaBanner}
|
|
22086
23274
|
${pendingOrders.length > 0 ? `<div class="alert alert-warning">📬 ${t('你有')} ${pendingOrders.length} ${t('个订单需要处理')}</div>` : ''}
|
|
22087
|
-
<div style="
|
|
22088
|
-
|
|
23275
|
+
<div style="display:grid;grid-template-columns:1fr;gap:10px;margin-bottom:12px">
|
|
23276
|
+
<section>
|
|
23277
|
+
<div style="font-weight:700;margin-bottom:8px">📬 ${t('待接单')}</div>
|
|
23278
|
+
${acceptHtml}
|
|
23279
|
+
</section>
|
|
23280
|
+
<section>
|
|
23281
|
+
<div style="font-weight:700;margin-bottom:8px">📦 ${t('待发货')}</div>
|
|
23282
|
+
${shipHtml}
|
|
23283
|
+
</section>
|
|
23284
|
+
<section>
|
|
23285
|
+
<div style="font-weight:700;margin-bottom:8px">⚠ ${t('退货 · 争议 · 异常')}</div>
|
|
23286
|
+
${exceptionsHtml}
|
|
23287
|
+
</section>
|
|
23288
|
+
</div>
|
|
22089
23289
|
${insightsBlock}
|
|
22090
23290
|
` : ''
|
|
22091
23291
|
|
|
@@ -23058,6 +24258,7 @@ function skillCard(s, context) {
|
|
|
23058
24258
|
<span class="badge badge-green">${t('运行中')}</span>
|
|
23059
24259
|
</div>
|
|
23060
24260
|
<div style="font-size:13px;color:#6b7280;margin-top:8px">${s.description}</div>
|
|
24261
|
+
${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>` : ''}
|
|
23061
24262
|
</div>`
|
|
23062
24263
|
}
|
|
23063
24264
|
// buyer context
|
|
@@ -23078,7 +24279,7 @@ function skillCard(s, context) {
|
|
|
23078
24279
|
|
|
23079
24280
|
const SKILL_CONFIG_HINTS = {
|
|
23080
24281
|
catalog_sync: '目录同步:订阅此 Skill 的买家在搜索时会优先看到你的商品。成交后协议自动给你 0.5% 推荐佣金。',
|
|
23081
|
-
auto_accept: '
|
|
24282
|
+
auto_accept: '⚠️ 自动接单:买家下单后系统自动接受(省去手动「接单」)。注意责任不变:①接单后你仍须按时发货,超时/不发货仍按卖家违约判责;②会跳过「拒绝接单」窗口,订单自动进入已接单后无法再拒单;③不校验库存,售罄商品也会被自动接单,你仍要履约或担责。建议用金额范围/每日上限控制风险。',
|
|
23082
24283
|
price_negotiation: '价格协商:允许买家 Agent 在你设定的折扣范围内自动议价,减少沟通成本。',
|
|
23083
24284
|
quality_guarantee: '质量承诺:额外质押 WAZ 作为品质担保,增强买家信任,适合高客单价商品。',
|
|
23084
24285
|
instant_ship: '极速发货:承诺接单后 24h 内发货,违约自动赔付。适合有充足现货的卖家。',
|
|
@@ -30605,6 +31806,7 @@ async function renderAdminProtocol(app) {
|
|
|
30605
31806
|
${adminLinkCard('📜', t('审计日志'), t('全部 admin 写操作'), '#admin/audit')}
|
|
30606
31807
|
${adminLinkCard('🛑', t('错误监控'), t('24h 趋势 + burst 告警'), '#admin/errors')}
|
|
30607
31808
|
${adminLinkCard('📨', t('Welcome 提交'), t('#welcome 留下的邮箱订阅 + 建议'), '#admin/public-ideas')}
|
|
31809
|
+
${adminLinkCard('🛠️', t('任务建议收件箱'), t('陌生人 / agent 提交的共建任务建议;审阅 → 转正式任务'), '#admin/task-proposals')}
|
|
30608
31810
|
</div>
|
|
30609
31811
|
`, 'admin-protocol')
|
|
30610
31812
|
}
|
|
@@ -33021,7 +34223,7 @@ async function renderMyNotes(app) {
|
|
|
33021
34223
|
<h2 style="font-size:18px;font-weight:700">📝 ${t('我的笔记')}</h2>
|
|
33022
34224
|
${draftCount > 0 ? `<button class="btn btn-outline btn-sm" style="font-size:11px;padding:5px 10px" onclick="openDraftsModal()">📋 ${t('草稿')} ${draftCount}</button>` : ''}
|
|
33023
34225
|
</div>
|
|
33024
|
-
<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>
|
|
33025
34227
|
<div class="card" style="padding:14px;margin-bottom:14px;background:linear-gradient(135deg,#fef3c7,#fff7ed)">
|
|
33026
34228
|
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:8px;text-align:center">
|
|
33027
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>
|