811 lines
43 KiB
HTML
811 lines
43 KiB
HTML
<!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()">×</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> |