1482 lines
62 KiB
HTML
1482 lines
62 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>易分宝现场回收 - 管理后台</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> |