加强了多个组件的预览百分比处理。

在 API 中新增了章节专属的预览百分比,并更新了相关模型和处理器。
改进了阅读场景下确定有效预览百分比的逻辑。
更新了小程序配置,加入了新的阅读页面入口。
This commit is contained in:
Alex-larget
2026-03-25 17:21:55 +08:00
parent d8362bc7a9
commit ae7402fafa
9 changed files with 233 additions and 82 deletions

View File

@@ -20,6 +20,8 @@ export interface SectionItem {
payCount?: number
hotScore?: number
hotRank?: number
/** 章节试读%覆盖(与列表接口 previewPercent 一致) */
previewPercent?: number | null
}
export interface ChapterItem {

View File

@@ -74,6 +74,8 @@ interface SectionListItem {
payCount?: number
hotScore?: number
hotRank?: number
/** 章节级试读比例覆盖,未设则走全局「未付费预览比例」 */
previewPercent?: number | null
}
interface Section {
@@ -88,6 +90,7 @@ interface Section {
payCount?: number
hotScore?: number
hotRank?: number
previewPercent?: number | null
}
interface Chapter {
@@ -114,6 +117,17 @@ interface SectionOrder {
payTime?: string
}
/** 解析接口/列表中的试读比例(兼容数字、字符串、部分代理下划线字段) */
function parsePreviewPercentInput(raw: unknown): number | undefined {
if (raw === null || raw === undefined) return undefined
if (typeof raw === 'number' && Number.isFinite(raw)) return Math.round(raw)
if (typeof raw === 'string' && raw.trim() !== '') {
const n = Number(raw.trim().replace(/%/g, ''))
if (Number.isFinite(n)) return Math.round(n)
}
return undefined
}
interface EditingSection {
id: string
originalId?: string
@@ -125,7 +139,8 @@ interface EditingSection {
isNew?: boolean
isPinned?: boolean
hotScore?: number
previewPercent?: number
/** undefined=未改不写库null=清空用全局;数字=章节覆盖 */
previewPercent?: number | null
editionStandard?: boolean
editionPremium?: boolean
}
@@ -159,6 +174,7 @@ function buildTree(sections: SectionListItem[], hotRankMap: Map<string, number>,
payCount: s.payCount ?? 0,
hotScore: s.hotScore ?? 0,
hotRank: hotRankMap.get(s.id) ?? 0,
previewPercent: parsePreviewPercentInput(s.previewPercent),
})
}
const parts = Array.from(partMap.values()).map((p) => ({
@@ -823,12 +839,24 @@ export function ContentPage() {
section.mid != null && section.mid > 0
? `/api/db/book?action=read&mid=${section.mid}`
: `/api/db/book?action=read&id=${encodeURIComponent(section.id)}`
const data = await get<{ success?: boolean; section?: { title?: string; price?: number; content?: string; editionStandard?: boolean; editionPremium?: boolean }; error?: string }>(
url,
)
const data = await get<{
success?: boolean
section?: {
title?: string
price?: number
content?: string
editionStandard?: boolean
editionPremium?: boolean
previewPercent?: number | null
}
error?: string
}>(url)
if (data?.success && data.section) {
const sec = data.section as { isNew?: boolean; editionStandard?: boolean; editionPremium?: boolean }
const sec = data.section as Record<string, unknown>
const isPremium = sec.editionPremium === true
const fromRead =
parsePreviewPercentInput(sec.previewPercent) ?? parsePreviewPercentInput(sec.preview_percent)
const fromTree = parsePreviewPercentInput(section.previewPercent)
setEditingSection({
id: section.id,
originalId: section.id,
@@ -837,10 +865,11 @@ export function ContentPage() {
content: data.section.content ?? '',
filePath: section.filePath,
isFree: section.isFree || section.price === 0,
isNew: sec.isNew ?? section.isNew,
isNew: (sec.isNew as boolean | undefined) ?? section.isNew,
isPinned: pinnedSectionIds.includes(section.id),
hotScore: section.hotScore ?? 0,
editionStandard: isPremium ? false : (sec.editionStandard ?? true),
previewPercent: fromRead ?? fromTree ?? undefined,
editionStandard: isPremium ? false : ((sec.editionStandard as boolean | undefined) ?? true),
editionPremium: isPremium,
})
} else {
@@ -855,6 +884,7 @@ export function ContentPage() {
isNew: section.isNew,
isPinned: pinnedSectionIds.includes(section.id),
hotScore: section.hotScore ?? 0,
previewPercent: parsePreviewPercentInput(section.previewPercent),
editionStandard: true,
editionPremium: false,
})
@@ -871,6 +901,7 @@ export function ContentPage() {
content: '',
filePath: section.filePath,
isFree: section.isFree,
previewPercent: parsePreviewPercentInput(section.previewPercent),
})
} finally {
setIsLoadingContent(false)
@@ -892,22 +923,27 @@ export function ContentPage() {
const originalId = editingSection.originalId || editingSection.id
const idChanged = editingSection.id !== originalId
const saveBody: Record<string, unknown> = {
id: originalId,
...(idChanged ? { newId: editingSection.id } : {}),
title: editingSection.title,
price: editingSection.isFree ? 0 : editingSection.price,
content,
isFree: editingSection.isFree || editingSection.price === 0,
isNew: editingSection.isNew,
hotScore: editingSection.hotScore,
editionStandard: editingSection.editionPremium ? false : (editingSection.editionStandard ?? true),
editionPremium: editingSection.editionPremium ?? false,
saveToFile: true,
}
if (editingSection.previewPercent === null) {
saveBody.previewPercent = null
} else if (typeof editingSection.previewPercent === 'number' && Number.isFinite(editingSection.previewPercent)) {
saveBody.previewPercent = editingSection.previewPercent
}
const res = await put<{ success?: boolean; error?: string }>(
'/api/db/book',
{
id: originalId,
...(idChanged ? { newId: editingSection.id } : {}),
title: editingSection.title,
price: editingSection.isFree ? 0 : editingSection.price,
content,
isFree: editingSection.isFree || editingSection.price === 0,
isNew: editingSection.isNew,
hotScore: editingSection.hotScore,
previewPercent: editingSection.previewPercent ?? null,
editionStandard: editingSection.editionPremium ? false : (editingSection.editionStandard ?? true),
editionPremium: editingSection.editionPremium ?? false,
saveToFile: true,
},
saveBody,
{ timeout: SAVE_REQUEST_TIMEOUT },
)
const effectiveId = idChanged ? editingSection.id : originalId
@@ -2107,10 +2143,19 @@ export function ContentPage() {
max={100}
className="bg-[#0a1628] border-gray-700 text-white"
placeholder={`全局 ${previewPercent}%`}
value={editingSection.previewPercent ?? ''}
value={editingSection.previewPercent != null ? String(editingSection.previewPercent) : ''}
onChange={(e) => {
const v = e.target.value === '' ? undefined : Math.min(100, Math.max(0, Number(e.target.value)))
setEditingSection({ ...editingSection, previewPercent: v })
const raw = e.target.value.trim()
if (raw === '') {
setEditingSection({ ...editingSection, previewPercent: null })
return
}
const n = Number(raw)
if (!Number.isFinite(n)) return
setEditingSection({
...editingSection,
previewPercent: Math.min(100, Math.max(1, Math.round(n))),
})
}}
/>
</div>
@@ -2404,15 +2449,17 @@ export function ContentPage() {
<div
key={result.id}
className="p-3 rounded-lg bg-[#162840] hover:bg-[#1a3050] cursor-pointer transition-colors"
onClick={() =>
onClick={() => {
const row = sectionsList.find((x) => x.id === result.id)
handleReadSection({
id: result.id,
mid: result.mid,
title: result.title,
price: result.price ?? 1,
filePath: '',
previewPercent: row?.previewPercent,
})
}
}}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
@@ -2564,7 +2611,16 @@ export function ContentPage() {
variant="ghost"
size="sm"
className="text-gray-500 hover:text-[#38bdac] h-6 px-1"
onClick={() => handleReadSection({ id: s.id, mid: s.mid, title: s.title, price: s.price, filePath: '' })}
onClick={() =>
handleReadSection({
id: s.id,
mid: s.mid,
title: s.title,
price: s.price,
filePath: '',
previewPercent: s.previewPercent,
})
}
title="编辑文章"
>
<Edit3 className="w-3 h-3" />