mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-17 19:47:08 +08:00
feat: 优化 AI 日报结果中心信息架构
This commit is contained in:
+9
-1
@@ -10,7 +10,11 @@ import { StickerService } from './sticker-service'
|
||||
import { parseMessageContent } from './message-parser'
|
||||
import { ImageDecryptService } from './image-decrypt-service'
|
||||
import { exportGroupReport } from './group-report-service'
|
||||
import { listGeneratedReports, saveGeneratedReport } from './report-history-service'
|
||||
import {
|
||||
deleteGeneratedReport,
|
||||
listGeneratedReports,
|
||||
saveGeneratedReport
|
||||
} from './report-history-service'
|
||||
import type { GroupReportExportRequest } from '../shared/group-report'
|
||||
import type { SaveGeneratedReportRequest } from '../shared/report-history'
|
||||
import { DatabaseKeyStore } from './database-key-store'
|
||||
@@ -343,6 +347,10 @@ app.whenReady().then(async () => {
|
||||
return saveGeneratedReport(request)
|
||||
})
|
||||
|
||||
ipcMain.handle('report:deleteGenerated', async (_, reportId: string) => {
|
||||
return deleteGeneratedReport(reportId)
|
||||
})
|
||||
|
||||
ipcMain.handle('report:reveal', async (_, filePath: string) => {
|
||||
try {
|
||||
shell.showItemInFolder(filePath)
|
||||
|
||||
@@ -3,6 +3,7 @@ import { promises as fs } from 'fs'
|
||||
import path from 'path'
|
||||
import type {
|
||||
GeneratedReportRecord,
|
||||
DeleteGeneratedReportResult,
|
||||
ReportAssetStatus,
|
||||
ReportHistoryResult,
|
||||
SaveGeneratedReportRequest,
|
||||
@@ -166,3 +167,35 @@ export async function saveGeneratedReport(
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) }
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteGeneratedReport(reportId: string): Promise<DeleteGeneratedReportResult> {
|
||||
try {
|
||||
const jsonFiles = await walkJsonFiles(getReportsRoot())
|
||||
for (const jsonPath of jsonFiles) {
|
||||
try {
|
||||
const content = await fs.readFile(jsonPath, 'utf8')
|
||||
const record = JSON.parse(content) as GeneratedReportRecord
|
||||
if (record.id !== reportId) continue
|
||||
|
||||
const paths = [record.htmlPath, record.pngPath, jsonPath].filter(
|
||||
(filePath): filePath is string => Boolean(filePath)
|
||||
)
|
||||
await Promise.all(
|
||||
paths.map(async (filePath) => {
|
||||
try {
|
||||
await fs.unlink(filePath)
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
|
||||
}
|
||||
})
|
||||
)
|
||||
return { success: true, deletedId: reportId }
|
||||
} catch (error) {
|
||||
console.warn(`[ReportHistory] skip invalid report record while deleting: ${jsonPath}`, error)
|
||||
}
|
||||
}
|
||||
return { success: false, error: '未找到要删除的日报记录' }
|
||||
} catch (error) {
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) }
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+2
@@ -2,6 +2,7 @@ import { ElectronAPI } from '@electron-toolkit/preload'
|
||||
import { Contact, Message } from '../shared/types'
|
||||
import { GroupReportExportRequest, GroupReportExportResult } from '../shared/group-report'
|
||||
import {
|
||||
DeleteGeneratedReportResult,
|
||||
ReportHistoryResult,
|
||||
SaveGeneratedReportRequest,
|
||||
SaveGeneratedReportResult
|
||||
@@ -93,6 +94,7 @@ declare global {
|
||||
saveGeneratedReport: (
|
||||
request: SaveGeneratedReportRequest
|
||||
) => Promise<SaveGeneratedReportResult>
|
||||
deleteGeneratedReport: (reportId: string) => Promise<DeleteGeneratedReportResult>
|
||||
revealGroupReport: (filePath: string) => Promise<{ success: boolean; error?: string }>
|
||||
getSavedDbKey: () => Promise<{ success: boolean; key?: string; error?: string }>
|
||||
autoGetDbKey: () => Promise<{
|
||||
|
||||
@@ -40,6 +40,7 @@ const api = {
|
||||
listGeneratedReports: () => ipcRenderer.invoke('report:listGenerated'),
|
||||
saveGeneratedReport: (request: SaveGeneratedReportRequest) =>
|
||||
ipcRenderer.invoke('report:saveGenerated', request),
|
||||
deleteGeneratedReport: (reportId: string) => ipcRenderer.invoke('report:deleteGenerated', reportId),
|
||||
revealGroupReport: (filePath: string) => ipcRenderer.invoke('report:reveal', filePath),
|
||||
getSavedDbKey: () => ipcRenderer.invoke('key:getSavedDbKey'),
|
||||
autoGetDbKey: () => ipcRenderer.invoke('key:autoGetDbKey'),
|
||||
|
||||
@@ -216,7 +216,9 @@ function App(): React.ReactElement {
|
||||
}
|
||||
const reports = result.reports || []
|
||||
setGeneratedReports(reports)
|
||||
setSelectedReportId((current) => current || reports[0]?.id || null)
|
||||
setSelectedReportId((current) =>
|
||||
current && reports.some((report) => report.id === current) ? current : null
|
||||
)
|
||||
} catch (error) {
|
||||
setReportNotice(error instanceof Error ? error.message : String(error))
|
||||
window.setTimeout(() => setReportNotice(''), 3200)
|
||||
@@ -783,7 +785,8 @@ function App(): React.ReactElement {
|
||||
}
|
||||
if (page === 'report') {
|
||||
setReportWorkspaceView('result')
|
||||
if (!selectedReportId && generatedReports[0]) setSelectedReportId(generatedReports[0].id)
|
||||
setSelectedReportId(null)
|
||||
setSelectedReportImageSize(null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -876,10 +879,9 @@ function App(): React.ReactElement {
|
||||
])
|
||||
|
||||
const selectedReport =
|
||||
generatedReports.find((report) => report.id === selectedReportId) || generatedReports[0] || null
|
||||
generatedReports.find((report) => report.id === selectedReportId) || null
|
||||
|
||||
const openReportResult = (): void => {
|
||||
if (generatedReports[0] && !selectedReportId) setSelectedReportId(generatedReports[0].id)
|
||||
setReportWorkspaceView('result')
|
||||
}
|
||||
|
||||
@@ -910,6 +912,20 @@ function App(): React.ReactElement {
|
||||
return window.api.revealGroupReport(filePath)
|
||||
}
|
||||
|
||||
const handleDeleteReport = async (
|
||||
reportId: string
|
||||
): Promise<{ success: boolean; error?: string }> => {
|
||||
const result = await window.api.deleteGeneratedReport(reportId)
|
||||
if (!result.success) return { success: false, error: result.error || '删除日报失败' }
|
||||
|
||||
setGeneratedReports((current) => current.filter((report) => report.id !== reportId))
|
||||
if (selectedReportId === reportId) {
|
||||
setSelectedReportId(null)
|
||||
setSelectedReportImageSize(null)
|
||||
}
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
const renderPlaceholderPage = (page: Exclude<AppPage, 'archive' | 'report'>): React.ReactElement => {
|
||||
const labels: Record<Exclude<AppPage, 'archive' | 'report'>, string> = {
|
||||
search: '检索',
|
||||
@@ -963,7 +979,7 @@ function App(): React.ReactElement {
|
||||
<div className="report-center-page">
|
||||
<ReportHistorySidebar
|
||||
reports={generatedReports}
|
||||
selectedReportId={selectedReport?.id || null}
|
||||
selectedReportId={selectedReportId}
|
||||
selfInfo={selfInfo}
|
||||
dbReady={isAuthenticated}
|
||||
onSelectReport={(reportId) => {
|
||||
@@ -971,10 +987,12 @@ function App(): React.ReactElement {
|
||||
setSelectedReportImageSize(null)
|
||||
}}
|
||||
onCreateReport={openReportConfigure}
|
||||
onDeleteReport={handleDeleteReport}
|
||||
onOpenSettings={() => setShowSettings(true)}
|
||||
/>
|
||||
<ReportViewer
|
||||
report={selectedReport}
|
||||
hasReports={generatedReports.length > 0}
|
||||
onBackToConfigure={openReportConfigure}
|
||||
onRegenerate={handleRegenerateReport}
|
||||
onCopyImage={handleCopyReportImage}
|
||||
|
||||
@@ -3526,28 +3526,16 @@ body {
|
||||
font: 13px/18px var(--wxex-font);
|
||||
}
|
||||
|
||||
.report-history-filters {
|
||||
display: grid;
|
||||
.report-history-create {
|
||||
flex: 0 0 auto;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 6px;
|
||||
padding: 0 14px 10px;
|
||||
}
|
||||
|
||||
.report-history-filters button {
|
||||
height: 28px;
|
||||
border: 1px solid var(--wxex-border);
|
||||
border-radius: var(--wxex-radius-sm);
|
||||
background: var(--wxex-bg-elevated);
|
||||
color: var(--wxex-text-secondary);
|
||||
height: 36px;
|
||||
margin: 0 14px 12px;
|
||||
border: 1px solid var(--wxex-brand);
|
||||
border-radius: var(--wxex-radius-md);
|
||||
background: var(--wxex-brand);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font: 12px/16px var(--wxex-font);
|
||||
}
|
||||
|
||||
.report-history-filters button.active {
|
||||
border-color: var(--wxex-brand);
|
||||
background: var(--wxex-brand-soft);
|
||||
color: var(--wxex-brand);
|
||||
font: 700 13px/18px var(--wxex-font);
|
||||
}
|
||||
|
||||
.report-history-list {
|
||||
@@ -3557,6 +3545,23 @@ body {
|
||||
padding: 0 10px 12px;
|
||||
}
|
||||
|
||||
.report-history-list-title {
|
||||
padding: 4px 4px 8px;
|
||||
color: var(--wxex-text-secondary);
|
||||
font: 700 13px/18px var(--wxex-font);
|
||||
}
|
||||
|
||||
.report-history-group {
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
|
||||
.report-history-group h3 {
|
||||
margin: 0;
|
||||
padding: 6px 4px;
|
||||
color: var(--wxex-text-muted);
|
||||
font: 700 12px/16px var(--wxex-font);
|
||||
}
|
||||
|
||||
.report-history-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
@@ -3614,6 +3619,7 @@ body {
|
||||
|
||||
.report-history-text {
|
||||
display: grid;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
gap: 2px;
|
||||
}
|
||||
@@ -3637,6 +3643,28 @@ body {
|
||||
font: normal 12px/16px var(--wxex-font);
|
||||
}
|
||||
|
||||
.report-history-delete {
|
||||
display: grid;
|
||||
width: 38px;
|
||||
height: 26px;
|
||||
flex: 0 0 auto;
|
||||
place-items: center;
|
||||
align-self: center;
|
||||
border: 1px solid rgba(198, 72, 72, 0.22);
|
||||
border-radius: var(--wxex-radius-sm);
|
||||
background: rgba(198, 72, 72, 0.06);
|
||||
color: #a64242;
|
||||
cursor: pointer;
|
||||
font: 600 12px/16px var(--wxex-font);
|
||||
opacity: 0;
|
||||
transition: opacity 0.12s ease;
|
||||
}
|
||||
|
||||
.report-history-item:hover .report-history-delete,
|
||||
.report-history-delete:focus {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.report-history-empty {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
@@ -3664,6 +3692,65 @@ body {
|
||||
border-top: 1px solid var(--wxex-border);
|
||||
}
|
||||
|
||||
.report-delete-confirm {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 30;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: rgba(32, 39, 36, 0.28);
|
||||
}
|
||||
|
||||
.report-delete-confirm-card {
|
||||
display: grid;
|
||||
width: min(360px, calc(100vw - 48px));
|
||||
gap: 12px;
|
||||
padding: 18px;
|
||||
border: 1px solid var(--wxex-border);
|
||||
border-radius: var(--wxex-radius-lg);
|
||||
background: var(--wxex-bg-main);
|
||||
box-shadow: var(--wxex-shadow-popover);
|
||||
}
|
||||
|
||||
.report-delete-confirm-card h2 {
|
||||
margin: 0;
|
||||
color: var(--wxex-text-primary);
|
||||
font: 700 18px/24px var(--wxex-font);
|
||||
}
|
||||
|
||||
.report-delete-confirm-card p {
|
||||
margin: 0;
|
||||
color: var(--wxex-text-secondary);
|
||||
font: 13px/20px var(--wxex-font);
|
||||
}
|
||||
|
||||
.report-delete-confirm-card > div {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.report-delete-confirm-card button {
|
||||
min-height: 32px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid var(--wxex-border);
|
||||
border-radius: var(--wxex-radius-md);
|
||||
background: var(--wxex-bg-elevated);
|
||||
color: var(--wxex-text-primary);
|
||||
cursor: pointer;
|
||||
font: 600 13px/18px var(--wxex-font);
|
||||
}
|
||||
|
||||
.report-delete-confirm-card button.danger {
|
||||
border-color: #a64242;
|
||||
background: #a64242;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.report-delete-error {
|
||||
color: #a64242 !important;
|
||||
}
|
||||
|
||||
.report-viewer {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
@@ -3800,12 +3887,33 @@ body {
|
||||
align-self: center;
|
||||
justify-self: center;
|
||||
max-width: 360px;
|
||||
gap: 10px;
|
||||
gap: 12px;
|
||||
margin: auto;
|
||||
color: var(--wxex-text-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.report-center-empty-icon {
|
||||
display: grid;
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
place-items: center;
|
||||
justify-self: center;
|
||||
border: 1px solid rgba(104, 110, 220, 0.18);
|
||||
border-radius: 18px;
|
||||
background: var(--wxex-ai-soft);
|
||||
color: var(--wxex-ai);
|
||||
}
|
||||
|
||||
.report-center-empty-icon svg {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.report-center-empty-icon path {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.report-center-empty h2 {
|
||||
margin: 0;
|
||||
color: var(--wxex-text-primary);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react'
|
||||
|
||||
interface ReportEmptyStateProps {
|
||||
icon?: 'spark'
|
||||
title: string
|
||||
message: string
|
||||
actionLabel: string
|
||||
@@ -8,6 +9,7 @@ interface ReportEmptyStateProps {
|
||||
}
|
||||
|
||||
export function ReportEmptyState({
|
||||
icon,
|
||||
title,
|
||||
message,
|
||||
actionLabel,
|
||||
@@ -15,6 +17,14 @@ export function ReportEmptyState({
|
||||
}: ReportEmptyStateProps): React.ReactElement {
|
||||
return (
|
||||
<div className="report-center-empty">
|
||||
{icon === 'spark' && (
|
||||
<div className="report-center-empty-icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" focusable="false">
|
||||
<path d="M12 3l1.6 5.2L19 10l-5.4 1.8L12 17l-1.6-5.2L5 10l5.4-1.8L12 3Z" />
|
||||
<path d="M19 15l.8 2.3L22 18l-2.2.7L19 21l-.8-2.3L16 18l2.2-.7L19 15Z" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
<h2>{title}</h2>
|
||||
<p>{message}</p>
|
||||
<button type="button" onClick={onAction}>
|
||||
|
||||
@@ -16,10 +16,14 @@ interface ReportHistorySidebarProps {
|
||||
dbReady: boolean
|
||||
onSelectReport: (reportId: string) => void
|
||||
onCreateReport: () => void
|
||||
onDeleteReport: (reportId: string) => Promise<{ success: boolean; error?: string }>
|
||||
onOpenSettings: () => void
|
||||
}
|
||||
|
||||
type HistoryFilter = 'today' | 'yesterday' | 'week' | 'older'
|
||||
interface ReportGroup {
|
||||
label: string
|
||||
reports: GeneratedReportRecord[]
|
||||
}
|
||||
|
||||
const DAY_MS = 86400000
|
||||
|
||||
@@ -31,7 +35,44 @@ const dateKey = (value: string): number => {
|
||||
const formatGeneratedAt = (value: string): string => {
|
||||
const date = new Date(value)
|
||||
if (!Number.isFinite(date.getTime())) return value
|
||||
return date.toLocaleString('zh-CN', { hour12: false })
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
})
|
||||
}
|
||||
|
||||
const groupLabelFor = (value: string): string => {
|
||||
const date = new Date(value)
|
||||
if (!Number.isFinite(date.getTime())) return '更早'
|
||||
|
||||
const now = new Date()
|
||||
const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime()
|
||||
const startOfYesterday = startOfToday - DAY_MS
|
||||
const time = new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime()
|
||||
|
||||
if (time >= startOfToday) return '今天'
|
||||
if (time >= startOfYesterday) return '昨天'
|
||||
if (date.getFullYear() === now.getFullYear()) return `${date.getMonth() + 1}月${date.getDate()}日`
|
||||
return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日`
|
||||
}
|
||||
|
||||
const buildGroups = (reports: GeneratedReportRecord[]): ReportGroup[] => {
|
||||
const groups: ReportGroup[] = []
|
||||
const groupMap = new Map<string, GeneratedReportRecord[]>()
|
||||
|
||||
reports.forEach((report) => {
|
||||
const label = groupLabelFor(report.generatedAt)
|
||||
const items = groupMap.get(label) || []
|
||||
items.push(report)
|
||||
groupMap.set(label, items)
|
||||
})
|
||||
|
||||
groupMap.forEach((items, label) => groups.push({ label, reports: items }))
|
||||
return groups
|
||||
}
|
||||
|
||||
export function ReportHistorySidebar({
|
||||
@@ -41,37 +82,44 @@ export function ReportHistorySidebar({
|
||||
dbReady,
|
||||
onSelectReport,
|
||||
onCreateReport,
|
||||
onDeleteReport,
|
||||
onOpenSettings
|
||||
}: ReportHistorySidebarProps): React.ReactElement {
|
||||
const [keyword, setKeyword] = useState('')
|
||||
const [filter, setFilter] = useState<HistoryFilter>('today')
|
||||
const [pendingDelete, setPendingDelete] = useState<GeneratedReportRecord | null>(null)
|
||||
const [deleteError, setDeleteError] = useState('')
|
||||
|
||||
const filteredReports = useMemo(() => {
|
||||
const reportGroups = useMemo(() => {
|
||||
const lower = keyword.trim().toLowerCase()
|
||||
const now = new Date()
|
||||
const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime()
|
||||
const startOfYesterday = startOfToday - DAY_MS
|
||||
const startOfWeek = startOfToday - 6 * DAY_MS
|
||||
|
||||
return reports
|
||||
const filteredReports = reports
|
||||
.filter((report) => {
|
||||
const haystack = `${report.contactName} ${report.dateRange}`.toLowerCase()
|
||||
if (lower && !haystack.includes(lower)) return false
|
||||
const time = dateKey(report.generatedAt)
|
||||
if (filter === 'today') return time >= startOfToday
|
||||
if (filter === 'yesterday') return time >= startOfYesterday && time < startOfToday
|
||||
if (filter === 'week') return time >= startOfWeek
|
||||
return time < startOfWeek
|
||||
const haystack = `${report.contactName} ${report.dateRange} ${formatGeneratedAt(
|
||||
report.generatedAt
|
||||
)}`.toLowerCase()
|
||||
return lower ? haystack.includes(lower) : true
|
||||
})
|
||||
.sort((left, right) => dateKey(right.generatedAt) - dateKey(left.generatedAt))
|
||||
}, [filter, keyword, reports])
|
||||
|
||||
return buildGroups(filteredReports)
|
||||
}, [keyword, reports])
|
||||
|
||||
const confirmDelete = async (): Promise<void> => {
|
||||
if (!pendingDelete) return
|
||||
setDeleteError('')
|
||||
const result = await onDeleteReport(pendingDelete.id)
|
||||
if (!result.success) {
|
||||
setDeleteError(result.error || '删除日报失败')
|
||||
return
|
||||
}
|
||||
setPendingDelete(null)
|
||||
}
|
||||
|
||||
return (
|
||||
<aside className="report-history-sidebar">
|
||||
<div className="report-history-header">
|
||||
<div>
|
||||
<h2>AI 日报</h2>
|
||||
<p>本地保存的群聊日报资产</p>
|
||||
<p>已生成报告管理中心</p>
|
||||
</div>
|
||||
<span>{reports.length}</span>
|
||||
</div>
|
||||
@@ -86,64 +134,93 @@ export function ReportHistorySidebar({
|
||||
placeholder="搜索群聊或日报"
|
||||
/>
|
||||
</label>
|
||||
<div className="report-history-filters">
|
||||
{[
|
||||
['today', '今天'],
|
||||
['yesterday', '昨天'],
|
||||
['week', '本周'],
|
||||
['older', '更早']
|
||||
].map(([value, label]) => (
|
||||
<button
|
||||
key={value}
|
||||
type="button"
|
||||
className={filter === value ? 'active' : ''}
|
||||
onClick={() => setFilter(value as HistoryFilter)}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="report-history-list">
|
||||
{filteredReports.length ? (
|
||||
filteredReports.map((report) => (
|
||||
<button
|
||||
key={report.id}
|
||||
type="button"
|
||||
className={`report-history-item ${report.id === selectedReportId ? 'active' : ''}`}
|
||||
onClick={() => onSelectReport(report.id)}
|
||||
>
|
||||
<span className="report-history-avatar">
|
||||
{report.contactAvatar ? (
|
||||
<img src={report.contactAvatar} alt={report.contactName} referrerPolicy="no-referrer" />
|
||||
) : (
|
||||
report.contactName.charAt(0)
|
||||
)}
|
||||
</span>
|
||||
<span className="report-history-text">
|
||||
<b>{report.contactName}</b>
|
||||
<small>
|
||||
{report.dateRange} · {formatGeneratedAt(report.generatedAt)}
|
||||
</small>
|
||||
<small>{report.messageCount} 条消息</small>
|
||||
<em>
|
||||
HTML {report.htmlStatus === 'ready' ? '已保存' : '缺失'} · PNG{' '}
|
||||
{report.pngStatus === 'ready' ? '已保存' : '缺失'}
|
||||
</em>
|
||||
</span>
|
||||
</button>
|
||||
<button type="button" className="report-history-create" onClick={onCreateReport}>
|
||||
+ 新建日报
|
||||
</button>
|
||||
<div className="report-history-list" aria-label="历史报告">
|
||||
<div className="report-history-list-title">历史报告</div>
|
||||
{reportGroups.length ? (
|
||||
reportGroups.map((group) => (
|
||||
<section className="report-history-group" key={group.label}>
|
||||
<h3>{group.label}</h3>
|
||||
{group.reports.map((report) => (
|
||||
<div
|
||||
key={report.id}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={`report-history-item ${report.id === selectedReportId ? 'active' : ''}`}
|
||||
onClick={() => onSelectReport(report.id)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== 'Enter' && event.key !== ' ') return
|
||||
event.preventDefault()
|
||||
onSelectReport(report.id)
|
||||
}}
|
||||
>
|
||||
<span className="report-history-avatar">
|
||||
{report.contactAvatar ? (
|
||||
<img
|
||||
src={report.contactAvatar}
|
||||
alt={report.contactName}
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
) : (
|
||||
report.contactName.charAt(0)
|
||||
)}
|
||||
</span>
|
||||
<span className="report-history-text">
|
||||
<b>{report.contactName}</b>
|
||||
<small>{formatGeneratedAt(report.generatedAt)}</small>
|
||||
<small>{report.messageCount} 条消息</small>
|
||||
<em>PNG{report.pngStatus === 'ready' ? '已保存' : '缺失'}</em>
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="report-history-delete"
|
||||
title="删除日报"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
setPendingDelete(report)
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== 'Enter' && event.key !== ' ') return
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
setPendingDelete(report)
|
||||
}}
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
))
|
||||
) : (
|
||||
<div className="report-history-empty">
|
||||
<b>当前分组暂无日报</b>
|
||||
<button type="button" onClick={onCreateReport}>
|
||||
生成新日报
|
||||
</button>
|
||||
<b>暂无历史报告</b>
|
||||
<span>新建日报后会出现在这里。</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="report-history-account">
|
||||
<AccountSummary selfInfo={selfInfo} dbReady={dbReady} onClick={onOpenSettings} />
|
||||
</div>
|
||||
{pendingDelete && (
|
||||
<div className="report-delete-confirm" role="dialog" aria-modal="true">
|
||||
<div className="report-delete-confirm-card">
|
||||
<h2>删除日报?</h2>
|
||||
<p>只删除本地生成报告,不会影响微信聊天记录。</p>
|
||||
{deleteError && <p className="report-delete-error">{deleteError}</p>}
|
||||
<div>
|
||||
<button type="button" onClick={() => setPendingDelete(null)}>
|
||||
取消
|
||||
</button>
|
||||
<button type="button" className="danger" onClick={() => void confirmDelete()}>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ export function ReportSettingsPanel({
|
||||
<aside className="report-settings-panel">
|
||||
<header>
|
||||
<h2>报告信息</h2>
|
||||
<p>当前本地日报资产的真实保存状态</p>
|
||||
<p>当前选中报告的本地资产状态</p>
|
||||
</header>
|
||||
<section className="report-settings-section">
|
||||
<h3>生成信息</h3>
|
||||
@@ -75,8 +75,8 @@ export function ReportSettingsPanel({
|
||||
)}
|
||||
</section>
|
||||
<section className="report-settings-section muted">
|
||||
<h3>暂未支持</h3>
|
||||
<p>云同步、报告编辑器、模板切换和复杂历史数据库不属于本阶段。</p>
|
||||
<h3>说明</h3>
|
||||
<p>删除历史日报只会删除本地生成报告,不会影响微信聊天记录。</p>
|
||||
</section>
|
||||
</aside>
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ReportZoomBar } from './ReportZoomBar'
|
||||
|
||||
interface ReportViewerProps {
|
||||
report: GeneratedReportRecord | null
|
||||
hasReports: boolean
|
||||
onBackToConfigure: () => void
|
||||
onRegenerate: () => void
|
||||
onCopyImage: (report: GeneratedReportRecord) => Promise<{ success: boolean; error?: string }>
|
||||
@@ -15,6 +16,7 @@ interface ReportViewerProps {
|
||||
|
||||
export function ReportViewer({
|
||||
report,
|
||||
hasReports,
|
||||
onBackToConfigure,
|
||||
onRegenerate,
|
||||
onCopyImage,
|
||||
@@ -39,7 +41,7 @@ export function ReportViewer({
|
||||
}, [onImageSizeChange, report?.id])
|
||||
|
||||
const title = useMemo(
|
||||
() => (report ? `${report.contactName} 群聊日报` : 'AI 群聊日报'),
|
||||
() => (report ? `${report.contactName} 群聊日报` : 'AI 日报'),
|
||||
[report]
|
||||
)
|
||||
|
||||
@@ -66,9 +68,14 @@ export function ReportViewer({
|
||||
return (
|
||||
<main className="report-viewer">
|
||||
<ReportEmptyState
|
||||
title="尚未生成日报"
|
||||
message="生成一份 AI 群聊日报后,可以在这里查看本地保存的长图。"
|
||||
actionLabel="生成新日报"
|
||||
icon="spark"
|
||||
title={hasReports ? '选择一份历史日报' : 'AI日报'}
|
||||
message={
|
||||
hasReports
|
||||
? '从左侧历史报告中选择一份日报,查看本地保存的长图和文件信息。'
|
||||
: '还没有生成过日报。选择一个群聊,让 AI 自动整理讨论重点、热点话题和重要消息。'
|
||||
}
|
||||
actionLabel="开始生成日报"
|
||||
onAction={onBackToConfigure}
|
||||
/>
|
||||
</main>
|
||||
|
||||
@@ -40,3 +40,9 @@ export interface SaveGeneratedReportResult {
|
||||
record?: GeneratedReportRecord
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface DeleteGeneratedReportResult {
|
||||
success: boolean
|
||||
deletedId?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user