feat: 新增藏品和用户Tab
- 新增 💎藏品 和 👤用户 两个Tab - 藏品列表:按品类筛选+名称/冠号搜索,点击进入详情 - 藏品详情:品类/系列/年份/冠号/描述/关联信息 - 藏品详情可跳转关联帖子 - 用户列表:用户名搜索,展示商家/个人/发帖数/信用分 - 修复 page_size 上限500导致422错误 - 帖子列表增加服务器来源标识 - 修复 ON CONFLICT 时 instance_id 被覆盖问题
This commit is contained in:
parent
52ece4f412
commit
35d238555f
|
|
@ -0,0 +1,684 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
|
<title>一尘数据</title>
|
||||||
|
<style>
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}
|
||||||
|
body{font-family:-apple-system,BlinkMacSystemFont,PingFang SC,Helvetica,Arial,sans-serif;background:#f2f2f2;font-size:14px;color:#333;min-height:100vh;padding-bottom:60px}
|
||||||
|
a{color:inherit;text-decoration:none}
|
||||||
|
|
||||||
|
/* 顶部 */
|
||||||
|
.header{background:linear-gradient(135deg,#1a1a2e,#16213e);color:#fff;padding:12px 16px;position:sticky;top:0;z-index:50;display:flex;align-items:center;justify-content:space-between}
|
||||||
|
.header h1{font-size:17px;font-weight:600}
|
||||||
|
.hdr-btn{font-size:20px;background:none;border:none;cursor:pointer}
|
||||||
|
|
||||||
|
/* 统计 */
|
||||||
|
.stats{padding:12px;display:grid;grid-template-columns:1fr 1fr;gap:10px}
|
||||||
|
.stat{background:#fff;border-radius:10px;padding:14px;text-align:center;cursor:pointer}
|
||||||
|
.stat:active{background:#f0f0f0}
|
||||||
|
.s-num{font-size:26px;font-weight:700;margin-bottom:4px}
|
||||||
|
.s-num.blue{color:#1976d2}.s-num.green{color:#2e7d32}.s-num.orange{color:#e65100}.s-num.red{color:#c62828}
|
||||||
|
.s-label{font-size:12px;color:#999}
|
||||||
|
.stat.full{grid-column:1/-1;display:flex;justify-content:space-around}
|
||||||
|
|
||||||
|
/* 筛选 */
|
||||||
|
.f-bar{background:#fff;padding:10px 12px;border-bottom:1px solid #eee;overflow-x:auto;white-space:nowrap}
|
||||||
|
.f-row{display:flex;gap:8px}
|
||||||
|
.f-btn{flex-shrink:0;padding:5px 14px;border:1px solid #ddd;border-radius:16px;font-size:13px;background:#fff;color:#666;cursor:pointer}
|
||||||
|
.f-btn:active,.f-btn.on{background:#1a1a2e;color:#fff;border-color:#1a1a2e}
|
||||||
|
|
||||||
|
/* 列表 */
|
||||||
|
.list{padding:10px 12px;padding-bottom:65px}
|
||||||
|
.post{background:#fff;border-radius:10px;padding:12px;margin-bottom:10px;cursor:pointer}
|
||||||
|
.post:active{background:#fafafa}
|
||||||
|
.post-title{font-size:14px;font-weight:500;line-height:1.5;margin-bottom:8px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
||||||
|
.post-meta{display:flex;flex-wrap:wrap;gap:5px;align-items:center;font-size:12px}
|
||||||
|
.badge{padding:2px 7px;border-radius:4px;font-weight:500}
|
||||||
|
.b-deal{background:#e8f5e9;color:#2e7d32}.b-want{background:#fff3e0;color:#e65100}.b-normal{background:#e3f2fd;color:#1565c0}.b-lifebuoy{background:#ffebee;color:#c62828}
|
||||||
|
.price{color:#e53935;font-weight:600}
|
||||||
|
.author{color:#1976d2}.tm{color:#999}
|
||||||
|
|
||||||
|
/* 分页 */
|
||||||
|
.pager{display:flex;justify-content:center;align-items:center;gap:12px;padding:14px;background:#fff;border-radius:10px;margin-top:10px}
|
||||||
|
.pg-btn{padding:8px 18px;background:#f5f5f5;border:1px solid #ddd;border-radius:6px;font-size:14px;cursor:pointer}
|
||||||
|
.pg-btn:disabled{opacity:.4;cursor:not-allowed}
|
||||||
|
.pg-info{font-size:13px;color:#999}
|
||||||
|
/* 统计页 */
|
||||||
|
.stats-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}
|
||||||
|
.st-card{background:#fff;border-radius:10px;padding:16px;text-align:center;cursor:pointer}
|
||||||
|
.st-card:active{background:#f0f0f0}
|
||||||
|
.st-num{font-size:32px;font-weight:700;color:#e65100;line-height:1.2}
|
||||||
|
.st-card.st-deal .st-num{color:#2e7d32}
|
||||||
|
.st-label{font-size:14px;color:#333;margin:6px 0 4px;font-weight:500}
|
||||||
|
.st-price{font-size:12px;color:#999}
|
||||||
|
|
||||||
|
/* 空/加载 */
|
||||||
|
.empty,.loading{text-align:center;padding:50px;color:#999}
|
||||||
|
.empty::before{content:'📭';font-size:40px;display:block;margin-bottom:10px}
|
||||||
|
|
||||||
|
/* 底部导航 */
|
||||||
|
.tab{position:fixed;bottom:0;left:0;right:0;background:#fff;border-top:1px solid #eee;display:flex;z-index:50;padding-bottom:env(safe-area-inset-bottom,0)}
|
||||||
|
.tab-i{flex:1;text-align:center;padding:10px 0;font-size:11px;color:#999;cursor:pointer}
|
||||||
|
.tab-i.on{color:#1a1a2e}
|
||||||
|
.tab-i span{display:block;font-size:22px;margin-bottom:2px}
|
||||||
|
|
||||||
|
/* 详情页 */
|
||||||
|
.detail{position:fixed;inset:0;background:#fff;z-index:100;overflow-y:auto}
|
||||||
|
.detail-hdr{position:sticky;top:0;background:#fff;padding:12px 16px;border-bottom:1px solid #eee;display:flex;align-items:center;justify-content:space-between;z-index:5}
|
||||||
|
.back{background:none;border:none;font-size:16px;color:#1976d2;cursor:pointer}
|
||||||
|
.detail-link{font-size:13px;color:#1976d2}
|
||||||
|
.detail-body{padding:16px}
|
||||||
|
.detail-title{font-size:16px;font-weight:600;line-height:1.5;margin-bottom:14px}
|
||||||
|
.sec{margin-bottom:20px}
|
||||||
|
.sec-title{font-size:13px;font-weight:600;margin-bottom:10px;padding-bottom:6px;border-bottom:2px solid #1a1a2e}
|
||||||
|
.info-g{ display:grid;grid-template-columns:1fr 1fr;gap:10px}
|
||||||
|
.info-i{display:flex;flex-direction:column;gap:2px}
|
||||||
|
.info-l{font-size:11px;color:#999}
|
||||||
|
.info-v{font-size:14px;color:#333}
|
||||||
|
.info-v.prime{font-size:22px;color:#e53935;font-weight:700}
|
||||||
|
.content{background:#f8f9fa;padding:12px;border-radius:8px;white-space:pre-wrap;font-size:14px;line-height:1.7;word-break:break-all;max-height:300px;overflow-y:auto}
|
||||||
|
.tag-l{display:flex;flex-wrap:wrap;gap:6px}
|
||||||
|
.tag{padding:3px 10px;background:#e3f2fd;color:#1565c0;border-radius:12px;font-size:12px}
|
||||||
|
|
||||||
|
/* 品类 */
|
||||||
|
.cat-list{background:#fff;border-radius:10px;padding:12px;margin:0 12px 80px}
|
||||||
|
.cat-row{display:flex;justify-content:space-between;padding:8px 0;border-bottom:1px solid #f0f0f0;font-size:13px}
|
||||||
|
.cat-row:last-child{border:none}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="app"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const API = '/api/v1'
|
||||||
|
let S = {
|
||||||
|
v: 'home', // home | list | detail
|
||||||
|
stats: {},
|
||||||
|
posts: [],
|
||||||
|
cur: null,
|
||||||
|
loading: false,
|
||||||
|
pg: 1,
|
||||||
|
ps: 300,
|
||||||
|
total: 0,
|
||||||
|
ft: { type: '', cat: '', lifebuoy: '' }
|
||||||
|
}
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
const app = document.getElementById('app')
|
||||||
|
if (S.v === 'home') {
|
||||||
|
const s = S.stats
|
||||||
|
app.innerHTML = `
|
||||||
|
<div class="header">
|
||||||
|
<h1>📊 一尘数据分析</h1>
|
||||||
|
<button class="hdr-btn" onclick="loadStats()">🔄</button>
|
||||||
|
</div>
|
||||||
|
<div class="stats">
|
||||||
|
<div class="stat" onclick="goList('deal')">
|
||||||
|
<div class="s-num green">${s.deal_count||0}</div>
|
||||||
|
<div class="s-label">今日出售</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat" onclick="goList('want')">
|
||||||
|
<div class="s-num orange">${s.want_count||0}</div>
|
||||||
|
<div class="s-label">今日求购</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat" onclick="goList('')">
|
||||||
|
<div class="s-num blue">${s.today_count||0}</div>
|
||||||
|
<div class="s-label">今日新增</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat" onclick="goList('lifebuoy')">
|
||||||
|
<div class="s-num red">${s.lifebuoy_count||0}</div>
|
||||||
|
<div class="s-label">大救生圈</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat full">
|
||||||
|
<div>
|
||||||
|
<div class="s-num blue" style="font-size:20px">${s.total_posts||0}</div>
|
||||||
|
<div class="s-label">总帖子</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style="font-size:22px">🟢</div>
|
||||||
|
<div class="s-label">在线</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="padding:0 12px 20px">
|
||||||
|
<div class="sec-title">📋 品类分布</div>
|
||||||
|
<div class="cat-list">
|
||||||
|
<div style="margin-bottom:12px">
|
||||||
|
<div style="font-size:12px;color:#1976d2;margin-bottom:8px">📅 当日新增</div>
|
||||||
|
${((S.catReport&&S.catReport.today&&S.catReport.today.categories)||[]).map(c=>`
|
||||||
|
<div class="cat-row" onclick="goList('','today','${c.category}')">
|
||||||
|
<span>${c.category}</span>
|
||||||
|
<span style="color:#1976d2;font-weight:600">${c.count}条</span>
|
||||||
|
</div>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style="font-size:12px;color:#e65100;margin-bottom:8px">⏰ 近1小时新增</div>
|
||||||
|
${((S.catReport&&S.catReport.last_1h&&S.catReport.last_1h.categories)||[]).map(c=>`
|
||||||
|
<div class="cat-row" onclick="goList('','1h','${c.category}')">
|
||||||
|
<span>${c.category}</span>
|
||||||
|
<span style="color:#e65100;font-weight:600">${c.count}条</span>
|
||||||
|
</div>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tab">
|
||||||
|
<div class="tab-i on" onclick="S.v='home';render()"><span>📊</span>首页</div>
|
||||||
|
<div class="tab-i" onclick="goList('')"><span>📋</span>帖子</div>
|
||||||
|
<div class="tab-i" onclick="goCollections()"><span>💎</span>藏品</div>
|
||||||
|
<div class="tab-i" onclick="goUsers()"><span>👤</span>用户</div>
|
||||||
|
<div class="tab-i" onclick="S.v='stats';loadStats()"><span>📈</span>统计</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (S.v === 'collections') {
|
||||||
|
const cols = S.collections || []
|
||||||
|
app.innerHTML = `
|
||||||
|
<div class="header"><h1>💎 藏品列表</h1><button class="hdr-btn" onclick="loadCollections()">🔄</button></div>
|
||||||
|
<div class="filter-bar">
|
||||||
|
<select id="col-cat" onchange="S.colCat=this.value;render()">
|
||||||
|
<option value="">全部品类</option>
|
||||||
|
<option value="龙钞">🐉 龙钞</option>
|
||||||
|
<option value="马钞">🐴 马钞</option>
|
||||||
|
<option value="蛇钞">🐍 蛇钞</option>
|
||||||
|
<option value="其他">📦 其他</option>
|
||||||
|
</select>
|
||||||
|
<input type="text" id="col-search" placeholder="搜索名称/冠号" onkeyup="S.colSearch=this.value;render()" style="flex:1;padding:8px;border:1px solid #ddd;border-radius:6px">
|
||||||
|
</div>
|
||||||
|
<div class="list">
|
||||||
|
${cols.filter(c => (!S.colCat || c.category === S.colCat) && (!S.colSearch || (c.name&&c.name.includes(S.colSearch)) || (c.crown_code&&c.crown_code.includes(S.colSearch)))).map(c => `
|
||||||
|
<div class="post-item" onclick="goCollectionDetail('${c.id}')">
|
||||||
|
<div class="post-title">${c.name || c.crown_code || '无名'}</div>
|
||||||
|
<div class="post-meta">
|
||||||
|
<span class="${c.category==='龙钞'?'green':c.category==='马钞'?'orange':c.category==='蛇钞'?'purple':'gray'}">${c.category||'其他'}</span>
|
||||||
|
<span>🏷️ ${c.series||'-'}</span>
|
||||||
|
<span>📅 ${c.issue_year||'-'}</span>
|
||||||
|
<span>🖥️ ${c.instance_id?c.instance_id.replace('server_','S'):'-'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('')}
|
||||||
|
${cols.length===0?'<div style="padding:40px;text-align:center;color:#999">暂无藏品,点击刷新</div>':''}
|
||||||
|
</div>
|
||||||
|
<div class="tab">
|
||||||
|
<div class="tab-i" onclick="S.v='home';render()"><span>📊</span>首页</div>
|
||||||
|
<div class="tab-i" onclick="goList('')"><span>📋</span>帖子</div>
|
||||||
|
<div class="tab-i on" onclick="goCollections()"><span>💎</span>藏品</div>
|
||||||
|
<div class="tab-i" onclick="goUsers()"><span>👤</span>用户</div>
|
||||||
|
<div class="tab-i" onclick="S.v='stats';loadStats()"><span>📈</span>统计</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (S.v === 'users') {
|
||||||
|
const users = S.users || []
|
||||||
|
app.innerHTML = `
|
||||||
|
<div class="header"><h1>👤 用户列表</h1><button class="hdr-btn" onclick="loadUsers()">🔄</button></div>
|
||||||
|
<div class="filter-bar">
|
||||||
|
<input type="text" id="user-search" placeholder="搜索用户名" onkeyup="S.userSearch=this.value;render()" style="flex:1;padding:8px;border:1px solid #ddd;border-radius:6px">
|
||||||
|
</div>
|
||||||
|
<div class="list">
|
||||||
|
${users.filter(u => !S.userSearch || u.username.includes(S.userSearch)).map(u => `
|
||||||
|
<div class="post-item">
|
||||||
|
<div class="post-title">${u.username}</div>
|
||||||
|
<div class="post-meta">
|
||||||
|
<span>${u.is_seller?'🏪 商家':'👤 个人'}</span>
|
||||||
|
<span>📝 ${u.post_count||0}帖</span>
|
||||||
|
<span>⭐ ${u.credit_score||0}分</span>
|
||||||
|
<span>📅 ${u.registration_date?u.registration_date.substring(0,10):'-'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('')}
|
||||||
|
${users.length===0?'<div style="padding:40px;text-align:center;color:#999">暂无用户,点击刷新</div>':''}
|
||||||
|
</div>
|
||||||
|
<div class="tab">
|
||||||
|
<div class="tab-i" onclick="S.v='home';render()"><span>📊</span>首页</div>
|
||||||
|
<div class="tab-i" onclick="goList('')"><span>📋</span>帖子</div>
|
||||||
|
<div class="tab-i" onclick="goCollections()"><span>💎</span>藏品</div>
|
||||||
|
<div class="tab-i on" onclick="goUsers()"><span>👤</span>用户</div>
|
||||||
|
<div class="tab-i" onclick="S.v='stats';loadStats()"><span>📈</span>统计</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (S.v === 'stats') {
|
||||||
|
const st = S.statsData || {}
|
||||||
|
const want = st.want || {}
|
||||||
|
const deal = st.deal || {}
|
||||||
|
app.innerHTML = `
|
||||||
|
<div class="header"><h1>📈 统计分析</h1><button class="hdr-btn" onclick="loadStats()">🔄</button></div>
|
||||||
|
<div style="padding:12px">
|
||||||
|
<div style="font-size:13px;font-weight:600;color:#e65100;margin-bottom:10px">🔍 求购龙钞类</div>
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="st-card" onclick="goStatPosts('want_dai4_biao10')">
|
||||||
|
<div class="st-num">${want.dai4_biao10?.count||0}</div>
|
||||||
|
<div class="st-label">带4标十</div>
|
||||||
|
<div class="st-price">均 ${want.dai4_biao10?.avg||0}元</div>
|
||||||
|
</div>
|
||||||
|
<div class="st-card" onclick="goStatPosts('want_wu47_biao10')">
|
||||||
|
<div class="st-num">${want.wu47_biao10?.count||0}</div>
|
||||||
|
<div class="st-label">无47标十</div>
|
||||||
|
<div class="st-price">均 ${want.wu47_biao10?.avg||0}元</div>
|
||||||
|
</div>
|
||||||
|
<div class="st-card" onclick="goStatPosts('want_wu247_biao10')">
|
||||||
|
<div class="st-num">${want.wu247_biao10?.count||0}</div>
|
||||||
|
<div class="st-label">无247/天马标十</div>
|
||||||
|
<div class="st-price">均 ${want.wu247_biao10?.avg||0}元</div>
|
||||||
|
</div>
|
||||||
|
<div class="st-card" onclick="goStatPosts('want_wu247_biaobai')">
|
||||||
|
<div class="st-num">${want.wu247_biaobai?.count||0}</div>
|
||||||
|
<div class="st-label">无247/天马标百/刀</div>
|
||||||
|
<div class="st-price">均 ${want.wu247_biaobai?.avg||0}元</div>
|
||||||
|
</div>
|
||||||
|
<div class="st-card" onclick="goStatPosts('want_wu47_biaobai')">
|
||||||
|
<div class="st-num">${want.wu47_biaobai?.count||0}</div>
|
||||||
|
<div class="st-label">无47标百/刀</div>
|
||||||
|
<div class="st-price">均 ${want.wu47_biaobai?.avg||0}元</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="font-size:13px;font-weight:600;color:#2e7d32;margin:16px 0 10px">🔍 出售龙钞类</div>
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="st-card st-deal" onclick="goStatPosts('deal_dai4_biao10')">
|
||||||
|
<div class="st-num">${deal.dai4_biao10?.count||0}</div>
|
||||||
|
<div class="st-label">带4标十</div>
|
||||||
|
<div class="st-price">均 ${deal.dai4_biao10?.avg||0}元</div>
|
||||||
|
</div>
|
||||||
|
<div class="st-card st-deal" onclick="goStatPosts('deal_wu47_biao10')">
|
||||||
|
<div class="st-num">${deal.wu47_biao10?.count||0}</div>
|
||||||
|
<div class="st-label">无47标十</div>
|
||||||
|
<div class="st-price">均 ${deal.wu47_biao10?.avg||0}元</div>
|
||||||
|
</div>
|
||||||
|
<div class="st-card st-deal" onclick="goStatPosts('deal_wu247_biao10')">
|
||||||
|
<div class="st-num">${deal.wu247_biao10?.count||0}</div>
|
||||||
|
<div class="st-label">无247/天马标十</div>
|
||||||
|
<div class="st-price">均 ${deal.wu247_biao10?.avg||0}元</div>
|
||||||
|
</div>
|
||||||
|
<div class="st-card st-deal" onclick="goStatPosts('deal_wu247_biaobai')">
|
||||||
|
<div class="st-num">${deal.wu247_biaobai?.count||0}</div>
|
||||||
|
<div class="st-label">无247/天马标百/刀</div>
|
||||||
|
<div class="st-price">均 ${deal.wu247_biaobai?.avg||0}元</div>
|
||||||
|
</div>
|
||||||
|
<div class="st-card st-deal" onclick="goStatPosts('deal_wu47_biaobai')">
|
||||||
|
<div class="st-num">${deal.wu47_biaobai?.count||0}</div>
|
||||||
|
<div class="st-label">无47标百/刀</div>
|
||||||
|
<div class="st-price">均 ${deal.wu47_biaobai?.avg||0}元</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tab">
|
||||||
|
<div class="tab-i" onclick="S.v='home';render()"><span>📊</span>首页</div>
|
||||||
|
<div class="tab-i" onclick="goList('')"><span>📋</span>帖子</div>
|
||||||
|
<div class="tab-i" onclick="goCollections()"><span>💎</span>藏品</div>
|
||||||
|
<div class="tab-i" onclick="goUsers()"><span>👤</span>用户</div>
|
||||||
|
<div class="tab-i on" onclick="S.v='stats';loadStats()"><span>📈</span>统计</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (S.v === 'list') {
|
||||||
|
const ft = S.ft
|
||||||
|
app.innerHTML = `
|
||||||
|
<div class="header"><h1>📋 帖子列表</h1></div>
|
||||||
|
<div class="f-bar">
|
||||||
|
<div class="f-row">
|
||||||
|
<button class="f-btn${ft.type===''?' on':''}" onclick="setF('type','')">全部</button>
|
||||||
|
<button class="f-btn${ft.type==='deal'?' on':''}" onclick="setF('type','deal')">出售</button>
|
||||||
|
<button class="f-btn${ft.type==='want'?' on':''}" onclick="setF('type','want')">求购</button>
|
||||||
|
<button class="f-btn${ft.type==='other'?' on':''}" onclick="setF('type','other')">其他</button>
|
||||||
|
</div>
|
||||||
|
<div class="f-row" style="margin-top:8px">
|
||||||
|
<button class="f-btn${ft.cat===''?' on':''}" onclick="setF('cat','')">全品类</button>
|
||||||
|
<button class="f-btn${ft.cat==='龙钞'?' on':''}" onclick="setF('cat','龙钞')">龙钞</button>
|
||||||
|
<button class="f-btn${ft.cat==='蛇钞'?' on':''}" onclick="setF('cat','蛇钞')">蛇钞</button>
|
||||||
|
<button class="f-btn${ft.cat==='马钞'?' on':''}" onclick="setF('cat','马钞')">马钞</button>
|
||||||
|
<button class="f-btn${ft.cat==='其他'?' on':''}" onclick="setF('cat','其他')">其他</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="list">
|
||||||
|
${S.loading?'<div class="loading">加载中...</div>':''}
|
||||||
|
${!S.loading&&S.posts.length===0?'<div class="empty">暂无数据</div>':''}
|
||||||
|
${!S.loading&&S.posts.length>0?S.posts.map(p=>`
|
||||||
|
<div class="post" onclick="showDetail(${p.id})">
|
||||||
|
<div class="post-title">${p.title||'无标题'}</div>
|
||||||
|
<div class="post-meta">
|
||||||
|
<span class="badge b-${p.post_type||'normal'}">${p.post_type==='deal'?'出售':p.post_type==='want'?'求购':'普通'}</span>
|
||||||
|
${p.has_lifebuoy?'<span class="badge b-lifebuoy">🔥大救生圈</span>':''}
|
||||||
|
${p.price?'<span class="price">'+p.price+'元</span>':''}
|
||||||
|
${p.category?'<span class="tm">'+p.category+'</span>':''}
|
||||||
|
${p.author_username?'<span class="author">'+p.author_username+'</span>':''}
|
||||||
|
<span class="tm">${fmtTime(p.post_time)}</span>
|
||||||
|
${p.instance_id?'<span class="tm">🖥️'+p.instance_id.replace('server_','S')+'</span>':''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join(''):''}
|
||||||
|
${!S.loading&&S.posts.length>0?`
|
||||||
|
<div class="pager">
|
||||||
|
<button class="pg-btn"${S.pg<=1?' disabled':''} onclick="prevP()">上一页</button>
|
||||||
|
<span class="pg-info">${S.pg} / ${Math.max(1,Math.ceil(S.total/S.ps))} (共${S.total}条)</span>
|
||||||
|
<button class="pg-btn"${S.pg>=Math.ceil(S.total/S.ps)?' disabled':''} onclick="nextP()">下一页</button>
|
||||||
|
</div>
|
||||||
|
`:''}
|
||||||
|
</div>
|
||||||
|
<div class="tab">
|
||||||
|
<div class="tab-i" onclick="S.v='home';render()"><span>📊</span>首页</div>
|
||||||
|
<div class="tab-i on" onclick="goList('')"><span>📋</span>帖子</div>
|
||||||
|
<div class="tab-i" onclick="goCollections()"><span>💎</span>藏品</div>
|
||||||
|
<div class="tab-i" onclick="goUsers()"><span>👤</span>用户</div>
|
||||||
|
<div class="tab-i" onclick="S.v='stats';loadStats()"><span>📈</span>统计</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (S.v === 'collection-detail' && S.curCollection) {
|
||||||
|
const c = S.curCollection
|
||||||
|
app.innerHTML = `
|
||||||
|
<div class="detail">
|
||||||
|
<div class="detail-hdr">
|
||||||
|
<button class="back" onclick="S.v='collections';render()">← 返回藏品</button>
|
||||||
|
<a class="detail-link" href="${c.post_url||'#'}" target="_blank">原文链接</a>
|
||||||
|
</div>
|
||||||
|
<div class="detail-body">
|
||||||
|
<div class="detail-title">${c.name || c.crown_code || '无名称'}</div>
|
||||||
|
|
||||||
|
<div class="sec">
|
||||||
|
<div class="sec-title">🏷️ 藏品信息</div>
|
||||||
|
<div class="info-g">
|
||||||
|
<div class="info-i"><span class="info-l">品类</span><span class="${c.category==='龙钞'?'green':c.category==='马钞'?'orange':c.category==='蛇钞'?'purple':'gray'}">${c.category||'其他'}</span></div>
|
||||||
|
<div class="info-i"><span class="info-l">系列</span><span class="info-v">${c.series||'-'}</span></div>
|
||||||
|
<div class="info-i"><span class="info-l">发行年份</span><span class="info-v">${c.issue_year||'-'}</span></div>
|
||||||
|
<div class="info-i"><span class="info-l">冠号</span><span class="info-v" style="font-size:16px;font-weight:600;color:#1976d2">${c.crown_code||'-'}</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sec">
|
||||||
|
<div class="sec-title">📝 描述</div>
|
||||||
|
<div class="content">${c.description||'暂无描述'}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sec">
|
||||||
|
<div class="sec-title">🔗 关联信息</div>
|
||||||
|
<div class="info-g">
|
||||||
|
<div class="info-i"><span class="info-l">帖子ID</span><span class="info-v">${c.post_id||'-'}</span></div>
|
||||||
|
<div class="info-i"><span class="info-l">藏品ID</span><span class="info-v">${c.id||'-'}</span></div>
|
||||||
|
<div class="info-i"><span class="info-l">创建时间</span><span class="info-v">${c.created_at?new Date(c.created_at).toLocaleString('zh-CN'):'-'}</span></div>
|
||||||
|
<div class="info-i"><span class="info-l">更新时间</span><span class="info-v">${c.updated_at?new Date(c.updated_at).toLocaleString('zh-CN'):'-'}</span></div>
|
||||||
|
<div class="info-i"><span class="info-l">采集服务器</span><span class="info-v">${c.instance_id||'-'}</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top:20px">
|
||||||
|
<button onclick="goCollectionPost('${c.post_id}')" style="width:100%;padding:12px;background:#1976d2;color:#fff;border:none;border-radius:8px;font-size:15px;cursor:pointer">查看关联帖子 →</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (S.v === 'detail' && S.cur) {
|
||||||
|
const p = S.cur
|
||||||
|
app.innerHTML = `
|
||||||
|
<div class="detail">
|
||||||
|
<div class="detail-hdr">
|
||||||
|
<button class="back" onclick="S.v='list';render()">← 返回</button>
|
||||||
|
<a class="detail-link" href="${p.url||'#'}" target="_blank">原文链接</a>
|
||||||
|
</div>
|
||||||
|
<div class="detail-body">
|
||||||
|
<div class="detail-title">${p.title||'无标题'}</div>
|
||||||
|
|
||||||
|
<div class="sec">
|
||||||
|
<div class="sec-title">💰 价格信息</div>
|
||||||
|
<div class="info-g">
|
||||||
|
<div class="info-i">
|
||||||
|
<span class="info-l">价格</span>
|
||||||
|
<span class="info-v prime">${p.price?p.price+'元':'-'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-i">
|
||||||
|
<span class="info-l">品类</span>
|
||||||
|
<span class="info-v">${p.category||'-'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-i">
|
||||||
|
<span class="info-l">包装</span>
|
||||||
|
<span class="info-v">${p.special_types||'-'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-i">
|
||||||
|
<span class="info-l">号码</span>
|
||||||
|
<span class="info-v">${p.number_features||'-'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
${p.special_types?`
|
||||||
|
<div class="sec">
|
||||||
|
<div class="sec-title">📦 包装分类</div>
|
||||||
|
<div class="tag-l">
|
||||||
|
${p.special_types.split(/[,\/|]/).map(s=>`<span class="tag">${s.trim()}</span>`).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`:''}
|
||||||
|
|
||||||
|
${p.number_features?`
|
||||||
|
<div class="sec">
|
||||||
|
<div class="sec-title">🔢 号码分类</div>
|
||||||
|
<div class="tag-l">
|
||||||
|
${p.number_features.split(/[,\/|]/).map(s=>`<span class="tag">${s.trim()}</span>`).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`:''}
|
||||||
|
|
||||||
|
${p.special_tags?`
|
||||||
|
<div class="sec">
|
||||||
|
<div class="sec-title">🏷️ 特殊标识</div>
|
||||||
|
<div class="tag-l">
|
||||||
|
${p.special_tags.split(/[,\/|]/).map(s=>`<span class="tag">${s.trim()}</span>`).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`:''}
|
||||||
|
|
||||||
|
<div class="sec">
|
||||||
|
<div class="sec-title">👤 作者信息</div>
|
||||||
|
<div class="info-g">
|
||||||
|
<div class="info-i">
|
||||||
|
<span class="info-l">用户名</span>
|
||||||
|
<span class="info-v" style="color:#1976d2">${p.author_username||'-'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-i">
|
||||||
|
<span class="info-l">联系方式</span>
|
||||||
|
<span class="info-v">${p.contact||'-'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sec">
|
||||||
|
<div class="sec-title">📊 帖子状态</div>
|
||||||
|
<div class="info-g">
|
||||||
|
<div class="info-i">
|
||||||
|
<span class="info-l">类型</span>
|
||||||
|
<span class="badge b-${p.post_type||'normal'}">${p.post_type==='deal'?'出售':p.post_type==='want'?'求购':'普通'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-i">
|
||||||
|
<span class="info-l">大救生圈</span>
|
||||||
|
<span class="info-v">${p.has_lifebuoy?'🔥 是':'否'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-i">
|
||||||
|
<span class="info-l">发帖时间</span>
|
||||||
|
<span class="info-v">${p.post_time?new Date(p.post_time).toLocaleString('zh-CN'):'-'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-i">
|
||||||
|
<span class="info-l">回复数</span>
|
||||||
|
<span class="info-v">${p.reply_count||0}</span>
|
||||||
|
</div>
|
||||||
|
${p.instance_id?'<div class="info-i"><span class="info-l">来源</span><span class="info-v">🖥️ '+p.instance_id.replace('server_','服务器')+'</span></div>':''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sec">
|
||||||
|
<div class="sec-title">📝 帖子内容</div>
|
||||||
|
<div class="content">${p.content||'暂无内容'}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function goCollections() {
|
||||||
|
console.log('goCollections clicked')
|
||||||
|
S.v = 'collections'
|
||||||
|
loadCollections()
|
||||||
|
}
|
||||||
|
function goUsers() {
|
||||||
|
console.log('goUsers clicked')
|
||||||
|
S.v = 'users'
|
||||||
|
loadUsers()
|
||||||
|
}
|
||||||
|
async function loadCollections() {
|
||||||
|
console.log('loadCollections called, current S.v=', S.v)
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/v1/collections?page=1&page_size=100')
|
||||||
|
const json = await res.json()
|
||||||
|
console.log('collections API response code:', json.code, 'items:', (json.data&&json.data.items||[]).length)
|
||||||
|
if (json.code === 0) {
|
||||||
|
S.collections = json.data.items || []
|
||||||
|
render()
|
||||||
|
}
|
||||||
|
} catch(e) { console.error('loadCollections error:', e.message) }
|
||||||
|
}
|
||||||
|
async function loadUsers() {
|
||||||
|
console.log('loadUsers called, current S.v=', S.v)
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/v1/users?page=1&page_size=100')
|
||||||
|
const json = await res.json()
|
||||||
|
console.log('users API response code:', json.code, 'items:', (json.data&&json.data.items||[]).length)
|
||||||
|
if (json.code === 0) {
|
||||||
|
S.users = json.data.items || []
|
||||||
|
render()
|
||||||
|
}
|
||||||
|
} catch(e) { console.error('loadUsers error:', e.message) }
|
||||||
|
}
|
||||||
|
function showCollectionDetail(c) {
|
||||||
|
alert(['名称: '+(c.name||'-'), '品类: '+(c.category||'-'), '系列: '+(c.series||'-'),
|
||||||
|
'年份: '+(c.issue_year||'-'), '冠号: '+(c.crown_code||'-'), '描述: '+(c.description||'-')].join('\n'))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function goCollectionDetail(collectionId) {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/v1/collections/'+collectionId)
|
||||||
|
const json = await res.json()
|
||||||
|
if (json.code === 0) {
|
||||||
|
S.curCollection = json.data
|
||||||
|
S.v = 'collection-detail'
|
||||||
|
render()
|
||||||
|
} else {
|
||||||
|
alert('藏品不存在')
|
||||||
|
}
|
||||||
|
} catch(e) { alert('加载失败: '+e.message) }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function goCollectionPost(postId) {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/v1/yichens/posts/'+encodeURIComponent(postId))
|
||||||
|
const json = await res.json()
|
||||||
|
if (json.code === 0) {
|
||||||
|
S.cur = json.data
|
||||||
|
S.v = 'detail'
|
||||||
|
render()
|
||||||
|
} else {
|
||||||
|
alert('帖子不存在或已删除')
|
||||||
|
}
|
||||||
|
} catch(e) { alert('加载失败: '+e.message) }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadStats() {
|
||||||
|
try {
|
||||||
|
const r = await fetch(API+'/yichens/statistics')
|
||||||
|
const d = await r.json()
|
||||||
|
if (d.code === 0) { S.stats = d.data }
|
||||||
|
const r2 = await fetch(API+'/yichens/report/categories')
|
||||||
|
const d2 = await r2.json()
|
||||||
|
if (d2.code === 0) { S.catReport = d2.data }
|
||||||
|
const r3 = await fetch(API+'/yichens/report/stats')
|
||||||
|
const d3 = await r3.json()
|
||||||
|
if (d3.code === 0) { S.statsData = d3.data }
|
||||||
|
render()
|
||||||
|
} catch(e) { console.error(e) }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadPosts() {
|
||||||
|
S.loading = true; render()
|
||||||
|
try {
|
||||||
|
let url = API+'/yichens/posts?page='+S.pg+'&page_size='+S.ps
|
||||||
|
if (S.ft.type) url += '&post_type='+S.ft.type
|
||||||
|
if (S.ft.cat) url += '&category='+S.ft.cat
|
||||||
|
if (S.ft.lifebuoy === 'yes') url += '&has_lifebuoy=true'
|
||||||
|
if (S.ft.timeRange) url += '&time_range='+S.ft.timeRange
|
||||||
|
if (S.ft.statType) { S.loading = false; loadStatPosts(); return }
|
||||||
|
const r = await fetch(url)
|
||||||
|
const d = await r.json()
|
||||||
|
if (d.code === 0) { S.posts = d.data.items||[]; S.total = d.data.total||0 }
|
||||||
|
} catch(e) { console.error(e) }
|
||||||
|
S.loading = false; render()
|
||||||
|
}
|
||||||
|
|
||||||
|
function goList(type, timeRange, cat) {
|
||||||
|
S.ft = { type: '', cat: '', lifebuoy: '', timeRange: '' }
|
||||||
|
if (type === 'lifebuoy') S.ft.lifebuoy = 'yes'
|
||||||
|
else if (type) S.ft.type = type
|
||||||
|
if (timeRange) S.ft.timeRange = timeRange
|
||||||
|
if (cat) S.ft.cat = cat
|
||||||
|
S.pg = 1; S.v = 'list'; loadPosts()
|
||||||
|
S.collections = []; S.users = []; S.colCat = ''; S.colSearch = ''; S.userSearch = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function goStatPosts(statType) {
|
||||||
|
S.ft = { type: '', cat: '', lifebuoy: '', timeRange: '', statType: statType }
|
||||||
|
S.pg = 1; S.v = 'list'; loadStatPosts()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadStatPosts() {
|
||||||
|
if (!S.ft.statType) return
|
||||||
|
S.loading = true; render()
|
||||||
|
try {
|
||||||
|
let url = API+'/yichens/report/posts?stat_type='+S.ft.statType+'&page='+S.pg+'&page_size='+S.ps
|
||||||
|
const r = await fetch(url)
|
||||||
|
const d = await r.json()
|
||||||
|
if (d.code === 0) { S.posts = d.data.items||[]; S.total = d.data.total||0 }
|
||||||
|
} catch(e) { console.error(e) }
|
||||||
|
S.loading = false; render()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setF(key, val) {
|
||||||
|
if (key === 'type') { S.ft.type = val; S.ft.timeRange = '' }
|
||||||
|
if (key === 'cat') { S.ft.cat = val; S.ft.timeRange = '' }
|
||||||
|
S.pg = 1; loadPosts()
|
||||||
|
}
|
||||||
|
|
||||||
|
function showDetail(id) {
|
||||||
|
S.cur = S.posts.find(p => p.id === id)
|
||||||
|
S.v = 'detail'; render()
|
||||||
|
}
|
||||||
|
|
||||||
|
function prevP() { if (S.pg > 1) { S.pg--; loadPosts(); window.scrollTo(0,0) } }
|
||||||
|
function nextP() { S.pg++; loadPosts(); window.scrollTo(0,0) }
|
||||||
|
|
||||||
|
function fmtTime(ts) {
|
||||||
|
if (!ts) return '-'
|
||||||
|
const d = new Date(ts), n = new Date()
|
||||||
|
const diff = n - d
|
||||||
|
if (diff < 60000) return '刚刚'
|
||||||
|
if (diff < 3600000) return Math.floor(diff/60000)+'分钟前'
|
||||||
|
if (diff < 86400000) return Math.floor(diff/3600000)+'小时前'
|
||||||
|
if (diff < 604800000) return Math.floor(diff/86400000)+'天前'
|
||||||
|
return d.toLocaleDateString('zh-CN')
|
||||||
|
}
|
||||||
|
|
||||||
|
// init
|
||||||
|
loadStats()
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Reference in New Issue