basic test

This commit is contained in:
邓智航
2026-02-10 11:56:03 +08:00
parent de68563f69
commit 411d823cfa
89 changed files with 33858 additions and 2027 deletions

811
002/2-all.html Normal file
View File

@@ -0,0 +1,811 @@
<!DOCTYPE html>
<html lang="en">
<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 {
/* Color System - Maintain V9.2 Modern Style */
--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;
/* Status Colors */
--success: #10b981;
--warning: #f59e0b;
--danger: #ef4444;
--info: #06b6d4;
/* Dimensions & Shadows */
--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);
}
/* Dark Mode Variables */
[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);
}
/* --- Basic Resets --- */
* { 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; }
/* --- Layout Framework --- */
.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; }
/* Table Styles (Enhanced: Sticky Header Support) */
.order-table-wrapper { border: 1px solid var(--border); border-radius: 8px; overflow: auto; background: var(--bg-card); max-height: calc(100vh - 250px); /* Limit height for scrolling */ }
.order-table { width: 100%; border-collapse: collapse; font-size: 13px; text-align: left; }
/* Sticky Header */
.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 & Modals */
.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">Workspace</a>
<a onclick="app.nav('orders')">Order Queue (200+)</a>
<a onclick="app.nav('settings')">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)">East China Audit Group</div>
</div>
<div class="avatar">A</div>
<button class="btn" style="background:rgba(255,255,255,0.1); padding:5px 10px;" onclick="app.showLogout()">Logout</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> Dashboard Overview
</div>
<div class="menu-item" onclick="app.nav('orders')" id="menu-orders">
<svg class="icon"><use xlink:href="#i-list"></use></svg> Audit Queue
<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> System Settings
</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;">Today's Data Overview (Live)</h2>
<div class="stat-grid">
<div class="stat-box">
<span class="stat-label">Pending Orders</span>
<span class="stat-num" id="dash-pending">--</span>
<span class="stat-trend trend-down">Action Required</span>
</div>
<div class="stat-box">
<span class="stat-label">High Risk Intercepts</span>
<span class="stat-num" style="color:var(--danger)" id="dash-risk">--</span>
<span class="stat-trend">Ratio <span id="dash-risk-rate">--%</span></span>
</div>
<div class="stat-box">
<span class="stat-label">Total Amount (CNY)</span>
<span class="stat-num" id="dash-amount">--</span>
<span class="stat-trend trend-up">vs Yesterday ↑ 23%</span>
</div>
<div class="stat-box">
<span class="stat-label">My Performance (Today)</span>
<span class="stat-num" style="color:var(--success)">42</span>
<span class="stat-trend">KPI Achievement 80%</span>
</div>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">System Announcements</span>
<span class="tag tag-urgent">New</span>
</div>
<div style="font-size:13px; color:var(--text-muted); line-height:1.6;">
<p><strong>[Security Alert] Notification on Risk Upgrade for Graphics Card Proxy Buying</strong></p>
<p>Recently detected bulk ordering behavior targeting RTX 5090 series graphics cards. Auditors please focus on "fuzzy matching of same shipping address" and "large payments from new accounts". High-risk orders should be transferred to Level 2 Audit.</p>
<hr style="border:0; border-top:1px dashed var(--border); margin:10px 0;">
<p><strong>[System Update] V9.4 Patch Notes</strong></p>
<p>Integrated mock data generation engine, demo environment now supports 200+ concurrent order displays.</p>
</div>
</div>
</div>
<div id="view-orders" class="view-panel">
<h2 style="margin-bottom:20px;">Manual Audit Queue</h2>
<div class="filter-panel">
<div class="form-group">
<label>Keywords (Order/User/Product)</label>
<input type="text" class="input-control" id="search-keyword" placeholder="Enter keywords...">
</div>
<div class="form-group">
<label>Risk Level</label>
<select class="input-control" id="search-risk">
<option value="all">All Levels</option>
<option value="high">High (High Risk)</option>
<option value="medium">Medium (Medium Risk)</option>
<option value="low">Low (Low Risk)</option>
</select>
</div>
<div class="form-group">
<label>Order Status</label>
<select class="input-control" id="search-status">
<option value="pending">Pending Audit</option>
<option value="all_history">All History</option>
</select>
</div>
<div style="margin-left:auto; display:flex; gap:10px;">
<button class="btn btn-secondary" onclick="app.resetFilter()">Reset</button>
<button class="btn btn-primary" onclick="app.applyFilter()">
<svg class="icon"><use xlink:href="#i-list"></use></svg> Search
</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>Order Info</th>
<th>User Profile</th>
<th>Product Overview</th>
<th>Amount</th>
<th>Risk Score</th>
<th>Status</th>
<th style="text-align:right;">Actions</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;">
No orders found matching your criteria
</div>
</div>
<div style="margin-top:15px; display:flex; gap:10px; align-items:center;">
<button class="btn btn-primary" onclick="app.batchAction('approve')">Batch Approve</button>
<button class="btn btn-danger" onclick="app.batchAction('reject')">Batch Reject</button>
<span style="margin-left:auto; font-size:12px; color:var(--text-muted);">
Showing <strong id="current-count">0</strong> results / Total <span id="total-db-count">0</span>
</span>
</div>
</div>
<div id="view-settings" class="view-panel">
<h2>System Preferences</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);">Switch interface to dark theme, suitable for night work.</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;">Auto-Refresh List</h4>
<div style="font-size:12px; color:var(--text-muted);">Automatically fetch new orders every 60 seconds.</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;">Order Details <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()">Reject Order</button>
<button class="btn btn-primary" onclick="app.approveCurrent()">Approve Order</button>
</div>
</div>
<div class="modal-overlay" id="modal-reject">
<div class="modal">
<div class="modal-body">
<h3 style="margin-top:0;">Confirm Reject?</h3>
<p style="color:var(--text-muted); font-size:13px; margin-bottom:15px;">Please select a reason for rejection. The system will notify the user.</p>
<select class="input-control" id="reject-reason" style="width:100%; margin-bottom:10px;">
<option value="风险拦截-疑似盗刷">Risk Intercept - Suspected Fraud</option>
<option value="信息不全-地址模糊">Incomplete Info - Vague Address</option>
<option value="限购限制-超出数量">Purchase Limit - Exceeded Quantity</option>
<option value="其他原因">Other Reason</option>
</select>
<textarea class="input-control" id="reject-note" style="width:100%; height:80px;" placeholder="Notes (Optional)..."></textarea>
</div>
<div class="modal-actions">
<button onclick="app.closeModal('modal-reject')">Cancel</button>
<button class="confirm" onclick="app.confirmReject()">Confirm Reject</button>
</div>
</div>
</div>
<div class="modal-overlay" id="modal-logout">
<div class="modal">
<div class="modal-body">
<h3>System Logout</h3>
<p style="color:var(--text-muted);">Are you sure you want to log out? Unsaved changes will be lost.</p>
</div>
<div class="modal-actions">
<button onclick="app.closeModal('modal-logout')">Cancel</button>
<button class="confirm" onclick="location.reload()">Confirm Logout</button>
</div>
</div>
</div>
<div class="toast-container" id="toast-root"></div>
</div>
<script>
const app = {
// Data Center
data: {
orders: [], // Store all orders
currentId: null, // Currently viewed/operated ID
selectedIds: new Set(),
filter: {
keyword: '',
risk: 'all',
status: 'pending'
}
},
// Initialization
init() {
// 1. Keep core "Story" data (V9.2)
const storyOrders = [
{ id: '20251221-A001', user: 'user_9921', userScore: 780, items: [{name: 'RTX 5090 Graphics Card', qty: 2, price: 17499}], total: 34999.00, address: 'Zhongguancun Software Park, Haidian District, Beijing...', date: '2025-12-21 14:30', risk: 'low', score: 12, status: 'pending', tags: ['urgent'], logs: 'Payment fingerprint match. IP location consistent.' },
{ id: '20251221-A002', user: 'temp_user_001', userScore: null, items: [{name: 'Game Card Top-up (Virtual)', qty: 50, price: 100}], total: 5000.00, address: 'Virtual Delivery (Auto)', date: '2025-12-21 14:31', risk: 'high', score: 92, status: 'pending', tags: ['risk'], logs: 'Warning: This IP failed 5 order attempts today. Interception recommended.' },
{ id: '20251221-A003', user: 'loyal_cust_88', userScore: 900, items: [{name: 'Aptamil Infant Formula Stage 3', qty: 6, price: 200}], total: 1200.00, address: 'Lujiazui, Pudong New Area, Shanghai...', date: '2025-12-21 14:32', risk: 'low', score: 5, status: 'pending', tags: [], logs: 'History match. Routine repurchase.' },
{ id: '20251221-A004', user: 'company_buyer_22', userScore: 850, items: [{name: 'Ergonomic Office Chair', qty: 50, price: 300}], total: 15000.00, address: 'Science Park, Nanshan District, Shenzhen...', date: '2025-12-21 14:35', risk: 'medium', score: 45, status: 'pending', tags: ['large'], logs: 'First large purchase. Phone verification of business qualifications required.' },
{ id: '20251221-A007', user: 'unknown_proxy', userScore: 120, items: [{name: 'Amazon Gift Card $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: 'High-risk proxy IP detected. Immediate block recommended.' }
];
// 2. Hybrid Data Gen: Auto-generate 200 simulation records
const randomData = this.generateBulkData(200);
this.data.orders = [...storyOrders, ...randomData];
// Render Initial View
this.renderTable();
this.updateDashboard();
console.log(`System Initialized: Loaded ${this.data.orders.length} orders.`);
this.showToast(`System ready, loaded ${this.data.orders.length} records`, 'success');
},
// --- Data Gen Engine (Adapted V9.2) ---
generateBulkData(count) {
const products = [
{n: 'iPhone 16 Pro Max', p: 9999}, {n: 'AirPods Pro 3', p: 1899}, {n: 'Dyson Hair Dryer', p: 2999},
{n: 'Sony WH-1000XM5', p: 2499}, {n: 'Nike Dunk Low', p: 899}, {n: 'Logitech MX Master 3S', p: 799},
{n: 'Starbucks Gift Card', p: 200}, {n: 'Switch OLED (JP Ver)', p: 2100}, {n: 'La Mer Cream', p: 3500}
];
const users = ['alex', 'bill', 'candy', 'david', 'echo', 'frank', 'grace', 'helen', 'ivy', 'jack'];
const cities = ['Beijing', 'Shanghai', 'Guangzhou', 'Shenzhen', 'Hangzhou', 'Chengdu', 'Wuhan', 'Nanjing'];
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);
// Determine risk level
let risk = 'low';
if(score > 60) risk = 'medium';
if(score > 85) risk = 'high';
// Random status (80% pending)
const status = Math.random() > 0.8 ? (Math.random()>0.5 ? 'approved' : 'rejected') : 'pending';
// Random tags
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)]} City...`,
date: `2025-12-22 14:${Math.floor(Math.random()*60).toString().padStart(2,'0')}`,
risk: risk,
score: score,
status: status,
tags: tags,
logs: 'System generated mock log record.'
});
}
return res;
},
// --- Navigation & View ---
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');
},
// --- Core Business Logic: Filter & Render ---
applyFilter() {
const btn = event.target.closest('button');
if(btn) {
const originalHtml = btn.innerHTML;
btn.innerHTML = '<span class="loader"></span> Searching...';
btn.disabled = true;
setTimeout(() => {
this._executeFilter();
this.showToast('Filters updated');
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;
});
// Limit render count for performance (if > 500)
const displayList = filtered.slice(0, 500);
// Update counts
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;
// Generate 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 ? `and ${order.items.length} more` : ''}`;
const tagsHtml = order.tags.map(t => {
if(t === 'urgent') return `<span class="tag tag-urgent">Urgent</span>`;
if(t === 'large') return `<span class="tag tag-large">Large</span>`;
if(t === 'risk') return `<span class="tag tag-risk">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);">Credit Score: ${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();
},
// --- Drawer Logic ---
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>Risk Control Decision Engine</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'};">
Composite Risk Score: ${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>Shipping Info</h4>
<div class="info-grid">
<span class="info-label">Consignee:</span> <span class="info-val">${order.user}</span>
<span class="info-label">Address:</span> <span class="info-val" style="grid-column:span 2">${order.address}</span>
</div>
</div>
<div class="detail-section">
<h4>Item List</h4>
${itemsHtml}
<div style="text-align:right; margin-top:10px; font-size:16px; font-weight:700;">
Total: ¥ ${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;
},
// --- Action Logic ---
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(`Order ${id} approved`, '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(`Batch rejected ${count} orders`, 'success');
} else {
const order = this.data.orders.find(o => o.id === id);
if(order) {
order.status = 'rejected';
order.logs += ` [Manual Rejection: ${reason}]`;
}
this.showToast(`Order ${id} rejected`, '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('Please select orders first', '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(`Successfully batch approved ${count} orders`, '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>