Files
basicBench/002/002_r.html
2026-01-19 21:14:58 +08:00

811 lines
43 KiB
HTML
Raw Permalink 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">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>E-Commerce Order Audit System V9.4 (Full Stack)</title>
<style>
:root {
/* 配色系统 - 保持 V9.2 的现代风格 */
--primary: #2563eb;
--primary-hover: #1d4ed8;
--primary-light: #eff6ff;
--bg-body: #f1f5f9;
--bg-card: #ffffff;
--bg-sidebar: #ffffff;
--bg-header: #1e293b;
--text-main: #0f172a;
--text-muted: #64748b;
--border: #e2e8f0;
/* 状态色 */
--success: #10b981;
--warning: #f59e0b;
--danger: #ef4444;
--info: #06b6d4;
/* 尺寸与阴影 */
--sidebar-w: 260px;
--header-h: 60px;
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
--trans: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
/* 深色模式变量 */
[data-theme="dark"] {
--bg-body: #0f172a;
--bg-card: #1e293b;
--bg-sidebar: #1e293b;
--bg-header: #020617;
--text-main: #f1f5f9;
--text-muted: #94a3b8;
--border: #334155;
--primary-light: rgba(37, 99, 235, 0.1);
}
/* --- 基础重置 --- */
* { box-sizing: border-box; outline: none; }
body { font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; background-color: var(--bg-body); margin: 0; color: var(--text-main); font-size: 14px; overflow: hidden; transition: background 0.3s; }
a, button { cursor: pointer; transition: var(--trans); color: inherit; text-decoration: none; }
ul { list-style: none; padding: 0; margin: 0; }
.icon { width: 16px; height: 16px; fill: currentColor; vertical-align: middle; margin-right: 4px; }
/* --- 布局框架 --- */
.app-container { display: flex; flex-direction: column; height: 100vh; }
.header { height: var(--header-h); background: var(--bg-header); color: white; display: flex; justify-content: space-between; align-items: center; padding: 0 20px; z-index: 50; box-shadow: var(--shadow-md); }
.brand { font-size: 18px; font-weight: 700; display: flex; align-items: center; gap: 10px; }
.brand span { padding: 4px 8px; background: var(--primary); border-radius: 4px; font-size: 14px; }
.nav-top a { padding: 8px 12px; color: #94a3b8; font-size: 13px; border-radius: 4px; }
.nav-top a:hover, .nav-top a.active { color: white; background: rgba(255,255,255,0.1); }
.user-area { display: flex; align-items: center; gap: 15px; }
.avatar { width: 32px; height: 32px; background: var(--primary); border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; border: 2px solid rgba(255,255,255,0.2); }
.main-body { display: flex; flex: 1; overflow: hidden; }
.sidebar { width: var(--sidebar-w); background: var(--bg-sidebar); border-right: 1px solid var(--border); display: flex; flex-direction: column; padding-top: 10px; transition: background 0.3s; }
.menu-group { margin-bottom: 20px; }
.menu-header { font-size: 11px; text-transform: uppercase; color: var(--text-muted); padding: 0 20px; margin-bottom: 8px; font-weight: 700; }
.menu-item { padding: 10px 20px; display: flex; align-items: center; color: var(--text-muted); font-weight: 500; border-left: 3px solid transparent; }
.menu-item:hover { background: var(--bg-body); color: var(--text-main); }
.menu-item.active { background: var(--primary-light); color: var(--primary); border-left-color: var(--primary); }
.badge { margin-left: auto; background: var(--danger); color: white; font-size: 10px; padding: 2px 6px; border-radius: 10px; }
.content { flex: 1; overflow-y: auto; padding: 25px; position: relative; }
.view-panel { display: none; animation: fadeIn 0.3s ease; }
.view-panel.active { display: block; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
/* --- 组件样式 --- */
.card { background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px; padding: 20px; box-shadow: var(--shadow-sm); margin-bottom: 20px; transition: background 0.3s; }
.card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; border-bottom: 1px solid var(--border); padding-bottom: 10px; }
.stat-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin-bottom: 20px; }
.stat-box { background: var(--bg-card); padding: 20px; border-radius: 8px; border: 1px solid var(--border); display: flex; flex-direction: column; }
.stat-label { font-size: 12px; color: var(--text-muted); }
.stat-num { font-size: 28px; font-weight: 700; margin: 10px 0; }
.stat-trend { font-size: 12px; display: flex; align-items: center; gap: 5px; }
.trend-up { color: var(--success); }
.trend-down { color: var(--danger); }
.filter-panel { background: var(--bg-card); padding: 15px; border-radius: 8px; border: 1px solid var(--border); margin-bottom: 20px; display: flex; gap: 15px; flex-wrap: wrap; align-items: flex-end; }
.form-group { display: flex; flex-direction: column; gap: 5px; }
.form-group label { font-size: 12px; font-weight: 600; color: var(--text-muted); }
.input-control { padding: 8px 12px; border: 1px solid var(--border); border-radius: 4px; background: var(--bg-body); color: var(--text-main); width: 180px; font-size: 13px; }
.btn { padding: 8px 16px; border-radius: 4px; border: none; font-size: 13px; font-weight: 500; display: inline-flex; align-items: center; justify-content: center; gap: 5px; }
.btn-primary { background: var(--primary); color: white; }
.btn-secondary { background: var(--bg-card); border: 1px solid var(--border); color: var(--text-main); }
.btn-danger { background: var(--danger); color: white; }
/* 表格样式 (增强版:支持表头固定) */
.order-table-wrapper { border: 1px solid var(--border); border-radius: 8px; overflow: auto; background: var(--bg-card); max-height: calc(100vh - 250px); /* 限制高度使内容可滚动 */ }
.order-table { width: 100%; border-collapse: collapse; font-size: 13px; text-align: left; }
/* 粘性表头 */
.order-table th { background: var(--bg-body); padding: 12px 15px; font-weight: 600; color: var(--text-muted); border-bottom: 1px solid var(--border); position: sticky; top: 0; z-index: 10; box-shadow: 0 1px 2px rgba(0,0,0,0.05); }
.order-table td { padding: 12px 15px; border-bottom: 1px solid var(--border); color: var(--text-main); vertical-align: top; }
.order-table tr:hover td { background: var(--primary-light); }
.tag { font-size: 11px; padding: 2px 6px; border-radius: 4px; display: inline-block; font-weight: 600; margin-right: 4px; }
.tag-urgent { background: #fee2e2; color: #b91c1c; border: 1px solid #fecaca; }
.tag-large { background: #e0f2fe; color: #0369a1; border: 1px solid #bae6fd; }
.tag-risk { background: #fef3c7; color: #b45309; border: 1px solid #fde68a; }
.tag-vip { background: #f3e8ff; color: #7e22ce; border: 1px solid #d8b4fe; }
.status-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; }
.status-pending { background: #f59e0b; }
.status-approved { background: #10b981; }
.status-rejected { background: #ef4444; }
.risk-bar-container { width: 80px; height: 6px; background: #e2e8f0; border-radius: 3px; margin-top: 5px; overflow: hidden; }
.risk-bar-fill { height: 100%; transition: width 0.3s; }
/* 抽屉 & 模态框 */
.drawer-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 100; display: none; opacity: 0; transition: opacity 0.3s; }
.drawer { position: fixed; top: 0; right: -600px; width: 500px; height: 100vh; background: var(--bg-card); z-index: 101; box-shadow: -5px 0 20px rgba(0,0,0,0.1); transition: right 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); display: flex; flex-direction: column; }
.drawer.open { right: 0; }
.drawer-backdrop.open { display: block; opacity: 1; }
.drawer-header { padding: 20px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; background: var(--bg-body); }
.drawer-body { flex: 1; padding: 25px; overflow-y: auto; }
.drawer-footer { padding: 15px 20px; border-top: 1px solid var(--border); display: flex; justify-content: flex-end; gap: 10px; background: var(--bg-body); }
.detail-section { margin-bottom: 25px; }
.detail-section h4 { margin: 0 0 10px 0; font-size: 14px; border-left: 3px solid var(--primary); padding-left: 8px; color: var(--text-main); }
.info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; font-size: 13px; }
.info-label { color: var(--text-muted); }
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.6); z-index: 200; display: none; justify-content: center; align-items: center; backdrop-filter: blur(2px); }
.modal { background: var(--bg-card); width: 400px; border-radius: 8px; padding: 0; box-shadow: 0 10px 25px rgba(0,0,0,0.2); transform: scale(0.9); transition: transform 0.2s; }
.modal.open { transform: scale(1); }
.modal-body { padding: 20px; text-align: center; }
.modal-actions { display: flex; border-top: 1px solid var(--border); }
.modal-actions button { flex: 1; padding: 15px; background: transparent; border: none; border-right: 1px solid var(--border); font-weight: 600; }
.modal-actions button:last-child { border-right: none; }
.modal-actions button.confirm { color: var(--danger); }
.toast-container { position: fixed; top: 20px; right: 20px; z-index: 300; display: flex; flex-direction: column; gap: 10px; }
.toast { background: var(--bg-card); padding: 12px 20px; border-radius: 4px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); border-left: 4px solid var(--primary); font-size: 13px; display: flex; align-items: center; gap: 10px; transform: translateX(120%); transition: transform 0.3s ease; }
.toast.show { transform: translateX(0); }
.switch { position: relative; display: inline-block; width: 40px; height: 20px; }
.switch input { opacity: 0; width: 0; height: 0; }
.slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #cbd5e1; transition: .4s; border-radius: 20px; }
.slider:before { position: absolute; content: ""; height: 16px; width: 16px; left: 2px; bottom: 2px; background-color: white; transition: .4s; border-radius: 50%; }
input:checked + .slider { background-color: var(--primary); }
input:checked + .slider:before { transform: translateX(20px); }
.loader { border: 2px solid #f3f3f3; border-radius: 50%; border-top: 2px solid var(--primary); width: 16px; height: 16px; animation: spin 1s linear infinite; display: inline-block; }
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
</style>
</head>
<body>
<svg style="display: none;">
<symbol id="i-home" viewBox="0 0 24 24"><path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/></symbol>
<symbol id="i-list" viewBox="0 0 24 24"><path d="M3 13h2v-2H3v2zm0 4h2v-2H3v2zm0-8h2V7H3v2zm4 4h14v-2H7v2zm0 4h14v-2H7v2zM7 7v2h14V7H7z"/></symbol>
<symbol id="i-risk" viewBox="0 0 24 24"><path d="M12 2L1 21h22L12 2zm1 14h-2v2h2v-2zm0-6h-2v4h2v-4z"/></symbol>
<symbol id="i-settings" viewBox="0 0 24 24"><path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L3.16 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.04.64.09.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.04.24.24.41.48.41h3.84c.24 0 .43-.17.47-.41l.36-2.54c.59-.24 1.13-.57 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.08-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/></symbol>
<symbol id="i-eye" viewBox="0 0 24 24"><path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/></symbol>
<symbol id="i-check" viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></symbol>
</svg>
<div class="app-container">
<header class="header">
<div class="brand">
<svg class="icon" style="width:24px; height:24px;"><use xlink:href="#i-check"></use></svg>
GlobalGuard <span>Audit V9.4</span>
</div>
<div class="nav-top">
<a onclick="app.nav('dashboard')" class="active">工作台</a>
<a onclick="app.nav('orders')">订单队列 (200+)</a>
<a onclick="app.nav('settings')">设置</a>
</div>
<div class="user-area">
<div style="text-align: right; font-size:12px;">
<div>Auditor_4421</div>
<div style="color:rgba(255,255,255,0.6)">华东审核组</div>
</div>
<div class="avatar">A</div>
<button class="btn" style="background:rgba(255,255,255,0.1); padding:5px 10px;" onclick="app.showLogout()">退出</button>
</div>
</header>
<div class="main-body">
<aside class="sidebar">
<div class="menu-group">
<div class="menu-header">Core Operations</div>
<div class="menu-item active" onclick="app.nav('dashboard')" id="menu-dashboard">
<svg class="icon"><use xlink:href="#i-home"></use></svg> 仪表盘概览
</div>
<div class="menu-item" onclick="app.nav('orders')" id="menu-orders">
<svg class="icon"><use xlink:href="#i-list"></use></svg> 待审核队列
<span class="badge" id="sidebar-count">--</span>
</div>
</div>
<div class="menu-group">
<div class="menu-header">Tools</div>
<div class="menu-item" onclick="app.nav('settings')" id="menu-settings">
<svg class="icon"><use xlink:href="#i-settings"></use></svg> 系统设置
</div>
</div>
<div style="margin-top:auto; padding:20px;">
<div style="background:var(--bg-body); padding:10px; border-radius:4px; font-size:11px; color:var(--text-muted);">
<strong>Server Status:</strong><br>
<span style="color:var(--success)"></span> Live Data Stream<br>
<span style="color:var(--success)"></span> Database (OK)
</div>
</div>
</aside>
<main class="content">
<div id="view-dashboard" class="view-panel active">
<h2 style="margin-bottom:20px;">今日数据大盘 (Live)</h2>
<div class="stat-grid">
<div class="stat-box">
<span class="stat-label">待审核订单</span>
<span class="stat-num" id="dash-pending">--</span>
<span class="stat-trend trend-down">需要立刻处理</span>
</div>
<div class="stat-box">
<span class="stat-label">今日高风险拦截</span>
<span class="stat-num" style="color:var(--danger)" id="dash-risk">--</span>
<span class="stat-trend">占比 <span id="dash-risk-rate">--%</span></span>
</div>
<div class="stat-box">
<span class="stat-label">涉及总金额 (CNY)</span>
<span class="stat-num" id="dash-amount">--</span>
<span class="stat-trend trend-up">较昨日 ↑ 23%</span>
</div>
<div class="stat-box">
<span class="stat-label">我的绩效 (今日)</span>
<span class="stat-num" style="color:var(--success)">42</span>
<span class="stat-trend">KPI 达成率 80%</span>
</div>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">系统公告</span>
<span class="tag tag-urgent">New</span>
</div>
<div style="font-size:13px; color:var(--text-muted); line-height:1.6;">
<p><strong>[安全通告] 关于近期显卡代购风险升级的通知</strong></p>
<p>近期检测到针对 RTX 5090 系列显卡的批量下单行为,请审核员重点关注“同一收货地址模糊匹配”以及“新注册账号大额支付”的情况。对于高风险订单建议直接转交二审。</p>
<hr style="border:0; border-top:1px dashed var(--border); margin:10px 0;">
<p><strong>[系统更新] V9.4 补丁说明</strong></p>
<p>已接入模拟数据生成引擎,现在演示环境可承载 200+ 并发订单展示。</p>
</div>
</div>
</div>
<div id="view-orders" class="view-panel">
<h2 style="margin-bottom:20px;">人工审核队列</h2>
<div class="filter-panel">
<div class="form-group">
<label>关键词 (订单号/用户/商品)</label>
<input type="text" class="input-control" id="search-keyword" placeholder="输入关键词...">
</div>
<div class="form-group">
<label>风险等级</label>
<select class="input-control" id="search-risk">
<option value="all">全部等级</option>
<option value="high">High (高风险)</option>
<option value="medium">Medium (中风险)</option>
<option value="low">Low (低风险)</option>
</select>
</div>
<div class="form-group">
<label>订单状态</label>
<select class="input-control" id="search-status">
<option value="pending">待审核 (Pending)</option>
<option value="all_history">全部历史记录</option>
</select>
</div>
<div style="margin-left:auto; display:flex; gap:10px;">
<button class="btn btn-secondary" onclick="app.resetFilter()">重置</button>
<button class="btn btn-primary" onclick="app.applyFilter()">
<svg class="icon"><use xlink:href="#i-list"></use></svg> 查询
</button>
</div>
</div>
<div class="order-table-wrapper">
<table class="order-table">
<thead>
<tr>
<th style="width:40px;"><input type="checkbox" id="check-all" onclick="app.toggleSelectAll()"></th>
<th>订单信息</th>
<th>用户画像</th>
<th>商品概览</th>
<th>金额</th>
<th>风险评分</th>
<th>状态</th>
<th style="text-align:right;">操作</th>
</tr>
</thead>
<tbody id="table-body">
</tbody>
</table>
<div id="empty-state" style="padding:40px; text-align:center; color:var(--text-muted); display:none;">
没有找到符合条件的订单
</div>
</div>
<div style="margin-top:15px; display:flex; gap:10px; align-items:center;">
<button class="btn btn-primary" onclick="app.batchAction('approve')">批量通过</button>
<button class="btn btn-danger" onclick="app.batchAction('reject')">批量驳回</button>
<span style="margin-left:auto; font-size:12px; color:var(--text-muted);">
显示 <strong id="current-count">0</strong> 条结果 / 共 <span id="total-db-count">0</span>
</span>
</div>
</div>
<div id="view-settings" class="view-panel">
<h2>系统偏好设置</h2>
<div class="card" style="margin-top:20px; max-width:600px;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
<div>
<h4 style="margin:0 0 5px 0;">深色模式 (Dark Mode)</h4>
<div style="font-size:12px; color:var(--text-muted);">切换界面为深色主题,适合夜间工作。</div>
</div>
<label class="switch">
<input type="checkbox" id="theme-toggle" onchange="app.toggleTheme()">
<span class="slider"></span>
</label>
</div>
<hr style="border:0; border-top:1px solid var(--border); margin:15px 0;">
<div style="display:flex; justify-content:space-between; align-items:center;">
<div>
<h4 style="margin:0 0 5px 0;">自动刷新列表</h4>
<div style="font-size:12px; color:var(--text-muted);">每隔 60 秒自动拉取新订单。</div>
</div>
<label class="switch">
<input type="checkbox" checked>
<span class="slider"></span>
</label>
</div>
</div>
</div>
</main>
</div>
<div class="drawer-backdrop" id="drawer-backdrop" onclick="app.closeDrawer()"></div>
<div class="drawer" id="drawer-panel">
<div class="drawer-header">
<h3 style="margin:0;">订单详情 <span id="d-id" style="font-weight:400; color:var(--text-muted);"></span></h3>
<button class="btn btn-secondary" style="padding:4px 8px;" onclick="app.closeDrawer()">&times;</button>
</div>
<div class="drawer-body" id="d-content">
</div>
<div class="drawer-footer" id="d-footer">
<button class="btn btn-danger" onclick="app.rejectCurrent()">驳回订单</button>
<button class="btn btn-primary" onclick="app.approveCurrent()">通过审核</button>
</div>
</div>
<div class="modal-overlay" id="modal-reject">
<div class="modal">
<div class="modal-body">
<h3 style="margin-top:0;">确认驳回订单?</h3>
<p style="color:var(--text-muted); font-size:13px; margin-bottom:15px;">请选择驳回理由,系统将发送通知给用户。</p>
<select class="input-control" id="reject-reason" style="width:100%; margin-bottom:10px;">
<option value="风险拦截-疑似盗刷">风险拦截 - 疑似盗刷</option>
<option value="信息不全-地址模糊">信息不全 - 地址模糊</option>
<option value="限购限制-超出数量">限购限制 - 超出数量</option>
<option value="其他原因">其他原因</option>
</select>
<textarea class="input-control" id="reject-note" style="width:100%; height:80px;" placeholder="备注信息 (可选)..."></textarea>
</div>
<div class="modal-actions">
<button onclick="app.closeModal('modal-reject')">取消</button>
<button class="confirm" onclick="app.confirmReject()">确认驳回</button>
</div>
</div>
</div>
<div class="modal-overlay" id="modal-logout">
<div class="modal">
<div class="modal-body">
<h3>退出系统</h3>
<p style="color:var(--text-muted);">确定要退出当前账号吗?未保存的操作将丢失。</p>
</div>
<div class="modal-actions">
<button onclick="app.closeModal('modal-logout')">取消</button>
<button class="confirm" onclick="location.reload()">确认退出</button>
</div>
</div>
</div>
<div class="toast-container" id="toast-root"></div>
</div>
<script>
const app = {
// 数据中心
data: {
orders: [], // 存储所有订单
currentId: null, // 当前查看/操作的ID
selectedIds: new Set(),
filter: {
keyword: '',
risk: 'all',
status: 'pending'
}
},
// 初始化
init() {
// 1. 保留核心“故事”数据 (V9.2)
const storyOrders = [
{ id: '20251221-A001', user: 'user_9921', userScore: 780, items: [{name: 'RTX 5090 显卡', qty: 2, price: 17499}], total: 34999.00, address: '北京市海淀区中关村软件园...', date: '2025-12-21 14:30', risk: 'low', score: 12, status: 'pending', tags: ['urgent'], logs: '支付指纹匹配IP归属地一致。' },
{ id: '20251221-A002', user: 'temp_user_001', userScore: null, items: [{name: '游戏点卡充值 (虚拟)', qty: 50, price: 100}], total: 5000.00, address: '虚拟发货 (自动)', date: '2025-12-21 14:31', risk: 'high', score: 92, status: 'pending', tags: ['risk'], logs: '警告该IP今日已尝试下单5次失败建议拦截。' },
{ id: '20251221-A003', user: 'loyal_cust_88', userScore: 900, items: [{name: '爱他美婴儿奶粉 3段', qty: 6, price: 200}], total: 1200.00, address: '上海市浦东新区陆家嘴...', date: '2025-12-21 14:32', risk: 'low', score: 5, status: 'pending', tags: [], logs: '历史购买记录匹配,常规复购。' },
{ id: '20251221-A004', user: 'company_buyer_22', userScore: 850, items: [{name: '人体工学办公椅', qty: 50, price: 300}], total: 15000.00, address: '深圳市南山区科技园...', date: '2025-12-21 14:35', risk: 'medium', score: 45, status: 'pending', tags: ['large'], logs: '首次大额采购,需电话核实企业资质。' },
{ id: '20251221-A007', user: 'unknown_proxy', userScore: 120, items: [{name: '亚马逊礼品卡 $100', qty: 10, price: 7100}], total: 7100.00, address: 'Email Delivery', date: '2025-12-21 14:40', risk: 'high', score: 98, status: 'pending', tags: ['risk'], logs: '检测到高危代理IP建议立即封锁。' }
];
// 2. 混合数据生成自动生成200条仿真数据
const randomData = this.generateBulkData(200);
this.data.orders = [...storyOrders, ...randomData];
// 渲染初始界面
this.renderTable();
this.updateDashboard();
console.log(`System Initialized: Loaded ${this.data.orders.length} orders.`);
this.showToast(`系统就绪,已加载 ${this.data.orders.length} 条数据`, 'success');
},
// --- 数据生成引擎 (适配 V9.2 结构) ---
generateBulkData(count) {
const products = [
{n: 'iPhone 16 Pro Max', p: 9999}, {n: 'AirPods Pro 3', p: 1899}, {n: 'Dyson 吹风机', p: 2999},
{n: 'Sony WH-1000XM5', p: 2499}, {n: 'Nike Dunk Low', p: 899}, {n: 'Logitech MX Master 3S', p: 799},
{n: 'Starbucks 星礼卡', p: 200}, {n: 'Switch OLED 日版', p: 2100}, {n: 'La Mer 面霜', p: 3500}
];
const users = ['alex', 'bill', 'candy', 'david', 'echo', 'frank', 'grace', 'helen', 'ivy', 'jack'];
const cities = ['北京', '上海', '广州', '深圳', '杭州', '成都', '武汉', '南京'];
const res = [];
for(let i=0; i<count; i++) {
const prod = products[Math.floor(Math.random() * products.length)];
const qty = Math.floor(Math.random() * 3) + 1;
const score = Math.floor(Math.random() * 100);
// 确定风险等级
let risk = 'low';
if(score > 60) risk = 'medium';
if(score > 85) risk = 'high';
// 随机状态 (80% pending)
const status = Math.random() > 0.8 ? (Math.random()>0.5 ? 'approved' : 'rejected') : 'pending';
// 随机标签
const tags = [];
if(risk === 'high') tags.push('risk');
if(prod.p * qty > 10000) tags.push('large');
if(Math.random() > 0.9) tags.push('vip');
res.push({
id: `20251222-R${(1000+i).toString()}`,
user: `${users[Math.floor(Math.random()*users.length)]}_${Math.floor(Math.random()*999)}`,
userScore: Math.floor(Math.random() * 500) + 300,
items: [{ name: prod.n, qty: qty, price: prod.p }],
total: prod.p * qty,
address: `${cities[Math.floor(Math.random()*cities.length)]}市...`,
date: `2025-12-22 14:${Math.floor(Math.random()*60).toString().padStart(2,'0')}`,
risk: risk,
score: score,
status: status,
tags: tags,
logs: '系统自动生成的仿真日志记录。'
});
}
return res;
},
// --- 导航与视图 ---
nav(viewId) {
document.querySelectorAll('.view-panel').forEach(el => el.classList.remove('active'));
document.getElementById(`view-${viewId}`).classList.add('active');
document.querySelectorAll('.menu-item').forEach(el => el.classList.remove('active'));
const menuId = `menu-${viewId}`;
const menuEl = document.getElementById(menuId);
if(menuEl) menuEl.classList.add('active');
document.querySelectorAll('.nav-top a').forEach(el => el.classList.remove('active'));
if(viewId === 'orders') document.querySelector('.nav-top a:nth-child(2)').classList.add('active');
if(viewId === 'dashboard') document.querySelector('.nav-top a:nth-child(1)').classList.add('active');
if(viewId === 'settings') document.querySelector('.nav-top a:nth-child(3)').classList.add('active');
},
toggleTheme() {
const isDark = document.getElementById('theme-toggle').checked;
document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
},
// --- 核心业务逻辑:筛选与渲染 ---
applyFilter() {
const btn = event.target.closest('button');
if(btn) {
const originalHtml = btn.innerHTML;
btn.innerHTML = '<span class="loader"></span> 查询中';
btn.disabled = true;
setTimeout(() => {
this._executeFilter();
this.showToast('筛选已更新');
btn.innerHTML = originalHtml;
btn.disabled = false;
}, 300);
} else {
this._executeFilter();
}
},
_executeFilter() {
this.data.filter.keyword = document.getElementById('search-keyword').value.toLowerCase();
this.data.filter.risk = document.getElementById('search-risk').value;
this.data.filter.status = document.getElementById('search-status').value;
this.renderTable();
},
resetFilter() {
document.getElementById('search-keyword').value = '';
document.getElementById('search-risk').value = 'all';
document.getElementById('search-status').value = 'pending';
this.applyFilter();
},
renderTable() {
const tbody = document.getElementById('table-body');
tbody.innerHTML = '';
const filtered = this.data.orders.filter(order => {
const matchKw = !this.data.filter.keyword ||
order.id.toLowerCase().includes(this.data.filter.keyword) ||
order.user.toLowerCase().includes(this.data.filter.keyword) ||
order.items.some(i => i.name.toLowerCase().includes(this.data.filter.keyword));
const matchRisk = this.data.filter.risk === 'all' || order.risk === this.data.filter.risk;
const matchStatus = this.data.filter.status === 'all_history' ? true : order.status === this.data.filter.status;
return matchKw && matchRisk && matchStatus;
});
// 限制渲染数量以保证性能如果超过500条
const displayList = filtered.slice(0, 500);
// 更新计数
const pendingCount = this.data.orders.filter(o => o.status === 'pending').length;
document.getElementById('sidebar-count').innerText = pendingCount;
document.getElementById('empty-state').style.display = displayList.length ? 'none' : 'block';
document.getElementById('current-count').innerText = displayList.length;
document.getElementById('total-db-count').innerText = this.data.orders.length;
// 生成 HTML
let htmlBuffer = '';
displayList.forEach(order => {
let riskColor = '#10b981';
if(order.risk === 'medium') riskColor = '#f59e0b';
if(order.risk === 'high') riskColor = '#ef4444';
let statusBadge = `<span class="status-dot status-${order.status}"></span>Pending`;
if(order.status === 'approved') statusBadge = `<span class="status-dot status-approved"></span>Approved`;
if(order.status === 'rejected') statusBadge = `<span class="status-dot status-rejected"></span>Rejected`;
const itemSummary = `${order.items[0].name} ${order.items.length > 1 ? `${order.items.length}` : ''}`;
const tagsHtml = order.tags.map(t => {
if(t === 'urgent') return `<span class="tag tag-urgent">加急</span>`;
if(t === 'large') return `<span class="tag tag-large">大宗</span>`;
if(t === 'risk') return `<span class="tag tag-risk">风险</span>`;
if(t === 'vip') return `<span class="tag tag-vip">VIP</span>`;
return '';
}).join('');
htmlBuffer += `
<tr>
<td><input type="checkbox" class="row-check" value="${order.id}" onchange="app.toggleSelect('${order.id}')" ${this.data.selectedIds.has(order.id) ? 'checked' : ''} ${order.status !== 'pending' ? 'disabled' : ''}></td>
<td>
<div style="font-weight:600; font-family:'Consolas', monospace;">${order.id}</div>
<div style="font-size:12px; color:var(--text-muted);">${order.date}</div>
<div style="margin-top:4px;">${tagsHtml}</div>
</td>
<td>
<div>${order.user}</div>
<div style="font-size:11px; color:var(--text-muted);">信誉分: ${order.userScore || 'N/A'}</div>
</td>
<td><div title="${itemSummary}">${itemSummary}</div></td>
<td style="font-weight:600; color:var(--text-main);">¥ ${order.total.toLocaleString()}</td>
<td>
<div style="font-weight:bold; color:${riskColor};">${order.score} / 100</div>
<div class="risk-bar-container">
<div class="risk-bar-fill" style="width: ${order.score}%; background: ${riskColor};"></div>
</div>
</td>
<td>${statusBadge}</td>
<td style="text-align:right;">
<button class="btn btn-secondary" style="padding:4px 8px;" onclick="app.openDrawer('${order.id}')">
<svg class="icon" style="margin:0;"><use xlink:href="#i-eye"></use></svg>
</button>
</td>
</tr>
`;
});
tbody.innerHTML = htmlBuffer;
document.getElementById('check-all').checked = false;
},
updateDashboard() {
const pending = this.data.orders.filter(o => o.status === 'pending');
const highRisk = this.data.orders.filter(o => o.risk === 'high' && o.status === 'pending');
const totalAmt = pending.reduce((sum, o) => sum + o.total, 0);
document.getElementById('dash-pending').innerText = pending.length;
document.getElementById('dash-risk').innerText = highRisk.length;
document.getElementById('dash-risk-rate').innerText = ((highRisk.length / (pending.length || 1)) * 100).toFixed(1) + '%';
document.getElementById('dash-amount').innerText = totalAmt.toLocaleString();
},
// --- 详情抽屉逻辑 ---
openDrawer(id) {
this.data.currentId = id;
const order = this.data.orders.find(o => o.id === id);
if(!order) return;
document.getElementById('d-id').innerText = `#${order.id}`;
const content = document.getElementById('d-content');
let itemsHtml = order.items.map(item => `
<div style="display:flex; justify-content:space-between; border-bottom:1px dashed #eee; padding:8px 0;">
<span>${item.name} x ${item.qty}</span>
<span style="font-weight:600;">¥ ${(item.price * item.qty).toLocaleString()}</span>
</div>
`).join('');
content.innerHTML = `
<div class="detail-section">
<h4>风控决策引擎</h4>
<div style="background:${order.risk === 'high' ? '#fff5f5' : '#f0fdf4'}; padding:10px; border-radius:4px; border:1px solid ${order.risk === 'high' ? '#feb2b2' : '#bbf7d0'};">
<div style="font-weight:bold; color:${order.risk === 'high' ? '#c53030' : '#22543d'};">
综合风险分: ${order.score} (${order.risk.toUpperCase()})
</div>
<p style="margin:5px 0 0 0; font-size:12px; color:var(--text-muted);">${order.logs}</p>
</div>
</div>
<div class="detail-section">
<h4>收货信息</h4>
<div class="info-grid">
<span class="info-label">收货人:</span> <span class="info-val">${order.user}</span>
<span class="info-label">收货地址:</span> <span class="info-val" style="grid-column:span 2">${order.address}</span>
</div>
</div>
<div class="detail-section">
<h4>商品清单</h4>
${itemsHtml}
<div style="text-align:right; margin-top:10px; font-size:16px; font-weight:700;">
总计: ¥ ${order.total.toLocaleString()}
</div>
</div>
`;
const footer = document.getElementById('d-footer');
footer.style.display = order.status !== 'pending' ? 'none' : 'flex';
document.getElementById('drawer-backdrop').classList.add('open');
document.getElementById('drawer-panel').classList.add('open');
},
closeDrawer() {
document.getElementById('drawer-backdrop').classList.remove('open');
document.getElementById('drawer-panel').classList.remove('open');
this.data.currentId = null;
},
// --- 操作逻辑 ---
approveCurrent() {
const id = this.data.currentId;
const order = this.data.orders.find(o => o.id === id);
if(order && order.status === 'pending') {
order.status = 'approved';
this.showToast(`订单 ${id} 已审核通过`, 'success');
this.refreshAll();
this.closeDrawer();
}
},
rejectCurrent() {
document.getElementById('modal-reject').style.display = 'flex';
setTimeout(() => document.getElementById('modal-reject').querySelector('.modal').classList.add('open'), 10);
},
confirmReject() {
const id = this.data.currentId;
const reason = document.getElementById('reject-reason').value;
if (id === 'batch') {
let count = 0;
this.data.selectedIds.forEach(sid => {
const o = this.data.orders.find(x => x.id === sid);
if(o && o.status === 'pending') {
o.status = 'rejected';
count++;
}
});
this.data.selectedIds.clear();
this.showToast(`已批量驳回 ${count} 个订单`, 'success');
} else {
const order = this.data.orders.find(o => o.id === id);
if(order) {
order.status = 'rejected';
order.logs += ` [人工驳回: ${reason}]`;
}
this.showToast(`订单 ${id} 已驳回`, 'success');
}
this.closeModal('modal-reject');
this.refreshAll();
this.closeDrawer();
},
toggleSelectAll() {
const checkAll = document.getElementById('check-all').checked;
const checkboxes = document.querySelectorAll('.row-check:not(:disabled)');
checkboxes.forEach(cb => {
cb.checked = checkAll;
this.toggleSelect(cb.value);
});
},
toggleSelect(id) {
if(this.data.selectedIds.has(id)) this.data.selectedIds.delete(id);
else this.data.selectedIds.add(id);
},
batchAction(type) {
if(this.data.selectedIds.size === 0) return this.showToast('请先选择订单', 'error');
if(type === 'approve') {
let count = 0;
this.data.selectedIds.forEach(id => {
const o = this.data.orders.find(x => x.id === id);
if(o && o.status === 'pending') {
o.status = 'approved';
count++;
}
});
this.showToast(`成功批量通过 ${count}`, 'success');
this.data.selectedIds.clear();
this.refreshAll();
} else if (type === 'reject') {
this.data.currentId = 'batch';
this.rejectCurrent();
}
},
refreshAll() {
this.renderTable();
this.updateDashboard();
},
showLogout() {
document.getElementById('modal-logout').style.display = 'flex';
setTimeout(() => document.getElementById('modal-logout').querySelector('.modal').classList.add('open'), 10);
},
closeModal(id) {
document.getElementById(id).querySelector('.modal').classList.remove('open');
setTimeout(() => document.getElementById(id).style.display = 'none', 200);
},
showToast(msg, type='info') {
const container = document.getElementById('toast-root');
const el = document.createElement('div');
el.className = `toast ${type}`;
el.innerHTML = `<span>${msg}</span>`;
container.appendChild(el);
setTimeout(() => el.classList.add('show'), 10);
setTimeout(() => {
el.classList.remove('show');
setTimeout(() => el.remove(), 300);
}, 3000);
}
};
window.addEventListener('DOMContentLoaded', () => app.init());
</script>
</body>
</html>