2236 lines
84 KiB
HTML
2236 lines
84 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: 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()">×</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> |