Clear existing content
This commit is contained in:
@@ -1,151 +0,0 @@
|
||||
import { bookData, specialSections } from "@/lib/book-data"
|
||||
|
||||
export type DocumentationPage = {
|
||||
path: string
|
||||
title: string
|
||||
subtitle?: string
|
||||
caption?: string
|
||||
group: string
|
||||
waitForSelector?: string
|
||||
order?: number
|
||||
}
|
||||
|
||||
function pickRepresentativeReadIds(): { id: string; title: string; group: string }[] {
|
||||
const picks: { id: string; title: string; group: string }[] = []
|
||||
|
||||
picks.push({ id: specialSections.preface.id, title: specialSections.preface.title, group: "阅读页面" })
|
||||
|
||||
for (const part of bookData) {
|
||||
const firstChapter = part.chapters[0]
|
||||
const firstSection = firstChapter?.sections?.[0]
|
||||
if (firstSection) {
|
||||
picks.push({
|
||||
id: firstSection.id,
|
||||
title: `${part.number} ${part.title}|${firstSection.title}`,
|
||||
group: "阅读页面",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const extraReadIds = ["9.11", "9.10", "9.9"]
|
||||
for (const targetId of extraReadIds) {
|
||||
const found = bookData
|
||||
.flatMap((p) => p.chapters)
|
||||
.flatMap((c) => c.sections)
|
||||
.find((s) => s.id === targetId)
|
||||
if (found) {
|
||||
picks.push({ id: found.id, title: found.title, group: "阅读页面" })
|
||||
}
|
||||
}
|
||||
|
||||
const seen = new Set<string>()
|
||||
return picks.filter((p) => {
|
||||
if (seen.has(p.id)) return false
|
||||
seen.add(p.id)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
export function getDocumentationCatalog(): DocumentationPage[] {
|
||||
const pages: DocumentationPage[] = [
|
||||
{
|
||||
path: "/",
|
||||
title: "首页",
|
||||
subtitle: "应用主入口",
|
||||
caption:
|
||||
"首页是用户进入应用的第一个页面,展示书籍封面、简介、目录预览和购买入口。用户可以快速了解内容概要并进行购买决策。",
|
||||
group: "核心页面",
|
||||
order: 1,
|
||||
},
|
||||
{
|
||||
path: "/chapters",
|
||||
title: "目录页",
|
||||
subtitle: "章节浏览与导航",
|
||||
caption:
|
||||
"目录页展示全书的完整章节结构,用户可以浏览各篇、各章内容,查看已解锁和待解锁章节,并快速跳转到阅读页面。",
|
||||
group: "核心页面",
|
||||
order: 2,
|
||||
},
|
||||
{
|
||||
path: "/about",
|
||||
title: "关于页面",
|
||||
subtitle: "作者与产品介绍",
|
||||
caption: "关于页面展示作者信息、产品理念、运营数据等,帮助用户建立对内容的信任和理解。",
|
||||
group: "核心页面",
|
||||
order: 3,
|
||||
},
|
||||
{
|
||||
path: "/my",
|
||||
title: "个人中心",
|
||||
subtitle: "用户账户入口",
|
||||
caption: "个人中心聚合用户的账户信息、购买记录、分销收益等功能入口,是用户管理个人信息的核心页面。",
|
||||
group: "用户中心",
|
||||
order: 4,
|
||||
},
|
||||
{
|
||||
path: "/my/purchases",
|
||||
title: "我的购买",
|
||||
subtitle: "已购内容管理",
|
||||
caption: "展示用户已购买的所有章节,包括购买时间、解锁进度,用户可快速跳转到已购内容继续阅读。",
|
||||
group: "用户中心",
|
||||
order: 5,
|
||||
},
|
||||
{
|
||||
path: "/my/settings",
|
||||
title: "账户设置",
|
||||
subtitle: "个人信息配置",
|
||||
caption: "用户可在此页面管理个人基础信息、通知偏好、隐私设置等账户相关配置。",
|
||||
group: "用户中心",
|
||||
order: 6,
|
||||
},
|
||||
{
|
||||
path: "/my/referral",
|
||||
title: "分销中心",
|
||||
subtitle: "邀请与收益管理",
|
||||
caption: "分销中心展示用户的专属邀请链接、邀请人数统计、收益明细,支持一键分享到朋友圈或Soul派对。",
|
||||
group: "用户中心",
|
||||
order: 7,
|
||||
},
|
||||
{
|
||||
path: "/admin/login",
|
||||
title: "后台登录",
|
||||
subtitle: "管理员入口",
|
||||
caption: "管理后台的登录页面,管理员通过账号密码验证后进入管理系统。",
|
||||
group: "管理后台",
|
||||
order: 8,
|
||||
},
|
||||
{
|
||||
path: "/admin",
|
||||
title: "后台管理",
|
||||
subtitle: "系统配置中心",
|
||||
caption: "管理后台的核心页面,包含数据概览、内容管理、用户管理、支付配置、二维码管理、系统设置等功能模块。",
|
||||
group: "管理后台",
|
||||
order: 9,
|
||||
},
|
||||
{
|
||||
path: "/docs",
|
||||
title: "开发文档",
|
||||
subtitle: "技术与配置说明",
|
||||
caption: "面向开发者和运营人员的技术文档,包含支付接口配置说明、分销规则详解、提现流程等内容。",
|
||||
group: "运营支持",
|
||||
order: 10,
|
||||
},
|
||||
]
|
||||
|
||||
const readPicks = pickRepresentativeReadIds()
|
||||
for (let i = 0; i < readPicks.length; i++) {
|
||||
const pick = readPicks[i]
|
||||
pages.push({
|
||||
path: `/read/${encodeURIComponent(pick.id)}`,
|
||||
title: pick.title,
|
||||
subtitle: "章节阅读",
|
||||
caption: "阅读页面展示章节的完整内容,未购买用户可预览部分内容,付费墙引导购买解锁全文。",
|
||||
group: pick.group,
|
||||
waitForSelector: "main",
|
||||
order: 100 + i,
|
||||
})
|
||||
}
|
||||
|
||||
// Sort by order
|
||||
return pages.sort((a, b) => (a.order || 999) - (b.order || 999))
|
||||
}
|
||||
@@ -1,272 +0,0 @@
|
||||
import {
|
||||
Document,
|
||||
HeadingLevel,
|
||||
ImageRun,
|
||||
Packer,
|
||||
Paragraph,
|
||||
TableOfContents,
|
||||
TextRun,
|
||||
AlignmentType,
|
||||
PageBreak,
|
||||
BorderStyle,
|
||||
} from "docx"
|
||||
import type { DocumentationPage } from "@/lib/documentation/catalog"
|
||||
|
||||
export type DocumentationRenderItem = {
|
||||
page: DocumentationPage
|
||||
screenshotPng?: Buffer
|
||||
error?: string
|
||||
}
|
||||
|
||||
function groupBy<T>(items: T[], getKey: (item: T) => string): Record<string, T[]> {
|
||||
const map: Record<string, T[]> = {}
|
||||
for (const item of items) {
|
||||
const key = getKey(item)
|
||||
if (!map[key]) map[key] = []
|
||||
map[key].push(item)
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
export async function renderDocumentationDocx(items: DocumentationRenderItem[]) {
|
||||
const now = new Date()
|
||||
const title = "Soul派对 - 应用功能文档"
|
||||
const subtitle = `生成时间:${now.toLocaleString("zh-CN", { hour12: false })}`
|
||||
const version = `文档版本:v1.0`
|
||||
|
||||
const children: Paragraph[] = []
|
||||
|
||||
children.push(new Paragraph({ text: "" }))
|
||||
children.push(new Paragraph({ text: "" }))
|
||||
children.push(
|
||||
new Paragraph({
|
||||
text: title,
|
||||
heading: HeadingLevel.TITLE,
|
||||
alignment: AlignmentType.CENTER,
|
||||
}),
|
||||
)
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [new TextRun({ text: subtitle, size: 24 })],
|
||||
alignment: AlignmentType.CENTER,
|
||||
}),
|
||||
)
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [new TextRun({ text: version, size: 20, color: "666666" })],
|
||||
alignment: AlignmentType.CENTER,
|
||||
}),
|
||||
)
|
||||
children.push(new Paragraph({ text: "" }))
|
||||
children.push(new Paragraph({ text: "" }))
|
||||
|
||||
children.push(
|
||||
new Paragraph({
|
||||
text: "文档概述",
|
||||
heading: HeadingLevel.HEADING_1,
|
||||
}),
|
||||
)
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: "本文档自动生成,包含应用程序所有核心页面的功能说明与界面截图。文档按功能模块分组,便于快速查阅和理解应用结构。",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
)
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: `共包含 ${items.length} 个页面,${Object.keys(groupBy(items, (i) => i.page.group)).length} 个功能模块。`,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
)
|
||||
children.push(new Paragraph({ text: "" }))
|
||||
|
||||
children.push(
|
||||
new Paragraph({
|
||||
text: "目录",
|
||||
heading: HeadingLevel.HEADING_1,
|
||||
}),
|
||||
)
|
||||
children.push(
|
||||
new TableOfContents("目录", {
|
||||
hyperlink: true,
|
||||
headingStyleRange: "1-3",
|
||||
}),
|
||||
)
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [new PageBreak()],
|
||||
}),
|
||||
)
|
||||
|
||||
const grouped = groupBy(items, (i) => i.page.group)
|
||||
const groupNames = Object.keys(grouped)
|
||||
|
||||
let pageNumber = 1
|
||||
for (const groupName of groupNames) {
|
||||
children.push(
|
||||
new Paragraph({
|
||||
text: groupName,
|
||||
heading: HeadingLevel.HEADING_1,
|
||||
border: {
|
||||
bottom: { color: "2DD4BF", size: 6, style: BorderStyle.SINGLE },
|
||||
},
|
||||
}),
|
||||
)
|
||||
children.push(new Paragraph({ text: "" }))
|
||||
|
||||
for (const item of grouped[groupName]) {
|
||||
const { page } = item
|
||||
|
||||
children.push(
|
||||
new Paragraph({
|
||||
text: `${pageNumber}. ${page.title}`,
|
||||
heading: HeadingLevel.HEADING_2,
|
||||
}),
|
||||
)
|
||||
|
||||
if (page.subtitle) {
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [new TextRun({ text: page.subtitle, italics: true, color: "666666" })],
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({ text: "页面路径:", bold: true }),
|
||||
new TextRun({ text: page.path, color: "2563EB" }),
|
||||
],
|
||||
}),
|
||||
)
|
||||
|
||||
if (page.caption) {
|
||||
children.push(new Paragraph({ text: "" }))
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [new TextRun({ text: "功能说明:", bold: true })],
|
||||
}),
|
||||
)
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [new TextRun({ text: page.caption })],
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
children.push(new Paragraph({ text: "" }))
|
||||
|
||||
if (item.error) {
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({ text: "截图状态:", bold: true }),
|
||||
new TextRun({ text: `失败 - ${item.error}`, color: "DC2626" }),
|
||||
],
|
||||
}),
|
||||
)
|
||||
} else if (item.screenshotPng) {
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [new TextRun({ text: "界面截图:", bold: true })],
|
||||
}),
|
||||
)
|
||||
children.push(new Paragraph({ text: "" }))
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [
|
||||
new ImageRun({
|
||||
data: item.screenshotPng,
|
||||
transformation: { width: 320, height: 693 },
|
||||
}),
|
||||
],
|
||||
alignment: AlignmentType.CENTER,
|
||||
}),
|
||||
)
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [new TextRun({ text: `图${pageNumber}: ${page.title}界面`, size: 20, color: "666666" })],
|
||||
alignment: AlignmentType.CENTER,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
children.push(new Paragraph({ text: "" }))
|
||||
children.push(new Paragraph({ text: "" }))
|
||||
pageNumber++
|
||||
}
|
||||
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [new PageBreak()],
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
children.push(
|
||||
new Paragraph({
|
||||
text: "附录",
|
||||
heading: HeadingLevel.HEADING_1,
|
||||
}),
|
||||
)
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [new TextRun({ text: "技术说明", bold: true })],
|
||||
}),
|
||||
)
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: "• 截图尺寸:430×932像素 (iPhone 14 Pro Max)",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
)
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: "• 截图方式:Playwright自动化浏览器截图",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
)
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: "• 文档格式:Microsoft Word (.docx)",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
)
|
||||
children.push(new Paragraph({ text: "" }))
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [new TextRun({ text: "本文档由系统自动生成,如有问题请联系技术支持。", color: "666666", size: 20 })],
|
||||
alignment: AlignmentType.CENTER,
|
||||
}),
|
||||
)
|
||||
|
||||
const doc = new Document({
|
||||
title: "Soul派对 - 应用功能文档",
|
||||
description: "自动生成的应用功能文档",
|
||||
creator: "Soul派对文档生成器",
|
||||
sections: [
|
||||
{
|
||||
properties: {},
|
||||
children,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
return await Packer.toBuffer(doc)
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
import type { DocumentationPage } from "@/lib/documentation/catalog"
|
||||
|
||||
export type ScreenshotResult = {
|
||||
page: DocumentationPage
|
||||
screenshotPng?: Buffer
|
||||
error?: string
|
||||
}
|
||||
|
||||
type CaptureOptions = {
|
||||
baseUrl: string
|
||||
timeoutMs: number
|
||||
viewport: { width: number; height: number }
|
||||
}
|
||||
|
||||
export async function captureScreenshots(
|
||||
pages: DocumentationPage[],
|
||||
options: CaptureOptions,
|
||||
): Promise<ScreenshotResult[]> {
|
||||
const { chromium } = await import("playwright")
|
||||
const browser = await chromium.launch({
|
||||
headless: true,
|
||||
args: ["--no-sandbox", "--disable-setuid-sandbox"],
|
||||
})
|
||||
|
||||
try {
|
||||
const results: ScreenshotResult[] = []
|
||||
|
||||
for (const pageInfo of pages) {
|
||||
const page = await browser.newPage({ viewport: options.viewport })
|
||||
try {
|
||||
const captureUrl = new URL("/documentation/capture", options.baseUrl)
|
||||
captureUrl.searchParams.set("path", pageInfo.path)
|
||||
|
||||
console.log(`[v0] Capturing: ${pageInfo.path}`)
|
||||
|
||||
await page.goto(captureUrl.toString(), {
|
||||
waitUntil: "networkidle",
|
||||
timeout: options.timeoutMs,
|
||||
})
|
||||
|
||||
const iframeHandle = await page.waitForSelector('iframe[data-doc-iframe="true"]', {
|
||||
timeout: options.timeoutMs,
|
||||
})
|
||||
|
||||
const frame = await iframeHandle.contentFrame()
|
||||
if (!frame) {
|
||||
throw new Error("无法获取iframe内容")
|
||||
}
|
||||
|
||||
await frame.waitForLoadState("domcontentloaded", { timeout: options.timeoutMs })
|
||||
|
||||
// Allow network to settle
|
||||
await frame.waitForLoadState("networkidle", { timeout: options.timeoutMs }).catch(() => {
|
||||
console.log(`[v0] Network idle timeout for ${pageInfo.path}, continuing...`)
|
||||
})
|
||||
|
||||
if (pageInfo.waitForSelector) {
|
||||
await frame
|
||||
.waitForSelector(pageInfo.waitForSelector, {
|
||||
timeout: options.timeoutMs,
|
||||
})
|
||||
.catch(() => {
|
||||
console.log(`[v0] Selector timeout for ${pageInfo.path}, continuing...`)
|
||||
})
|
||||
}
|
||||
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
const screenshot = await iframeHandle.screenshot({
|
||||
type: "png",
|
||||
animations: "disabled",
|
||||
})
|
||||
|
||||
results.push({
|
||||
page: pageInfo,
|
||||
screenshotPng: Buffer.from(screenshot),
|
||||
})
|
||||
|
||||
console.log(`[v0] Success: ${pageInfo.path}`)
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
console.log(`[v0] Error capturing ${pageInfo.path}: ${message}`)
|
||||
results.push({ page: pageInfo, error: message })
|
||||
} finally {
|
||||
await page.close().catch(() => undefined)
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
} finally {
|
||||
await browser.close().catch(() => undefined)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user