feat: report analysis with date aggregation and user filtering

- Implement date aggregation logic, sum multiple reports on same day
- Add user filtering with multi-select Chip components
- Fix IpcResult data unwrapping bug in frontend
- Add loading state and empty state indicators
- Update test cases to verify aggregation logic

Fixes:
- Timeline confusion: Now sorted by date ascending
- Data duplication: Same-day data automatically aggregated
- No filtering: Added user selector for filtering by users
This commit is contained in:
Misaka_Company
2026-03-26 09:38:29 +08:00
parent 72d452f75e
commit a5f503773e
7 changed files with 454 additions and 37 deletions

View File

@@ -6,7 +6,11 @@ import { ConfigManager } from '../services/config/config-manager'
import { RustfsService } from '../services/rustfs' import { RustfsService } from '../services/rustfs'
import { ListObjectsV2Command, S3Client } from '@aws-sdk/client-s3' import { ListObjectsV2Command, S3Client } from '@aws-sdk/client-s3'
import { SessionManager } from '../services/user/session-manager' import { SessionManager } from '../services/user/session-manager'
import { ReportAnalyzer, type ParsedReportData } from '../services/report/report-analyzer' import {
ReportAnalyzer,
type ParsedReportData,
type AggregatedDailyData
} from '../services/report/report-analyzer'
const log = createLogger('ReportHandler') const log = createLogger('ReportHandler')
@@ -182,7 +186,7 @@ export function registerReportHandlers(): void {
ipcMain.handle( ipcMain.handle(
IPC_CHANNELS.REPORT_ANALYZE_ALL, IPC_CHANNELS.REPORT_ANALYZE_ALL,
async (): Promise<IpcResult<ParsedReportData[]>> => { async (_event, selectedUsernames?: string[]): Promise<IpcResult<AggregatedDailyData[]>> => {
return withErrorHandling(async () => { return withErrorHandling(async () => {
// Check Admin permission // Check Admin permission
const sessionManager = SessionManager.getInstance() const sessionManager = SessionManager.getInstance()
@@ -247,17 +251,91 @@ export function registerReportHandlers(): void {
} }
} }
// Analyze all reports // Analyze reports
const analyzer = new ReportAnalyzer() const analyzer = new ReportAnalyzer()
const analyzedData = analyzer.analyzeReports(reports)
// If specific users are selected, filter by users; otherwise, aggregate all
let analyzedData: AggregatedDailyData[]
if (selectedUsernames && selectedUsernames.length > 0) {
log.info('Analyzing reports for selected users', { usernames: selectedUsernames })
analyzedData = analyzer.analyzeReportsByUsers(reports, selectedUsernames)
} else {
log.info('Analyzing all reports without user filter')
analyzedData = analyzer.analyzeReports(reports)
}
log.info('Report analysis completed', { log.info('Report analysis completed', {
totalReports: reports.length, totalReports: reports.length,
successfulAnalyses: analyzedData.length aggregatedDays: analyzedData.length
}) })
return analyzedData return analyzedData
}, 'report:analyzeAll') }, 'report:analyzeAll')
} }
) )
// Get all unique usernames from reports
ipcMain.handle(IPC_CHANNELS.REPORT_GET_USERNAMES, async (): Promise<IpcResult<string[]>> => {
return withErrorHandling(async () => {
// Check Admin permission
const sessionManager = SessionManager.getInstance()
if (!sessionManager.isAdmin()) {
log.warn('Non-Admin user attempted to access report usernames')
throw new Error('Unauthorized: Admin access required')
}
const rustfs = getRustfsService()
if (!rustfs) {
throw new Error('RustFS is not configured or enabled')
}
const configManager = ConfigManager.getInstance()
const config = configManager.getConfig()
// Create S3Client to list objects
const client = new S3Client({
region: config.rustfs?.region || 'us-east-1',
endpoint: config.rustfs?.endpoint || '',
credentials: {
accessKeyId: config.rustfs?.accessKey || '',
secretAccessKey: config.rustfs?.secretKey || ''
},
forcePathStyle: true
})
log.info('Fetching usernames from reports')
const input = {
Bucket: config.rustfs?.bucket || '',
Prefix: 'reports/cleaner/'
}
const command = new ListObjectsV2Command(input)
const response = await client.send(command)
const reports: Array<{ content: string; filename?: string }> = []
if (response.Contents) {
for (const item of response.Contents) {
if (item.Key && item.Key.endsWith('.md')) {
log.debug('Downloading report for username extraction', { key: item.Key })
const downloadResult = await rustfs.downloadFile(item.Key)
if (downloadResult.success) {
reports.push({
content: downloadResult.content.toString('utf-8'),
filename: item.Key
})
}
}
}
}
const analyzer = new ReportAnalyzer()
const usernames = analyzer.getAllUsernames(reports)
log.info('Username extraction completed', { count: usernames.length })
return usernames
}, 'report:getUsernames')
})
} }

View File

@@ -196,10 +196,12 @@ No execution summary section here`
const results = analyzer.analyzeReports(reports) const results = analyzer.analyzeReports(reports)
expect(results.length).toBe(2) expect(results.length).toBe(2)
expect(results[0].username).toBe('user1')
expect(results[0].date).toBe('2026-03-25') expect(results[0].date).toBe('2026-03-25')
expect(results[1].username).toBe('user2') expect(results[0].ordersProcessed).toBe(100)
expect(results[0].executionCount).toBe(1)
expect(results[1].date).toBe('2026-03-26') expect(results[1].date).toBe('2026-03-26')
expect(results[1].ordersProcessed).toBe(200)
expect(results[1].executionCount).toBe(1)
}) })
it('should skip invalid reports in batch', () => { it('should skip invalid reports in batch', () => {
@@ -228,7 +230,51 @@ No execution summary section here`
const results = analyzer.analyzeReports(reports) const results = analyzer.analyzeReports(reports)
expect(results.length).toBe(1) expect(results.length).toBe(1)
expect(results[0].username).toBe('valid') expect(results[0].ordersProcessed).toBe(50)
})
it('should aggregate data by date', () => {
const reports = [
{
content: `## 执行摘要
| **操作用户** | \`user1\` |
| **处理订单数** | \`100\` |
| **删除物料数** | \`20\` |
| **跳过物料数** | \`10\` |
| **错误数量** | \`1\` |
| **重试订单数** | \`5\` |
| **成功重试数** | \`4\` |
| **执行耗时** | \`1 分\` |
| **执行时间** | \`2026-03-25 08:00:00\` |
`,
filename: 'report1-2026-03-25.md'
},
{
content: `## 执行摘要
| **操作用户** | \`user2\` |
| **处理订单数** | \`150\` |
| **删除物料数** | \`30\` |
| **跳过物料数** | \`15\` |
| **错误数量** | \`2\` |
| **重试订单数** | \`8\` |
| **成功重试数** | \`6\` |
| **执行耗时** | \`2 分\` |
| **执行时间** | \`2026-03-25 14:00:00\` |
`,
filename: 'report2-2026-03-25.md'
}
]
const results = analyzer.analyzeReports(reports)
expect(results.length).toBe(1)
expect(results[0].date).toBe('2026-03-25')
expect(results[0].ordersProcessed).toBe(250) // 100 + 150
expect(results[0].materialsDeleted).toBe(50) // 20 + 30
expect(results[0].executionCount).toBe(2)
expect(results[0].avgDurationSeconds).toBe(90) // (60 + 120) / 2
}) })
it('should use filename date to override content date', () => { it('should use filename date to override content date', () => {

View File

@@ -3,7 +3,7 @@ import { createLogger } from '../logger'
const log = createLogger('ReportAnalyzer') const log = createLogger('ReportAnalyzer')
/** /**
* Parsed report data structure * Parsed report data structure - 单条报告记录
*/ */
export interface ParsedReportData { export interface ParsedReportData {
/** 报告日期 (从文件名或内容提取) */ /** 报告日期 (从文件名或内容提取) */
@@ -26,6 +26,30 @@ export interface ParsedReportData {
durationSeconds: number durationSeconds: number
} }
/**
* Aggregated daily data - 按日期聚合后的数据
*/
export interface AggregatedDailyData {
/** 日期 */
date: string
/** 处理订单数总和 */
ordersProcessed: number
/** 删除物料数总和 */
materialsDeleted: number
/** 跳过物料数总和 */
materialsSkipped: number
/** 错误数量总和 */
errorCount: number
/** 重试订单数总和 */
retriedOrders: number
/** 成功重试数总和 */
successfulRetries: number
/** 执行耗时平均值 (秒) */
avgDurationSeconds: number
/** 执行次数 */
executionCount: number
}
/** /**
* ReportAnalyzer - 解析和分析 ERP 物料清理执行报告 * ReportAnalyzer - 解析和分析 ERP 物料清理执行报告
*/ */
@@ -85,12 +109,13 @@ export class ReportAnalyzer {
} }
/** /**
* 批量解析多个报告 * 批量解析多个报告并按日期聚合
* @param reports 报告元数据和内容列表 * @param reports 报告元数据和内容列表
* @returns 解析后的数据列表 (自动跳过解析失败的报告) * @returns 按日期聚合后的数据,按日期升序排序
*/ */
analyzeReports(reports: Array<{ content: string; filename?: string }>): ParsedReportData[] { analyzeReports(reports: Array<{ content: string; filename?: string }>): AggregatedDailyData[] {
const results: ParsedReportData[] = [] // 第一步:解析所有报告
const parsedReports: ParsedReportData[] = []
for (const report of reports) { for (const report of reports) {
const parsed = this.parseMarkdownReport(report.content) const parsed = this.parseMarkdownReport(report.content)
@@ -102,13 +127,153 @@ export class ReportAnalyzer {
parsed.date = dateFromFilename parsed.date = dateFromFilename
} }
} }
results.push(parsed) parsedReports.push(parsed)
} else { } else {
log.warn('Skipping report due to parse failure', { filename: report.filename }) log.warn('Skipping report due to parse failure', { filename: report.filename })
} }
} }
return results // 第二步:按日期聚合数据
const dateMap = new Map<string, ParsedReportData[]>()
for (const report of parsedReports) {
const existing = dateMap.get(report.date) || []
existing.push(report)
dateMap.set(report.date, existing)
}
// 第三步:计算每天的聚合数据
const aggregated: AggregatedDailyData[] = []
for (const [date, reportsOnDate] of dateMap.entries()) {
const totalOrdersProcessed = reportsOnDate.reduce((sum, r) => sum + r.ordersProcessed, 0)
const totalMaterialsDeleted = reportsOnDate.reduce((sum, r) => sum + r.materialsDeleted, 0)
const totalMaterialsSkipped = reportsOnDate.reduce((sum, r) => sum + r.materialsSkipped, 0)
const totalErrorCount = reportsOnDate.reduce((sum, r) => sum + r.errorCount, 0)
const totalRetriedOrders = reportsOnDate.reduce((sum, r) => sum + r.retriedOrders, 0)
const totalSuccessfulRetries = reportsOnDate.reduce((sum, r) => sum + r.successfulRetries, 0)
const totalDurationSeconds = reportsOnDate.reduce((sum, r) => sum + r.durationSeconds, 0)
const executionCount = reportsOnDate.length
aggregated.push({
date,
ordersProcessed: totalOrdersProcessed,
materialsDeleted: totalMaterialsDeleted,
materialsSkipped: totalMaterialsSkipped,
errorCount: totalErrorCount,
retriedOrders: totalRetriedOrders,
successfulRetries: totalSuccessfulRetries,
avgDurationSeconds: Math.round(totalDurationSeconds / executionCount),
executionCount
})
}
// 第四步:按日期升序排序(从早到晚),确保图表时间轴正确
aggregated.sort((a, b) => {
const dateA = new Date(a.date)
const dateB = new Date(b.date)
return dateA.getTime() - dateB.getTime()
})
return aggregated
}
/**
* 获取所有唯一的用户名列表
* @param reports 报告元数据和内容列表
* @returns 按字母顺序排序的用户名列表
*/
getAllUsernames(reports: Array<{ content: string; filename?: string }>): string[] {
const usernames = new Set<string>()
for (const report of reports) {
const parsed = this.parseMarkdownReport(report.content)
if (parsed && parsed.username) {
usernames.add(parsed.username)
}
}
return Array.from(usernames).sort()
}
/**
* 按用户筛选报告并返回聚合数据
* @param reports 报告元数据和内容列表
* @param selectedUsernames 选中的用户名列表
* @returns 按日期聚合后的数据,仅包含选中用户的报告
*/
analyzeReportsByUsers(
reports: Array<{ content: string; filename?: string }>,
selectedUsernames: string[]
): AggregatedDailyData[] {
// 如果未选择任何用户,返回空数组
if (selectedUsernames.length === 0) {
return []
}
// 第一步:解析所有报告
const parsedReports: ParsedReportData[] = []
for (const report of reports) {
const parsed = this.parseMarkdownReport(report.content)
if (parsed) {
// 如果有文件名,尝试从文件名提取日期覆盖
if (report.filename) {
const dateFromFilename = this.extractDateFromFilename(report.filename)
if (dateFromFilename) {
parsed.date = dateFromFilename
}
}
parsedReports.push(parsed)
} else {
log.warn('Skipping report due to parse failure', { filename: report.filename })
}
}
// 第二步:按选中的用户过滤
const filteredReports = parsedReports.filter((report) =>
selectedUsernames.includes(report.username)
)
// 第三步:按日期聚合过滤后的数据
const dateMap = new Map<string, ParsedReportData[]>()
for (const report of filteredReports) {
const existing = dateMap.get(report.date) || []
existing.push(report)
dateMap.set(report.date, existing)
}
// 第四步:计算每天的聚合数据
const aggregated: AggregatedDailyData[] = []
for (const [date, reportsOnDate] of dateMap.entries()) {
const totalOrdersProcessed = reportsOnDate.reduce((sum, r) => sum + r.ordersProcessed, 0)
const totalMaterialsDeleted = reportsOnDate.reduce((sum, r) => sum + r.materialsDeleted, 0)
const totalMaterialsSkipped = reportsOnDate.reduce((sum, r) => sum + r.materialsSkipped, 0)
const totalErrorCount = reportsOnDate.reduce((sum, r) => sum + r.errorCount, 0)
const totalRetriedOrders = reportsOnDate.reduce((sum, r) => sum + r.retriedOrders, 0)
const totalSuccessfulRetries = reportsOnDate.reduce((sum, r) => sum + r.successfulRetries, 0)
const totalDurationSeconds = reportsOnDate.reduce((sum, r) => sum + r.durationSeconds, 0)
const executionCount = reportsOnDate.length
aggregated.push({
date,
ordersProcessed: totalOrdersProcessed,
materialsDeleted: totalMaterialsDeleted,
materialsSkipped: totalMaterialsSkipped,
errorCount: totalErrorCount,
retriedOrders: totalRetriedOrders,
successfulRetries: totalSuccessfulRetries,
avgDurationSeconds: Math.round(totalDurationSeconds / executionCount),
executionCount
})
}
// 第五步:按日期升序排序(从早到晚),确保图表时间轴正确
aggregated.sort((a, b) => {
const dateA = new Date(a.date)
const dateB = new Date(b.date)
return dateA.getTime() - dateB.getTime()
})
return aggregated
} }
/** /**

View File

@@ -201,21 +201,27 @@ export interface ReportAPI {
download: (key: string) => Promise<IpcResult<string>> download: (key: string) => Promise<IpcResult<string>>
/** /**
* Analyze all reports and return parsed data (Admin only) * Analyze all reports and return aggregated daily data (Admin only)
* @param selectedUsernames - Optional array of usernames to filter by
*/ */
analyzeAll: () => Promise< analyzeAll: (selectedUsernames?: string[]) => Promise<
IpcResult< IpcResult<
{ {
date: string date: string
username: string
ordersProcessed: number ordersProcessed: number
materialsDeleted: number materialsDeleted: number
materialsSkipped: number materialsSkipped: number
errorCount: number errorCount: number
retriedOrders: number retriedOrders: number
successfulRetries: number successfulRetries: number
durationSeconds: number avgDurationSeconds: number
executionCount: number
}[] }[]
> >
> >
/**
* Get all unique usernames from reports (Admin only)
*/
getUsernames: () => Promise<IpcResult<string[]>>
} }

View File

@@ -72,7 +72,9 @@ export const reportApi = {
listAll: () => invokeIpc(IPC_CHANNELS.REPORT_LIST_ALL), listAll: () => invokeIpc(IPC_CHANNELS.REPORT_LIST_ALL),
listByUser: (username: string) => invokeIpc(IPC_CHANNELS.REPORT_LIST_BY_USER, username), listByUser: (username: string) => invokeIpc(IPC_CHANNELS.REPORT_LIST_BY_USER, username),
download: (key: string) => invokeIpc(IPC_CHANNELS.REPORT_DOWNLOAD, key), download: (key: string) => invokeIpc(IPC_CHANNELS.REPORT_DOWNLOAD, key),
analyzeAll: () => invokeIpc(IPC_CHANNELS.REPORT_ANALYZE_ALL) analyzeAll: (selectedUsernames?: string[]) =>
invokeIpc(IPC_CHANNELS.REPORT_ANALYZE_ALL, selectedUsernames),
getUsernames: () => invokeIpc<string[]>(IPC_CHANNELS.REPORT_GET_USERNAMES)
} as const } as const
export const updateApi = { export const updateApi = {

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react' import React, { useState, useEffect } from 'react'
import { X, FileText, Loader2, AlertCircle, RefreshCw } from 'lucide-react' import { X, FileText, Loader2, AlertCircle, RefreshCw, Users } from 'lucide-react'
import { import {
LineChart, LineChart,
Line, Line,
@@ -11,16 +11,16 @@ import {
ResponsiveContainer ResponsiveContainer
} from 'recharts' } from 'recharts'
interface ParsedReportData { interface AggregatedDailyData {
date: string date: string
username: string
ordersProcessed: number ordersProcessed: number
materialsDeleted: number materialsDeleted: number
materialsSkipped: number materialsSkipped: number
errorCount: number errorCount: number
retriedOrders: number retriedOrders: number
successfulRetries: number successfulRetries: number
durationSeconds: number avgDurationSeconds: number
executionCount: number
} }
interface ReportAnalysisDialogProps { interface ReportAnalysisDialogProps {
@@ -42,7 +42,7 @@ export const ReportAnalysisDialog: React.FC<ReportAnalysisDialogProps> = ({
{ key: 'errorCount', label: '错误数量', color: '#ef4444' }, { key: 'errorCount', label: '错误数量', color: '#ef4444' },
{ key: 'retriedOrders', label: '重试订单数', color: '#8b5cf6' }, { key: 'retriedOrders', label: '重试订单数', color: '#8b5cf6' },
{ key: 'successfulRetries', label: '成功重试数', color: '#06b6d4' }, { key: 'successfulRetries', label: '成功重试数', color: '#06b6d4' },
{ key: 'durationSeconds', label: '执行耗时', color: '#ec4899' } { key: 'avgDurationSeconds', label: '执行耗时', color: '#ec4899' }
] ]
// State: selected metrics (default all) // State: selected metrics (default all)
@@ -56,33 +56,92 @@ export const ReportAnalysisDialog: React.FC<ReportAnalysisDialogProps> = ({
} }
// Data state // Data state
const [reportData, setReportData] = useState<ParsedReportData[]>([]) const [reportData, setReportData] = useState<AggregatedDailyData[]>([])
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
// Fetch data when dialog opens // User filter state
const [allUsernames, setAllUsernames] = useState<string[]>([])
const [selectedUsernames, setSelectedUsernames] = useState<string[]>([])
const [loadingUsers, setLoadingUsers] = useState(true)
// Fetch usernames when dialog opens
useEffect(() => { useEffect(() => {
if (!isOpen || !isAdmin) return if (!isOpen || !isAdmin) return
const fetchUsernames = async () => {
setLoadingUsers(true)
try {
console.log('[ReportAnalysis] Fetching usernames...')
const result = await window.electron.report.getUsernames()
console.log('[ReportAnalysis] Received usernames:', result)
// IpcResult shape: { success: boolean, data?: string[] }
if (result && Array.isArray(result.data)) {
const usernames = result.data
console.log('[ReportAnalysis] Setting usernames:', usernames)
setAllUsernames(usernames)
setSelectedUsernames(usernames) // Select all users by default
} else {
console.warn('[ReportAnalysis] Unexpected result format:', result)
}
} catch (err) {
console.error('Failed to fetch usernames', err)
} finally {
setLoadingUsers(false)
}
}
fetchUsernames()
}, [isOpen, isAdmin])
// Fetch data when dialog opens or user selection changes
useEffect(() => {
if (!isOpen || !isAdmin || allUsernames.length === 0) return
const fetchData = async () => { const fetchData = async () => {
setLoading(true) setLoading(true)
setError(null) setError(null)
try { try {
const result = await window.electron.report.analyzeAll() console.log('[ReportAnalysis] Fetching data for users:', selectedUsernames)
if (result.success && result.data) { const result = await window.electron.report.analyzeAll(selectedUsernames)
setReportData(result.data)
// IpcResult shape: { success: boolean, data?: AggregatedDailyData[] }
if (result && Array.isArray(result.data)) {
const data = result.data
console.log('[ReportAnalysis] Received data:', data)
setReportData(data)
} else { } else {
setError(result.error || '加载失败') console.warn('[ReportAnalysis] Unexpected result format:', result)
setError('数据格式错误')
} }
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : '未知错误') console.error('Failed to fetch analysis data', err)
setError(err instanceof Error ? err.message : '加载失败')
} finally { } finally {
setLoading(false) setLoading(false)
} }
} }
fetchData() fetchData()
}, [isOpen, isAdmin]) }, [isOpen, isAdmin, selectedUsernames, allUsernames.length])
// Toggle user selection
const toggleUser = (username: string) => {
setSelectedUsernames((prev) =>
prev.includes(username) ? prev.filter((u) => u !== username) : [...prev, username]
)
}
// Select all users
const selectAllUsers = () => {
setSelectedUsernames(allUsernames)
}
// Deselect all users
const deselectAllUsers = () => {
setSelectedUsernames([])
}
// Transform data for chart // Transform data for chart
const chartData = reportData.map((item) => ({ const chartData = reportData.map((item) => ({
@@ -117,7 +176,8 @@ export const ReportAnalysisDialog: React.FC<ReportAnalysisDialogProps> = ({
{/* Controls */} {/* Controls */}
<div className="px-6 py-4 border-b border-slate-200 bg-white flex-shrink-0"> <div className="px-6 py-4 border-b border-slate-200 bg-white flex-shrink-0">
<div className="flex items-center gap-4"> {/* Metrics Selection */}
<div className="flex items-center gap-4 mb-4">
<label className="text-sm font-medium text-slate-700 flex-shrink-0">:</label> <label className="text-sm font-medium text-slate-700 flex-shrink-0">:</label>
<div className="flex flex-wrap gap-2 flex-1"> <div className="flex flex-wrap gap-2 flex-1">
{numericMetrics.map((metric) => ( {numericMetrics.map((metric) => (
@@ -138,6 +198,64 @@ export const ReportAnalysisDialog: React.FC<ReportAnalysisDialogProps> = ({
))} ))}
</div> </div>
</div> </div>
{/* User Filter */}
<div className="flex items-center gap-4">
<label className="text-sm font-medium text-slate-700 flex-shrink-0 flex items-center gap-1">
<Users size={16} />
:
</label>
{loadingUsers ? (
<div className="flex items-center gap-2 text-sm text-slate-500">
<Loader2 size={14} className="animate-spin" />
<span>...</span>
</div>
) : allUsernames.length === 0 ? (
<div className="flex items-center gap-2 text-sm text-slate-500">
<AlertCircle size={14} />
<span></span>
{reportData.length > 0 && (
<>
<span className="text-slate-400">|</span>
<span> {reportData.length} </span>
</>
)}
</div>
) : (
<>
<div className="flex flex-wrap gap-2 flex-1">
{allUsernames.map((username) => (
<button
key={username}
onClick={() => toggleUser(username)}
className={`px-3 py-1.5 rounded-full text-sm font-medium transition-all border ${
selectedUsernames.includes(username)
? 'bg-blue-600 text-white border-blue-600 hover:bg-blue-700'
: 'bg-white text-slate-600 border-slate-300 hover:bg-slate-50'
}`}
>
{username}
</button>
))}
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<button
onClick={selectAllUsers}
className="text-xs text-blue-600 hover:text-blue-800 font-medium px-2 py-1 rounded hover:bg-blue-50"
>
</button>
<span className="text-slate-300">|</span>
<button
onClick={deselectAllUsers}
className="text-xs text-slate-600 hover:text-slate-800 font-medium px-2 py-1 rounded hover:bg-slate-100"
>
</button>
</div>
</>
)}
</div>
</div> </div>
{/* Content */} {/* Content */}
@@ -162,12 +280,13 @@ export const ReportAnalysisDialog: React.FC<ReportAnalysisDialogProps> = ({
setLoading(true) setLoading(true)
setError(null) setError(null)
window.electron.report window.electron.report
.analyzeAll() .analyzeAll(selectedUsernames)
.then((result) => { .then((result) => {
if (result.success && result.data) { // IpcResult shape: { success: boolean, data?: AggregatedDailyData[] }
if (result && Array.isArray(result.data)) {
setReportData(result.data) setReportData(result.data)
} else { } else {
setError(result.error || '加载失败') setError('加载失败')
} }
}) })
.catch((err) => { .catch((err) => {

View File

@@ -98,6 +98,7 @@ export const IPC_CHANNELS = {
REPORT_LIST_BY_USER: 'report:listByUser', REPORT_LIST_BY_USER: 'report:listByUser',
REPORT_DOWNLOAD: 'report:download', REPORT_DOWNLOAD: 'report:download',
REPORT_ANALYZE_ALL: 'report:analyzeAll', REPORT_ANALYZE_ALL: 'report:analyzeAll',
REPORT_GET_USERNAMES: 'report:getUsernames',
// Update // Update
UPDATE_GET_STATUS: 'update:getStatus', UPDATE_GET_STATUS: 'update:getStatus',