/** * 星环OPC中心 — Dashboard 监控中心 UI * * 生成 Dashboard 页面的 HTML/CSS,包含: * - 4个关键指标卡片(本月收入、本月利润、现金余额、应收账款) * - 风险预警列表 * - 今日待办列表 * - AI 建议区域 */ export interface DashboardMetrics { monthlyIncome: number; monthlyIncomeChange: number; // 同比变化百分比 monthlyProfit: number; monthlyProfitChange: number; cashBalance: number; monthsOfRunway: number; // 可撑月数 receivables: number; overdueReceivables: number; } export interface DashboardAlert { id: string; title: string; severity: 'critical' | 'warning' | 'info'; category: string; message: string; created_at: string; } export interface DashboardTodo { id: string; title: string; priority: 'urgent' | 'high' | 'normal'; category: string; due_date?: string; description?: string; } export interface DashboardSuggestion { id: string; title: string; description: string; action?: { label: string; url?: string; onclick?: string; }; } export interface DashboardData { metrics: DashboardMetrics; alerts: DashboardAlert[]; todos: DashboardTodo[]; suggestions: DashboardSuggestion[]; } /** * 生成 Dashboard HTML */ export function generateDashboardHtml(data: DashboardData): string { return `
${generateMetricsSection(data.metrics)}
${generateAlertsSection(data.alerts)} ${generateTodosSection(data.todos)}
${generateSuggestionsSection(data.suggestions)}
`; } /** * 生成关键指标卡片区域 */ function generateMetricsSection(metrics: DashboardMetrics): string { const formatMoney = (amount: number) => { if (amount >= 10000) { return (amount / 10000).toFixed(1) + '万'; } return amount.toFixed(0); }; const formatChange = (change: number) => { const sign = change >= 0 ? '+' : ''; const className = change >= 0 ? 'trend-up' : 'trend-down'; const arrow = change >= 0 ? '↑' : '↓'; return `${arrow} ${sign}${change.toFixed(1)}%`; }; return `
本月收入 ${formatChange(metrics.monthlyIncomeChange)}
¥${formatMoney(metrics.monthlyIncome)}
本月利润 ${formatChange(metrics.monthlyProfitChange)}
¥${formatMoney(metrics.monthlyProfit)}
现金余额 ${metrics.monthsOfRunway < 2 ? '预警' : ''}
¥${formatMoney(metrics.cashBalance)}
应收账款 ${metrics.overdueReceivables > 0 ? '逾期' : ''}
¥${formatMoney(metrics.receivables)}
${metrics.overdueReceivables > 0 ? ` ` : ''}
`; } /** * 生成风险预警列表 */ function generateAlertsSection(alerts: DashboardAlert[]): string { if (alerts.length === 0) { return `

风险预警

暂无风险预警

`; } const alertItems = alerts.map(alert => { const severityClass = `alert-${alert.severity}`; const severityIcon = alert.severity === 'critical' ? '⚠️' : alert.severity === 'warning' ? '⚡' : 'ℹ️'; return `
${severityIcon}
${alert.title}
${alert.message}
`; }).join(''); return `

风险预警 ${alerts.length}

${alertItems}
`; } /** * 生成今日待办列表 */ function generateTodosSection(todos: DashboardTodo[]): string { if (todos.length === 0) { return `

今日待办

太棒了!今天没有待办事项

`; } // 按优先级排序 const sortedTodos = [...todos].sort((a, b) => { const priorityOrder = { urgent: 0, high: 1, normal: 2 }; return priorityOrder[a.priority] - priorityOrder[b.priority]; }); const todoItems = sortedTodos.map(todo => { const priorityBadge = todo.priority === 'urgent' ? 'badge-critical' : todo.priority === 'high' ? 'badge-warning' : 'badge-info'; const priorityLabel = todo.priority === 'urgent' ? '紧急' : todo.priority === 'high' ? '重要' : '一般'; return `
${priorityLabel} ${todo.category}
${todo.title}
${todo.description ? `
${todo.description}
` : ''} ${todo.due_date ? `
截止: ${todo.due_date}
` : ''}
`; }).join(''); return `

今日待办 ${todos.length}

${todoItems}
`; } /** * 生成 AI 建议区域 */ function generateSuggestionsSection(suggestions: DashboardSuggestion[]): string { if (suggestions.length === 0) { return `

AI 建议

💡

暂无建议

`; } const suggestionItems = suggestions.map(suggestion => { const actionButton = suggestion.action ? ` ` : ''; return `
${suggestion.title}
${suggestion.description}
${actionButton}
`; }).join(''); return `

💡 AI 建议

${suggestionItems}
`; } /** * Dashboard 专用 CSS */ export function getDashboardCss(): string { return ` /* Dashboard Container */ .dashboard-container { animation: fadeIn 0.3s ease; } .dashboard-two-column { display: grid; grid-template-columns: 1fr 400px; gap: 20px; margin-top: 20px; } @media (max-width: 1200px) { .dashboard-two-column { grid-template-columns: 1fr; } } /* Metrics Cards */ .dashboard-metrics { margin-bottom: 28px; } .stat-card-warning { border-color: #fde68a; background: linear-gradient(135deg, #fffbeb 0%, #ffffff 100%); } .stat-card-footer { margin-top: 12px; font-size: 12px; color: var(--tx3); } .stat-card-footer-warning { color: #92400e; font-weight: 500; } /* Dashboard Sections */ .dashboard-section { margin-bottom: 20px; } /* Empty State */ .dashboard-empty-state { text-align: center; padding: 40px 20px; } .dashboard-empty-icon { font-size: 48px; margin-bottom: 12px; opacity: 0.3; } .dashboard-empty-state p { color: var(--tx3); font-size: 14px; } /* Alerts */ .dashboard-alerts { display: flex; flex-direction: column; gap: 8px; } .alert-banner { position: relative; padding-right: 40px; } .alert-icon { font-size: 18px; line-height: 1; } .alert-content { flex: 1; } .alert-title { font-weight: 600; margin-bottom: 2px; } .alert-message { font-size: 12px; opacity: 0.9; } .alert-dismiss { position: absolute; top: 8px; right: 8px; width: 24px; height: 24px; border: none; background: rgba(0, 0, 0, 0.05); border-radius: 4px; cursor: pointer; font-size: 18px; line-height: 1; color: currentColor; opacity: 0.5; transition: all 0.15s; } .alert-dismiss:hover { opacity: 1; background: rgba(0, 0, 0, 0.1); } /* Todos */ .dashboard-todos { display: flex; flex-direction: column; gap: 12px; } .dashboard-todo-item { padding: 14px; background: #f9fafb; border-radius: 6px; border-left: 3px solid var(--bd); transition: all 0.15s; } .dashboard-todo-item:hover { background: #f3f4f6; border-left-color: var(--tx); } .dashboard-todo-header { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; } .dashboard-todo-category { font-size: 11px; color: var(--tx3); text-transform: uppercase; letter-spacing: 0.05em; } .dashboard-todo-title { font-weight: 600; font-size: 14px; color: var(--tx); margin-bottom: 4px; } .dashboard-todo-desc { font-size: 12px; color: var(--tx2); margin-bottom: 6px; } .dashboard-todo-due { font-size: 11px; color: var(--tx3); } /* Suggestions */ .dashboard-suggestions { display: flex; flex-direction: column; gap: 16px; } .dashboard-suggestion-item { padding: 16px; background: linear-gradient(135deg, #f0f9ff 0%, #ffffff 100%); border-radius: 8px; border: 1px solid #bae6fd; } .dashboard-suggestion-title { font-weight: 600; font-size: 14px; color: var(--tx); margin-bottom: 6px; } .dashboard-suggestion-desc { font-size: 13px; color: var(--tx2); margin-bottom: 12px; line-height: 1.5; } .dashboard-suggestion-action { padding: 8px 16px; background: var(--tx); color: white; border: none; border-radius: 6px; font-size: 13px; font-weight: 500; cursor: pointer; transition: all 0.15s; font-family: var(--font); } .dashboard-suggestion-action:hover { background: var(--pri-l); transform: translateY(-1px); } /* Mobile Responsive */ @media (max-width: 768px) { .stats-grid { grid-template-columns: 1fr; } .dashboard-two-column { grid-template-columns: 1fr; } .main { padding: 20px 16px; } } `; } /** * Dashboard 专用 JavaScript */ export function getDashboardJs(): string { return ` // 忽略预警 async function dismissAlert(alertId) { if (!confirm('确定忽略此预警?')) return; try { const res = await fetch('/opc/admin/api/alerts/' + alertId + '/dismiss', { method: 'POST', headers: { 'Content-Type': 'application/json' } }); if (res.ok) { showToast('预警已忽略', 'ok'); // 移除元素 const alertEl = document.querySelector('[data-alert-id="' + alertId + '"]'); if (alertEl) { alertEl.style.opacity = '0'; setTimeout(() => alertEl.remove(), 200); } } else { showToast('操作失败', 'err'); } } catch (err) { showToast('网络错误', 'err'); } } // 加载 Dashboard 数据 async function loadDashboard() { showLoading(); try { const res = await fetch('/opc/admin/api/dashboard'); if (!res.ok) throw new Error('加载失败'); const data = await res.json(); renderDashboard(data); } catch (err) { document.getElementById('app').innerHTML = '

加载失败: ' + err.message + '

'; } finally { hideLoading(); } } // 渲染 Dashboard function renderDashboard(data) { // Dashboard HTML 已在服务端生成,这里只需要绑定事件 // 实际渲染逻辑在 config-ui.ts 的 renderDashboard 函数中 } `; }