/** * 主应用逻辑 */ 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 = `

加载报告失败: ${error.message}

`; 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 += `
    0${num}
    ${title}
    `; }; // 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 => ` `).join('')}
    任务编号 商家组合 状态 配件数 建议金额 执行时间
    ${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)}
    `; }, /** * 显示任务列表页面 */ 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 = `

    任务列表 (${total})

    ${items.map(task => ` `).join('')}
    任务编号 商家组合 状态 配件数 建议金额 基准库销比 统计日期 执行时长 操作
    ${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)} 查看
    `; // 渲染分页 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 = ` 返回任务列表

    ${task.task_no} ${Components.getStatusBadge(task.status, task.status_text)}

    ${task.dealer_grouping_name || '未知商家组合'} ${task.statistics_date || '-'} ${Components.formatDuration(task.duration_seconds)}
    ${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 = `

    配件补货建议 (商家组合维度) - ${total}个配件

    点击表头可排序
    ${items.length > 0 ? items.map((item, index) => ` `).join('') : ` `}
    配件编码 ${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')}
    ${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)}
    暂无符合条件的配件建议
    `; // 渲染分页 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 => ` `).join('')}
    库房 有效库存 月均销量 当前库销比 计划后库销比 建议数量 建议金额 建议理由
    ${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 || '-'}
    `; }, /** * 渲染执行日志标签页 */ 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 = `

    执行日志时间线

    总耗时: ${Components.formatDuration(totalTime / 1000)} | 总Tokens: ${totalTokens}
    ${items.map((log, index) => `
    ${log.status === 1 ? '' : log.status === 2 ? '' : ''}
    ${index < items.length - 1 ? '
    ' : ''}
    ${Components.getStepNameDisplay(log.step_name)} ${Components.getLogStatusBadge(log.status)}
    ${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)}

    LLM 信息

    ${Components.renderInfoItem('LLM 提供商', task.llm_provider || '-')} ${Components.renderInfoItem('模型名称', task.llm_model || '-')} ${Components.renderInfoItem('Token 消耗', task.llm_total_tokens)}
    ${task.error_message ? `

    错误信息

    ${task.error_message}
    ` : ''}
    `; }, }; // DOM 加载完成后初始化 document.addEventListener('DOMContentLoaded', () => { App.init(); }); // 导出到全局 window.App = App;