Files
ylt_diy/templates/index.html
user9994793890 f02648a9b5 fix: 修复配置管理页面一直显示加载中的问题
Coze-Commit-Type: user
Coze-User-ID: 3722323274763196
Coze-Conversation-ID: 9894087
2026-07-13 14:22:12 +08:00

2236 lines
84 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>订单日报系统</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', '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: 20px;
}
.form-group label {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 8px;
font-weight: 600;
color: #2c3e50;
font-size: 14px;
}
.form-group label .label-icon {
width: 20px;
height: 20px;
display: inline-flex;
align-items: center;
justify-content: center;
background: #eef2f7;
border-radius: 4px;
font-size: 12px;
}
.form-group .form-hint {
font-size: 12px;
color: #999;
margin-top: 4px;
font-weight: 400;
}
.form-control {
width: 100%;
padding: 10px 14px;
border: 1.5px solid #e1e8ed;
border-radius: 8px;
font-size: 14px;
transition: all 0.2s;
background: #fafbfc;
}
.form-control:focus {
outline: none;
border-color: #3498db;
background: white;
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
}
select {
background: #fafbfc;
cursor: pointer;
}
select[multiple] {
padding: 6px;
}
select[multiple] option {
padding: 8px 12px;
border-radius: 4px;
margin-bottom: 2px;
cursor: pointer;
transition: background 0.15s;
}
select[multiple] option:hover {
background: #eef2f7;
}
select[multiple] option:checked {
background: #3498db;
color: white;
}
.btn {
padding: 8px 20px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s;
}
.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-secondary {
background: #eef2f7;
color: #555;
border: 1px solid #e1e8ed;
}
.btn-secondary:hover {
background: #dde4ed;
color: #333;
}
.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: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 6px;
max-height: 240px;
overflow-y: auto;
padding: 12px;
border: 1.5px solid #e1e8ed;
border-radius: 8px;
background: #fafbfc;
}
.checkbox-group::-webkit-scrollbar {
width: 6px;
}
.checkbox-group::-webkit-scrollbar-track {
background: transparent;
}
.checkbox-group::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 3px;
}
.checkbox-group::-webkit-scrollbar-thumb:hover {
background: #aaa;
}
.selected-fields-order {
display: flex;
flex-direction: column;
gap: 4px;
padding: 12px;
border: 1.5px dashed #4a90e2;
border-radius: 8px;
background: #f8fbff;
min-height: 40px;
}
.selected-field-item {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
background: #f0f9ff;
border: 1px solid #bae6fd;
border-radius: 4px;
font-size: 12px;
}
.selected-field-item .field-index {
color: #0284c7;
font-weight: 600;
min-width: 24px;
}
.selected-field-item .field-name {
flex: 1;
font-size: 13px;
color: #333;
}
.selected-field-item .field-actions {
display: flex;
gap: 4px;
}
.selected-field-item .btn-move {
width: 20px;
height: 20px;
border: 1px solid #cbd5e1;
background: white;
border-radius: 3px;
cursor: pointer;
font-size: 12px;
line-height: 1;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
}
.selected-field-item .btn-move:hover:not(:disabled) {
background: #e2e8f0;
border-color: #94a3b8;
}
.selected-field-item .btn-move:disabled {
opacity: 0.4;
cursor: not-allowed;
}
}
.selected-field-item .field-index {
font-size: 11px;
color: #999;
background: #f0f0f0;
padding: 2px 6px;
border-radius: 10px;
}
.checkbox-item {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
border-radius: 6px;
cursor: pointer;
transition: all 0.15s;
user-select: none;
}
.checkbox-item:hover {
background: #eef2f7;
}
.checkbox-item.checked {
background: #e8f4fd;
border: 1px solid #bee5eb;
}
.checkbox-item.required-field {
background: #fff3cd;
border: 1px solid #ffc107;
}
.checkbox-item.required-field:hover {
background: #fff3cd;
cursor: default;
}
.required-badge {
display: inline-block;
padding: 2px 6px;
background: #ffc107;
color: #000;
border-radius: 3px;
font-size: 11px;
font-weight: 500;
margin-left: 4px;
}
.checkbox-item input[type="checkbox"] {
width: 16px;
height: 16px;
accent-color: #3498db;
cursor: pointer;
flex-shrink: 0;
}
.checkbox-item label {
font-size: 13px;
color: #444;
cursor: pointer;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.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.4);
z-index: 1000;
backdrop-filter: blur(2px);
}
.modal.active {
display: flex;
align-items: center;
justify-content: center;
animation: fadeIn 0.2s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.modal-content {
background: white;
border-radius: 16px;
padding: 0;
max-width: 860px;
width: 92%;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 20px 60px rgba(0,0,0,0.15);
animation: slideUp 0.25s ease;
}
@keyframes slideUp {
from { transform: translateY(20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.modal-content::-webkit-scrollbar {
width: 6px;
}
.modal-content::-webkit-scrollbar-track {
background: transparent;
}
.modal-content::-webkit-scrollbar-thumb {
background: #ddd;
border-radius: 3px;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 28px;
border-bottom: 1px solid #eef2f7;
background: linear-gradient(135deg, #f8fafc 0%, #eef2f7 100%);
border-radius: 16px 16px 0 0;
position: sticky;
top: 0;
z-index: 10;
}
.modal-title {
font-size: 18px;
font-weight: 700;
color: #2c3e50;
display: flex;
align-items: center;
gap: 8px;
}
.modal-title::before {
content: '';
display: inline-block;
width: 4px;
height: 20px;
background: #3498db;
border-radius: 2px;
}
.close-btn {
background: none;
border: none;
font-size: 22px;
cursor: pointer;
color: #999;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
transition: all 0.2s;
}
.close-btn:hover {
background: #eef2f7;
color: #333;
}
.modal-body {
padding: 24px 28px;
}
.modal-footer {
display: flex;
gap: 10px;
justify-content: flex-end;
padding: 16px 28px;
border-top: 1px solid #eef2f7;
background: #fafbfc;
border-radius: 0 0 16px 16px;
}
.form-section {
margin-bottom: 24px;
padding-bottom: 20px;
border-bottom: 1px dashed #eef2f7;
}
.form-section:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.form-section-title {
font-size: 13px;
font-weight: 600;
color: #3498db;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 14px;
display: flex;
align-items: center;
gap: 8px;
}
.form-section-title .section-num {
width: 22px;
height: 22px;
display: inline-flex;
align-items: center;
justify-content: center;
background: #3498db;
color: white;
border-radius: 50%;
font-size: 12px;
font-weight: 700;
}
/* 时段编辑器样式 */
.time-period-editor {
background: #f8f9fa;
border-radius: 10px;
padding: 16px;
border: 1px solid #e9ecef;
}
.time-period-header {
margin-bottom: 12px;
}
.time-period-hint {
font-size: 12px;
color: #6c757d;
}
.time-period-tools {
display: flex;
gap: 8px;
margin-bottom: 12px;
flex-wrap: wrap;
}
.tp-tool-btn {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 6px 12px;
border: 1px solid #dee2e6;
border-radius: 6px;
background: white;
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
}
.tp-tool-btn:hover {
background: #f8f9fa;
border-color: #adb5bd;
}
.tp-tool-btn.active {
background: #3498db;
color: white;
border-color: #3498db;
}
.tp-tool-btn.tp-sharp { background: #e74c3c; color: white; border-color: #e74c3c; }
.tp-tool-btn.tp-peak { background: #f39c12; color: white; border-color: #f39c12; }
.tp-tool-btn.tp-flat { background: #27ae60; color: white; border-color: #27ae60; }
.tp-tool-btn.tp-valley { background: #3498db; color: white; border-color: #3498db; }
.time-period-grid-wrapper {
overflow-x: auto;
margin-bottom: 12px;
}
.tp-hours-header {
display: flex;
gap: 2px;
margin-bottom: 4px;
}
.tp-label-col {
width: 50px;
flex-shrink: 0;
}
.tp-hours-row {
display: flex;
gap: 2px;
flex: 1;
}
.tp-hour-label {
width: 28px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
color: #6c757d;
flex-shrink: 0;
}
.tp-period-rows {
display: flex;
flex-direction: column;
gap: 4px;
}
.tp-period-row {
display: flex;
gap: 2px;
align-items: center;
}
.tp-period-label {
width: 50px;
flex-shrink: 0;
font-size: 12px;
font-weight: 600;
padding: 4px 8px;
border-radius: 4px;
text-align: center;
}
.tp-period-label.sharp { background: #e74c3c; color: white; }
.tp-period-label.peak { background: #f39c12; color: white; }
.tp-period-label.flat { background: #27ae60; color: white; }
.tp-period-label.valley { background: #3498db; color: white; }
.tp-period-cells {
display: flex;
gap: 2px;
flex: 1;
}
.tp-cell {
width: 28px;
height: 28px;
border: 1px solid #dee2e6;
border-radius: 4px;
background: white;
cursor: pointer;
transition: all 0.15s;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
flex-shrink: 0;
}
.tp-cell:hover {
border-color: #adb5bd;
transform: scale(1.05);
}
.tp-cell.sharp { background: #e74c3c; border-color: #e74c3c; color: white; }
.tp-cell.peak { background: #f39c12; border-color: #f39c12; color: white; }
.tp-cell.flat { background: #27ae60; border-color: #27ae60; color: white; }
.tp-cell.valley { background: #3498db; border-color: #3498db; color: white; }
.time-period-summary {
font-size: 12px;
color: #495057;
padding: 8px 12px;
background: white;
border-radius: 6px;
border: 1px solid #e9ecef;
}
.time-period-summary span {
margin-right: 12px;
}
.time-period-summary .tp-count {
font-weight: 600;
}
.entity-select-wrapper {
position: relative;
}
.entity-select-wrapper .selected-count {
position: absolute;
top: -8px;
right: 12px;
background: #3498db;
color: white;
font-size: 11px;
padding: 2px 8px;
border-radius: 10px;
font-weight: 600;
z-index: 1;
}
.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>选择配置 * <span style="color: #666; font-size: 12px;">(可多选)</span></label>
<div style="border: 1px solid #ddd; border-radius: 8px; padding: 10px; max-height: 200px; overflow-y: auto;">
<div style="margin-bottom: 10px; padding-bottom: 10px; border-bottom: 1px solid #eee;">
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
<input type="checkbox" id="select-all-configs" onchange="toggleSelectAllConfigs()">
<strong>全选/取消全选</strong>
</label>
</div>
<div id="config-checkbox-list">
<!-- 配置复选框列表将通过 JavaScript 动态生成 -->
</div>
</div>
</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()">&times;</button>
</div>
<div class="modal-body">
<!-- 基本信息 -->
<div class="form-section">
<div class="form-section-title"><span class="section-num">1</span> 基本信息</div>
<div class="form-group">
<label>配置名称</label>
<input type="text" id="config-name" class="form-control" placeholder="请输入配置名称108路日报">
</div>
</div>
<!-- 拆分设置 -->
<div class="form-section">
<div class="form-section-title"><span class="section-num">2</span> 拆分设置</div>
<div class="form-group">
<label>拆分方式</label>
<select id="split-type" class="form-control" onchange="loadEntities()">
<option value="">请选择拆分方式</option>
<option value="company_id">按企业</option>
<option value="user_id">按用户</option>
<option value="station_id">按场站</option>
</select>
</div>
<div class="form-group">
<label>选择拆分对象 <span class="form-hint">(可多选,按住 Ctrl/Shift 批量选择)</span></label>
<div class="entity-select-wrapper">
<span class="selected-count" id="selected-entity-count" style="display:none;">已选 0 项</span>
<div style="display: flex; gap: 8px; margin-bottom: 8px;">
<input type="text" id="entity-search" class="form-control" placeholder="输入名称搜索..." style="flex: 1;" onkeydown="if(event.key==='Enter')searchEntities()">
<button type="button" class="btn btn-secondary" onclick="searchEntities()">搜索</button>
</div>
<select id="split-value" class="form-control" multiple size="8" onchange="updateSelectedCount()">
<option value="">请先选择拆分方式</option>
</select>
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 8px;">
<small class="form-hint">按住 Ctrl 或 Shift 键可多选</small>
<div id="entity-pagination" style="display: flex; gap: 4px; align-items: center;"></div>
</div>
</div>
</div>
<!-- 合并特来电数据选项(仅按企业拆分时显示) -->
<div class="form-group" id="merge-telecom-group" style="display:none; margin-top: 16px; padding: 12px; background: var(--muted); border-radius: 8px;">
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer; margin-bottom: 8px;">
<input type="checkbox" id="merge-telecom" onchange="toggleTelecomVehicleNo()" style="width: 16px; height: 16px;">
<span>合并特来电平台数据</span>
<small class="form-hint" style="margin: 0;">(需要输入车量自编号进行匹配)</small>
</label>
<div id="telecom-vehicle-no-group" style="display:none; margin-top: 8px;">
<label>特来电车量自编号 <span class="form-hint">(对应企业名称,多个用逗号分隔)</span></label>
<input type="text" id="telecom-vehicle-no" class="form-control" placeholder="输入车量自编号,多个用逗号分隔">
</div>
</div>
</div>
<!-- 时段设置 -->
<div class="form-section">
<div class="form-section-title"><span class="section-num">3</span> 时段设置 <span class="form-hint">(可选,用于分时段统计电量)</span></div>
<div class="time-period-editor">
<div class="time-period-header">
<span class="time-period-hint">点击下方时段按钮,再点选小时格子来分配时段。每个小时只能属于一个时段。</span>
</div>
<div class="time-period-tools">
<button type="button" class="tp-tool-btn" id="tp-tool-select" onclick="setTimePeriodTool('select')" title="点选">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 3l7.07 16.97 2.51-7.39 7.39-2.51L3 3z"/></svg>
点选
</button>
<button type="button" class="tp-tool-btn" id="tp-tool-clear" onclick="clearAllTimePeriods()" title="清空">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>
清空
</button>
</div>
<div class="time-period-grid-wrapper">
<div class="tp-hours-header">
<div class="tp-label-col"></div>
<div class="tp-hours-row" id="tp-hours-row"></div>
</div>
<div class="tp-period-rows" id="tp-period-rows"></div>
</div>
<div class="time-period-summary" id="tp-summary"></div>
</div>
</div>
<!-- 字段设置 -->
<div class="form-section">
<div class="form-section-title"><span class="section-num">4</span> 字段设置</div>
<div class="form-group">
<label>选择报表字段 <span class="form-hint">(勾选需要在报表中显示的字段,勾选后可调整顺序)</span></label>
<div class="checkbox-group" id="fields-list">
<div class="loading">加载中...</div>
</div>
</div>
<div class="form-group" id="selected-fields-order-group" style="display:none;">
<label>字段顺序 <span class="form-hint">(点击 ↑↓ 调整报表中的列顺序)</span></label>
<div class="selected-fields-order" id="selected-fields-order">
<!-- 已选字段会显示在这里,可拖拽排序 -->
</div>
</div>
<div class="form-group">
<label>求和字段 <span class="form-hint">(仅显示已选字段中的数值类型,可多选)</span></label>
<div class="checkbox-group" id="sum-fields-list">
<div class="loading">加载中...</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<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>';
// 重置字段自定义名头
fieldCustomNames = {};
// 重置合并特来电配置
const mergeTelecomCheckbox = document.getElementById('merge-telecom');
const telecomVehicleNoInput = document.getElementById('telecom-vehicle-no');
const mergeTelecomGroup = document.getElementById('merge-telecom-group');
if (mergeTelecomCheckbox) mergeTelecomCheckbox.checked = false;
if (telecomVehicleNoInput) telecomVehicleNoInput.value = '';
if (mergeTelecomGroup) mergeTelecomGroup.style.display = 'none';
toggleTelecomVehicleNo();
loadFields();
initTimePeriodEditor();
clearAllTimePeriods();
document.getElementById('config-modal').classList.add('active');
}
// 加载字段列表
async function loadFields(selectedFields = [], sumFields = []) {
try {
const response = await fetch('/api/fields');
const result = await response.json();
if (result.success) {
allFields = result.data;
renderFields(selectedFields, sumFields);
}
} catch (error) {
console.error('加载字段失败:', error);
}
}
// 可求和的数值类型字段
const SUMMABLE_FIELD_KEYS = [
'charge_degree', 'charge_ah', 'charge_begin_soc', 'charge_end_soc',
'receivable_electric_fee', 'receivable_service_fee', 'receivable_total_fee',
'charge_elecfee_amount', 'charge_elecfee_cost_amount', 'charge_service_amount',
'charging_amt', 'actual_pay_amount', 'pay_amount',
'settle_electric_fee', 'settle_service_fee', 'settle_fee',
'coupon_amount', 'coupon_total_amount', 'elecfee_coupon_amount', 'servicefee_coupon_amount',
'invoice_fee', 'activity_electric_fee', 'activity_service_fee', 'activity_total_fee',
'total_money',
// 分时段电量
'sharp_electricity', 'peak_electricity', 'flat_electricity', 'valley_electricity'
];
// 渲染字段列表
function renderFields(selectedFields = [], sumFields = []) {
const fieldsList = document.getElementById('fields-list');
const sumFieldsList = document.getElementById('sum-fields-list');
fieldsList.innerHTML = allFields.map(field => {
const isSelected = selectedFields.includes(field.key);
const isRequired = field.key === 'charge_degree'; // 充电量为必选字段
return `
<div class="checkbox-item ${isSelected ? 'checked' : ''} ${isRequired ? 'required-field' : ''}" onclick="${isRequired ? '' : 'toggleCheckboxItem(this)'}">
<input type="checkbox" value="${field.key}"
${isSelected ? 'checked' : ''}
${isRequired ? 'disabled checked' : ''}
onchange="onFieldCheckboxChange(this)">
<label>${field.display}${isRequired ? ' <span class="required-badge">必选</span>' : ''}</label>
</div>
`;
}).join('');
// 初始化求和字段列表(只显示已选字段中的可求和字段)
refreshSumFieldsList(selectedFields, sumFields);
}
// 点击 checkbox-item 容器时切换选中状态
function toggleCheckboxItem(item) {
const cb = item.querySelector('input[type="checkbox"]');
if (event.target === cb) return; // 如果点击的就是 checkbox 本身,不重复触发
cb.checked = !cb.checked;
item.classList.toggle('checked', cb.checked);
onFieldCheckboxChange(cb);
}
// 字段 checkbox 变化时更新求和字段列表和已选字段顺序
function onFieldCheckboxChange(cb) {
// 防止取消选择必选字段(充电量)
if (cb.value === 'charge_degree' && !cb.checked) {
cb.checked = true;
return;
}
const item = cb.closest('.checkbox-item');
if (item) item.classList.toggle('checked', cb.checked);
const fieldsList = document.getElementById('fields-list');
const checkedFields = Array.from(fieldsList.querySelectorAll('input:checked')).map(c => c.value);
// 保留当前求和字段的选择状态
const sumFieldsList = document.getElementById('sum-fields-list');
const currentSumFields = sumFieldsList ? Array.from(sumFieldsList.querySelectorAll('input:checked')).map(c => c.value) : [];
refreshSumFieldsList(checkedFields, currentSumFields);
refreshSelectedFieldsOrder(checkedFields);
}
// 字段自定义名头存储
let fieldCustomNames = {};
// 刷新已选字段顺序列表
function refreshSelectedFieldsOrder(selectedFields, customNames = null) {
const orderGroup = document.getElementById('selected-fields-order-group');
const orderList = document.getElementById('selected-fields-order');
if (!orderList) return;
// 如果传入了自定义名头,更新全局变量
if (customNames !== null) {
fieldCustomNames = {...customNames};
}
// 根据是否有已选字段,显示/隐藏字段顺序区域
if (orderGroup) {
orderGroup.style.display = selectedFields.length > 0 ? 'block' : 'none';
}
if (selectedFields.length === 0) {
orderList.innerHTML = '<small style="color:#999;text-align:center;padding:8px;">请先在上方勾选字段</small>';
} else {
orderList.innerHTML = selectedFields.map((fieldKey, index) => {
const field = allFields.find(f => f.key === fieldKey);
const displayName = field ? field.display : fieldKey;
const customName = fieldCustomNames[fieldKey] || '';
const isFirst = index === 0;
const isLast = index === selectedFields.length - 1;
return `
<div class="selected-field-item" data-field="${fieldKey}">
<span class="field-index">#${index + 1}</span>
<span class="field-name" title="${displayName}">${displayName}</span>
<input type="text" class="field-custom-name" placeholder="自定义报表名头" value="${customName}"
onchange="updateFieldCustomName('${fieldKey}', this.value)"
style="flex: 1; min-width: 100px; padding: 4px 8px; font-size: 12px;">
<div class="field-actions">
<button type="button" class="btn-move" onclick="moveFieldUp(${index})" ${isFirst ? 'disabled' : ''} title="上移">↑</button>
<button type="button" class="btn-move" onclick="moveFieldDown(${index})" ${isLast ? 'disabled' : ''} title="下移">↓</button>
</div>
</div>
`;
}).join('');
}
}
// 更新字段自定义名头
function updateFieldCustomName(fieldKey, customName) {
if (customName && customName.trim()) {
fieldCustomNames[fieldKey] = customName.trim();
} else {
delete fieldCustomNames[fieldKey];
}
}
// 获取字段自定义名头
function getFieldCustomNames() {
return {...fieldCustomNames};
}
// 字段上移
function moveFieldUp(index) {
if (index <= 0) return;
const orderList = document.getElementById('selected-fields-order');
const items = Array.from(orderList.querySelectorAll('.selected-field-item'));
const item = items[index];
const prevItem = items[index - 1];
orderList.insertBefore(item, prevItem);
updateFieldIndexes();
}
// 字段下移
function moveFieldDown(index) {
const orderList = document.getElementById('selected-fields-order');
const items = Array.from(orderList.querySelectorAll('.selected-field-item'));
if (index >= items.length - 1) return;
const item = items[index];
const nextItem = items[index + 1];
orderList.insertBefore(nextItem, item);
updateFieldIndexes();
}
// 更新字段序号
function updateFieldIndexes() {
const orderList = document.getElementById('selected-fields-order');
const items = orderList.querySelectorAll('.selected-field-item');
items.forEach((item, index) => {
const indexSpan = item.querySelector('.field-index');
if (indexSpan) indexSpan.textContent = `#${index + 1}`;
// 更新按钮状态
const upBtn = item.querySelector('.btn-move[title="上移"]');
const downBtn = item.querySelector('.btn-move[title="下移"]');
if (upBtn) upBtn.disabled = index === 0;
if (downBtn) downBtn.disabled = index === items.length - 1;
});
}
// 获取当前字段顺序
function getSelectedFieldsOrder() {
const orderList = document.getElementById('selected-fields-order');
if (!orderList) return [];
const items = orderList.querySelectorAll('.selected-field-item');
return Array.from(items).map(item => item.dataset.field);
}
// 刷新求和字段列表(仅显示已选字段中的数值类型字段)
function refreshSumFieldsList(selectedFields, currentSumFields) {
const sumFieldsList = document.getElementById('sum-fields-list');
const availableSumFields = allFields.filter(f =>
selectedFields.includes(f.key) && SUMMABLE_FIELD_KEYS.includes(f.key)
);
if (availableSumFields.length === 0) {
sumFieldsList.innerHTML = '<small style="color:#999;padding:8px;">请先在上方勾选数值类型的字段</small>';
} else {
sumFieldsList.innerHTML = availableSumFields.map(field => `
<div class="checkbox-item ${currentSumFields.includes(field.key) ? 'checked' : ''}" onclick="toggleCheckboxItem(this)">
<input type="checkbox" value="${field.key}"
${currentSumFields.includes(field.key) ? 'checked' : ''}>
<label>${field.display}</label>
</div>
`).join('');
}
}
// 切换特来电车量自编号输入框显示
function toggleTelecomVehicleNo() {
const mergeTelecom = document.getElementById('merge-telecom').checked;
const vehicleNoGroup = document.getElementById('telecom-vehicle-no-group');
if (vehicleNoGroup) {
vehicleNoGroup.style.display = mergeTelecom ? 'block' : 'none';
}
}
// 更新已选实体计数
function updateSelectedCount() {
const select = document.getElementById('split-value');
const countEl = document.getElementById('selected-entity-count');
const count = select.selectedOptions.length;
if (count > 0) {
countEl.textContent = '已选 ' + count + ' 项';
countEl.style.display = 'inline';
} else {
countEl.style.display = 'none';
}
}
// ========== 时段编辑器 ==========
const TIME_PERIODS = [
{ key: 'sharp', label: '尖', color: '#e74c3c' },
{ key: 'peak', label: '峰', color: '#f39c12' },
{ key: 'flat', label: '平', color: '#27ae60' },
{ key: 'valley', label: '谷', color: '#3498db' }
];
// 时段配置: { sharp: [11,12], peak: [9,10], flat: [7,8], valley: [0,1,2,3] }
let timePeriodConfig = { sharp: [], peak: [], flat: [], valley: [] };
let currentTpTool = 'select'; // 'select' or period key
// 初始化时段编辑器
function initTimePeriodEditor() {
const hoursRow = document.getElementById('tp-hours-row');
const periodRows = document.getElementById('tp-period-rows');
// 渲染小时标题行
let hoursHtml = '';
for (let h = 0; h < 24; h++) {
hoursHtml += `<div class="tp-hour-label">${h}</div>`;
}
hoursRow.innerHTML = hoursHtml;
// 渲染时段行
let rowsHtml = '';
TIME_PERIODS.forEach(period => {
rowsHtml += `
<div class="tp-period-row" data-period="${period.key}">
<div class="tp-period-label ${period.key}">${period.label}时</div>
<div class="tp-period-cells" id="tp-cells-${period.key}">
${Array.from({length: 24}, (_, h) =>
`<div class="tp-cell" data-hour="${h}" data-period="${period.key}" onclick="toggleTimePeriodCell(${h}, '${period.key}')"></div>`
).join('')}
</div>
</div>
`;
});
periodRows.innerHTML = rowsHtml;
updateTimePeriodSummary();
}
// 切换时段格子
function toggleTimePeriodCell(hour, periodKey) {
console.log('toggleTimePeriodCell called:', hour, periodKey);
// 检查该小时是否已被其他时段占用
const occupiedBy = Object.entries(timePeriodConfig).find(([k, v]) => k !== periodKey && v.includes(hour));
if (occupiedBy) {
// 已被其他时段占用,移除占用并分配给当前时段
const [otherKey] = occupiedBy;
timePeriodConfig[otherKey] = timePeriodConfig[otherKey].filter(h => h !== hour);
updateCellDisplay(hour, otherKey);
}
// 切换当前时段
if (timePeriodConfig[periodKey].includes(hour)) {
timePeriodConfig[periodKey] = timePeriodConfig[periodKey].filter(h => h !== hour);
} else {
timePeriodConfig[periodKey].push(hour);
timePeriodConfig[periodKey].sort((a, b) => a - b);
}
updateCellDisplay(hour, periodKey);
updateTimePeriodSummary();
}
// 更新格子显示
function updateCellDisplay(hour, periodKey) {
// 清除所有时段中该小时的样式
TIME_PERIODS.forEach(p => {
const cell = document.querySelector(`.tp-cell[data-hour="${hour}"][data-period="${p.key}"]`);
if (cell) {
cell.className = 'tp-cell';
cell.textContent = '';
}
});
// 检查该小时属于哪个时段,并设置对应的样式
for (const p of TIME_PERIODS) {
if (timePeriodConfig[p.key].includes(hour)) {
const cell = document.querySelector(`.tp-cell[data-hour="${hour}"][data-period="${p.key}"]`);
if (cell) {
cell.classList.add(p.key);
cell.textContent = '✓';
}
break; // 每个小时只能属于一个时段
}
}
}
// 更新时段摘要
function updateTimePeriodSummary() {
const summaryEl = document.getElementById('tp-summary');
const parts = TIME_PERIODS.map(p => {
const hours = timePeriodConfig[p.key];
const count = hours.length;
const hoursStr = count > 0 ? hours.map(h => `${h}`).join(',') : '未设置';
return `<span style="color:${p.color}"><b>${p.label}</b>: ${count} (${hoursStr})</span>`;
});
summaryEl.innerHTML = parts.join('');
}
// 设置时段配置(编辑时加载)
function setTimePeriodConfig(config) {
if (!config) {
timePeriodConfig = { sharp: [], peak: [], flat: [], valley: [] };
} else {
timePeriodConfig = {
sharp: config.sharp || [],
peak: config.peak || [],
flat: config.flat || [],
valley: config.valley || []
};
}
// 更新所有格子显示
TIME_PERIODS.forEach(period => {
for (let h = 0; h < 24; h++) {
updateCellDisplay(h, period.key);
}
});
updateTimePeriodSummary();
}
// 获取时段配置(保存时调用)
function getTimePeriodConfig() {
console.log('getTimePeriodConfig called, current config:', timePeriodConfig);
return { ...timePeriodConfig };
}
// 清空所有时段
function clearAllTimePeriods() {
timePeriodConfig = { sharp: [], peak: [], flat: [], valley: [] };
TIME_PERIODS.forEach(period => {
for (let h = 0; h < 24; h++) {
updateCellDisplay(h, period.key);
}
});
updateTimePeriodSummary();
}
// 当前实体加载状态
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');
// 显示/隐藏合并特来电选项(仅按企业拆分时显示)
const mergeTelecomGroup = document.getElementById('merge-telecom-group');
if (mergeTelecomGroup) {
mergeTelecomGroup.style.display = splitType === 'company_id' ? 'block' : 'none';
if (splitType !== 'company_id') {
document.getElementById('merge-telecom').checked = false;
toggleTelecomVehicleNo();
}
}
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 ? '(企业用户)' : '(普通用户)';
if (e.phone) {
name = `${e.user_name} (${e.phone})`;
}
} 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 = getSelectedFieldsOrder();
const sumFields = Array.from(document.querySelectorAll('#sum-fields-list input:checked')).map(cb => cb.value);
// 获取时段配置
const timePeriods = getTimePeriodConfig();
// 获取合并特来电配置
const mergeTelecom = document.getElementById('merge-telecom')?.checked || false;
const telecomVehicleNo = document.getElementById('telecom-vehicle-no')?.value.trim() || '';
if (!configName || !splitType || !splitValue) {
alert('请填写所有必填字段');
return;
}
if (selectedFields.length === 0) {
alert('请至少选择一个字段');
return;
}
// 确保充电量为必选字段
if (!selectedFields.includes('charge_degree')) {
selectedFields.push('charge_degree');
}
// 验证合并特来电配置
if (mergeTelecom && splitType !== 'company_id') {
alert('合并特来电数据仅支持按企业拆分');
return;
}
if (mergeTelecom && !telecomVehicleNo) {
alert('请输入特来电车量自编号');
return;
}
// 获取字段自定义名头
const fieldCustomNamesData = getFieldCustomNames();
const data = {
config_name: configName,
split_type: splitType,
split_value: splitValue,
split_name: splitName,
selected_fields: selectedFields,
sum_fields: sumFields,
time_periods: timePeriods,
merge_telecom: mergeTelecom,
telecom_vehicle_no: telecomVehicleNo,
field_custom_names: fieldCustomNamesData
};
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(config.selected_fields, config.sum_fields);
// 加载字段顺序和自定义名头
const customNames = config.field_custom_names || {};
refreshSelectedFieldsOrder(config.selected_fields, customNames);
// 初始化时段编辑器并加载配置
initTimePeriodEditor();
setTimePeriodConfig(config.time_periods);
// 加载合并特来电配置
const mergeTelecomCheckbox = document.getElementById('merge-telecom');
const telecomVehicleNoInput = document.getElementById('telecom-vehicle-no');
if (mergeTelecomCheckbox) {
mergeTelecomCheckbox.checked = config.merge_telecom === 1 || config.merge_telecom === true;
toggleTelecomVehicleNo();
}
if (telecomVehicleNoInput) {
telecomVehicleNoInput.value = config.telecom_vehicle_no || '';
}
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 {
// 先查询已选择的实体信息
let selectedEntities = [];
if (selectedValues.length > 0 && selectedValues[0] !== '') {
const response = await fetch('/api/entities/by_ids', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: entityType, ids: selectedValues })
});
const result = await response.json();
if (result.success) {
selectedEntities = result.data;
}
}
// 加载第一页数据
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);
// 构建已选择实体的ID集合
const selectedIdSet = new Set(selectedValues);
// 合并已选择的实体和第一页数据(去重)
const mergedEntities = [];
const addedIds = new Set();
// 先添加已选择的实体(确保它们显示在顶部)
for (const e of selectedEntities) {
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 ? '(企业用户)' : '(普通用户)';
if (e.phone) {
name = `${e.user_name} (${e.phone})`;
}
} else if (splitType === 'station_id') {
value = e.station_id;
name = e.station_name;
}
if (!addedIds.has(String(value))) {
mergedEntities.push({ value, name, typeLabel, isSelected: true });
addedIds.add(String(value));
}
}
// 再添加第一页数据(跳过已添加的)
for (const e of entities) {
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 ? '(企业用户)' : '(普通用户)';
if (e.phone) {
name = `${e.user_name} (${e.phone})`;
}
} else if (splitType === 'station_id') {
value = e.station_id;
name = e.station_name;
}
if (!addedIds.has(String(value))) {
const isSelected = selectedIdSet.has(String(value));
mergedEntities.push({ value, name, typeLabel, isSelected });
addedIds.add(String(value));
}
}
// 渲染选项
select.innerHTML = mergedEntities.map(item =>
`<option value="${item.value}" data-name="${item.name}" ${item.isSelected ? 'selected' : ''}>${item.name} ${item.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.message || 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 container = document.getElementById('config-checkbox-list');
container.innerHTML = result.data.map(c => `
<label style="display: flex; align-items: center; gap: 8px; padding: 6px 0; cursor: pointer;">
<input type="checkbox" class="config-checkbox" value="${c.id}" data-name="${c.config_name}">
<span>${c.config_name}</span>
</label>
`).join('');
}
} catch (error) {
console.error('加载配置失败:', error);
}
}
// 全选/取消全选配置
function toggleSelectAllConfigs() {
const selectAll = document.getElementById('select-all-configs');
const checkboxes = document.querySelectorAll('.config-checkbox');
checkboxes.forEach(cb => {
cb.checked = selectAll.checked;
});
}
// 设置预设时间
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 checkboxes = document.querySelectorAll('.config-checkbox:checked');
const startTime = document.getElementById('start-time').value;
const endTime = document.getElementById('end-time').value;
if (checkboxes.length === 0) {
alert('请至少选择一个配置');
return;
}
if (!startTime || !endTime) {
alert('请选择时间范围');
return;
}
const resultDiv = document.getElementById('generate-result');
resultDiv.innerHTML = '<div class="alert alert-info">正在批量生成日报,请稍候...</div>';
let successCount = 0;
let failCount = 0;
let results = [];
for (const cb of checkboxes) {
const configId = parseInt(cb.value);
const configName = cb.dataset.name;
try {
const response = await fetch('/api/report/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
config_id: configId,
start_time: startTime.replace('T', ' ') + ':00',
end_time: endTime.replace('T', ' ') + ':00'
})
});
const result = await response.json();
if (result.success) {
successCount++;
results.push({ name: configName, success: true, ...result });
} else {
failCount++;
results.push({ name: configName, success: false, message: result.message });
}
} catch (error) {
failCount++;
results.push({ name: configName, success: false, message: error.message });
}
}
// 显示结果
let html = '<div class="alert alert-info"><strong>生成完成</strong><br><br>';
html += `成功:${successCount} 个,失败:${failCount} 个<br><br>`;
results.forEach(r => {
if (r.success) {
html += `<div style="margin: 10px 0; padding: 10px; background: #f0f8ff; border-radius: 4px;">`;
html += `<strong>✓ ${r.name}</strong><br>`;
html += `订单数量:${r.total_orders} 条,总金额:¥${r.total_amount.toFixed(2)}<br>`;
html += `<a href="/api/download?path=${r.file_path}" target="_blank" class="btn btn-sm btn-success" style="margin-top: 5px;">📥 下载</a>`;
html += `</div>`;
} else {
html += `<div style="margin: 10px 0; padding: 10px; background: #fff0f0; border-radius: 4px;">`;
html += `<strong>✗ ${r.name}</strong><br>`;
html += `错误:${r.message}`;
html += `</div>`;
}
});
html += '</div>';
resultDiv.innerHTML = html;
}
// 批量下载历史记录
async function batchDownloadHistory() {
const checkboxes = document.querySelectorAll('.history-checkbox:checked');
if (checkboxes.length === 0) {
alert('请至少选择一条记录');
return;
}
const ids = Array.from(checkboxes).map(cb => parseInt(cb.value));
// 逐个下载
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>