diff --git a/assets/image_20260713160840329.png b/assets/image_20260713160840329.png
new file mode 100644
index 0000000..d19b963
Binary files /dev/null and b/assets/image_20260713160840329.png differ
diff --git a/public/static/js/common.js b/public/static/js/common.js
index 9f59d4b..34f49ee 100644
--- a/public/static/js/common.js
+++ b/public/static/js/common.js
@@ -66,3 +66,347 @@ function showCreateModal() {
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 `
+
+
+
+
+ `;
+ }).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 = '请先在上方勾选字段';
+ } 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 `
+
+
#${index + 1}
+
${displayName}
+
+
+
+
+
+
+ `;
+ }).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 = '请先在上方勾选数值类型的字段';
+ } else {
+ sumFieldsList.innerHTML = availableSumFields.map(field => `
+
+
+
+
+ `).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 += `${h}
`;
+ }
+ hoursRow.innerHTML = hoursHtml;
+
+ // 渲染时段行
+ let rowsHtml = '';
+ TIME_PERIODS.forEach(period => {
+ rowsHtml += `
+
+
${period.label}时
+
+ ${Array.from({length: 24}, (_, h) =>
+ `
`
+ ).join('')}
+
+
+ `;
+ });
+ 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 `${p.label}时: ${count}个 (${hoursStr})`;
+ });
+ summaryEl.innerHTML = parts.join('');
+}
+
+// 设置时段配置(编辑时加载)
diff --git a/public/static/js/history.js b/public/static/js/history.js
index df867a7..899316f 100644
--- a/public/static/js/history.js
+++ b/public/static/js/history.js
@@ -222,4 +222,3 @@ async function loadHistoryWithDate(page, startDate, endDate) {
// 初始化
loadConfigs();
loadFields();
-