# 小说阅读器 
u-novel-reader 面向纯文本小说正文,提供纵向滚动、横向分页、目录、书签、阅读设置、章节预加载、进度恢复和阅读时长统计能力。组件不直接请求网络,章节加载由业务通过事件控制。
# 平台差异说明
| App(vue) | App(nvue) | H5 | 小程序 |
|---|---|---|---|
| √ | √ | √ | √ |
组件正文仅支持纯文本和字符串数组,不解析 HTML、Markdown、图片、音频或视频。App nvue 和小程序上的字体测量、系统安全区和自定义字体能力以平台实际支持为准。
# 基本使用
chapters 提供目录元数据,currentChapter 提供当前章节正文。章节对象建议保持以下结构:
const chapters = [
{
id: 'chapter-1',
index: 0,
title: '第一章 初见',
isLocked: false,
progress: 0
}
]
const currentChapter = {
id: 'chapter-1',
index: 0,
title: '第一章 初见',
content: [
'这是第一段正文。',
'这是第二段正文。'
]
}
index 必须是业务目录中的稳定索引。content 可以是字符串或字符串数组,组件会按换行归一化为段落。
<template>
<view class="reader-page">
<up-novel-reader
book-id="book-1"
:chapters="chapters"
:current-chapter="currentChapter"
@chapter-request="loadChapter"
/>
</view>
</template>
# 受控章节加载
组件不会在内部发起接口请求。用户点击目录、上一章、下一章或翻到边界时,组件触发 chapter-request,业务完成请求后必须更新 currentChapter:
<script setup>
import { ref } from 'vue'
const currentChapter = ref(chapters[0])
const loading = ref(false)
const error = ref(null)
function loadChapter({ targetIndex }) {
const target = chapters[targetIndex]
if (!target || target.isLocked) return
loading.value = true
error.value = null
loadChapterContent(target.id)
.then((content) => {
currentChapter.value = { ...target, content }
})
.catch((requestError) => {
error.value = requestError
})
.finally(() => {
loading.value = false
})
}
</script>
事件只表达切换意图,不会替业务修改 currentChapter。组件会忽略重复请求和过期响应。
# 阅读模式
通过 mode 在两种阅读模式之间切换:
scroll:使用scroll-view纵向阅读,适合连续阅读。page:按正文宽度、字号、行高和段距计算页面,使用左右滑动翻页。
横向分页支持点击左侧区域上一页、右侧区域下一页、中部区域显示或隐藏工具栏。pageAnimation 和设置中的 animation 同时为 true 时才启用翻页动画。
# 目录、书签和预加载
目录数据来自 chapters。章节设置 isLocked: true 时会显示为不可选状态。组件在接近章节末尾时触发 chapter-prefetch,业务可以据此提前加载目标章节,但不应在该事件中直接切换当前章节。
书签由组件维护或通过 bookmarks 受控传入,书签定位同时保存章节 ID 和字符偏移,设置重排后仍可恢复到相近正文位置。
# 阅读设置与主题
settings 支持受控传入;未传入时使用 defaultSettings、本地存储或默认值。内置主题如下:
| 主题 | 背景色 | 正文色 |
|---|---|---|
day | #f7f8fa | #303133 |
paper | #f3ead7 | #51483d |
green | #e7f1e4 | #3f5140 |
night | #202124 | #d6d7da |
dark | #111214 | #e5e7eb |
默认设置:
{
theme: 'day',
fontSize: 18,
lineHeight: 1.8,
paragraphSpacing: 16,
contentWidth: '92%',
fontFamily: 'system',
fontWeight: 400,
animation: true
}
# 持久化
persist 默认为 true。传入 storageKey 时使用自定义键;未传入时,有 bookId 则使用:
uview-plus:novel-reader:${bookId}
本地数据使用版本 1,保存 progress、settings、bookmarks、readingTime 和 updatedAt。状态优先级为:显式受控状态 > 初始状态 > 本地存储 > 默认值。存储损坏或写入失败不会阻塞正文渲染。
# 安全区与工具栏
顶部工具栏使用 u-status-bar 适配状态栏,底部工具栏使用 u-safe-bottom 适配底部指示条,可分别通过 safeAreaInsetTop 和 safeAreaInsetBottom 关闭。
返回图标默认是 arrow-left,但只属于顶部工具栏,不是常驻按钮:工具栏隐藏时返回图标也同步隐藏。autoBack 为 true 时点击返回图标会自动调用 uni.navigateBack(),否则只触发 back 事件。
# 插槽
| 插槽名 | 说明 | 插槽参数 |
|---|---|---|
top | 顶部工具栏右侧扩展区域 | - |
bottom | 底部工具栏扩展区域 | - |
toolbar-extra | 工具栏操作项 | - |
catalog | 替换目录内容 | { chapters, currentChapter, bookmarks, progress } |
settings | 替换设置面板内容 | { settings } |
loading | 章节加载状态 | - |
error | 错误状态 | { error, retry } |
empty | 无章节或正文为空状态 | - |
# API
# Props
| 参数 | 说明 | 类型 | 默认值 | 可选值 |
|---|---|---|---|---|
chapters | 章节目录元数据 | Array | [] | - |
currentChapter | 当前章节及正文 | Object | null | null | - |
loading | 当前章节是否正在加载 | Boolean | false | - |
error | 当前章节加载或排版错误 | Object | null | null | - |
bookId | 书籍唯一标识,用于默认存储键 | String | Number | '' | - |
storageKey | 自定义存储键,优先级高于 bookId | String | '' | - |
persist | 是否启用进度、设置和书签本地持久化 | Boolean | true | - |
initialProgress | 外部传入的初始进度 | Object | null | null | - |
progress | 外部进度状态 | Object | null | null | - |
initialBookmarks | 外部传入的初始书签 | Array | [] | - |
bookmarks | 外部书签状态 | Array | null | null | - |
defaultSettings | 初始阅读设置 | Object | 见上方说明 | - |
settings | 外部设置状态 | Object | null | null | - |
mode | 阅读模式 | String | 'scroll' | scroll、page |
showBack | 工具栏显示后是否显示返回图标 | Boolean | true | - |
autoBack | 点击返回图标后是否自动返回上一页 | Boolean | false | - |
backIcon | 返回图标名称 | String | 'arrow-left' | - |
safeAreaInsetTop | 是否适配顶部状态栏安全区 | Boolean | true | - |
safeAreaInsetBottom | 是否适配底部安全区 | Boolean | true | - |
preloadThreshold | 距离章节末尾多少页时触发预加载 | Number | 2 | - |
pageAnimation | 是否启用横向分页动画 | Boolean | true | - |
controlsAutoHide | 工具栏自动隐藏延迟,0 表示关闭 | Number | 0 | - |
# Events
| 事件名 | 说明 | 回调参数 |
|---|---|---|
back | 点击返回图标时触发 | - |
chapter-request | 请求切换到目标章节 | { targetIndex, targetId, direction } |
chapter-prefetch | 接近章节边界时请求预加载 | { targetIndex, targetId, direction } |
progress-change | 阅读位置发生变化 | { chapterId, chapterIndex, pageIndex, pageCount, charOffset, chapterProgress, totalProgress, scrollTop, updatedAt } |
settings-change | 阅读设置发生变化 | { mode, theme, fontSize, lineHeight, paragraphSpacing, contentWidth, fontFamily, fontWeight, animation } |
bookmark-change | 书签列表发生变化 | 书签数组 |
reading-time-change | 阅读时长发生变化 | { readingTime, delta, updatedAt } |
mode-change | 外部 mode 发生变化 | scroll 或 page |
toolbar-change | 工具栏显隐状态发生变化 | { visible, reason } |
layout-ready | 正文布局完成 | { mode, width, height, pageCount } |
retry | 点击错误状态的重试操作 | 最近一次章节请求 payload,未发起请求时为 null |
# 平台与可访问性
- 支持 H5、小程序、App Vue 和 App nvue;安全区由
u-status-bar、u-safe-bottom和平台窗口信息共同适配。 - 正文只支持纯文本,复杂富文本请使用
up-parse等专用组件。 - 使用自定义字体时,需要确保目标平台已安装或已正确加载字体。
- 关闭
settings.animation或遵循系统减少动态效果时,分页使用无动画过渡。 - 业务处理
chapter-request后必须更新currentChapter,否则组件不会伪造下一章正文。