Files
2026-07-27 14:07:26 +08:00

1280 lines
53 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>易分宝现场回收 - 管理后台</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background: #f5f7fa; }
.login-page { display: flex; justify-content: center; align-items: center; min-height: 100vh; background: linear-gradient(135deg, #3366ff 0%, #254edb 100%); }
.login-box { background: white; border-radius: 16px; padding: 48px 40px; width: 100%; max-width: 420px; box-shadow: 0 20px 60px rgba(0,0,0,0.2); }
.login-box .logo { text-align: center; margin-bottom: 32px; }
.login-box .logo h1 { color: #3366ff; font-size: 28px; }
.login-box .form-group { margin-bottom: 24px; }
.login-box .form-group label { display: block; font-size: 14px; color: #666; margin-bottom: 8px; }
.login-box .form-group input { width: 100%; padding: 14px 16px; border: 1px solid #e8e8e8; border-radius: 8px; font-size: 14px; transition: all 0.3s; }
.login-box .form-group input:focus { outline: none; border-color: #3366ff; box-shadow: 0 0 0 2px rgba(51,102,255,0.1); }
.login-box .login-btn { width: 100%; padding: 14px; background: linear-gradient(135deg, #3366ff 0%, #254edb 100%); color: white; border: none; border-radius: 8px; font-size: 16px; cursor: pointer; transition: all 0.3s; }
.login-box .login-btn:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(51,102,255,0.3); }
.login-box .error-msg { color: #ff4d4f; font-size: 12px; margin-top: 12px; display: none; }
.admin-layout { display: flex; min-height: 100vh; }
.sidebar { width: 220px; background: #1f2937; color: white; padding: 24px 0; }
.sidebar .logo { padding: 20px 24px; margin-bottom: 32px; display: flex; align-items: center; }
.sidebar .logo img.logo-icon { width: 30px; height: 30px; margin-right: 10px; object-fit: contain; }
.sidebar .logo h2 { font-size: 18px; color: #fff; margin: 0; }
.sidebar .menu { list-style: none; }
.sidebar .menu li { margin-bottom: 4px; }
.sidebar .menu li a { display: flex; align-items: center; padding: 12px 24px; color: #9ca3af; text-decoration: none; transition: all 0.3s; }
.sidebar .menu li a:hover { background: rgba(255,255,255,0.1); color: #fff; }
.sidebar .menu li a.active { background: #3366ff; color: #fff; }
.sidebar .menu li a img.menu-icon { width: 20px; height: 20px; margin-right: 12px; object-fit: contain; }
.main-content { flex: 1; display: flex; flex-direction: column; }
.header { background: white; padding: 16px 24px; display: flex; justify-content: space-between; align-items: center; box-shadow: 0 2px 8px rgba(0,0,0,0.05); position: sticky; top: 0; z-index: 100; }
.header .title { font-size: 20px; font-weight: 600; color: #333; flex: 1; }
.header .user-info { display: flex; align-items: center; margin-left: auto; }
.header .user-info span { margin-right: 16px; color: #666; }
.header .logout-btn { padding: 8px 16px; background: #f5f5f5; border: none; border-radius: 4px; cursor: pointer; color: #666; }
.header .logout-btn:hover { background: #e8e8e8; }
.content-area { padding: 24px; }
.card { background: white; border-radius: 12px; padding: 24px; margin-bottom: 24px; box-shadow: 0 4px 12px rgba(0,0,0,0.05); }
.card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; }
.card-header h3 { font-size: 18px; font-weight: 600; color: #333; }
.card-header .btn { padding: 8px 16px; background: #3366ff; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; }
.card-header .btn:hover { background: #254edb; }
.card-header .btn-secondary { background: #666; }
.card-header .btn-secondary:hover { background: #444; }
.table { width: 100%; border-collapse: collapse; }
.table th, .table td { padding: 12px 16px; text-align: center; border-bottom: 1px solid #f0f0f0; }
.table th { background: #fafafa; font-weight: 600; color: #666; font-size: 14px; }
.table td { color: #333; font-size: 14px; }
.table .action-btns { display: flex; gap: 8px; }
.table .action-btns button { padding: 4px 12px; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; }
.table .action-btns .edit-btn { background: #fff3cd; color: #856404; }
.table .action-btns .delete-btn { background: #f8d7da; color: #721c24; }
.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: none; justify-content: center; align-items: center; z-index: 1000; }
.modal-overlay.show { display: flex; }
.modal { background: white; border-radius: 12px; padding: 24px; width: 100%; max-width: 480px; }
.modal h4 { font-size: 18px; margin-bottom: 24px; color: #333; }
.modal .form-group { margin-bottom: 16px; }
.modal .form-group label { display: block; font-size: 14px; color: #666; margin-bottom: 8px; }
.modal .form-group input, .modal .form-group select { width: 100%; padding: 10px 12px; border: 1px solid #e8e8e8; border-radius: 4px; font-size: 14px; }
.modal .modal-footer { display: flex; justify-content: flex-end; gap: 12px; margin-top: 24px; }
.modal .modal-footer button { padding: 8px 20px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; }
.modal .modal-footer .cancel-btn { background: #f5f5f5; color: #666; }
.modal .modal-footer .confirm-btn { background: #3366ff; color: white; }
.summary-cards { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin-bottom: 24px; }
.summary-card { background: white; border-radius: 12px; padding: 24px; box-shadow: 0 4px 12px rgba(0,0,0,0.05); text-align: center; }
.summary-card .card-icon { font-size: 32px; margin-bottom: 12px; }
.summary-card .card-icon-img { width: 40px; height: 40px; margin-bottom: 12px; object-fit: contain; }
.summary-card .card-value { font-size: 28px; font-weight: 700; color: #3366ff; display: block; }
.summary-card .card-label { font-size: 14px; color: #999; margin-top: 4px; }
.chart-section { background: white; border-radius: 12px; padding: 24px; box-shadow: 0 4px 12px rgba(0,0,0,0.05); }
.chart-section h3 { font-size: 18px; font-weight: 600; color: #333; margin-bottom: 24px; }
.chart-bar-wrap { display: flex; align-items: flex-end; height: 200px; gap: 20px; padding: 20px 0; }
.chart-bar-item { flex: 1; display: flex; flex-direction: column; align-items: center; }
.chart-bar-item .bar { width: 100%; background: linear-gradient(180deg, #3366ff 0%, #254edb 100%); border-radius: 4px 4px 0 0; min-height: 4px; transition: height 0.5s; }
.chart-bar-item .label { font-size: 12px; color: #666; margin-top: 8px; }
.bar-chart-container { display: flex; flex-direction: column; gap: 12px; padding: 10px 0; }
.bar-chart-row { display: flex; align-items: center; gap: 16px; }
.bar-chart-label { width: 80px; font-size: 14px; color: #333; text-align: right; flex-shrink: 0; }
.bar-chart-bar-wrap { flex: 1; height: 32px; background: #f5f5f5; border-radius: 16px; overflow: hidden; position: relative; }
.bar-chart-bar { height: 100%; background: linear-gradient(90deg, #3366ff 0%, #254edb 100%); border-radius: 16px; transition: width 0.5s; display: flex; align-items: center; justify-content: flex-end; padding-right: 12px; }
.bar-chart-value { font-size: 12px; color: white; font-weight: 600; }
.bar-chart-percent { margin-left: 12px; font-size: 12px; color: #666; min-width: 60px; }
.status-badge { padding: 4px 12px; border-radius: 20px; font-size: 12px; }
.status-badge.pending { background: #fff3cd; color: #856404; }
.status-badge.completed { background: #d4edda; color: #155724; }
.status-badge.cancelled { background: #f8d7da; color: #721c24; }
.empty-state { text-align: center; padding: 60px 0; color: #999; }
.empty-state .icon { font-size: 48px; margin-bottom: 16px; }
.empty-state p { font-size: 14px; }
.pagination { display: flex; justify-content: center; align-items: center; gap: 8px; margin-top: 24px; }
.pagination button { padding: 8px 16px; border: 1px solid #e8e8e8; border-radius: 4px; background: white; cursor: pointer; }
.pagination button.active { background: #3366ff; color: white; border-color: #3366ff; }
.pagination button:disabled { opacity: 0.5; cursor: not-allowed; }
</style>
</head>
<body>
<div id="app">
<div id="login-page" class="login-page">
<div class="login-box">
<div class="logo">
<img src="logo.png" style="width: 80px; height: 80px; border-radius: 8px; margin-bottom: 16px;" />
<h1>易分宝管理后台</h1>
</div>
<div class="form-group">
<label>用户名</label>
<input type="text" id="username" placeholder="请输入用户名">
</div>
<div class="form-group">
<label>密码</label>
<input type="password" id="password" placeholder="请输入密码">
</div>
<button class="login-btn" onclick="handleLogin()">登录</button>
<div class="error-msg" id="login-error">用户名或密码错误</div>
</div>
</div>
<div id="admin-layout" class="admin-layout" style="display: none;">
<div class="sidebar">
<div class="logo">
<img src="icons/logo.png" class="logo-icon">
<h2>易分宝回收</h2>
</div>
<ul class="menu">
<li><a href="#" onclick="showPage('statistics')" id="menu-statistics"><img src="icons/report.png" class="menu-icon">统计报表</a></li>
<li><a href="#" onclick="showPage('orders')" id="menu-orders"><img src="icons/order.png" class="menu-icon">订单管理</a></li>
<li><a href="#" onclick="showPage('prices')" id="menu-prices"><img src="icons/price.png" class="menu-icon">价格管理</a></li>
<li><a href="#" onclick="showPage('regions')" id="menu-regions"><img src="icons/area.png" class="menu-icon">区域管理</a></li>
<li><a href="#" onclick="showPage('users')" id="menu-users"><img src="icons/user.png" class="menu-icon">用户管理</a></li>
</ul>
</div>
<div class="main-content">
<div class="header">
<div class="title" id="page-title">统计报表</div>
<div class="user-info">
<span>管理员</span>
<button class="logout-btn" onclick="handleLogout()">退出登录</button>
</div>
</div>
<div class="content-area" id="content-area">
<div id="page-statistics">
<div class="summary-cards">
<div class="summary-card">
<img src="icons/dingdan.png" class="card-icon-img">
<div class="card-value" id="stat-total-orders">0</div>
<div class="card-label">总订单数</div>
</div>
<div class="summary-card">
<img src="icons/jine.png" class="card-icon-img">
<div class="card-value">¥<span id="stat-total-amount">0.00</span></div>
<div class="card-label">总金额</div>
</div>
<div class="summary-card">
<img src="icons/zhongliang.png" class="card-icon-img">
<div class="card-value" id="stat-total-weight">0.00</div>
<div class="card-label">总回收量(kg)</div>
</div>
<div class="summary-card">
<img src="icons/danjia.png" class="card-icon-img">
<div class="card-value" id="stat-avg-amount">0.00</div>
<div class="card-label">平均单价</div>
</div>
</div>
<div class="card">
<h3>品类重量统计</h3>
<table class="table">
<thead>
<tr>
<th>品类名称</th>
<th>订单数</th>
<th>总重量(kg)</th>
<th>重量占比</th>
<th>总金额</th>
<th>金额占比</th>
</tr>
</thead>
<tbody id="category-weight-table"></tbody>
</table>
<div id="category-weight-empty" class="empty-state" style="display: none;">
<div class="icon">📊</div>
</div>
</div>
<div class="chart-section">
<h3>品类回收排行</h3>
<div id="category-chart" class="bar-chart-container"></div>
</div>
</div>
<div id="page-orders" style="display: none;">
<div class="card">
<div class="card-header">
<h3>订单列表</h3>
<div style="display: flex; gap: 12px;">
<button class="btn" onclick="exportOrders()">导出订单</button>
<button class="btn" style="background: #22c55e;" onclick="exportOrdersCSV()">导出CSV</button>
</div>
</div>
<table class="table">
<thead>
<tr>
<th>订单号</th>
<th>品类</th>
<th>重量/数量</th>
<th>金额</th>
<th>来源</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="orders-table-body"></tbody>
</table>
<div id="orders-empty" class="empty-state" style="display: none;">
<div class="icon">📦</div>
<p>暂无订单数据</p>
</div>
</div>
</div>
<div id="page-prices" style="display: none;">
<div class="card">
<div class="card-header">
<h3>价格管理</h3>
<button class="btn" onclick="openPriceModal()">添加价格</button>
</div>
<table class="table">
<thead>
<tr>
<th>品类编码</th>
<th>品类名称</th>
<th>单位</th>
<th>单价(元)</th>
<th>操作</th>
</tr>
</thead>
<tbody id="prices-table-body"></tbody>
</table>
</div>
</div>
<div id="page-regions" style="display: none;">
<div class="card">
<div class="card-header">
<h3>区域管理</h3>
<div style="display: flex; gap: 10px;">
<button class="btn" onclick="openRegionModal()">添加区域</button>
<button class="btn btn-secondary" onclick="document.getElementById('import-file').click()">批量导入</button>
<input type="file" id="import-file" accept=".xlsx,.xls" style="display: none;" onchange="importRegions(this)">
</div>
</div>
<table class="table">
<thead>
<tr>
<th>城市</th>
<th>区域</th>
<th>站点</th>
<th>站点用户</th>
<th>操作</th>
</tr>
</thead>
<tbody id="regions-table-body"></tbody>
</table>
<div id="regions-empty" class="empty-state" style="display: none;">
<div class="icon">📍</div>
<p>暂无区域数据</p>
</div>
</div>
</div>
<div id="page-users" style="display: none;">
<div class="card">
<div class="card-header">
<h3>用户管理</h3>
<button class="btn" onclick="openUserModal()">添加用户</button>
</div>
<table class="table">
<thead>
<tr>
<th>用户ID</th>
<th>姓名</th>
<th>手机号</th>
<th>角色</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="users-table-body"></tbody>
</table>
<div id="users-empty" class="empty-state" style="display: none;">
<p>暂无用户数据</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal-overlay" id="price-modal">
<div class="modal">
<h4 id="price-modal-title">添加价格</h4>
<form id="price-form" enctype="multipart/form-data">
<input type="hidden" id="price-edit-id">
<input type="hidden" id="price-current-icon">
<div class="form-group">
<label>品类编码</label>
<input type="text" id="price-category" name="category" placeholder="例如: 001">
</div>
<div class="form-group">
<label>品类名称</label>
<input type="text" id="price-name" name="name" placeholder="例如: 纸类">
</div>
<div class="form-group">
<label>单位</label>
<select id="price-unit" name="unit">
<option value="kg">kg</option>
<option value="piece"></option>
</select>
</div>
<div class="form-group">
<label>单价</label>
<input type="number" id="price-value" name="price" placeholder="例如: 1.2">
</div>
<div class="form-group">
<label>图标上传</label>
<div style="display: flex; align-items: center; gap: 16px;">
<input type="file" id="price-icon" name="icon" accept="image/png, image/jpeg, image/jpg, image/gif" style="display: none;" onchange="previewIcon()">
<button type="button" class="btn" style="padding: 8px 16px; font-size: 14px;" onclick="document.getElementById('price-icon').click()">选择图片</button>
<div id="price-icon-preview" style="width: 60px; height: 60px; border: 1px dashed #ddd; display: flex; align-items: center; justify-content: center;">
<span style="color: #999; font-size: 24px;">📷</span>
</div>
</div>
<p style="font-size: 12px; color: #999; margin-top: 8px;">支持 PNG、JPG、GIF 格式,最大 5MB</p>
</div>
<div class="modal-footer">
<button type="button" class="cancel-btn" onclick="closePriceModal()">取消</button>
<button type="button" class="confirm-btn" onclick="savePrice()">保存</button>
</div>
</form>
</div>
</div>
<div class="modal-overlay" id="user-modal">
<div class="modal">
<h4 id="user-modal-title">添加用户</h4>
<input type="hidden" id="user-edit-id">
<div class="form-group">
<label>姓名</label>
<input type="text" id="user-name" placeholder="请输入姓名">
</div>
<div class="form-group">
<label>手机号</label>
<input type="text" id="user-phone" placeholder="请输入手机号">
</div>
<div class="form-group">
<label>角色</label>
<select id="user-role">
<option value="admin">管理员</option>
<option value="collector">回收员</option>
</select>
</div>
<div class="modal-footer">
<button class="cancel-btn" onclick="closeUserModal()">取消</button>
<button class="confirm-btn" onclick="saveUser()">保存</button>
</div>
</div>
</div>
<div class="modal-overlay" id="order-modal">
<div class="modal">
<h4>订单详情</h4>
<div id="order-detail-content"></div>
<div class="modal-footer">
<button class="confirm-btn" onclick="closeOrderModal()">关闭</button>
</div>
</div>
</div>
<div class="modal-overlay" id="region-modal">
<div class="modal">
<h4 id="region-modal-title">添加区域</h4>
<input type="hidden" id="region-edit-id">
<div class="form-group">
<label>城市</label>
<select id="region-city" onchange="loadRegionDistricts()">
<option value="">选择城市</option>
<option value="new">+ 新增城市</option>
</select>
</div>
<div class="form-group" id="region-city-new-group" style="display: none;">
<label>新城市名称</label>
<input type="text" id="region-city-new" placeholder="例如: 天津市">
</div>
<div class="form-group">
<label>区域</label>
<select id="region-district">
<option value="">选择区域</option>
<option value="new">+ 新增区域</option>
</select>
</div>
<div class="form-group" id="region-district-new-group" style="display: none;">
<label>新区域名称</label>
<input type="text" id="region-district-new" placeholder="例如: 高新区">
</div>
<div class="form-group">
<label>站点名称</label>
<input type="text" id="region-station-name" placeholder="例如: 金融街融汇景苑">
</div>
<div class="form-group">
<label>站点用户(可选,多个用逗号分隔)</label>
<input type="text" id="region-site-users" placeholder="例如: 商户A, 商户B">
</div>
<div class="modal-footer">
<button class="cancel-btn" onclick="closeRegionModal()">取消</button>
<button class="confirm-btn" onclick="saveRegion()">保存</button>
</div>
</div>
</div>
</div>
<script>
const API_BASE = '/api';
let currentPage = 'statistics';
async function apiRequest(url, method = 'GET', data = null) {
const options = { method };
if (data) {
options.headers = { 'Content-Type': 'application/json' };
options.body = JSON.stringify(data);
}
const response = await fetch(API_BASE + url, options);
return await response.json();
}
async function handleLogin() {
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const errorMsg = document.getElementById('login-error');
if (!username || !password) {
errorMsg.textContent = '请输入用户名和密码';
errorMsg.style.display = 'block';
return;
}
try {
const result = await apiRequest('/admin/login', 'POST', { username, password });
if (result.success) {
localStorage.setItem('admin_login', 'true');
document.getElementById('login-page').style.display = 'none';
document.getElementById('admin-layout').style.display = 'flex';
loadPageData();
} else {
errorMsg.style.display = 'block';
}
} catch (error) {
errorMsg.textContent = '登录失败,请检查后端服务是否运行';
errorMsg.style.display = 'block';
}
}
function handleLogout() {
localStorage.removeItem('admin_login');
document.getElementById('login-page').style.display = 'flex';
document.getElementById('admin-layout').style.display = 'none';
}
function showPage(pageName) {
currentPage = pageName;
document.querySelectorAll('.menu li a').forEach(a => a.classList.remove('active'));
document.getElementById('menu-' + pageName).classList.add('active');
const titles = {
statistics: '统计报表',
orders: '订单管理',
prices: '价格管理',
regions: '区域管理',
users: '用户管理'
};
document.getElementById('page-title').textContent = titles[pageName];
document.querySelectorAll('[id^="page-"]').forEach(el => el.style.display = 'none');
document.getElementById('page-' + pageName).style.display = 'block';
loadPageData();
}
async function loadPageData() {
switch (currentPage) {
case 'statistics':
await loadStatistics();
break;
case 'orders':
await loadOrders();
break;
case 'prices':
await loadPrices();
break;
case 'regions':
await loadRegionsData();
break;
case 'users':
await loadUsers();
break;
}
}
async function loadStatistics() {
try {
const result = await apiRequest('/statistics');
const stats = result.data;
document.getElementById('stat-total-orders').textContent = stats.totalOrders || 0;
document.getElementById('stat-total-amount').textContent = (stats.totalAmount || 0).toFixed(2);
document.getElementById('stat-total-weight').textContent = (stats.totalWeight || 0).toFixed(2);
document.getElementById('stat-avg-amount').textContent = (stats.avgAmount || 0).toFixed(2);
renderCategoryChart(stats.categoryStats || []);
renderCategoryWeightTable(stats.categoryStats || []);
} catch (error) {
console.error('加载统计数据失败:', error);
}
}
function renderCategoryWeightTable(data) {
const tbody = document.getElementById('category-weight-table');
if (!data || data.length === 0) {
tbody.innerHTML = '';
document.getElementById('category-weight-empty').style.display = 'block';
return;
}
document.getElementById('category-weight-empty').style.display = 'none';
tbody.innerHTML = data.map(item => `
<tr>
<td>${item.name}</td>
<td>${item.orders || 0}</td>
<td>${item.weight?.toFixed(2) || '0.00'} kg</td>
<td>${(item.weightPercent || 0).toFixed(2)}%</td>
<td>¥${item.amount?.toFixed(2) || '0.00'}</td>
<td>${(item.amountPercent || 0).toFixed(2)}%</td>
</tr>
`).join('');
}
function renderCategoryChart(data) {
const container = document.getElementById('category-chart');
if (!data || data.length === 0) {
container.innerHTML = '<div class="empty-state"><div class="icon">📊</div><p>暂无品类数据</p></div>';
return;
}
const sortedData = [...data].sort((a, b) => (b.amount || 0) - (a.amount || 0));
const maxAmount = Math.max(...data.map(item => item.amount), 1);
container.innerHTML = sortedData.map(item => {
const percent = ((item.amount || 0) / maxAmount * 100).toFixed(1);
return `
<div class="bar-chart-row">
<div class="bar-chart-label">${item.name}</div>
<div class="bar-chart-bar-wrap">
<div class="bar-chart-bar" style="width: ${percent}%">
<span class="bar-chart-value">¥${(item.amount || 0).toFixed(2)}</span>
</div>
</div>
<div class="bar-chart-percent">${percent}%</div>
</div>
`;
}).join('');
}
async function loadOrders() {
try {
const result = await apiRequest('/orders');
const orders = result.data || [];
const tbody = document.getElementById('orders-table-body');
if (orders.length === 0) {
tbody.innerHTML = '';
document.getElementById('orders-empty').style.display = 'block';
return;
}
document.getElementById('orders-empty').style.display = 'none';
tbody.innerHTML = orders.map(order => {
const regionParts = [order.city, order.district, order.community, order.user].filter(Boolean);
const regionText = regionParts.length > 0 ? regionParts.join(' > ') : '-';
return `
<tr>
<td>${order.id}</td>
<td>${order.items?.map(item => item.name).join(', ') || '-'}</td>
<td>${order.items?.map(item => item.weight ? item.weight + 'kg' : item.count + '件').join(', ') || '-'}</td>
<td>¥${order.totalAmount?.toFixed(2) || '0.00'}</td>
<td>${regionText}</td>
<td>${formatDate(order.createTime)}</td>
<td>
<div class="action-btns">
<button class="edit-btn" onclick="viewOrder('${order.id}')">查看</button>
<button class="delete-btn" onclick="deleteOrder('${order.id}')">删除</button>
</div>
</td>
</tr>
`}).join('');
} catch (error) {
console.error('加载订单失败:', error);
}
}
function formatDate(dateStr) {
if (!dateStr) return '-';
const date = new Date(dateStr);
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
}
async function viewOrder(orderId) {
try {
const result = await apiRequest('/orders');
const orders = result.data || [];
const order = orders.find(o => o.id === orderId);
if (order) {
const regionParts = [order.city, order.district, order.community, order.user].filter(Boolean);
const regionText = regionParts.length > 0 ? regionParts.join(' > ') : '-';
const content = `
<div style="margin-bottom: 16px;">
<p><strong>订单号:</strong> ${order.id}</p>
<p><strong>总金额:</strong> ¥${order.totalAmount?.toFixed(2) || '0.00'}</p>
<p><strong>状态:</strong> ${order.status === 'pending' ? '待处理' : order.status === 'completed' ? '已完成' : '已取消'}</p>
<p><strong>来源:</strong> ${regionText}</p>
<p><strong>创建时间:</strong> ${formatDate(order.createTime)}</p>
</div>
<div style="border-top: 1px solid #f0f0f0; padding-top: 16px;">
<p><strong>回收物品:</strong></p>
<ul style="margin-left: 20px;">
${order.items?.map(item => `<li>${item.name}: ${item.weight ? item.weight + 'kg' : item.count + '件'} - ¥${item.amount?.toFixed(2)}</li>`).join('') || '<li>无</li>'}
</ul>
</div>
`;
document.getElementById('order-detail-content').innerHTML = content;
document.getElementById('order-modal').classList.add('show');
}
} catch (error) {
console.error('查看订单失败:', error);
}
}
function closeOrderModal() {
document.getElementById('order-modal').classList.remove('show');
}
async function updateOrderStatus(orderId, status) {
if (!confirm(`确定将订单标记为${status === 'completed' ? '已完成' : '已取消'}吗?`)) return;
try {
await apiRequest(`/orders/${orderId}`, 'PUT', { status });
await loadOrders();
} catch (error) {
alert('更新订单状态失败');
}
}
async function deleteOrder(orderId) {
if (!confirm('确定删除该订单吗?')) return;
try {
await apiRequest(`/orders/${orderId}`, 'DELETE');
await loadOrders();
} catch (error) {
alert('删除订单失败');
}
}
async function loadPrices() {
try {
const result = await apiRequest('/prices');
const prices = result.prices || [];
const tbody = document.getElementById('prices-table-body');
tbody.innerHTML = prices.map(price => {
return `
<tr>
<td>${price.category}</td>
<td>${price.name}</td>
<td>${price.unit === 'kg' ? '公斤' : '件'}</td>
<td>¥${price.price.toFixed(2)}</td>
<td>
<div class="action-btns">
<button class="edit-btn" onclick="editPrice('${price.category}')">编辑</button>
<button class="delete-btn" onclick="deletePrice('${price.category}')">删除</button>
</div>
</td>
</tr>
`;
}).join('');
} catch (error) {
console.error('加载价格失败:', error);
}
}
function openPriceModal() {
document.getElementById('price-modal-title').textContent = '添加价格';
document.getElementById('price-edit-id').value = '';
document.getElementById('price-category').value = '';
document.getElementById('price-name').value = '';
document.getElementById('price-unit').value = 'kg';
document.getElementById('price-value').value = '';
document.getElementById('price-current-icon').value = '';
document.getElementById('price-icon').value = '';
document.getElementById('price-icon-preview').innerHTML = '<span style="color: #999; font-size: 24px;">📷</span>';
document.getElementById('price-modal').classList.add('show');
}
function closePriceModal() {
document.getElementById('price-modal').classList.remove('show');
}
async function editPrice(category) {
try {
const result = await apiRequest('/prices');
const prices = result.prices || [];
const price = prices.find(p => p.category === category);
if (price) {
document.getElementById('price-modal-title').textContent = '编辑价格';
document.getElementById('price-edit-id').value = price.category;
document.getElementById('price-category').value = price.category;
document.getElementById('price-name').value = price.name;
document.getElementById('price-unit').value = price.unit;
document.getElementById('price-value').value = price.price;
document.getElementById('price-current-icon').value = price.icon || '';
document.getElementById('price-icon').value = '';
if (price.icon) {
const iconSrc = price.icon;
document.getElementById('price-icon-preview').innerHTML = `<img src="${iconSrc}" style="width: 60px; height: 60px; border-radius: 4px; object-fit: cover;" />`;
} else {
document.getElementById('price-icon-preview').innerHTML = '<span style="color: #999; font-size: 24px;">📷</span>';
}
document.getElementById('price-modal').classList.add('show');
}
} catch (error) {
console.error('获取价格信息失败:', error);
}
}
function previewIcon() {
const fileInput = document.getElementById('price-icon');
const preview = document.getElementById('price-icon-preview');
if (fileInput.files && fileInput.files[0]) {
const reader = new FileReader();
reader.onload = function(e) {
preview.innerHTML = `<img src="${e.target.result}" style="width: 60px; height: 60px; border-radius: 4px; object-fit: cover;" />`;
};
reader.readAsDataURL(fileInput.files[0]);
}
}
async function savePrice() {
const category = document.getElementById('price-category').value;
const name = document.getElementById('price-name').value;
const unit = document.getElementById('price-unit').value;
const price = parseFloat(document.getElementById('price-value').value);
const editId = document.getElementById('price-edit-id').value;
const currentIcon = document.getElementById('price-current-icon').value;
const iconFile = document.getElementById('price-icon').files[0];
if (!category || !name || isNaN(price)) {
alert('请填写完整信息');
return;
}
try {
const formData = new FormData();
formData.append('category', category);
formData.append('name', name);
formData.append('unit', unit);
formData.append('price', price);
if (!iconFile && currentIcon) {
formData.append('icon', currentIcon);
}
if (iconFile) {
formData.append('icon', iconFile);
}
let response;
if (editId) {
response = await fetch(API_BASE + `/prices/${editId}`, {
method: 'PUT',
body: formData
});
} else {
response = await fetch(API_BASE + '/prices', {
method: 'POST',
body: formData
});
}
const result = await response.json();
if (result.success) {
closePriceModal();
await loadPrices();
} else {
alert(result.message || '保存失败');
}
} catch (error) {
alert('保存失败');
}
}
async function deletePrice(category) {
if (!confirm('确定删除该价格吗?')) return;
try {
await apiRequest(`/prices/${category}`, 'DELETE');
await loadPrices();
} catch (error) {
alert('删除失败');
}
}
async function loadUsers() {
try {
const result = await apiRequest('/users');
const users = result.data || [];
const tbody = document.getElementById('users-table-body');
if (users.length === 0) {
tbody.innerHTML = '';
document.getElementById('users-empty').style.display = 'block';
return;
}
document.getElementById('users-empty').style.display = 'none';
tbody.innerHTML = users.map(user => `
<tr>
<td>${user.id}</td>
<td>${user.name}</td>
<td>${user.phone}</td>
<td>${user.role === 'admin' ? '管理员' : '回收员'}</td>
<td>${formatDate(user.createTime)}</td>
<td>
<div class="action-btns">
<button class="edit-btn" onclick="editUser('${user.id}')">编辑</button>
<button class="delete-btn" onclick="deleteUser('${user.id}')">删除</button>
</div>
</td>
</tr>
`).join('');
} catch (error) {
console.error('加载用户失败:', error);
}
}
function openUserModal() {
document.getElementById('user-modal-title').textContent = '添加用户';
document.getElementById('user-edit-id').value = '';
document.getElementById('user-name').value = '';
document.getElementById('user-phone').value = '';
document.getElementById('user-role').value = 'collector';
document.getElementById('user-modal').classList.add('show');
}
function closeUserModal() {
document.getElementById('user-modal').classList.remove('show');
}
async function editUser(userId) {
try {
const result = await apiRequest('/users');
const users = result.data || [];
const user = users.find(u => u.id === userId);
if (user) {
document.getElementById('user-modal-title').textContent = '编辑用户';
document.getElementById('user-edit-id').value = user.id;
document.getElementById('user-name').value = user.name;
document.getElementById('user-phone').value = user.phone;
document.getElementById('user-role').value = user.role;
document.getElementById('user-modal').classList.add('show');
}
} catch (error) {
console.error('获取用户信息失败:', error);
}
}
async function saveUser() {
const name = document.getElementById('user-name').value;
const phone = document.getElementById('user-phone').value;
const role = document.getElementById('user-role').value;
const editId = document.getElementById('user-edit-id').value;
if (!name || !phone) {
alert('请填写完整信息');
return;
}
try {
if (editId) {
await apiRequest(`/users/${editId}`, 'PUT', { name, phone, role });
} else {
await apiRequest('/users', 'POST', { name, phone, role });
}
closeUserModal();
await loadUsers();
} catch (error) {
alert('保存失败');
}
}
async function deleteUser(userId) {
if (!confirm('确定删除该用户吗?')) return;
try {
await apiRequest(`/users/${userId}`, 'DELETE');
await loadUsers();
} catch (error) {
alert('删除失败');
}
}
async function exportOrders() {
try {
const result = await apiRequest('/orders/export');
const data = result.data || [];
if (data.length === 0) {
alert('暂无订单数据可导出');
return;
}
const headers = Object.keys(data[0]);
const csvContent = [headers.join(','), ...data.map(row => headers.map(h => `"${row[h] || ''}"`).join(','))].join('\n');
const blob = new Blob(['\uFEFF' + csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = 'orders_export.csv';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} catch (error) {
console.error('导出订单失败:', error);
alert('导出失败');
}
}
async function exportOrdersCSV() {
try {
const response = await fetch('/api/orders/export/csv');
const blob = await response.blob();
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = 'orders.csv';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} catch (error) {
console.error('导出CSV失败:', error);
alert('导出失败');
}
}
document.addEventListener('DOMContentLoaded', () => {
const isLoggedIn = localStorage.getItem('admin_login');
if (isLoggedIn) {
document.getElementById('login-page').style.display = 'none';
document.getElementById('admin-layout').style.display = 'flex';
loadPageData();
} else {
document.getElementById('login-page').style.display = 'flex';
document.getElementById('admin-layout').style.display = 'none';
}
});
let allCities = [];
let allDistricts = [];
let allStations = [];
let allSiteUsers = [];
async function loadRegionsData() {
await loadRegionsFlat();
}
async function loadRegionsFlat() {
try {
const [citiesResult, districtsResult, stationsResult, usersResult] = await Promise.all([
apiRequest('/regions/cities'),
apiRequest('/regions/districts'),
apiRequest('/regions/stations'),
apiRequest('/regions/site-users')
]);
allCities = citiesResult.data || [];
allDistricts = districtsResult.data || [];
allStations = stationsResult.data || [];
allSiteUsers = usersResult.data || [];
const tbody = document.getElementById('regions-table-body');
const emptyState = document.getElementById('regions-empty');
if (allStations.length === 0) {
tbody.innerHTML = '';
emptyState.style.display = 'block';
return;
}
emptyState.style.display = 'none';
tbody.innerHTML = allStations.map(station => {
const city = allCities.find(c => c.id === station.cityId);
const district = allDistricts.find(d => d.id === station.districtId);
const stationUsers = allSiteUsers.filter(u => u.stationId === station.id);
const userNames = stationUsers.map(u => u.name).join(', ') || '-';
return `
<tr>
<td>${city ? city.name : '-'}</td>
<td>${district ? district.name : '-'}</td>
<td>${station.name}</td>
<td>${userNames}</td>
<td>
<div class="action-btns">
<button class="edit-btn" onclick="editRegion('${station.id}')">编辑</button>
<button class="delete-btn" onclick="deleteRegion('${station.id}')">删除</button>
</div>
</td>
</tr>
`;
}).join('');
populateRegionCitySelect();
} catch (error) {
console.error('加载区域数据失败:', error);
}
}
function populateRegionCitySelect() {
const select = document.getElementById('region-city');
const options = allCities.map(city => `<option value="${city.id}">${city.name}</option>`).join('');
select.innerHTML = `<option value="">选择城市</option><option value="new">+ 新增城市</option>${options}`;
}
async function importRegions(input) {
const file = input.files[0];
if (!file) return;
const formData = new FormData();
formData.append('file', file);
try {
const response = await fetch('/api/regions/import', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
alert(`导入成功!\n新增城市: ${result.data.addedCities}\n新增区域: ${result.data.addedDistricts}\n新增站点: ${result.data.addedStations}\n\n已存在跳过: 城市${result.data.skippedCities}, 区域${result.data.skippedDistricts}, 站点${result.data.skippedStations}`);
await loadRegionsFlat();
} else {
alert('导入失败: ' + result.message);
}
} catch (error) {
alert('导入失败: ' + error.message);
} finally {
input.value = '';
}
}
function openRegionModal() {
document.getElementById('region-modal-title').textContent = '添加区域';
document.getElementById('region-edit-id').value = '';
document.getElementById('region-city').value = '';
document.getElementById('region-city-new').value = '';
document.getElementById('region-city-new-group').style.display = 'none';
document.getElementById('region-district').innerHTML = '<option value="">请先选择城市</option>';
document.getElementById('region-district-new').value = '';
document.getElementById('region-district-new-group').style.display = 'none';
document.getElementById('region-station-name').value = '';
document.getElementById('region-site-users').value = '';
document.getElementById('region-modal').classList.add('show');
populateRegionCitySelect();
const citySelect = document.getElementById('region-city');
citySelect.removeEventListener('change', loadRegionDistricts);
citySelect.addEventListener('change', loadRegionDistricts);
}
function closeRegionModal() {
document.getElementById('region-modal').classList.remove('show');
}
async function loadRegionDistricts() {
const citySelect = document.getElementById('region-city');
const districtSelect = document.getElementById('region-district');
const cityNewGroup = document.getElementById('region-city-new-group');
if (citySelect.value === 'new') {
cityNewGroup.style.display = 'block';
districtSelect.innerHTML = '<option value="">请先选择城市</option>';
} else if (citySelect.value) {
cityNewGroup.style.display = 'none';
const cityDistricts = allDistricts.filter(d => d.cityId === citySelect.value);
const options = cityDistricts.map(d => `<option value="${d.id}">${d.name}</option>`).join('');
districtSelect.innerHTML = `<option value="">选择区域</option><option value="new">+ 新增区域</option>${options}`;
} else {
cityNewGroup.style.display = 'none';
districtSelect.innerHTML = '<option value="">请先选择城市</option>';
}
const districtNewGroup = document.getElementById('region-district-new-group');
districtNewGroup.style.display = districtSelect.value === 'new' ? 'block' : 'none';
}
document.getElementById('region-district').addEventListener('change', function() {
const districtNewGroup = document.getElementById('region-district-new-group');
districtNewGroup.style.display = this.value === 'new' ? 'block' : 'none';
});
document.getElementById('region-city-new').addEventListener('blur', async function() {
const cityName = this.value.trim();
if (!cityName) return;
const existingCity = allCities.find(c => c.name === cityName);
if (existingCity) {
document.getElementById('region-city').value = existingCity.id;
document.getElementById('region-city-new-group').style.display = 'none';
await loadRegionDistricts();
return;
}
try {
const result = await apiRequest('/regions/cities', 'POST', { name: cityName });
const newCity = result.data;
allCities.push(newCity);
document.getElementById('region-city').value = newCity.id;
document.getElementById('region-city-new-group').style.display = 'none';
populateRegionCitySelect();
document.getElementById('region-city').value = newCity.id;
await loadRegionDistricts();
} catch (error) {
console.error('自动创建城市失败:', error);
}
});
async function editRegion(stationId) {
try {
const station = allStations.find(s => s.id === stationId);
if (!station) return;
const city = allCities.find(c => c.id === station.cityId);
const district = allDistricts.find(d => d.id === station.districtId);
const stationUsers = allSiteUsers.filter(u => u.stationId === stationId);
const userNames = stationUsers.map(u => u.name).join(', ');
document.getElementById('region-modal-title').textContent = '编辑区域';
document.getElementById('region-edit-id').value = stationId;
if (city) {
document.getElementById('region-city').value = city.id;
document.getElementById('region-city-new-group').style.display = 'none';
}
await new Promise(resolve => setTimeout(resolve, 100));
if (district) {
loadRegionDistricts();
document.getElementById('region-district').value = district.id;
document.getElementById('region-district-new-group').style.display = 'none';
}
document.getElementById('region-station-name').value = station.name;
document.getElementById('region-site-users').value = userNames;
document.getElementById('region-modal').classList.add('show');
} catch (error) {
console.error('获取区域信息失败:', error);
}
}
async function saveRegion() {
const citySelect = document.getElementById('region-city');
const cityNew = document.getElementById('region-city-new').value;
const districtSelect = document.getElementById('region-district');
const districtNew = document.getElementById('region-district-new').value;
const stationName = document.getElementById('region-station-name').value;
const siteUsersInput = document.getElementById('region-site-users').value;
const editId = document.getElementById('region-edit-id').value;
let cityId = citySelect.value;
let districtId = districtSelect.value;
if (!cityId || cityId === 'new') {
if (!cityNew) {
alert('请选择城市或输入新城市名称');
return;
}
try {
const result = await apiRequest('/regions/cities', 'POST', { name: cityNew });
cityId = result.data.id;
} catch (error) {
alert('创建城市失败');
return;
}
}
if (!districtId || districtId === 'new') {
if (!districtNew) {
alert('请选择区域或输入新区域名称');
return;
}
try {
const result = await apiRequest('/regions/districts', 'POST', { cityId, name: districtNew });
districtId = result.data.id;
} catch (error) {
alert('创建区域失败');
return;
}
}
if (!stationName) {
alert('请填写站点名称');
return;
}
try {
if (editId) {
await apiRequest(`/regions/stations/${editId}`, 'PUT', { cityId, districtId, name: stationName });
} else {
const result = await apiRequest('/regions/stations', 'POST', { cityId, districtId, name: stationName });
const newStationId = result.data.id;
if (siteUsersInput.trim()) {
const userNames = siteUsersInput.split(/[,]/).map(u => u.trim()).filter(u => u);
for (const userName of userNames) {
await apiRequest('/regions/site-users', 'POST', { stationId: newStationId, name: userName, type: 'merchant' });
}
}
}
closeRegionModal();
await loadRegionsFlat();
} catch (error) {
alert('保存失败');
}
}
async function deleteRegion(stationId) {
if (!confirm('确定删除该站点吗?删除后关联的用户也将被删除')) return;
try {
await apiRequest(`/regions/stations/${stationId}`, 'DELETE');
await loadRegionsFlat();
} catch (error) {
alert('删除失败');
}
}
</script>
</body>
</html>