加强了多个组件的预览百分比处理。
在 API 中新增了章节专属的预览百分比,并更新了相关模型和处理器。 改进了阅读场景下确定有效预览百分比的逻辑。 更新了小程序配置,加入了新的阅读页面入口。
This commit is contained in:
@@ -20,6 +20,8 @@ export interface SectionItem {
|
||||
payCount?: number
|
||||
hotScore?: number
|
||||
hotRank?: number
|
||||
/** 章节试读%覆盖(与列表接口 previewPercent 一致) */
|
||||
previewPercent?: number | null
|
||||
}
|
||||
|
||||
export interface ChapterItem {
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user