feat: 将项目从Next.js重构为Python Flask版本
完全重构为Python + Flask架构,包含: 1. 核心模块: - lib/db.py:数据库连接(PyMySQL) - lib/field_mapping.py:字段映射(100+字段英文→中文) - lib/report_generator.py:日报生成核心逻辑 2. Flask应用: - app.py:主应用,包含所有API路由 - 配置管理API(CRUD、启用/禁用、排序) - 日报生成API(支持预设/自定义时间) - 历史记录API(分页查询) - 文件下载API - 定时任务(APScheduler,每天8:01) 3. 前端页面: - templates/index.html:完整的单页面应用 - 三个标签页:配置管理、日报生成、历史记录 - 原生HTML+CSS+JavaScript,无需构建 4. 依赖管理: - requirements.txt:Python依赖清单 - Flask 3.0.0 + Flask-CORS 4.0.0 - PyMySQL 1.1.0(数据库驱动) - openpyxl 3.1.2(Excel处理) - APScheduler 3.10.4(定时任务) 5. 启动脚本: - scripts/dev.bat:Windows开发环境启动 - scripts/start.bat:Windows生产环境启动 6. 文档: - README.md:完整的安装、运行、使用指南 - 包含API接口说明、数据库表结构、常见问题 Windows运行方式: 1. 安装Python 3.8+ 2. pip install -r requirements.txt 3. scripts\dev.bat 或 python app.py 4. 访问 http://localhost:5000 5. 初始化数据库:POST /api/init-tables 所有功能完整保留: ✅ 定时任务(每天8:01) ✅ 配置管理(CRUD+启用/禁用+排序) ✅ 日报生成(按配置+时间范围) ✅ 历史记录(查看+下载) ✅ 自定义字段(中文显示) ✅ 求和功能(多字段求和) ✅ 按企业/用户拆分 ✅ Excel导出 Coze-Commit-Type: user Coze-User-ID: 3722323274763196 Coze-Conversation-ID: 9894087
This commit is contained in:
957
templates/index.html
Normal file
957
templates/index.html
Normal file
@@ -0,0 +1,957 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>订单日报系统</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
||||
background: #f5f7fa;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
border-bottom: 2px solid #e1e8ed;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 12px 24px;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background: none;
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
color: #3498db;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
color: #3498db;
|
||||
border-bottom: 2px solid #3498db;
|
||||
margin-bottom: -2px;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: #3498db;
|
||||
}
|
||||
|
||||
select {
|
||||
background: white;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #3498db;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #2980b9;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #27ae60;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: #229954;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #e74c3c;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #c0392b;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: #f39c12;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-warning:hover {
|
||||
background: #d35400;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #e1e8ed;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #f8f9fa;
|
||||
font-weight: 600;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-active {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.status-inactive {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 12px 16px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
background: #d1ecf1;
|
||||
color: #0c5460;
|
||||
border: 1px solid #bee5eb;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.empty {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.5);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal.active {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 30px;
|
||||
max-width: 800px;
|
||||
width: 90%;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.pagination button {
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ddd;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.pagination button:hover:not(:disabled) {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.pagination button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.pagination span {
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>订单日报系统</h1>
|
||||
|
||||
<div class="tabs">
|
||||
<button class="tab active" onclick="switchTab('config')">配置管理</button>
|
||||
<button class="tab" onclick="switchTab('generate')">日报生成</button>
|
||||
<button class="tab" onclick="switchTab('history')">历史记录</button>
|
||||
</div>
|
||||
|
||||
<!-- 配置管理 -->
|
||||
<div id="config-tab" class="tab-content active">
|
||||
<div class="card">
|
||||
<button class="btn btn-primary" onclick="showCreateModal()">创建配置</button>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>配置名称</th>
|
||||
<th>拆分方式</th>
|
||||
<th>拆分值</th>
|
||||
<th>字段数</th>
|
||||
<th>状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="config-list">
|
||||
<tr><td colspan="6" class="loading">加载中...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 日报生成 -->
|
||||
<div id="generate-tab" class="tab-content">
|
||||
<div class="card">
|
||||
<h3 style="margin-bottom: 20px;">生成日报</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<label>选择配置 *</label>
|
||||
<select id="generate-config" class="form-control">
|
||||
<option value="">请选择配置</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>时间范围</label>
|
||||
<div style="display: flex; gap: 10px; margin-bottom: 10px;">
|
||||
<button class="btn btn-sm btn-primary" onclick="setPresetTime('yesterday')">昨天</button>
|
||||
<button class="btn btn-sm btn-primary" onclick="setPresetTime('last3days')">最近3天</button>
|
||||
<button class="btn btn-sm btn-primary" onclick="setPresetTime('last7days')">最近7天</button>
|
||||
</div>
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<div style="flex: 1;">
|
||||
<input type="datetime-local" id="start-time" class="form-control">
|
||||
</div>
|
||||
<div style="flex: 1;">
|
||||
<input type="datetime-local" id="end-time" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-success" onclick="generateReport()">生成日报</button>
|
||||
|
||||
<div id="generate-result" style="margin-top: 20px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 历史记录 -->
|
||||
<div id="history-tab" class="tab-content">
|
||||
<div class="card">
|
||||
<button class="btn btn-primary" onclick="refreshHistory()">刷新</button>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<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="8" class="loading">加载中...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="pagination" id="pagination"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 创建/编辑配置模态框 -->
|
||||
<div id="config-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title" id="modal-title">创建配置</h3>
|
||||
<button class="close-btn" onclick="closeModal()">×</button>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>配置名称 *</label>
|
||||
<input type="text" id="config-name" class="form-control" placeholder="请输入配置名称">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>拆分方式 *</label>
|
||||
<select id="split-type" class="form-control" onchange="loadEntities()">
|
||||
<option value="">请选择拆分方式</option>
|
||||
<option value="company_id">按企业ID</option>
|
||||
<option value="user_id">按用户ID</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>选择企业/用户 *</label>
|
||||
<select id="split-value" class="form-control">
|
||||
<option value="">请先选择拆分方式</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>选择字段 *</label>
|
||||
<div class="checkbox-group" id="fields-list">
|
||||
<div class="loading">加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>求和字段(可选)</label>
|
||||
<div class="checkbox-group" id="sum-fields-list">
|
||||
<div class="loading">加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 10px; justify-content: flex-end;">
|
||||
<button class="btn" onclick="closeModal()">取消</button>
|
||||
<button class="btn btn-primary" onclick="saveConfig()">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let editingConfigId = null;
|
||||
let allFields = [];
|
||||
let currentPage = 1;
|
||||
const pageSize = 20;
|
||||
|
||||
// 切换标签页
|
||||
function switchTab(tabName) {
|
||||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
|
||||
|
||||
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-danger" onclick="deleteConfig(${config.id})">删除</button>
|
||||
<button class="btn btn-sm" onclick="moveConfig(${config.id}, 'up')">↑</button>
|
||||
<button class="btn btn-sm" onclick="moveConfig(${config.id}, 'down')">↓</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// 显示创建模态框
|
||||
function showCreateModal() {
|
||||
editingConfigId = null;
|
||||
document.getElementById('modal-title').textContent = '创建配置';
|
||||
document.getElementById('config-name').value = '';
|
||||
document.getElementById('split-type').value = '';
|
||||
document.getElementById('split-value').innerHTML = '<option value="">请先选择拆分方式</option>';
|
||||
|
||||
loadFields();
|
||||
document.getElementById('config-modal').classList.add('active');
|
||||
}
|
||||
|
||||
// 加载字段列表
|
||||
async function loadFields() {
|
||||
try {
|
||||
const response = await fetch('/api/fields');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
allFields = result.data;
|
||||
renderFields();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载字段失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染字段列表
|
||||
function renderFields(selectedFields = [], sumFields = []) {
|
||||
const fieldsList = document.getElementById('fields-list');
|
||||
const sumFieldsList = document.getElementById('sum-fields-list');
|
||||
|
||||
fieldsList.innerHTML = allFields.map(field => `
|
||||
<label class="checkbox-item">
|
||||
<input type="checkbox" value="${field.field}"
|
||||
${selectedFields.includes(field.field) ? 'checked' : ''}>
|
||||
${field.display}
|
||||
</label>
|
||||
`).join('');
|
||||
|
||||
sumFieldsList.innerHTML = allFields.map(field => `
|
||||
<label class="checkbox-item">
|
||||
<input type="checkbox" value="${field.field}"
|
||||
${sumFields.includes(field.field) ? 'checked' : ''}>
|
||||
${field.display}
|
||||
</label>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// 加载企业/用户列表
|
||||
async function loadEntities() {
|
||||
const splitType = document.getElementById('split-type').value;
|
||||
const select = document.getElementById('split-value');
|
||||
|
||||
if (!splitType) {
|
||||
select.innerHTML = '<option value="">请先选择拆分方式</option>';
|
||||
return;
|
||||
}
|
||||
|
||||
select.innerHTML = '<option value="">加载中...</option>';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/entities?type=${splitType === 'company_id' ? 'company' : 'user'}`);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
const entities = result.data;
|
||||
select.innerHTML = '<option value="">请选择</option>' +
|
||||
entities.map(e => {
|
||||
const value = splitType === 'company_id' ? e.company_id : e.user_id;
|
||||
const name = splitType === 'company_id' ? e.company_name : e.user_name;
|
||||
const type = e.order_type === 3 ? '(企业用户)' : '(普通用户)';
|
||||
return `<option value="${value}" data-name="${name}">${name} ${splitType === 'user_id' ? type : ''}</option>`;
|
||||
}).join('');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载列表失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
async function saveConfig() {
|
||||
const configName = document.getElementById('config-name').value;
|
||||
const splitType = document.getElementById('split-type').value;
|
||||
const splitValueSelect = document.getElementById('split-value');
|
||||
const splitValue = splitValueSelect.value;
|
||||
const splitName = splitValueSelect.options[splitValueSelect.selectedIndex].dataset.name || '';
|
||||
|
||||
const selectedFields = Array.from(document.querySelectorAll('#fields-list input:checked')).map(cb => cb.value);
|
||||
const sumFields = Array.from(document.querySelectorAll('#sum-fields-list input:checked')).map(cb => cb.value);
|
||||
|
||||
if (!configName || !splitType || !splitValue) {
|
||||
alert('请填写所有必填字段');
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedFields.length === 0) {
|
||||
alert('请至少选择一个字段');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
config_name: configName,
|
||||
split_type: splitType,
|
||||
split_value: splitValue,
|
||||
split_name: splitName,
|
||||
selected_fields: selectedFields,
|
||||
sum_fields: sumFields
|
||||
};
|
||||
|
||||
try {
|
||||
let response;
|
||||
if (editingConfigId) {
|
||||
response = await fetch(`/api/config/${editingConfigId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
} else {
|
||||
response = await fetch('/api/config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
alert(editingConfigId ? '配置更新成功' : '配置创建成功');
|
||||
closeModal();
|
||||
loadConfigs();
|
||||
} else {
|
||||
alert('保存失败: ' + result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
alert('保存失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑配置
|
||||
async function editConfig(configId) {
|
||||
try {
|
||||
const response = await fetch('/api/config');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
const config = result.data.find(c => c.id === configId);
|
||||
if (config) {
|
||||
editingConfigId = configId;
|
||||
document.getElementById('modal-title').textContent = '编辑配置';
|
||||
document.getElementById('config-name').value = config.config_name;
|
||||
document.getElementById('split-type').value = config.split_type;
|
||||
|
||||
await loadEntities();
|
||||
document.getElementById('split-value').value = config.split_value;
|
||||
|
||||
await loadFields();
|
||||
renderFields(config.selected_fields, config.sum_fields);
|
||||
|
||||
document.getElementById('config-modal').classList.add('active');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
alert('加载配置失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 删除配置
|
||||
async function deleteConfig(configId) {
|
||||
if (!confirm('确定要删除这个配置吗?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/config/${configId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
alert('配置删除成功');
|
||||
loadConfigs();
|
||||
} else {
|
||||
alert('删除失败: ' + result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
alert('删除失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 启用/禁用配置
|
||||
async function toggleConfig(configId) {
|
||||
try {
|
||||
const response = await fetch(`/api/config/${configId}/toggle`, {
|
||||
method: 'POST'
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
loadConfigs();
|
||||
} else {
|
||||
alert('操作失败: ' + result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
alert('操作失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 移动配置
|
||||
async function moveConfig(configId, direction) {
|
||||
try {
|
||||
const response = await fetch(`/api/config/${configId}/move`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ direction })
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
loadConfigs();
|
||||
} else {
|
||||
alert('移动失败: ' + result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
alert('移动失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭模态框
|
||||
function closeModal() {
|
||||
document.getElementById('config-modal').classList.remove('active');
|
||||
}
|
||||
|
||||
// 加载生成配置列表
|
||||
async function loadGenerateConfigs() {
|
||||
try {
|
||||
const response = await fetch('/api/config');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
const select = document.getElementById('generate-config');
|
||||
select.innerHTML = '<option value="">请选择配置</option>' +
|
||||
result.data.map(c => `<option value="${c.id}">${c.config_name}</option>`).join('');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载配置失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 设置预设时间
|
||||
function setPresetTime(preset) {
|
||||
const now = new Date();
|
||||
let start, end;
|
||||
|
||||
if (preset === 'yesterday') {
|
||||
end = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 8, 0, 0);
|
||||
start = new Date(end.getTime() - 24 * 60 * 60 * 1000);
|
||||
} else if (preset === 'last3days') {
|
||||
end = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 8, 0, 0);
|
||||
start = new Date(end.getTime() - 3 * 24 * 60 * 60 * 1000);
|
||||
} else if (preset === 'last7days') {
|
||||
end = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 8, 0, 0);
|
||||
start = new Date(end.getTime() - 7 * 24 * 60 * 60 * 1000);
|
||||
}
|
||||
|
||||
document.getElementById('start-time').value = formatDateTimeLocal(start);
|
||||
document.getElementById('end-time').value = formatDateTimeLocal(end);
|
||||
}
|
||||
|
||||
// 格式化日期时间
|
||||
function formatDateTimeLocal(date) {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
||||
}
|
||||
|
||||
// 生成日报
|
||||
async function generateReport() {
|
||||
const configId = document.getElementById('generate-config').value;
|
||||
const startTime = document.getElementById('start-time').value;
|
||||
const endTime = document.getElementById('end-time').value;
|
||||
|
||||
if (!configId) {
|
||||
alert('请选择配置');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!startTime || !endTime) {
|
||||
alert('请选择时间范围');
|
||||
return;
|
||||
}
|
||||
|
||||
const resultDiv = document.getElementById('generate-result');
|
||||
resultDiv.innerHTML = '<div class="alert alert-info">正在生成日报,请稍候...</div>';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/report/generate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
config_id: parseInt(configId),
|
||||
start_time: startTime.replace('T', ' ') + ':00',
|
||||
end_time: endTime.replace('T', ' ') + ':00'
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
resultDiv.innerHTML = `
|
||||
<div class="alert alert-success">
|
||||
<strong>日报生成成功!</strong><br>
|
||||
订单数量: ${result.total_orders}<br>
|
||||
总金额: ¥${result.total_amount.toFixed(2)}<br>
|
||||
<a href="/api/download?path=${result.file_path}" target="_blank" class="btn btn-sm btn-success" style="margin-top: 10px;">下载Excel</a>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
resultDiv.innerHTML = `<div class="alert alert-error">生成失败: ${result.message}</div>`;
|
||||
}
|
||||
} catch (error) {
|
||||
resultDiv.innerHTML = `<div class="alert alert-error">生成失败: ${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载历史记录
|
||||
async function loadHistory(page = 1) {
|
||||
currentPage = page;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/report/history?page=${page}&page_size=${pageSize}`);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
renderHistory(result.data.list);
|
||||
renderPagination(result.data.total, page);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载历史记录失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染历史记录
|
||||
function renderHistory(history) {
|
||||
const tbody = document.getElementById('history-list');
|
||||
|
||||
if (history.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="8" class="empty">暂无历史记录</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = history.map(item => `
|
||||
<tr>
|
||||
<td>${item.report_date}</td>
|
||||
<td>${item.start_time || '-'} ~ ${item.end_time || '-'}</td>
|
||||
<td>${item.config_name || '-'}</td>
|
||||
<td>${item.split_name || item.split_value}</td>
|
||||
<td>${item.total_orders}</td>
|
||||
<td>¥${(item.total_amount || 0).toFixed(2)}</td>
|
||||
<td>
|
||||
<span class="status-badge ${item.status === 1 ? 'status-active' : 'status-inactive'}">
|
||||
${item.status === 1 ? '成功' : '失败'}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
${item.file_path ? `<a href="/api/download?path=${item.file_path}" target="_blank" class="btn btn-sm btn-primary">下载</a>` : '-'}
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// 渲染分页
|
||||
function renderPagination(total, currentPage) {
|
||||
const totalPages = Math.ceil(total / pageSize);
|
||||
const pagination = document.getElementById('pagination');
|
||||
|
||||
if (totalPages <= 1) {
|
||||
pagination.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
pagination.innerHTML = `
|
||||
<button ${currentPage === 1 ? 'disabled' : ''} onclick="loadHistory(${currentPage - 1})">上一页</button>
|
||||
<span>第 ${currentPage} / ${totalPages} 页,共 ${total} 条</span>
|
||||
<button ${currentPage === totalPages ? 'disabled' : ''} onclick="loadHistory(${currentPage + 1})">下一页</button>
|
||||
`;
|
||||
}
|
||||
|
||||
// 刷新历史记录
|
||||
function refreshHistory() {
|
||||
loadHistory(currentPage);
|
||||
}
|
||||
|
||||
// 初始化
|
||||
loadConfigs();
|
||||
loadFields();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user