/**
* 主应用逻辑
*/
const App = {
currentPage: 'dashboard',
currentTaskNo: null,
// 配件汇总表排序状态
partSummarySort: {
sortBy: 'total_suggest_amount',
sortOrder: 'desc'
},
// 配件汇总表筛选状态
partSummaryFilters: {
page: 1,
page_size: 50,
part_code: '',
priority: ''
},
// 侧边栏折叠状态
isSidebarCollapsed: localStorage.getItem('sidebar-collapsed') === 'true',
/**
* 切换侧边栏折叠状态
*/
toggleSidebarCollapse() {
this.isSidebarCollapsed = !this.isSidebarCollapsed;
localStorage.setItem('sidebar-collapsed', this.isSidebarCollapsed);
this.applySidebarState();
},
/**
* 应用侧边栏状态
*/
applySidebarState() {
const sidebar = document.querySelector('.sidebar');
// 主内容区通过 CSS 选择器 .sidebar.collapsed + ... .main-content 自动调整
// 或者我们需要手动给 main-content 加类,但 CSS 中用了兄弟选择器,
// 不过兄弟选择器在这里可能不生效,因为中间隔了 .sidebar-overlay
// 让我们看看 index.html 结构: sidebar, sidebar-overlay, main-content
// CSS 选择器是 .sidebar.collapsed + .sidebar-overlay + .main-content
// 这样是可以的。
if (this.isSidebarCollapsed) {
sidebar.classList.add('collapsed');
} else {
sidebar.classList.remove('collapsed');
}
// 触发 icon 刷新以确保显示正确(虽然 CSS 旋转已处理)
lucide.createIcons();
},
/**
* 切换侧边栏显示状态(移动端)
*/
toggleSidebar(show) {
const sidebar = document.querySelector('.sidebar');
const overlay = document.getElementById('sidebar-overlay');
if (show) {
sidebar.classList.add('visible');
overlay.classList.add('active');
} else {
sidebar.classList.remove('visible');
overlay.classList.remove('active');
}
},
/**
* 应用配件筛选
*/
applyPartFilters() {
const partCode = document.getElementById('filter-part-code')?.value || '';
const priorityElement = document.getElementById('filter-priority');
const priority = priorityElement ? priorityElement.value : '';
this.partSummaryFilters.part_code = partCode;
this.partSummaryFilters.priority = priority;
this.partSummaryFilters.page = 1; // 重置到第一页
this.loadPartSummaries();
},
/**
* 重置配件筛选
*/
resetPartFilters() {
this.partSummaryFilters.part_code = '';
this.partSummaryFilters.priority = '';
this.partSummaryFilters.page = 1;
this.loadPartSummaries();
},
/**
* 渲染分析报告标签页
*/
async renderReportTab(container, taskNo) {
container.innerHTML = '
加载分析报告...
';
try {
const report = await API.getAnalysisReport(taskNo);
if (!report) {
container.innerHTML = `
${Components.renderEmptyState('file-x', '暂无分析报告', '该任务尚未生成分析报告')}
`;
return;
}
container.innerHTML = `
核心经营综述
${this.renderOverallAssessment(report.replenishment_insights)}
风险管控预警
${this.renderRiskAlerts(report.urgency_assessment)}
补货策略建议
${this.renderStrategy(report.strategy_recommendations)}
效果预期与建议
${this.renderExpectedImpact(report.expected_outcomes)}
`;
lucide.createIcons();
} catch (error) {
container.innerHTML = `
`;
lucide.createIcons();
}
},
renderOverallAssessment(insights) {
if (!insights) return '';
let heroHtml = '';
// Scale (Hero Main)
if (insights.scale_evaluation) {
heroHtml += `
补货规模
${insights.scale_evaluation.current_vs_historical || '-'}
${insights.scale_evaluation.possible_reasons || ''}
`;
}
// Structure (Hero Middle)
if (insights.structure_analysis) {
const data = insights.structure_analysis;
const details = [
data.category_distribution ? `• ${data.category_distribution}` : '',
data.price_range_distribution ? `• ${data.price_range_distribution}` : '',
data.turnover_distribution ? `• ${data.turnover_distribution}` : ''
].filter(Boolean).join('
');
heroHtml += `
结构特征
${data.imbalance_warning || '结构均衡'}
${details}
`;
}
// Timing (Hero End)
if (insights.timing_judgment) {
const data = insights.timing_judgment;
const isPos = data.is_favorable;
heroHtml += `
时机判断
${isPos ? '有利时机' : '建议观望'}
${data.recommendation}
${data.timing_factors || ''}
`;
}
return `${heroHtml}
`;
},
renderRiskAlerts(risks) {
if (!risks) return '';
let feedHtml = '';
const addRiskItem = (level, type, desc, action) => {
let icon = 'alert-circle';
if (level === 'high') icon = 'alert-octagon';
if (level === 'low') icon = 'info';
feedHtml += `
${type}
${level.toUpperCase()}
${desc}
${action ? `
${action}
` : ''}
`;
};
// Supply Risks
if (risks.supply_risks && Array.isArray(risks.supply_risks)) {
risks.supply_risks.forEach(r => addRiskItem(
r.likelihood === '高' ? 'high' : 'medium',
r.risk_type || '供应风险',
r.affected_scope,
r.mitigation
));
}
// Capital Risks
if (risks.capital_risks) {
const data = risks.capital_risks;
addRiskItem('medium', '资金风险', data.cash_flow_pressure, data.recommendation);
}
// Market Risks
if (risks.market_risks && Array.isArray(risks.market_risks)) {
risks.market_risks.forEach(r => addRiskItem('medium', '市场风险', r.risk_description, r.recommendation));
}
// Execution
if (risks.execution_anomalies && Array.isArray(risks.execution_anomalies)) {
risks.execution_anomalies.forEach(a => addRiskItem('high', a.anomaly_type || '执行异常', a.description, a.review_suggestion));
}
feedHtml += '
';
return feedHtml;
},
renderStrategy(strategy) {
if (!strategy) return '';
let html = '';
const addStep = (num, title, items) => {
const listItems = Array.isArray(items) ? items : [items];
const listHtml = listItems.map(i => `
${i}`).join('');
html += `
`;
};
// 1. Priority
if (strategy.priority_principle) {
const p = strategy.priority_principle;
addStep(1, '优先级排序', [
`
P1: ${p.tier1_criteria}`,
`
P2: ${p.tier2_criteria}`,
`
P3: ${p.tier3_criteria}`
]);
}
// 2. Phased
if (strategy.phased_procurement) {
addStep(2, '分批节奏', [
`节奏: ${strategy.phased_procurement.suggested_rhythm}`,
`范围: ${strategy.phased_procurement.recommended_parts}`
]);
}
// 3. Coordination
if (strategy.supplier_coordination) {
addStep(3, '供应商协同', [
strategy.supplier_coordination.key_communications,
`时机: ${strategy.supplier_coordination.timing_suggestions}`
]);
}
html += '
';
return html;
},
renderExpectedImpact(impact) {
if (!impact) return '';
let html = '';
// Inventory
if (impact.inventory_health) {
html += `
库存健康度
${Components.formatAmount(impact.inventory_health.shortage_reduction || 0)}
${impact.inventory_health.structure_improvement}
`;
}
// Efficiency
if (impact.capital_efficiency) {
html += `
资金效率
${Components.formatAmount(impact.capital_efficiency.investment_amount)}
${impact.capital_efficiency.expected_return}
`;
}
// Next
if (impact.follow_up_actions) {
html += `
下一步关注
Key Actions
${impact.follow_up_actions.next_steps}
`;
}
html += '
';
return html;
},
// 辅助方法:renderReportCard, renderRiskCard, renderImpactCard 已被新的独立渲染逻辑取代,保留为空或删除
renderReportCard(title, data) { return ''; },
renderRiskCard(title, data, level) { return ''; },
renderImpactCard(title, data) { return ''; },
/**
* 初始化应用
*/
init() {
this.bindEvents();
this.handleRoute();
this.applySidebarState(); // 初始化侧边栏状态
window.addEventListener('hashchange', () => this.handleRoute());
lucide.createIcons();
},
/**
* 绑定全局事件
*/
bindEvents() {
// 刷新按钮
document.getElementById('refresh-btn').addEventListener('click', () => {
this.handleRoute();
});
// 模态框关闭
document.getElementById('modal-close').addEventListener('click', () => {
Components.closeModal();
});
document.getElementById('modal-overlay').addEventListener('click', (e) => {
if (e.target.id === 'modal-overlay') {
Components.closeModal();
}
});
// 侧边栏切换
const menuToggle = document.getElementById('menu-toggle');
const sidebarOverlay = document.getElementById('sidebar-overlay');
if (menuToggle) {
menuToggle.addEventListener('click', () => {
this.toggleSidebar(true);
});
}
if (sidebarOverlay) {
sidebarOverlay.addEventListener('click', () => {
this.toggleSidebar(false);
});
}
// 桌面端侧边栏折叠按钮
const collapseBtn = document.getElementById('sidebar-collapse-btn');
if (collapseBtn) {
collapseBtn.addEventListener('click', () => {
this.toggleSidebarCollapse();
});
}
// 导航点击自动关闭侧边栏(移动端)
document.querySelectorAll('.nav-item').forEach(item => {
item.addEventListener('click', () => {
if (window.innerWidth <= 1024) {
this.toggleSidebar(false);
}
});
});
},
/**
* 路由处理
*/
handleRoute() {
const hash = window.location.hash || '#/';
const [, path, param] = hash.match(/#\/([^/]*)(?:\/(.*))?/) || [, '', ''];
// 更新导航状态
document.querySelectorAll('.nav-item').forEach(item => {
item.classList.remove('active');
if (item.dataset.page === (path || 'dashboard')) {
item.classList.add('active');
}
});
// 路由分发
switch (path) {
case 'tasks':
if (param) {
this.showTaskDetail(param);
} else {
this.showTaskList();
}
break;
case '':
default:
this.showDashboard();
break;
}
},
/**
* 更新面包屑
*/
updateBreadcrumb(items) {
const breadcrumb = document.getElementById('breadcrumb');
breadcrumb.innerHTML = items.map((item, index) => {
if (item.href) {
return `${item.text}`;
}
return `${item.text}`;
}).join('');
},
/**
* 显示概览页面
*/
async showDashboard() {
this.currentPage = 'dashboard';
this.updateBreadcrumb([{ text: '概览' }]);
const container = document.getElementById('page-container');
container.innerHTML = '';
try {
// 获取统计数据
const [stats, tasksData] = await Promise.all([
API.getStatsSummary().catch(() => ({})),
API.getTasks({ page: 1, page_size: 5 }).catch(() => ({ items: [] })),
]);
// 渲染统计卡片
const statsGrid = document.getElementById('stats-grid');
statsGrid.innerHTML = `
${Components.renderStatCard('list-checks', '总任务数', stats.total_tasks || 0, 'primary')}
${Components.renderStatCard('check-circle', '成功任务', stats.success_tasks || 0, 'success')}
${Components.renderStatCard('x-circle', '失败任务', stats.failed_tasks || 0, 'danger')}
${Components.renderStatCard('package', '建议配件', stats.total_parts || 0, 'info')}
${Components.renderStatCard('dollar-sign', '建议金额', Components.formatAmount(stats.total_suggest_amount), 'warning')}
`;
// 渲染最近任务
this.renderRecentTasks(tasksData.items || []);
lucide.createIcons();
} catch (error) {
Components.showToast('加载数据失败: ' + error.message, 'error');
}
},
/**
* 渲染最近任务
*/
renderRecentTasks(tasks) {
const container = document.getElementById('recent-tasks');
if (!tasks.length) {
container.innerHTML = `
${Components.renderEmptyState('inbox', '暂无任务', '还没有执行过任何补货建议任务')}
`;
return;
}
container.innerHTML = `
| 任务编号 |
商家组合 |
状态 |
配件数 |
建议金额 |
执行时间 |
${tasks.map(task => `
|
${task.task_no}
|
${task.dealer_grouping_name || '-'} |
${Components.getStatusBadge(task.status, task.status_text)} |
${task.part_count} |
${Components.formatAmount(task.actual_amount)} |
${Components.formatDuration(task.duration_seconds)} |
`).join('')}
`;
},
/**
* 显示任务列表页面
*/
async showTaskList(page = 1) {
this.currentPage = 'tasks';
this.updateBreadcrumb([{ text: '任务列表' }]);
const container = document.getElementById('page-container');
container.innerHTML = '';
try {
Components.showLoading();
const data = await API.getTasks({ page, page_size: 20 });
Components.hideLoading();
this.renderTaskList(data);
lucide.createIcons();
} catch (error) {
Components.hideLoading();
Components.showToast('加载任务列表失败: ' + error.message, 'error');
}
},
/**
* 渲染任务列表
*/
renderTaskList(data) {
const container = document.getElementById('task-list-container');
const { items, total, page, page_size } = data;
if (!items.length) {
container.innerHTML = `
${Components.renderEmptyState('inbox', '暂无任务', '还没有执行过任何补货建议任务')}
`;
return;
}
container.innerHTML = `
| 任务编号 |
商家组合 |
状态 |
配件数 |
建议金额 |
基准库销比 |
统计日期 |
执行时长 |
操作 |
${items.map(task => `
|
${task.task_no}
|
${task.dealer_grouping_name || '-'} |
${Components.getStatusBadge(task.status, task.status_text)} |
${task.part_count} |
${Components.formatAmount(task.actual_amount)} |
${Components.formatRatio(task.base_ratio)} |
${task.statistics_date || '-'} |
${Components.formatDuration(task.duration_seconds)} |
查看
|
`).join('')}
`;
// 渲染分页
const paginationContainer = document.getElementById('pagination-container');
paginationContainer.innerHTML = Components.renderPagination(page, total, page_size);
// 绑定分页事件
paginationContainer.querySelectorAll('.pagination-btn[data-page]').forEach(btn => {
btn.addEventListener('click', () => {
const targetPage = parseInt(btn.dataset.page);
if (targetPage && targetPage !== page) {
this.showTaskList(targetPage);
}
});
});
lucide.createIcons();
},
/**
* 显示任务详情页面
*/
async showTaskDetail(taskNo) {
this.currentPage = 'task-detail';
this.currentTaskNo = taskNo;
this.updateBreadcrumb([
{ text: '任务列表', href: '#/tasks' },
{ text: taskNo },
]);
const container = document.getElementById('page-container');
container.innerHTML = '';
try {
Components.showLoading();
const [task, partSummaries, logs] = await Promise.all([
API.getTask(taskNo),
API.getPartSummaries(taskNo, { page: 1, page_size: 100 }).catch(() => ({ items: [], total: 0 })),
API.getTaskLogs(taskNo).catch(() => ({ items: [] })),
]);
Components.hideLoading();
this.renderTaskDetail(task, partSummaries, logs);
lucide.createIcons();
} catch (error) {
Components.hideLoading();
Components.showToast('加载任务详情失败: ' + error.message, 'error');
}
},
/**
* 渲染任务详情
*/
renderTaskDetail(task, partSummaries, logs) {
this._currentLogs = logs;
this._partSummaries = partSummaries;
const container = document.getElementById('task-detail-container');
container.innerHTML = `
返回任务列表
${Components.renderStatCard('package', '建议配件数', task.part_count, 'primary')}
${Components.renderStatCard('dollar-sign', '建议金额', Components.formatAmount(task.actual_amount), 'success')}
${Components.renderStatCard('percent', '基准库销比', Components.formatRatio(task.base_ratio), 'info')}
${Components.renderStatCard('cpu', 'LLM Tokens', task.llm_total_tokens || 0, 'warning')}
`;
// 绑定标签页事件
const tabs = container.querySelectorAll('.tab');
tabs.forEach(tab => {
tab.addEventListener('click', () => {
tabs.forEach(t => t.classList.remove('active'));
tab.classList.add('active');
this.renderTabContent(tab.dataset.tab, task, partSummaries);
});
});
// 默认显示配件汇总
this.renderTabContent('details', task, partSummaries);
},
/**
* 渲染标签页内容
*/
renderTabContent(tabName, task, details) {
const container = document.getElementById('tab-content');
switch (tabName) {
case 'details':
this.renderDetailsTab(container, details);
break;
case 'logs':
this.renderLogsTab(container, this._currentLogs);
break;
case 'report':
this.renderReportTab(container, task.task_no);
break;
case 'info':
this.renderInfoTab(container, task);
break;
}
lucide.createIcons();
},
/**
* 加载配件汇总数据(支持排序和筛选)
*/
async loadPartSummaries() {
if (!this.currentTaskNo) return;
try {
const params = {
page: this.partSummaryFilters.page,
page_size: this.partSummaryFilters.page_size,
sort_by: this.partSummarySort.sortBy,
sort_order: this.partSummarySort.sortOrder,
part_code: this.partSummaryFilters.part_code,
priority: this.partSummaryFilters.priority
};
// 移除空值参数
Object.keys(params).forEach(key => {
if (params[key] === '' || params[key] === null || params[key] === undefined) {
delete params[key];
}
});
const data = await API.getPartSummaries(this.currentTaskNo, params);
this._partSummaries = data;
const container = document.getElementById('tab-content');
if (container) {
this.renderDetailsTab(container, data);
lucide.createIcons();
}
} catch (error) {
Components.showToast('加载配件数据失败: ' + error.message, 'error');
}
},
/**
* 切换配件汇总排序
*/
togglePartSummarySort(field) {
if (this.partSummarySort.sortBy === field) {
this.partSummarySort.sortOrder = this.partSummarySort.sortOrder === 'desc' ? 'asc' : 'desc';
} else {
this.partSummarySort.sortBy = field;
this.partSummarySort.sortOrder = 'desc';
}
this.loadPartSummaries();
},
/**
* 获取排序图标
*/
getSortIcon(field) {
if (this.partSummarySort.sortBy !== field) {
return '';
}
if (this.partSummarySort.sortOrder === 'desc') {
return '';
}
return '';
},
/**
* 渲染配件明细标签页
*/
renderDetailsTab(container, partSummaries) {
const items = partSummaries.items || [];
const { total, page, page_size } = partSummaries;
container.innerHTML = `
|
配件编码 ${this.getSortIcon('part_code')}
|
配件名称 |
成本价 ${this.getSortIcon('cost_price')}
|
总库存 ${this.getSortIcon('total_storage_cnt')}
|
总销量 ${this.getSortIcon('total_avg_sales_cnt')}
|
商家组合库销比 ${this.getSortIcon('group_current_ratio')}
|
计划后库销比 ${this.getSortIcon('group_post_plan_ratio')}
|
门店数 ${this.getSortIcon('shop_count')}
|
需补货门店 ${this.getSortIcon('need_replenishment_shop_count')}
|
总建议数量 ${this.getSortIcon('total_suggest_cnt')}
|
总建议金额 ${this.getSortIcon('total_suggest_amount')}
|
${items.length > 0 ? items.map((item, index) => `
|
|
${item.part_code} |
${item.part_name || '-'} |
${Components.formatAmount(item.cost_price)} |
${Components.formatNumber(item.total_storage_cnt)} |
${Components.formatNumber(item.total_avg_sales_cnt)} |
${Components.getRatioIndicator(item.group_current_ratio, 1.1)} |
${Components.formatRatio(item.group_post_plan_ratio)} |
${item.shop_count} |
${item.need_replenishment_shop_count} |
${item.total_suggest_cnt} |
${Components.formatAmount(item.total_suggest_amount)} |
|
|
`).join('') : `
|
暂无符合条件的配件建议
|
`}
`;
// 渲染分页
const paginationContainer = document.getElementById('part-summary-pagination');
if (paginationContainer) {
paginationContainer.innerHTML = Components.renderPagination(page, total, page_size);
// 绑定分页事件
paginationContainer.querySelectorAll('.pagination-btn[data-page]').forEach(btn => {
btn.addEventListener('click', () => {
const targetPage = parseInt(btn.dataset.page);
if (targetPage && targetPage !== page) {
this.partSummaryFilters.page = targetPage;
this.loadPartSummaries();
}
});
});
}
// 绑定搜索框回车事件
const partCodeInput = document.getElementById('filter-part-code');
if (partCodeInput) {
partCodeInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
App.applyPartFilters();
}
});
}
},
/**
* 切换配件门店展开/收起
*/
async togglePartShops(partCode, index) {
const row = document.getElementById(`shops-${index}`);
const container = document.getElementById(`shops-container-${index}`);
const btn = document.querySelector(`tr[data-index="${index}"] .expand-icon`);
if (row.style.display === 'none') {
row.style.display = 'table-row';
btn.classList.add('expanded');
try {
const data = await API.getPartShopDetails(this.currentTaskNo, partCode);
const partSummary = this._partSummaries.items.find(p => p.part_code === partCode);
this.renderPartShops(container, data.items, partSummary);
lucide.createIcons();
} catch (error) {
container.innerHTML = `加载失败: ${error.message}
`;
}
} else {
row.style.display = 'none';
btn.classList.remove('expanded');
}
},
/**
* 渲染配件门店明细
*/
renderPartShops(container, shops, partSummary) {
if (!shops || shops.length === 0) {
container.innerHTML = '无门店建议数据
';
return;
}
container.innerHTML = `
${partSummary && partSummary.part_decision_reason ? `
配件补货理由:
${partSummary.part_decision_reason}
` : ''}
| 库房 |
有效库存 |
月均销量 |
当前库销比 |
计划后库销比 |
建议数量 |
建议金额 |
建议理由 |
${shops.map(shop => `
| ${shop.shop_name || '-'} |
${Components.formatNumber(shop.valid_storage_cnt)} |
${Components.formatNumber(shop.avg_sales_cnt)} |
${Components.getRatioIndicator(shop.current_ratio, shop.base_ratio)} |
${Components.formatRatio(shop.post_plan_ratio)} |
${shop.suggest_cnt} |
${Components.formatAmount(shop.suggest_amount)} |
${shop.suggestion_reason || '-'}
|
`).join('')}
`;
},
/**
* 渲染执行日志标签页
*/
renderLogsTab(container, logs) {
if (!logs || !logs.items || logs.items.length === 0) {
container.innerHTML = `
${Components.renderEmptyState('activity', '暂无执行日志', '该任务没有执行日志记录')}
`;
return;
}
const items = logs.items;
const totalTokens = items.reduce((sum, item) => sum + (item.llm_tokens || 0), 0);
const totalTime = items.reduce((sum, item) => sum + (item.execution_time_ms || 0), 0);
container.innerHTML = `
${items.map((log, index) => `
${log.status === 1 ? '' :
log.status === 2 ? '' :
''}
${index < items.length - 1 ? '
' : ''}
${log.execution_time_ms ? Components.formatDuration(log.execution_time_ms / 1000) : '-'}
${log.llm_tokens > 0 ? `
${log.llm_tokens} tokens
` : ''}
${log.retry_count > 0 ? `
重试 ${log.retry_count} 次
` : ''}
${log.error_message ? `
${log.error_message}
` : ''}
`).join('')}
`;
},
/**
* 渲染任务信息标签页
*/
renderInfoTab(container, task) {
container.innerHTML = `
${Components.renderInfoItem('任务编号', task.task_no)}
${Components.renderInfoItem('集团ID', task.group_id)}
${Components.renderInfoItem('商家组合ID', task.dealer_grouping_id)}
${Components.renderInfoItem('商家组合名称', task.dealer_grouping_name)}
${Components.renderInfoItem('品牌组合ID', task.brand_grouping_id)}
${Components.renderInfoItem('统计日期', task.statistics_date)}
${Components.renderInfoItem('状态', Components.getStatusBadge(task.status, task.status_text))}
${Components.renderInfoItem('开始时间', task.start_time)}
${Components.renderInfoItem('结束时间', task.end_time)}
${Components.renderInfoItem('执行时长', Components.formatDuration(task.duration_seconds))}
${Components.renderInfoItem('创建时间', task.create_time)}
${Components.renderInfoItem('LLM 提供商', task.llm_provider || '-')}
${Components.renderInfoItem('模型名称', task.llm_model || '-')}
${Components.renderInfoItem('Token 消耗', task.llm_total_tokens)}
${task.error_message ? `
` : ''}
`;
},
};
// DOM 加载完成后初始化
document.addEventListener('DOMContentLoaded', () => {
App.init();
});
// 导出到全局
window.App = App;