@seasonkoh/webaz 0.1.26 → 0.1.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +2 -2
- package/NOTICE +24 -3
- package/README.md +74 -330
- package/README.zh-CN.md +419 -0
- package/dist/layer0-foundation/L0-2-state-machine/genuine-sale.js +21 -0
- package/dist/layer0-foundation/L0-5-manifest/manifest.js +8 -3
- package/dist/layer1-agent/L1-1-mcp-server/auth.js +13 -1
- package/dist/layer1-agent/L1-1-mcp-server/network-mode.js +69 -0
- package/dist/layer1-agent/L1-1-mcp-server/server.js +270 -82
- package/dist/layer2-business/L2-9-contribution/admin-coordination-ingestion-engine.js +181 -0
- package/dist/layer2-business/L2-9-contribution/admin-coordination-resolver.js +114 -0
- package/dist/layer2-business/L2-9-contribution/admin-coordination-store.js +251 -0
- package/dist/layer2-business/L2-9-contribution/admin-operator-claim-workflow.js +390 -0
- package/dist/layer2-business/L2-9-contribution/build-task-agent-metadata-store.js +24 -0
- package/dist/layer2-business/L2-9-contribution/build-task-participation.js +6 -2
- package/dist/layer2-business/L2-9-contribution/build-task-quota.js +337 -0
- package/dist/layer2-business/L2-9-contribution/build-task-read.js +25 -2
- package/dist/layer2-business/L2-9-contribution/build-tasks-engine.js +57 -7
- package/dist/layer2-business/L2-9-contribution/canonical-contribution-target.js +1 -1
- package/dist/layer2-business/L2-9-contribution/contribution-facts-read.js +66 -0
- package/dist/layer2-business/L2-9-contribution/task-proposal-draft.js +187 -18
- package/dist/layer2-business/L2-9-contribution/task-proposal-store.js +29 -4
- package/dist/ledger.js +1 -1
- package/dist/pwa/admin-audit.js +38 -0
- package/dist/pwa/anti-abuse-thresholds.js +135 -0
- package/dist/pwa/cf-origin-guard.js +33 -0
- package/dist/pwa/contract-fingerprint.js +1 -0
- package/dist/pwa/data/onboarding-cases.js +2 -2
- package/dist/pwa/data/onboarding-quiz.js +1 -1
- package/dist/pwa/economic-participation.js +2 -2
- package/dist/pwa/integration-contract.js +46 -4
- package/dist/pwa/internal/pv-settlement.js +12 -0
- package/dist/pwa/internal/wallet-signer.js +26 -0
- package/dist/pwa/public/app-account.js +977 -0
- package/dist/pwa/public/app-admin.js +608 -0
- package/dist/pwa/public/app-agents.js +63 -0
- package/dist/pwa/public/app-ai.js +2162 -0
- package/dist/pwa/public/app-contribution.js +836 -0
- package/dist/pwa/public/app-discover.js +1296 -0
- package/dist/pwa/public/app-listings.js +226 -0
- package/dist/pwa/public/app-profile.js +1692 -0
- package/dist/pwa/public/app-seller.js +199 -0
- package/dist/pwa/public/app-shop.js +1145 -0
- package/dist/pwa/public/app.js +15075 -23960
- package/dist/pwa/public/i18n.js +31 -28
- package/dist/pwa/public/index.html +11 -1
- package/dist/pwa/public/openapi.json +4851 -2776
- package/dist/pwa/pv-kill-switch.js +31 -0
- package/dist/pwa/routes/admin-admins.js +48 -1
- package/dist/pwa/routes/admin-analytics.js +1 -10
- package/dist/pwa/routes/admin-atomic.js +4 -17
- package/dist/pwa/routes/admin-operator-claims.js +280 -0
- package/dist/pwa/routes/admin-reports.js +4 -26
- package/dist/pwa/routes/admin-tokenomics.js +2 -76
- package/dist/pwa/routes/admin-users-lifecycle.js +1 -14
- package/dist/pwa/routes/admin-users-query.js +23 -1
- package/dist/pwa/routes/admin-wallet-ops.js +1 -1
- package/dist/pwa/routes/agent-grants.js +255 -0
- package/dist/pwa/routes/auth-read.js +1 -5
- package/dist/pwa/routes/auth-register.js +3 -13
- package/dist/pwa/routes/build-task-quota.js +113 -0
- package/dist/pwa/routes/claim-verify.js +15 -11
- package/dist/pwa/routes/contribution-facts.js +18 -0
- package/dist/pwa/routes/dispute-cases.js +5 -4
- package/dist/pwa/routes/growth.js +3 -3
- package/dist/pwa/routes/orders-action.js +27 -10
- package/dist/pwa/routes/orders-create.js +1 -1
- package/dist/pwa/routes/products-meta.js +19 -6
- package/dist/pwa/routes/profile-placement.js +1 -1
- package/dist/pwa/routes/promoter.js +10 -29
- package/dist/pwa/routes/public-build-tasks.js +5 -1
- package/dist/pwa/routes/public-utils.js +9 -12
- package/dist/pwa/routes/referral.js +5 -26
- package/dist/pwa/routes/rewards-apply.js +3 -2
- package/dist/pwa/routes/share-redirects.js +1 -1
- package/dist/pwa/routes/shareables-interactions.js +2 -1
- package/dist/pwa/routes/task-proposals.js +85 -9
- package/dist/pwa/routes/users-public.js +1 -4
- package/dist/pwa/routes/wallet-read.js +2 -14
- package/dist/pwa/routes/webauthn.js +7 -2
- package/dist/pwa/server-schema.js +9 -0
- package/dist/pwa/server.js +319 -2034
- package/dist/runtime/agent-grant-scopes.js +128 -0
- package/dist/runtime/agent-grant-verifier.js +67 -0
- package/dist/runtime/agent-pairing.js +60 -0
- package/dist/runtime/apply-webaz-runtime-schema.js +15 -0
- package/dist/runtime/webaz-schema-helpers.js +1848 -0
- package/dist/settlement-math.js +3 -3
- package/dist/version.js +6 -4
- package/package.json +43 -8
- package/dist/index.js +0 -182
- package/dist/pwa/public/docs/ECONOMIC-MODEL.md +0 -287
- package/dist/pwa/public/docs/INTEGRATOR.md +0 -67
- package/dist/pwa/public/docs/META-RULES-FULL.md +0 -543
- package/dist/test-dispute.js +0 -153
- package/dist/test-manifest.js +0 -61
- package/dist/test-mcp-tools.js +0 -135
- package/dist/test-reputation.js +0 -116
- package/dist/test-skill-market.js +0 -101
|
@@ -0,0 +1,1692 @@
|
|
|
1
|
+
// WebAZ — Profile / Social / My-Home domain (classic multi-script split, slice G / app-profile.js)
|
|
2
|
+
//
|
|
3
|
+
// Loaded as a CLASSIC script in this order (index.html):
|
|
4
|
+
// i18n → app-admin → app-contribution → app-ai → app-discover → app-profile → app-account → app-shop → app-listings → app-seller → app.js (source of truth: index.html)
|
|
5
|
+
// (after app-discover.js, since profile/nearby feeds call productCardHtml/
|
|
6
|
+
// discoverGoodsTabs which live there). Top-level functions / window.* handlers
|
|
7
|
+
// are global; pages run on route/click (after app.js loads), so cross-file
|
|
8
|
+
// globals (GET/POST/state/shell/escHtml/navigate/t/toast$/skeleton$/pageHeader/
|
|
9
|
+
// PAGE_HEADER_GRADIENTS/feedActor/feedEmpty/pageHotFeedToggle/copyAnchor/
|
|
10
|
+
// toggleFollow/kpiGrid/card/productCardHtml/...) resolve at call time. No import/export.
|
|
11
|
+
//
|
|
12
|
+
// Pure relocation of: #u/<id> user profile (+ reputation wall, metrics, content
|
|
13
|
+
// tabs, content/shares feeds), #follows, #nearby + nearby feed, and the #me
|
|
14
|
+
// home variants (trusted/seller/buyer/dispatcher). Auth/security, seller
|
|
15
|
+
// workbench/product-edit, cart/order/payment/wallet/status surfaces stay in app.js.
|
|
16
|
+
//
|
|
17
|
+
// No money/order/payment/wallet/status path; no auth/security function moved.
|
|
18
|
+
// No UI/behavior change.
|
|
19
|
+
|
|
20
|
+
// ─── P14.5:用户主页 #u/<user_id> ─────────────────────
|
|
21
|
+
// D2 信誉徽章墙 — 4 维度聚合
|
|
22
|
+
function renderReputationWall(badges) {
|
|
23
|
+
const tiles = []
|
|
24
|
+
if (badges.commercial) {
|
|
25
|
+
const c = badges.commercial
|
|
26
|
+
tiles.push({ icon: c.emoji, label: t('商业 ' + c.label), value: c.score + ' rep', color: c.color, bg: c.color + '15' })
|
|
27
|
+
}
|
|
28
|
+
if (badges.agent) {
|
|
29
|
+
const lvlMap = { legend:{ label:'传奇', color:'#dc2626' }, quality:{ label:'优质', color:'#9333ea' }, trusted:{ label:'信赖', color:'#4f46e5' }, new:{ label:'新手', color:'#9ca3af' } }
|
|
30
|
+
const m = lvlMap[badges.agent.level] || { label: badges.agent.level, color: '#6b7280' }
|
|
31
|
+
// P1.2: score 可能 undefined(非 owner 视角已脱敏)— 用 level 名替代
|
|
32
|
+
const valueStr = badges.agent.score != null ? 'trust ' + badges.agent.score : badges.agent.level
|
|
33
|
+
tiles.push({ icon: '🤖', label: t('Agent ' + m.label), value: valueStr, color: m.color, bg: m.color + '15' })
|
|
34
|
+
}
|
|
35
|
+
if (badges.charity && badges.charity.prestige > 0) {
|
|
36
|
+
const c = badges.charity
|
|
37
|
+
const badgeEmoji = { diamond:'💎', gold:'🥇', silver:'🥈', bronze:'🥉' }[c.badge] || '🌱'
|
|
38
|
+
tiles.push({ icon: badgeEmoji, label: t('慈善 ') + (c.badge || 'none'), value: c.prestige + ' ' + t('威望'), color: '#dc2626', bg: '#fef2f2' })
|
|
39
|
+
}
|
|
40
|
+
if (badges.verifier) {
|
|
41
|
+
tiles.push({ icon: '🔍', label: t('审核员'), value: badges.verifier.tier, color: '#0891b2', bg: '#ecfeff' })
|
|
42
|
+
}
|
|
43
|
+
if (tiles.length === 0) return ''
|
|
44
|
+
return `
|
|
45
|
+
<div style="margin-top:14px;padding-top:14px;border-top:1px solid #e0e7ff">
|
|
46
|
+
<div style="font-size:11px;color:#6b7280;margin-bottom:8px;font-weight:600">🏆 ${t('信誉徽章')}</div>
|
|
47
|
+
<div style="display:flex;flex-wrap:wrap;gap:6px">
|
|
48
|
+
${tiles.map(tile => `
|
|
49
|
+
<div style="display:inline-flex;align-items:center;gap:6px;background:${tile.bg};color:${tile.color};padding:5px 11px;border-radius:99px;font-size:11px;font-weight:600;border:1px solid ${tile.color}30">
|
|
50
|
+
<span style="font-size:14px">${tile.icon}</span>
|
|
51
|
+
<span>${tile.label}</span>
|
|
52
|
+
<span style="opacity:0.7;font-weight:400">· ${tile.value}</span>
|
|
53
|
+
</div>
|
|
54
|
+
`).join('')}
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
`
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 信誉 + 参与度指标块(用 /api/users/:id 已返回的数据,无需额外请求)
|
|
61
|
+
function profileMetricsCard(data) {
|
|
62
|
+
const c = (data.badges && data.badges.commercial) || { emoji: '🌱', label: t('新手'), score: 0 }
|
|
63
|
+
const charity = data.badges && data.badges.charity
|
|
64
|
+
const verifier = data.badges && data.badges.verifier
|
|
65
|
+
const ageDays = data.created_at
|
|
66
|
+
? Math.max(0, Math.floor((Date.now() - new Date(String(data.created_at).replace(' ', 'T') + 'Z').getTime()) / 86400_000))
|
|
67
|
+
: null
|
|
68
|
+
const metric = (label, val) => `
|
|
69
|
+
<div style="text-align:center;flex:1;min-width:0">
|
|
70
|
+
<div style="font-size:18px;font-weight:700;color:#1f2937">${val}</div>
|
|
71
|
+
<div style="font-size:10px;color:#9ca3af;margin-top:2px">${label}</div>
|
|
72
|
+
</div>`
|
|
73
|
+
const extraBadges = [
|
|
74
|
+
charity && (charity.fulfilled > 0 || charity.made > 0) ? `<span style="background:#fef2f2;color:#dc2626;border-radius:99px;padding:3px 9px;font-size:11px">💝 ${t('圆梦')} ${charity.fulfilled || 0} · ${t('许愿')} ${charity.made || 0}</span>` : '',
|
|
75
|
+
verifier ? `<span style="background:#eff6ff;color:#2563eb;border-radius:99px;padding:3px 9px;font-size:11px">🔍 ${t('验证员')} ${escHtml(verifier.tier || '')}</span>` : '',
|
|
76
|
+
].filter(Boolean).join('')
|
|
77
|
+
return `
|
|
78
|
+
<div class="card" style="margin-bottom:14px">
|
|
79
|
+
<div class="card-body">
|
|
80
|
+
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
|
|
81
|
+
<span style="font-size:20px">${c.emoji}</span>
|
|
82
|
+
<div>
|
|
83
|
+
<div style="font-size:14px;font-weight:700;color:${c.color || '#1f2937'}">${t('信誉')}·${escHtml(c.label)}</div>
|
|
84
|
+
<div style="font-size:11px;color:#9ca3af">${t('信誉分')} ${c.score || 0}</div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
<div style="display:flex;gap:6px;padding:8px 0;border-top:1px solid #f3f4f6">
|
|
88
|
+
${metric(t('完成购买'), Number(data.purchase_count || 0))}
|
|
89
|
+
${metric(t('累计售出'), Number(data.sales_count || 0))}
|
|
90
|
+
${ageDays != null ? metric(t('入驻天数'), ageDays) : ''}
|
|
91
|
+
</div>
|
|
92
|
+
${extraBadges ? `<div style="display:flex;flex-wrap:wrap;gap:6px;margin-top:10px">${extraBadges}</div>` : ''}
|
|
93
|
+
</div>
|
|
94
|
+
</div>`
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function renderUserProfile(app, userId) {
|
|
98
|
+
if (!userId) return navigate('#buy')
|
|
99
|
+
// 'me' 别名 → 当前登录用户 ID(保持 URL 友好)
|
|
100
|
+
if (userId === 'me') {
|
|
101
|
+
if (!state.user?.id) return navigate('#login')
|
|
102
|
+
userId = state.user.id
|
|
103
|
+
}
|
|
104
|
+
app.innerHTML = shell(skeleton$('profile'), null)
|
|
105
|
+
|
|
106
|
+
const [data, blockStatus] = await Promise.all([
|
|
107
|
+
GET(`/users/${userId}`),
|
|
108
|
+
GET(`/blocklist/${userId}/status`).catch(() => ({ blocked: false })),
|
|
109
|
+
])
|
|
110
|
+
if (data.error) return void (app.innerHTML = shell(alert$('error', data.error), null))
|
|
111
|
+
|
|
112
|
+
const isOwner = data.is_owner
|
|
113
|
+
// 存视图上下文供 switchProfileTab 用(非 owner 也能切 tab);每次进主页重置到 笔记
|
|
114
|
+
state._profileViewId = data.id
|
|
115
|
+
state._profileIsOwner = isOwner
|
|
116
|
+
state._profileTab = 'shares'
|
|
117
|
+
const isBlocked = !!blockStatus.blocked
|
|
118
|
+
const heroColor = isOwner ? 'linear-gradient(135deg,#eef2ff,#faf5ff)' : 'linear-gradient(135deg,#fff,#f9fafb)'
|
|
119
|
+
const roleEmoji = { buyer: '🛍️', seller: '🏪', verifier: '🔍', logistics: '🚚', arbitrator: '⚖️', admin: '🛡' }[data.role] || '👤'
|
|
120
|
+
|
|
121
|
+
const followBtn = isOwner ? '' : `
|
|
122
|
+
<button class="btn ${data.is_following ? 'btn-outline' : 'btn-primary'} btn-sm"
|
|
123
|
+
style="width:auto;padding:6px 18px"
|
|
124
|
+
data-following="${data.is_following ? '1' : '0'}"
|
|
125
|
+
onclick="toggleFollow('${data.id}', this)">${data.is_following ? t('已关注') : t('关注')}</button>`
|
|
126
|
+
|
|
127
|
+
// 钱包/资产已移除 — 个人主页只保留 P2P 节点的社交/商业分享属性
|
|
128
|
+
// 钱包入口请前往 #profile(个人资料 & 设置)
|
|
129
|
+
|
|
130
|
+
// 小红书风格 hero(居中大头像 + bio + 3 数字 KPI)
|
|
131
|
+
const heroBg = isOwner
|
|
132
|
+
? 'background:linear-gradient(180deg,#fef3f2 0%,#fff 80%)'
|
|
133
|
+
: 'background:linear-gradient(180deg,#fdf4ff 0%,#fff 80%)'
|
|
134
|
+
|
|
135
|
+
app.innerHTML = shell(`
|
|
136
|
+
<div class="card" style="margin-bottom:14px;${heroBg};border:none;padding:24px 16px 16px 16px;text-align:center">
|
|
137
|
+
<!-- 居中大头像 -->
|
|
138
|
+
<div style="width:88px;height:88px;border-radius:50%;background:#fff;display:flex;align-items:center;justify-content:center;font-size:44px;box-shadow:0 4px 12px rgba(0,0,0,0.08);margin:0 auto 12px auto">${roleEmoji}</div>
|
|
139
|
+
|
|
140
|
+
<!-- 名字 + handle -->
|
|
141
|
+
<div style="font-size:20px;font-weight:700;color:#1f2937;margin-bottom:2px">${escHtml(data.name)}</div>
|
|
142
|
+
<div style="font-size:11px;color:#9ca3af;margin-bottom:10px">@${data.id}</div>
|
|
143
|
+
|
|
144
|
+
<!-- bio -->
|
|
145
|
+
${data.bio
|
|
146
|
+
? `<div style="font-size:13px;color:#4b5563;line-height:1.5;max-width:480px;margin:0 auto 12px auto;padding:0 12px">${escHtml(data.bio)}</div>`
|
|
147
|
+
: (isOwner ? `<div style="font-size:12px;color:#9ca3af;margin-bottom:12px;font-style:italic">${t('(设置一句话简介让人记住你)')}</div>` : '')}
|
|
148
|
+
|
|
149
|
+
${data.search_anchor ? `<div style="margin:0 0 12px 0">${searchAnchorBadge(data.search_anchor)}</div>` : ''}
|
|
150
|
+
|
|
151
|
+
<!-- 3 数字 KPI(关注 / 粉丝 / 获赞)小红书风格 -->
|
|
152
|
+
<div style="display:flex;justify-content:center;gap:8px;margin-bottom:14px">
|
|
153
|
+
<a href="#follows" style="flex:1;max-width:120px;color:inherit;text-decoration:none;padding:6px">
|
|
154
|
+
<div style="font-size:18px;font-weight:700;color:#1f2937">${data.following}</div>
|
|
155
|
+
<div style="font-size:11px;color:#9ca3af;margin-top:2px">${t('关注')}</div>
|
|
156
|
+
</a>
|
|
157
|
+
<a href="#follows" style="flex:1;max-width:120px;color:inherit;text-decoration:none;padding:6px">
|
|
158
|
+
<div style="font-size:18px;font-weight:700;color:#1f2937">${data.followers}</div>
|
|
159
|
+
<div style="font-size:11px;color:#9ca3af;margin-top:2px">${t('粉丝')}</div>
|
|
160
|
+
</a>
|
|
161
|
+
<div style="flex:1;max-width:120px;padding:6px">
|
|
162
|
+
<div style="font-size:18px;font-weight:700;color:#1f2937">${data.likes_received || 0}</div>
|
|
163
|
+
<div style="font-size:11px;color:#9ca3af;margin-top:2px">${t('获赞')}</div>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
${data.badges ? `<div style="margin-bottom:12px">${renderReputationWall(data.badges)}</div>` : ''}
|
|
168
|
+
|
|
169
|
+
<!-- 动作按钮居中 -->
|
|
170
|
+
<div style="display:flex;justify-content:center;gap:8px;flex-wrap:wrap">
|
|
171
|
+
${followBtn}
|
|
172
|
+
${isOwner ? `<button class="btn btn-outline btn-sm" style="width:auto;padding:6px 18px" onclick="toggleProfileEditor()">📝 ${t('编辑资料')}</button>` : ''}
|
|
173
|
+
${!isOwner ? `<button class="btn btn-outline btn-sm" style="width:auto;padding:6px 14px;color:${isBlocked ? '#dc2626' : '#9ca3af'}" onclick="toggleBlock('${data.id}', ${isBlocked})">${isBlocked ? '✓ ' + t('已拉黑(点击解除)') : '🚫 ' + t('拉黑')}</button>` : ''}
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
${profileMetricsCard(data)}
|
|
178
|
+
|
|
179
|
+
${isOwner ? `
|
|
180
|
+
<!-- 📝 inline 社交资料编辑(默认折叠)-->
|
|
181
|
+
<div id="profile-editor-card" class="card" style="margin-bottom:16px;display:none">
|
|
182
|
+
<div class="card-body">
|
|
183
|
+
<div style="font-size:11px;font-weight:600;color:#9ca3af;text-transform:uppercase;letter-spacing:0.5px;margin-bottom:10px">📝 ${t('个人资料')}</div>
|
|
184
|
+
|
|
185
|
+
<div style="margin-bottom:14px">
|
|
186
|
+
<div style="font-size:13px;color:#374151;margin-bottom:6px">${t('一句话简介')} <span style="color:#9ca3af;font-size:11px">(${t('≤ 120 字')})</span></div>
|
|
187
|
+
<input class="form-control" id="bio-inp" placeholder="${t('例如:一句话介绍你自己')}" style="font-size:13px" value="${escHtml(data.bio || '')}" maxlength="120">
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
<div style="margin-bottom:14px">
|
|
191
|
+
<div style="font-size:13px;color:#374151;margin-bottom:6px">🔍 ${t('流量口令')} <span style="color:#9ca3af;font-size:11px">(${t('≤ 40 字,字母/数字/汉字/-_.')})</span></div>
|
|
192
|
+
<div style="display:flex;gap:8px">
|
|
193
|
+
<input class="form-control" id="anchor-inp" placeholder="${t('例如:好记的字母或数字组合')}" style="font-size:13px;flex:1" value="${escHtml(data.search_anchor || '')}" maxlength="40">
|
|
194
|
+
<button class="btn btn-primary btn-sm" style="white-space:nowrap" onclick="saveSocialProfile()">${t('保存')}</button>
|
|
195
|
+
</div>
|
|
196
|
+
<p style="font-size:11px;color:#9ca3af;margin-top:4px">${t('在 TikTok / 小红书 口播这个口令,粉丝在 WebAZ 搜它就能找到你')}</p>
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
<div style="display:flex;align-items:center;gap:10px;padding-top:10px;border-top:1px solid #f3f4f6">
|
|
200
|
+
<input type="checkbox" id="feed-visible-tg" style="width:16px;height:16px" ${data.feed_visible ? 'checked' : ''} onchange="toggleFeedVisible(this.checked)">
|
|
201
|
+
<label for="feed-visible-tg" style="font-size:13px;cursor:pointer">${t('在公开动态流显示我的活动')}</label>
|
|
202
|
+
</div>
|
|
203
|
+
<p style="font-size:11px;color:#9ca3af;margin-top:4px;margin-left:26px">${t('关闭后,你的购买/匹配/分润事件不会出现在 发现好物 > 动态')}</p>
|
|
204
|
+
|
|
205
|
+
<div id="social-msg" style="margin-top:10px"></div>
|
|
206
|
+
|
|
207
|
+
<div style="margin-top:14px;padding-top:10px;border-top:1px solid #f3f4f6;font-size:12px">
|
|
208
|
+
<a href="#follows" style="color:#4f46e5;text-decoration:none">→ ${t('我的关注/粉丝')}</a>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
` : ''}
|
|
213
|
+
|
|
214
|
+
${isOwner ? `
|
|
215
|
+
<!-- 🔎 claim 验证活动(社交行为面板)— 仅自己可见 -->
|
|
216
|
+
<div class="card" style="margin-bottom:16px">
|
|
217
|
+
<div class="card-body">
|
|
218
|
+
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px">
|
|
219
|
+
<div style="font-size:11px;font-weight:600;color:#9ca3af;text-transform:uppercase;letter-spacing:0.5px">🔎 ${t('验证活动')}</div>
|
|
220
|
+
<div style="font-size:10px;color:#9ca3af">${t('参与协议级仲裁')}</div>
|
|
221
|
+
</div>
|
|
222
|
+
<div id="claim-verify-inline">
|
|
223
|
+
<div style="font-size:12px;color:#9ca3af;padding:14px;text-align:center">${t('加载中...')}</div>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
` : ''}
|
|
228
|
+
|
|
229
|
+
<!-- 内容分类 tabs(笔记/测评/二手/拍卖/商品 + owner 转发/赞/收藏)-->
|
|
230
|
+
<div id="user-shares-feed" class="card" style="margin-bottom:16px">
|
|
231
|
+
<div class="card-body">
|
|
232
|
+
${profileContentTabs(isOwner, data.role, state._profileTab || 'shares')}
|
|
233
|
+
${isOwner ? `
|
|
234
|
+
<div style="display:flex;gap:6px;margin-bottom:10px">
|
|
235
|
+
<button class="btn btn-outline btn-sm" style="font-size:11px;padding:4px 10px" onclick="quickCreateFromProfile('anchor')" title="${t('选订单 → 添加口令')}">🔗 ${t('添加口令')}</button>
|
|
236
|
+
<button class="btn btn-primary btn-sm" style="font-size:11px;padding:4px 10px" onclick="quickCreateFromProfile('note')" title="${t('选订单 → 创作笔记')}">📝 ${t('创作笔记')}</button>
|
|
237
|
+
</div>` : ''}
|
|
238
|
+
<div id="user-shares-list">
|
|
239
|
+
<div style="font-size:12px;color:#9ca3af;padding:14px;text-align:center">${t('加载中...')}</div>
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
|
|
244
|
+
${!isOwner ? `<div class="alert" style="font-size:12px;color:#6b7280;background:#f9fafb;border-color:#f3f4f6">ℹ️ ${t('查看 TA 在 WebAZ 上发布的所有公开内容')}</div>` : ''}
|
|
245
|
+
`, isOwner ? 'me' : null)
|
|
246
|
+
|
|
247
|
+
// 异步加载内容 feed(按当前 tab 分发)
|
|
248
|
+
loadProfileTab(userId, isOwner, state._profileTab || 'shares')
|
|
249
|
+
// M7.3c 修订:claim 验证 inline 注入(仅自己个人主页)
|
|
250
|
+
if (isOwner) injectClaimVerifyPanel('claim-verify-inline')
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// 内容分类 tab 栏(横向滚动)— 笔记/测评/二手/拍卖/[商品(卖家)] + [转发/赞/收藏(owner)]
|
|
254
|
+
function profileContentTabs(isOwner, role, active) {
|
|
255
|
+
const tabs = [
|
|
256
|
+
{ k: 'shares', label: t('笔记') },
|
|
257
|
+
{ k: 'reviews', label: '⭐ ' + t('测评') },
|
|
258
|
+
{ k: 'secondhand', label: '♻️ ' + t('二手') },
|
|
259
|
+
{ k: 'auctions', label: '🔨 ' + t('拍卖') },
|
|
260
|
+
...(role === 'seller' ? [{ k: 'products', label: '🛍 ' + t('商品') }] : []),
|
|
261
|
+
...(isOwner ? [
|
|
262
|
+
{ k: 'reposted', label: '🔁 ' + t('转发') },
|
|
263
|
+
{ k: 'liked', label: '❤ ' + t('赞') },
|
|
264
|
+
{ k: 'bookmarked', label: '★ ' + t('收藏') },
|
|
265
|
+
] : []),
|
|
266
|
+
]
|
|
267
|
+
return `<div style="display:flex;gap:18px;border-bottom:1px solid #f3f4f6;margin-bottom:12px;overflow-x:auto;-webkit-overflow-scrolling:touch">
|
|
268
|
+
${tabs.map(tb => {
|
|
269
|
+
const on = tb.k === active
|
|
270
|
+
return `<button onclick="switchProfileTab('${tb.k}')" id="ptab-${tb.k}" style="flex:0 0 auto;background:none;border:none;padding:8px 0;cursor:pointer;font-size:14px;font-weight:${on ? '600' : '500'};color:${on ? '#1f2937' : '#9ca3af'};border-bottom:2px solid ${on ? '#dc2626' : 'transparent'};white-space:nowrap">${tb.label}</button>`
|
|
271
|
+
}).join('')}
|
|
272
|
+
</div>`
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// tab 分发:笔记类走 loadUserSharesFeed;测评/二手/拍卖/商品走 loadUserContentFeed
|
|
276
|
+
function loadProfileTab(userId, isOwner, tab) {
|
|
277
|
+
if (['reviews', 'secondhand', 'auctions', 'products'].includes(tab)) {
|
|
278
|
+
return loadUserContentFeed(userId, tab)
|
|
279
|
+
}
|
|
280
|
+
return loadUserSharesFeed(userId, isOwner, tab)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// 测评/二手/拍卖/商品 内容加载
|
|
284
|
+
async function loadUserContentFeed(userId, tab) {
|
|
285
|
+
const wrap = document.getElementById('user-shares-list')
|
|
286
|
+
if (!wrap) return
|
|
287
|
+
wrap.innerHTML = `<div style="font-size:12px;color:#9ca3af;padding:14px;text-align:center">${t('加载中...')}</div>`
|
|
288
|
+
const data = await GET(`/users/${userId}/${tab}`).catch(() => ({ items: [] }))
|
|
289
|
+
const items = data.items || []
|
|
290
|
+
if (items.length === 0) {
|
|
291
|
+
const empty = {
|
|
292
|
+
reviews: t('TA 还没写过测评'),
|
|
293
|
+
secondhand: t('TA 没有在售的二手'),
|
|
294
|
+
auctions: t('TA 没有进行中的拍卖'),
|
|
295
|
+
products: t('TA 没有在售商品'),
|
|
296
|
+
}[tab] || t('暂无内容')
|
|
297
|
+
wrap.innerHTML = `<div style="font-size:12px;color:#9ca3af;padding:18px;text-align:center">${empty}</div>`
|
|
298
|
+
return
|
|
299
|
+
}
|
|
300
|
+
const firstImg = (imgsJson) => { try { const a = JSON.parse(imgsJson || '[]'); return Array.isArray(a) && a[0] ? a[0] : null } catch { return null } }
|
|
301
|
+
let html = ''
|
|
302
|
+
if (tab === 'reviews') {
|
|
303
|
+
html = items.map(r => `
|
|
304
|
+
<div onclick="navigate('#order-product/${r.product_id}')" style="padding:10px 4px;border-bottom:1px solid #f3f4f6;cursor:pointer">
|
|
305
|
+
<div style="font-size:13px;font-weight:500;margin-bottom:2px">${'★'.repeat(Number(r.stars) || 0)}<span style="color:#d1d5db">${'★'.repeat(5 - (Number(r.stars) || 0))}</span> <span style="color:#6b7280;font-size:12px">${escHtml(r.product_title || '')}</span></div>
|
|
306
|
+
${r.comment ? `<div style="font-size:12px;color:#4b5563;line-height:1.5">${escHtml(r.comment)}</div>` : ''}
|
|
307
|
+
${r.reply ? `<div style="font-size:11px;color:#9ca3af;margin-top:4px;padding-left:8px;border-left:2px solid #e5e7eb">${t('卖家回复')}: ${escHtml(r.reply)}</div>` : ''}
|
|
308
|
+
<div style="font-size:10px;color:#9ca3af;margin-top:4px">${fmtTime(r.created_at)}</div>
|
|
309
|
+
</div>`).join('')
|
|
310
|
+
} else if (tab === 'secondhand') {
|
|
311
|
+
const CG = { brand_new: t('全新'), like_new: t('几乎全新'), lightly_used: t('轻度使用'), well_used: t('使用明显'), heavily_used: t('重度使用') }
|
|
312
|
+
html = `<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">${items.map(s => {
|
|
313
|
+
const img = firstImg(s.images)
|
|
314
|
+
return `<div onclick="navigate('#secondhand/item/${s.id}')" style="background:#fff;border:1px solid #f3f4f6;border-radius:10px;overflow:hidden;cursor:pointer">
|
|
315
|
+
${img ? `<img src="${escHtml(img)}" style="width:100%;aspect-ratio:1;object-fit:cover;display:block" onerror="this.style.display='none'">` : `<div style="aspect-ratio:1;background:#f9fafb;display:flex;align-items:center;justify-content:center;font-size:32px">♻️</div>`}
|
|
316
|
+
<div style="padding:8px">
|
|
317
|
+
<div style="font-size:12px;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escHtml(s.title)}</div>
|
|
318
|
+
<div style="font-size:13px;font-weight:700;color:#dc2626;margin-top:2px">${s.price} WAZ</div>
|
|
319
|
+
<div style="font-size:10px;color:#9ca3af;margin-top:2px">${CG[s.condition_grade] || s.condition_grade}${s.status === 'reserved' ? ' · ' + t('已预订') : ''}</div>
|
|
320
|
+
</div>
|
|
321
|
+
</div>`
|
|
322
|
+
}).join('')}</div>`
|
|
323
|
+
} else if (tab === 'auctions') {
|
|
324
|
+
html = items.map(a => `
|
|
325
|
+
<div onclick="navigate('#auction/${a.id}')" style="display:flex;justify-content:space-between;align-items:center;padding:10px 4px;border-bottom:1px solid #f3f4f6;cursor:pointer">
|
|
326
|
+
<div style="flex:1;min-width:0">
|
|
327
|
+
<div style="font-size:13px;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">🔨 ${escHtml(a.title)}</div>
|
|
328
|
+
<div style="font-size:11px;color:#6b7280">${t('当前价')} ${a.current_price} WAZ · ${a.bid_count || 0} ${t('次出价')}</div>
|
|
329
|
+
</div>
|
|
330
|
+
<span style="font-size:13px;color:#9ca3af">›</span>
|
|
331
|
+
</div>`).join('')
|
|
332
|
+
} else if (tab === 'products') {
|
|
333
|
+
html = `<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">${items.map(p => {
|
|
334
|
+
const img = firstImg(p.images)
|
|
335
|
+
return `<div onclick="navigate('#order-product/${p.id}')" style="background:#fff;border:1px solid #f3f4f6;border-radius:10px;overflow:hidden;cursor:pointer">
|
|
336
|
+
${img ? `<img src="${escHtml(img)}" style="width:100%;aspect-ratio:1;object-fit:cover;display:block" onerror="this.style.display='none'">` : `<div style="aspect-ratio:1;background:#f9fafb;display:flex;align-items:center;justify-content:center;font-size:32px">${getCategoryIcon(p.category)}</div>`}
|
|
337
|
+
<div style="padding:8px">
|
|
338
|
+
<div style="font-size:12px;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escHtml(p.title)}</div>
|
|
339
|
+
<div style="font-size:13px;font-weight:700;color:#1f2937;margin-top:2px">${p.price} WAZ</div>
|
|
340
|
+
<div style="font-size:10px;color:#9ca3af;margin-top:2px">✅ ${p.completion_count || 0} · ❤ ${p.total_likes || 0}</div>
|
|
341
|
+
</div>
|
|
342
|
+
</div>`
|
|
343
|
+
}).join('')}</div>`
|
|
344
|
+
}
|
|
345
|
+
wrap.innerHTML = html
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// 📝 切换个人资料编辑卡
|
|
349
|
+
window.toggleProfileEditor = () => {
|
|
350
|
+
const card = document.getElementById('profile-editor-card')
|
|
351
|
+
if (!card) return
|
|
352
|
+
card.style.display = card.style.display === 'none' ? '' : 'none'
|
|
353
|
+
if (card.style.display !== 'none') card.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// 个人主页 tabs 切换(支持 笔记/测评/二手/拍卖/商品/转发/赞/收藏,owner + 非 owner)
|
|
357
|
+
window.switchProfileTab = (tab) => {
|
|
358
|
+
state._profileTab = tab
|
|
359
|
+
// 视觉 active 切换(遍历所有 ptab-* 按钮,兼容动态 tab 集)
|
|
360
|
+
document.querySelectorAll('[id^="ptab-"]').forEach(el => {
|
|
361
|
+
const active = el.id === 'ptab-' + tab
|
|
362
|
+
el.style.color = active ? '#1f2937' : '#9ca3af'
|
|
363
|
+
el.style.fontWeight = active ? '600' : '500'
|
|
364
|
+
el.style.borderBottomColor = active ? '#dc2626' : 'transparent'
|
|
365
|
+
})
|
|
366
|
+
const userId = state._profileViewId || state.user?.id
|
|
367
|
+
if (userId) loadProfileTab(userId, !!state._profileIsOwner, tab)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// 📺 加载用户内容 feed(shareables + manifests)
|
|
371
|
+
// tab: 'shares' (默认 我的笔记) | 'liked' (我赞过)
|
|
372
|
+
async function loadUserSharesFeed(userId, isOwner, tab) {
|
|
373
|
+
tab = tab || state._profileTab || 'shares'
|
|
374
|
+
state._profileTab = tab
|
|
375
|
+
const wrap = document.getElementById('user-shares-list')
|
|
376
|
+
if (!wrap) return
|
|
377
|
+
wrap.innerHTML = `<div style="font-size:12px;color:#9ca3af;padding:14px;text-align:center">${t('加载中...')}</div>`
|
|
378
|
+
|
|
379
|
+
let url
|
|
380
|
+
if (tab === 'liked') url = `/users/me/liked-shareables`
|
|
381
|
+
else if (tab === 'bookmarked') url = `/users/me/bookmarked-shareables`
|
|
382
|
+
else url = isOwner ? '/shareables/me' : `/users/${userId}/shareables`
|
|
383
|
+
|
|
384
|
+
let shareables = []
|
|
385
|
+
let manifests = []
|
|
386
|
+
try {
|
|
387
|
+
const [s, m] = await Promise.all([
|
|
388
|
+
GET(url).catch(() => ({ shareables: [] })),
|
|
389
|
+
// 仅 shares tab + owner 才拉 manifests(其他 tab 不含 manifest)
|
|
390
|
+
(tab === 'shares' && isOwner) ? GET('/manifests/me').catch(() => ({ manifests: [] })) : Promise.resolve({ manifests: [] }),
|
|
391
|
+
])
|
|
392
|
+
shareables = s.shareables || s.items || []
|
|
393
|
+
manifests = m.manifests || []
|
|
394
|
+
} catch {}
|
|
395
|
+
|
|
396
|
+
// 'shares' (原创) vs 'reposted' (转发别人) — 按 parent_id 前端过滤
|
|
397
|
+
// 数据源都是 /shareables/me,差别在 parent_id 字段
|
|
398
|
+
if (tab === 'shares') shareables = shareables.filter(s => !s.parent_id)
|
|
399
|
+
else if (tab === 'reposted') shareables = shareables.filter(s => s.parent_id)
|
|
400
|
+
|
|
401
|
+
const total = shareables.length + manifests.length
|
|
402
|
+
if (total === 0) {
|
|
403
|
+
const emptyMsg =
|
|
404
|
+
tab === 'reposted' ? t('还没转发过任何笔记 — 在他人笔记页点 🔁 转发') :
|
|
405
|
+
tab === 'liked' ? t('还没赞过任何笔记 — 看到喜欢的就点 ❤') :
|
|
406
|
+
tab === 'bookmarked' ? t('还没收藏任何笔记 — 看到想保存的就点 ★') :
|
|
407
|
+
(isOwner ? t('还没有原创内容 — 添加 YouTube/TikTok/小红书 链接或在 WebAZ 直接创作') : t('TA 还没发布任何内容'))
|
|
408
|
+
wrap.innerHTML = `<div style="font-size:12px;color:#9ca3af;padding:18px;text-align:center">${emptyMsg}</div>`
|
|
409
|
+
return
|
|
410
|
+
}
|
|
411
|
+
const platformIcon = (p) => ({ youtube: '📺', tiktok: '🎵', xiaohongshu: '📕', bilibili: '🅱', instagram: '📷', twitter: '🐦' }[p] || '🔗')
|
|
412
|
+
|
|
413
|
+
// 小红书风格双列瀑布流卡片:图 + 标题 + 元信息 + ❤
|
|
414
|
+
const shareableCard = (s) => {
|
|
415
|
+
const isNote = s.type === 'note'
|
|
416
|
+
// 笔记的图:photo_hashes 第一张;外链:thumbnail_url;都没就 emoji 占位
|
|
417
|
+
const hash = isNote && Array.isArray(s.photo_hashes) ? s.photo_hashes[0] : null
|
|
418
|
+
const imgSrc = hash ? `/api/notes/photo/${hash}` : s.thumbnail_url
|
|
419
|
+
const clickAction = isNote
|
|
420
|
+
? `navigate('#note/${s.id}')`
|
|
421
|
+
: (s.external_url ? `window.open('${escHtml(s.external_url)}', '_blank')` : `navigate('#u/${userId}')`)
|
|
422
|
+
return `
|
|
423
|
+
<div onclick="${clickAction}" style="background:#fff;border-radius:10px;overflow:hidden;cursor:pointer;box-shadow:0 1px 3px rgba(0,0,0,0.06);break-inside:avoid;margin-bottom:8px;transition:transform 0.15s;position:relative" onmouseover="this.style.transform='translateY(-2px)'" onmouseout="this.style.transform=''">
|
|
424
|
+
${s.parent_id ? `<div style="position:absolute;top:6px;right:6px;background:rgba(255,255,255,0.9);color:#4f46e5;font-size:10px;font-weight:600;padding:2px 6px;border-radius:99px;z-index:1">🔁 ${t('转发')}</div>` : ''}
|
|
425
|
+
${imgSrc
|
|
426
|
+
? `<img src="${escHtml(imgSrc)}" style="width:100%;height:auto;display:block;background:#f3f4f6" onerror="this.style.display='none'">`
|
|
427
|
+
: `<div style="aspect-ratio:1/1;background:#f9fafb;display:flex;align-items:center;justify-content:center;font-size:48px">${platformIcon(s.external_platform || s.type)}</div>`}
|
|
428
|
+
<div style="padding:8px 10px">
|
|
429
|
+
<div style="font-size:13px;font-weight:500;color:#1f2937;line-height:1.4;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;min-height:36px">${escHtml(s.title || s.external_url || '(无标题)')}</div>
|
|
430
|
+
${isNote && noteAuthBadges(s.badges, 'sm') ? `<div style="margin-top:4px">${noteAuthBadges(s.badges, 'sm')}</div>` : ''}
|
|
431
|
+
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:6px;font-size:10px;color:#9ca3af">
|
|
432
|
+
<span>${platformIcon(s.external_platform || s.type)} ${s.click_count || 0} 👁</span>
|
|
433
|
+
<span>❤ ${s.like_count || 0}</span>
|
|
434
|
+
</div>
|
|
435
|
+
${isOwner ? `
|
|
436
|
+
<div style="display:flex;gap:4px;margin-top:6px">
|
|
437
|
+
<button onclick="event.stopPropagation();showQRModal('${location.origin}/s/${s.id}', '${escHtml((s.title || '').slice(0,30)).replace(/'/g, ''')}')" style="flex:1;font-size:10px;padding:4px 6px;border:1px solid #e5e7eb;background:#fff;border-radius:6px;cursor:pointer">📱 QR</button>
|
|
438
|
+
<button onclick="event.stopPropagation();deleteShareable('${s.id}')" style="flex:1;font-size:10px;padding:4px 6px;border:1px solid #fecaca;color:#dc2626;background:#fff;border-radius:6px;cursor:pointer">${t('删除')}</button>
|
|
439
|
+
</div>` : ''}
|
|
440
|
+
</div>
|
|
441
|
+
</div>`
|
|
442
|
+
}
|
|
443
|
+
const manifestCard = (m) => `
|
|
444
|
+
<div style="background:#fff;border-radius:10px;overflow:hidden;break-inside:avoid;margin-bottom:8px;box-shadow:0 1px 3px rgba(0,0,0,0.06)">
|
|
445
|
+
${m.thumbnail_data_uri ? `<img src="${m.thumbnail_data_uri}" style="width:100%;height:auto;display:block">` : `<div style="aspect-ratio:1/1;background:#e0e7ff;display:flex;align-items:center;justify-content:center;font-size:48px">📦</div>`}
|
|
446
|
+
<div style="padding:8px 10px">
|
|
447
|
+
<div style="font-size:13px;font-weight:500;color:#1f2937;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;min-height:36px">${escHtml(m.title || t('(原生内容)'))}</div>
|
|
448
|
+
<div style="display:flex;justify-content:space-between;margin-top:6px;font-size:10px;color:#9ca3af">
|
|
449
|
+
<span>P2P · ${(m.byte_size/1024).toFixed(0)}KB</span>
|
|
450
|
+
<span>${fmtTime(m.created_at)}</span>
|
|
451
|
+
</div>
|
|
452
|
+
${isOwner ? `<button onclick="takedownManifest('${m.hash}')" style="width:100%;margin-top:6px;font-size:10px;padding:4px;border:1px solid #fecaca;color:#dc2626;background:#fff;border-radius:6px;cursor:pointer">${t('下架')}</button>` : ''}
|
|
453
|
+
</div>
|
|
454
|
+
</div>`
|
|
455
|
+
|
|
456
|
+
// CSS column 实现真瀑布流(不同高度图片自然错开)
|
|
457
|
+
wrap.innerHTML = `
|
|
458
|
+
<div style="column-count:2;column-gap:8px;padding:4px">
|
|
459
|
+
${shareables.map(shareableCard).join('')}
|
|
460
|
+
${manifests.map(manifestCard).join('')}
|
|
461
|
+
</div>`
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// P-AI V2 multi-provider assistant (provider registry/chain, AI IndexedDB, tool
|
|
465
|
+
// calling, LLM transport, task state machine, #ai-recommend/#ai-demo renders +
|
|
466
|
+
// ai* handlers) → moved to app-ai.js (classic split, slice E). aiCallLLM /
|
|
467
|
+
// aiGetProvider stay global there and are still called cross-file from app.js.
|
|
468
|
+
|
|
469
|
+
// ─── P14.5:关注/粉丝双 tab 列表 #follows ─────────────────────
|
|
470
|
+
async function renderFollows(app) {
|
|
471
|
+
if (!state.user) return navigate('#login')
|
|
472
|
+
app.innerHTML = shell(`
|
|
473
|
+
<h1 class="page-title">👥 ${t('我的网络')}</h1>
|
|
474
|
+
<div style="display:flex;gap:6px;margin-bottom:12px;border-bottom:1px solid #e5e7eb">
|
|
475
|
+
<button class="follows-tab" data-k="following" onclick="setFollowsTab('following')" style="background:none;border:none;padding:8px 14px;font-size:13px;cursor:pointer;border-bottom:2px solid ${(state.followsTab||'following')==='following'?'#4f46e5':'transparent'};color:${(state.followsTab||'following')==='following'?'#4f46e5':'#6b7280'};font-weight:${(state.followsTab||'following')==='following'?'600':'400'}">👤 ${t('我关注的')}</button>
|
|
476
|
+
<button class="follows-tab" data-k="followers" onclick="setFollowsTab('followers')" style="background:none;border:none;padding:8px 14px;font-size:13px;cursor:pointer;border-bottom:2px solid ${(state.followsTab||'following')==='followers'?'#4f46e5':'transparent'};color:${(state.followsTab||'following')==='followers'?'#4f46e5':'#6b7280'};font-weight:${(state.followsTab||'following')==='followers'?'600':'400'}">👥 ${t('粉丝')}</button>
|
|
477
|
+
</div>
|
|
478
|
+
<div id="follows-list">${skeleton$('list')}</div>
|
|
479
|
+
`, null)
|
|
480
|
+
|
|
481
|
+
const data = await GET('/follows/me')
|
|
482
|
+
const tab = state.followsTab || 'following'
|
|
483
|
+
const arr = tab === 'following' ? (data.following || []) : (data.followers || [])
|
|
484
|
+
const myFollowing = new Set((data.following || []).map(u => u.id))
|
|
485
|
+
|
|
486
|
+
if (arr.length === 0) {
|
|
487
|
+
document.getElementById('follows-list').innerHTML = `
|
|
488
|
+
<div class="empty" style="padding:40px 20px">
|
|
489
|
+
<div class="empty-icon">${tab === 'following' ? '👥' : '✨'}</div>
|
|
490
|
+
<div class="empty-text">${tab === 'following' ? t('你还没有关注任何人') : t('还没有粉丝 — 多发动态吸引关注吧')}</div>
|
|
491
|
+
${tab === 'following' ? `<button class="btn btn-outline btn-sm" style="margin-top:12px;width:auto" onclick="navigate('#discover')">${t('去发现好物 →')}</button>` : ''}
|
|
492
|
+
</div>`
|
|
493
|
+
return
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const roleEmoji = { buyer: '🛍️', seller: '🏪', verifier: '🔍', logistics: '🚚', arbitrator: '⚖️', admin: '🛡' }
|
|
497
|
+
const rows = arr.map(u => {
|
|
498
|
+
const isFollowing = myFollowing.has(u.id)
|
|
499
|
+
return `
|
|
500
|
+
<div style="display:flex;align-items:center;gap:10px;padding:12px 4px;border-bottom:1px solid #f3f4f6;cursor:pointer" onclick="if(event.target.tagName!=='BUTTON')navigate('#u/${u.id}')">
|
|
501
|
+
<div style="width:40px;height:40px;border-radius:50%;background:#f3f4f6;display:flex;align-items:center;justify-content:center;font-size:18px;flex-shrink:0">${roleEmoji[u.role] || '👤'}</div>
|
|
502
|
+
<div style="flex:1;min-width:0">
|
|
503
|
+
<div style="font-size:14px;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escHtml(u.name)}</div>
|
|
504
|
+
<div style="font-size:11px;color:#9ca3af">@${u.id} · ${fmtTime(u.created_at)}</div>
|
|
505
|
+
</div>
|
|
506
|
+
${u.id === state.user.id ? '' : `
|
|
507
|
+
<button class="btn ${isFollowing ? 'btn-outline' : 'btn-primary'} btn-sm" style="width:auto;padding:5px 14px;font-size:12px"
|
|
508
|
+
data-following="${isFollowing ? '1' : '0'}"
|
|
509
|
+
onclick="event.stopPropagation();toggleFollow('${u.id}', this)">${isFollowing ? t('已关注') : t('关注')}</button>`}
|
|
510
|
+
</div>`
|
|
511
|
+
}).join('')
|
|
512
|
+
document.getElementById('follows-list').innerHTML = rows
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
window.setFollowsTab = (k) => {
|
|
516
|
+
state.followsTab = k
|
|
517
|
+
renderFollows(document.getElementById('app'))
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// ─── P15 雷达扫描 #nearby(QVOD 风格匿名聚合)─────────────────────
|
|
521
|
+
// 雷达扫描 MVP (2026-05-29):scope 范围档(本格/周边/同城/全网) + window 时间窗(24h/7d/30d)
|
|
522
|
+
const NEARBY_SCOPES = [
|
|
523
|
+
{ k: 'cell', label: '本格', needsGeo: true },
|
|
524
|
+
{ k: 'neighbors', label: '周边', needsGeo: true },
|
|
525
|
+
{ k: 'region', label: '同城', needsGeo: false },
|
|
526
|
+
{ k: 'global', label: '全网', needsGeo: false },
|
|
527
|
+
]
|
|
528
|
+
const NEARBY_WINDOWS = [['24h', '24h'], ['7d', '7天'], ['30d', '30天']]
|
|
529
|
+
window.setNearbyScope = (s) => { state._nearbyScope = s; renderNearby(document.getElementById('app')) }
|
|
530
|
+
window.setNearbyWindow = (w) => { state._nearbyWindow = w; renderNearby(document.getElementById('app')) }
|
|
531
|
+
|
|
532
|
+
async function renderNearby(app) {
|
|
533
|
+
if (!state.user) return navigate('#login')
|
|
534
|
+
const esc = (s) => String(s).replace(/'/g, "\\'").replace(/"/g, '"')
|
|
535
|
+
// 默认 global(全网):无需定位、最广、真实活动最早可见(冷启动友好)
|
|
536
|
+
const scope = state._nearbyScope || 'global'
|
|
537
|
+
const win = state._nearbyWindow || '7d'
|
|
538
|
+
app.innerHTML = shell(skeleton$('list'), 'discover')
|
|
539
|
+
|
|
540
|
+
const data = await GET('/nearby?scope=' + encodeURIComponent(scope) + '&window=' + encodeURIComponent(win))
|
|
541
|
+
if (data.error) return void (app.innerHTML = shell(alert$('error', data.error), 'discover'))
|
|
542
|
+
|
|
543
|
+
// 范围档 chips(本格/周边需定位 → 无定位时点击转授权)
|
|
544
|
+
const scopeChips = `<div style="display:flex;gap:6px;margin-bottom:8px;overflow-x:auto;-webkit-overflow-scrolling:touch">
|
|
545
|
+
${NEARBY_SCOPES.map(s => {
|
|
546
|
+
const on = s.k === scope
|
|
547
|
+
return `<button onclick="setNearbyScope('${s.k}')" style="flex:0 0 auto;padding:6px 14px;border-radius:99px;font-size:13px;font-weight:${on ? '600' : '400'};cursor:pointer;border:1px solid ${on ? '#6366f1' : '#e5e7eb'};background:${on ? '#6366f1' : '#fff'};color:${on ? '#fff' : '#374151'}">${t(s.label)}</button>`
|
|
548
|
+
}).join('')}
|
|
549
|
+
</div>`
|
|
550
|
+
// 时间窗 chips
|
|
551
|
+
const winChips = `<div style="display:flex;gap:6px;margin-bottom:10px">
|
|
552
|
+
${NEARBY_WINDOWS.map(([k, l]) => {
|
|
553
|
+
const on = k === win
|
|
554
|
+
return `<button onclick="setNearbyWindow('${k}')" style="flex:0 0 auto;padding:4px 12px;border-radius:6px;font-size:12px;font-weight:${on ? '600' : '400'};cursor:pointer;border:1px solid ${on ? '#0284c7' : '#e5e7eb'};background:${on ? '#e0f2fe' : '#fff'};color:${on ? '#0369a1' : '#6b7280'}">${t(l)}</button>`
|
|
555
|
+
}).join('')}
|
|
556
|
+
</div>`
|
|
557
|
+
const controls = `${scopeChips}${winChips}`
|
|
558
|
+
|
|
559
|
+
// 本格/周边 缺定位 → 授权卡(保留 chips,用户可切同城/全网)
|
|
560
|
+
if (!data.has_location) {
|
|
561
|
+
app.innerHTML = shell(`
|
|
562
|
+
${renderSmartBuyHeader('nearby')}
|
|
563
|
+
${discoverGoodsTabs('nearby')}
|
|
564
|
+
${pageHotFeedToggle('#nearby', '#nearby/feed', { hotIcon: '🛰', hotLabel: t('雷达') })}
|
|
565
|
+
${controls}
|
|
566
|
+
<div class="card" style="background:linear-gradient(135deg,#f0f9ff,#eef2ff);border-color:#bae6fd">
|
|
567
|
+
<div style="text-align:center;padding:14px 0">
|
|
568
|
+
<div style="font-size:48px;margin-bottom:10px">🌐</div>
|
|
569
|
+
<div style="font-size:16px;font-weight:600;margin-bottom:8px">${t('看看你这片在买什么')}</div>
|
|
570
|
+
<p style="font-size:13px;color:#374151;margin-bottom:6px">${t('隐私级聚合 · 不暴露任何买家身份')}</p>
|
|
571
|
+
<p style="font-size:12px;color:#6b7280;margin-bottom:16px">${t('「本格 / 周边」需要定位;「同城 / 全网」无需定位即可看')}</p>
|
|
572
|
+
<button class="btn btn-primary" style="width:auto;padding:10px 24px" onclick="requestLocation()">📍 ${t('授权位置')}</button>
|
|
573
|
+
<div style="margin-top:12px"><button class="btn btn-outline btn-sm" style="width:auto" onclick="setNearbyScope('region')">🏙 ${t('先看同城')}</button></div>
|
|
574
|
+
<p style="font-size:11px;color:#9ca3af;margin-top:14px">${t('我们只存储 0.1° 精度(约 11km × 11km 格子),不会暴露你的精确坐标')}</p>
|
|
575
|
+
</div>
|
|
576
|
+
</div>
|
|
577
|
+
`, 'discover')
|
|
578
|
+
return
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const agg = data.aggregate || {}
|
|
582
|
+
const sufficient = data.sufficient
|
|
583
|
+
const scopeLabel = data.scope_label || ''
|
|
584
|
+
// 下一档(用于"放大范围"CTA)
|
|
585
|
+
const nextScope = { cell: 'neighbors', neighbors: 'region', region: 'global', global: null }[scope]
|
|
586
|
+
const nextLabel = { neighbors: '周边', region: '同城', global: '全网' }[nextScope] || ''
|
|
587
|
+
|
|
588
|
+
// 范围不足(G 强化空状态):清楚提示 + 一键放大 + 邀请(仅本格/周边)
|
|
589
|
+
const renderInsufficientCard = () => {
|
|
590
|
+
const inviteLink = `${location.origin}/r/${state.user?.handle || state.user?.id || ''}`
|
|
591
|
+
const enlargeBtn = nextScope
|
|
592
|
+
? `<button class="btn btn-primary btn-sm" style="width:auto" onclick="setNearbyScope('${nextScope}')">🔭 ${t('放大到')}${t(nextLabel)}</button>`
|
|
593
|
+
: `<button class="btn btn-primary btn-sm" style="width:auto" onclick="navigate('#discover')">🔥 ${t('看全网热门')}</button>`
|
|
594
|
+
const inviteBtns = (scope === 'cell' || scope === 'neighbors')
|
|
595
|
+
? `<button class="btn btn-outline btn-sm" style="width:auto" onclick="showQRModal('${esc(inviteLink)}','📍 ${t('邀请邻居')}')">📱 ${t('邀请邻居')}</button>`
|
|
596
|
+
: ''
|
|
597
|
+
return `
|
|
598
|
+
<div class="card" style="background:#fffbeb;border-color:#fde68a">
|
|
599
|
+
<div style="font-size:14px;font-weight:600;color:#92400e;margin-bottom:6px">📡 ${t('扫描结果')}:${escHtml(scopeLabel)} ${t('人气不足')}</div>
|
|
600
|
+
<p style="font-size:12px;color:#78350f;margin-bottom:12px">${t('该范围近')}${win === '24h' ? '24h' : win === '30d' ? t('30天') : t('7天')}${t('活跃 < 3 人 — 隐私保护(k≥3)下不显示聚合')}</p>
|
|
601
|
+
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
|
602
|
+
${enlargeBtn}
|
|
603
|
+
${inviteBtns}
|
|
604
|
+
</div>
|
|
605
|
+
</div>`
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const renderAggCard = () => `
|
|
609
|
+
<div class="card" style="background:linear-gradient(135deg,#ecfdf5,#f0fdf4);border-color:#bbf7d0">
|
|
610
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px;font-size:13px">
|
|
611
|
+
<div>
|
|
612
|
+
<div style="color:#6b7280;font-size:11px;margin-bottom:2px">${t('活跃买家')}</div>
|
|
613
|
+
<div style="font-size:24px;font-weight:700;color:#16a34a">${agg.active_users >= 0 ? agg.active_users : '—'}<span style="font-size:11px;font-weight:400;color:#9ca3af"> ${t('人')}</span></div>
|
|
614
|
+
</div>
|
|
615
|
+
<div>
|
|
616
|
+
<div style="color:#6b7280;font-size:11px;margin-bottom:2px">${t('成交订单')}</div>
|
|
617
|
+
<div style="font-size:24px;font-weight:700;color:#0284c7">${agg.orders >= 0 ? agg.orders : '—'}<span style="font-size:11px;font-weight:400;color:#9ca3af"> ${t('单')}</span></div>
|
|
618
|
+
</div>
|
|
619
|
+
</div>
|
|
620
|
+
</div>`
|
|
621
|
+
|
|
622
|
+
// scoped 搜索 — 客户端过滤 top_products by title
|
|
623
|
+
const nq = (state._nearbyQ || '').toLowerCase()
|
|
624
|
+
const topProductsRaw = data.top_products || []
|
|
625
|
+
const topProductsFiltered = nq ? topProductsRaw.filter(p => (p.title || '').toLowerCase().includes(nq)) : topProductsRaw
|
|
626
|
+
const topProductsHtml = topProductsFiltered.length === 0
|
|
627
|
+
? `<div style="font-size:12px;color:#9ca3af;padding:14px;text-align:center">${nq ? t('当前关键词无匹配') : t('该范围内还没有 ≥ 3 人买过同一商品')}</div>`
|
|
628
|
+
: topProductsFiltered.map(p => `
|
|
629
|
+
<div onclick="navigate('#order-product/${p.id}')" style="display:flex;justify-content:space-between;align-items:center;padding:10px 4px;border-bottom:1px solid #f3f4f6;cursor:pointer">
|
|
630
|
+
<div style="display:flex;align-items:center;gap:10px;flex:1;min-width:0">
|
|
631
|
+
<div style="width:38px;height:38px;border-radius:8px;background:#f3f4f6;display:flex;align-items:center;justify-content:center;font-size:18px;flex-shrink:0">${getCategoryIcon(p.category)}</div>
|
|
632
|
+
<div style="flex:1;min-width:0">
|
|
633
|
+
<div style="font-size:13px;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escHtml(p.title)}</div>
|
|
634
|
+
<div style="font-size:11px;color:#6b7280">${p.price} WAZ · 🔥 ${p.buyers} ${t('人买')}</div>
|
|
635
|
+
</div>
|
|
636
|
+
</div>
|
|
637
|
+
<span style="font-size:13px;color:#9ca3af">›</span>
|
|
638
|
+
</div>`).join('')
|
|
639
|
+
|
|
640
|
+
const topCatsHtml = (data.top_categories || []).length === 0
|
|
641
|
+
? `<div style="font-size:12px;color:#9ca3af;padding:10px;text-align:center">${t('暂无可显示的类目')}</div>`
|
|
642
|
+
: `<div style="display:flex;flex-wrap:wrap;gap:8px;padding:6px 0">
|
|
643
|
+
${(data.top_categories || []).map(c => `
|
|
644
|
+
<span style="background:#f3f4f6;border-radius:14px;padding:5px 12px;font-size:12px">
|
|
645
|
+
${getCategoryIcon(c.category)} ${escHtml(c.category)} · <strong>${c.orders}</strong>
|
|
646
|
+
</span>`).join('')}
|
|
647
|
+
</div>`
|
|
648
|
+
|
|
649
|
+
// 范围信息行
|
|
650
|
+
const scopeInfoLine = (() => {
|
|
651
|
+
const stale = (data.location_stale_days != null && data.location_stale_days > 30)
|
|
652
|
+
? `<a href="#" onclick="event.preventDefault();requestLocation()" style="color:#dc2626;margin-left:6px">⚠ ${t('位置已过期')} ${data.location_stale_days}${t('天,点此刷新')}</a>` : ''
|
|
653
|
+
return `<div style="font-size:11px;color:#9ca3af;margin-bottom:10px">📡 ${escHtml(scopeLabel)} · ${t('k-anonymity ≥')} ${data.k_threshold} ${stale}</div>`
|
|
654
|
+
})()
|
|
655
|
+
|
|
656
|
+
const winLabel = win === '24h' ? '24h' : win === '30d' ? t('30天') : t('7天')
|
|
657
|
+
app.innerHTML = shell(`
|
|
658
|
+
${renderSmartBuyHeader('nearby')}
|
|
659
|
+
${discoverGoodsTabs('nearby')}
|
|
660
|
+
${pageHotFeedToggle('#nearby', '#nearby/feed', { hotIcon: '🛰', hotLabel: t('雷达') })}
|
|
661
|
+
${controls}
|
|
662
|
+
${scopeInfoLine}
|
|
663
|
+
|
|
664
|
+
${sufficient ? renderAggCard() : renderInsufficientCard()}
|
|
665
|
+
|
|
666
|
+
${sufficient ? `
|
|
667
|
+
<div class="card" style="margin-top:12px">
|
|
668
|
+
<div style="font-size:14px;font-weight:600;margin-bottom:8px">🔥 ${winLabel} ${t('热门商品')}</div>
|
|
669
|
+
${topProductsHtml}
|
|
670
|
+
</div>
|
|
671
|
+
|
|
672
|
+
<div class="card" style="margin-top:12px">
|
|
673
|
+
<div style="font-size:14px;font-weight:600;margin-bottom:8px">🏷 ${winLabel} ${t('热门类目')}</div>
|
|
674
|
+
${topCatsHtml}
|
|
675
|
+
</div>` : ''}
|
|
676
|
+
|
|
677
|
+
<div id="nearby-sh-strip" style="margin-top:14px"></div>
|
|
678
|
+
|
|
679
|
+
<div style="margin-top:14px;display:flex;gap:8px;justify-content:center">
|
|
680
|
+
<button class="btn btn-outline btn-sm" style="width:auto" onclick="requestLocation()">📍 ${t('重设位置')}</button>
|
|
681
|
+
<button class="btn btn-outline btn-sm" style="width:auto" onclick="clearLocation()">🚫 ${t('清除位置')}</button>
|
|
682
|
+
</div>
|
|
683
|
+
`, 'discover')
|
|
684
|
+
shInjectStrip('nearby-sh-strip', { limit: 6, kind: 'nearby' })
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// 请求/重设位置(geolocation 拿坐标 → 截断 0.1° → POST 保存;HTTP 站点回退手动输入)
|
|
688
|
+
// 2026-05-24 P1-5:复制 nearby 邀请文案
|
|
689
|
+
window.copyNearbyInvite = () => {
|
|
690
|
+
const ta = document.getElementById('nearby-invite-tpl')
|
|
691
|
+
if (!ta) return
|
|
692
|
+
ta.select()
|
|
693
|
+
try {
|
|
694
|
+
if (navigator.clipboard) {
|
|
695
|
+
navigator.clipboard.writeText(ta.value).then(() => toast(t('文案已复制')))
|
|
696
|
+
} else {
|
|
697
|
+
document.execCommand('copy')
|
|
698
|
+
toast(t('文案已复制'))
|
|
699
|
+
}
|
|
700
|
+
} catch { toast(t('复制失败 — 请手动选中')) }
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
window.requestLocation = () => {
|
|
704
|
+
// Geolocation API 要求 secure context(HTTPS 或 localhost)
|
|
705
|
+
// 手机访问 http://192.168.x.x:3000 时浏览器直接拒绝,err.message 通常为空
|
|
706
|
+
if (!window.isSecureContext || !navigator.geolocation) {
|
|
707
|
+
return openManualLocationModal(
|
|
708
|
+
!window.isSecureContext
|
|
709
|
+
? t('当前为 HTTP 不安全连接,浏览器禁止自动定位 — 请改用手动选择')
|
|
710
|
+
: t('浏览器不支持地理定位 — 请改用手动选择')
|
|
711
|
+
)
|
|
712
|
+
}
|
|
713
|
+
toast$(t('正在获取位置…'))
|
|
714
|
+
navigator.geolocation.getCurrentPosition(
|
|
715
|
+
async (pos) => {
|
|
716
|
+
const lat = Math.round(pos.coords.latitude * 10) / 10
|
|
717
|
+
const lng = Math.round(pos.coords.longitude * 10) / 10
|
|
718
|
+
const res = await POST('/profile/set-location', { lat, lng })
|
|
719
|
+
if (res.error) toast$(res.error, 'error')
|
|
720
|
+
else {
|
|
721
|
+
toast$(t('位置已更新(约 11km 精度)'))
|
|
722
|
+
renderNearby(document.getElementById('app'))
|
|
723
|
+
}
|
|
724
|
+
},
|
|
725
|
+
(err) => {
|
|
726
|
+
const codeMsg = {
|
|
727
|
+
1: t('位置权限被拒 — 请在浏览器/系统设置中允许后重试'),
|
|
728
|
+
2: t('无法获取位置(GPS 或网络问题)'),
|
|
729
|
+
3: t('定位超时,请重试'),
|
|
730
|
+
}[err.code] || (err.message || t('未知错误'))
|
|
731
|
+
// 不直接 toast 错误,引导到手动输入(保证总有出路)
|
|
732
|
+
openManualLocationModal(codeMsg)
|
|
733
|
+
},
|
|
734
|
+
{ enableHighAccuracy: false, timeout: 10000, maximumAge: 600000 }
|
|
735
|
+
)
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// 手动选位置 — geolocation 不可用时的兜底入口
|
|
739
|
+
// 提供常见城市快速选择 + 自定义 lat/lng 输入
|
|
740
|
+
window.openManualLocationModal = (reason) => {
|
|
741
|
+
const cities = [
|
|
742
|
+
{ name: '北京', lat: 39.9, lng: 116.4 },
|
|
743
|
+
{ name: '上海', lat: 31.2, lng: 121.5 },
|
|
744
|
+
{ name: '广州', lat: 23.1, lng: 113.3 },
|
|
745
|
+
{ name: '深圳', lat: 22.5, lng: 114.1 },
|
|
746
|
+
{ name: '杭州', lat: 30.3, lng: 120.2 },
|
|
747
|
+
{ name: '成都', lat: 30.7, lng: 104.1 },
|
|
748
|
+
{ name: '武汉', lat: 30.6, lng: 114.3 },
|
|
749
|
+
{ name: '西安', lat: 34.3, lng: 108.9 },
|
|
750
|
+
]
|
|
751
|
+
const cityBtns = cities.map(c =>
|
|
752
|
+
`<button onclick="setManualLocation(${c.lat}, ${c.lng})" style="padding:8px 6px;border-radius:8px;background:#f3f4f6;border:1px solid #e5e7eb;cursor:pointer;font-size:13px;color:#374151">${t(c.name)}</button>`
|
|
753
|
+
).join('')
|
|
754
|
+
_openModal(`
|
|
755
|
+
<h2 style="font-size:16px;font-weight:600;margin-bottom:8px">📍 ${t('手动选择位置')}</h2>
|
|
756
|
+
${reason ? `<div style="font-size:12px;color:#92400e;background:#fef3c7;padding:8px 10px;border-radius:6px;margin-bottom:12px;line-height:1.5">⚠ ${escHtml(reason)}</div>` : ''}
|
|
757
|
+
<div style="font-size:12px;color:#6b7280;margin-bottom:8px">${t('常见城市')}</div>
|
|
758
|
+
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:6px;margin-bottom:14px">${cityBtns}</div>
|
|
759
|
+
<details>
|
|
760
|
+
<summary style="font-size:12px;color:#6366f1;cursor:pointer;padding:4px 0">${t('自定义经纬度(高级)')}</summary>
|
|
761
|
+
<div style="padding:8px 0;display:grid;grid-template-columns:1fr 1fr;gap:8px">
|
|
762
|
+
<div><label style="font-size:11px;color:#6b7280">${t('纬度 lat')}</label><input id="loc-lat" type="number" step="0.1" placeholder="39.9" class="form-control" style="font-size:13px"></div>
|
|
763
|
+
<div><label style="font-size:11px;color:#6b7280">${t('经度 lng')}</label><input id="loc-lng" type="number" step="0.1" placeholder="116.4" class="form-control" style="font-size:13px"></div>
|
|
764
|
+
</div>
|
|
765
|
+
<button class="btn btn-primary btn-sm" style="margin-top:8px;padding:6px 14px" onclick="setManualLocation(Number(document.getElementById('loc-lat').value), Number(document.getElementById('loc-lng').value))">${t('使用此坐标')}</button>
|
|
766
|
+
</details>
|
|
767
|
+
<div style="text-align:right;margin-top:12px">
|
|
768
|
+
<button class="btn btn-outline btn-sm" onclick="closeModal()">${t('取消')}</button>
|
|
769
|
+
</div>
|
|
770
|
+
`)
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
window.setManualLocation = async (lat, lng) => {
|
|
774
|
+
if (!Number.isFinite(lat) || !Number.isFinite(lng)) return toast$(t('请输入有效的经纬度'), 'error')
|
|
775
|
+
if (lat < -90 || lat > 90 || lng < -180 || lng > 180) return toast$(t('经纬度超出有效范围'), 'error')
|
|
776
|
+
const latR = Math.round(lat * 10) / 10
|
|
777
|
+
const lngR = Math.round(lng * 10) / 10
|
|
778
|
+
const res = await POST('/profile/set-location', { lat: latR, lng: lngR })
|
|
779
|
+
if (res.error) return toast$(res.error, 'error')
|
|
780
|
+
closeModal()
|
|
781
|
+
toast$(t('位置已更新'))
|
|
782
|
+
renderNearby(document.getElementById('app'))
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
window.clearLocation = async () => {
|
|
786
|
+
if (!confirm(t('确认清除已存储的位置?清除后将无法查看雷达扫描。'))) return
|
|
787
|
+
const res = await POST('/profile/clear-location', {})
|
|
788
|
+
if (res.error) return toast$(res.error, 'error')
|
|
789
|
+
toast$(t('位置已清除'))
|
|
790
|
+
renderNearby(document.getElementById('app'))
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// 2026-05-24 雷达扫描 · 动态:附近匿名聚合 + 24h 时段切片
|
|
794
|
+
async function renderNearbyFeed(app) {
|
|
795
|
+
if (!state.user) return navigate('#login')
|
|
796
|
+
app.innerHTML = shell(loading$(), 'discover')
|
|
797
|
+
const data = await GET('/nearby').catch(() => null)
|
|
798
|
+
let body
|
|
799
|
+
if (!data || data.error || !data.has_location) {
|
|
800
|
+
body = feedEmpty('📡', t('请先在 好物 tab 授权位置'), t('去授权'), '#nearby')
|
|
801
|
+
} else {
|
|
802
|
+
const agg = data.aggregate || {}
|
|
803
|
+
const topCats = agg.top_categories || []
|
|
804
|
+
const topProducts = agg.top_products_24h || []
|
|
805
|
+
body = `
|
|
806
|
+
<div class="card" style="padding:14px;background:linear-gradient(135deg,#f0f9ff,#eef2ff);border-color:#bae6fd;margin-bottom:10px">
|
|
807
|
+
<div style="font-size:12px;color:#374151;margin-bottom:6px">📍 ${data.cell.approx_km}km × ${data.cell.approx_km}km · k-anonymity ≥ ${data.k_threshold}</div>
|
|
808
|
+
<div style="font-size:13px;color:#1e40af">${agg.active_users_24h > 0 ? `👥 ${agg.active_users_24h} ${t('位邻居 24h 活跃')}` : t('该区域近 24h 活动稀少')}</div>
|
|
809
|
+
</div>
|
|
810
|
+
${topCats.length > 0 ? `
|
|
811
|
+
<h3 style="font-size:13px;font-weight:700;margin:14px 0 8px">🏷 ${t('近 24h 同城热门品类')}</h3>
|
|
812
|
+
${topCats.slice(0,5).map((c, i) => `
|
|
813
|
+
<div class="card" style="padding:10px 12px;margin-bottom:6px;display:flex;align-items:center;gap:10px">
|
|
814
|
+
<div style="font-size:18px;font-weight:800;color:${i<3?'#dc2626':'#9ca3af'};min-width:24px;text-align:center">${i+1}</div>
|
|
815
|
+
<div style="flex:1">
|
|
816
|
+
<div style="font-size:13px;color:#1f2937">${getCategoryIcon(c.category)} ${escHtml(c.category || t('未分类'))}</div>
|
|
817
|
+
<div style="font-size:11px;color:#6b7280">🛒 ${c.purchase_count} ${t('单')} · 👥 ${c.buyer_count} ${t('人买')}</div>
|
|
818
|
+
</div>
|
|
819
|
+
</div>`).join('')}` : ''}
|
|
820
|
+
${topProducts.length > 0 ? `
|
|
821
|
+
<h3 style="font-size:13px;font-weight:700;margin:14px 0 8px">🔥 ${t('近 24h 同城热销商品')}</h3>
|
|
822
|
+
${topProducts.slice(0,5).map(p => `
|
|
823
|
+
<div class="card" style="padding:10px 12px;margin-bottom:6px;cursor:pointer" onclick="navigate('#order-product/${p.id}')">
|
|
824
|
+
<div style="font-size:13px;font-weight:600">${escHtml(p.title)} <span style="color:#dc2626;font-weight:700">${p.price} WAZ</span></div>
|
|
825
|
+
<div style="font-size:11px;color:#6b7280;margin-top:2px">🛒 ${p.buy_count} ${t('单')} · 同城共鸣</div>
|
|
826
|
+
</div>`).join('')}` : ''}
|
|
827
|
+
`
|
|
828
|
+
}
|
|
829
|
+
app.innerHTML = shell(`
|
|
830
|
+
${renderSmartBuyHeader('nearby')}
|
|
831
|
+
${discoverGoodsTabs('nearby')}
|
|
832
|
+
${pageHotFeedToggle('#nearby', '#nearby/feed', { hotIcon: '🛰', hotLabel: t('雷达') })}
|
|
833
|
+
<h2 style="font-size:16px;font-weight:700;margin:14px 0 10px">📡 ${t('附近动态')}</h2>
|
|
834
|
+
<div style="font-size:11px;color:#6b7280;margin-bottom:14px">${t('同城匿名聚合 · k≥3 保护身份')}</div>
|
|
835
|
+
${body}
|
|
836
|
+
`, 'discover')
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// ─── #me 受信角色专属(admin / verifier / logistics / arbitrator)────────
|
|
840
|
+
async function renderTrustedMyHome(app, role) {
|
|
841
|
+
app.innerHTML = shell(loading$(), 'me')
|
|
842
|
+
try {
|
|
843
|
+
const n = await GET('/notifications?unread=1').catch(() => null)
|
|
844
|
+
if (n) state.unread = n.unread || 0
|
|
845
|
+
} catch {}
|
|
846
|
+
|
|
847
|
+
const profileRes = await GET('/profile').catch(() => null)
|
|
848
|
+
const wal = profileRes?.wallet || { balance: 0, staked: 0 }
|
|
849
|
+
|
|
850
|
+
// logistics / arbitrator 是 trusted role 但非 isTrustedRole 限制集(仅 admin/verifier 禁钱包)
|
|
851
|
+
// 因此他们可参与慈善捐款(普通用户铁律)
|
|
852
|
+
const canDonate = role === 'logistics' || role === 'arbitrator'
|
|
853
|
+
// Wave B-4: 物流绩效卡(仅 logistics 拉)
|
|
854
|
+
const perfRes = role === 'logistics' ? await GET('/logistics/me/performance').catch(() => null) : null
|
|
855
|
+
// Wave D-5: verifier / arbitrator KPI
|
|
856
|
+
const verifierKpi = role === 'verifier' ? await GET('/verifier/me/kpi').catch(() => null) : null
|
|
857
|
+
const arbitratorKpi = role === 'arbitrator' ? await GET('/arbitrator/me/kpi').catch(() => null) : null
|
|
858
|
+
const charityRes = canDonate ? await GET('/charity/me').catch(() => null) : null
|
|
859
|
+
const charity = charityRes && !charityRes.error ? charityRes : null
|
|
860
|
+
const rep = charity?.reputation || {}
|
|
861
|
+
const pendingRepays = (charity?.pending_repayments || []).length
|
|
862
|
+
|
|
863
|
+
const roleMeta = {
|
|
864
|
+
admin: { icon: '🛡', label: t('管理员'), home: '#admin', homeLabel: t('管理后台') },
|
|
865
|
+
verifier: { icon: '🔍', label: t('审核员'), home: '#verify-tasks', homeLabel: t('审核任务') },
|
|
866
|
+
logistics: { icon: '🚚', label: t('物流'), home: '#seller', homeLabel: t('配送任务') },
|
|
867
|
+
arbitrator: { icon: '⚖', label: t('仲裁员'), home: '#seller', homeLabel: t('仲裁台') },
|
|
868
|
+
}[role] || { icon: '👤', label: role, home: '#', homeLabel: t('首页') }
|
|
869
|
+
|
|
870
|
+
const card = (icon, label, sub, hash, badge) => `
|
|
871
|
+
<div class="card" onclick="location.hash='${hash}'" style="padding:14px;cursor:pointer;display:flex;align-items:center;gap:10px;min-height:64px;position:relative">
|
|
872
|
+
<div style="font-size:24px;flex-shrink:0">${icon}</div>
|
|
873
|
+
<div style="flex:1;min-width:0">
|
|
874
|
+
<div style="font-weight:600;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${label}</div>
|
|
875
|
+
${sub ? `<div style="font-size:11px;color:#9ca3af;margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${sub}</div>` : ''}
|
|
876
|
+
</div>
|
|
877
|
+
${badge ? `<div style="background:#dc2626;color:#fff;border-radius:99px;font-size:10px;padding:2px 7px;min-width:18px;text-align:center;flex-shrink:0">${badge}</div>` : ''}
|
|
878
|
+
</div>`
|
|
879
|
+
|
|
880
|
+
// 头部:身份 + 权责分离声明(与各角色 hub 主题色同步 — 跨角色一致性)
|
|
881
|
+
const [c1, c2] = PAGE_HEADER_GRADIENTS[role] || PAGE_HEADER_GRADIENTS.admin
|
|
882
|
+
const header = `
|
|
883
|
+
<div class="card" style="padding:16px;margin-bottom:14px;background:linear-gradient(135deg,${c1},${c2});color:#fff">
|
|
884
|
+
<div style="display:flex;align-items:center;gap:10px;margin-bottom:12px">
|
|
885
|
+
<div style="font-size:32px">${roleMeta.icon}</div>
|
|
886
|
+
<div style="flex:1">
|
|
887
|
+
<div style="font-weight:700;font-size:15px">${escHtml(state.user.name || state.user.handle)}</div>
|
|
888
|
+
<div style="font-size:11px;opacity:0.85;margin-top:2px">@${escHtml(state.user.handle || '')} · ${roleMeta.label} · 🔒 ${t('受信角色')}${role === 'admin' ? ' · ' + t('不可个人交易') : ''}</div>
|
|
889
|
+
</div>
|
|
890
|
+
</div>
|
|
891
|
+
<div onclick="location.hash='${roleMeta.home}'" style="cursor:pointer;padding:10px 12px;background:rgba(255,255,255,0.12);border-radius:8px;display:flex;justify-content:space-between;align-items:center;font-size:12px">
|
|
892
|
+
<span style="font-weight:600">${roleMeta.icon} ${roleMeta.homeLabel}</span>
|
|
893
|
+
<span style="opacity:0.85">→</span>
|
|
894
|
+
</div>
|
|
895
|
+
</div>
|
|
896
|
+
`
|
|
897
|
+
|
|
898
|
+
// 角色专属工作入口(按角色不同)
|
|
899
|
+
let workGrid = ''
|
|
900
|
+
if (role === 'admin') {
|
|
901
|
+
// admin 不应有钱包/交易;通过 hub tab 完成所有治理工作
|
|
902
|
+
// root admin: 隐式 all 权限 + 可管理其他 admin
|
|
903
|
+
// regional admin: 限定 scope + admin_permissions JSON 控制可见性
|
|
904
|
+
const adminType = state.user.admin_type || 'root'
|
|
905
|
+
const isRoot = adminType === 'root'
|
|
906
|
+
const adminScope = state.user.admin_scope || 'global'
|
|
907
|
+
let adminPerms = []
|
|
908
|
+
try { adminPerms = JSON.parse(state.user.admin_permissions || '[]') } catch {}
|
|
909
|
+
const canDo = (perm) => isRoot || adminPerms.includes('all') || adminPerms.includes(perm)
|
|
910
|
+
const typeBadge = isRoot
|
|
911
|
+
? `<span style="background:#fee2e2;color:#991b1b;font-size:10px;padding:1px 8px;border-radius:99px;font-weight:700;margin-left:6px">🔱 ROOT</span>`
|
|
912
|
+
: `<span style="background:#dbeafe;color:#1e40af;font-size:10px;padding:1px 8px;border-radius:99px;font-weight:700;margin-left:6px">🌏 ${adminScope.toUpperCase()}</span>`
|
|
913
|
+
workGrid = `
|
|
914
|
+
<div style="font-size:12px;color:#6b7280;font-weight:600;margin:14px 0 6px;display:flex;align-items:center">🏛 ${t('治理工作台')}${typeBadge}</div>
|
|
915
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px">
|
|
916
|
+
${card('📊', t('概览'), t('KPI + 异常告警'), '#admin')}
|
|
917
|
+
${card('📈', t('协议指标看板'), t('DAU / GMV / 争议率'), '#admin/kpi')}
|
|
918
|
+
${canDo('protocol') ? card('⚙️', t('协议参数'), t('费率 / 奖励 / 上限'), '#admin/params') : ''}
|
|
919
|
+
${canDo('users') ? card('🔎', t('用户活动 timeline'), t('任意用户完整事件流'), '#admin/timeline') : ''}
|
|
920
|
+
${canDo('users') ? card('🛡', t('风控告警'), t('可疑账户 / 一键暂停'), '#admin/risk') : ''}
|
|
921
|
+
${canDo('users') ? card('🆔', t('KYC 审核'), t('待审实名认证'), '#admin/kyc') : ''}
|
|
922
|
+
${canDo('users') ? card('📥', t('数据导出'), t('全平台 CSV 报表'), '#admin/export') : ''}
|
|
923
|
+
${canDo('protocol') ? card('📒', t('平台财务'), t('协议费 vs 拨付 月度'), '#admin/finance') : ''}
|
|
924
|
+
${card('📡', t('实时事件 stream'), t('全局事件 SSE 推流'), '#admin/events')}
|
|
925
|
+
${canDo('protocol') ? card('🩺', t('系统健康'), t('DB / RPC / 内存'), '#admin/health') : ''}
|
|
926
|
+
${canDo('users') ? card('👥', t('用户与权限'), t('用户 / 申请 / 申诉 / 配额'), '#admin/users') : ''}
|
|
927
|
+
${canDo('content') ? card('📦', t('内容管理'), t('商品 / 订单 / 举报'), '#admin/content') : ''}
|
|
928
|
+
${canDo('content') ? card('📌', t('编辑精选'), t('每周推荐 / 商品 / 卖家'), '#admin/editor-picks') : ''}
|
|
929
|
+
${canDo('arbitration') ? card('⚖', t('仲裁审核'), t('争议 / 验证任务'), '#admin/arbitration') : ''}
|
|
930
|
+
${canDo('users') ? card('📥', t('用户反馈'), t('工单 / bug / 申诉'), '#admin-feedback') : ''}
|
|
931
|
+
${canDo('protocol') ? card('⚛', t('协议管理'), t('Tokenomics / 金库 / 拨款 / 审计'), '#admin/protocol') : ''}
|
|
932
|
+
${isRoot ? card('💳', t('支付选项'), t('多链 / 多区域 / 多渠道'), '#admin-payments') : ''}
|
|
933
|
+
${isRoot ? card('🛡', t('管理员账号'), t('创建 / 撤销 admin(仅 root)'), '#admin/manage-admins') : ''}
|
|
934
|
+
</div>
|
|
935
|
+
${!isRoot ? `<div style="margin-top:6px;padding:8px 12px;background:#fef3c7;border:1px dashed #f59e0b;border-radius:8px;font-size:11px;color:#92400e;line-height:1.5">🌏 <strong>${t('区域管理员')}</strong> · ${t('范围')}:${adminScope} · ${t('权限')}:${adminPerms.join(' / ') || t('无')}<br>${t('如需扩展权限或管理其他 admin,请联系 root 管理员。')}</div>` : ''}
|
|
936
|
+
`
|
|
937
|
+
} else if (role === 'verifier') {
|
|
938
|
+
// verifier 受铁律 isTrustedRole 限制:无钱包 / 无交易 / 无慈善
|
|
939
|
+
workGrid = `
|
|
940
|
+
<div style="font-size:12px;color:#6b7280;font-weight:600;margin:14px 0 6px">🔍 ${t('审核工作')}</div>
|
|
941
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px">
|
|
942
|
+
${card('🔍', t('审核任务'), t('可接 / 已接未投 / 已投'), '#verify-tasks')}
|
|
943
|
+
${card('📩', t('我要申诉'), t('针对争议判定'), '#verifier-appeal')}
|
|
944
|
+
${card('📦', t('订单记录'), t('我审核相关'), '#orders')}
|
|
945
|
+
</div>
|
|
946
|
+
`
|
|
947
|
+
} else if (role === 'logistics') {
|
|
948
|
+
workGrid = `
|
|
949
|
+
<div style="font-size:12px;color:#6b7280;font-weight:600;margin:14px 0 6px">🚚 ${t('配送工作')}</div>
|
|
950
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px">
|
|
951
|
+
${card('🚚', t('配送任务'), t('待揽收 / 在途 / 待投递'), '#seller')}
|
|
952
|
+
${card('📦', t('历史记录'), t('我配送过的'), '#orders')}
|
|
953
|
+
${card('💬', t('客户消息'), t('协调买家 / 卖家'), '#chats')}
|
|
954
|
+
</div>
|
|
955
|
+
<div style="font-size:12px;color:#6b7280;font-weight:600;margin:14px 0 6px">💰 ${t('个人')}</div>
|
|
956
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px">
|
|
957
|
+
${card('💰', t('钱包'), `${Number(wal.balance).toFixed(2)} WAZ`, '#wallet')}
|
|
958
|
+
</div>
|
|
959
|
+
`
|
|
960
|
+
} else if (role === 'arbitrator') {
|
|
961
|
+
workGrid = `
|
|
962
|
+
<div style="font-size:12px;color:#6b7280;font-weight:600;margin:14px 0 6px">⚖ ${t('仲裁工作')}</div>
|
|
963
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px">
|
|
964
|
+
${card('⚖', t('仲裁台'), t('待响应 / 仲裁中 / 已结'), '#seller')}
|
|
965
|
+
${card('📦', t('记录'), t('我裁定过的'), '#orders')}
|
|
966
|
+
${card('💬', t('双方沟通'), t('当事方协调'), '#chats')}
|
|
967
|
+
</div>
|
|
968
|
+
<div style="font-size:12px;color:#6b7280;font-weight:600;margin:14px 0 6px">💰 ${t('个人')}</div>
|
|
969
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px">
|
|
970
|
+
${card('💰', t('钱包'), `${Number(wal.balance).toFixed(2)} WAZ`, '#wallet')}
|
|
971
|
+
</div>
|
|
972
|
+
`
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// 社交与发现(仅 logistics / arbitrator — 受信角色但非交易禁集)
|
|
976
|
+
const socialGrid = canDonate ? `
|
|
977
|
+
<div style="font-size:12px;color:#6b7280;font-weight:600;margin:14px 0 6px">🧭 ${t('社交与发现')}</div>
|
|
978
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px">
|
|
979
|
+
${card('📍', t('附近'), t('同城节点 · 面交可达'), '#nearby')}
|
|
980
|
+
${card('🏆', t('排行榜'), t('热门 / 创作者 / 威望'), '#leaderboard')}
|
|
981
|
+
</div>
|
|
982
|
+
` : ''
|
|
983
|
+
|
|
984
|
+
// 公益折叠区(仅 logistics / arbitrator — 普通用户铁律允许;admin/verifier 严禁)
|
|
985
|
+
const charitySection = canDonate ? `
|
|
986
|
+
<details style="margin:14px 0 10px;background:#fff;border:1px solid #e5e7eb;border-radius:8px">
|
|
987
|
+
<summary style="padding:10px 14px;cursor:pointer;font-size:13px;color:#6b7280;font-weight:600;display:flex;justify-content:space-between;align-items:center">
|
|
988
|
+
<span>🌸 ${t('公益')}</span>
|
|
989
|
+
<span style="font-size:11px;color:#9ca3af">${rep.badge_tier && rep.badge_tier !== 'none' ? rep.badge_tier + ' · ' : ''}${t('威望')} ${Number(rep.prestige_score||0).toFixed(0)}</span>
|
|
990
|
+
</summary>
|
|
991
|
+
<div style="padding:8px;border-top:1px solid #f3f4f6;display:grid;grid-template-columns:1fr 1fr;gap:8px">
|
|
992
|
+
${card('🌸', t('许愿池'), pendingRepays ? pendingRepays + ' ' + t('待还愿') : t('浏览许愿 / 为他人圆梦'), '#wishes', pendingRepays || '')}
|
|
993
|
+
${card('💝', t('慈善基金'), t('捐款 · 公开账目'), '#wish/fund')}
|
|
994
|
+
${card('📚', t('我的慈善'), t('我的许愿 / 捐款 / 圆梦记录'), '#wish/mine')}
|
|
995
|
+
${card('🎁', t('圆梦故事'), t('已圆愿公开故事板'), '#wish/stories')}
|
|
996
|
+
</div>
|
|
997
|
+
</details>
|
|
998
|
+
` : ''
|
|
999
|
+
|
|
1000
|
+
// 2026-05-24 「个人资料」tile 已删 — Settings sub-tab 紧邻面板 1 click 可达
|
|
1001
|
+
const commonGrid = `
|
|
1002
|
+
<div style="margin-top:10px;padding:8px 12px;background:#f9fafb;border:1px dashed #d1d5db;border-radius:8px;font-size:11px;color:#6b7280;line-height:1.5">
|
|
1003
|
+
🔒 <strong>${t('权责分离')}</strong>: ${t('受信角色不能自助添加 buyer / seller 等其他身份,避免利益冲突。')}
|
|
1004
|
+
</div>
|
|
1005
|
+
`
|
|
1006
|
+
|
|
1007
|
+
// Wave B-4: 物流绩效卡 HTML(仅 logistics)
|
|
1008
|
+
let perfCard = ''
|
|
1009
|
+
if (role === 'logistics' && perfRes && !perfRes.error) {
|
|
1010
|
+
// P2-5: 样本不足提示 — 评估单数 < 10 时百分比不稳定,显式标注
|
|
1011
|
+
const evaluatedSamples = (perfRes.pickup.on_time || 0) + (perfRes.pickup.overdue || 0)
|
|
1012
|
+
+ (perfRes.delivery.on_time || 0) + (perfRes.delivery.overdue || 0)
|
|
1013
|
+
const lowSample = evaluatedSamples < 10
|
|
1014
|
+
const pct = (v) => v == null ? '—' : (v * 100).toFixed(1) + '%'
|
|
1015
|
+
const hr = (v) => v == null ? '—' : v.toFixed(1) + ' ' + t('小时')
|
|
1016
|
+
const color = (v, good, warn) => {
|
|
1017
|
+
if (v == null) return '#9ca3af'
|
|
1018
|
+
if (lowSample) return '#6b7280' // 样本不足 → 灰色,避免误导
|
|
1019
|
+
return v >= good ? '#16a34a' : v >= warn ? '#d97706' : '#dc2626'
|
|
1020
|
+
}
|
|
1021
|
+
perfCard = `
|
|
1022
|
+
<div class="card" style="background:linear-gradient(135deg,#ecfdf5,#f0fdfa);border:1px solid #a7f3d0;margin-bottom:14px;padding:14px">
|
|
1023
|
+
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px">
|
|
1024
|
+
<div style="font-size:14px;font-weight:700">📊 ${t('物流绩效')} <span style="font-size:11px;color:#6b7280;font-weight:400">${t('近')} ${perfRes.window_days} ${t('天')}</span></div>
|
|
1025
|
+
<div style="font-size:11px;color:#9ca3af">${perfRes.total_orders} ${t('单')}</div>
|
|
1026
|
+
</div>
|
|
1027
|
+
${lowSample ? `<div style="background:#fef3c7;border:1px dashed #f59e0b;color:#92400e;padding:6px 10px;border-radius:6px;font-size:11px;margin-bottom:10px">⚠ ${t('样本不足(评估单数 <10),百分比仅供参考')}</div>` : ''}
|
|
1028
|
+
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:8px;text-align:center">
|
|
1029
|
+
<div>
|
|
1030
|
+
<div style="font-size:18px;font-weight:700;color:${color(perfRes.pickup.on_time_rate, 0.9, 0.7)}">${pct(perfRes.pickup.on_time_rate)}</div>
|
|
1031
|
+
<div style="font-size:11px;color:#6b7280;margin-top:2px">${t('准时揽收率')}</div>
|
|
1032
|
+
<div style="font-size:10px;color:#9ca3af">${t('中位')} ${hr(perfRes.pickup.median_hours)}</div>
|
|
1033
|
+
</div>
|
|
1034
|
+
<div>
|
|
1035
|
+
<div style="font-size:18px;font-weight:700;color:${color(perfRes.delivery.on_time_rate, 0.9, 0.7)}">${pct(perfRes.delivery.on_time_rate)}</div>
|
|
1036
|
+
<div style="font-size:11px;color:#6b7280;margin-top:2px">${t('准时投递率')}</div>
|
|
1037
|
+
<div style="font-size:10px;color:#9ca3af">${t('中位')} ${hr(perfRes.delivery.median_hours)}</div>
|
|
1038
|
+
</div>
|
|
1039
|
+
<div>
|
|
1040
|
+
<div style="font-size:18px;font-weight:700;color:${color(perfRes.disputes.loss_rate == null ? 1 : 1 - perfRes.disputes.loss_rate, 0.9, 0.7)}">${perfRes.disputes.total}</div>
|
|
1041
|
+
<div style="font-size:11px;color:#6b7280;margin-top:2px">${t('争议数')}</div>
|
|
1042
|
+
<div style="font-size:10px;color:#9ca3af">${t('败诉')} ${perfRes.disputes.lost}</div>
|
|
1043
|
+
</div>
|
|
1044
|
+
</div>
|
|
1045
|
+
<div style="margin-top:10px;font-size:11px;color:#6b7280;display:flex;justify-content:space-between">
|
|
1046
|
+
<span>${t('在途')} ${perfRes.in_progress}</span>
|
|
1047
|
+
<span>${t('已投递')} ${perfRes.delivered}</span>
|
|
1048
|
+
<span>${t('已完成')} ${perfRes.completed}</span>
|
|
1049
|
+
</div>
|
|
1050
|
+
</div>
|
|
1051
|
+
`
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// Wave D-5: verifier KPI 卡
|
|
1055
|
+
let verifierCard = ''
|
|
1056
|
+
if (role === 'verifier' && verifierKpi && !verifierKpi.error) {
|
|
1057
|
+
const acc = verifierKpi.cumulative.accuracy
|
|
1058
|
+
const accColor = acc == null ? '#9ca3af' : acc >= 0.9 ? '#16a34a' : acc >= 0.7 ? '#d97706' : '#dc2626'
|
|
1059
|
+
verifierCard = `
|
|
1060
|
+
<div class="card" style="background:linear-gradient(135deg,#f5f3ff,#fff);border:1px solid #c4b5fd;margin-bottom:14px;padding:14px">
|
|
1061
|
+
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px">
|
|
1062
|
+
<div style="font-size:14px;font-weight:700">🔍 ${t('审核员绩效')} <span style="font-size:11px;color:#6b7280;font-weight:400">${verifierKpi.tier || '—'}</span></div>
|
|
1063
|
+
<div style="font-size:11px;color:#9ca3af">${t('近')} ${verifierKpi.window_days} ${t('天')}</div>
|
|
1064
|
+
</div>
|
|
1065
|
+
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:8px;text-align:center">
|
|
1066
|
+
<div>
|
|
1067
|
+
<div style="font-size:18px;font-weight:700;color:#7c3aed">${verifierKpi.cumulative.tasks_done}</div>
|
|
1068
|
+
<div style="font-size:10px;color:#6b7280">${t('累计任务')}</div>
|
|
1069
|
+
</div>
|
|
1070
|
+
<div>
|
|
1071
|
+
<div style="font-size:18px;font-weight:700;color:${accColor}">${acc == null ? '—' : (acc * 100).toFixed(1) + '%'}</div>
|
|
1072
|
+
<div style="font-size:10px;color:#6b7280">${t('准确率')} <span style="color:#9ca3af">(${t('全期')})</span></div>
|
|
1073
|
+
</div>
|
|
1074
|
+
<div>
|
|
1075
|
+
<div style="font-size:18px;font-weight:700;color:#16a34a">${Number(verifierKpi.total_earned_waz || 0).toFixed(2)}</div>
|
|
1076
|
+
<div style="font-size:10px;color:#6b7280">${t('累计收益')} WAZ</div>
|
|
1077
|
+
</div>
|
|
1078
|
+
</div>
|
|
1079
|
+
<div style="margin-top:10px;font-size:11px;color:#6b7280;display:flex;justify-content:space-between">
|
|
1080
|
+
<span>${t('窗口投票')} ${verifierKpi.window.votes}</span>
|
|
1081
|
+
<span>${t('今日配额')} ${verifierKpi.tasks_today}/${verifierKpi.daily_quota}</span>
|
|
1082
|
+
<span>${t('验证权')} ${verifierKpi.verify_rights}</span>
|
|
1083
|
+
</div>
|
|
1084
|
+
</div>
|
|
1085
|
+
`
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// Wave D-5: arbitrator KPI 卡
|
|
1089
|
+
let arbitratorCard = ''
|
|
1090
|
+
if (role === 'arbitrator' && arbitratorKpi && !arbitratorKpi.error) {
|
|
1091
|
+
arbitratorCard = `
|
|
1092
|
+
<div class="card" style="background:linear-gradient(135deg,#fef3c7,#fff);border:1px solid #fcd34d;margin-bottom:14px;padding:14px">
|
|
1093
|
+
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px">
|
|
1094
|
+
<div style="font-size:14px;font-weight:700">⚖ ${t('仲裁员绩效')}</div>
|
|
1095
|
+
<div style="font-size:11px;color:#9ca3af">${t('近')} ${arbitratorKpi.window_days} ${t('天')}</div>
|
|
1096
|
+
</div>
|
|
1097
|
+
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:8px;text-align:center">
|
|
1098
|
+
<div>
|
|
1099
|
+
<div style="font-size:18px;font-weight:700;color:#92400e">${arbitratorKpi.cumulative.total}</div>
|
|
1100
|
+
<div style="font-size:10px;color:#6b7280">${t('累计裁决')}</div>
|
|
1101
|
+
</div>
|
|
1102
|
+
<div>
|
|
1103
|
+
<div style="font-size:18px;font-weight:700;color:${arbitratorKpi.pending > 0 ? '#dc2626' : '#16a34a'}">${arbitratorKpi.pending}</div>
|
|
1104
|
+
<div style="font-size:10px;color:#6b7280">${t('待处理')}</div>
|
|
1105
|
+
</div>
|
|
1106
|
+
<div>
|
|
1107
|
+
<div style="font-size:18px;font-weight:700;color:#16a34a">${Number(arbitratorKpi.total_earned_waz || 0).toFixed(2)}</div>
|
|
1108
|
+
<div style="font-size:10px;color:#6b7280">${t('累计收益')} WAZ</div>
|
|
1109
|
+
</div>
|
|
1110
|
+
</div>
|
|
1111
|
+
<div style="margin-top:10px;font-size:11px;color:#6b7280">${t('裁定分布')}: ${t('退买家')} ${arbitratorKpi.cumulative.refund_buyer} · ${t('部分退')} ${arbitratorKpi.cumulative.partial_refund} · ${t('归卖家')} ${arbitratorKpi.cumulative.release_seller}</div>
|
|
1112
|
+
</div>
|
|
1113
|
+
`
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
app.innerHTML = shell(mySubTabsHTML('dashboard') + header + perfCard + verifierCard + arbitratorCard + workGrid + socialGrid + commonGrid + charitySection, 'me')
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// ─── 卖家 #me 专业版(剥离慈善/排行/社交;聚焦商品+订单+资金)───────
|
|
1120
|
+
async function renderSellerMyHome(app) {
|
|
1121
|
+
try { refreshCartBadge() } catch {}
|
|
1122
|
+
try {
|
|
1123
|
+
const n = await GET('/notifications?unread=1').catch(() => null)
|
|
1124
|
+
if (n) state.unread = n.unread || 0
|
|
1125
|
+
} catch {}
|
|
1126
|
+
try { await refreshAnnouncementsBadge() } catch {}
|
|
1127
|
+
|
|
1128
|
+
const [profileRes, ordersRes, rfqsRes, skillsRes, agentRes, charityRes, claimTasksRes, returnsRes] = await Promise.all([
|
|
1129
|
+
GET('/profile').catch(() => null),
|
|
1130
|
+
GET('/orders').catch(() => []),
|
|
1131
|
+
GET('/rfqs?limit=50').catch(() => []),
|
|
1132
|
+
GET('/skills/mine').catch(() => []),
|
|
1133
|
+
GET('/agents/me/reputation').catch(() => null),
|
|
1134
|
+
GET('/charity/me').catch(() => null),
|
|
1135
|
+
GET('/claim-tasks/mine').catch(() => null),
|
|
1136
|
+
GET('/return-requests?role=seller&status=pending').catch(() => null),
|
|
1137
|
+
])
|
|
1138
|
+
const pendingReturns = (returnsRes?.items || []).length
|
|
1139
|
+
const profile = profileRes && !profileRes.error ? profileRes : null
|
|
1140
|
+
const wal = { balance: Number(profile?.wallet?.balance || 0), staked: Number(profile?.wallet?.staked || 0) }
|
|
1141
|
+
const orders = Array.isArray(ordersRes) ? ordersRes : []
|
|
1142
|
+
const myUid = state.user.id
|
|
1143
|
+
const sellOrders = orders.filter(o => o.seller_id === myUid)
|
|
1144
|
+
const toShip = sellOrders.filter(o => ['paid','accepted'].includes(o.status)).length
|
|
1145
|
+
const inDispute = sellOrders.filter(o => o.status === 'disputed').length
|
|
1146
|
+
const rfqs = Array.isArray(rfqsRes) ? rfqsRes : []
|
|
1147
|
+
const openRfqs = rfqs.filter(r => r.status === 'open').length
|
|
1148
|
+
const mySkills = Array.isArray(skillsRes) ? skillsRes : []
|
|
1149
|
+
const skillCount = mySkills.length
|
|
1150
|
+
const activeSubs = mySkills.filter(s => s.subscribed_count > 0 || s.is_subscribed).length
|
|
1151
|
+
const charity = charityRes && !charityRes.error ? charityRes : null
|
|
1152
|
+
const rep = charity?.reputation || {}
|
|
1153
|
+
const pendingRepays = (charity?.pending_repayments || []).length
|
|
1154
|
+
// 索赔验证任务:卖家被诉 + 卖家主动核实
|
|
1155
|
+
// 注:外部审核员仅 buyer 可申请,seller 不可(业务规则)
|
|
1156
|
+
const myClaimTasks = claimTasksRes && !claimTasksRes.error
|
|
1157
|
+
? ((claimTasksRes.as_seller || []).length + (claimTasksRes.as_buyer || []).length) : 0
|
|
1158
|
+
const agentLevel = agentRes?.level || 'new'
|
|
1159
|
+
const agentTrust = Math.round(agentRes?.trust_score || 0)
|
|
1160
|
+
const agentBandColor = { legend:'#dc2626', quality:'#9333ea', trusted:'#4f46e5', new:'#9ca3af' }[agentLevel] || '#6b7280'
|
|
1161
|
+
|
|
1162
|
+
const card = (icon, label, sub, hash, badge, accent) => `
|
|
1163
|
+
<div class="card" onclick="location.hash='${hash}'" style="padding:14px;cursor:pointer;display:flex;align-items:center;gap:10px;min-height:64px;position:relative${accent ? ';border-left:3px solid '+accent : ''}">
|
|
1164
|
+
<div style="font-size:24px;flex-shrink:0">${icon}</div>
|
|
1165
|
+
<div style="flex:1;min-width:0">
|
|
1166
|
+
<div style="font-weight:600;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${label}</div>
|
|
1167
|
+
${sub ? `<div style="font-size:11px;color:#9ca3af;margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${sub}</div>` : ''}
|
|
1168
|
+
</div>
|
|
1169
|
+
${badge ? `<div style="background:#dc2626;color:#fff;border-radius:99px;font-size:10px;padding:2px 7px;min-width:18px;text-align:center;flex-shrink:0">${badge}</div>` : ''}
|
|
1170
|
+
</div>`
|
|
1171
|
+
|
|
1172
|
+
const header = `
|
|
1173
|
+
<div class="card" style="padding:16px;margin-bottom:14px;background:linear-gradient(135deg,#7c2d12,#9a3412);color:#fff">
|
|
1174
|
+
<div style="display:flex;align-items:center;gap:10px;margin-bottom:12px">
|
|
1175
|
+
<div style="font-size:28px">🏪</div>
|
|
1176
|
+
<div style="flex:1">
|
|
1177
|
+
<div style="font-weight:700;font-size:15px">${escHtml(state.user.name || state.user.handle)}</div>
|
|
1178
|
+
<div style="font-size:11px;opacity:0.85;margin-top:2px">@${escHtml(state.user.handle || '')} · ${t('卖家')}</div>
|
|
1179
|
+
</div>
|
|
1180
|
+
</div>
|
|
1181
|
+
<div onclick="location.hash='#wallet'" style="cursor:pointer;padding:10px 12px;background:rgba(255,255,255,0.12);border-radius:8px;display:flex;justify-content:space-between;align-items:center">
|
|
1182
|
+
<div>
|
|
1183
|
+
<div style="font-size:10px;opacity:0.85;text-transform:uppercase;letter-spacing:0.5px">${t('钱包余额')}</div>
|
|
1184
|
+
<div style="font-size:22px;font-weight:800;line-height:1.2">${Number(wal.balance).toFixed(2)} <span style="font-size:13px;font-weight:600">WAZ</span></div>
|
|
1185
|
+
${wal.staked > 0 ? `<div style="font-size:10px;opacity:0.75;margin-top:2px">${t('已锁定')} ${Number(wal.staked).toFixed(2)} WAZ</div>` : ''}
|
|
1186
|
+
</div>
|
|
1187
|
+
<div style="font-size:18px;opacity:0.85">→</div>
|
|
1188
|
+
</div>
|
|
1189
|
+
</div>
|
|
1190
|
+
`
|
|
1191
|
+
|
|
1192
|
+
// 2026-05-24 agentDash 移除 — Advanced sub-tab 的 heroAgent 已展示同样 Agent trust + skill 统计
|
|
1193
|
+
|
|
1194
|
+
const workGrid = `
|
|
1195
|
+
<div style="font-size:12px;color:#6b7280;font-weight:600;margin:14px 0 6px">⚡ ${t('工作中心')}</div>
|
|
1196
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px">
|
|
1197
|
+
${card('📦', t('订单管理'), toShip > 0 ? toShip + ' ' + t('待发货') : t('全部已发货'), '#orders', toShip || '', toShip > 0 ? '#f59e0b' : '')}
|
|
1198
|
+
${card('💎', t('抢单(RFQ)'), openRfqs > 0 ? openRfqs + ' ' + t('个公开求购') : t('暂无求购'), '#rfqs', openRfqs > 0 ? String(openRfqs) : '')}
|
|
1199
|
+
${card('🏪', t('店铺管理'), t('商品 / 营销 / Skill'), '#seller')}
|
|
1200
|
+
${card('🎨', t('店铺主页'), t('编辑公开店铺装饰'), '#shop-edit')}
|
|
1201
|
+
${card('↩', t('退货管理'), pendingReturns > 0 ? pendingReturns + ' ' + t('待处理') : t('数据 / 历史'), '#returns', pendingReturns || '', pendingReturns > 0 ? '#dc2626' : '')}
|
|
1202
|
+
${card('📊', t('销售分析'), t('GMV / 复购 / 转化'), '#analytics')}
|
|
1203
|
+
${card('⚡', t('我的促销'), t('限时降价管理'), '#my-flash')}
|
|
1204
|
+
${card('🎁', t('签到 / 任务'), t('每日 WAZ + 成长奖励'), '#checkin')}
|
|
1205
|
+
</div>
|
|
1206
|
+
${inDispute > 0 ? `<div onclick="location.hash='#orders'" class="card" style="padding:10px 14px;margin-bottom:10px;cursor:pointer;border-left:3px solid #dc2626;background:#fef2f2;display:flex;align-items:center;gap:8px"><div style="font-size:20px">⚖</div><div style="flex:1;font-size:13px;color:#991b1b">${inDispute} ${t('个争议待处理')}</div><div style="font-size:18px;color:#dc2626">→</div></div>` : ''}
|
|
1207
|
+
`
|
|
1208
|
+
|
|
1209
|
+
// 销售扩展(seller 限定的销售形式 — 拍卖 / 跟卖 / P2P / 链接对标 / 二手)
|
|
1210
|
+
const marketGrid = `
|
|
1211
|
+
<div style="font-size:12px;color:#6b7280;font-weight:600;margin:14px 0 6px">🛍️ ${t('销售扩展')}</div>
|
|
1212
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px">
|
|
1213
|
+
${card('🔨', t('加价竞拍'), t('限量稀缺 · 发起拍卖'), '#auctions')}
|
|
1214
|
+
${card('🏬', t('跟卖加入'), t('多商家同款 · 抢市占'), '#listings')}
|
|
1215
|
+
${card('📋', t('我的跟卖'), t('已上架 listings · 价格竞争位'), '#listings/mine')}
|
|
1216
|
+
${card('🌐', t('P2P 商店'), t('数字商品 / 服务 / 加密发货'), '#p2p-shop')}
|
|
1217
|
+
${card('🔗', t('链接对标'), t('外部独家价 · 链接认领'), '#seller')}
|
|
1218
|
+
${card('♻️', t('个人闲置'), t('二手集市 · 公私分离'), '#secondhand')}
|
|
1219
|
+
</div>
|
|
1220
|
+
`
|
|
1221
|
+
|
|
1222
|
+
// 2026-05-24 toolGrid 整段移除:4 个 tile 全是 Advanced sub-tab 重复(Auto-bid/Webhook/Timeline)+ 数据中心 重复 店铺管理
|
|
1223
|
+
const toolGrid = ''
|
|
1224
|
+
|
|
1225
|
+
// 卖家账户:只留钱包;个人资料/设置已在 Settings sub-tab
|
|
1226
|
+
const commsGrid = `
|
|
1227
|
+
<div style="font-size:12px;color:#6b7280;font-weight:600;margin:14px 0 6px">💰 ${t('账户')}</div>
|
|
1228
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px">
|
|
1229
|
+
${card('💰', t('钱包'), `${Number(wal.balance).toFixed(2)} WAZ`, '#wallet')}
|
|
1230
|
+
</div>
|
|
1231
|
+
`
|
|
1232
|
+
|
|
1233
|
+
// 社交与发现(卖家弱网络 — 同城商家 + 排行)
|
|
1234
|
+
const socialGrid = `
|
|
1235
|
+
<div style="font-size:12px;color:#6b7280;font-weight:600;margin:14px 0 6px">🧭 ${t('社交与发现')}</div>
|
|
1236
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px">
|
|
1237
|
+
${card('📍', t('附近'), t('同城节点 · 面交可达'), '#nearby')}
|
|
1238
|
+
${card('🏆', t('排行榜'), t('热门商品 / 创作者 / 威望'), '#leaderboard')}
|
|
1239
|
+
</div>
|
|
1240
|
+
`
|
|
1241
|
+
|
|
1242
|
+
// 公益:折叠次要区(卖家可参与,与 buyer 对等;admin/verifier 禁参)
|
|
1243
|
+
const charitySection = `
|
|
1244
|
+
<details style="margin:14px 0 10px;background:#fff;border:1px solid #e5e7eb;border-radius:8px">
|
|
1245
|
+
<summary style="padding:10px 14px;cursor:pointer;font-size:13px;color:#6b7280;font-weight:600;display:flex;justify-content:space-between;align-items:center">
|
|
1246
|
+
<span>🌸 ${t('公益')}</span>
|
|
1247
|
+
<span style="font-size:11px;color:#9ca3af">${rep.badge_tier && rep.badge_tier !== 'none' ? rep.badge_tier + ' · ' : ''}${t('威望')} ${Number(rep.prestige_score||0).toFixed(0)}</span>
|
|
1248
|
+
</summary>
|
|
1249
|
+
<div style="padding:8px;border-top:1px solid #f3f4f6;display:grid;grid-template-columns:1fr 1fr;gap:8px">
|
|
1250
|
+
${card('🌸', t('许愿池'), pendingRepays ? pendingRepays + ' ' + t('待还愿') : t('浏览许愿 / 为他人圆梦'), '#wishes', pendingRepays || '')}
|
|
1251
|
+
${card('💝', t('慈善基金'), t('捐款 · 公开账目'), '#wish/fund')}
|
|
1252
|
+
${card('📚', t('我的慈善'), t('我的许愿 / 捐款 / 圆梦记录'), '#wish/mine')}
|
|
1253
|
+
${card('🎁', t('圆梦故事'), t('已圆愿公开故事板'), '#wish/stories')}
|
|
1254
|
+
</div>
|
|
1255
|
+
</details>
|
|
1256
|
+
`
|
|
1257
|
+
|
|
1258
|
+
// 信任与协议(卖家:索赔被诉响应 — 注:外部审核员仅限 buyer 角色申请,seller 不可)
|
|
1259
|
+
const trustGrid = (myClaimTasks > 0) ? `
|
|
1260
|
+
<div style="font-size:12px;color:#6b7280;font-weight:600;margin:14px 0 6px">🛡 ${t('信任与协议')}</div>
|
|
1261
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px">
|
|
1262
|
+
${card('🔎', t('我的验证'), myClaimTasks + ' ' + t('个索赔任务'), '#verify', myClaimTasks)}
|
|
1263
|
+
</div>
|
|
1264
|
+
` : ''
|
|
1265
|
+
|
|
1266
|
+
// 顺序:工作中心 → 销售扩展 → 效率工具 → 资金/沟通 → 信任与协议(条件显示)→ 社交发现 → 公益折叠
|
|
1267
|
+
app.innerHTML = shell(mySubTabsHTML('dashboard') + header + workGrid + marketGrid + commsGrid + trustGrid + socialGrid + charitySection, 'me')
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// ─── 买家 #me 专业版(聚焦购物/订单/AI;慈善折叠次要) ────────
|
|
1271
|
+
async function renderBuyerMyHome(app) {
|
|
1272
|
+
try { refreshCartBadge() } catch {}
|
|
1273
|
+
try {
|
|
1274
|
+
const n = await GET('/notifications?unread=1').catch(() => null)
|
|
1275
|
+
if (n) state.unread = n.unread || 0
|
|
1276
|
+
} catch {}
|
|
1277
|
+
try { await refreshAnnouncementsBadge() } catch {}
|
|
1278
|
+
|
|
1279
|
+
const [profileRes, ordersRes, charityRes, skillsRes, agentRes, eligibilityRes, claimTasksRes, verifierStatusRes, arbEligibilityRes, arbStatusRes] = await Promise.all([
|
|
1280
|
+
GET('/profile').catch(() => null),
|
|
1281
|
+
GET('/orders').catch(() => []),
|
|
1282
|
+
GET('/charity/me').catch(() => null),
|
|
1283
|
+
GET('/skills/mine').catch(() => []),
|
|
1284
|
+
GET('/agents/me/reputation').catch(() => null),
|
|
1285
|
+
GET('/verifier/eligibility').catch(() => null),
|
|
1286
|
+
GET('/claim-tasks/mine').catch(() => null),
|
|
1287
|
+
GET('/verifier/status').catch(() => null),
|
|
1288
|
+
GET('/arbitrator/eligibility').catch(() => null),
|
|
1289
|
+
GET('/arbitrator/status').catch(() => null),
|
|
1290
|
+
])
|
|
1291
|
+
const profile = profileRes && !profileRes.error ? profileRes : null
|
|
1292
|
+
const wal = { balance: Number(profile?.wallet?.balance || 0), staked: Number(profile?.wallet?.staked || 0) }
|
|
1293
|
+
const orders = Array.isArray(ordersRes) ? ordersRes : []
|
|
1294
|
+
const myUid = state.user.id
|
|
1295
|
+
const buyOrders = orders.filter(o => o.buyer_id === myUid)
|
|
1296
|
+
const toPay = buyOrders.filter(o => o.status === 'created').length
|
|
1297
|
+
const toReceive = buyOrders.filter(o => ['shipped','picked_up','in_transit','delivered'].includes(o.status)).length
|
|
1298
|
+
const inDispute = buyOrders.filter(o => o.status === 'disputed').length
|
|
1299
|
+
const charity = charityRes && !charityRes.error ? charityRes : null
|
|
1300
|
+
const rep = charity?.reputation || {}
|
|
1301
|
+
const pendingRepays = (charity?.pending_repayments || []).length
|
|
1302
|
+
const mySkills = Array.isArray(skillsRes) ? skillsRes : []
|
|
1303
|
+
const skillCount = mySkills.length
|
|
1304
|
+
const agentLevel = agentRes?.level || 'new'
|
|
1305
|
+
const agentTrust = Math.round(agentRes?.trust_score || 0)
|
|
1306
|
+
const agentBandColor = { legend:'#dc2626', quality:'#9333ea', trusted:'#4f46e5', new:'#9ca3af' }[agentLevel] || '#6b7280'
|
|
1307
|
+
// 外部审核员状态机(仅 buyer 可申请):
|
|
1308
|
+
// none / rejected → 资格 OK 时显示申请 tile
|
|
1309
|
+
// pending → 申请审核中(不可再点)
|
|
1310
|
+
// approved (whitelist 存在) → 外部审核员 — 显示审核任务等入口
|
|
1311
|
+
// 注:getVerifierState 在 whitelist 存在时返回 tier 名('trial-1' 等),不是 'approved'
|
|
1312
|
+
const verifierEligible = eligibilityRes && !eligibilityRes.error && eligibilityRes.eligible === true
|
|
1313
|
+
const verifierState = (verifierStatusRes && !verifierStatusRes.error) ? verifierStatusRes.state : 'none'
|
|
1314
|
+
// 判 approved:whitelist 行存在(is_system=0 才是外部,但 buyer 走此路径肯定 is_system=0)
|
|
1315
|
+
const isExternalVerifier = !!(verifierStatusRes?.whitelist)
|
|
1316
|
+
const verifierPending = verifierState === 'pending'
|
|
1317
|
+
const verifierSuspended = verifierState === 'suspended' || verifierState === 'cooldown'
|
|
1318
|
+
const verifierTier = verifierStatusRes?.tier || null
|
|
1319
|
+
const verifierRemaining = Number(verifierStatusRes?.remaining ?? 0)
|
|
1320
|
+
// 索赔验证记录:买家相关任务数(发起 + 被诉,buyer 只会是 buyer 视角)
|
|
1321
|
+
const myClaimTasks = claimTasksRes && !claimTasksRes.error ? (claimTasksRes.as_buyer || []).length : 0
|
|
1322
|
+
// 外部审核员相关任务(作为 verifier 视角)
|
|
1323
|
+
const verifierTasks = claimTasksRes && !claimTasksRes.error ? (claimTasksRes.as_verifier || []).length : 0
|
|
1324
|
+
// 外部仲裁员状态机(与 verifier 平行)
|
|
1325
|
+
const arbEligible = arbEligibilityRes && !arbEligibilityRes.error && arbEligibilityRes.eligible === true
|
|
1326
|
+
const arbState = (arbStatusRes && !arbStatusRes.error) ? arbStatusRes.state : 'none'
|
|
1327
|
+
const isExternalArb = !!(arbStatusRes?.whitelist)
|
|
1328
|
+
const arbPending = arbState === 'pending'
|
|
1329
|
+
|
|
1330
|
+
const card = (icon, label, sub, hash, badge, accent) => `
|
|
1331
|
+
<div class="card" onclick="location.hash='${hash}'" style="padding:14px;cursor:pointer;display:flex;align-items:center;gap:10px;min-height:64px;position:relative${accent ? ';border-left:3px solid '+accent : ''}">
|
|
1332
|
+
<div style="font-size:24px;flex-shrink:0">${icon}</div>
|
|
1333
|
+
<div style="flex:1;min-width:0">
|
|
1334
|
+
<div style="font-weight:600;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${label}</div>
|
|
1335
|
+
${sub ? `<div style="font-size:11px;color:#9ca3af;margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${sub}</div>` : ''}
|
|
1336
|
+
</div>
|
|
1337
|
+
${badge ? `<div style="background:#dc2626;color:#fff;border-radius:99px;font-size:10px;padding:2px 7px;min-width:18px;text-align:center;flex-shrink:0">${badge}</div>` : ''}
|
|
1338
|
+
</div>`
|
|
1339
|
+
|
|
1340
|
+
const header = `
|
|
1341
|
+
<div class="card" style="padding:16px;margin-bottom:14px;background:linear-gradient(135deg,#1e3a8a,#1e40af);color:#fff">
|
|
1342
|
+
<div style="display:flex;align-items:center;gap:10px;margin-bottom:12px">
|
|
1343
|
+
<div style="font-size:28px">🛒</div>
|
|
1344
|
+
<div style="flex:1">
|
|
1345
|
+
<div style="font-weight:700;font-size:15px">${escHtml(state.user.name || state.user.handle)}</div>
|
|
1346
|
+
<div style="font-size:11px;opacity:0.85;margin-top:2px">@${escHtml(state.user.handle || '')} · ${t('买家')}</div>
|
|
1347
|
+
</div>
|
|
1348
|
+
</div>
|
|
1349
|
+
<div onclick="location.hash='#wallet'" style="cursor:pointer;padding:10px 12px;background:rgba(255,255,255,0.12);border-radius:8px;display:flex;justify-content:space-between;align-items:center">
|
|
1350
|
+
<div>
|
|
1351
|
+
<div style="font-size:10px;opacity:0.85;text-transform:uppercase;letter-spacing:0.5px">${t('钱包余额')}</div>
|
|
1352
|
+
<div style="font-size:22px;font-weight:800;line-height:1.2">${Number(wal.balance).toFixed(2)} <span style="font-size:13px;font-weight:600">WAZ</span></div>
|
|
1353
|
+
${wal.staked > 0 ? `<div style="font-size:10px;opacity:0.75;margin-top:2px">${t('已锁定')} ${Number(wal.staked).toFixed(2)} WAZ</div>` : ''}
|
|
1354
|
+
</div>
|
|
1355
|
+
<div style="font-size:18px;opacity:0.85">→</div>
|
|
1356
|
+
</div>
|
|
1357
|
+
</div>
|
|
1358
|
+
`
|
|
1359
|
+
|
|
1360
|
+
// 2026-05-24 agentDash 移除 — Advanced sub-tab heroAgent 已展示
|
|
1361
|
+
|
|
1362
|
+
// 我的购物 — 个人交易记录 + 个人资产(去掉发现页内容:限时促销/群组团购/精选/评测/动态/AI 推荐 → 发现 tab)
|
|
1363
|
+
const shopGrid = `
|
|
1364
|
+
<div style="font-size:12px;color:#6b7280;font-weight:600;margin:14px 0 6px">🛒 ${t('我的购物')}</div>
|
|
1365
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px">
|
|
1366
|
+
${card('📦', t('我的订单'), toPay > 0 ? toPay + ' ' + t('待付款') : (toReceive > 0 ? toReceive + ' ' + t('待收货') : t('全部完成')), '#orders', (toPay || toReceive) || '', toPay > 0 ? '#dc2626' : (toReceive > 0 ? '#f59e0b' : ''))}
|
|
1367
|
+
${card('🧺', t('购物车'), state.cartCount ? `${state.cartCount} ${t('件')}` : t('空'), '#cart', state.cartCount || '')}
|
|
1368
|
+
${card('❤', t('心愿单'), t('喜欢的商品 · 降价提醒'), '#wishlist')}
|
|
1369
|
+
${card('⏰', t('补货提醒'), t('缺货商品到货通知'), '#waitlist')}
|
|
1370
|
+
${card('🤝', t('我关注'), t('卖家 / 商品'), '#follows')}
|
|
1371
|
+
${card('↩', t('我的退货'), t('退货申请 / 进度'), '#returns')}
|
|
1372
|
+
${card('🎟️', t('我的优惠券'), t('可用券 · 使用历史'), '#my-coupons')}
|
|
1373
|
+
${state.user?.mlm_ui_visible !== false ? card('🎁', t('邀请奖励'), t('邀请码 · 收益 · 邀请人'), '#referral') : ''}
|
|
1374
|
+
</div>
|
|
1375
|
+
${inDispute > 0 ? `<div onclick="location.hash='#orders'" class="card" style="padding:10px 14px;margin-bottom:10px;cursor:pointer;border-left:3px solid #dc2626;background:#fef2f2;display:flex;align-items:center;gap:8px"><div style="font-size:20px">⚖</div><div style="flex:1;font-size:13px;color:#991b1b">${inDispute} ${t('个争议待处理')}</div><div style="font-size:18px;color:#dc2626">→</div></div>` : ''}
|
|
1376
|
+
`
|
|
1377
|
+
|
|
1378
|
+
// 我的市场记录 — buyer 视角的 my-* 入口(不含跟卖:跟卖是卖家行为;不含拍卖:buyer 没有 auction/mine 概念)
|
|
1379
|
+
const marketGrid = `
|
|
1380
|
+
<div style="font-size:12px;color:#6b7280;font-weight:600;margin:14px 0 6px">📋 ${t('我的市场记录')}</div>
|
|
1381
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px">
|
|
1382
|
+
${card('📝', t('我的笔记'), state.user?.mlm_ui_visible !== false ? t('购买体验分享 · 返佣') : t('购买体验分享 · 公益贡献'), '#me/notes')}
|
|
1383
|
+
${card('💬', t('我的求购'), t('我发布的求购单'), '#rfq/mine')}
|
|
1384
|
+
${card('♻️', t('我的二手'), t('我发布的闲置'), '#secondhand/mine')}
|
|
1385
|
+
${card('🎁', t('我的测评'), t('测评免单申请 + 进度'), '#trials')}
|
|
1386
|
+
</div>
|
|
1387
|
+
`
|
|
1388
|
+
|
|
1389
|
+
// 2026-05-24 Skill / Auto-bid / Timeline 都已在 Advanced sub-tab,此处仅留高频 签到
|
|
1390
|
+
const aiGrid = `
|
|
1391
|
+
<div style="font-size:12px;color:#6b7280;font-weight:600;margin:14px 0 6px">🎁 ${t('日常')}</div>
|
|
1392
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px">
|
|
1393
|
+
${card('🎁', t('签到 / 任务'), t('每日 WAZ + 成长奖励'), '#checkin')}
|
|
1394
|
+
</div>
|
|
1395
|
+
`
|
|
1396
|
+
|
|
1397
|
+
// 2026-05-24 客服/反馈/我的 agents 都已迁移:
|
|
1398
|
+
// - 反馈/客服 → 消息中心的「客服」sub-tab(含「+ 新建反馈」按钮)
|
|
1399
|
+
// - 我的 agents → #me/advanced sub-tab
|
|
1400
|
+
const commsGrid = ''
|
|
1401
|
+
|
|
1402
|
+
// 公益:折叠次要区(买家可主动参与,但不抢主流)— 扩展 4 tile 覆盖捐款/圆梦/我的慈善
|
|
1403
|
+
const charitySection = `
|
|
1404
|
+
<details style="margin:14px 0 10px;background:#fff;border:1px solid #e5e7eb;border-radius:8px">
|
|
1405
|
+
<summary style="padding:10px 14px;cursor:pointer;font-size:13px;color:#6b7280;font-weight:600;display:flex;justify-content:space-between;align-items:center">
|
|
1406
|
+
<span>🌸 ${t('公益')}</span>
|
|
1407
|
+
<span style="font-size:11px;color:#9ca3af">${rep.badge_tier && rep.badge_tier !== 'none' ? rep.badge_tier + ' · ' : ''}${t('威望')} ${Number(rep.prestige_score||0).toFixed(0)}</span>
|
|
1408
|
+
</summary>
|
|
1409
|
+
<div style="padding:8px;border-top:1px solid #f3f4f6;display:grid;grid-template-columns:1fr 1fr;gap:8px">
|
|
1410
|
+
${card('🌸', t('许愿池'), pendingRepays ? pendingRepays + ' ' + t('待还愿') : t('浏览许愿 / 为他人圆梦'), '#wishes', pendingRepays || '')}
|
|
1411
|
+
${card('💝', t('慈善基金'), t('捐款 · 公开账目'), '#wish/fund')}
|
|
1412
|
+
${card('📚', t('我的慈善'), t('我的许愿 / 捐款 / 圆梦记录'), '#wish/mine')}
|
|
1413
|
+
${card('🏆', t('排行榜'), t('热门 / 创作者 / 威望'), '#leaderboard')}
|
|
1414
|
+
</div>
|
|
1415
|
+
</details>
|
|
1416
|
+
`
|
|
1417
|
+
|
|
1418
|
+
// 社交与发现 — 已移除(附近/雷达扫描 属于发现页内容)
|
|
1419
|
+
const socialGrid = ''
|
|
1420
|
+
|
|
1421
|
+
// 信任与协议 — 两个独立状态机(verifier + arbitrator)+ 通用索赔任务 tile
|
|
1422
|
+
// ① 外部审核员区(4 状态)
|
|
1423
|
+
let verifierSection = ''
|
|
1424
|
+
if (isExternalVerifier) {
|
|
1425
|
+
const tierBadge = verifierTier ? `<span style="background:#dcfce7;color:#166534;font-size:10px;padding:1px 7px;border-radius:99px;font-weight:600;margin-left:6px">${verifierTier}</span>` : ''
|
|
1426
|
+
verifierSection = `
|
|
1427
|
+
<div style="font-size:12px;color:#6b7280;font-weight:600;margin:14px 0 6px;display:flex;align-items:center">🔍 ${t('外部审核员')}${tierBadge}<span style="font-size:10px;color:#9ca3af;margin-left:6px">${t('今日剩余')} ${verifierRemaining}</span></div>
|
|
1428
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px">
|
|
1429
|
+
${card('🔍', t('审核任务'), t('可接 / 已接未投 / 已投'), '#verify-tasks', verifierTasks || '')}
|
|
1430
|
+
${card('📩', t('我要申诉'), t('针对争议判定'), '#verifier-appeal')}
|
|
1431
|
+
</div>
|
|
1432
|
+
`
|
|
1433
|
+
} else if (verifierPending) {
|
|
1434
|
+
verifierSection = `
|
|
1435
|
+
<div class="card" style="padding:12px;margin:14px 0 10px;background:linear-gradient(135deg,#fef3c7,#fde68a);border-color:#fbbf24;display:flex;align-items:center;gap:10px">
|
|
1436
|
+
<div style="font-size:22px">⏳</div>
|
|
1437
|
+
<div style="flex:1">
|
|
1438
|
+
<div style="font-size:13px;font-weight:600;color:#92400e">${t('审核员申请审核中')}</div>
|
|
1439
|
+
<div style="font-size:11px;color:#92400e;opacity:0.85;margin-top:2px">${t('管理员审批后将通知你')}</div>
|
|
1440
|
+
</div>
|
|
1441
|
+
</div>
|
|
1442
|
+
`
|
|
1443
|
+
} else if (verifierEligible) {
|
|
1444
|
+
verifierSection = `
|
|
1445
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px">
|
|
1446
|
+
${card('🎖', t('申请审核员'), t('资格已达标 — 申请加入'), '#apply-verifier')}
|
|
1447
|
+
</div>
|
|
1448
|
+
`
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
// ② 外部仲裁员区(4 状态 — 与 verifier 平行)
|
|
1452
|
+
let arbSection = ''
|
|
1453
|
+
if (isExternalArb) {
|
|
1454
|
+
arbSection = `
|
|
1455
|
+
<div style="font-size:12px;color:#6b7280;font-weight:600;margin:14px 0 6px;display:flex;align-items:center">⚖ ${t('外部仲裁员')}<span style="background:#ede9fe;color:#6b21a8;font-size:10px;padding:1px 7px;border-radius:99px;font-weight:600;margin-left:6px">${t('已批准')}</span></div>
|
|
1456
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px">
|
|
1457
|
+
${card('⚖', t('仲裁台'), t('待响应 / 仲裁中 / 已结'), '#disputes')}
|
|
1458
|
+
</div>
|
|
1459
|
+
`
|
|
1460
|
+
} else if (arbPending) {
|
|
1461
|
+
arbSection = `
|
|
1462
|
+
<div class="card" style="padding:12px;margin:14px 0 10px;background:linear-gradient(135deg,#ede9fe,#ddd6fe);border-color:#a78bfa;display:flex;align-items:center;gap:10px">
|
|
1463
|
+
<div style="font-size:22px">⏳</div>
|
|
1464
|
+
<div style="flex:1">
|
|
1465
|
+
<div style="font-size:13px;font-weight:600;color:#6b21a8">${t('仲裁员申请审核中')}</div>
|
|
1466
|
+
<div style="font-size:11px;color:#6b21a8;opacity:0.85;margin-top:2px">${t('管理员审批后将通知你')}</div>
|
|
1467
|
+
</div>
|
|
1468
|
+
</div>
|
|
1469
|
+
`
|
|
1470
|
+
} else if (arbEligible) {
|
|
1471
|
+
arbSection = `
|
|
1472
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px">
|
|
1473
|
+
${card('⚖', t('申请仲裁员'), t('资格已达标 — 申请加入'), '#apply-arbitrator')}
|
|
1474
|
+
</div>
|
|
1475
|
+
`
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
// 阶段 4(#1093):新治理 onboarding 入口 — 始终可访问我的治理岗位面板(卸任 / 申诉 / 历史)
|
|
1479
|
+
const governanceMeSection = (isExternalArb || isExternalVerifier) ? `
|
|
1480
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px">
|
|
1481
|
+
${card('🏛', t('我的治理岗位'), t('在岗 / 申诉 / 卸任'), '#governance-me')}
|
|
1482
|
+
</div>
|
|
1483
|
+
` : ''
|
|
1484
|
+
|
|
1485
|
+
// ③ 通用索赔任务 tile
|
|
1486
|
+
const claimsTile = myClaimTasks > 0 ? `
|
|
1487
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px">
|
|
1488
|
+
${card('🔎', t('我的验证'), myClaimTasks + ' ' + t('个索赔任务'), '#verify', myClaimTasks)}
|
|
1489
|
+
</div>
|
|
1490
|
+
` : ''
|
|
1491
|
+
|
|
1492
|
+
// 拼装:有内容才显标题
|
|
1493
|
+
const hasAnyTrust = verifierSection || arbSection || claimsTile || governanceMeSection
|
|
1494
|
+
const trustGrid = hasAnyTrust ? `
|
|
1495
|
+
${(!isExternalVerifier && !isExternalArb) ? `<div style="font-size:12px;color:#6b7280;font-weight:600;margin:14px 0 6px">🛡 ${t('信任与协议')}</div>` : ''}
|
|
1496
|
+
${verifierSection}
|
|
1497
|
+
${arbSection}
|
|
1498
|
+
${governanceMeSection}
|
|
1499
|
+
${claimsTile}
|
|
1500
|
+
` : ''
|
|
1501
|
+
|
|
1502
|
+
// 账户与配置 — 个人配置集中区
|
|
1503
|
+
const settingsGrid = `
|
|
1504
|
+
<div style="font-size:12px;color:#6b7280;font-weight:600;margin:14px 0 6px">⚙️ ${t('账户与配置')}</div>
|
|
1505
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px">
|
|
1506
|
+
${card('📍', t('收货地址簿'), t('常用地址 · 一键填充'), '#addresses')}
|
|
1507
|
+
${card('🆔', t('实名认证'), t('提升账户可信度'), '#kyc')}
|
|
1508
|
+
${card('🚫', t('我的黑名单'), t('屏蔽不想看的人'), '#blocklist')}
|
|
1509
|
+
${card('👁', t('公开主页'), '@' + (state.user.handle || state.user.name), '#u/' + state.user.id)}
|
|
1510
|
+
${card('🏛', t('协议治理'), t('参数公开 · 变更可追溯'), '#governance')}
|
|
1511
|
+
</div>
|
|
1512
|
+
`
|
|
1513
|
+
|
|
1514
|
+
// 顺序:我的购物 → 我的市场记录 → Agent 进阶 → 通信 → 信任与协议(条件显示)→ 公益折叠 → 账户与配置
|
|
1515
|
+
app.innerHTML = shell(mySubTabsHTML('dashboard') + header + notePromptPlaceholder('me') + shopGrid + marketGrid + aiGrid + commsGrid + trustGrid + socialGrid + charitySection + settingsGrid, 'me')
|
|
1516
|
+
hydrateNotePrompt('me')
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
// ─── #me 私人 hub ──────────────────────────────────────────────
|
|
1520
|
+
// 2026-05-24 #me sticky sub-tab 助手 — Dashboard / 设置 / 高级
|
|
1521
|
+
window.mySubTabsHTML = function(active) {
|
|
1522
|
+
const tab = (key, icon, label) => {
|
|
1523
|
+
const isActive = active === key
|
|
1524
|
+
return `<button onclick="navigate('#me${key === 'dashboard' ? '' : '/' + key}')" style="
|
|
1525
|
+
flex:1;background:${isActive ? '#4f46e5' : '#fff'};color:${isActive ? '#fff' : '#374151'};
|
|
1526
|
+
border:1px solid ${isActive ? '#4f46e5' : '#e5e7eb'};border-radius:99px;
|
|
1527
|
+
padding:8px 4px;font-size:12px;font-weight:600;cursor:pointer;
|
|
1528
|
+
display:flex;align-items:center;justify-content:center;gap:5px;min-width:0
|
|
1529
|
+
">${icon}<span>${label}</span></button>`
|
|
1530
|
+
}
|
|
1531
|
+
return `<div style="display:flex;gap:6px;margin-bottom:14px;position:sticky;top:60px;z-index:5;background:rgba(249,250,251,0.95);backdrop-filter:blur(8px);padding:8px 0">
|
|
1532
|
+
${tab('dashboard', '🏠', t('面板'))}
|
|
1533
|
+
${tab('settings', '⚙️', t('设置'))}
|
|
1534
|
+
${tab('advanced', '🚀', t('高级'))}
|
|
1535
|
+
</div>`
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
async function renderMyHome(app, subTab) {
|
|
1539
|
+
if (!state.user) { renderLogin(); return }
|
|
1540
|
+
subTab = subTab || 'dashboard'
|
|
1541
|
+
|
|
1542
|
+
// settings / advanced sub-tabs 走独立 renderer
|
|
1543
|
+
if (subTab === 'settings') return renderMySettings(app)
|
|
1544
|
+
if (subTab === 'advanced') return renderMyAdvanced(app)
|
|
1545
|
+
|
|
1546
|
+
// dashboard: 按角色分支(现有 renderer 内部会注入 mySubTabsHTML)
|
|
1547
|
+
app.innerHTML = shell(loading$(), 'me')
|
|
1548
|
+
const role = state.user.role
|
|
1549
|
+
const TRUSTED_ROLES = ['admin', 'verifier', 'logistics', 'arbitrator']
|
|
1550
|
+
if (TRUSTED_ROLES.includes(role)) {
|
|
1551
|
+
return renderTrustedMyHome(app, role)
|
|
1552
|
+
}
|
|
1553
|
+
if (role === 'seller') {
|
|
1554
|
+
return renderSellerMyHome(app)
|
|
1555
|
+
}
|
|
1556
|
+
if (role === 'buyer') {
|
|
1557
|
+
return renderBuyerMyHome(app)
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
// P1.3 修复:进入 #me 前主动刷新 cart + 通知 + 未读数
|
|
1561
|
+
try { refreshCartBadge() } catch {}
|
|
1562
|
+
try {
|
|
1563
|
+
const n = await GET('/notifications?unread=1').catch(() => null)
|
|
1564
|
+
if (n) state.unread = n.unread || 0
|
|
1565
|
+
} catch {}
|
|
1566
|
+
|
|
1567
|
+
// 并行拉关键数据:wallet, charity, agent reputation, skills mine
|
|
1568
|
+
const [profileRes, charityRes, agentRes, skillsRes] = await Promise.all([
|
|
1569
|
+
GET('/profile').catch(e => ({ error: '_net_' })),
|
|
1570
|
+
GET('/charity/me').catch(e => ({ error: '_net_' })),
|
|
1571
|
+
GET('/agents/me/reputation').catch(() => null),
|
|
1572
|
+
GET('/skills/mine').catch(() => []),
|
|
1573
|
+
])
|
|
1574
|
+
const profile = profileRes?.error ? null : profileRes
|
|
1575
|
+
const charity = charityRes?.error ? null : charityRes
|
|
1576
|
+
const agentRep = agentRes
|
|
1577
|
+
const mySkills = Array.isArray(skillsRes) ? skillsRes : []
|
|
1578
|
+
// P1.1 + P2.3 修复:NaN 兜底 + 显示加载错误
|
|
1579
|
+
const wal = { balance: Number(profile?.wallet?.balance || 0), staked: Number(profile?.wallet?.staked || 0) }
|
|
1580
|
+
const rep = charity?.reputation || {}
|
|
1581
|
+
const pendingRepays = (charity?.pending_repayments || []).length
|
|
1582
|
+
const loadErrors = []
|
|
1583
|
+
if (!profile) loadErrors.push(t('钱包'))
|
|
1584
|
+
if (!charity) loadErrors.push(t('慈善'))
|
|
1585
|
+
|
|
1586
|
+
// P2.2 修复:长 sub 文本 ellipsis 避免高度跳动
|
|
1587
|
+
const card = (icon, label, sub, hash, badge) => `
|
|
1588
|
+
<div class="card" onclick="location.hash='${hash}'" style="padding:14px;cursor:pointer;display:flex;align-items:center;gap:10px;min-height:64px;position:relative">
|
|
1589
|
+
<div style="font-size:24px;flex-shrink:0">${icon}</div>
|
|
1590
|
+
<div style="flex:1;min-width:0">
|
|
1591
|
+
<div style="font-weight:600;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${label}</div>
|
|
1592
|
+
${sub ? `<div style="font-size:11px;color:#9ca3af;margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${sub}</div>` : ''}
|
|
1593
|
+
</div>
|
|
1594
|
+
${badge ? `<div style="background:#dc2626;color:#fff;border-radius:99px;font-size:10px;padding:2px 7px;min-width:18px;text-align:center;flex-shrink:0">${badge}</div>` : ''}
|
|
1595
|
+
</div>`
|
|
1596
|
+
|
|
1597
|
+
// D3 Agent 仪表盘 widget — 我的 Agent 在做什么
|
|
1598
|
+
const skillCount = mySkills.length
|
|
1599
|
+
const activeSubs = mySkills.filter(s => s.subscribed_count > 0 || s.is_subscribed).length
|
|
1600
|
+
const agentLevel = agentRep?.level || 'new'
|
|
1601
|
+
const agentTrust = Math.round(agentRep?.trust_score || 0)
|
|
1602
|
+
const agentBandColor = { legend:'#dc2626', quality:'#9333ea', trusted:'#4f46e5', new:'#9ca3af' }[agentLevel] || '#6b7280'
|
|
1603
|
+
// 2026-05-24 agentDash 移除 — Advanced sub-tab heroAgent 已展示
|
|
1604
|
+
|
|
1605
|
+
// 通用入口(通知 / 私信归"消息"tab 专管,此处不重复)
|
|
1606
|
+
const commonGrid = `
|
|
1607
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px">
|
|
1608
|
+
${card('💰', t('钱包'), `${Number(wal.balance).toFixed(2)} WAZ`, '#wallet')}
|
|
1609
|
+
${card('📦', t('订单'), t('我买我卖'), '#orders')}
|
|
1610
|
+
${card('🌸', t('慈善许愿'), `${rep.badge_tier && rep.badge_tier !== 'none' ? rep.badge_tier + ' · ' : ''}${t('威望')} ${Number(rep.prestige_score||0).toFixed(0)}`, '#wishes', pendingRepays || '')}
|
|
1611
|
+
${card('🏆', t('排行榜'), t('热门 / 创作者 / 威望'), '#leaderboard')}
|
|
1612
|
+
</div>
|
|
1613
|
+
`
|
|
1614
|
+
|
|
1615
|
+
// 买家专属
|
|
1616
|
+
const buyerGrid = role === 'buyer' ? `
|
|
1617
|
+
<div style="font-size:12px;color:#6b7280;font-weight:600;margin:14px 0 6px">📡 ${t('买家专区')}</div>
|
|
1618
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px">
|
|
1619
|
+
${card('📡', t('分享管理'), t('推广 / 邀请 / 佣金'), '#promoter')}
|
|
1620
|
+
${card('🤖', t('AI 推荐'), t('给我推商品'), '#ai-recommend')}
|
|
1621
|
+
${card('🛒', t('购物车'), state.cartCount ? `${state.cartCount} ${t('件')}` : t('空'), '#cart')}
|
|
1622
|
+
${card('🤝', t('我关注'), t('卖家 / 商品'), '#follows')}
|
|
1623
|
+
</div>
|
|
1624
|
+
` : ''
|
|
1625
|
+
|
|
1626
|
+
// 卖家专属(上移:放通用前更显眼)
|
|
1627
|
+
const sellerGrid = role === 'seller' ? `
|
|
1628
|
+
<div style="font-size:12px;color:#6b7280;font-weight:600;margin:14px 0 6px">🏪 ${t('卖家专区')}</div>
|
|
1629
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px">
|
|
1630
|
+
${card('💎', t('我的拍卖'), t('发布 / 中标记录'), '#auction/mine')}
|
|
1631
|
+
${card('🌐', t('P2P 原生商店'), t('本地节点商品'), '#p2p-shop')}
|
|
1632
|
+
${card('⚡', t('Skill 市场'), t('我的技能 / 订阅'), '#skills')}
|
|
1633
|
+
${card('🤖', t('自动报价'), t('auto_bid 配置'), '#auto-bid')}
|
|
1634
|
+
${card('📊', t('数据中心'), t('销售分析 / 趋势'), '#seller')}
|
|
1635
|
+
${card('🎓', t('Skill 训练营'), t('如何高效使用'), '#skills')}
|
|
1636
|
+
</div>
|
|
1637
|
+
` : ''
|
|
1638
|
+
|
|
1639
|
+
// 本月统计折叠(所有角色)— 数据从已有 charity 接口取
|
|
1640
|
+
const monthlyStats = `
|
|
1641
|
+
<details style="margin-bottom:10px;background:#fff;border:1px solid #e5e7eb;border-radius:8px">
|
|
1642
|
+
<summary style="padding:10px 14px;cursor:pointer;font-size:13px;color:#374151;font-weight:600">📈 ${t('本月统计')}</summary>
|
|
1643
|
+
<div style="padding:8px 14px;border-top:1px solid #f3f4f6;display:grid;grid-template-columns:repeat(3,1fr);gap:8px;font-size:11px;text-align:center">
|
|
1644
|
+
<div><div style="font-size:18px;font-weight:700;color:#4f46e5">${rep.wishes_made || 0}</div><div style="color:#9ca3af">${t('许愿')}</div></div>
|
|
1645
|
+
<div><div style="font-size:18px;font-weight:700;color:#dc2626">${rep.wishes_fulfilled || 0}</div><div style="color:#9ca3af">${t('圆梦')}</div></div>
|
|
1646
|
+
<div><div style="font-size:18px;font-weight:700;color:#9333ea">${Number(rep.donation_total||0).toFixed(1)}</div><div style="color:#9ca3af">${t('捐款 WAZ')}</div></div>
|
|
1647
|
+
</div>
|
|
1648
|
+
</details>
|
|
1649
|
+
`
|
|
1650
|
+
|
|
1651
|
+
// 公开主页 + 设置 + 高级工具(所有角色)
|
|
1652
|
+
const settingsGrid = `
|
|
1653
|
+
<div style="font-size:12px;color:#6b7280;font-weight:600;margin:14px 0 6px">⚙️ ${t('账户')}</div>
|
|
1654
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px">
|
|
1655
|
+
${card('👁', t('公开主页'), '@' + (state.user.handle || state.user.name), '#u/' + state.user.id)}
|
|
1656
|
+
${card('🏛', t('协议治理'), t('参数公开 · 变更可追溯'), '#governance')}
|
|
1657
|
+
</div>
|
|
1658
|
+
`
|
|
1659
|
+
|
|
1660
|
+
const header = `
|
|
1661
|
+
<div class="card" style="padding:16px;margin-bottom:14px;background:linear-gradient(135deg,#eef2ff,#f0f9ff)">
|
|
1662
|
+
<div style="display:flex;align-items:center;gap:10px;margin-bottom:12px">
|
|
1663
|
+
<div style="font-size:28px">👤</div>
|
|
1664
|
+
<div style="flex:1">
|
|
1665
|
+
<div style="font-weight:700;font-size:15px">${escHtml(state.user.name || state.user.handle)}</div>
|
|
1666
|
+
<div style="font-size:11px;color:#6b7280;margin-top:2px">@${escHtml(state.user.handle || '')} · ${t({buyer:'买家',seller:'卖家',admin:'管理员',logistics:'物流',arbitrator:'仲裁',verifier:'审核员'}[role] || role)}</div>
|
|
1667
|
+
</div>
|
|
1668
|
+
</div>
|
|
1669
|
+
<div onclick="location.hash='#wallet'" style="cursor:pointer;padding:10px 12px;background:rgba(255,255,255,0.6);border-radius:8px;display:flex;justify-content:space-between;align-items:center">
|
|
1670
|
+
<div>
|
|
1671
|
+
<div style="font-size:10px;color:#6b7280;text-transform:uppercase;letter-spacing:0.5px">${t('钱包余额')}</div>
|
|
1672
|
+
<div style="font-size:22px;font-weight:800;color:#3730a3;line-height:1.2">${Number(wal.balance).toFixed(2)} <span style="font-size:13px;font-weight:600">WAZ</span></div>
|
|
1673
|
+
${wal.staked > 0 ? `<div style="font-size:10px;color:#9ca3af;margin-top:2px">${t('已锁定')} ${Number(wal.staked).toFixed(2)} WAZ</div>` : ''}
|
|
1674
|
+
</div>
|
|
1675
|
+
<div style="font-size:18px;color:#4f46e5">→</div>
|
|
1676
|
+
</div>
|
|
1677
|
+
</div>
|
|
1678
|
+
`
|
|
1679
|
+
|
|
1680
|
+
// P2.3 加载错误提示
|
|
1681
|
+
const errBanner = loadErrors.length > 0 ? `
|
|
1682
|
+
<div style="background:#fef2f2;border:1px solid #fecaca;color:#b91c1c;padding:8px 12px;border-radius:8px;font-size:11px;margin-bottom:10px">
|
|
1683
|
+
⚠ ${t('部分数据加载失败')}: ${loadErrors.join(' · ')} · <a href="javascript:renderMyHome(document.getElementById('app'))" style="color:#b91c1c;text-decoration:underline">${t('重试')}</a>
|
|
1684
|
+
</div>
|
|
1685
|
+
` : ''
|
|
1686
|
+
// 卖家专区上移到 commonGrid 前;其他角色保持原顺序
|
|
1687
|
+
// Agent dash 始终在头部之下、内容区之上
|
|
1688
|
+
const sections = role === 'seller'
|
|
1689
|
+
? header + errBanner + sellerGrid + commonGrid + monthlyStats + settingsGrid
|
|
1690
|
+
: header + errBanner + commonGrid + buyerGrid + monthlyStats + settingsGrid
|
|
1691
|
+
app.innerHTML = shell(mySubTabsHTML('dashboard') + sections, 'me')
|
|
1692
|
+
}
|