mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-10-05 12:34:35 +08:00
feat: 日报新增模板市场,支持社区模板安装与使用
This commit is contained in:
@@ -37,7 +37,7 @@ import {
|
||||
} from './utils/message-pages'
|
||||
import { isRelevantMessageMonitorEvent, parseWcdbMonitorEvent } from './utils/message-monitor'
|
||||
import { enrichQuotedMessages } from './utils/quoted-messages'
|
||||
import type { SelectableReportTemplateId } from '../../shared/report-templates'
|
||||
import type { ReportTemplateSelectionId } from '../../shared/report-templates'
|
||||
import { switchGeneratedReportTemplate } from './utils/report-template-switch'
|
||||
import { runtimePlatform, supportsPersonalWechatSend } from './utils/runtime-environment'
|
||||
import { useToast } from './components/ui'
|
||||
@@ -1704,7 +1704,7 @@ function App(): React.ReactElement {
|
||||
|
||||
const handleSwitchReportTemplate = async (
|
||||
report: GeneratedReportRecord,
|
||||
templateId: SelectableReportTemplateId
|
||||
templateId: ReportTemplateSelectionId
|
||||
): Promise<{ success: boolean; error?: string }> => {
|
||||
try {
|
||||
const updated = await withTimeout(
|
||||
|
||||
@@ -16,7 +16,7 @@ import { ReportRangeSelector } from './ReportRangeSelector'
|
||||
import { ReportMemberNameSelector } from './ReportMemberNameSelector'
|
||||
import { ReportGroupMemberSelector } from './ReportGroupMemberSelector'
|
||||
import { ReportSectionSelector } from './ReportSectionSelector'
|
||||
import { ReportTemplateSelector, SelectableReportTemplateId } from './ReportTemplateSelector'
|
||||
import { ReportTemplateSelector, ReportTemplateSelectionId } from './ReportTemplateSelector'
|
||||
import { Button, Input } from '../ui'
|
||||
|
||||
interface AiReportWorkspaceProps {
|
||||
@@ -47,8 +47,8 @@ interface AiReportWorkspaceProps {
|
||||
onRevealReport: () => Promise<{ success: boolean; error?: string }>
|
||||
onViewResult: () => void
|
||||
hasReportResult: boolean
|
||||
templateId: SelectableReportTemplateId
|
||||
onTemplateIdChange: (value: SelectableReportTemplateId) => void
|
||||
templateId: ReportTemplateSelectionId
|
||||
onTemplateIdChange: (value: ReportTemplateSelectionId) => void
|
||||
memberNamePreference: ReportMemberNamePreference
|
||||
onMemberNamePreferenceChange: (value: ReportMemberNamePreference) => void
|
||||
reportTimeoutSeconds: number
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import React, { useRef, useState } from 'react'
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import {
|
||||
DEFAULT_REPORT_TEMPLATE,
|
||||
REPORT_TEMPLATES,
|
||||
encodeExternalReportTemplateId,
|
||||
type ReportTemplateDefinition,
|
||||
type SelectableReportTemplateId
|
||||
type ReportTemplateSelectionId
|
||||
} from '../../../../shared/report-templates'
|
||||
import type {
|
||||
ReportTemplateCatalog,
|
||||
ReportTemplateCatalogEntry
|
||||
} from '../../../../shared/report-template-market'
|
||||
import type { InstalledReportTemplate } from '../../../../shared/report-template-package'
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
@@ -18,14 +24,30 @@ import {
|
||||
RadioGroupItem
|
||||
} from '../ui'
|
||||
|
||||
export type { SelectableReportTemplateId } from '../../../../shared/report-templates'
|
||||
export type {
|
||||
SelectableReportTemplateId,
|
||||
ReportTemplateSelectionId
|
||||
} from '../../../../shared/report-templates'
|
||||
|
||||
interface ReportTemplateSelectorProps {
|
||||
value: SelectableReportTemplateId
|
||||
onChange: (value: SelectableReportTemplateId) => void
|
||||
value: ReportTemplateSelectionId
|
||||
onChange: (value: ReportTemplateSelectionId) => void
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
type PreviewItem =
|
||||
| { kind: 'builtin'; template: ReportTemplateDefinition }
|
||||
| {
|
||||
kind: 'external'
|
||||
id: string
|
||||
version: string
|
||||
name: string
|
||||
description: string
|
||||
installed: boolean
|
||||
platform?: 'default' | 'mobile' | 'desktop'
|
||||
preview?: string
|
||||
}
|
||||
|
||||
const TemplateDiagram = ({
|
||||
template
|
||||
}: {
|
||||
@@ -69,11 +91,100 @@ export const ReportTemplateSelector: React.FC<ReportTemplateSelectorProps> = ({
|
||||
onChange,
|
||||
disabled
|
||||
}) => {
|
||||
const [previewing, setPreviewing] = useState<ReportTemplateDefinition | null>(null)
|
||||
const [previewing, setPreviewing] = useState<PreviewItem | null>(null)
|
||||
const [installed, setInstalled] = useState<InstalledReportTemplate[]>([])
|
||||
const [catalog, setCatalog] = useState<ReportTemplateCatalog | null>(null)
|
||||
const [marketError, setMarketError] = useState('')
|
||||
const [marketBusyKey, setMarketBusyKey] = useState('')
|
||||
const previewTriggerRef = useRef<HTMLButtonElement | null>(null)
|
||||
const mobileTemplates = REPORT_TEMPLATES.filter((template) => template.platform === 'mobile')
|
||||
const desktopTemplates = REPORT_TEMPLATES.filter((template) => template.platform === 'desktop')
|
||||
|
||||
const refreshMarket = async (): Promise<void> => {
|
||||
if (typeof window === 'undefined' || !window.api) return
|
||||
if (typeof window.api.listReportTemplates === 'function') {
|
||||
try {
|
||||
const listed = await window.api.listReportTemplates()
|
||||
if (Array.isArray(listed)) setInstalled(listed)
|
||||
} catch (error) {
|
||||
setMarketError(error instanceof Error ? error.message : '已安装模板加载失败')
|
||||
}
|
||||
}
|
||||
if (typeof window.api.listReportTemplateCatalog !== 'function') return
|
||||
try {
|
||||
const result = await window.api.listReportTemplateCatalog()
|
||||
if (result.success && result.catalog) {
|
||||
setCatalog(result.catalog)
|
||||
setMarketError('')
|
||||
} else {
|
||||
setMarketError(result.error || '远端模板目录加载失败')
|
||||
}
|
||||
} catch (error) {
|
||||
setMarketError(error instanceof Error ? error.message : '远端模板目录加载失败')
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
void refreshMarket()
|
||||
}, [])
|
||||
|
||||
const externalInstalled = useMemo(
|
||||
() => installed.filter((template) => template.source === 'installed'),
|
||||
[installed]
|
||||
)
|
||||
const catalogEntries = catalog?.templates || []
|
||||
const installedKeys = useMemo(
|
||||
() => new Set(externalInstalled.map((template) => encodeExternalReportTemplateId(template.id, template.version))),
|
||||
[externalInstalled]
|
||||
)
|
||||
|
||||
const platformLabel = (platform?: 'default' | 'mobile' | 'desktop'): string =>
|
||||
platform === 'desktop' ? '桌面宽屏' : platform === 'default' ? '经典长图' : '手机长图'
|
||||
|
||||
const catalogEntryFor = (template: InstalledReportTemplate): ReportTemplateCatalogEntry | undefined =>
|
||||
catalogEntries.find((entry) => entry.id === template.id && entry.version === template.version)
|
||||
|
||||
const installFromCatalog = async (entry: ReportTemplateCatalogEntry): Promise<void> => {
|
||||
const key = encodeExternalReportTemplateId(entry.id, entry.version)
|
||||
if (typeof window.api.installReportTemplateFromCatalog !== 'function') {
|
||||
setMarketError('当前 TraceMemo 版本不支持模板市场安装')
|
||||
return
|
||||
}
|
||||
setMarketBusyKey(key)
|
||||
setMarketError('')
|
||||
try {
|
||||
const result = await window.api.installReportTemplateFromCatalog(entry.id, entry.version)
|
||||
if (!result.success) {
|
||||
setMarketError(result.error || `模板安装失败:${entry.name}`)
|
||||
return
|
||||
}
|
||||
await refreshMarket()
|
||||
} catch (error) {
|
||||
setMarketError(error instanceof Error ? error.message : `模板安装失败:${entry.name}`)
|
||||
} finally {
|
||||
setMarketBusyKey('')
|
||||
}
|
||||
}
|
||||
|
||||
const uninstallExternal = async (template: InstalledReportTemplate): Promise<void> => {
|
||||
const key = encodeExternalReportTemplateId(template.id, template.version)
|
||||
setMarketBusyKey(key)
|
||||
setMarketError('')
|
||||
try {
|
||||
const result = await window.api.uninstallReportTemplate(template.id, template.version)
|
||||
if (!result.success) {
|
||||
setMarketError(result.error || `模板卸载失败:${template.name}`)
|
||||
return
|
||||
}
|
||||
if (value === key) onChange('v1')
|
||||
await refreshMarket()
|
||||
} catch (error) {
|
||||
setMarketError(error instanceof Error ? error.message : `模板卸载失败:${template.name}`)
|
||||
} finally {
|
||||
setMarketBusyKey('')
|
||||
}
|
||||
}
|
||||
|
||||
const renderGroup = (
|
||||
title: string,
|
||||
templates: readonly ReportTemplateDefinition[]
|
||||
@@ -106,7 +217,7 @@ export const ReportTemplateSelector: React.FC<ReportTemplateSelectorProps> = ({
|
||||
size="sm"
|
||||
onClick={(event) => {
|
||||
previewTriggerRef.current = event.currentTarget
|
||||
setPreviewing(template)
|
||||
setPreviewing({ kind: 'builtin', template })
|
||||
}}
|
||||
>
|
||||
查看版式
|
||||
@@ -128,12 +239,134 @@ export const ReportTemplateSelector: React.FC<ReportTemplateSelectorProps> = ({
|
||||
className="report-template-catalog"
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
onValueChange={(nextValue) => onChange(nextValue as SelectableReportTemplateId)}
|
||||
onValueChange={(nextValue) => onChange(nextValue as ReportTemplateSelectionId)}
|
||||
>
|
||||
{renderGroup('默认模板', [DEFAULT_REPORT_TEMPLATE])}
|
||||
{renderGroup('手机端 · 375–414 px', mobileTemplates)}
|
||||
{renderGroup('电脑端 · 1280–1920 px', desktopTemplates)}
|
||||
{externalInstalled.length > 0 && (
|
||||
<div className="report-template-group">
|
||||
<div className="report-template-group-title">已安装市场模板</div>
|
||||
<div className="report-template-list">
|
||||
{externalInstalled.map((template) => {
|
||||
const key = encodeExternalReportTemplateId(template.id, template.version)
|
||||
const entry = catalogEntryFor(template)
|
||||
const active = value === key
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className={`report-template-item ${active ? 'active' : ''} ${disabled ? 'disabled' : ''}`}
|
||||
>
|
||||
<label htmlFor={`report-template-${key}`}>
|
||||
<RadioGroupItem
|
||||
id={`report-template-${key}`}
|
||||
value={key}
|
||||
aria-label={template.name}
|
||||
/>
|
||||
{entry?.preview ? (
|
||||
<img className="report-template-diagram" src={entry.preview} alt="" />
|
||||
) : (
|
||||
<TemplateDiagram template={DEFAULT_REPORT_TEMPLATE} />
|
||||
)}
|
||||
<div className="report-template-body">
|
||||
<div className="report-template-eyebrow">市场 · v{template.version}</div>
|
||||
<div className="report-template-title">{template.name}</div>
|
||||
<div className="report-template-tagline">
|
||||
{entry?.description || `${template.author} · ${platformLabel(entry?.platform)}`}
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={(event) => {
|
||||
previewTriggerRef.current = event.currentTarget
|
||||
setPreviewing({
|
||||
kind: 'external',
|
||||
id: template.id,
|
||||
version: template.version,
|
||||
name: template.name,
|
||||
description: entry?.description || `${template.author} · ${template.version}`,
|
||||
installed: true,
|
||||
platform: entry?.platform,
|
||||
preview: entry?.preview
|
||||
})
|
||||
}}
|
||||
>
|
||||
查看版式
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
disabled={disabled || marketBusyKey === key}
|
||||
onClick={() => void uninstallExternal(template)}
|
||||
>
|
||||
{marketBusyKey === key ? '处理中…' : '卸载'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{catalogEntries.filter((entry) => !installedKeys.has(encodeExternalReportTemplateId(entry.id, entry.version))).length > 0 && (
|
||||
<div className="report-template-group report-template-market">
|
||||
<div className="report-template-group-title">模板市场</div>
|
||||
<div className="report-template-list">
|
||||
{catalogEntries
|
||||
.filter((entry) => !installedKeys.has(encodeExternalReportTemplateId(entry.id, entry.version)))
|
||||
.map((entry) => {
|
||||
const key = encodeExternalReportTemplateId(entry.id, entry.version)
|
||||
return (
|
||||
<div key={key} className="report-template-item">
|
||||
<div className="report-template-body">
|
||||
<div className="report-template-eyebrow">远端 · v{entry.version}</div>
|
||||
<div className="report-template-title">{entry.name}</div>
|
||||
<div className="report-template-tagline">{entry.description}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
接口 {entry.interfaceVersion} · {platformLabel(entry.platform)} · {entry.sizeBytes} bytes
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{entry.preview && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={(event) => {
|
||||
previewTriggerRef.current = event.currentTarget
|
||||
setPreviewing({
|
||||
kind: 'external',
|
||||
id: entry.id,
|
||||
version: entry.version,
|
||||
name: entry.name,
|
||||
description: entry.description,
|
||||
installed: false,
|
||||
platform: entry.platform,
|
||||
preview: entry.preview
|
||||
})
|
||||
}}
|
||||
>
|
||||
查看版式
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={disabled || marketBusyKey === key}
|
||||
onClick={() => void installFromCatalog(entry)}
|
||||
>
|
||||
{marketBusyKey === key ? '安装中…' : '安装'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</RadioGroup>
|
||||
{marketError && <p className="report-inline-error">模板市场:{marketError}</p>}
|
||||
<Dialog
|
||||
open={Boolean(previewing)}
|
||||
onOpenChange={(open) => {
|
||||
@@ -151,20 +384,32 @@ export const ReportTemplateSelector: React.FC<ReportTemplateSelectorProps> = ({
|
||||
<DialogHeader className="pr-8">
|
||||
<div className="report-template-preview-heading">
|
||||
<div>
|
||||
<span>{previewing.label}</span>
|
||||
<DialogTitle>{previewing.name}</DialogTitle>
|
||||
<span>
|
||||
{previewing.kind === 'builtin'
|
||||
? previewing.template.label
|
||||
: `市场 · v${previewing.version}`}
|
||||
</span>
|
||||
<DialogTitle>
|
||||
{previewing.kind === 'builtin' ? previewing.template.name : previewing.name}
|
||||
</DialogTitle>
|
||||
</div>
|
||||
<em>
|
||||
{previewing.platform === 'desktop'
|
||||
? '桌面宽屏'
|
||||
: previewing.platform === 'default'
|
||||
? '经典长图'
|
||||
: '手机长图'}
|
||||
{platformLabel(
|
||||
previewing.kind === 'builtin' ? previewing.template.platform : previewing.platform
|
||||
)}
|
||||
</em>
|
||||
</div>
|
||||
<DialogDescription>{previewing.tagline}</DialogDescription>
|
||||
<DialogDescription>
|
||||
{previewing.kind === 'builtin' ? previewing.template.tagline : previewing.description}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<TemplateDiagram template={previewing} />
|
||||
{previewing.kind === 'builtin' ? (
|
||||
<TemplateDiagram template={previewing.template} />
|
||||
) : previewing.preview ? (
|
||||
<img src={previewing.preview} alt={`${previewing.name} 预览`} className="max-h-[60vh] w-full object-contain" />
|
||||
) : (
|
||||
<TemplateDiagram template={DEFAULT_REPORT_TEMPLATE} />
|
||||
)}
|
||||
<DialogDescription className="report-template-preview-note">
|
||||
生成时会自动代入当前群聊的真实头像、昵称、消息、讨论摘要、Q&A、统计与关键词。
|
||||
</DialogDescription>
|
||||
@@ -172,14 +417,22 @@ export const ReportTemplateSelector: React.FC<ReportTemplateSelectorProps> = ({
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline">关闭</Button>
|
||||
</DialogClose>
|
||||
<Button
|
||||
onClick={() => {
|
||||
onChange(previewing.id)
|
||||
setPreviewing(null)
|
||||
}}
|
||||
>
|
||||
选择此模板
|
||||
</Button>
|
||||
{previewing.kind === 'builtin' || previewing.installed ? (
|
||||
<Button
|
||||
onClick={() => {
|
||||
onChange(
|
||||
previewing.kind === 'builtin'
|
||||
? previewing.template.id
|
||||
: encodeExternalReportTemplateId(previewing.id, previewing.version)
|
||||
)
|
||||
setPreviewing(null)
|
||||
}}
|
||||
>
|
||||
选择此模板
|
||||
</Button>
|
||||
) : (
|
||||
<Button disabled>请先安装</Button>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
)}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import React from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import {
|
||||
SELECTABLE_REPORT_TEMPLATES,
|
||||
type SelectableReportTemplateId
|
||||
encodeExternalReportTemplateId,
|
||||
type ReportTemplateSelectionId
|
||||
} from '../../../../shared/report-templates'
|
||||
import type { InstalledReportTemplate } from '../../../../shared/report-template-package'
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
@@ -20,9 +22,9 @@ interface ReportToolbarProps {
|
||||
canSwitchTemplate: boolean
|
||||
canSendToGroup?: boolean
|
||||
sendToGroupHint?: string
|
||||
currentTemplateId?: SelectableReportTemplateId
|
||||
currentTemplateId?: ReportTemplateSelectionId
|
||||
isSwitchingTemplate: boolean
|
||||
onSwitchTemplate: (templateId: SelectableReportTemplateId) => void
|
||||
onSwitchTemplate: (templateId: ReportTemplateSelectionId) => void
|
||||
onRegenerate: () => void
|
||||
onCopyImage: () => void
|
||||
onReveal: () => void
|
||||
@@ -46,6 +48,16 @@ export function ReportToolbar({
|
||||
onShare,
|
||||
onSendToGroup
|
||||
}: ReportToolbarProps): React.ReactElement {
|
||||
const [installedTemplates, setInstalledTemplates] = useState<InstalledReportTemplate[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined' || !window.api || typeof window.api.listReportTemplates !== 'function') return
|
||||
void window.api
|
||||
.listReportTemplates()
|
||||
.then((templates) => setInstalledTemplates(Array.isArray(templates) ? templates.filter((template) => template.source === 'installed') : []))
|
||||
.catch(() => setInstalledTemplates([]))
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="report-viewer-toolbar">
|
||||
<DropdownMenu>
|
||||
@@ -81,6 +93,24 @@ export function ReportToolbar({
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
{installedTemplates.map((template) => {
|
||||
const templateId = encodeExternalReportTemplateId(template.id, template.version)
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={templateId}
|
||||
className="grid min-h-11 min-w-0 grid-cols-[62px_minmax(0,1fr)_auto] gap-2"
|
||||
onSelect={() => onSwitchTemplate(templateId)}
|
||||
>
|
||||
<span className="text-xs text-muted-foreground">市场</span>
|
||||
<strong className="overflow-hidden text-ellipsis whitespace-nowrap text-sm font-semibold">
|
||||
{template.name}
|
||||
</strong>
|
||||
{templateId === currentTemplateId && (
|
||||
<span className="text-xs text-primary">当前</span>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Button variant="outline" size="sm" onClick={onRegenerate}>
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { GeneratedReportRecord } from './types'
|
||||
import { ReportEmptyState } from './ReportEmptyState'
|
||||
import { ReportToolbar } from './ReportToolbar'
|
||||
import { ReportZoomBar } from './ReportZoomBar'
|
||||
import type { SelectableReportTemplateId } from '../../../../shared/report-templates'
|
||||
import type { ReportTemplateSelectionId } from '../../../../shared/report-templates'
|
||||
import type { Contact } from '../../../../shared/types'
|
||||
import { WechatShareCardDialog } from './WechatShareCardDialog'
|
||||
import { PersonalWechatSendDialog } from '../chat/PersonalWechatSendDialog'
|
||||
@@ -18,7 +18,7 @@ interface ReportViewerProps {
|
||||
onReveal: (report: GeneratedReportRecord) => Promise<{ success: boolean; error?: string }>
|
||||
onSwitchTemplate: (
|
||||
report: GeneratedReportRecord,
|
||||
templateId: SelectableReportTemplateId
|
||||
templateId: ReportTemplateSelectionId
|
||||
) => Promise<{ success: boolean; error?: string }>
|
||||
sendTarget?: Contact | null
|
||||
personalWechatSendSupported?: boolean
|
||||
@@ -141,7 +141,7 @@ export function ReportViewer({
|
||||
setStatus(result.success ? '已打开报告所在文件夹' : result.error || '打开文件夹失败')
|
||||
}
|
||||
|
||||
const handleSwitchTemplate = async (templateId: SelectableReportTemplateId): Promise<void> => {
|
||||
const handleSwitchTemplate = async (templateId: ReportTemplateSelectionId): Promise<void> => {
|
||||
if (!report || isSwitchingTemplate) return
|
||||
if (report.templateId === templateId) {
|
||||
setStatus('当前已是所选模板')
|
||||
|
||||
@@ -16,7 +16,11 @@ import type {
|
||||
ReportImageInsightSummary,
|
||||
ReportPreparationProgress
|
||||
} from '../utils/group-report-facts'
|
||||
import { SelectableReportTemplateId } from '../components/reports/ReportTemplateSelector'
|
||||
import {
|
||||
decodeExternalReportTemplateId,
|
||||
type ReportTemplateRequestId,
|
||||
type ReportTemplateSelectionId
|
||||
} from '../../../shared/report-templates'
|
||||
import {
|
||||
transcribeVoiceMessages as transcribeReportVoiceMessages,
|
||||
type VoiceTranscriptionProgress
|
||||
@@ -291,8 +295,8 @@ export function useGroupReportGeneration({
|
||||
closeResult: () => void
|
||||
copyImage: () => Promise<{ success: boolean; error?: string }>
|
||||
revealReport: () => Promise<{ success: boolean; error?: string }>
|
||||
templateId: SelectableReportTemplateId
|
||||
setTemplateId: (value: SelectableReportTemplateId) => void
|
||||
templateId: ReportTemplateSelectionId
|
||||
setTemplateId: (value: ReportTemplateSelectionId) => void
|
||||
memberNamePreference: ReportMemberNamePreference
|
||||
setMemberNamePreference: (value: ReportMemberNamePreference) => void
|
||||
reportTimeoutSeconds: number
|
||||
@@ -315,7 +319,7 @@ export function useGroupReportGeneration({
|
||||
const [reportPaths, setReportPaths] = useState<ReportPaths | null>(null)
|
||||
const [reportSnapshot, setReportSnapshot] = useState<GroupDailyReport | null>(null)
|
||||
const [reportMetadata, setReportMetadata] = useState<GroupReportMetadata | null>(null)
|
||||
const [templateId, setTemplateIdState] = useState<SelectableReportTemplateId>('v1')
|
||||
const [templateId, setTemplateIdState] = useState<ReportTemplateSelectionId>('v1')
|
||||
const [memberNamePreference, setMemberNamePreferenceState] = useState<ReportMemberNamePreference>(
|
||||
() => {
|
||||
const saved = localStorage.getItem('group_report_member_name_preference')
|
||||
@@ -336,7 +340,7 @@ export function useGroupReportGeneration({
|
||||
localStorage.setItem('group_report_member_name_preference', value)
|
||||
setMemberNamePreferenceState(value)
|
||||
}, [])
|
||||
const setTemplateId = useCallback((value: SelectableReportTemplateId): void => {
|
||||
const setTemplateId = useCallback((value: ReportTemplateSelectionId): void => {
|
||||
setTemplateIdState(value)
|
||||
}, [])
|
||||
const setReportTimeoutSeconds = useCallback((value: number): void => {
|
||||
@@ -614,11 +618,14 @@ export function useGroupReportGeneration({
|
||||
|
||||
setPhase('exportingReport')
|
||||
currentFailedAt = '导出 HTML 与 PNG'
|
||||
const externalTemplateRef = decodeExternalReportTemplateId(templateId)
|
||||
const exported = await withTimeout(
|
||||
window.api.exportGroupReport({
|
||||
report,
|
||||
metadata: context.input.metadata,
|
||||
templateId
|
||||
...(externalTemplateRef
|
||||
? { templateRef: externalTemplateRef }
|
||||
: { templateId: templateId as ReportTemplateRequestId })
|
||||
}),
|
||||
'日报图片导出'
|
||||
)
|
||||
|
||||
@@ -9,7 +9,11 @@ import type {
|
||||
UpdateGeneratedReportTemplateRequest,
|
||||
UpdateGeneratedReportTemplateResult
|
||||
} from '../../../shared/report-history'
|
||||
import type { SelectableReportTemplateId } from '../../../shared/report-templates'
|
||||
import {
|
||||
decodeExternalReportTemplateId,
|
||||
type ReportTemplateRequestId,
|
||||
type ReportTemplateSelectionId
|
||||
} from '../../../shared/report-templates'
|
||||
|
||||
interface ReportTemplateSwitchApi {
|
||||
exportGroupReport: (request: GroupReportExportRequest) => Promise<GroupReportExportResult>
|
||||
@@ -26,15 +30,19 @@ interface ReportTemplateSwitchApi {
|
||||
|
||||
export async function switchGeneratedReportTemplate(
|
||||
report: GeneratedReportRecord,
|
||||
templateId: SelectableReportTemplateId,
|
||||
templateId: ReportTemplateSelectionId,
|
||||
api: ReportTemplateSwitchApi
|
||||
): Promise<UpdateGeneratedReportTemplateResult> {
|
||||
const externalRef = decodeExternalReportTemplateId(templateId)
|
||||
const exportSelection = externalRef
|
||||
? { templateRef: externalRef }
|
||||
: { templateId: templateId as ReportTemplateRequestId }
|
||||
let exported: GroupReportExportResult
|
||||
if (report.reportSnapshot && report.reportMetadata) {
|
||||
exported = await api.exportGroupReport({
|
||||
report: report.reportSnapshot,
|
||||
metadata: report.reportMetadata,
|
||||
templateId
|
||||
...exportSelection
|
||||
})
|
||||
} else {
|
||||
const prepared = await api.prepareGeneratedReportTemplateSwitch(report.id)
|
||||
@@ -46,7 +54,7 @@ export async function switchGeneratedReportTemplate(
|
||||
}
|
||||
exported = await api.exportGroupReportSnapshot({
|
||||
snapshot: prepared.snapshot,
|
||||
templateId
|
||||
...exportSelection
|
||||
})
|
||||
}
|
||||
if (!exported.success || !exported.imageDataUrl || !exported.htmlPath || !exported.pngPath) {
|
||||
@@ -56,6 +64,7 @@ export async function switchGeneratedReportTemplate(
|
||||
return api.updateGeneratedReportTemplate({
|
||||
reportId: report.id,
|
||||
templateId,
|
||||
...(externalRef ? { templateRef: externalRef } : {}),
|
||||
generatedImage: exported.imageDataUrl,
|
||||
htmlPath: exported.htmlPath,
|
||||
pngPath: exported.pngPath
|
||||
|
||||
Reference in New Issue
Block a user