mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-10-06 13:54:10 +08:00
feat: 修改社区模板市场入口 放到日报二级菜单下, 增加跳转github
This commit is contained in:
@@ -19,6 +19,7 @@ import { ReportSourceSidebar } from './components/reports/ReportSourceSidebar'
|
|||||||
import { ReportTaskStatusPanel } from './components/reports/ReportTaskStatusPanel'
|
import { ReportTaskStatusPanel } from './components/reports/ReportTaskStatusPanel'
|
||||||
import { ReportViewer } from './components/reports/ReportViewer'
|
import { ReportViewer } from './components/reports/ReportViewer'
|
||||||
import { ScheduledReportsWorkspace } from './components/reports/ScheduledReportsWorkspace'
|
import { ScheduledReportsWorkspace } from './components/reports/ScheduledReportsWorkspace'
|
||||||
|
import { ReportTemplateMarketWorkspace } from './components/reports/ReportTemplateMarketWorkspace'
|
||||||
import { contactDisplayName } from './components/reports/types'
|
import { contactDisplayName } from './components/reports/types'
|
||||||
import type { GeneratedReportRecord, ReportWorkspaceView } from './components/reports/types'
|
import type { GeneratedReportRecord, ReportWorkspaceView } from './components/reports/types'
|
||||||
import { AiModelConfig, useGroupReportGeneration } from './hooks/useGroupReportGeneration'
|
import { AiModelConfig, useGroupReportGeneration } from './hooks/useGroupReportGeneration'
|
||||||
@@ -267,7 +268,7 @@ function App(): React.ReactElement {
|
|||||||
const [settingsCategory, setSettingsCategory] = useState<SettingsCategoryId>('account-database')
|
const [settingsCategory, setSettingsCategory] = useState<SettingsCategoryId>('account-database')
|
||||||
const [reportSourceContact, setReportSourceContact] = useState<Contact | null>(null)
|
const [reportSourceContact, setReportSourceContact] = useState<Contact | null>(null)
|
||||||
const [reportWorkspaceView, setReportWorkspaceView] = useState<ReportWorkspaceView>('result')
|
const [reportWorkspaceView, setReportWorkspaceView] = useState<ReportWorkspaceView>('result')
|
||||||
const [reportSection, setReportSection] = useState<'today' | 'scheduled'>('today')
|
const [reportSection, setReportSection] = useState<'today' | 'scheduled' | 'market'>('today')
|
||||||
const [generatedReports, setGeneratedReports] = useState<GeneratedReportRecord[]>([])
|
const [generatedReports, setGeneratedReports] = useState<GeneratedReportRecord[]>([])
|
||||||
const [selectedReportId, setSelectedReportId] = useState<string | null>(null)
|
const [selectedReportId, setSelectedReportId] = useState<string | null>(null)
|
||||||
const [latestGeneratedReportId, setLatestGeneratedReportId] = useState<string | null>(null)
|
const [latestGeneratedReportId, setLatestGeneratedReportId] = useState<string | null>(null)
|
||||||
@@ -1941,9 +1942,23 @@ function App(): React.ReactElement {
|
|||||||
>
|
>
|
||||||
定时日报{!supportsPersonalWechatSend && <small>仅 macOS / Windows</small>}
|
定时日报{!supportsPersonalWechatSend && <small>仅 macOS / Windows</small>}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="tab"
|
||||||
|
aria-selected={reportSection === 'market'}
|
||||||
|
className={reportSection === 'market' ? 'active' : ''}
|
||||||
|
onClick={() => setReportSection('market')}
|
||||||
|
>
|
||||||
|
社区模板市场
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="report-workspace-body">
|
<div className="report-workspace-body">
|
||||||
{reportSection === 'scheduled' ? (
|
{reportSection === 'market' ? (
|
||||||
|
<ReportTemplateMarketWorkspace
|
||||||
|
value={reportGeneration.templateId}
|
||||||
|
onChange={reportGeneration.setTemplateId}
|
||||||
|
/>
|
||||||
|
) : reportSection === 'scheduled' ? (
|
||||||
<ScheduledReportsWorkspace
|
<ScheduledReportsWorkspace
|
||||||
contacts={contacts}
|
contacts={contacts}
|
||||||
platformSupported={supportsPersonalWechatSend}
|
platformSupported={supportsPersonalWechatSend}
|
||||||
|
|||||||
@@ -0,0 +1,396 @@
|
|||||||
|
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
import {
|
||||||
|
DEFAULT_REPORT_TEMPLATE,
|
||||||
|
REPORT_TEMPLATES,
|
||||||
|
encodeExternalReportTemplateId,
|
||||||
|
type ReportTemplateDefinition,
|
||||||
|
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 } from '../ui'
|
||||||
|
import {
|
||||||
|
ReportTemplatePreviewDialog,
|
||||||
|
TemplateDiagram,
|
||||||
|
reportTemplatePlatformLabel,
|
||||||
|
type ReportTemplatePreviewItem
|
||||||
|
} from './ReportTemplatePreview'
|
||||||
|
|
||||||
|
/** 社区模板仓库:投稿模板从这里提交 PR。 */
|
||||||
|
const TEMPLATE_REPOSITORY_URL = 'https://github.com/Wxw-Gu/TraceMemo-Templates'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 社区模板市场独立页面:日报工作区顶部标签页的第三个入口。
|
||||||
|
*
|
||||||
|
* 与「生成群聊日报」里的模板选择器读同一套 IPC 与远端 catalog。这里是模板市场的主入口:
|
||||||
|
* 浏览、预览、安装、选用和卸载;生成页只保留选用与预览,不再重复展示市场列表。
|
||||||
|
*/
|
||||||
|
export function ReportTemplateMarketWorkspace({
|
||||||
|
value,
|
||||||
|
onChange
|
||||||
|
}: {
|
||||||
|
value: ReportTemplateSelectionId
|
||||||
|
onChange: (value: ReportTemplateSelectionId) => void
|
||||||
|
}): React.ReactElement {
|
||||||
|
const [installed, setInstalled] = useState<InstalledReportTemplate[]>([])
|
||||||
|
const [catalog, setCatalog] = useState<ReportTemplateCatalog | null>(null)
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [busyKey, setBusyKey] = useState('')
|
||||||
|
const [previewing, setPreviewing] = useState<ReportTemplatePreviewItem | null>(null)
|
||||||
|
const previewTriggerRef = useRef<HTMLButtonElement | null>(null)
|
||||||
|
|
||||||
|
const refresh = async (): Promise<void> => {
|
||||||
|
if (typeof window === 'undefined' || !window.api) {
|
||||||
|
setLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setError('')
|
||||||
|
if (typeof window.api.listReportTemplates === 'function') {
|
||||||
|
try {
|
||||||
|
const listed = await window.api.listReportTemplates()
|
||||||
|
if (Array.isArray(listed)) setInstalled(listed)
|
||||||
|
} catch (reason) {
|
||||||
|
setError(reason instanceof Error ? reason.message : '已安装模板加载失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeof window.api.listReportTemplateCatalog === 'function') {
|
||||||
|
try {
|
||||||
|
const result = await window.api.listReportTemplateCatalog()
|
||||||
|
if (result.success && result.catalog) setCatalog(result.catalog)
|
||||||
|
else setError(result.error || '远端模板目录加载失败')
|
||||||
|
} catch (reason) {
|
||||||
|
setError(reason instanceof Error ? reason.message : '远端模板目录加载失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void refresh()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
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 availableEntries = useMemo(
|
||||||
|
() =>
|
||||||
|
catalogEntries.filter(
|
||||||
|
(entry) => !installedKeys.has(encodeExternalReportTemplateId(entry.id, entry.version))
|
||||||
|
),
|
||||||
|
[catalogEntries, installedKeys]
|
||||||
|
)
|
||||||
|
|
||||||
|
const catalogEntryFor = (
|
||||||
|
template: InstalledReportTemplate
|
||||||
|
): ReportTemplateCatalogEntry | undefined =>
|
||||||
|
catalogEntries.find((entry) => entry.id === template.id && entry.version === template.version)
|
||||||
|
|
||||||
|
const install = async (entry: ReportTemplateCatalogEntry): Promise<void> => {
|
||||||
|
const key = encodeExternalReportTemplateId(entry.id, entry.version)
|
||||||
|
if (typeof window.api.installReportTemplateFromCatalog !== 'function') {
|
||||||
|
setError('当前 TraceMemo 版本不支持模板市场安装')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setBusyKey(key)
|
||||||
|
setError('')
|
||||||
|
try {
|
||||||
|
const result = await window.api.installReportTemplateFromCatalog(entry.id, entry.version)
|
||||||
|
if (!result.success) {
|
||||||
|
setError(result.error || `模板安装失败:${entry.name}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await refresh()
|
||||||
|
} catch (reason) {
|
||||||
|
setError(reason instanceof Error ? reason.message : `模板安装失败:${entry.name}`)
|
||||||
|
} finally {
|
||||||
|
setBusyKey('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 卸载市场模板。卸载掉的正好是当前选中的模板时回退到默认模板,
|
||||||
|
* 避免生成页保留一个已经不存在的模板引用。
|
||||||
|
*/
|
||||||
|
const uninstall = async (template: InstalledReportTemplate): Promise<void> => {
|
||||||
|
const key = encodeExternalReportTemplateId(template.id, template.version)
|
||||||
|
setBusyKey(key)
|
||||||
|
setError('')
|
||||||
|
try {
|
||||||
|
const result = await window.api.uninstallReportTemplate(template.id, template.version)
|
||||||
|
if (!result.success) {
|
||||||
|
setError(result.error || `模板卸载失败:${template.name}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (value === key) onChange(DEFAULT_REPORT_TEMPLATE.id)
|
||||||
|
await refresh()
|
||||||
|
} catch (reason) {
|
||||||
|
setError(reason instanceof Error ? reason.message : `模板卸载失败:${template.name}`)
|
||||||
|
} finally {
|
||||||
|
setBusyKey('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openPreview = (item: ReportTemplatePreviewItem, trigger: HTMLButtonElement): void => {
|
||||||
|
previewTriggerRef.current = trigger
|
||||||
|
setPreviewing(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderBuiltinRow = (template: ReportTemplateDefinition): React.ReactElement => (
|
||||||
|
<div
|
||||||
|
key={template.id}
|
||||||
|
className={`report-template-item ${value === template.id ? 'active' : ''}`}
|
||||||
|
>
|
||||||
|
<TemplateDiagram template={template} />
|
||||||
|
<div className="report-template-body">
|
||||||
|
<div className="report-template-eyebrow">
|
||||||
|
{template.platform === 'default' ? '默认模板' : template.label}
|
||||||
|
</div>
|
||||||
|
<div className="report-template-title">{template.name}</div>
|
||||||
|
<div className="report-template-tagline">{template.tagline}</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={(event) => openPreview({ kind: 'builtin', template }, event.currentTarget)}
|
||||||
|
>
|
||||||
|
查看版式
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const renderInstalledGroup = (): React.ReactElement | null => {
|
||||||
|
if (!externalInstalled.length) return null
|
||||||
|
return (
|
||||||
|
<section 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)
|
||||||
|
return (
|
||||||
|
<div key={key} className={`report-template-item ${value === key ? 'active' : ''}`}>
|
||||||
|
{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} · ${reportTemplatePlatformLabel(entry?.platform)}`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={(event) =>
|
||||||
|
openPreview(
|
||||||
|
{
|
||||||
|
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
|
||||||
|
},
|
||||||
|
event.currentTarget
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
查看版式
|
||||||
|
</Button>
|
||||||
|
{value === key ? (
|
||||||
|
<Button variant="ghost" size="sm" disabled>
|
||||||
|
当前使用
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
disabled={busyKey === key}
|
||||||
|
onClick={() => void uninstall(template)}
|
||||||
|
>
|
||||||
|
{busyKey === key ? '处理中…' : '卸载'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderAvailableGroup = (): React.ReactElement | null => {
|
||||||
|
if (!availableEntries.length) return null
|
||||||
|
return (
|
||||||
|
<section className="report-template-group">
|
||||||
|
<div className="report-template-group-title">模板市场</div>
|
||||||
|
<div className="report-template-list">
|
||||||
|
{availableEntries.map((entry) => {
|
||||||
|
const key = encodeExternalReportTemplateId(entry.id, entry.version)
|
||||||
|
return (
|
||||||
|
<div key={key} className="report-template-item">
|
||||||
|
{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{entry.version} · {entry.author}
|
||||||
|
</div>
|
||||||
|
<div className="report-template-title">{entry.name}</div>
|
||||||
|
<div className="report-template-tagline">{entry.description}</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={(event) =>
|
||||||
|
openPreview(
|
||||||
|
{
|
||||||
|
kind: 'external',
|
||||||
|
id: entry.id,
|
||||||
|
version: entry.version,
|
||||||
|
name: entry.name,
|
||||||
|
description: entry.description,
|
||||||
|
installed: false,
|
||||||
|
platform: entry.platform,
|
||||||
|
preview: entry.preview
|
||||||
|
},
|
||||||
|
event.currentTarget
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
查看版式
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" disabled={busyKey === key} onClick={() => void install(entry)}>
|
||||||
|
{busyKey === key ? '安装中…' : '安装'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const describeMarketState = (): string => {
|
||||||
|
if (loading) return '正在读取模板市场…'
|
||||||
|
if (error) return `模板市场:${error}`
|
||||||
|
if (!catalogEntries.length && !externalInstalled.length) return '模板市场暂无可安装模板。'
|
||||||
|
return `已安装 ${externalInstalled.length} 个市场模板,另有 ${availableEntries.length} 个可以安装。`
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="report-market-page">
|
||||||
|
<header className="report-market-header">
|
||||||
|
<div>
|
||||||
|
<h1>社区模板市场</h1>
|
||||||
|
<p>
|
||||||
|
模板与默认日报读取同一份真实数据,只改变展示方式,适合手机长图分享、桌面归档与团队复盘。
|
||||||
|
这里可以浏览、预览、安装、选用和卸载社区模板。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" size="sm" disabled={loading} onClick={() => void refresh()}>
|
||||||
|
{loading ? '刷新中…' : '刷新'}
|
||||||
|
</Button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section className="report-market-contribute" aria-label="投稿模板">
|
||||||
|
<div>
|
||||||
|
<strong>想把你做的模板分享给其他人?</strong>
|
||||||
|
<p>
|
||||||
|
如果想提供给其他人模板样式,可以来 TraceMemo-Templates
|
||||||
|
项目里提交 PR,方便给其他用户使用。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
className="report-market-contribute-link"
|
||||||
|
href={TEMPLATE_REPOSITORY_URL}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
前往提交模板
|
||||||
|
</a>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<p className="report-market-summary" role="status">
|
||||||
|
{describeMarketState()}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="report-market-body">
|
||||||
|
<section className="report-template-group">
|
||||||
|
<div className="report-template-group-title">默认模板</div>
|
||||||
|
<div className="report-template-list">{renderBuiltinRow(DEFAULT_REPORT_TEMPLATE)}</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="report-template-group">
|
||||||
|
<div className="report-template-group-title">手机端 · 375–414 px</div>
|
||||||
|
<div className="report-template-list">
|
||||||
|
{REPORT_TEMPLATES.filter((template) => template.platform === 'mobile').map(
|
||||||
|
renderBuiltinRow
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="report-template-group">
|
||||||
|
<div className="report-template-group-title">电脑端 · 1280–1920 px</div>
|
||||||
|
<div className="report-template-list">
|
||||||
|
{REPORT_TEMPLATES.filter((template) => template.platform === 'desktop').map(
|
||||||
|
renderBuiltinRow
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{renderInstalledGroup()}
|
||||||
|
{renderAvailableGroup()}
|
||||||
|
|
||||||
|
{catalog?.generatedAt ? (
|
||||||
|
<p className="report-market-footnote">
|
||||||
|
目录更新于 {new Date(catalog.generatedAt).toLocaleString('zh-CN')}
|
||||||
|
{catalog.source?.commit ? ` · ${catalog.source.commit.slice(0, 8)}` : ''}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ReportTemplatePreviewDialog
|
||||||
|
previewing={previewing}
|
||||||
|
restoreFocusRef={previewTriggerRef}
|
||||||
|
onClose={() => setPreviewing(null)}
|
||||||
|
onConfirm={(item) => {
|
||||||
|
onChange(
|
||||||
|
item.kind === 'builtin'
|
||||||
|
? item.template.id
|
||||||
|
: encodeExternalReportTemplateId(item.id, item.version)
|
||||||
|
)
|
||||||
|
setPreviewing(null)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
DEFAULT_REPORT_TEMPLATE,
|
||||||
|
type ReportTemplateDefinition
|
||||||
|
} from '../../../../shared/report-templates'
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle
|
||||||
|
} from '../ui'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日报模板预览的共享部分:生成页的模板选择器与社区模板市场页共用同一套
|
||||||
|
* 卡片示意图与预览弹窗,避免两处各维护一份。
|
||||||
|
*/
|
||||||
|
export type ReportTemplatePreviewItem =
|
||||||
|
| { kind: 'builtin'; template: ReportTemplateDefinition }
|
||||||
|
| {
|
||||||
|
kind: 'external'
|
||||||
|
id: string
|
||||||
|
version: string
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
installed: boolean
|
||||||
|
platform?: 'default' | 'mobile' | 'desktop'
|
||||||
|
preview?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const reportTemplatePlatformLabel = (
|
||||||
|
platform?: 'default' | 'mobile' | 'desktop'
|
||||||
|
): string => (platform === 'desktop' ? '桌面宽屏' : platform === 'default' ? '经典长图' : '手机长图')
|
||||||
|
|
||||||
|
export const TemplateDiagram = ({
|
||||||
|
template
|
||||||
|
}: {
|
||||||
|
template: ReportTemplateDefinition
|
||||||
|
}): React.ReactElement => (
|
||||||
|
<div className={`report-template-diagram diagram-${template.id}`} aria-hidden="true">
|
||||||
|
<div className="diagram-masthead">
|
||||||
|
<i />
|
||||||
|
<b />
|
||||||
|
<span />
|
||||||
|
</div>
|
||||||
|
<div className="diagram-kpis">
|
||||||
|
<i />
|
||||||
|
<i />
|
||||||
|
<i />
|
||||||
|
<i />
|
||||||
|
</div>
|
||||||
|
<div className="diagram-content">
|
||||||
|
<div className="diagram-column diagram-column-a">
|
||||||
|
<b />
|
||||||
|
<span />
|
||||||
|
<span />
|
||||||
|
</div>
|
||||||
|
<div className="diagram-column diagram-column-b">
|
||||||
|
<b />
|
||||||
|
<span />
|
||||||
|
<span />
|
||||||
|
<span />
|
||||||
|
</div>
|
||||||
|
<div className="diagram-column diagram-column-c">
|
||||||
|
<b />
|
||||||
|
<span />
|
||||||
|
<span />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
export function ReportTemplatePreviewDialog({
|
||||||
|
previewing,
|
||||||
|
confirmLabel = '选择此模板',
|
||||||
|
restoreFocusRef,
|
||||||
|
onClose,
|
||||||
|
onConfirm
|
||||||
|
}: {
|
||||||
|
previewing: ReportTemplatePreviewItem | null
|
||||||
|
confirmLabel?: string
|
||||||
|
restoreFocusRef: React.RefObject<HTMLButtonElement | null>
|
||||||
|
onClose: () => void
|
||||||
|
onConfirm: (item: ReportTemplatePreviewItem) => void
|
||||||
|
}): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={Boolean(previewing)}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) onClose()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{previewing && (
|
||||||
|
<DialogContent
|
||||||
|
className="report-template-preview-card max-h-[90vh] max-w-[520px] overflow-y-auto bg-surface-muted p-[18px]"
|
||||||
|
onCloseAutoFocus={(event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
restoreFocusRef.current?.focus()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogHeader className="pr-8">
|
||||||
|
<div className="report-template-preview-heading">
|
||||||
|
<div>
|
||||||
|
<span>
|
||||||
|
{previewing.kind === 'builtin'
|
||||||
|
? previewing.template.label
|
||||||
|
: `市场 · v${previewing.version}`}
|
||||||
|
</span>
|
||||||
|
<DialogTitle>
|
||||||
|
{previewing.kind === 'builtin' ? previewing.template.name : previewing.name}
|
||||||
|
</DialogTitle>
|
||||||
|
</div>
|
||||||
|
<em>
|
||||||
|
{reportTemplatePlatformLabel(
|
||||||
|
previewing.kind === 'builtin'
|
||||||
|
? previewing.template.platform
|
||||||
|
: previewing.platform
|
||||||
|
)}
|
||||||
|
</em>
|
||||||
|
</div>
|
||||||
|
<DialogDescription>
|
||||||
|
{previewing.kind === 'builtin'
|
||||||
|
? previewing.template.tagline
|
||||||
|
: previewing.description}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
{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>
|
||||||
|
<DialogFooter className="report-template-preview-actions">
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button variant="outline">关闭</Button>
|
||||||
|
</DialogClose>
|
||||||
|
{previewing.kind === 'builtin' || previewing.installed ? (
|
||||||
|
<Button onClick={() => onConfirm(previewing)}>{confirmLabel}</Button>
|
||||||
|
) : (
|
||||||
|
<Button disabled>请先安装</Button>
|
||||||
|
)}
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
)}
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -13,16 +13,15 @@ import type {
|
|||||||
import type { InstalledReportTemplate } from '../../../../shared/report-template-package'
|
import type { InstalledReportTemplate } from '../../../../shared/report-template-package'
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Dialog,
|
|
||||||
DialogClose,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
RadioGroup,
|
RadioGroup,
|
||||||
RadioGroupItem
|
RadioGroupItem
|
||||||
} from '../ui'
|
} from '../ui'
|
||||||
|
import {
|
||||||
|
ReportTemplatePreviewDialog,
|
||||||
|
TemplateDiagram,
|
||||||
|
reportTemplatePlatformLabel,
|
||||||
|
type ReportTemplatePreviewItem
|
||||||
|
} from './ReportTemplatePreview'
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
SelectableReportTemplateId,
|
SelectableReportTemplateId,
|
||||||
@@ -35,67 +34,15 @@ interface ReportTemplateSelectorProps {
|
|||||||
disabled?: boolean
|
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
|
|
||||||
}: {
|
|
||||||
template: ReportTemplateDefinition
|
|
||||||
}): React.ReactElement => (
|
|
||||||
<div className={`report-template-diagram diagram-${template.id}`} aria-hidden="true">
|
|
||||||
<div className="diagram-masthead">
|
|
||||||
<i />
|
|
||||||
<b />
|
|
||||||
<span />
|
|
||||||
</div>
|
|
||||||
<div className="diagram-kpis">
|
|
||||||
<i />
|
|
||||||
<i />
|
|
||||||
<i />
|
|
||||||
<i />
|
|
||||||
</div>
|
|
||||||
<div className="diagram-content">
|
|
||||||
<div className="diagram-column diagram-column-a">
|
|
||||||
<b />
|
|
||||||
<span />
|
|
||||||
<span />
|
|
||||||
</div>
|
|
||||||
<div className="diagram-column diagram-column-b">
|
|
||||||
<b />
|
|
||||||
<span />
|
|
||||||
<span />
|
|
||||||
<span />
|
|
||||||
</div>
|
|
||||||
<div className="diagram-column diagram-column-c">
|
|
||||||
<b />
|
|
||||||
<span />
|
|
||||||
<span />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
export const ReportTemplateSelector: React.FC<ReportTemplateSelectorProps> = ({
|
export const ReportTemplateSelector: React.FC<ReportTemplateSelectorProps> = ({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
disabled
|
disabled
|
||||||
}) => {
|
}) => {
|
||||||
const [previewing, setPreviewing] = useState<PreviewItem | null>(null)
|
const [previewing, setPreviewing] = useState<ReportTemplatePreviewItem | null>(null)
|
||||||
const [installed, setInstalled] = useState<InstalledReportTemplate[]>([])
|
const [installed, setInstalled] = useState<InstalledReportTemplate[]>([])
|
||||||
const [catalog, setCatalog] = useState<ReportTemplateCatalog | null>(null)
|
const [catalog, setCatalog] = useState<ReportTemplateCatalog | null>(null)
|
||||||
const [marketError, setMarketError] = useState('')
|
const [marketError, setMarketError] = useState('')
|
||||||
const [marketBusyKey, setMarketBusyKey] = useState('')
|
|
||||||
const previewTriggerRef = useRef<HTMLButtonElement | null>(null)
|
const previewTriggerRef = useRef<HTMLButtonElement | null>(null)
|
||||||
const mobileTemplates = REPORT_TEMPLATES.filter((template) => template.platform === 'mobile')
|
const mobileTemplates = REPORT_TEMPLATES.filter((template) => template.platform === 'mobile')
|
||||||
const desktopTemplates = REPORT_TEMPLATES.filter((template) => template.platform === 'desktop')
|
const desktopTemplates = REPORT_TEMPLATES.filter((template) => template.platform === 'desktop')
|
||||||
@@ -133,58 +80,12 @@ export const ReportTemplateSelector: React.FC<ReportTemplateSelectorProps> = ({
|
|||||||
[installed]
|
[installed]
|
||||||
)
|
)
|
||||||
const catalogEntries = catalog?.templates || []
|
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 =>
|
const platformLabel = reportTemplatePlatformLabel
|
||||||
platform === 'desktop' ? '桌面宽屏' : platform === 'default' ? '经典长图' : '手机长图'
|
|
||||||
|
|
||||||
const catalogEntryFor = (template: InstalledReportTemplate): ReportTemplateCatalogEntry | undefined =>
|
const catalogEntryFor = (template: InstalledReportTemplate): ReportTemplateCatalogEntry | undefined =>
|
||||||
catalogEntries.find((entry) => entry.id === template.id && entry.version === template.version)
|
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 = (
|
const renderGroup = (
|
||||||
title: string,
|
title: string,
|
||||||
templates: readonly ReportTemplateDefinition[]
|
templates: readonly ReportTemplateDefinition[]
|
||||||
@@ -276,7 +177,6 @@ export const ReportTemplateSelector: React.FC<ReportTemplateSelectorProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -296,69 +196,6 @@ export const ReportTemplateSelector: React.FC<ReportTemplateSelectorProps> = ({
|
|||||||
>
|
>
|
||||||
查看版式
|
查看版式
|
||||||
</Button>
|
</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>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
@@ -367,76 +204,19 @@ export const ReportTemplateSelector: React.FC<ReportTemplateSelectorProps> = ({
|
|||||||
)}
|
)}
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
{marketError && <p className="report-inline-error">模板市场:{marketError}</p>}
|
{marketError && <p className="report-inline-error">模板市场:{marketError}</p>}
|
||||||
<Dialog
|
<ReportTemplatePreviewDialog
|
||||||
open={Boolean(previewing)}
|
previewing={previewing}
|
||||||
onOpenChange={(open) => {
|
restoreFocusRef={previewTriggerRef}
|
||||||
if (!open) setPreviewing(null)
|
onClose={() => setPreviewing(null)}
|
||||||
}}
|
onConfirm={(item) => {
|
||||||
>
|
|
||||||
{previewing && (
|
|
||||||
<DialogContent
|
|
||||||
className="report-template-preview-card max-h-[90vh] max-w-[520px] overflow-y-auto bg-surface-muted p-[18px]"
|
|
||||||
onCloseAutoFocus={(event) => {
|
|
||||||
event.preventDefault()
|
|
||||||
previewTriggerRef.current?.focus()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DialogHeader className="pr-8">
|
|
||||||
<div className="report-template-preview-heading">
|
|
||||||
<div>
|
|
||||||
<span>
|
|
||||||
{previewing.kind === 'builtin'
|
|
||||||
? previewing.template.label
|
|
||||||
: `市场 · v${previewing.version}`}
|
|
||||||
</span>
|
|
||||||
<DialogTitle>
|
|
||||||
{previewing.kind === 'builtin' ? previewing.template.name : previewing.name}
|
|
||||||
</DialogTitle>
|
|
||||||
</div>
|
|
||||||
<em>
|
|
||||||
{platformLabel(
|
|
||||||
previewing.kind === 'builtin' ? previewing.template.platform : previewing.platform
|
|
||||||
)}
|
|
||||||
</em>
|
|
||||||
</div>
|
|
||||||
<DialogDescription>
|
|
||||||
{previewing.kind === 'builtin' ? previewing.template.tagline : previewing.description}
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
{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>
|
|
||||||
<DialogFooter className="report-template-preview-actions">
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Button variant="outline">关闭</Button>
|
|
||||||
</DialogClose>
|
|
||||||
{previewing.kind === 'builtin' || previewing.installed ? (
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
onChange(
|
onChange(
|
||||||
previewing.kind === 'builtin'
|
item.kind === 'builtin'
|
||||||
? previewing.template.id
|
? item.template.id
|
||||||
: encodeExternalReportTemplateId(previewing.id, previewing.version)
|
: encodeExternalReportTemplateId(item.id, item.version)
|
||||||
)
|
)
|
||||||
setPreviewing(null)
|
setPreviewing(null)
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
选择此模板
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button disabled>请先安装</Button>
|
|
||||||
)}
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
)}
|
|
||||||
</Dialog>
|
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -986,3 +986,102 @@
|
|||||||
color: var(--wxex-text-muted);
|
color: var(--wxex-text-muted);
|
||||||
font: 11px/17px var(--wxex-font);
|
font: 11px/17px var(--wxex-font);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================================ */
|
||||||
|
/* 社区模板市场页 */
|
||||||
|
/* ============================================================ */
|
||||||
|
.report-market-page {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 28px clamp(20px, 4vw, 56px) 40px;
|
||||||
|
color: var(--wxex-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-market-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-market-header h1 {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--wxex-text-primary);
|
||||||
|
font: 700 24px/32px var(--wxex-font);
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-market-header p {
|
||||||
|
margin: 5px 0 0;
|
||||||
|
max-width: 720px;
|
||||||
|
color: var(--wxex-text-secondary);
|
||||||
|
font: 13px/20px var(--wxex-font);
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-market-summary {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--wxex-text-muted);
|
||||||
|
font: 12px/18px var(--wxex-font);
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-market-body {
|
||||||
|
display: grid;
|
||||||
|
gap: 22px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-market-footnote {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--wxex-text-muted);
|
||||||
|
font: 11px/17px var(--wxex-font);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.report-market-header {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-market-contribute {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 18px;
|
||||||
|
padding: 16px 18px;
|
||||||
|
border: 1px solid var(--wxex-border);
|
||||||
|
border-radius: var(--wxex-radius-lg);
|
||||||
|
background: var(--wxex-bg-elevated);
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-market-contribute strong {
|
||||||
|
display: block;
|
||||||
|
color: var(--wxex-text-primary);
|
||||||
|
font: 700 13px/20px var(--wxex-font);
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-market-contribute p {
|
||||||
|
margin: 4px 0 0;
|
||||||
|
max-width: 640px;
|
||||||
|
color: var(--wxex-text-secondary);
|
||||||
|
font: 12px/18px var(--wxex-font);
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-market-contribute-link {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: 1px solid var(--wxex-border);
|
||||||
|
border-radius: var(--wxex-radius-md);
|
||||||
|
color: var(--wxex-text-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
font: 600 12px/18px var(--wxex-font);
|
||||||
|
text-decoration: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-market-contribute-link:hover {
|
||||||
|
border-color: var(--wxex-brand);
|
||||||
|
color: var(--wxex-brand);
|
||||||
|
}
|
||||||
|
|||||||
@@ -911,10 +911,23 @@ describe('daily report controls', () => {
|
|||||||
expect(onChange).toHaveBeenCalledWith('mobile-magazine')
|
expect(onChange).toHaveBeenCalledWith('mobile-magazine')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does not select a market template before it is installed', async () => {
|
it('keeps market browsing on the community page instead of the generation form', async () => {
|
||||||
const user = userEvent.setup()
|
const user = userEvent.setup()
|
||||||
const onChange = vi.fn()
|
const onChange = vi.fn()
|
||||||
Object.assign(window.api, {
|
Object.assign(window.api, {
|
||||||
|
listReportTemplates: vi.fn(async () => [
|
||||||
|
{
|
||||||
|
id: 'community.github.tracememo.quickread',
|
||||||
|
version: '1.0.0',
|
||||||
|
interfaceVersion: '1',
|
||||||
|
source: 'installed' as const,
|
||||||
|
name: '极简速读',
|
||||||
|
author: 'fixture',
|
||||||
|
entryPath: '/tmp/entry.html',
|
||||||
|
capture: { width: 414, maxWidth: 414, maxHeight: 2000 },
|
||||||
|
license: { spdx: 'MIT' }
|
||||||
|
}
|
||||||
|
]),
|
||||||
listReportTemplateCatalog: vi.fn(async () => ({
|
listReportTemplateCatalog: vi.fn(async () => ({
|
||||||
success: true,
|
success: true,
|
||||||
catalog: {
|
catalog: {
|
||||||
@@ -931,11 +944,10 @@ describe('daily report controls', () => {
|
|||||||
tags: [],
|
tags: [],
|
||||||
license: 'MIT',
|
license: 'MIT',
|
||||||
minAppVersion: null,
|
minAppVersion: null,
|
||||||
download: 'https://raw.githubusercontent.com/Wxw-Gu/TraceMemo/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/packages/community.github.example.market/1.0.0/template.zip',
|
download: 'https://example.com/template.zip',
|
||||||
sizeBytes: 1,
|
sizeBytes: 1,
|
||||||
sha256: 'a'.repeat(64),
|
sha256: 'a'.repeat(64),
|
||||||
preview: 'https://raw.githubusercontent.com/Wxw-Gu/TraceMemo/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/previews/community.github.example.market.png',
|
status: 'published' as const
|
||||||
status: 'published'
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -943,12 +955,18 @@ describe('daily report controls', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
render(<ReportTemplateSelector value="v1" onChange={onChange} />)
|
render(<ReportTemplateSelector value="v1" onChange={onChange} />)
|
||||||
const marketItem = (await screen.findByText('市场测试模板')).closest('.report-template-item')
|
|
||||||
expect(marketItem).not.toBeNull()
|
// 已安装的市场模板仍可在生成页选用,但不再提供卸载入口。
|
||||||
await user.click(within(marketItem!).getByRole('button', { name: '查看版式' }))
|
expect(await screen.findByText('极简速读')).toBeVisible()
|
||||||
const dialog = screen.getByRole('dialog', { name: '市场测试模板' })
|
expect(screen.getByText('已安装市场模板')).toBeVisible()
|
||||||
expect(within(dialog).getByRole('button', { name: '请先安装' })).toBeDisabled()
|
expect(screen.queryByRole('button', { name: '卸载' })).not.toBeInTheDocument()
|
||||||
expect(within(dialog).queryByRole('button', { name: '选择此模板' })).not.toBeInTheDocument()
|
// 未安装的远端模板只出现在社区模板市场页。
|
||||||
expect(onChange).not.toHaveBeenCalled()
|
expect(screen.queryByText('市场测试模板')).not.toBeInTheDocument()
|
||||||
|
expect(screen.queryByText('安装')).not.toBeInTheDocument()
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('radio', { name: /极简速读/ }))
|
||||||
|
expect(onChange).toHaveBeenCalledWith(
|
||||||
|
'external:community.github.tracememo.quickread@1.0.0'
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,204 @@
|
|||||||
|
import { render, screen, waitFor, within } from '@testing-library/react'
|
||||||
|
import userEvent from '@testing-library/user-event'
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { ReportTemplateMarketWorkspace } from '../../src/renderer/src/components/reports/ReportTemplateMarketWorkspace'
|
||||||
|
|
||||||
|
const installed = {
|
||||||
|
id: 'community.github.tracememo.quickread',
|
||||||
|
version: '1.0.0',
|
||||||
|
interfaceVersion: '1',
|
||||||
|
source: 'installed' as const,
|
||||||
|
name: '极简速读',
|
||||||
|
author: 'fixture',
|
||||||
|
entryPath: '/tmp/entry.html',
|
||||||
|
capture: { width: 414, maxWidth: 414, maxHeight: 2000 },
|
||||||
|
license: { spdx: 'MIT' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const availableEntry = {
|
||||||
|
id: 'community.github.tracememo.paperdaily',
|
||||||
|
version: '1.0.2',
|
||||||
|
interfaceVersion: '1',
|
||||||
|
name: '霓光指挥日报',
|
||||||
|
description: '霓光主题的桌面指挥日报。',
|
||||||
|
author: 'fixture',
|
||||||
|
platform: 'desktop' as const,
|
||||||
|
tags: [],
|
||||||
|
license: 'MIT',
|
||||||
|
minAppVersion: null,
|
||||||
|
download: 'https://example.com/template.zip',
|
||||||
|
sizeBytes: 1,
|
||||||
|
sha256: 'a'.repeat(64),
|
||||||
|
preview: 'https://example.com/preview.png',
|
||||||
|
status: 'published' as const
|
||||||
|
}
|
||||||
|
|
||||||
|
const listReportTemplates = vi.fn()
|
||||||
|
const listReportTemplateCatalog = vi.fn()
|
||||||
|
const installReportTemplateFromCatalog = vi.fn()
|
||||||
|
const uninstallReportTemplate = vi.fn()
|
||||||
|
|
||||||
|
describe('ReportTemplateMarketWorkspace', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
listReportTemplates.mockReset().mockResolvedValue([installed])
|
||||||
|
listReportTemplateCatalog.mockReset().mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
catalog: { schemaVersion: '1', status: 'published', templates: [availableEntry] }
|
||||||
|
})
|
||||||
|
installReportTemplateFromCatalog.mockReset().mockResolvedValue({ success: true })
|
||||||
|
uninstallReportTemplate.mockReset().mockResolvedValue({ success: true })
|
||||||
|
Object.defineProperty(window, 'api', {
|
||||||
|
configurable: true,
|
||||||
|
value: {
|
||||||
|
listReportTemplates,
|
||||||
|
listReportTemplateCatalog,
|
||||||
|
installReportTemplateFromCatalog,
|
||||||
|
uninstallReportTemplate
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('lists builtin, installed and installable market templates with all market actions', async () => {
|
||||||
|
render(<ReportTemplateMarketWorkspace value="v1" onChange={vi.fn()} />)
|
||||||
|
|
||||||
|
expect(await screen.findByText('极简速读')).toBeVisible()
|
||||||
|
expect(screen.getByText('霓光指挥日报')).toBeVisible()
|
||||||
|
expect(screen.getByText('经典日报')).toBeVisible()
|
||||||
|
expect(screen.getByText('已安装市场模板')).toBeVisible()
|
||||||
|
expect(screen.getByText('已安装 1 个市场模板,另有 1 个可以安装。')).toBeVisible()
|
||||||
|
expect(screen.getByRole('button', { name: '安装' })).toBeEnabled()
|
||||||
|
expect(screen.getByRole('button', { name: '卸载' })).toBeEnabled()
|
||||||
|
expect(screen.queryByRole('button', { name: '当前使用' })).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('links to the community template repository for submitting new templates', async () => {
|
||||||
|
render(<ReportTemplateMarketWorkspace value="v1" onChange={vi.fn()} />)
|
||||||
|
|
||||||
|
const link = await screen.findByRole('link', { name: '前往提交模板' })
|
||||||
|
expect(link).toHaveAttribute('href', 'https://github.com/Wxw-Gu/TraceMemo-Templates')
|
||||||
|
expect(link).toHaveAttribute('target', '_blank')
|
||||||
|
expect(
|
||||||
|
screen.getByText(
|
||||||
|
'如果想提供给其他人模板样式,可以来 TraceMemo-Templates 项目里提交 PR,方便给其他用户使用。'
|
||||||
|
)
|
||||||
|
).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uninstalls a market template and refreshes the list', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
uninstallReportTemplate.mockImplementation(async () => {
|
||||||
|
listReportTemplates.mockResolvedValue([])
|
||||||
|
return { success: true }
|
||||||
|
})
|
||||||
|
render(<ReportTemplateMarketWorkspace value="v1" onChange={vi.fn()} />)
|
||||||
|
|
||||||
|
await user.click(await screen.findByRole('button', { name: '卸载' }))
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(uninstallReportTemplate).toHaveBeenCalledWith(installed.id, installed.version)
|
||||||
|
)
|
||||||
|
expect(await screen.findByText('已安装 0 个市场模板,另有 1 个可以安装。')).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('falls back to the default template when the active market template is uninstalled', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
const onChange = vi.fn()
|
||||||
|
uninstallReportTemplate.mockResolvedValue({ success: true })
|
||||||
|
render(
|
||||||
|
<ReportTemplateMarketWorkspace
|
||||||
|
value={`external:${installed.id}@${installed.version}`}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
await user.click(await screen.findByRole('button', { name: '卸载' }))
|
||||||
|
|
||||||
|
expect(onChange).toHaveBeenCalledWith('v1')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('reports an uninstall failure without dropping the installed template', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
uninstallReportTemplate.mockResolvedValue({ success: false, error: '模板正在使用中' })
|
||||||
|
render(<ReportTemplateMarketWorkspace value="v1" onChange={vi.fn()} />)
|
||||||
|
|
||||||
|
await user.click(await screen.findByRole('button', { name: '卸载' }))
|
||||||
|
|
||||||
|
expect(await screen.findByText('模板市场:模板正在使用中')).toBeVisible()
|
||||||
|
expect(screen.getByText('极简速读')).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('installs a market template and refreshes the list', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
installReportTemplateFromCatalog.mockImplementation(async () => {
|
||||||
|
listReportTemplates.mockResolvedValue([installed, { ...availableEntry, source: 'installed' }])
|
||||||
|
listReportTemplateCatalog.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
catalog: { schemaVersion: '1', status: 'published', templates: [availableEntry] }
|
||||||
|
})
|
||||||
|
return { success: true }
|
||||||
|
})
|
||||||
|
render(<ReportTemplateMarketWorkspace value="v1" onChange={vi.fn()} />)
|
||||||
|
|
||||||
|
await user.click(await screen.findByRole('button', { name: '安装' }))
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(installReportTemplateFromCatalog).toHaveBeenCalledWith(
|
||||||
|
availableEntry.id,
|
||||||
|
availableEntry.version
|
||||||
|
)
|
||||||
|
)
|
||||||
|
expect(await screen.findByText('已安装 2 个市场模板,另有 0 个可以安装。')).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('reports an install failure without losing the catalog', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
installReportTemplateFromCatalog.mockResolvedValue({ success: false, error: '下载校验失败' })
|
||||||
|
render(<ReportTemplateMarketWorkspace value="v1" onChange={vi.fn()} />)
|
||||||
|
|
||||||
|
await user.click(await screen.findByRole('button', { name: '安装' }))
|
||||||
|
|
||||||
|
expect(await screen.findByText('模板市场:下载校验失败')).toBeVisible()
|
||||||
|
expect(screen.getByText('霓光指挥日报')).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('previews a market template and can switch to it once installed', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
const onChange = vi.fn()
|
||||||
|
render(<ReportTemplateMarketWorkspace value="v1" onChange={onChange} />)
|
||||||
|
|
||||||
|
const installedRow = (await screen.findByText('极简速读')).closest('.report-template-item')!
|
||||||
|
await user.click(within(installedRow as HTMLElement).getByRole('button', { name: '查看版式' }))
|
||||||
|
const dialog = await screen.findByRole('dialog', { name: '极简速读' })
|
||||||
|
await user.click(within(dialog).getByRole('button', { name: '选择此模板' }))
|
||||||
|
|
||||||
|
expect(onChange).toHaveBeenCalledWith(`external:${installed.id}@${installed.version}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not let an uninstalled market template be selected from the preview', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
const onChange = vi.fn()
|
||||||
|
render(<ReportTemplateMarketWorkspace value="v1" onChange={onChange} />)
|
||||||
|
|
||||||
|
const availableRow = (await screen.findByText('霓光指挥日报')).closest('.report-template-item')!
|
||||||
|
await user.click(within(availableRow as HTMLElement).getByRole('button', { name: '查看版式' }))
|
||||||
|
const dialog = await screen.findByRole('dialog', { name: '霓光指挥日报' })
|
||||||
|
|
||||||
|
expect(within(dialog).getByRole('button', { name: '请先安装' })).toBeDisabled()
|
||||||
|
expect(within(dialog).queryByRole('button', { name: '使用此模板' })).not.toBeInTheDocument()
|
||||||
|
expect(onChange).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('marks the active market template and hides the switch action', async () => {
|
||||||
|
render(
|
||||||
|
<ReportTemplateMarketWorkspace
|
||||||
|
value={`external:${installed.id}@${installed.version}`}
|
||||||
|
onChange={vi.fn()}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
const installedRow = (await screen.findByText('极简速读')).closest('.report-template-item')!
|
||||||
|
expect(within(installedRow as HTMLElement).getByRole('button', { name: '当前使用' })).toBeDisabled()
|
||||||
|
expect(within(installedRow as HTMLElement).queryByRole('button', { name: '使用' })).toBeNull()
|
||||||
|
expect(within(installedRow as HTMLElement).getByRole('button', { name: '卸载' })).toBeEnabled()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { expect, test } from '@playwright/test'
|
||||||
|
import { launchTestApp } from './support/electron'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 社区模板市场现在是日报工作区顶部的第三个标签页,也是模板市场的主入口:
|
||||||
|
* 浏览、预览、安装、选用与卸载都在这里;生成页只保留选用与预览。
|
||||||
|
*/
|
||||||
|
test('REPORT-TEMPLATE-MARKET-TAB-01 browses and installs community templates from the report tabs', async () => {
|
||||||
|
const fixture = await launchTestApp({ templateMarket: 'fixture' })
|
||||||
|
const page = fixture.page
|
||||||
|
const pageErrors: Error[] = []
|
||||||
|
page.on('pageerror', (error) => pageErrors.push(error))
|
||||||
|
try {
|
||||||
|
await page.getByRole('button', { name: '日报' }).click()
|
||||||
|
await page.getByRole('button', { name: '开始生成日报' }).click()
|
||||||
|
|
||||||
|
await page.getByRole('tab', { name: '社区模板市场' }).click()
|
||||||
|
await expect(page.getByRole('heading', { name: '社区模板市场' })).toBeVisible()
|
||||||
|
await expect(page.getByText('经典日报')).toBeVisible()
|
||||||
|
await expect(page.getByText('微信信息流')).toBeVisible()
|
||||||
|
await expect(page.getByText('已安装 0 个市场模板,另有 1 个可以安装。')).toBeVisible()
|
||||||
|
|
||||||
|
// 投稿入口紧跟在标题下方,不需要滚到底部就能看到。
|
||||||
|
const contributeLink = page.getByRole('link', { name: '前往提交模板' })
|
||||||
|
await expect(contributeLink).toBeInViewport()
|
||||||
|
await expect(contributeLink).toHaveAttribute(
|
||||||
|
'href',
|
||||||
|
'https://github.com/Wxw-Gu/TraceMemo-Templates'
|
||||||
|
)
|
||||||
|
await expect(page.getByText('想把你做的模板分享给其他人?')).toBeVisible()
|
||||||
|
|
||||||
|
const marketRow = page.locator('.report-template-item').filter({ hasText: '霓光指挥日报' })
|
||||||
|
await expect(marketRow).toBeVisible()
|
||||||
|
await expect(marketRow.getByRole('button', { name: '安装' })).toBeEnabled()
|
||||||
|
|
||||||
|
await marketRow.getByRole('button', { name: '安装' }).click()
|
||||||
|
await expect(page.getByText('已安装 1 个市场模板,另有 0 个可以安装。')).toBeVisible()
|
||||||
|
await expect(page.getByText('已安装市场模板')).toBeVisible()
|
||||||
|
|
||||||
|
// 已安装后市场页提供卸载入口,未安装时只有安装。
|
||||||
|
await expect(
|
||||||
|
marketRow.getByRole('button', { name: '卸载' })
|
||||||
|
).toBeEnabled()
|
||||||
|
|
||||||
|
await page.getByRole('tab', { name: '今日日报' }).click()
|
||||||
|
await expect(page.getByRole('heading', { name: '生成群聊日报' })).toBeVisible()
|
||||||
|
|
||||||
|
const templateSection = page.getByRole('heading', { name: '日报模板' }).locator('..')
|
||||||
|
// 生成页只保留选用与预览:没有卸载,也不重复展示市场列表。
|
||||||
|
await expect(templateSection.getByText('霓光指挥日报')).toBeVisible()
|
||||||
|
await expect(templateSection.getByRole('button', { name: '卸载' })).toHaveCount(0)
|
||||||
|
await expect(templateSection.getByRole('button', { name: '安装' })).toHaveCount(0)
|
||||||
|
|
||||||
|
await page.getByRole('tab', { name: '社区模板市场' }).click()
|
||||||
|
const roundTripRow = page.locator('.report-template-item').filter({ hasText: '霓光指挥日报' })
|
||||||
|
await roundTripRow.getByRole('button', { name: '卸载' }).click()
|
||||||
|
await expect(page.getByText('已安装 0 个市场模板,另有 1 个可以安装。')).toBeVisible()
|
||||||
|
|
||||||
|
expect(pageErrors).toEqual([])
|
||||||
|
} finally {
|
||||||
|
await fixture.close()
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -991,6 +991,72 @@ handle('report:updateGeneratedTemplate', (request) => ({
|
|||||||
handle('report:deleteGenerated', () => ({ success: true }))
|
handle('report:deleteGenerated', () => ({ success: true }))
|
||||||
handle('report:reveal', () => ({ success: true }))
|
handle('report:reveal', () => ({ success: true }))
|
||||||
handle('copy-image', () => ({ success: true }))
|
handle('copy-image', () => ({ success: true }))
|
||||||
|
// 社区模板市场:默认关闭(返回空目录),测试用例可通过 WXE_E2E_TEMPLATE_MARKET=fixture 打开合成目录。
|
||||||
|
const templateMarketEnabled = process.env.WXE_E2E_TEMPLATE_MARKET === 'fixture'
|
||||||
|
const templateMarketEntry = {
|
||||||
|
id: 'community.github.example.neonboard',
|
||||||
|
version: '1.0.2',
|
||||||
|
interfaceVersion: '1',
|
||||||
|
name: '霓光指挥日报',
|
||||||
|
description: '霓光主题的桌面指挥日报,突出实时讨论、重点消息与行动项。',
|
||||||
|
author: 'TraceMemo 社区',
|
||||||
|
platform: 'desktop',
|
||||||
|
tags: ['desktop'],
|
||||||
|
license: 'MIT',
|
||||||
|
minAppVersion: null,
|
||||||
|
download: 'https://example.com/packages/community.github.example.neonboard/1.0.2/template.zip',
|
||||||
|
sizeBytes: 2048,
|
||||||
|
sha256: 'b'.repeat(64),
|
||||||
|
status: 'published'
|
||||||
|
}
|
||||||
|
const templateMarketInstalled = []
|
||||||
|
handle('report-template:list', () =>
|
||||||
|
templateMarketInstalled.map((entry) => ({
|
||||||
|
id: entry.id,
|
||||||
|
version: entry.version,
|
||||||
|
interfaceVersion: entry.interfaceVersion,
|
||||||
|
source: 'installed',
|
||||||
|
name: entry.name,
|
||||||
|
author: entry.author,
|
||||||
|
entryPath: path.join(userData, 'report-templates', entry.id, 'template.html'),
|
||||||
|
capture: { width: 1280, maxWidth: 1280, maxHeight: 4000 },
|
||||||
|
license: { spdx: entry.license },
|
||||||
|
installedAt: '2026-09-15T00:00:00.000Z'
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
handle('report-template-market:list', () =>
|
||||||
|
templateMarketEnabled
|
||||||
|
? {
|
||||||
|
success: true,
|
||||||
|
catalog: {
|
||||||
|
schemaVersion: '1',
|
||||||
|
generatedAt: '2026-09-15T00:00:00.000Z',
|
||||||
|
status: 'published',
|
||||||
|
templates: [templateMarketEntry]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: { success: true, catalog: { schemaVersion: '1', status: 'published', templates: [] } }
|
||||||
|
)
|
||||||
|
handle('report-template-market:install', (id, version) => {
|
||||||
|
if (
|
||||||
|
!templateMarketEnabled ||
|
||||||
|
String(id) !== templateMarketEntry.id ||
|
||||||
|
String(version) !== templateMarketEntry.version
|
||||||
|
) {
|
||||||
|
return { success: false, error: '模板不存在', code: 'unknown_template' }
|
||||||
|
}
|
||||||
|
if (!templateMarketInstalled.some((entry) => entry.id === id && entry.version === version)) {
|
||||||
|
templateMarketInstalled.push(templateMarketEntry)
|
||||||
|
}
|
||||||
|
return { success: true, catalogEntry: templateMarketEntry }
|
||||||
|
})
|
||||||
|
handle('report-template:uninstall', (id, version) => {
|
||||||
|
const index = templateMarketInstalled.findIndex(
|
||||||
|
(entry) => entry.id === String(id) && entry.version === String(version)
|
||||||
|
)
|
||||||
|
if (index >= 0) templateMarketInstalled.splice(index, 1)
|
||||||
|
return { success: true }
|
||||||
|
})
|
||||||
handle('api:copyText', () => ({ success: true }))
|
handle('api:copyText', () => ({ success: true }))
|
||||||
handle('app-log:write', (entry) => {
|
handle('app-log:write', (entry) => {
|
||||||
const safe = JSON.stringify(entry)
|
const safe = JSON.stringify(entry)
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ export interface TestApplication {
|
|||||||
export async function launchTestApp(
|
export async function launchTestApp(
|
||||||
options: {
|
options: {
|
||||||
mode?: 'connected' | 'disconnected'
|
mode?: 'connected' | 'disconnected'
|
||||||
|
/** 让模板市场 IPC 返回确定性的合成目录,供社区模板市场页视觉/行为用例使用。 */
|
||||||
|
templateMarket?: 'off' | 'fixture'
|
||||||
userData?: string
|
userData?: string
|
||||||
largeContacts?: number
|
largeContacts?: number
|
||||||
corruptCache?: boolean
|
corruptCache?: boolean
|
||||||
@@ -55,7 +57,8 @@ export async function launchTestApp(
|
|||||||
WXE_E2E_UNSIGNED_MAC_UPDATE: options.unsignedMacUpdate ? '1' : '0',
|
WXE_E2E_UNSIGNED_MAC_UPDATE: options.unsignedMacUpdate ? '1' : '0',
|
||||||
WXE_E2E_REALTIME_REORDER: options.realtimeReorder ? '1' : '0',
|
WXE_E2E_REALTIME_REORDER: options.realtimeReorder ? '1' : '0',
|
||||||
WXE_E2E_NOW_MS: options.now ? String(options.now) : '',
|
WXE_E2E_NOW_MS: options.now ? String(options.now) : '',
|
||||||
WXE_E2E_APPEARANCE_THEME: options.appearanceTheme || 'light'
|
WXE_E2E_APPEARANCE_THEME: options.appearanceTheme || 'light',
|
||||||
|
WXE_E2E_TEMPLATE_MARKET: options.templateMarket || 'off'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const page = await app.firstWindow()
|
const page = await app.firstWindow()
|
||||||
|
|||||||
Reference in New Issue
Block a user