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