Files
shaguabijia-app-server/h5/reports/index.html
T

611 lines
22 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="../shared/fonts.css">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>上报记录 · 傻瓜比价</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
/* 【去框】原型 html/body 是黑底 + flex 居中(浏览器手机框预览用)。WebView 里直接铺页面色、不居中。
WebView 是 MATCH_PARENTheight:100% 链即可拿到真实视口高;不要再叠 min-height:100vh
—— 在 WebView 里 100vh≈整窗高(含状态栏区),比真实视口高一截,会把顶栏整体顶下去、与设置页不对齐。 */
html, body { height: 100%; overflow: hidden; }
html { background: var(--color-surface-page); }
body {
font-family: 'PuHuiTi', -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Helvetica Neue', sans-serif;
background: var(--color-surface-page);
}
:root {
--color-primary: #FFD600;
--color-on-primary: #1A1A1A;
--color-surface-page: #F5F5F5;
--color-surface-card: #FFFFFF;
--color-surface-muted: #FAFAFA;
--color-ink: #1A1A1A;
--color-ink-muted: #666666;
--color-ink-subtle: #999999;
--color-ink-disabled: #CCCCCC;
--color-divider: #F0F0F0;
--color-divider-strong: #E5E5E5;
--color-price-lowest: #FF3D00;
--color-price-lowest-bg: #FFEBEE;
--color-state-success-dark: #2E7D32;
--color-state-success-bg: #E8F5E9;
--color-state-warning-bg: #FFF3E0;
--color-state-warning-ink: #B57400;
--color-state-danger: #E53935;
--color-state-danger-bg: #FFEBEE;
--color-coin-deep: #5A3A00;
--radius: 14px;
--radius-sm: 10px;
--radius-pill: 999px;
--shadow-card: 0 1px 2px rgba(0,0,0,.03);
--font-num: 'DIN Alternate', 'DIN Pro', 'SF Pro Display', system-ui;
}
/* 【去框】原型 .device 是 375x812 的手机预览框(圆角 + 阴影 + 固定宽高)。
WebView 里改成全屏自适应容器:满宽、最少占满视口高、无圆角/阴影。 */
.device {
width: 100%;
height: 100%;
background: var(--color-surface-page);
overflow: hidden;
position: relative;
}
.screen {
position: relative;
height: 100%;
display: flex;
flex-direction: column;
}
/* 【调试返回】原型左下角的浏览器调试返回按钮,WebView 里不需要 */
#debugBack { display: none; }
/* 【去框】原型假状态栏(9:41 + 信号格),App 顶部有原生状态栏/标题栏,这里隐藏 */
.statusbar { display: none; }
.topnav {
height: 48px;
display: flex;
align-items: center;
padding: 0 12px;
background: var(--color-surface-page);
flex-shrink: 0;
position: relative;
}
.topnav-back {
width: 32px; height: 32px;
display: flex; align-items: center; justify-content: center;
cursor: pointer;
background: none; border: none;
}
.topnav-title {
position: absolute;
left: 50%;
transform: translateX(-50%);
font-size: 17px;
font-weight: 600;
color: var(--color-ink);
}
.topnav-right {
margin-left: auto;
padding: 6px 4px;
font-size: 14px;
color: var(--color-ink-muted);
font-weight: 400;
background: none; border: none;
cursor: pointer;
}
.scroll {
flex: 1;
overflow-y: auto;
padding: 8px 14px 24px;
}
.scroll::-webkit-scrollbar { display: none; }
.filter-row {
display: flex;
gap: 8px;
padding: 4px 0 12px;
overflow-x: auto;
}
.filter-row::-webkit-scrollbar { display: none; }
.filter-chip {
flex-shrink: 0;
padding: 6px 14px;
border-radius: var(--radius-pill);
font-size: 13px;
font-weight: 400;
color: var(--color-ink-muted);
background: var(--color-surface-card);
border: .5px solid var(--color-divider-strong);
cursor: pointer;
}
.filter-chip.active {
background: var(--color-ink);
color: #fff;
font-weight: 600;
border-color: var(--color-ink);
}
.filter-chip .count { margin-left: 4px; opacity: .7; font-weight: 400; }
.rec-card {
background: var(--color-surface-card);
border-radius: var(--radius);
padding: 14px;
margin-bottom: 12px;
box-shadow: var(--shadow-card);
overflow: hidden;
}
.rec-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 10px;
}
.rec-store {
font-size: 15px;
font-weight: 400;
color: var(--color-ink);
line-height: 1.4;
flex: 1;
min-width: 0;
}
.rec-dish {
font-size: 13px;
color: var(--color-ink-muted);
line-height: 1.5;
margin-top: 4px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.badge {
flex-shrink: 0;
display: inline-flex;
align-items: center;
gap: 3px;
padding: 3px 8px;
border-radius: var(--radius-pill);
font-size: 11px;
font-weight: 700;
line-height: 1.2;
white-space: nowrap;
}
.badge svg { width: 12px; height: 12px; }
.badge-pending { background: var(--color-state-warning-bg); color: var(--color-state-warning-ink); }
.badge-success { background: var(--color-state-success-bg); color: var(--color-state-success-dark); }
.badge-danger { background: var(--color-state-danger-bg); color: var(--color-state-danger); }
.rec-compare {
margin-top: 12px;
background: var(--color-surface-muted);
border-radius: var(--radius-sm);
padding: 10px 12px;
display: flex;
flex-direction: column;
gap: 8px;
}
.rec-compare-row {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
}
.rec-compare-label {
color: var(--color-ink);
font-weight: 400;
width: 56px;
flex-shrink: 0;
}
.rec-compare-pl {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 6px 2px 2px;
background: #fff;
border-radius: 5px;
border: .5px solid var(--color-divider-strong);
}
.rec-compare-pl img {
width: 16px; height: 16px;
border-radius: 3px;
object-fit: cover;
flex-shrink: 0;
}
.rec-compare-pl span {
font-size: 12px;
color: var(--color-ink);
font-weight: 400;
line-height: 1.4;
}
.rec-compare-price {
margin-left: auto;
font-family: var(--font-num);
font-weight: 700;
font-size: 15px;
color: var(--color-ink);
}
.rec-compare-price .cur { font-size: 11px; font-weight: 600; margin-right: 1px; }
.rec-compare-row.reported .rec-compare-price { color: var(--color-price-lowest); }
.rec-reward {
margin-top: 12px;
background: linear-gradient(90deg, #FFF8E1 0%, #FFFDE7 100%);
border-radius: var(--radius-sm);
padding: 8px 12px;
display: flex;
align-items: baseline;
gap: 4px;
font-size: 13px;
color: var(--color-coin-deep);
font-weight: 400;
}
.rec-reward-num {
font-family: var(--font-num);
font-size: 15px;
font-weight: 700;
}
.rec-reject {
margin-top: 12px;
background: var(--color-state-danger-bg);
border-radius: var(--radius-sm);
padding: 10px 12px;
font-size: 13px;
color: var(--color-state-danger);
line-height: 1.5;
}
.rec-reject b { font-weight: 600; }
.list-end {
text-align: center;
padding: 24px 0 8px;
font-size: 12px;
color: var(--color-ink-disabled);
font-weight: 400;
}
/* 空态(未登录 / 无记录)—— 原型没有,照"比价记录页"补一个轻量居中空态,沿用其字号/颜色 */
.rec-empty {
display: none;
padding: 96px 24px;
text-align: center;
color: var(--color-ink-subtle);
font-size: 14px;
line-height: 1.7;
}
</style>
<script src="../shared/bridge.js"></script>
</head>
<body>
<div class="device" id="app">
<div id="debugBack" onclick="goBack()">返回</div>
<div class="screen active" id="reports">
<!-- 【去框】假状态栏:App 内有原生状态栏,已 display:none 隐藏,保留 DOM 防脚本引用报错 -->
<div class="statusbar">
<span>9:41</span>
<span class="statusbar-right">
<svg width="18" height="10" viewBox="0 0 18 10" fill="#1A1A1A"><rect x="0" y="6" width="3" height="4" rx=".5"/><rect x="5" y="4" width="3" height="6" rx=".5"/><rect x="10" y="2" width="3" height="8" rx=".5"/><rect x="15" y="0" width="3" height="10" rx=".5"/></svg>
<svg width="16" height="11" viewBox="0 0 16 11" fill="#1A1A1A"><path d="M8 11l8-8c-4.5-4-11.5-4-16 0l8 8z"/></svg>
<svg width="24" height="11" viewBox="0 0 24 11" fill="none" stroke="#1A1A1A"><rect x=".5" y=".5" width="20" height="10" rx="2.5"/><rect x="2" y="2" width="17" height="7" rx="1.2" fill="#1A1A1A"/><rect x="22" y="3.5" width="1.5" height="4" rx=".5" fill="#1A1A1A"/></svg>
</span>
</div>
<div class="topnav">
<!-- 返回交给原生接管(占位 console.log),保留按钮视觉 -->
<button class="topnav-back" onclick="goBack()" aria-label="返回">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#1A1A1A" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"/></svg>
</button>
<div class="topnav-title">上报记录</div>
</div>
<div class="scroll">
<!-- 四态筛选:count 用 counts;点击本地按 status 过滤已拉全量 records 重渲染(不重新请求) -->
<div class="filter-row" id="filterRow">
<div class="filter-chip active" data-status="all" onclick="onFilterClick(this)">全部<span class="count" data-count="all">0</span></div>
<div class="filter-chip" data-status="pending" onclick="onFilterClick(this)">审核中<span class="count" data-count="pending">0</span></div>
<div class="filter-chip" data-status="approved" onclick="onFilterClick(this)">已通过<span class="count" data-count="approved">0</span></div>
<div class="filter-chip" data-status="rejected" onclick="onFilterClick(this)">未通过<span class="count" data-count="rejected">0</span></div>
</div>
<!-- 卡片列表(JS 动态渲染,替代原型 4 张写死 rec-card -->
<div id="recList"></div>
<!-- 列表末尾「— 已显示全部 N 条 —」,N = 当前筛选后条数 -->
<div class="list-end" id="listEnd"></div>
<!-- 空态(未登录 / 无记录),由 JS 控制显隐 + 文案 -->
<div class="rec-empty" id="recEmpty"></div>
</div>
</div>
</div>
<script>
/* ============================================================
* 数据层(与「比价记录页」records/index.html 完全同款写法)
* SGBridge.getToken(同源相对路径,无 getApiBase)、401→requestLogin、
* 无 token→空态、无 Bridge→假数据预览;snake_case → 渲染字段
* ============================================================ */
// ===== 平台 → logo 映射 =====
// 原型用 ../../assets/shared/logos/{meituan-waimai,taobao-shanguang,jd-waimai}.png
// 原封不动拷进本页 logos/(保留原型文件名 -waimai/-shanguang,与原型逐字一致)。
// 原最低价 / 上报价的平台是动态的(original_platform_id / reported_platform_id),先归一化再取图。
var PLATFORM_LOGOS = {
'meituan-waimai': 'logos/meituan.png',
'taobao-shanguang': 'logos/taobao.png',
'jd-waimai': 'logos/jd.png',
};
// ===== 平台别名归一化(复用「比价记录页」PLATFORM_ALIASES 思路)=====
// 后端 platform_id 是短名(meituan/taobao_flash/jd_waimai),统一归一成规范 id 再取 logo;找不到返回 null。
var PLATFORM_ALIASES = {};
[
['meituan-waimai', ['meituan','meituan-waimai','meituan_waimai','美团','美团外卖']],
['taobao-shanguang', ['taobao_flash','taobao-shanguang','taobao_shanguang','taobao','淘宝闪购','淘宝']],
['jd-waimai', ['jd_waimai','jd_waimai_standalone','jd-waimai','jingdong-waimai','jingdong_waimai','jingdong','jd','京东外卖','京东','京东到家']],
].forEach(function (pair) {
pair[1].forEach(function (a) { PLATFORM_ALIASES[a.toLowerCase()] = pair[0]; });
});
function resolvePlatformId(idOrName) {
return PLATFORM_ALIASES[String(idOrName || '').toLowerCase()] || null;
}
// 取平台 logo:归一化命中→对应 png;命中不了→空串(img 不显示也不破图)
function platformLogo(idOrName) {
var id = resolvePlatformId(idOrName);
return id ? PLATFORM_LOGOS[id] : '';
}
// 平台父名(展示:子平台→父名,美团外卖→美团 / 淘宝闪购→淘宝 / 京东外卖→京东,对齐 history 卡片)
var PLATFORM_NAMES = { 'meituan-waimai': '美团', 'taobao-shanguang': '淘宝', 'jd-waimai': '京东' };
function platformDisplayName(idOrName, fallback) {
var id = resolvePlatformId(idOrName);
return (id && PLATFORM_NAMES[id]) || fallback || '';
}
// ===== 状态映射:后端 status → badge class + 文案 + 图标 =====
// 图标 svg 照搬原型三个 badge 内对应的 svg(审核中=时钟 / 已通过=对勾 / 未通过=叉)。
var STATUS_MAP = {
pending: { cls: 'badge-pending', text: '审核中',
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg>' },
approved: { cls: 'badge-success', text: '已通过',
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.8" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.5l4.5 4.5L19 7"/></svg>' },
rejected: { cls: 'badge-danger', text: '未通过',
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M9 9l6 6M15 9l-6 6"/></svg>' },
};
// ===== 价格:后端是分(cents),显示 = (cents/100).toFixed(2) =====
function fenToYuan(cents) {
return (Number(cents || 0) / 100).toFixed(2);
}
// ===== 假数据(仅电脑浏览器预览用;装进 App 后走真接口)=====
// 字段用后端真实会返回的 snake_case(顺带验证归一化 + 分换算)。覆盖三态 + 不同平台。
var MOCK = {
records: [
{
store_name: '麦稻中式健康菜(首都机场店)',
dish_summary: '秘制口水鸡、干锅土豆片、白米饭',
status: 'pending',
original_platform_id: 'taobao_flash', original_platform_name: '淘宝闪购', original_price_cents: 1910,
reported_platform_id: 'meituan', reported_platform_name: '美团外卖', reported_price_cents: 1750,
},
{
store_name: '蜀大侠火锅(朝阳大悦城店)',
dish_summary: '毛肚拼盘、鲜鸭血、招牌牛肉',
status: 'approved', reward_coins: 1000,
original_platform_id: 'meituan', original_platform_name: '美团外卖', original_price_cents: 18800,
reported_platform_id: 'jd_waimai', reported_platform_name: '京东外卖', reported_price_cents: 15600,
},
{
store_name: '和府捞面(国贸店)',
dish_summary: '草本汤养鸡汤面、卤蛋',
status: 'rejected', reject_reason: '截图模糊,无法确认商品和价格信息。请重新上传清晰截图。',
original_platform_id: 'meituan', original_platform_name: '美团外卖', original_price_cents: 4200,
reported_platform_id: 'taobao_flash', reported_platform_name: '淘宝闪购', reported_price_cents: 3850,
},
{
store_name: '瑞幸咖啡(望京 SOHO 店)',
dish_summary: '生椰拿铁、丝绒拿铁',
status: 'approved', reward_coins: 1000,
original_platform_id: 'jd_waimai', original_platform_name: '京东外卖', original_price_cents: 2280,
reported_platform_id: 'taobao_flash', reported_platform_name: '淘宝闪购', reported_price_cents: 1790,
},
],
counts: { all: 4, pending: 1, approved: 2, rejected: 1 },
};
// ===== 数据加载:App 里走真接口,浏览器预览走假数据 =====
// 同源部署:fetch 走相对路径 /api/v1/...(不依赖原生给域名,无 getApiBase)。
// 返回 null 表示未登录(无 token)→ 空态"登录后查看上报记录"。
async function loadReports() {
var hasBridge = window.SGBridge && typeof SGBridge.getToken === 'function';
if (hasBridge) {
var token = SGBridge.getToken();
if (!token) return null; // 未登录 → 空态
try {
// status 不传 = 全部(四态切换在本地过滤,不重新请求)
var res = await fetch('/api/v1/report/records',
{ headers: { 'Authorization': 'Bearer ' + token } });
if (res.status === 401) { SGBridge.requestLogin && SGBridge.requestLogin(); return null; }
var data = await res.json();
return {
records: data.records || [],
counts: data.counts || computeCounts(data.records || []),
};
} catch (e) { /* 网络失败,落回预览数据 */ }
}
return MOCK; // 浏览器预览(无 Bridge
}
// counts 兜底:万一后端没带 counts,就本地数一遍
function computeCounts(records) {
var c = { all: records.length, pending: 0, approved: 0, rejected: 0 };
records.forEach(function (r) { if (c[r.status] != null) c[r.status]++; });
return c;
}
/* ============================================================
* 渲染(用原型的 class,把 4 张写死卡片改成动态生成)
* ============================================================ */
var RECORDS = []; // null = 未登录态
var COUNTS = { all: 0, pending: 0, approved: 0, rejected: 0 };
var currentStatus = 'all'; // 当前筛选:all|pending|approved|rejected
function esc(s){ return String(s == null ? '' : s).replace(/[&<>"]/g, function(c){ return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]; }); }
// 单行「原最低价 / 您上报的」比价行
// label:「原最低价」=original / 「您上报的」=reported(走红色,由 .reported 类控制)
function renderCompareRow(kind, platformId, platformName, cents) {
var logo = platformLogo(platformId || platformName);
var label = (kind === 'reported') ? '您上报的' : '原最低价';
// platform_name 直接用后端中文名显示
return '<div class="rec-compare-row ' + kind + '">' +
'<span class="rec-compare-label">' + label + '</span>' +
'<span class="rec-compare-pl">' +
(logo ? '<img src="' + logo + '" alt="">' : '') +
'<span>' + esc(platformDisplayName(platformId || platformName, platformName)) + '</span>' +
'</span>' +
'<span class="rec-compare-price"><span class="cur">¥</span>' + fenToYuan(cents) + '</span>' +
'</div>';
}
// 单张上报卡片(结构/class 完全对齐原型)
function renderRecCard(rec) {
var st = STATUS_MAP[rec.status] || STATUS_MAP.pending;
var head = '<div class="rec-head">' +
'<div>' +
'<div class="rec-store">' + esc(rec.store_name || '') + '</div>' +
'<div class="rec-dish">' + esc(rec.dish_summary || '') + '</div>' +
'</div>' +
'<span class="badge ' + st.cls + '">' + st.icon + st.text + '</span>' +
'</div>';
var compare = '<div class="rec-compare">' +
renderCompareRow('original', rec.original_platform_id, rec.original_platform_name, rec.original_price_cents) +
renderCompareRow('reported', rec.reported_platform_id, rec.reported_platform_name, rec.reported_price_cents) +
'</div>';
// 通过 → 金币到账横幅(用后端 reward_coins);未通过 → 原因;审核中 → 都不追加
var extra = '';
if (rec.status === 'approved') {
extra = '<div class="rec-reward"><span>+</span>' +
'<span class="rec-reward-num">' + Number(rec.reward_coins || 0) + '</span>' +
'<span>金币已到账</span></div>';
} else if (rec.status === 'rejected') {
extra = '<div class="rec-reject"><b>未通过原因:</b>' + esc(rec.reject_reason || '') + '</div>';
}
var card = document.createElement('div');
card.className = 'rec-card';
card.innerHTML = head + compare + extra;
return card;
}
// 刷新四态 chip 的 count 数字
function renderCounts() {
['all', 'pending', 'approved', 'rejected'].forEach(function (k) {
var el = document.querySelector('.count[data-count="' + k + '"]');
if (el) el.textContent = (COUNTS[k] != null ? COUNTS[k] : 0);
});
}
// 渲染列表 + 末尾「已显示全部 N 条」+ 空态
function renderList() {
var listEl = document.getElementById('recList');
var endEl = document.getElementById('listEnd');
var emptyEl = document.getElementById('recEmpty');
if (!listEl) return;
listEl.innerHTML = '';
// 未登录态:清空列表,空态提示登录
if (RECORDS === null) {
endEl.style.display = 'none';
emptyEl.style.display = 'block';
emptyEl.textContent = '登录后查看上报记录';
return;
}
// 本地按当前 status 过滤已拉全量(all = 不过滤)
var shown = (currentStatus === 'all')
? RECORDS
: RECORDS.filter(function (r) { return r.status === currentStatus; });
if (shown.length === 0) {
endEl.style.display = 'none';
emptyEl.style.display = 'block';
emptyEl.textContent = '暂无上报记录';
return;
}
emptyEl.style.display = 'none';
shown.forEach(function (rec) { listEl.appendChild(renderRecCard(rec)); });
// N = 当前筛选后条数
endEl.style.display = 'block';
endEl.textContent = '— 已显示全部 ' + shown.length + ' 条 —';
}
// 点 chip:切 active + 本地按 status 过滤重渲染(不重新请求)
function onFilterClick(el) {
if (RECORDS === null) return; // 未登录态不响应筛选
document.querySelectorAll('.filter-chip').forEach(function (c) { c.classList.remove('active'); });
el.classList.add('active');
currentStatus = el.getAttribute('data-status') || 'all';
renderList();
}
/* ============================================================
* 导航占位(App 原生接管返回 / 跳转),同「比价记录页」用 console.log 兜底
* ============================================================ */
function navigate(id) { console.log('[stub] navigateApp 内由原生处理跳转):', id); }
window.go = navigate;
// 顶栏返回箭头:调原生 SGBridge.closePage() 回上一页(WebViewScreen 的 onBack → popBackStack);浏览器预览仅 log
function goBack() {
if (window.SGBridge && typeof SGBridge.closePage === 'function') { SGBridge.closePage(); }
else { console.log('[stub] goBack(浏览器预览,无原生返回)'); }
}
window.goBack = goBack;
function goBackToProfile() { goBack(); } // 从「我的」进来的返回同样走 closePage 回上一页
window.goBackToProfile = goBackToProfile;
/* ============================================================
* 启动:拉数据 → 渲染
* ============================================================ */
(async function init(){
var data = await loadReports();
if (data === null) {
// 未登录(无 token)→ 空态
RECORDS = null;
COUNTS = { all: 0, pending: 0, approved: 0, rejected: 0 };
} else {
RECORDS = data.records || [];
COUNTS = data.counts || computeCounts(RECORDS);
}
renderCounts();
renderList();
})();
</script>
</body>
</html>