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

1482 lines
62 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; }
.filter-section { display: flex; gap: 12px; margin-bottom: 20px; flex-wrap: wrap; align-items: center; }
.filter-section input[type="text"] { padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; min-width: 200px; }
.filter-section select { padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; }
.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; justify-content: center; }
.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/category.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>
<div class="filter-section">
<input type="text" id="order-search-input" placeholder="搜索订单号、品类、来源..." onkeyup="handleOrderSearch(event)">
<select id="order-category-filter" onchange="loadOrders()">
<option value="">全部分类</option>
</select>
<input type="date" id="order-date-start" onchange="loadOrders()" placeholder="开始日期">
<span style="color: #999;"></span>
<input type="date" id="order-date-end" onchange="loadOrders()" placeholder="结束日期">
<button class="btn btn-secondary" onclick="resetOrderFilter()">重置</button>
</div>
<table class="table">
<thead>
<tr>
<th><input type="checkbox" id="order-select-all" onclick="toggleSelectAll()"></th>
<th>订单号</th>
<th>品类</th>
<th>重量(kg</th>
<th>金额(元)</th>
<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>
</tr>
</thead>
<tbody id="prices-table-body"></tbody>
</table>
<div id="prices-empty" class="empty-state" style="display: none;">
<div class="icon">📦</div>
<p>暂无品类数据</p>
</div>
</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="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>
<select id="region-station-select" onchange="toggleStationNameInput()">
<option value="">选择站点</option>
<option value="new">+ 新增站点</option>
</select>
</div>
<div class="form-group" id="region-station-name-group" style="display: none;">
<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 class="modal-overlay" id="price-modal">
<div class="modal">
<h4 id="price-modal-title">新增品类</h4>
<input type="hidden" id="price-edit-id">
<div class="form-group">
<label>分类编码</label>
<input type="text" id="price-category" placeholder="例如: 001">
</div>
<div class="form-group">
<label>品类名称</label>
<input type="text" id="price-name" placeholder="例如: 纸类(书本)">
</div>
<div class="form-group">
<label>图标</label>
<select id="price-icon">
<option value="paper">paper - 纸类</option>
<option value="metal">metal - 金属</option>
<option value="plastic">plastic - 塑料</option>
<option value="fabric">fabric - 织物</option>
<option value="glass">glass - 玻璃</option>
</select>
</div>
<div class="form-group">
<label>单位</label>
<input type="text" id="price-unit" value="kg">
</div>
<div class="form-group">
<label>单价(元)</label>
<input type="number" id="price-price" step="0.01" placeholder="0.00">
</div>
<div class="modal-footer">
<button class="cancel-btn" onclick="closePriceModal()">取消</button>
<button class="confirm-btn" onclick="savePrice()">保存</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 phone = document.getElementById('username').value;
const password = document.getElementById('password').value;
const errorMsg = document.getElementById('login-error');
if (!phone || !password) {
errorMsg.textContent = '请输入手机号和密码';
errorMsg.style.display = 'block';
return;
}
try {
const result = await apiRequest('/admin/login', 'POST', { phone, 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 loadCategoriesForFilter();
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('');
}
let selectedOrderIds = new Set();
function toggleOrderSelect(orderId) {
if (selectedOrderIds.has(orderId)) {
selectedOrderIds.delete(orderId);
} else {
selectedOrderIds.add(orderId);
}
updateSelectAll();
}
function toggleSelectAll() {
const checkboxes = document.querySelectorAll('input[name="order-checkbox"]');
const selectAllCheckbox = document.getElementById('order-select-all');
if (selectAllCheckbox.checked) {
checkboxes.forEach(cb => {
cb.checked = true;
selectedOrderIds.add(cb.value);
});
} else {
checkboxes.forEach(cb => {
cb.checked = false;
selectedOrderIds.delete(cb.value);
});
}
}
function updateSelectAll() {
const checkboxes = document.querySelectorAll('input[name="order-checkbox"]');
const selectAllCheckbox = document.getElementById('order-select-all');
if (checkboxes.length > 0) {
selectAllCheckbox.checked = Array.from(checkboxes).every(cb => cb.checked);
}
}
async function loadOrders() {
try {
const result = await apiRequest('/orders');
let orders = result.data || [];
const searchText = document.getElementById('order-search-input').value.toLowerCase();
const categoryFilter = document.getElementById('order-category-filter').value;
const dateStart = document.getElementById('order-date-start').value;
const dateEnd = document.getElementById('order-date-end').value;
orders = orders.filter(order => {
const idMatch = order.id.toLowerCase().includes(searchText);
const itemsText = (order.items?.map(item => item.name).join(', ') || '').toLowerCase();
const itemsMatch = itemsText.includes(searchText);
const regionParts = [order.city, order.district, order.community, order.user].filter(Boolean);
const regionText = regionParts.join(' > ').toLowerCase();
const regionMatch = regionText.includes(searchText);
const categoryMatch = !categoryFilter || order.items?.some(item => item.name === categoryFilter);
const orderDate = order.createTime ? (order.createTime.split(' ')[0] || order.createTime.split('T')[0]) : '';
const dateStartMatch = !dateStart || orderDate >= dateStart;
const dateEndMatch = !dateEnd || orderDate <= dateEnd;
return (idMatch || itemsMatch || regionMatch) && categoryMatch && dateStartMatch && dateEndMatch;
});
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';
selectedOrderIds.clear();
tbody.innerHTML = orders.map(order => {
return `
<tr>
<td><input type="checkbox" name="order-checkbox" value="${order.id}" onclick="toggleOrderSelect('${order.id}')"></td>
<td>${order.id}</td>
<td>${order.items?.map(item => item.name).join(', ') || '-'}</td>
<td>${order.items?.map(item => item.weight !== undefined ? item.weight : (item.count !== undefined ? item.count : '-')).join(', ') || '-'}</td>
<td>${order.totalAmount?.toFixed(2) || '0.00'}</td>
<td>${order.city || '-'}</td>
<td>${order.district || '-'}</td>
<td>${order.community || '-'}</td>
<td>${order.user || '-'}</td>
<td>${order.collector || '-'}</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('');
document.getElementById('order-select-all').checked = false;
} catch (error) {
console.error('加载订单失败:', error);
}
}
function handleOrderSearch(event) {
if (event.key === 'Enter') {
loadOrders();
}
}
function resetOrderFilter() {
document.getElementById('order-search-input').value = '';
document.getElementById('order-category-filter').value = '';
document.getElementById('order-date-start').value = '';
document.getElementById('order-date-end').value = '';
loadOrders();
}
async function loadCategoriesForFilter() {
try {
const result = await apiRequest('/prices');
const prices = result.prices || [];
const select = document.getElementById('order-category-filter');
select.innerHTML = '<option value="">全部分类</option>';
prices.forEach(price => {
const option = document.createElement('option');
option.value = price.name;
option.textContent = price.name;
select.appendChild(option);
});
} catch (error) {
console.error('加载分类失败:', error);
}
}
function formatDate(dateStr) {
if (!dateStr) return '-';
const date = new Date(dateStr);
if (isNaN(date.getTime())) {
const parts = dateStr.split(' ');
if (parts.length === 2) {
const dateParts = parts[0].split('-');
const timeParts = parts[1].split(':');
if (dateParts.length === 3 && timeParts.length >= 2) {
return `${dateParts[0]}-${dateParts[1].padStart(2, '0')}-${dateParts[2].padStart(2, '0')} ${timeParts[0].padStart(2, '0')}:${timeParts[1].padStart(2, '0')}`;
}
}
return dateStr;
}
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).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 loadPrices() {
try {
const result = await apiRequest('/prices');
const prices = result.prices || [];
const tbody = document.getElementById('prices-table-body');
if (!prices || prices.length === 0) {
tbody.innerHTML = '';
document.getElementById('prices-empty').style.display = 'block';
return;
}
document.getElementById('prices-empty').style.display = 'none';
tbody.innerHTML = prices.map(price => `
<tr>
<td>${price.category}</td>
<td>${price.name}</td>
<td>${price.unit}</td>
<td>
<div class="action-btns">
<button class="edit-btn" onclick="editPrice('${price.name}')">编辑</button>
<button class="delete-btn" onclick="deletePrice('${price.name}')">删除</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-icon').value = 'paper';
document.getElementById('price-unit').value = 'kg';
document.getElementById('price-price').value = '';
document.getElementById('price-modal').classList.add('show');
}
function closePriceModal() {
document.getElementById('price-modal').classList.remove('show');
}
async function editPrice(name) {
try {
const result = await apiRequest('/prices');
const prices = result.prices || [];
const price = prices.find(p => p.name === name);
if (price) {
document.getElementById('price-modal-title').textContent = '编辑品类';
document.getElementById('price-edit-id').value = price.name;
document.getElementById('price-category').value = price.category;
document.getElementById('price-name').value = price.name;
document.getElementById('price-icon').value = price.icon || 'paper';
document.getElementById('price-unit').value = price.unit || 'kg';
document.getElementById('price-price').value = price.price;
document.getElementById('price-modal').classList.add('show');
}
} catch (error) {
console.error('获取品类信息失败:', error);
}
}
async function savePrice() {
const category = document.getElementById('price-category').value.trim();
const name = document.getElementById('price-name').value.trim();
const icon = document.getElementById('price-icon').value;
const unit = document.getElementById('price-unit').value.trim();
const price = parseFloat(document.getElementById('price-price').value);
const editId = document.getElementById('price-edit-id').value;
if (!category || !name || isNaN(price)) {
alert('请填写完整信息');
return;
}
try {
if (editId) {
await apiRequest('/prices', 'PUT', { oldName: editId, category, name, icon, unit, price });
} else {
await apiRequest('/prices', 'POST', { category, name, icon, unit, price });
}
closePriceModal();
await loadPrices();
} catch (error) {
console.error('保存失败:', error);
alert('保存失败');
}
}
async function deletePrice(name) {
if (!confirm(`确定删除品类 "${name}" 吗?`)) return;
try {
await apiRequest('/prices', 'DELETE', { name });
await loadPrices();
} catch (error) {
console.error('删除失败:', error);
alert('删除失败');
}
}
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 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');
let orders = result.data || [];
if (selectedOrderIds.size > 0) {
orders = orders.filter(o => selectedOrderIds.has(o.id));
}
if (orders.length === 0) {
alert(selectedOrderIds.size > 0 ? '没有选中的订单可导出' : '暂无订单数据可导出');
return;
}
const exportData = orders.map(order => ({
'订单号': order.id,
'品类': order.items?.map(item => item.name).join(', ') || '-',
'重量(kg': order.items?.map(item => item.weight !== undefined ? item.weight : (item.count !== undefined ? item.count : '-')).join(', ') || '-',
'金额(元)': order.totalAmount || '-',
'城市': order.city || '-',
'区域': order.district || '-',
'站点': order.community || '-',
'站点用户': order.user || '-',
'回收人员': order.collector || '-',
'创建时间': order.createTime || '-'
}));
const headers = Object.keys(exportData[0]);
const csvContent = [headers.join(','), ...exportData.map(row => headers.map(h => row[h] || '').join(','))].join('\n');
const blob = new Blob(['\uFEFF' + csvContent], { type: 'text/plain;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 result = await apiRequest('/orders');
let orders = result.data || [];
if (selectedOrderIds.size > 0) {
orders = orders.filter(o => selectedOrderIds.has(o.id));
}
if (orders.length === 0) {
alert(selectedOrderIds.size > 0 ? '没有选中的订单可导出' : '暂无订单数据可导出');
return;
}
const exportData = orders.map(order => ({
'订单号': order.id,
'品类': order.items?.map(item => item.name).join(', ') || '-',
'重量(kg': order.items?.map(item => item.weight !== undefined ? item.weight : (item.count !== undefined ? item.count : '-')).join(', ') || '-',
'金额(元)': order.totalAmount || '-',
'城市': order.city || '-',
'区域': order.district || '-',
'站点': order.community || '-',
'站点用户': order.user || '-',
'回收人员': order.collector || '-',
'创建时间': order.createTime || '-'
}));
const headers = Object.keys(exportData[0]);
const csvContent = [headers.join(','), ...exportData.map(row => headers.map(h => row[h] || '').join(','))].join('\n');
const blob = new Blob(['\uFEFF' + csvContent], { type: 'text/plain;charset=utf-8;' });
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';
const rows = [];
allStations.forEach(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);
if (stationUsers.length === 0) {
rows.push(`
<tr>
<td>${city ? city.name : '-'}</td>
<td>${district ? district.name : '-'}</td>
<td>${station.name}</td>
<td>-</td>
<td>
<div class="action-btns">
<button class="edit-btn" onclick="editRegion('${station.id}')">编辑</button>
<button class="delete-btn" onclick="deleteStation('${station.id}')">删除</button>
</div>
</td>
</tr>
`);
} else {
stationUsers.forEach(user => {
rows.push(`
<tr>
<td>${city ? city.name : '-'}</td>
<td>${district ? district.name : '-'}</td>
<td>${station.name}</td>
<td>${user.name}</td>
<td>
<div class="action-btns">
<button class="edit-btn" onclick="editRegion('${station.id}')">编辑</button>
<button class="delete-btn" onclick="deleteSiteUser('${user.id}')">删除</button>
</div>
</td>
</tr>
`);
});
}
});
tbody.innerHTML = rows.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新增站点用户: ${result.data.addedSiteUsers}\n\n已存在跳过: 城市${result.data.skippedCities}, 区域${result.data.skippedDistricts}, 站点${result.data.skippedStations}, 站点用户${result.data.skippedSiteUsers}`);
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-select').innerHTML = '<option value="">请先选择区域</option><option value="new">+ 新增站点</option>';
document.getElementById('region-station-name').value = '';
document.getElementById('region-station-name-group').style.display = 'none';
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';
loadRegionStations();
});
function toggleStationNameInput() {
const stationSelect = document.getElementById('region-station-select');
const stationNameGroup = document.getElementById('region-station-name-group');
stationNameGroup.style.display = stationSelect.value === 'new' ? 'block' : 'none';
}
function loadRegionStations() {
const citySelect = document.getElementById('region-city');
const districtSelect = document.getElementById('region-district');
const stationSelect = document.getElementById('region-station-select');
if (!citySelect.value || !districtSelect.value || districtSelect.value === 'new') {
stationSelect.innerHTML = '<option value="">请先选择区域</option><option value="new">+ 新增站点</option>';
document.getElementById('region-station-name-group').style.display = 'none';
return;
}
const districtStations = allStations.filter(s => s.cityId === citySelect.value && s.districtId === districtSelect.value);
const options = districtStations.map(s => `<option value="${s.id}">${s.name}</option>`).join('');
stationSelect.innerHTML = `<option value="">选择站点</option><option value="new">+ 新增站点</option>${options}`;
document.getElementById('region-station-name-group').style.display = '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';
}
await new Promise(resolve => setTimeout(resolve, 100));
document.getElementById('region-station-select').value = station.id;
document.getElementById('region-station-name').value = station.name;
document.getElementById('region-station-name-group').style.display = 'none';
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 stationSelect = document.getElementById('region-station-select');
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;
let stationId = stationSelect.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 (!stationId || stationId === 'new') {
if (!stationName) {
alert('请填写站点名称');
return;
}
try {
const result = await apiRequest('/regions/stations', 'POST', { cityId, districtId, name: stationName });
stationId = result.data.id;
} catch (error) {
alert('创建站点失败');
return;
}
}
try {
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, name: userName, type: 'merchant' });
}
}
closeRegionModal();
await loadRegionsFlat();
} catch (error) {
alert('保存失败');
}
}
async function deleteStation(stationId) {
if (!confirm('确定删除该站点吗?删除后关联的用户也将被删除')) return;
try {
await apiRequest(`/regions/stations/${stationId}`, 'DELETE');
await loadRegionsFlat();
} catch (error) {
alert('删除失败');
}
}
async function deleteSiteUser(userId) {
if (!confirm('确定删除该站点用户吗?')) return;
try {
await apiRequest(`/regions/site-users/${userId}`, 'DELETE');
await loadRegionsFlat();
} catch (error) {
alert('删除失败');
}
}
</script>
</body>
</html>