Files
ylt_diy/public/static/js/config.js

1021 lines
39 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

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

// 当前配置列表缓存(用于乐观更新)
let currentConfigList = [];
// 加载配置列表
async function loadConfigs() {
try {
const response = await fetch('/api/config');
const result = await response.json();
if (result.success) {
currentConfigList = result.data;
renderConfigList(currentConfigList);
} 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="7" class="empty">暂无配置,请点击"创建配置"按钮</td></tr>';
return;
}
tbody.innerHTML = configs.map((config, index) => {
const isFirst = index === 0;
const isLast = index === configs.length - 1;
const sortOrder = index + 1;
return `
<tr>
<td style="text-align: center; font-weight: bold; color: #4472C4;">${sortOrder}</td>
<td>${config.config_name}</td>
<td>${config.split_type === 'company_id' ? '按企业 ID' : config.split_type === 'user_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>
<button class="btn btn-sm btn-warning" onclick="toggleConfig(${config.id}, ${config.is_active})">
${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')" ${isFirst ? 'disabled style="opacity:0.4;cursor:not-allowed;"' : ''}>↑</button>
<button class="btn btn-sm" onclick="moveConfig(${config.id}, 'down')" ${isLast ? 'disabled style="opacity:0.4;cursor:not-allowed;"' : ''}>↓</button>
</td>
</tr>
`}).join('');
}
function showCreateModal() {
editingConfigId = null;
document.getElementById('modal-title').textContent = '创建配置';
document.getElementById('config-name').value = '';
document.getElementById('split-type').value = '';
// 清空已选实体
selectedEntityIds.clear();
// 清空实体列表
document.getElementById('entity-list').innerHTML = '<div class="empty">请先选择拆分方式</div>';
document.getElementById('entity-search').value = '';
document.getElementById('selected-count').textContent = '已选0 个';
// 重置字段自定义名头
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();
// 重置月度总计开关
const showMonthlyTotalCheckbox = document.getElementById('show-monthly-total');
if (showMonthlyTotalCheckbox) showMonthlyTotalCheckbox.checked = false;
// 重置自定义服务费单价
const customServiceFeePriceInput = document.getElementById('custom-service-fee-price');
if (customServiceFeePriceInput) customServiceFeePriceInput.value = '';
// 重置自定义服务费表头名称
const customServiceFeeNameInput = document.getElementById('custom-service-fee-name');
if (customServiceFeeNameInput) customServiceFeeNameInput.value = '';
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('field-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} <span class="field-key-name">(${field.key})</span>${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 currentOrder = getSelectedFieldsOrder();
// 获取所有勾选的字段
const fieldsList = document.getElementById('field-list');
const allCheckedFields = Array.from(fieldsList.querySelectorAll('input:checked')).map(c => c.value);
// 构建新的字段顺序:保留当前顺序,添加新勾选的字段,移除取消勾选的字段
let newOrder = [];
for (const fieldKey of currentOrder) {
if (allCheckedFields.includes(fieldKey)) {
newOrder.push(fieldKey);
}
}
// 添加新勾选的字段(不在当前顺序中的)
for (const fieldKey of allCheckedFields) {
if (!newOrder.includes(fieldKey)) {
newOrder.push(fieldKey);
}
}
// 保留当前求和字段的选择状态
const sumFieldsList = document.getElementById('sum-fields-list');
const currentSumFields = sumFieldsList ? Array.from(sumFieldsList.querySelectorAll('input:checked')).map(c => c.value) : [];
refreshSumFieldsList(newOrder, currentSumFields);
refreshSelectedFieldsOrder(newOrder);
}
// 刷新已选字段顺序列表
function refreshSelectedFieldsOrder(selectedFields, customNames = null) {
const orderGroup = document.getElementById('field-order-section');
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 class="field-key-name">(${fieldKey})</span></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('${fieldKey}')" ${isFirst ? 'disabled' : ''} title="上移">↑</button>
<button type="button" class="btn-move" onclick="moveFieldDown('${fieldKey}')" ${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(fieldKey) {
const orderList = document.getElementById('selected-fields-order');
const items = Array.from(orderList.querySelectorAll('.selected-field-item'));
const index = items.findIndex(item => item.dataset.field === fieldKey);
if (index <= 0) return;
const item = items[index];
const prevItem = items[index - 1];
orderList.insertBefore(item, prevItem);
updateFieldIndexes();
}
// 字段下移
function moveFieldDown(fieldKey) {
const orderList = document.getElementById('selected-fields-order');
const items = Array.from(orderList.querySelectorAll('.selected-field-item'));
const index = items.findIndex(item => item.dataset.field === fieldKey);
if (index < 0 || 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} <span class="field-key-name">(${field.key})</span></label>
</div>
`).join('');
}
}
// 切换特来电车量自编号输入框显示
function toggleTelecomVehicleNo() {
const mergeTelecom = document.getElementById('merge-telecom').checked;
const vehicleNoGroup = document.getElementById('telecom-vehicle-section');
if (vehicleNoGroup) {
vehicleNoGroup.style.display = mergeTelecom ? 'block' : '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('time-period-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();
}
// 当前实体加载状态
// 加载企业/用户列表(支持分页和搜索)
async function saveConfig() {
const configName = document.getElementById('config-name').value;
const splitType = document.getElementById('split-type').value;
// 处理多选:从 selectedEntityIds 获取选中的实体
const splitValue = Array.from(selectedEntityIds).join(',');
// 通过 API 查询所有已选实体的名称(确保翻页后也能正确获取)
let splitName = '';
if (selectedEntityIds.size > 0) {
const entityType = splitType === 'company_id' ? 'company' :
splitType === 'user_id' ? 'user' : 'station';
try {
const response = await fetch('/api/entities/by_ids', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: entityType, ids: Array.from(selectedEntityIds) })
});
const result = await response.json();
if (result.success && result.data) {
const idSet = new Set(Array.from(selectedEntityIds));
const sortedEntities = result.data.filter(e => {
const value = splitType === 'company_id' ? e.company_id :
splitType === 'user_id' ? e.user_id : e.station_id;
return idSet.has(String(value));
}).sort((a, b) => {
const aVal = splitType === 'company_id' ? a.company_id :
splitType === 'user_id' ? a.user_id : a.station_id;
const bVal = splitType === 'company_id' ? b.company_id :
splitType === 'user_id' ? b.user_id : b.station_id;
const aIdx = Array.from(selectedEntityIds).indexOf(String(aVal));
const bIdx = Array.from(selectedEntityIds).indexOf(String(bVal));
return aIdx - bIdx;
});
splitName = sortedEntities.map(e => {
if (splitType === 'company_id') return e.company_name;
if (splitType === 'user_id') return e.user_name;
return e.station_name;
}).join(',');
}
} catch (err) {
console.error('获取实体名称失败:', err);
}
}
// 获取字段顺序(从已选字段顺序列表获取)
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() || '';
// 获取月度总计开关
const showMonthlyTotal = document.getElementById('show-monthly-total')?.checked || false;
// 获取自定义服务费单价
const customServiceFeePriceInput = document.getElementById('custom-service-fee-price');
const customServiceFeePrice = customServiceFeePriceInput?.value !== '' ? parseFloat(customServiceFeePriceInput.value) : null;
// 获取自定义服务费表头名称
const customServiceFeeNameInput = document.getElementById('custom-service-fee-name');
const customServiceFeeName = customServiceFeeNameInput?.value.trim() || null;
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,
show_monthly_total: showMonthlyTotal,
custom_service_fee_price: customServiceFeePrice,
custom_service_fee_name: customServiceFeeName
};
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 || '';
}
// 加载月度总计开关
const showMonthlyTotalCheckbox = document.getElementById('show-monthly-total');
if (showMonthlyTotalCheckbox) {
showMonthlyTotalCheckbox.checked = config.show_monthly_total === 1 || config.show_monthly_total === true;
}
// 加载自定义服务费单价
const customServiceFeePriceInput = document.getElementById('custom-service-fee-price');
if (customServiceFeePriceInput) {
customServiceFeePriceInput.value = config.custom_service_fee_price !== null && config.custom_service_fee_price !== undefined ? config.custom_service_fee_price : '';
}
// 加载自定义服务费表头名称
const customServiceFeeNameInput = document.getElementById('custom-service-fee-name');
if (customServiceFeeNameInput) {
customServiceFeeNameInput.value = config.custom_service_fee_name || '';
}
document.getElementById('config-modal').classList.add('active');
}
}
} catch (error) {
alert('加载配置失败: ' + error.message);
}
}
// 加载实体用于编辑回显
async function loadEntitiesForEdit(splitType, selectedValues) {
const container = document.getElementById('entity-list');
if (!splitType) {
container.innerHTML = '<div class="empty">请先选择拆分方式</div>';
return;
}
const entityType = splitType === 'company_id' ? 'company' :
splitType === 'user_id' ? 'user' :
splitType === 'station_id' ? 'station' : '';
container.innerHTML = '<div class="loading">加载中...</div>';
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);
// 清空并重建 selectedEntityIds
selectedEntityIds.clear();
selectedValues.forEach(v => selectedEntityIds.add(String(v)));
// 合并已选择的实体和第一页数据(去重)
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));
}
}
// 渲染 checkbox 列表
container.innerHTML = mergedEntities.map(item => {
const isChecked = item.isSelected ? 'checked' : '';
const checkedClass = item.isSelected ? 'checked' : '';
const topClass = item.isSelected ? 'entity-selected-top' : '';
return `
<div class="checkbox-item ${checkedClass} ${topClass}" onclick="toggleEntityCheckbox(this, '${item.value}')">
<input type="checkbox" value="${item.value}" ${isChecked} onclick="event.stopPropagation()">
<span>${item.name} ${item.typeLabel}</span>
</div>
`;
}).join('');
// 更新分页UI
currentEntityType = entityType;
entityPage = 1;
updateEntityPagination(totalPages, total);
updateSelectedCount();
}
} catch (error) {
console.error('加载列表失败:', error);
container.innerHTML = '<div class="empty">加载失败</div>';
}
}
// 复制配置
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) {
// 找到当前配置索引
const currentIndex = currentConfigList.findIndex(c => c.id === configId);
if (currentIndex === -1) return;
// 边界检查
const targetIndex = direction === 'up' ? currentIndex - 1 : currentIndex + 1;
if (targetIndex < 0 || targetIndex >= currentConfigList.length) return;
// 乐观更新先在前端交换立即刷新UI
const temp = currentConfigList[currentIndex];
currentConfigList[currentIndex] = currentConfigList[targetIndex];
currentConfigList[targetIndex] = temp;
renderConfigList(currentConfigList);
// 异步同步到后端
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) {
// 失败了就回滚
const rollbackIndex = currentConfigList.findIndex(c => c.id === configId);
const rollbackTarget = direction === 'up' ? rollbackIndex + 1 : rollbackIndex - 1;
if (rollbackTarget >= 0 && rollbackTarget < currentConfigList.length) {
const temp = currentConfigList[rollbackIndex];
currentConfigList[rollbackIndex] = currentConfigList[rollbackTarget];
currentConfigList[rollbackTarget] = temp;
renderConfigList(currentConfigList);
}
alert('移动失败: ' + result.message);
}
} catch (error) {
// 网络错误也回滚
const rollbackIndex = currentConfigList.findIndex(c => c.id === configId);
const rollbackTarget = direction === 'up' ? rollbackIndex + 1 : rollbackIndex - 1;
if (rollbackTarget >= 0 && rollbackTarget < currentConfigList.length) {
const temp = currentConfigList[rollbackIndex];
currentConfigList[rollbackIndex] = currentConfigList[rollbackTarget];
currentConfigList[rollbackTarget] = temp;
renderConfigList(currentConfigList);
}
alert('移动失败: ' + error.message);
}
}
// 关闭模态框
function closeModal() {
document.getElementById('config-modal').classList.remove('active');
selectedEntityIds.clear();
}
// 清空时段配置
function clearTimePeriods() {
timePeriodConfig = { sharp: [], peak: [], flat: [], valley: [] };
updateTimePeriodSummary();
}
// 设置默认时段配置
function setDefaultTimePeriods() {
timePeriodConfig = {
sharp: [10, 11, 14, 15],
peak: [8, 9, 12, 13, 16, 17, 18, 19],
flat: [7, 20, 21, 22],
valley: [0, 1, 2, 3, 4, 5, 6, 23]
};
updateTimePeriodSummary();
}
// 导出配置
function exportConfigs() {
try {
window.open('/api/config/export', '_blank');
} catch (error) {
alert('导出失败: ' + error.message);
}
}
// 下载导入模板
function downloadTemplate() {
try {
window.open('/api/config/template', '_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('.xlsx') && !file.name.endsWith('.xls')) {
alert('只支持 Excel 格式(.xlsx/.xls的配置文件');
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 = '';
}