From cc2214ccf661b9cfdbcd560d8a54309011f4b493 Mon Sep 17 00:00:00 2001 From: haoslm <6083297@qq.com> Date: Tue, 14 Jul 2026 16:05:43 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=BA=8F=E5=8F=B7=EF=BC=8C?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E5=AF=BC=E5=87=BA=EF=BC=8C=E5=AF=BC=E5=85=A5?= =?UTF-8?q?=EF=BC=8C=E6=B1=82=E5=92=8C=E9=87=87=E9=9B=86=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=AF=BC=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/api/config_api.py | 147 ++++++++++++++++++++++ lib/api/sum_data_api.py | 45 ++++++- lib/db_init.py | 13 ++ lib/report_generator.py | 51 ++++++-- lib/sum_data_collector.py | 233 ++++++++++++++++++++++++++++++++--- public/static/css/style.css | 9 ++ public/static/js/config.js | 57 ++++++++- public/static/js/sum_data.js | 49 +++++++- templates/index.html | 25 ++-- 9 files changed, 587 insertions(+), 42 deletions(-) diff --git a/lib/api/config_api.py b/lib/api/config_api.py index 2c19137..8c46889 100644 --- a/lib/api/config_api.py +++ b/lib/api/config_api.py @@ -272,3 +272,150 @@ def move_config(config_id): traceback.print_exc() print(f"[move_config] 异常: {e}") return jsonify({'success': False, 'message': str(e)}), 500 + + +@config_bp.route('/config/export', methods=['GET']) +def export_configs(): + """导出所有配置为JSON文件""" + try: + from flask import Response + import os + + configs = execute_query( + 'SELECT * FROM t_daily_report_config ORDER BY sort_order ASC, id ASC' + ) + + export_list = [] + for config in configs: + export_item = {} + for key, value in config.items(): + if key == 'id': + continue + export_item[key] = value + export_list.append(export_item) + + export_data = { + 'version': '1.0', + 'export_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + 'total': len(export_list), + 'configs': export_list + } + + json_str = json.dumps(export_data, ensure_ascii=False, indent=2) + filename = f'日报配置导出_{datetime.now().strftime("%Y%m%d_%H%M%S")}.json' + + response = Response(json_str, mimetype='application/json') + response.headers['Content-Disposition'] = f'attachment; filename="{filename}"' + return response + except Exception as e: + import traceback + traceback.print_exc() + return jsonify({'success': False, 'message': f'导出失败: {str(e)}'}), 500 + + +@config_bp.route('/config/import', methods=['POST']) +def import_configs(): + """从JSON文件导入配置""" + try: + if 'file' not in request.files: + return jsonify({'success': False, 'message': '请选择要导入的文件'}), 400 + + file = request.files['file'] + if file.filename == '': + return jsonify({'success': False, 'message': '请选择要导入的文件'}), 400 + + if not file.filename.endswith('.json'): + return jsonify({'success': False, 'message': '只支持 JSON 格式的配置文件'}), 400 + + file_content = file.read().decode('utf-8') + import_data = json.loads(file_content) + + if 'configs' not in import_data: + return jsonify({'success': False, 'message': '文件格式不正确,缺少 configs 字段'}), 400 + + configs_to_import = import_data['configs'] + if not isinstance(configs_to_import, list) or len(configs_to_import) == 0: + return jsonify({'success': False, 'message': '没有可导入的配置'}), 400 + + max_sort_result = execute_query('SELECT MAX(sort_order) as max_sort FROM t_daily_report_config') + current_max_sort = max_sort_result[0]['max_sort'] or 0 + + success_count = 0 + fail_count = 0 + fail_messages = [] + + for idx, config_data in enumerate(configs_to_import): + try: + config_name = config_data.get('config_name', '') + split_type = config_data.get('split_type', '') + split_value = config_data.get('split_value', '') + + if not config_name or not split_type or not split_value: + fail_count += 1 + fail_messages.append(f'第{idx+1}条:缺少必填字段') + continue + + new_id = int(datetime.now().timestamp() * 1000) + idx + new_name = f"{config_name}_导入" + sort_order = current_max_sort + 1 + current_max_sort += 1 + + selected_fields = config_data.get('selected_fields', '[]') + if isinstance(selected_fields, str): + selected_fields = json.loads(selected_fields) if selected_fields else [] + + sum_fields = config_data.get('sum_fields', '[]') + if isinstance(sum_fields, str): + sum_fields = json.loads(sum_fields) if sum_fields else [] + + time_periods = config_data.get('time_periods', '{}') + if isinstance(time_periods, str): + time_periods = json.loads(time_periods) if time_periods else {} + + field_custom_names = config_data.get('field_custom_names') + if isinstance(field_custom_names, str): + field_custom_names = json.loads(field_custom_names) if field_custom_names else None + + sql = """ + INSERT INTO t_daily_report_config + (id, config_name, split_type, split_value, split_name, selected_fields, + sum_fields, time_periods, merge_telecom, telecom_vehicle_no, field_custom_names, + show_monthly_total, is_active, sort_order, create_time, update_time) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, 0, %s, NOW(), NOW()) + """ + + params = ( + new_id, + new_name, + split_type, + split_value, + config_data.get('split_name', ''), + json.dumps(selected_fields, ensure_ascii=False), + json.dumps(sum_fields, ensure_ascii=False), + json.dumps(time_periods, ensure_ascii=False), + config_data.get('merge_telecom', 0), + config_data.get('telecom_vehicle_no', ''), + json.dumps(field_custom_names, ensure_ascii=False) if field_custom_names else None, + config_data.get('show_monthly_total', 0), + sort_order + ) + + execute_insert(sql, params) + success_count += 1 + except Exception as item_error: + fail_count += 1 + fail_messages.append(f'第{idx+1}条:{str(item_error)}') + + return jsonify({ + 'success': True, + 'message': f'导入完成:成功 {success_count} 条,失败 {fail_count} 条', + 'data': { + 'success_count': success_count, + 'fail_count': fail_count, + 'fail_messages': fail_messages + } + }) + except Exception as e: + import traceback + traceback.print_exc() + return jsonify({'success': False, 'message': f'导入失败: {str(e)}'}), 500 diff --git a/lib/api/sum_data_api.py b/lib/api/sum_data_api.py index 50d55ed..07f26d9 100644 --- a/lib/api/sum_data_api.py +++ b/lib/api/sum_data_api.py @@ -9,7 +9,8 @@ from lib.sum_data_collector import ( update_sum_data, delete_sum_data, batch_delete_sum_data, - get_monthly_charge_degree_total + get_monthly_charge_degree_total, + export_sum_data_to_excel ) from . import sum_data_bp @@ -38,7 +39,8 @@ def list_sum_data(): try: page = int(request.args.get('page', 1)) page_size = int(request.args.get('page_size', 20)) - report_date = request.args.get('report_date') + start_date = request.args.get('start_date') + end_date = request.args.get('end_date') config_id = request.args.get('config_id') config_name = request.args.get('config_name') sum_field_key = request.args.get('sum_field_key') @@ -47,7 +49,8 @@ def list_sum_data(): result = get_sum_data_list( page=page, page_size=page_size, - report_date=report_date, + start_date=start_date, + end_date=end_date, config_id=config_id, config_name=config_name, sum_field_key=sum_field_key, @@ -152,3 +155,39 @@ def monthly_charge_degree_total(): import traceback traceback.print_exc() return jsonify({'success': False, 'message': str(e)}), 500 + + +@sum_data_bp.route('/sum-data/export', methods=['GET']) +def export_sum_data(): + """导出求和数据为Excel + + 参数: + start_date: 开始日期(可选) + end_date: 结束日期(可选) + config_id: 配置ID(可选) + data_source: 数据来源(可选) + keyword: 配置名称关键词(可选) + """ + try: + start_date = request.args.get('start_date') + end_date = request.args.get('end_date') + config_id = request.args.get('config_id') + data_source = request.args.get('data_source') + keyword = request.args.get('keyword') + + result = export_sum_data_to_excel( + start_date=start_date, + end_date=end_date, + config_id=config_id, + data_source=data_source, + keyword=keyword + ) + + if result['success']: + return jsonify(result) + else: + return jsonify(result), 400 + except Exception as e: + import traceback + traceback.print_exc() + return jsonify({'success': False, 'message': str(e)}), 500 diff --git a/lib/db_init.py b/lib/db_init.py index c6863da..ea06ba0 100644 --- a/lib/db_init.py +++ b/lib/db_init.py @@ -82,6 +82,7 @@ def init_database_tables(): sum_field_name VARCHAR(100) NULL COMMENT '求和字段中文名称', sum_value DECIMAL(18,3) NULL COMMENT '求和值', total_orders INT NULL COMMENT '订单数', + sort_order INT NULL COMMENT '配置排序号', data_source VARCHAR(20) NULL COMMENT '数据来源 auto=自动采集 manual=手动编辑', remark VARCHAR(500) NULL COMMENT '备注', create_time DATETIME NULL COMMENT '创建时间', @@ -107,6 +108,18 @@ def init_database_tables(): else: print(f"[数据库] 新增 show_monthly_total 字段时出错(可能已存在): {e}") + # 给求和数据表增加 sort_order 字段 + try: + execute_update( + "ALTER TABLE t_daily_report_sum_data ADD COLUMN sort_order INT NULL COMMENT '配置排序号' " + ) + print("[数据库] 求和数据表新增 sort_order 字段成功") + except Exception as e: + if "Duplicate" in str(e) or "duplicate" in str(e) or "Exists" in str(e) or "exists" in str(e): + pass + else: + print(f"[数据库] 求和数据表新增 sort_order 字段时出错(可能已存在): {e}") + # 数据迁移:给 sort_order 为 NULL 的配置记录按 id 顺序赋值 try: null_count = execute_query( diff --git a/lib/report_generator.py b/lib/report_generator.py index 6e23da8..5d12313 100644 --- a/lib/report_generator.py +++ b/lib/report_generator.py @@ -474,7 +474,8 @@ def generate_daily_report(config_id, start_time=None, end_time=None): has_supplement=has_supplement, show_monthly_total=config.get('show_monthly_total', 0) == 1, config_id=config['id'], - report_date=start_time.strftime('%Y-%m-%d') + report_date=start_time.strftime('%Y-%m-%d'), + sort_order=config.get('sort_order') ) print(f"[日报生成] Excel文件已生成: {file_path}") @@ -564,7 +565,7 @@ def generate_daily_report(config_id, start_time=None, end_time=None): def generate_excel(orders, selected_fields, sum_fields, sum_results, config_name, split_name, start_time, end_time, id_name_maps=None, field_custom_names=None, has_supplement=False, - show_monthly_total=False, config_id=None, report_date=None): + show_monthly_total=False, config_id=None, report_date=None, sort_order=None): """ 生成Excel文件 @@ -585,7 +586,7 @@ def generate_excel(orders, selected_fields, sum_fields, sum_results, reports_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'public', 'reports') os.makedirs(reports_dir, exist_ok=True) - # 生成文件名 - 格式:配置名称_几月几日消费记录.xlsx + # 生成文件名 - 格式:序号_配置名称_几月几日消费记录.xlsx # 从 start_time 提取日期(报表数据对应的是开始时间那天的数据) try: start_dt = datetime.strptime(start_time, '%Y-%m-%d %H:%M:%S') @@ -595,7 +596,13 @@ def generate_excel(orders, selected_fields, sum_fields, sum_results, # 如果有补单,添加标记 supplement_suffix = '_有补单' if has_supplement else '' - filename = f'{config_name}_{date_str}消费记录{supplement_suffix}.xlsx' + + # 顺序号前缀(两位数字,不足补零) + sort_prefix = '' + if sort_order: + sort_prefix = f'{sort_order:02d}_' + + filename = f'{sort_prefix}{config_name}_{date_str}消费记录{supplement_suffix}.xlsx' file_path = os.path.join(reports_dir, filename) # 创建工作簿 @@ -668,7 +675,15 @@ def generate_excel(orders, selected_fields, sum_fields, sum_results, decimal3_fields = numeric_fields # 写入表头 - for col_idx, field in enumerate(selected_fields, 1): + # 第1列:序号 + cell = ws.cell(row=header_row, column=1) + cell.value = '序号' + cell.font = header_font + cell.fill = header_fill + cell.alignment = header_alignment + cell.border = thin_border + + for col_idx, field in enumerate(selected_fields, 2): cell = ws.cell(row=header_row, column=col_idx) if field in field_custom_names and field_custom_names[field]: display_name = field_custom_names[field] @@ -688,7 +703,17 @@ def generate_excel(orders, selected_fields, sum_fields, sum_results, data_start_row = header_row + 1 for row_idx, order in enumerate(orders, data_start_row): is_even = (row_idx - data_start_row) % 2 == 1 - for col_idx, field in enumerate(selected_fields, 1): + + # 第1列:序号 + seq_num = row_idx - data_start_row + 1 + seq_cell = ws.cell(row=row_idx, column=1) + seq_cell.value = seq_num + seq_cell.border = thin_border + seq_cell.alignment = Alignment(horizontal='center', vertical='center') + if is_even: + seq_cell.fill = even_row_fill + + for col_idx, field in enumerate(selected_fields, 2): cell = ws.cell(row=row_idx, column=col_idx) value = order.get(field, '') @@ -734,7 +759,7 @@ def generate_excel(orders, selected_fields, sum_fields, sum_results, ws.cell(row=sum_row, column=1).alignment = Alignment(horizontal='center', vertical='center') ws.cell(row=sum_row, column=1).border = thin_border - for col_idx, field in enumerate(selected_fields, 1): + for col_idx, field in enumerate(selected_fields, 2): cell = ws.cell(row=sum_row, column=col_idx) cell.fill = sum_fill cell.border = thin_border @@ -767,9 +792,9 @@ def generate_excel(orders, selected_fields, sum_fields, sum_results, # 月度总计行(在合计行下面空一行) monthly_row = sum_row + 2 if sum_fields else sum_row + 1 - # 找 charge_degree 字段所在的列 + # 找 charge_degree 字段所在的列(+1因为序号占了第1列) charge_degree_col = None - for col_idx, field in enumerate(selected_fields, 1): + for col_idx, field in enumerate(selected_fields, 2): if field == 'charge_degree': charge_degree_col = col_idx break @@ -782,7 +807,7 @@ def generate_excel(orders, selected_fields, sum_fields, sum_results, ws.cell(row=monthly_row, column=1).border = thin_border # 其他列也填充背景色 - for col_idx in range(2, len(selected_fields) + 1): + for col_idx in range(2, len(selected_fields) + 2): cell = ws.cell(row=monthly_row, column=col_idx) cell.fill = monthly_fill cell.border = thin_border @@ -799,7 +824,11 @@ def generate_excel(orders, selected_fields, sum_fields, sum_results, print(f"[日报生成] 写入月度总计时出错: {e}") # 调整列宽(中文按2个字符宽度计算) - for col_idx, field in enumerate(selected_fields, 1): + # 序号列固定宽度 + from openpyxl.utils import get_column_letter + ws.column_dimensions['A'].width = 8 + + for col_idx, field in enumerate(selected_fields, 2): header_name = get_field_display_name(field) if field in field_custom_names and field_custom_names[field]: header_name = field_custom_names[field] diff --git a/lib/sum_data_collector.py b/lib/sum_data_collector.py index 8e5444f..1994018 100644 --- a/lib/sum_data_collector.py +++ b/lib/sum_data_collector.py @@ -2,6 +2,7 @@ 日报求和数据采集与管理 """ import json +import os from datetime import datetime from lib.db import execute_query, execute_update, execute_insert from lib.field_mapping import get_field_display_name @@ -35,6 +36,7 @@ def collect_sum_data_from_history(history_id=None): collected_count = 0 skipped_count = 0 + config_sort_cache = {} # 缓存配置的排序号 for history in history_list: sum_results = history.get('sum_results') @@ -45,6 +47,24 @@ def collect_sum_data_from_history(history_id=None): elif sum_results is None: sum_results = {} + # 获取配置的排序号(缓存) + config_id = history.get('config_id') + sort_order = None + if config_id: + if config_id in config_sort_cache: + sort_order = config_sort_cache[config_id] + else: + try: + cfg_result = execute_query( + 'SELECT sort_order FROM t_daily_report_config WHERE id = %s LIMIT 1', + (config_id,) + ) + if cfg_result and cfg_result[0]['sort_order'] is not None: + sort_order = cfg_result[0]['sort_order'] + config_sort_cache[config_id] = sort_order + except Exception: + config_sort_cache[config_id] = None + # 强制确保有充电电量(charge_degree) if 'charge_degree' not in sum_results or sum_results['charge_degree'] is None: # 从订单表重新计算充电电量 @@ -75,9 +95,9 @@ def collect_sum_data_from_history(history_id=None): sql = ''' INSERT INTO t_daily_report_sum_data (id, report_date, config_id, config_name, split_type, split_value, split_name, - sum_field_key, sum_field_name, sum_value, total_orders, data_source, remark, + sum_field_key, sum_field_name, sum_value, total_orders, sort_order, data_source, remark, create_time, update_time) - VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW()) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW()) ''' params = ( sum_data_id, @@ -91,6 +111,7 @@ def collect_sum_data_from_history(history_id=None): get_field_display_name(field_key), float(field_value) if field_value is not None else 0, history.get('total_orders', 0), + sort_order, 'auto', f'从历史记录自动采集,历史ID: {history["id"]}' ) @@ -161,7 +182,7 @@ def _calculate_charge_degree(history): return None -def get_sum_data_list(page=1, page_size=20, report_date=None, config_id=None, +def get_sum_data_list(page=1, page_size=20, start_date=None, end_date=None, config_id=None, config_name=None, sum_field_key=None, data_source=None): """ 查询求和数据列表 @@ -169,7 +190,8 @@ def get_sum_data_list(page=1, page_size=20, report_date=None, config_id=None, Args: page: 页码 page_size: 每页大小 - report_date: 报表日期(可选) + start_date: 开始日期(可选) + end_date: 结束日期(可选) config_id: 配置ID(可选) config_name: 配置名称(可选,模糊搜索) sum_field_key: 求和字段键名(可选) @@ -184,9 +206,13 @@ def get_sum_data_list(page=1, page_size=20, report_date=None, config_id=None, where_clauses = ['1=1'] params = [] - if report_date: - where_clauses.append('report_date = %s') - params.append(report_date) + if start_date: + where_clauses.append('report_date >= %s') + params.append(start_date) + + if end_date: + where_clauses.append('report_date <= %s') + params.append(end_date) if config_id: where_clauses.append('config_id = %s') @@ -217,7 +243,7 @@ def get_sum_data_list(page=1, page_size=20, report_date=None, config_id=None, list_result = execute_query( f'''SELECT * FROM t_daily_report_sum_data WHERE {where_sql} - ORDER BY report_date DESC, config_id, id DESC + ORDER BY report_date DESC, sort_order ASC, config_id ASC, id DESC LIMIT %s OFFSET %s''', tuple(params) + (page_size, offset) ) @@ -249,12 +275,27 @@ def add_sum_data(data): """ try: sum_data_id = int(datetime.now().timestamp() * 1000) + + # 获取配置的排序号 + sort_order = None + config_id = data.get('config_id') + if config_id: + try: + cfg_result = execute_query( + 'SELECT sort_order FROM t_daily_report_config WHERE id = %s LIMIT 1', + (config_id,) + ) + if cfg_result and cfg_result[0]['sort_order'] is not None: + sort_order = cfg_result[0]['sort_order'] + except Exception: + pass + sql = ''' INSERT INTO t_daily_report_sum_data (id, report_date, config_id, config_name, split_type, split_value, split_name, - sum_field_key, sum_field_name, sum_value, total_orders, data_source, remark, + sum_field_key, sum_field_name, sum_value, total_orders, sort_order, data_source, remark, create_time, update_time) - VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW()) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW()) ''' params = ( sum_data_id, @@ -268,6 +309,7 @@ def add_sum_data(data): data.get('sum_field_name', ''), float(data.get('sum_value', 0)), data.get('total_orders', 0), + sort_order, 'manual', data.get('remark', '') ) @@ -409,13 +451,176 @@ def batch_delete_sum_data(ids): return {'success': False, 'message': str(e)} +def export_sum_data_to_excel(start_date=None, end_date=None, config_id=None, data_source=None, keyword=None): + """ + 导出求和数据为Excel + + Args: + start_date: 开始日期(可选) + end_date: 结束日期(可选) + config_id: 配置ID(可选) + data_source: 数据来源(可选) + keyword: 配置名称关键词(可选) + + Returns: + dict: {success, message, file_path} + """ + try: + from openpyxl import Workbook + from openpyxl.styles import Font, Alignment, PatternFill, Border, Side + + # 构建查询条件 + where_clauses = [] + params = [] + + if start_date: + where_clauses.append('report_date >= %s') + params.append(start_date) + + if end_date: + where_clauses.append('report_date <= %s') + params.append(end_date) + + if config_id: + where_clauses.append('config_id = %s') + params.append(config_id) + + if data_source: + where_clauses.append('data_source = %s') + params.append(data_source) + + if keyword: + where_clauses.append('config_name LIKE %s') + params.append(f'%{keyword}%') + + where_sql = '' + if where_clauses: + where_sql = 'WHERE ' + ' AND '.join(where_clauses) + + # 查询数据(不分页,全量导出) + sql = f''' + SELECT id, report_date, config_id, config_name, split_type, split_value, split_name, + sum_field_key, sum_field_name, sum_value, total_orders, sort_order, data_source, remark, + create_time, update_time + FROM t_daily_report_sum_data + {where_sql} + ORDER BY report_date DESC, sort_order ASC, config_id ASC, sum_field_key ASC + ''' + rows = execute_query(sql, tuple(params) if params else ()) + + # 创建Excel + wb = Workbook() + ws = wb.active + ws.title = '求和数据' + + # 表头 + headers = ['序号', '报表日期', '配置名称', '拆分方式', '拆分值', '求和字段', '求和值', '订单数', '数据来源', '修改原因/备注', '创建时间', '更新时间'] + + header_fill = PatternFill(start_color='4472C4', end_color='4472C4', fill_type='solid') + header_font = Font(bold=True, color='FFFFFF', size=11) + header_alignment = Alignment(horizontal='center', vertical='center') + + thin_border = Border( + left=Side(style='thin', color='000000'), + right=Side(style='thin', color='000000'), + top=Side(style='thin', color='000000'), + bottom=Side(style='thin', color='000000') + ) + + even_fill = PatternFill(start_color='F2F2F2', end_color='F2F2F2', fill_type='solid') + + # 写入表头 + for col_idx, header in enumerate(headers, 1): + cell = ws.cell(row=1, column=col_idx) + cell.value = header + cell.font = header_font + cell.fill = header_fill + cell.alignment = header_alignment + cell.border = thin_border + + ws.row_dimensions[1].height = 25 + + # 拆分方式映射 + split_type_map = { + 'company_id': '按企业ID', + 'user_id': '按用户ID', + 'station_id': '按场站ID' + } + + # 数据来源映射 + source_map = { + 'auto': '自动采集', + 'manual': '手动编辑' + } + + # 写入数据 + for row_idx, row in enumerate(rows, 2): + is_even = (row_idx - 2) % 2 == 1 + + values = [ + row.get('sort_order') or '', + row.get('report_date', ''), + row.get('config_name', ''), + split_type_map.get(row.get('split_type', ''), row.get('split_type', '')), + row.get('split_name', '') or row.get('split_value', ''), + row.get('sum_field_name', ''), + float(row.get('sum_value', 0) or 0), + int(row.get('total_orders', 0) or 0), + source_map.get(row.get('data_source', ''), row.get('data_source', '')), + row.get('remark', ''), + str(row.get('create_time', '')) if row.get('create_time') else '', + str(row.get('update_time', '')) if row.get('update_time') else '' + ] + + for col_idx, val in enumerate(values, 1): + cell = ws.cell(row=row_idx, column=col_idx) + cell.value = val + cell.border = thin_border + + # 对齐方式:数字靠右,其他居中或靠左 + if col_idx in [7, 8]: # 求和值、订单数 + cell.alignment = Alignment(horizontal='right', vertical='center') + if col_idx == 7: + cell.number_format = '0.000' + elif col_idx in [1, 2, 4, 9]: # 序号、日期、拆分方式、数据来源 + cell.alignment = Alignment(horizontal='center', vertical='center') + else: + cell.alignment = Alignment(horizontal='left', vertical='center') + + if is_even: + cell.fill = even_fill + + ws.row_dimensions[row_idx].height = 22 + + # 调整列宽 + col_widths = [8, 14, 20, 12, 20, 18, 14, 10, 12, 30, 20, 20] + for col_idx, width in enumerate(col_widths, 1): + from openpyxl.utils import get_column_letter + col_letter = get_column_letter(col_idx) + ws.column_dimensions[col_letter].width = width + + # 冻结首行 + ws.freeze_panes = 'A2' + + # 保存文件 + reports_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'public', 'reports') + os.makedirs(reports_dir, exist_ok=True) + + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + filename = f'求和数据导出_{timestamp}.xlsx' + file_path = os.path.join(reports_dir, filename) + wb.save(file_path) + + return {'success': True, 'message': '导出成功', 'file_path': f'/public/reports/{filename}', 'filename': filename, 'total': len(rows)} + except Exception as e: + import traceback + traceback.print_exc() + return {'success': False, 'message': str(e)} + + def get_monthly_charge_degree_total(config_id=None, month=None): """ 获取当月充电电量累计(从求和数据表查询,对账后的准确数据) - - 注意: - - 数据来源于 t_daily_report_sum_data 表(线下对账后的数据,可能被手动修改过) - - 只统计 sum_field_key = 'charge_degree' 的记录 - 如果当天已经生成了日报并采集,自动包含在内,不会重复计算 Args: diff --git a/public/static/css/style.css b/public/static/css/style.css index 8afb5fb..2e4f0a2 100644 --- a/public/static/css/style.css +++ b/public/static/css/style.css @@ -208,6 +208,15 @@ color: white; background: #d35400; } +.btn-info { +background: #17a2b8; +color: white; +} + +.btn-info:hover { +background: #138496; +} + .btn-secondary { background: #eef2f7; color: #555; diff --git a/public/static/js/config.js b/public/static/js/config.js index 7340bca..9b42480 100644 --- a/public/static/js/config.js +++ b/public/static/js/config.js @@ -23,15 +23,17 @@ function renderConfigList(configs) { const tbody = document.getElementById('config-list'); if (configs.length === 0) { - tbody.innerHTML = '暂无配置,请点击"创建配置"按钮'; + tbody.innerHTML = '暂无配置,请点击"创建配置"按钮'; return; } tbody.innerHTML = configs.map((config, index) => { const isFirst = index === 0; const isLast = index === configs.length - 1; + const sortOrder = index + 1; return ` + ${sortOrder} ${config.config_name} ${config.split_type === 'company_id' ? '按企业 ID' : config.split_type === 'user_id' ? '按用户 ID' : '按场站 ID'} ${config.split_name || config.split_value} @@ -879,3 +881,56 @@ function onSplitTypeChange() { document.getElementById('entity-list').innerHTML = '
请先选择拆分方式
'; } } + +// 导出配置 +function exportConfigs() { + try { + window.open('/api/config/export', '_blank'); + } catch (error) { + alert('导出失败: ' + error.message); + } +} + +// 导入配置 +async function importConfigs(event) { + const fileInput = event.target; + const file = fileInput.files[0]; + if (!file) return; + + if (!file.name.endsWith('.json')) { + alert('只支持 JSON 格式的配置文件'); + fileInput.value = ''; + return; + } + + if (!confirm('确定要导入配置吗?\n导入的配置会自动命名为"原名称_导入",并默认禁用。')) { + fileInput.value = ''; + return; + } + + const formData = new FormData(); + formData.append('file', file); + + try { + const response = await fetch('/api/config/import', { + method: 'POST', + body: formData + }); + const result = await response.json(); + + if (result.success) { + let message = result.message; + if (result.data && result.data.fail_messages && result.data.fail_messages.length > 0) { + message += '\n\n失败详情:\n' + result.data.fail_messages.join('\n'); + } + alert(message); + loadConfigs(); + } else { + alert('导入失败: ' + (result.message || '未知错误')); + } + } catch (error) { + alert('导入失败: ' + error.message); + } + + fileInput.value = ''; +} diff --git a/public/static/js/sum_data.js b/public/static/js/sum_data.js index 56782d8..53f8ef6 100644 --- a/public/static/js/sum_data.js +++ b/public/static/js/sum_data.js @@ -77,6 +77,7 @@ function renderSumDataList(rawList) { split_type: item.split_type, split_value: item.split_value, split_name: item.split_name, + sort_order: item.sort_order || 9999, total_orders: item.total_orders || 0, data_source: item.data_source, remark: item.remark || '', @@ -97,7 +98,7 @@ function renderSumDataList(rawList) { sumDataGroupedList = Object.values(groupMap).sort((a, b) => { if (a.report_date !== b.report_date) return b.report_date.localeCompare(a.report_date); - return (a.config_name || '').localeCompare(b.config_name || ''); + return (a.sort_order || 9999) - (b.sort_order || 9999); }); // 分页处理 @@ -176,12 +177,21 @@ function renderSumDataPagination(total) { // 加载求和数据 async function loadSumData(page = 1) { sumDataCurrentPage = page; - const reportDate = document.getElementById('sum-data-report-date').value; + const startDate = document.getElementById('sum-data-start-date').value; + const endDate = document.getElementById('sum-data-end-date').value; const configName = document.getElementById('sum-data-config-name').value; const dataSource = document.getElementById('sum-data-source').value; + + const params = new URLSearchParams(); + params.append('page', '1'); + params.append('page_size', '5000'); + if (startDate) params.append('start_date', startDate); + if (endDate) params.append('end_date', endDate); + if (configName) params.append('config_name', configName); + if (dataSource) params.append('data_source', dataSource); try { - const response = await fetch(`/api/sum-data?page=1&page_size=5000&report_date=${encodeURIComponent(reportDate)}&config_name=${encodeURIComponent(configName)}&data_source=${encodeURIComponent(dataSource)}`); + const response = await fetch(`/api/sum-data?${params.toString()}`); const result = await response.json(); if (result.success) { renderSumDataList(result.data.list || []); @@ -195,7 +205,8 @@ async function loadSumData(page = 1) { // 清除查询 function clearSumDataQuery() { - document.getElementById('sum-data-report-date').value = ''; + document.getElementById('sum-data-start-date').value = ''; + document.getElementById('sum-data-end-date').value = ''; document.getElementById('sum-data-config-name').value = ''; document.getElementById('sum-data-source').value = ''; loadSumData(1); @@ -525,3 +536,33 @@ async function batchDeleteSumData() { alert('删除失败: ' + error.message); } } + +async function exportSumData() { + const startDate = document.getElementById('sum-data-start-date').value; + const endDate = document.getElementById('sum-data-end-date').value; + const configName = document.getElementById('sum-data-config-name').value; + const dataSource = document.getElementById('sum-data-source').value; + + const params = new URLSearchParams(); + if (startDate) params.append('start_date', startDate); + if (endDate) params.append('end_date', endDate); + if (configName) params.append('keyword', configName); + if (dataSource) params.append('data_source', dataSource); + + try { + const response = await fetch(`/api/sum-data/export?${params.toString()}`); + const result = await response.json(); + + if (result.success) { + if (result.total === 0) { + alert('没有可导出的数据'); + return; + } + window.open(result.file_path, '_blank'); + } else { + alert('导出失败: ' + (result.message || '未知错误')); + } + } catch (error) { + alert('导出失败: ' + error.message); + } +} diff --git a/templates/index.html b/templates/index.html index a9c0fd9..1469196 100644 --- a/templates/index.html +++ b/templates/index.html @@ -4,7 +4,7 @@ 订单日报系统 - +
@@ -21,12 +21,16 @@
+ + +
+ @@ -36,7 +40,7 @@ - +
序号 配置名称 拆分方式 拆分值
加载中...
加载中...
@@ -122,7 +126,9 @@
- + + +