/** * 主应用逻辑 */ 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(); }, /** * 初始化应用 */ 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 '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;