1339 lines
51 KiB
HTML
1339 lines
51 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', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
||
background: #f5f7fa;
|
||
color: #333;
|
||
}
|
||
|
||
.container {
|
||
max-width: 1400px;
|
||
margin: 0 auto;
|
||
padding: 20px;
|
||
}
|
||
|
||
h1 {
|
||
text-align: center;
|
||
margin-bottom: 30px;
|
||
color: #2c3e50;
|
||
}
|
||
|
||
.tabs {
|
||
display: flex;
|
||
border-bottom: 2px solid #e1e8ed;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.tab {
|
||
padding: 12px 24px;
|
||
cursor: pointer;
|
||
border: none;
|
||
background: none;
|
||
font-size: 16px;
|
||
color: #666;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.tab:hover {
|
||
color: #3498db;
|
||
}
|
||
|
||
.tab.active {
|
||
color: #3498db;
|
||
border-bottom: 2px solid #3498db;
|
||
margin-bottom: -2px;
|
||
}
|
||
|
||
.tab-content {
|
||
display: none;
|
||
}
|
||
|
||
.tab-content.active {
|
||
display: block;
|
||
}
|
||
|
||
.card {
|
||
background: white;
|
||
border-radius: 8px;
|
||
padding: 20px;
|
||
margin-bottom: 20px;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.form-group label {
|
||
display: block;
|
||
margin-bottom: 5px;
|
||
font-weight: 500;
|
||
color: #555;
|
||
}
|
||
|
||
.form-control {
|
||
width: 100%;
|
||
padding: 8px 12px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.form-control:focus {
|
||
outline: none;
|
||
border-color: #3498db;
|
||
}
|
||
|
||
select {
|
||
background: white;
|
||
}
|
||
|
||
.btn {
|
||
padding: 8px 16px;
|
||
border: none;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.btn-primary {
|
||
background: #3498db;
|
||
color: white;
|
||
}
|
||
|
||
.btn-primary:hover {
|
||
background: #2980b9;
|
||
}
|
||
|
||
.btn-success {
|
||
background: #27ae60;
|
||
color: white;
|
||
}
|
||
|
||
.btn-success:hover {
|
||
background: #229954;
|
||
}
|
||
|
||
.btn-danger {
|
||
background: #e74c3c;
|
||
color: white;
|
||
}
|
||
|
||
.btn-danger:hover {
|
||
background: #c0392b;
|
||
}
|
||
|
||
.btn-warning {
|
||
background: #f39c12;
|
||
color: white;
|
||
}
|
||
|
||
.btn-warning:hover {
|
||
background: #d35400;
|
||
}
|
||
|
||
.btn-sm {
|
||
padding: 4px 8px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
}
|
||
|
||
th, td {
|
||
padding: 12px;
|
||
text-align: left;
|
||
border-bottom: 1px solid #e1e8ed;
|
||
}
|
||
|
||
th {
|
||
background: #f8f9fa;
|
||
font-weight: 600;
|
||
color: #555;
|
||
}
|
||
|
||
tr:hover {
|
||
background: #f8f9fa;
|
||
}
|
||
|
||
.checkbox-group {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
max-height: 300px;
|
||
overflow-y: auto;
|
||
padding: 10px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.checkbox-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
min-width: 200px;
|
||
}
|
||
|
||
.status-badge {
|
||
display: inline-block;
|
||
padding: 4px 8px;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.status-active {
|
||
background: #d4edda;
|
||
color: #155724;
|
||
}
|
||
|
||
.status-inactive {
|
||
background: #f8d7da;
|
||
color: #721c24;
|
||
}
|
||
|
||
.alert {
|
||
padding: 12px 16px;
|
||
border-radius: 4px;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.alert-success {
|
||
background: #d4edda;
|
||
color: #155724;
|
||
border: 1px solid #c3e6cb;
|
||
}
|
||
|
||
.alert-error {
|
||
background: #f8d7da;
|
||
color: #721c24;
|
||
border: 1px solid #f5c6cb;
|
||
}
|
||
|
||
.alert-info {
|
||
background: #d1ecf1;
|
||
color: #0c5460;
|
||
border: 1px solid #bee5eb;
|
||
}
|
||
|
||
.loading {
|
||
text-align: center;
|
||
padding: 20px;
|
||
color: #666;
|
||
}
|
||
|
||
.empty {
|
||
text-align: center;
|
||
padding: 40px;
|
||
color: #999;
|
||
}
|
||
|
||
.action-buttons {
|
||
display: flex;
|
||
gap: 5px;
|
||
}
|
||
|
||
.modal {
|
||
display: none;
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0,0,0,0.5);
|
||
z-index: 1000;
|
||
}
|
||
|
||
.modal.active {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.modal-content {
|
||
background: white;
|
||
border-radius: 8px;
|
||
padding: 30px;
|
||
max-width: 800px;
|
||
width: 90%;
|
||
max-height: 90vh;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.modal-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.modal-title {
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.close-btn {
|
||
background: none;
|
||
border: none;
|
||
font-size: 24px;
|
||
cursor: pointer;
|
||
color: #999;
|
||
}
|
||
|
||
.close-btn:hover {
|
||
color: #333;
|
||
}
|
||
|
||
.pagination {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
gap: 10px;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.pagination button {
|
||
padding: 6px 12px;
|
||
border: 1px solid #ddd;
|
||
background: white;
|
||
cursor: pointer;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.pagination button:hover:not(:disabled) {
|
||
background: #f0f0f0;
|
||
}
|
||
|
||
.pagination button:disabled {
|
||
cursor: not-allowed;
|
||
opacity: 0.5;
|
||
}
|
||
|
||
.pagination span {
|
||
color: #666;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<h1>订单日报系统</h1>
|
||
|
||
<div class="tabs">
|
||
<button class="tab active" onclick="switchTab('config')">配置管理</button>
|
||
<button class="tab" onclick="switchTab('generate')">日报生成</button>
|
||
<button class="tab" onclick="switchTab('history')">历史记录</button>
|
||
</div>
|
||
|
||
<!-- 配置管理 -->
|
||
<div id="config-tab" class="tab-content active">
|
||
<div class="card">
|
||
<button class="btn btn-primary" onclick="showCreateModal()">创建配置</button>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>配置名称</th>
|
||
<th>拆分方式</th>
|
||
<th>拆分值</th>
|
||
<th>字段数</th>
|
||
<th>状态</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="config-list">
|
||
<tr><td colspan="6" class="loading">加载中...</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 日报生成 -->
|
||
<div id="generate-tab" class="tab-content">
|
||
<div class="card">
|
||
<div class="alert alert-info" style="margin-bottom: 20px;">
|
||
<strong>💡 使用说明:</strong><br>
|
||
1. 选择已创建的配置<br>
|
||
2. 选择时间范围(可点击预设按钮快速选择)<br>
|
||
3. 点击"生成日报"按钮<br>
|
||
4. 生成成功后,点击"立即下载Excel文件"按钮下载,或切换到"历史记录"标签页查看和下载
|
||
</div>
|
||
|
||
<h3 style="margin-bottom: 20px;">生成日报</h3>
|
||
|
||
<div class="form-group">
|
||
<label>选择配置 *</label>
|
||
<select id="generate-config" class="form-control">
|
||
<option value="">请选择配置</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>时间范围</label>
|
||
<div style="display: flex; gap: 10px; margin-bottom: 10px;">
|
||
<button class="btn btn-sm btn-primary" onclick="setPresetTime('yesterday')">昨天</button>
|
||
<button class="btn btn-sm btn-primary" onclick="setPresetTime('last3days')">最近3天</button>
|
||
<button class="btn btn-sm btn-primary" onclick="setPresetTime('last7days')">最近7天</button>
|
||
</div>
|
||
<div style="display: flex; gap: 10px;">
|
||
<div style="flex: 1;">
|
||
<input type="datetime-local" id="start-time" class="form-control">
|
||
</div>
|
||
<div style="flex: 1;">
|
||
<input type="datetime-local" id="end-time" class="form-control">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<button class="btn btn-success" onclick="generateReport()">生成日报</button>
|
||
|
||
<div id="generate-result" style="margin-top: 20px;"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 历史记录 -->
|
||
<div id="history-tab" class="tab-content">
|
||
<div class="card">
|
||
<div style="display: flex; gap: 10px; align-items: center; flex-wrap: wrap;">
|
||
<button class="btn btn-primary" onclick="refreshHistory()">刷新</button>
|
||
<button class="btn btn-danger" onclick="batchDeleteHistory()">批量删除</button>
|
||
<button class="btn btn-success" onclick="batchDownloadHistory()">批量下载</button>
|
||
<span style="margin-left: auto;">日期查询:</span>
|
||
<input type="date" id="query-start-date" class="form-control" style="width: auto;">
|
||
<span>至</span>
|
||
<input type="date" id="query-end-date" class="form-control" style="width: auto;">
|
||
<button class="btn btn-primary" onclick="queryHistoryByDate()">查询</button>
|
||
<button class="btn btn-secondary" onclick="clearDateQuery()">清除</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th><input type="checkbox" id="select-all" onchange="toggleSelectAll()"></th>
|
||
<th>报表日期</th>
|
||
<th>时间范围</th>
|
||
<th>配置名称</th>
|
||
<th>拆分值</th>
|
||
<th>订单数</th>
|
||
<th>总金额</th>
|
||
<th>状态</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="history-list">
|
||
<tr><td colspan="9" class="loading">加载中...</td></tr>
|
||
</tbody>
|
||
</table>
|
||
|
||
<div class="pagination" id="pagination"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 创建/编辑配置模态框 -->
|
||
<div id="config-modal" class="modal">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h3 class="modal-title" id="modal-title">创建配置</h3>
|
||
<button class="close-btn" onclick="closeModal()">×</button>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>配置名称 *</label>
|
||
<input type="text" id="config-name" class="form-control" placeholder="请输入配置名称">
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>拆分方式 *</label>
|
||
<select id="split-type" class="form-control" onchange="loadEntities()">
|
||
<option value="">请选择拆分方式</option>
|
||
<option value="company_id">按企业ID</option>
|
||
<option value="user_id">按用户ID</option>
|
||
<option value="station_id">按场站ID</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>选择企业/用户/场站(可多选) *</label>
|
||
<div style="display: flex; gap: 8px; margin-bottom: 8px;">
|
||
<input type="text" id="entity-search" class="form-control" placeholder="输入名称搜索..." style="flex: 1;">
|
||
<button type="button" class="btn btn-secondary" onclick="searchEntities()">搜索</button>
|
||
</div>
|
||
<select id="split-value" class="form-control" multiple size="8">
|
||
<option value="">请先选择拆分方式</option>
|
||
</select>
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 8px;">
|
||
<small class="form-text text-muted">按住 Ctrl 或 Shift 键可多选</small>
|
||
<div id="entity-pagination" style="display: flex; gap: 4px; align-items: center;"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>选择字段 *</label>
|
||
<div class="checkbox-group" id="fields-list">
|
||
<div class="loading">加载中...</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>求和字段(可选)</label>
|
||
<div class="checkbox-group" id="sum-fields-list">
|
||
<div class="loading">加载中...</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display: flex; gap: 10px; justify-content: flex-end;">
|
||
<button class="btn" onclick="closeModal()">取消</button>
|
||
<button class="btn btn-primary" onclick="saveConfig()">保存</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// 全局错误处理,防止JavaScript错误中断执行
|
||
window.onerror = function(msg, url, line, col, error) {
|
||
console.error('JavaScript错误:', msg, 'at', url, 'line:', line);
|
||
return true; // 阻止错误传播
|
||
};
|
||
|
||
let editingConfigId = null;
|
||
let allFields = [];
|
||
let currentPage = 1;
|
||
const pageSize = 20;
|
||
|
||
// 切换标签页
|
||
function switchTab(tabName, event) {
|
||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
|
||
|
||
if (event && event.target) {
|
||
event.target.classList.add('active');
|
||
}
|
||
document.getElementById(`${tabName}-tab`).classList.add('active');
|
||
|
||
if (tabName === 'config') {
|
||
loadConfigs();
|
||
} else if (tabName === 'generate') {
|
||
loadGenerateConfigs();
|
||
} else if (tabName === 'history') {
|
||
loadHistory();
|
||
}
|
||
}
|
||
|
||
// 加载配置列表
|
||
async function loadConfigs() {
|
||
try {
|
||
const response = await fetch('/api/config');
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
renderConfigList(result.data);
|
||
} else {
|
||
alert('加载配置失败: ' + result.message);
|
||
}
|
||
} catch (error) {
|
||
alert('加载配置失败: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// 渲染配置列表
|
||
function renderConfigList(configs) {
|
||
const tbody = document.getElementById('config-list');
|
||
|
||
if (configs.length === 0) {
|
||
tbody.innerHTML = '<tr><td colspan="6" class="empty">暂无配置,请点击"创建配置"按钮</td></tr>';
|
||
return;
|
||
}
|
||
|
||
tbody.innerHTML = configs.map(config => `
|
||
<tr>
|
||
<td>${config.config_name}</td>
|
||
<td>${config.split_type === 'company_id' ? '按企业ID' : '按用户ID'}</td>
|
||
<td>${config.split_name || config.split_value}</td>
|
||
<td>${config.selected_fields.length}</td>
|
||
<td>
|
||
<span class="status-badge ${config.is_active ? 'status-active' : 'status-inactive'}">
|
||
${config.is_active ? '已启用' : '已禁用'}
|
||
</span>
|
||
</td>
|
||
<td>
|
||
<div class="action-buttons">
|
||
<button class="btn btn-sm btn-warning" onclick="toggleConfig(${config.id})">
|
||
${config.is_active ? '禁用' : '启用'}
|
||
</button>
|
||
<button class="btn btn-sm btn-primary" onclick="editConfig(${config.id})">编辑</button>
|
||
<button class="btn btn-sm btn-success" onclick="copyConfig(${config.id})">复制</button>
|
||
<button class="btn btn-sm btn-danger" onclick="deleteConfig(${config.id})">删除</button>
|
||
<button class="btn btn-sm" onclick="moveConfig(${config.id}, 'up')">↑</button>
|
||
<button class="btn btn-sm" onclick="moveConfig(${config.id}, 'down')">↓</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
`).join('');
|
||
}
|
||
|
||
// 显示创建模态框
|
||
function showCreateModal() {
|
||
editingConfigId = null;
|
||
document.getElementById('modal-title').textContent = '创建配置';
|
||
document.getElementById('config-name').value = '';
|
||
document.getElementById('split-type').value = '';
|
||
document.getElementById('split-value').innerHTML = '<option value="">请先选择拆分方式</option>';
|
||
|
||
loadFields();
|
||
document.getElementById('config-modal').classList.add('active');
|
||
}
|
||
|
||
// 加载字段列表
|
||
async function loadFields() {
|
||
try {
|
||
const response = await fetch('/api/fields');
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
allFields = result.data;
|
||
renderFields();
|
||
}
|
||
} catch (error) {
|
||
console.error('加载字段失败:', error);
|
||
}
|
||
}
|
||
|
||
// 渲染字段列表
|
||
function renderFields(selectedFields = [], sumFields = []) {
|
||
const fieldsList = document.getElementById('fields-list');
|
||
const sumFieldsList = document.getElementById('sum-fields-list');
|
||
|
||
fieldsList.innerHTML = allFields.map(field => `
|
||
<label class="checkbox-item">
|
||
<input type="checkbox" value="${field.field}"
|
||
${selectedFields.includes(field.field) ? 'checked' : ''}>
|
||
${field.display}
|
||
</label>
|
||
`).join('');
|
||
|
||
sumFieldsList.innerHTML = allFields.map(field => `
|
||
<label class="checkbox-item">
|
||
<input type="checkbox" value="${field.field}"
|
||
${sumFields.includes(field.field) ? 'checked' : ''}>
|
||
${field.display}
|
||
</label>
|
||
`).join('');
|
||
}
|
||
|
||
// 当前实体加载状态
|
||
let currentEntityType = '';
|
||
let entityPage = 1;
|
||
let currentSearch = '';
|
||
const entityPageSize = 100;
|
||
|
||
// 加载企业/用户列表(支持分页和搜索)
|
||
async function loadEntities(page = 1, search = '') {
|
||
const splitType = document.getElementById('split-type').value;
|
||
const select = document.getElementById('split-value');
|
||
|
||
if (!splitType) {
|
||
select.innerHTML = '<option value="">请先选择拆分方式</option>';
|
||
return;
|
||
}
|
||
|
||
// 保存当前状态
|
||
currentEntityType = splitType === 'company_id' ? 'company' :
|
||
splitType === 'user_id' ? 'user' :
|
||
splitType === 'station_id' ? 'station' : '';
|
||
entityPage = page;
|
||
currentSearch = search;
|
||
|
||
select.innerHTML = '<option value="">加载中...</option>';
|
||
|
||
try {
|
||
const response = await fetch(`/api/entities?type=${currentEntityType}&page=${page}&page_size=${entityPageSize}&search=${encodeURIComponent(search)}`);
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
const entities = result.data;
|
||
const total = result.total || 0;
|
||
const totalPages = Math.ceil(total / entityPageSize);
|
||
|
||
if (entities.length === 0) {
|
||
select.innerHTML = '<option value="">无数据</option>';
|
||
} else {
|
||
select.innerHTML = entities.map(e => {
|
||
let value = '';
|
||
let name = '';
|
||
let typeLabel = '';
|
||
|
||
if (splitType === 'company_id') {
|
||
value = e.company_id;
|
||
name = e.company_name;
|
||
} else if (splitType === 'user_id') {
|
||
value = e.user_id;
|
||
name = e.user_name;
|
||
typeLabel = e.order_type === 3 ? '(企业用户)' : '(普通用户)';
|
||
} else if (splitType === 'station_id') {
|
||
value = e.station_id;
|
||
name = e.station_name;
|
||
}
|
||
|
||
return `<option value="${value}" data-name="${name}">${name} ${typeLabel}</option>`;
|
||
}).join('');
|
||
}
|
||
|
||
// 更新分页UI
|
||
updatePagination(totalPages, total);
|
||
}
|
||
} catch (error) {
|
||
console.error('加载列表失败:', error);
|
||
select.innerHTML = '<option value="">加载失败</option>';
|
||
}
|
||
}
|
||
|
||
// 更新分页UI
|
||
function updatePagination(totalPages, total) {
|
||
const paginationDiv = document.getElementById('entity-pagination');
|
||
if (totalPages <= 1) {
|
||
paginationDiv.innerHTML = `<small class="text-muted">共 ${total} 条</small>`;
|
||
return;
|
||
}
|
||
|
||
let html = `<small class="text-muted">共 ${total} 条</small>`;
|
||
html += `<button type="button" class="btn btn-sm btn-secondary" onclick="loadEntities(${entityPage - 1}, currentSearch)" ${entityPage <= 1 ? 'disabled' : ''}>上一页</button>`;
|
||
html += `<span>${entityPage}/${totalPages}</span>`;
|
||
html += `<button type="button" class="btn btn-sm btn-secondary" onclick="loadEntities(${entityPage + 1}, currentSearch)" ${entityPage >= totalPages ? 'disabled' : ''}>下一页</button>`;
|
||
|
||
paginationDiv.innerHTML = html;
|
||
}
|
||
|
||
// 搜索实体
|
||
function searchEntities() {
|
||
const search = document.getElementById('entity-search').value.trim();
|
||
loadEntities(1, search);
|
||
}
|
||
|
||
// 回车搜索
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
const searchInput = document.getElementById('entity-search');
|
||
if (searchInput) {
|
||
searchInput.addEventListener('keypress', function(e) {
|
||
if (e.key === 'Enter') {
|
||
searchEntities();
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
// 保存配置
|
||
async function saveConfig() {
|
||
const configName = document.getElementById('config-name').value;
|
||
const splitType = document.getElementById('split-type').value;
|
||
const splitValueSelect = document.getElementById('split-value');
|
||
|
||
// 处理多选:获取所有选中的选项
|
||
const selectedOptions = Array.from(splitValueSelect.selectedOptions).filter(opt => opt.value);
|
||
const splitValue = selectedOptions.map(opt => opt.value).join(',');
|
||
const splitName = selectedOptions.map(opt => opt.dataset.name || '').join(',');
|
||
|
||
const selectedFields = Array.from(document.querySelectorAll('#fields-list input:checked')).map(cb => cb.value);
|
||
const sumFields = Array.from(document.querySelectorAll('#sum-fields-list input:checked')).map(cb => cb.value);
|
||
|
||
if (!configName || !splitType || !splitValue) {
|
||
alert('请填写所有必填字段');
|
||
return;
|
||
}
|
||
|
||
if (selectedFields.length === 0) {
|
||
alert('请至少选择一个字段');
|
||
return;
|
||
}
|
||
|
||
const data = {
|
||
config_name: configName,
|
||
split_type: splitType,
|
||
split_value: splitValue,
|
||
split_name: splitName,
|
||
selected_fields: selectedFields,
|
||
sum_fields: sumFields
|
||
};
|
||
|
||
try {
|
||
let response;
|
||
if (editingConfigId) {
|
||
response = await fetch(`/api/config/${editingConfigId}`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data)
|
||
});
|
||
} else {
|
||
response = await fetch('/api/config', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data)
|
||
});
|
||
}
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
alert(editingConfigId ? '配置更新成功' : '配置创建成功');
|
||
closeModal();
|
||
loadConfigs();
|
||
} else {
|
||
alert('保存失败: ' + result.message);
|
||
}
|
||
} catch (error) {
|
||
alert('保存失败: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// 编辑配置
|
||
async function editConfig(configId) {
|
||
try {
|
||
const response = await fetch('/api/config');
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
const config = result.data.find(c => c.id === configId);
|
||
if (config) {
|
||
editingConfigId = configId;
|
||
document.getElementById('modal-title').textContent = '编辑配置';
|
||
document.getElementById('config-name').value = config.config_name;
|
||
document.getElementById('split-type').value = config.split_type;
|
||
|
||
// 获取已选择的值
|
||
const splitValues = config.split_value.split(',');
|
||
|
||
// 加载实体并回显
|
||
await loadEntitiesForEdit(config.split_type, splitValues);
|
||
|
||
await loadFields();
|
||
renderFields(config.selected_fields, config.sum_fields);
|
||
|
||
document.getElementById('config-modal').classList.add('active');
|
||
}
|
||
}
|
||
} catch (error) {
|
||
alert('加载配置失败: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// 加载实体用于编辑回显
|
||
async function loadEntitiesForEdit(splitType, selectedValues) {
|
||
const select = document.getElementById('split-value');
|
||
|
||
if (!splitType) {
|
||
select.innerHTML = '<option value="">请先选择拆分方式</option>';
|
||
return;
|
||
}
|
||
|
||
const entityType = splitType === 'company_id' ? 'company' :
|
||
splitType === 'user_id' ? 'user' :
|
||
splitType === 'station_id' ? 'station' : '';
|
||
|
||
select.innerHTML = '<option value="">加载中...</option>';
|
||
|
||
try {
|
||
// 先加载第一页数据
|
||
const response = await fetch(`/api/entities?type=${entityType}&page=1&page_size=${entityPageSize}`);
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
const entities = result.data;
|
||
const total = result.total || 0;
|
||
const totalPages = Math.ceil(total / entityPageSize);
|
||
|
||
select.innerHTML = entities.map(e => {
|
||
let value = '';
|
||
let name = '';
|
||
let typeLabel = '';
|
||
|
||
if (splitType === 'company_id') {
|
||
value = e.company_id;
|
||
name = e.company_name;
|
||
} else if (splitType === 'user_id') {
|
||
value = e.user_id;
|
||
name = e.user_name;
|
||
typeLabel = e.order_type === 3 ? '(企业用户)' : '(普通用户)';
|
||
} else if (splitType === 'station_id') {
|
||
value = e.station_id;
|
||
name = e.station_name;
|
||
}
|
||
|
||
const isSelected = selectedValues.includes(String(value));
|
||
return `<option value="${value}" data-name="${name}" ${isSelected ? 'selected' : ''}>${name} ${typeLabel}</option>`;
|
||
}).join('');
|
||
|
||
// 更新分页UI
|
||
currentEntityType = entityType;
|
||
entityPage = 1;
|
||
updatePagination(totalPages, total);
|
||
}
|
||
} catch (error) {
|
||
console.error('加载列表失败:', error);
|
||
select.innerHTML = '<option value="">加载失败</option>';
|
||
}
|
||
}
|
||
|
||
// 复制配置
|
||
async function copyConfig(configId) {
|
||
try {
|
||
const response = await fetch(`/api/config/${configId}/copy`, {
|
||
method: 'POST'
|
||
});
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
alert('配置复制成功!');
|
||
loadConfigs();
|
||
} else {
|
||
alert('复制失败: ' + result.error);
|
||
}
|
||
} catch (error) {
|
||
console.error('复制配置失败:', error);
|
||
alert('复制失败,请稍后重试');
|
||
}
|
||
}
|
||
|
||
// 删除配置
|
||
async function deleteConfig(configId) {
|
||
if (!confirm('确定要删除这个配置吗?')) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`/api/config/${configId}`, {
|
||
method: 'DELETE'
|
||
});
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
alert('配置删除成功');
|
||
loadConfigs();
|
||
} else {
|
||
alert('删除失败: ' + result.message);
|
||
}
|
||
} catch (error) {
|
||
alert('删除失败: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// 启用/禁用配置
|
||
async function toggleConfig(configId) {
|
||
try {
|
||
const response = await fetch(`/api/config/${configId}/toggle`, {
|
||
method: 'POST'
|
||
});
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
loadConfigs();
|
||
} else {
|
||
alert('操作失败: ' + result.message);
|
||
}
|
||
} catch (error) {
|
||
alert('操作失败: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// 移动配置
|
||
async function moveConfig(configId, direction) {
|
||
try {
|
||
const response = await fetch(`/api/config/${configId}/move`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ direction })
|
||
});
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
loadConfigs();
|
||
} else {
|
||
alert('移动失败: ' + result.message);
|
||
}
|
||
} catch (error) {
|
||
alert('移动失败: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// 关闭模态框
|
||
function closeModal() {
|
||
document.getElementById('config-modal').classList.remove('active');
|
||
}
|
||
|
||
// 加载生成配置列表
|
||
async function loadGenerateConfigs() {
|
||
try {
|
||
const response = await fetch('/api/config');
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
const select = document.getElementById('generate-config');
|
||
select.innerHTML = '<option value="">请选择配置</option>' +
|
||
result.data.map(c => `<option value="${c.id}">${c.config_name}</option>`).join('');
|
||
}
|
||
} catch (error) {
|
||
console.error('加载配置失败:', error);
|
||
}
|
||
}
|
||
|
||
// 设置预设时间
|
||
function setPresetTime(preset) {
|
||
const now = new Date();
|
||
let start, end;
|
||
|
||
if (preset === 'yesterday') {
|
||
end = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 8, 0, 0);
|
||
start = new Date(end.getTime() - 24 * 60 * 60 * 1000);
|
||
} else if (preset === 'last3days') {
|
||
end = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 8, 0, 0);
|
||
start = new Date(end.getTime() - 3 * 24 * 60 * 60 * 1000);
|
||
} else if (preset === 'last7days') {
|
||
end = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 8, 0, 0);
|
||
start = new Date(end.getTime() - 7 * 24 * 60 * 60 * 1000);
|
||
}
|
||
|
||
document.getElementById('start-time').value = formatDateTimeLocal(start);
|
||
document.getElementById('end-time').value = formatDateTimeLocal(end);
|
||
}
|
||
|
||
// 格式化日期时间(用于输入框)
|
||
function formatDateTimeLocal(date) {
|
||
const year = date.getFullYear();
|
||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||
const day = String(date.getDate()).padStart(2, '0');
|
||
const hours = String(date.getHours()).padStart(2, '0');
|
||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
||
}
|
||
|
||
// 格式化日期时间为数字格式(用于显示)
|
||
function formatDateTimeDisplay(dateTimeStr) {
|
||
if (!dateTimeStr) return '-';
|
||
// 如果是字符串,直接返回(已经是数字格式)
|
||
if (typeof dateTimeStr === 'string') {
|
||
// 如果是 ISO 格式或其他格式,转换为 YYYY-MM-DD HH:MM:SS
|
||
const date = new Date(dateTimeStr);
|
||
if (!isNaN(date.getTime())) {
|
||
const year = date.getFullYear();
|
||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||
const day = String(date.getDate()).padStart(2, '0');
|
||
const hours = String(date.getHours()).padStart(2, '0');
|
||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||
}
|
||
return dateTimeStr;
|
||
}
|
||
return String(dateTimeStr);
|
||
}
|
||
|
||
// 格式化日期为数字格式(只显示日期,用于显示)
|
||
function formatDateDisplay(dateStr) {
|
||
if (!dateStr) return '-';
|
||
const date = new Date(dateStr);
|
||
if (!isNaN(date.getTime())) {
|
||
const year = date.getFullYear();
|
||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||
const day = String(date.getDate()).padStart(2, '0');
|
||
return `${year}-${month}-${day}`;
|
||
}
|
||
return String(dateStr);
|
||
}
|
||
|
||
// 生成日报
|
||
async function generateReport() {
|
||
const configId = document.getElementById('generate-config').value;
|
||
const startTime = document.getElementById('start-time').value;
|
||
const endTime = document.getElementById('end-time').value;
|
||
|
||
if (!configId) {
|
||
alert('请选择配置');
|
||
return;
|
||
}
|
||
|
||
if (!startTime || !endTime) {
|
||
alert('请选择时间范围');
|
||
return;
|
||
}
|
||
|
||
const resultDiv = document.getElementById('generate-result');
|
||
resultDiv.innerHTML = '<div class="alert alert-info">正在生成日报,请稍候...</div>';
|
||
|
||
try {
|
||
const response = await fetch('/api/report/generate', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
config_id: parseInt(configId),
|
||
start_time: startTime.replace('T', ' ') + ':00',
|
||
end_time: endTime.replace('T', ' ') + ':00'
|
||
})
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
resultDiv.innerHTML = `
|
||
<div class="alert alert-success">
|
||
<strong>✓ 日报生成成功!</strong><br><br>
|
||
<div style="margin-left: 20px;">
|
||
<strong>统计信息:</strong><br>
|
||
• 订单数量: ${result.total_orders} 条<br>
|
||
• 总金额: ¥${result.total_amount.toFixed(2)}<br>
|
||
${result.sum_results && Object.keys(result.sum_results).length > 0 ?
|
||
'<strong>求和结果:</strong><br>' +
|
||
Object.entries(result.sum_results).map(([field, value]) =>
|
||
`• ${field}: ${typeof value === 'number' ? value.toFixed(2) : value}`
|
||
).join('<br>')
|
||
: ''}
|
||
</div>
|
||
<br>
|
||
<div style="display: flex; gap: 10px; margin-top: 15px;">
|
||
<a href="/api/download?path=${result.file_path}" target="_blank" class="btn btn-success">
|
||
📥 立即下载Excel文件
|
||
</a>
|
||
<button class="btn btn-primary" onclick="switchTab('history'); document.querySelectorAll('.tab')[2].click();">
|
||
📋 查看历史记录
|
||
</button>
|
||
</div>
|
||
<br>
|
||
<div style="font-size: 12px; color: #666; margin-top: 10px;">
|
||
💡 提示:生成的日报也会保存在"历史记录"标签页中,可以随时下载。
|
||
</div>
|
||
</div>
|
||
`;
|
||
} else {
|
||
resultDiv.innerHTML = `
|
||
<div class="alert alert-error">
|
||
<strong>✗ 生成失败</strong><br><br>
|
||
<strong>错误信息:</strong><br>
|
||
${result.message}<br><br>
|
||
<strong>可能的原因:</strong><br>
|
||
• 选择的时间范围内没有符合条件的订单<br>
|
||
• 数据库连接失败<br>
|
||
• 配置信息有误<br><br>
|
||
<strong>建议:</strong><br>
|
||
• 检查时间范围是否正确<br>
|
||
• 确认配置中的企业/用户是否有订单数据<br>
|
||
• 查看服务器日志获取详细错误信息
|
||
</div>
|
||
`;
|
||
}
|
||
} catch (error) {
|
||
resultDiv.innerHTML = `<div class="alert alert-error">生成失败: ${error.message}</div>`;
|
||
}
|
||
}
|
||
|
||
// 加载历史记录
|
||
async function loadHistory(page = 1) {
|
||
console.log('[loadHistory] 开始加载,page:', page);
|
||
currentPage = page;
|
||
|
||
const tbody = document.getElementById('history-list');
|
||
if (tbody) {
|
||
tbody.innerHTML = '<tr><td colspan="8" class="loading">加载中...</td></tr>';
|
||
}
|
||
|
||
try {
|
||
console.log('[loadHistory] 发送请求...');
|
||
const response = await fetch(`/api/report/history?page=${page}&page_size=${pageSize}`);
|
||
console.log('[loadHistory] 响应状态:', response.status);
|
||
const result = await response.json();
|
||
console.log('[loadHistory] 响应数据:', result);
|
||
|
||
if (result.success) {
|
||
console.log('[loadHistory] 渲染历史记录...');
|
||
renderHistory(result.data.list);
|
||
renderPagination(result.data.total, page);
|
||
} else {
|
||
console.error('[loadHistory] API返回失败:', result.error);
|
||
if (tbody) {
|
||
tbody.innerHTML = '<tr><td colspan="8" class="empty">加载失败: ' + (result.error || '未知错误') + '</td></tr>';
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('[loadHistory] 错误:', error);
|
||
if (tbody) {
|
||
tbody.innerHTML = '<tr><td colspan="8" class="empty">加载失败: ' + error.message + '</td></tr>';
|
||
}
|
||
}
|
||
}
|
||
|
||
// 渲染历史记录
|
||
function renderHistory(history) {
|
||
const tbody = document.getElementById('history-list');
|
||
|
||
if (history.length === 0) {
|
||
tbody.innerHTML = '<tr><td colspan="8" class="empty">暂无历史记录<br><br>生成日报后,记录会显示在这里</td></tr>';
|
||
return;
|
||
}
|
||
|
||
tbody.innerHTML = history.map(item => {
|
||
// 调试信息
|
||
console.log('历史记录项:', item);
|
||
console.log(' status:', item.status, '类型:', typeof item.status);
|
||
console.log(' file_path:', item.file_path, '类型:', typeof item.file_path);
|
||
console.log(' 是否有文件:', !!(item.status === 1 && item.file_path));
|
||
|
||
const hasFile = item.status === 1 && item.file_path && item.file_path.trim() !== '';
|
||
|
||
return `
|
||
<tr>
|
||
<td><input type="checkbox" class="history-checkbox" value="${item.id}"></td>
|
||
<td>${formatDateDisplay(item.report_date)}</td>
|
||
<td style="font-size: 12px;">${formatDateTimeDisplay(item.start_time)}<br>至<br>${formatDateTimeDisplay(item.end_time)}</td>
|
||
<td>${item.config_name || '-'}</td>
|
||
<td>${item.split_name || item.split_value}</td>
|
||
<td><strong>${item.total_orders}</strong></td>
|
||
<td><strong>¥${parseFloat(item.total_amount || 0).toFixed(2)}</strong></td>
|
||
<td>
|
||
<span class="status-badge ${item.status === 1 ? 'status-active' : 'status-inactive'}">
|
||
${item.status === 1 ? '✓ 成功' : '✗ 失败'}
|
||
</span>
|
||
</td>
|
||
<td>
|
||
${hasFile ?
|
||
`<a href="/api/download?path=${encodeURIComponent(item.file_path)}" target="_blank" class="btn btn-sm btn-success">📥 下载</a>` :
|
||
`<span style="color: #999;" title="文件路径: ${item.file_path || '无'}">-</span>`}
|
||
<button onclick="deleteHistory(${item.id})" class="btn btn-sm btn-danger" style="margin-left: 5px;">🗑️ 删除</button>
|
||
</td>
|
||
</tr>
|
||
`}).join('');
|
||
}
|
||
|
||
// 渲染分页
|
||
function renderPagination(total, currentPage) {
|
||
const totalPages = Math.ceil(total / pageSize);
|
||
const pagination = document.getElementById('pagination');
|
||
|
||
if (totalPages <= 1) {
|
||
pagination.innerHTML = '';
|
||
return;
|
||
}
|
||
|
||
pagination.innerHTML = `
|
||
<button ${currentPage === 1 ? 'disabled' : ''} onclick="loadHistory(${currentPage - 1})">上一页</button>
|
||
<span>第 ${currentPage} / ${totalPages} 页,共 ${total} 条</span>
|
||
<button ${currentPage === totalPages ? 'disabled' : ''} onclick="loadHistory(${currentPage + 1})">下一页</button>
|
||
`;
|
||
}
|
||
|
||
// 刷新历史记录
|
||
function refreshHistory() {
|
||
loadHistory(currentPage);
|
||
}
|
||
|
||
// 全选/取消全选
|
||
function toggleSelectAll() {
|
||
const selectAll = document.getElementById('select-all');
|
||
const checkboxes = document.querySelectorAll('.history-checkbox');
|
||
checkboxes.forEach(cb => cb.checked = selectAll.checked);
|
||
}
|
||
|
||
// 获取选中的ID列表
|
||
function getSelectedIds() {
|
||
const checkboxes = document.querySelectorAll('.history-checkbox:checked');
|
||
return Array.from(checkboxes).map(cb => parseInt(cb.value));
|
||
}
|
||
|
||
// 批量删除
|
||
async function batchDeleteHistory() {
|
||
const ids = getSelectedIds();
|
||
if (ids.length === 0) {
|
||
alert('请先选择要删除的记录');
|
||
return;
|
||
}
|
||
if (!confirm(`确定要删除选中的 ${ids.length} 条记录吗?`)) {
|
||
return;
|
||
}
|
||
try {
|
||
const response = await fetch('/api/report/history/batch-delete', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ ids })
|
||
});
|
||
const result = await response.json();
|
||
if (result.success) {
|
||
alert(`成功删除 ${result.deleted_count} 条记录`);
|
||
loadHistory(currentPage);
|
||
} else {
|
||
alert('批量删除失败: ' + (result.error || '未知错误'));
|
||
}
|
||
} catch (error) {
|
||
alert('批量删除失败: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// 批量下载
|
||
function batchDownloadHistory() {
|
||
const ids = getSelectedIds();
|
||
if (ids.length === 0) {
|
||
alert('请先选择要下载的记录');
|
||
return;
|
||
}
|
||
// 逐个下载
|
||
ids.forEach(id => {
|
||
window.open(`/api/download?id=${id}`, '_blank');
|
||
});
|
||
}
|
||
|
||
// 按日期查询
|
||
function queryHistoryByDate() {
|
||
const startDate = document.getElementById('query-start-date').value;
|
||
const endDate = document.getElementById('query-end-date').value;
|
||
if (!startDate && !endDate) {
|
||
alert('请至少选择一个日期');
|
||
return;
|
||
}
|
||
// 重新加载历史记录,带上日期参数
|
||
loadHistoryWithDate(1, startDate, endDate);
|
||
}
|
||
|
||
// 清除日期查询
|
||
function clearDateQuery() {
|
||
document.getElementById('query-start-date').value = '';
|
||
document.getElementById('query-end-date').value = '';
|
||
loadHistory(1);
|
||
}
|
||
|
||
// 带日期参数加载历史记录
|
||
async function loadHistoryWithDate(page, startDate, endDate) {
|
||
try {
|
||
let url = `/api/report/history?page=${page}&page_size=10`;
|
||
if (startDate) url += `&start_date=${startDate}`;
|
||
if (endDate) url += `&end_date=${endDate}`;
|
||
const response = await fetch(url);
|
||
const result = await response.json();
|
||
if (result.success) {
|
||
renderHistory(result.data.list);
|
||
renderPagination(result.data.total, page);
|
||
} else {
|
||
alert('加载失败: ' + (result.error || '未知错误'));
|
||
}
|
||
} catch (error) {
|
||
alert('加载失败: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// 初始化
|
||
loadConfigs();
|
||
loadFields();
|
||
</script>
|
||
</body>
|
||
</html> |