uview-plus 3.8.55 → 3.8.106

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (229) hide show
  1. package/changelog.md +2147 -1828
  2. package/components/u-action-sheet/props.js +3 -1
  3. package/components/u-action-sheet/u-action-sheet.vue +22 -2
  4. package/components/u-album/props.js +3 -1
  5. package/components/u-alert/props.js +4 -2
  6. package/components/u-avatar/props.js +4 -1
  7. package/components/u-avatar-group/props.js +4 -1
  8. package/components/u-back-top/props.js +4 -1
  9. package/components/u-badge/props.js +4 -1
  10. package/components/u-barcode/u-barcode.vue +44 -15
  11. package/components/u-box/props.js +5 -2
  12. package/components/u-button/props.js +4 -1
  13. package/components/u-button/u-button.vue +2 -5
  14. package/components/u-button/vue.scss +3 -3
  15. package/components/u-calendar/calendar.js +8 -0
  16. package/components/u-calendar/props.js +43 -1
  17. package/components/u-calendar/u-calendar.vue +16 -0
  18. package/components/u-calendar-strip/props.js +3 -1
  19. package/components/u-canvas/u-canvas.vue +1028 -423
  20. package/components/u-car-keyboard/props.js +3 -1
  21. package/components/u-card/props.js +3 -1
  22. package/components/u-cell/props.js +4 -1
  23. package/components/u-cell/u-cell.vue +9 -1
  24. package/components/u-cell-group/props.js +4 -1
  25. package/components/u-checkbox/props.js +4 -1
  26. package/components/u-checkbox/u-checkbox.vue +7 -3
  27. package/components/u-checkbox-group/props.js +3 -1
  28. package/components/u-circle-progress/props.js +4 -1
  29. package/components/u-code/props.js +4 -1
  30. package/components/u-code-input/props.js +4 -1
  31. package/components/u-col/props.js +4 -1
  32. package/components/u-collapse/props.js +4 -1
  33. package/components/u-collapse-item/props.js +4 -1
  34. package/components/u-color-picker/u-color-picker.vue +1 -0
  35. package/components/u-column-notice/props.js +4 -1
  36. package/components/u-count-down/props.js +4 -1
  37. package/components/u-count-to/props.js +4 -1
  38. package/components/u-cropper/u-cropper.vue +95 -51
  39. package/components/u-datetime-picker/props.js +4 -1
  40. package/components/u-datetime-picker/u-datetime-picker.vue +53 -4
  41. package/components/u-divider/props.js +4 -1
  42. package/components/u-dragsort/u-dragsort.vue +57 -32
  43. package/components/u-dropdown/props.js +3 -1
  44. package/components/u-dropdown-item/props.js +3 -1
  45. package/components/u-empty/props.js +4 -1
  46. package/components/u-form/props.js +4 -1
  47. package/components/u-form-item/props.js +4 -1
  48. package/components/u-gap/props.js +4 -1
  49. package/components/u-grid/props.js +4 -1
  50. package/components/u-grid-item/props.js +4 -1
  51. package/components/u-guide/props.js +3 -1
  52. package/components/u-icon/props.js +4 -1
  53. package/components/u-icon/u-icon.vue +11 -2
  54. package/components/u-icon/upicon.ttf +0 -0
  55. package/components/u-icon/util.js +103 -10
  56. package/components/u-image/props.js +4 -1
  57. package/components/u-image/u-image.vue +280 -267
  58. package/components/u-index-anchor/props.js +4 -1
  59. package/components/u-index-item/props.js +2 -1
  60. package/components/u-index-item/u-index-item.vue +1 -1
  61. package/components/u-index-list/props.js +4 -1
  62. package/components/u-index-list/u-index-list.vue +1 -1
  63. package/components/u-input/props.js +3 -1
  64. package/components/u-keyboard/props.js +4 -1
  65. package/components/u-line/props.js +4 -1
  66. package/components/u-line-progress/props.js +6 -3
  67. package/components/u-link/props.js +4 -1
  68. package/components/u-list/props.js +4 -1
  69. package/components/u-list-item/props.js +4 -1
  70. package/components/u-loading-icon/props.js +4 -1
  71. package/components/u-loading-page/props.js +3 -1
  72. package/components/u-loadmore/props.js +4 -1
  73. package/components/u-markdown/u-markdown.vue +45 -3
  74. package/components/u-modal/props.js +4 -1
  75. package/components/u-navbar/props.js +4 -2
  76. package/components/u-navbar/u-navbar.vue +15 -9
  77. package/components/u-no-network/props.js +4 -1
  78. package/components/u-notice-bar/props.js +4 -1
  79. package/components/u-notify/props.js +4 -1
  80. package/components/u-novel-reader/content-normalizer.js +80 -0
  81. package/components/u-novel-reader/layout-engine.js +225 -0
  82. package/components/u-novel-reader/measure-adapter.js +69 -0
  83. package/components/u-novel-reader/novelReader.js +35 -0
  84. package/components/u-novel-reader/persistence.js +144 -0
  85. package/components/u-novel-reader/props.js +100 -0
  86. package/components/u-novel-reader/reader-catalog.vue +234 -0
  87. package/components/u-novel-reader/reader-content.vue +226 -0
  88. package/components/u-novel-reader/reader-core.js +151 -0
  89. package/components/u-novel-reader/reader-settings.vue +325 -0
  90. package/components/u-novel-reader/reader-toolbar.vue +313 -0
  91. package/components/u-novel-reader/theme-vars.scss +49 -0
  92. package/components/u-novel-reader/u-novel-reader.vue +836 -0
  93. package/components/u-number-box/props.js +4 -1
  94. package/components/u-number-box/u-number-box.vue +2 -2
  95. package/components/u-number-keyboard/props.js +4 -1
  96. package/components/u-overlay/props.js +4 -1
  97. package/components/u-pagination/u-pagination.vue +60 -39
  98. package/components/u-parse/props.js +4 -1
  99. package/components/u-picker/props.js +4 -1
  100. package/components/u-picker-column/props.js +3 -1
  101. package/components/u-popover/props.js +2 -1
  102. package/components/u-popup/props.js +4 -1
  103. package/components/u-poster/u-poster.vue +246 -214
  104. package/components/u-qrcode/qrcode.js +105 -97
  105. package/components/u-qrcode/u-qrcode.vue +242 -289
  106. package/components/u-radio/props.js +4 -1
  107. package/components/u-radio/u-radio.vue +7 -3
  108. package/components/u-radio-group/props.js +4 -1
  109. package/components/u-rate/props.js +4 -1
  110. package/components/u-read-more/props.js +4 -1
  111. package/components/u-row/props.js +4 -1
  112. package/components/u-row-notice/props.js +4 -1
  113. package/components/u-safe-bottom/props.js +2 -1
  114. package/components/u-scroll-list/props.js +4 -1
  115. package/components/u-scroll-list/scrollWxs.wxs +1 -1
  116. package/components/u-search/props.js +3 -1
  117. package/components/u-skeleton/props.js +4 -1
  118. package/components/u-slider/props.js +3 -1
  119. package/components/u-slider/u-slider.vue +71 -29
  120. package/components/u-status-bar/props.js +4 -1
  121. package/components/u-steps/props.js +4 -1
  122. package/components/u-steps-item/props.js +4 -1
  123. package/components/u-sticky/props.js +4 -1
  124. package/components/u-sticky/u-sticky.vue +11 -0
  125. package/components/u-subsection/props.js +3 -1
  126. package/components/u-swipe-action/props.js +4 -1
  127. package/components/u-swipe-action-item/index.wxs +43 -25
  128. package/components/u-swipe-action-item/nvue.js +9 -0
  129. package/components/u-swipe-action-item/other.js +32 -21
  130. package/components/u-swipe-action-item/props.js +9 -1
  131. package/components/u-swipe-action-item/swipeActionItem.js +1 -0
  132. package/components/u-swipe-action-item/u-swipe-action-item.vue +42 -4
  133. package/components/u-swiper/props.js +9 -1
  134. package/components/u-swiper/swiper.js +1 -0
  135. package/components/u-swiper/u-swiper.vue +2 -0
  136. package/components/u-swiper-indicator/props.js +4 -1
  137. package/components/u-switch/props.js +4 -1
  138. package/components/u-switch/u-switch.vue +1 -1
  139. package/components/u-tabbar/props.js +3 -1
  140. package/components/u-tabbar/u-tabbar.vue +2 -1
  141. package/components/u-tabbar-item/props.js +4 -1
  142. package/components/u-tabbar-item/u-tabbar-item.vue +64 -21
  143. package/components/u-table/props.js +3 -1
  144. package/components/u-table2/tableRow.vue +155 -69
  145. package/components/u-table2/u-table2.vue +327 -137
  146. package/components/u-tabs/props.js +4 -1
  147. package/components/u-tabs/u-tabs.vue +11 -8
  148. package/components/u-tabs-item/props.js +3 -1
  149. package/components/u-tabs-pro/u-tabs-pro.vue +212 -0
  150. package/components/u-tag/props.js +4 -1
  151. package/components/u-td/props.js +3 -1
  152. package/components/u-text/props.js +4 -1
  153. package/components/u-text/text.js +1 -1
  154. package/components/u-text/u-text.vue +18 -6
  155. package/components/u-textarea/props.js +3 -1
  156. package/components/u-textarea/u-textarea.vue +16 -5
  157. package/components/u-th/props.js +3 -1
  158. package/components/u-toolbar/props.js +4 -1
  159. package/components/u-tooltip/props.js +10 -2
  160. package/components/u-tooltip/tooltip.js +2 -1
  161. package/components/u-tooltip/u-tooltip.vue +31 -3
  162. package/components/u-tr/props.js +3 -1
  163. package/components/u-transition/nvue-ani-map.js +75 -68
  164. package/components/u-transition/props.js +4 -1
  165. package/components/u-transition/transitionMixin.js +164 -163
  166. package/components/u-upload/props.js +4 -1
  167. package/components/u-upload/u-upload.vue +48 -41
  168. package/components/u-waterfall/u-waterfall.vue +162 -110
  169. package/index.js +65 -8
  170. package/libs/config/config.js +1 -1
  171. package/libs/config/props.js +180 -204
  172. package/libs/css/components.scss +3 -0
  173. package/libs/function/index.js +66 -0
  174. package/libs/function/uni-api-shims.js +29 -0
  175. package/libs/i18n/index.js +71 -36
  176. package/libs/i18n/locale-packs.js +11 -0
  177. package/libs/i18n/locales/all.js +23 -0
  178. package/libs/i18n/locales/{de.json → de.js} +1 -1
  179. package/libs/i18n/locales/{en.json → en.js} +2 -2
  180. package/libs/i18n/locales/{es.json → es.js} +1 -1
  181. package/libs/i18n/locales/{fr.json → fr.js} +2 -2
  182. package/libs/i18n/locales/{ja.json → ja.js} +1 -1
  183. package/libs/i18n/locales/{ko.json → ko.js} +1 -1
  184. package/libs/i18n/locales/{ru.json → ru.js} +1 -1
  185. package/libs/i18n/locales/{th.json → th.js} +1 -1
  186. package/libs/i18n/locales/{zh-Hans.json → zh-Hans.js} +1 -1
  187. package/libs/i18n/locales/{zh-Hant.json → zh-Hant.js} +1 -1
  188. package/libs/mixin/mixin.js +18 -54
  189. package/libs/root/index.js +78 -5
  190. package/libs/root/page.js +7 -7
  191. package/libs/root/root.js +73 -3
  192. package/libs/util/app-nvue-webview-canvas.js +486 -0
  193. package/libs/util/gcanvas/bridge/bridge-weex.js +10 -7
  194. package/libs/util/gcanvas/context-2d/FillStyleLinearGradient.js +0 -0
  195. package/libs/util/gcanvas/context-2d/FillStylePattern.js +0 -0
  196. package/libs/util/gcanvas/context-2d/FillStyleRadialGradient.js +0 -0
  197. package/libs/util/gcanvas/context-2d/RenderingContext.js +0 -0
  198. package/libs/util/gcanvas/context-webgl/ActiveInfo.js +0 -0
  199. package/libs/util/gcanvas/context-webgl/Buffer.js +0 -0
  200. package/libs/util/gcanvas/context-webgl/Framebuffer.js +0 -0
  201. package/libs/util/gcanvas/context-webgl/GLenum.js +0 -0
  202. package/libs/util/gcanvas/context-webgl/GLmethod.js +0 -0
  203. package/libs/util/gcanvas/context-webgl/GLtype.js +0 -0
  204. package/libs/util/gcanvas/context-webgl/Program.js +0 -0
  205. package/libs/util/gcanvas/context-webgl/Renderbuffer.js +0 -0
  206. package/libs/util/gcanvas/context-webgl/RenderingContext.js +0 -0
  207. package/libs/util/gcanvas/context-webgl/Shader.js +0 -0
  208. package/libs/util/gcanvas/context-webgl/ShaderPrecisionFormat.js +0 -0
  209. package/libs/util/gcanvas/context-webgl/Texture.js +0 -0
  210. package/libs/util/gcanvas/context-webgl/UniformLocation.js +0 -0
  211. package/libs/util/gcanvas/context-webgl/classUtils.js +0 -0
  212. package/libs/util/gcanvas/env/canvas.js +0 -0
  213. package/libs/util/gcanvas/env/image.js +0 -0
  214. package/libs/util/gcanvas/env/tool.js +0 -0
  215. package/libs/util/gcanvas/index.js +3 -3
  216. package/package.json +4 -5
  217. package/tsconfig.json +18 -0
  218. package/types/comps/calendar.d.ts +38 -0
  219. package/types/comps/checkbox.d.ts +12 -0
  220. package/types/comps/lineProgress.d.ts +5 -0
  221. package/types/comps/navbar.d.ts +3 -3
  222. package/types/comps/swipeActionItem.d.ts +13 -0
  223. package/types/comps/swiper.d.ts +5 -0
  224. package/types/comps/tabs.d.ts +2 -1
  225. package/types/comps/text.d.ts +1 -1
  226. package/types/comps/tooltip.d.ts +36 -1
  227. package/types/func.d.ts +23 -0
  228. package/types/index.d.ts +1 -0
  229. /package/components/u-markdown/{marked.esm.js → marked.esm.mjs} +0 -0
@@ -0,0 +1,226 @@
1
+ <template>
2
+ <view
3
+ class="up-novel-reader__content"
4
+ :style="{ color: themeTokens.text, backgroundColor: themeTokens.background }"
5
+ @tap="handleTap"
6
+ >
7
+ <view v-if="loading" class="up-novel-reader__state">
8
+ <slot name="loading">
9
+ <text class="up-novel-reader__state-text">章节加载中</text>
10
+ </slot>
11
+ </view>
12
+ <view v-else-if="error" class="up-novel-reader__state">
13
+ <slot name="error" :error="error" :retry="retry">
14
+ <text class="up-novel-reader__state-text">{{ error.message || '章节加载失败' }}</text>
15
+ <view class="up-novel-reader__state-action" @tap.stop="retry">重试</view>
16
+ </slot>
17
+ </view>
18
+ <view v-else-if="!hasContent" class="up-novel-reader__state">
19
+ <slot name="empty">
20
+ <text class="up-novel-reader__state-text">暂无正文</text>
21
+ </slot>
22
+ </view>
23
+ <scroll-view
24
+ v-else-if="mode === 'scroll'"
25
+ class="up-novel-reader__scroll"
26
+ scroll-y
27
+ :scroll-top="scrollTop"
28
+ @scroll="handleScroll"
29
+ >
30
+ <view class="up-novel-reader__article" :style="articleStyle">
31
+ <view
32
+ v-for="paragraph in paragraphs"
33
+ :key="paragraph.index"
34
+ class="up-novel-reader__paragraph"
35
+ :style="paragraphStyle"
36
+ >
37
+ <text>{{ paragraph.text }}</text>
38
+ </view>
39
+ </view>
40
+ </scroll-view>
41
+ <swiper
42
+ v-else
43
+ class="up-novel-reader__swiper"
44
+ :current="pageIndex"
45
+ :duration="pageAnimation ? 300 : 0"
46
+ :disable-touch="false"
47
+ @change="handlePageChange"
48
+ >
49
+ <swiper-item
50
+ v-for="page in pages"
51
+ :key="page.index"
52
+ class="up-novel-reader__page"
53
+ >
54
+ <view class="up-novel-reader__article" :style="articleStyle">
55
+ <text class="up-novel-reader__page-text">{{ page.text }}</text>
56
+ </view>
57
+ </swiper-item>
58
+ </swiper>
59
+ </view>
60
+ </template>
61
+
62
+ <script>
63
+ import { addUnit } from '../../libs/function/index'
64
+
65
+ export default {
66
+ name: 'NovelReaderContent',
67
+ props: {
68
+ mode: {
69
+ type: String,
70
+ default: 'scroll'
71
+ },
72
+ paragraphs: {
73
+ type: Array,
74
+ default: () => []
75
+ },
76
+ pages: {
77
+ type: Array,
78
+ default: () => []
79
+ },
80
+ pageIndex: {
81
+ type: Number,
82
+ default: 0
83
+ },
84
+ scrollTop: {
85
+ type: Number,
86
+ default: 0
87
+ },
88
+ settings: {
89
+ type: Object,
90
+ default: () => ({})
91
+ },
92
+ themeTokens: {
93
+ type: Object,
94
+ default: () => ({})
95
+ },
96
+ loading: {
97
+ type: Boolean,
98
+ default: false
99
+ },
100
+ error: {
101
+ type: Object,
102
+ default: null
103
+ },
104
+ pageAnimation: {
105
+ type: Boolean,
106
+ default: true
107
+ }
108
+ },
109
+ emits: ['content-scroll', 'page-change', 'tap-zone', 'retry'],
110
+ computed: {
111
+ hasContent() {
112
+ return this.paragraphs.length > 0 || this.pages.length > 0
113
+ },
114
+ articleStyle() {
115
+ const style = {
116
+ width: this.settings.contentWidth || '92%',
117
+ color: this.themeTokens.text,
118
+ fontSize: addUnit(this.settings.fontSize || 18, 'px'),
119
+ lineHeight: this.getLineHeight(this.settings.lineHeight),
120
+ fontWeight: this.settings.fontWeight || 400
121
+ }
122
+ if (this.settings.fontFamily && this.settings.fontFamily !== 'system') {
123
+ style.fontFamily = this.settings.fontFamily
124
+ }
125
+ return style
126
+ },
127
+ paragraphStyle() {
128
+ return {
129
+ marginBottom: addUnit(this.settings.paragraphSpacing || 0, 'px')
130
+ }
131
+ }
132
+ },
133
+ methods: {
134
+ getLineHeight(value) {
135
+ const lineHeight = Number(value)
136
+ if (!Number.isFinite(lineHeight)) return '1.8'
137
+ return lineHeight <= 4 ? String(lineHeight) : addUnit(lineHeight, 'px')
138
+ },
139
+ handleScroll(event) {
140
+ this.$emit('content-scroll', event)
141
+ },
142
+ handlePageChange(event) {
143
+ this.$emit('page-change', {
144
+ pageIndex: Number(event && event.detail && event.detail.current) || 0
145
+ })
146
+ },
147
+ handleTap(event) {
148
+ const detail = event && event.detail ? event.detail : {}
149
+ const x = Number(detail.x)
150
+ const width = Number(detail.width) || 0
151
+ const resolvedWidth = width || (typeof uni !== 'undefined' && uni.getWindowInfo
152
+ ? uni.getWindowInfo().windowWidth
153
+ : 375)
154
+ const zone = x < resolvedWidth / 3
155
+ ? 'left'
156
+ : x > resolvedWidth * 2 / 3
157
+ ? 'right'
158
+ : 'center'
159
+ this.$emit('tap-zone', zone)
160
+ },
161
+ retry() {
162
+ this.$emit('retry')
163
+ }
164
+ }
165
+ }
166
+ </script>
167
+
168
+ <style lang="scss" scoped>
169
+ .up-novel-reader__content {
170
+ width: 100%;
171
+ height: 100%;
172
+ overflow: hidden;
173
+ }
174
+
175
+ .up-novel-reader__scroll,
176
+ .up-novel-reader__swiper {
177
+ width: 100%;
178
+ height: 100%;
179
+ }
180
+
181
+ .up-novel-reader__article {
182
+ box-sizing: border-box;
183
+ min-height: 100%;
184
+ margin: 0 auto;
185
+ padding: 32px 0;
186
+ }
187
+
188
+ .up-novel-reader__paragraph {
189
+ white-space: pre-wrap;
190
+ word-break: break-word;
191
+ }
192
+
193
+ .up-novel-reader__page {
194
+ display: flex;
195
+ flex-direction: row;
196
+ align-items: flex-start;
197
+ justify-content: center;
198
+ overflow: hidden;
199
+ }
200
+
201
+ .up-novel-reader__page-text {
202
+ white-space: pre-wrap;
203
+ word-break: break-word;
204
+ }
205
+
206
+ .up-novel-reader__state {
207
+ display: flex;
208
+ align-items: center;
209
+ justify-content: center;
210
+ flex-direction: column;
211
+ width: 100%;
212
+ height: 100%;
213
+ gap: 16px;
214
+ }
215
+
216
+ .up-novel-reader__state-text {
217
+ color: var(--up-novel-reader-muted, #909399);
218
+ font-size: 14px;
219
+ }
220
+
221
+ .up-novel-reader__state-action {
222
+ padding: 8px 18px;
223
+ color: var(--up-primary, #2979ff);
224
+ font-size: 14px;
225
+ }
226
+ </style>
@@ -0,0 +1,151 @@
1
+ export const DEFAULT_READER_SETTINGS = {
2
+ theme: 'day',
3
+ fontSize: 18,
4
+ lineHeight: 1.8,
5
+ paragraphSpacing: 16,
6
+ contentWidth: '92%',
7
+ fontFamily: 'system',
8
+ fontWeight: 400,
9
+ animation: true
10
+ }
11
+
12
+ const SETTING_KEYS = Object.keys(DEFAULT_READER_SETTINGS)
13
+ const ALLOWED_MODES = ['scroll', 'page']
14
+
15
+ function clamp(value, min, max, fallback) {
16
+ const numericValue = Number(value)
17
+ if (!Number.isFinite(numericValue)) return fallback
18
+ return Math.min(max, Math.max(min, numericValue))
19
+ }
20
+
21
+ function normalizeContentWidth(value) {
22
+ if (typeof value === 'number') {
23
+ return clamp(value, 40, 100, 92)
24
+ }
25
+ if (typeof value === 'string' && value.trim()) {
26
+ return value.trim()
27
+ }
28
+ return DEFAULT_READER_SETTINGS.contentWidth
29
+ }
30
+
31
+ export function mergeReaderSettings(...sources) {
32
+ const result = { ...DEFAULT_READER_SETTINGS }
33
+ sources.filter(Boolean).forEach((source) => {
34
+ SETTING_KEYS.forEach((key) => {
35
+ if (source[key] !== undefined) {
36
+ result[key] = source[key]
37
+ }
38
+ })
39
+ })
40
+ result.fontSize = clamp(result.fontSize, 12, 48, DEFAULT_READER_SETTINGS.fontSize)
41
+ result.lineHeight = clamp(result.lineHeight, 1, 3, DEFAULT_READER_SETTINGS.lineHeight)
42
+ result.paragraphSpacing = clamp(result.paragraphSpacing, 0, 80, DEFAULT_READER_SETTINGS.paragraphSpacing)
43
+ result.contentWidth = normalizeContentWidth(result.contentWidth)
44
+ result.fontWeight = result.fontWeight >= 600 ? 600 : 400
45
+ result.animation = result.animation !== false
46
+ return result
47
+ }
48
+
49
+ export function normalizeMode(mode) {
50
+ return ALLOWED_MODES.includes(mode) ? mode : 'scroll'
51
+ }
52
+
53
+ export function toggleControls(state) {
54
+ return {
55
+ ...state,
56
+ controlsVisible: !state.controlsVisible
57
+ }
58
+ }
59
+
60
+ export function setMode(state, mode) {
61
+ const normalizedMode = normalizeMode(mode)
62
+ return {
63
+ ...state,
64
+ mode: normalizedMode
65
+ }
66
+ }
67
+
68
+ export function setSettings(state, settings) {
69
+ return {
70
+ ...state,
71
+ settings: mergeReaderSettings(state.settings, settings)
72
+ }
73
+ }
74
+
75
+ export function setProgress(state, progress) {
76
+ return {
77
+ ...state,
78
+ progress: {
79
+ ...state.progress,
80
+ ...progress,
81
+ pageIndex: Math.max(0, Number(progress && progress.pageIndex) || 0),
82
+ charOffset: Math.max(0, Number(progress && progress.charOffset) || 0),
83
+ scrollTop: Math.max(0, Number(progress && progress.scrollTop) || 0)
84
+ }
85
+ }
86
+ }
87
+
88
+ export function toggleBookmark(bookmarks, bookmark) {
89
+ const list = Array.isArray(bookmarks) ? bookmarks : []
90
+ const bookmarkId = bookmark && bookmark.id
91
+ if (!bookmarkId) return list.slice()
92
+ const existingIndex = list.findIndex((item) => item && item.id === bookmarkId)
93
+ if (existingIndex === -1) return [...list, bookmark]
94
+ return list.filter((_, index) => index !== existingIndex)
95
+ }
96
+
97
+ export function createBookmark({ chapterId, chapterIndex, charOffset, pageIndex, scrollTop, excerpt, createdAt } = {}) {
98
+ const normalizedChapterId = chapterId == null ? '' : String(chapterId)
99
+ const normalizedOffset = Math.max(0, Number(charOffset) || 0)
100
+ return {
101
+ id: `${normalizedChapterId}:${normalizedOffset}`,
102
+ chapterId,
103
+ chapterIndex: Number(chapterIndex) || 0,
104
+ charOffset: normalizedOffset,
105
+ pageIndex: Math.max(0, Number(pageIndex) || 0),
106
+ scrollTop: Math.max(0, Number(scrollTop) || 0),
107
+ excerpt: excerpt || '',
108
+ createdAt: Number(createdAt) || Date.now()
109
+ }
110
+ }
111
+
112
+ export function startReading(state, timestamp = Date.now()) {
113
+ return {
114
+ ...state,
115
+ active: true,
116
+ lastActiveAt: timestamp
117
+ }
118
+ }
119
+
120
+ export function pauseReading(state, timestamp = Date.now()) {
121
+ const nextState = consumeReadingTime(state, timestamp)
122
+ return {
123
+ ...nextState,
124
+ active: false
125
+ }
126
+ }
127
+
128
+ export function consumeReadingTime(state, timestamp = Date.now()) {
129
+ if (!state.active || !state.lastActiveAt) return state
130
+ const elapsed = Math.max(0, timestamp - state.lastActiveAt)
131
+ return {
132
+ ...state,
133
+ readingTime: Math.max(0, Number(state.readingTime) || 0) + elapsed,
134
+ lastActiveAt: timestamp
135
+ }
136
+ }
137
+
138
+ export default {
139
+ DEFAULT_READER_SETTINGS,
140
+ mergeReaderSettings,
141
+ normalizeMode,
142
+ toggleControls,
143
+ setMode,
144
+ setSettings,
145
+ setProgress,
146
+ toggleBookmark,
147
+ createBookmark,
148
+ startReading,
149
+ pauseReading,
150
+ consumeReadingTime
151
+ }
@@ -0,0 +1,325 @@
1
+ <template>
2
+ <view class="up-novel-reader__settings" :style="settingsStyle">
3
+ <view class="up-novel-reader__settings-header">
4
+ <text class="up-novel-reader__settings-title">阅读设置</text>
5
+ <view
6
+ class="up-novel-reader__settings-close"
7
+ @tap="close"
8
+ >
9
+ <u-icon name="close" size="18" :color="textColor"></u-icon>
10
+ </view>
11
+ </view>
12
+ <slot v-if="$slots.default"></slot>
13
+ <scroll-view v-else class="up-novel-reader__settings-scroll" scroll-y>
14
+ <view class="up-novel-reader__settings-section">
15
+ <text class="up-novel-reader__settings-label">主题</text>
16
+ <view class="up-novel-reader__theme-list">
17
+ <view
18
+ v-for="theme in themes"
19
+ :key="theme.value"
20
+ class="up-novel-reader__theme-option"
21
+ :class="{ 'is-active': draftSettings.theme === theme.value }"
22
+ :style="themeOptionStyle(theme)"
23
+ @tap="setTheme(theme.value)"
24
+ >
25
+ <text>{{ theme.label }}</text>
26
+ </view>
27
+ </view>
28
+ </view>
29
+ <view class="up-novel-reader__settings-section">
30
+ <view class="up-novel-reader__settings-row">
31
+ <text class="up-novel-reader__settings-label">字号</text>
32
+ <text class="up-novel-reader__settings-value">{{ draftSettings.fontSize }}px</text>
33
+ </view>
34
+ <u-slider
35
+ v-model="draftSettings.fontSize"
36
+ min="12"
37
+ max="32"
38
+ step="1"
39
+ active-color="#2979ff"
40
+ @change="emitSettings"
41
+ ></u-slider>
42
+ </view>
43
+ <view class="up-novel-reader__settings-section">
44
+ <view class="up-novel-reader__settings-row">
45
+ <text class="up-novel-reader__settings-label">行高</text>
46
+ <text class="up-novel-reader__settings-value">{{ Number(draftSettings.lineHeight).toFixed(1) }}</text>
47
+ </view>
48
+ <u-slider
49
+ v-model="draftSettings.lineHeight"
50
+ min="1.2"
51
+ max="2.6"
52
+ step="0.1"
53
+ active-color="#2979ff"
54
+ @change="emitSettings"
55
+ ></u-slider>
56
+ </view>
57
+ <view class="up-novel-reader__settings-section">
58
+ <view class="up-novel-reader__settings-row">
59
+ <text class="up-novel-reader__settings-label">段距</text>
60
+ <text class="up-novel-reader__settings-value">{{ draftSettings.paragraphSpacing }}px</text>
61
+ </view>
62
+ <u-slider
63
+ v-model="draftSettings.paragraphSpacing"
64
+ min="0"
65
+ max="36"
66
+ step="2"
67
+ active-color="#2979ff"
68
+ @change="emitSettings"
69
+ ></u-slider>
70
+ </view>
71
+ <view class="up-novel-reader__settings-section">
72
+ <view class="up-novel-reader__settings-row">
73
+ <text class="up-novel-reader__settings-label">正文宽度</text>
74
+ <text class="up-novel-reader__settings-value">{{ contentWidthPercent }}%</text>
75
+ </view>
76
+ <u-slider
77
+ v-model="contentWidthValue"
78
+ min="60"
79
+ max="100"
80
+ step="2"
81
+ active-color="#2979ff"
82
+ @change="emitContentWidth"
83
+ ></u-slider>
84
+ </view>
85
+ <view class="up-novel-reader__settings-section up-novel-reader__settings-options">
86
+ <view class="up-novel-reader__settings-option" @tap="toggleWeight">
87
+ <view>
88
+ <text class="up-novel-reader__settings-label">粗体正文</text>
89
+ <text class="up-novel-reader__settings-hint">增强低亮度主题下的可读性</text>
90
+ </view>
91
+ <u-icon
92
+ :name="draftSettings.fontWeight >= 600 ? 'checkmark-circle-fill' : 'circle'"
93
+ size="22"
94
+ :color="draftSettings.fontWeight >= 600 ? '#2979ff' : mutedColor"
95
+ ></u-icon>
96
+ </view>
97
+ <view class="up-novel-reader__settings-option" @tap="toggleAnimation">
98
+ <view>
99
+ <text class="up-novel-reader__settings-label">翻页动画</text>
100
+ <text class="up-novel-reader__settings-hint">关闭后立即切换页面</text>
101
+ </view>
102
+ <u-icon
103
+ :name="draftSettings.animation ? 'checkmark-circle-fill' : 'circle'"
104
+ size="22"
105
+ :color="draftSettings.animation ? '#2979ff' : mutedColor"
106
+ ></u-icon>
107
+ </view>
108
+ </view>
109
+ <view class="up-novel-reader__settings-footer">
110
+ <view class="up-novel-reader__settings-done" @tap="close">完成</view>
111
+ </view>
112
+ </scroll-view>
113
+ </view>
114
+ </template>
115
+
116
+ <script>
117
+ import { mergeReaderSettings } from './reader-core'
118
+
119
+ export default {
120
+ name: 'NovelReaderSettings',
121
+ props: {
122
+ settings: {
123
+ type: Object,
124
+ default: () => ({})
125
+ },
126
+ themeTokens: {
127
+ type: Object,
128
+ default: () => ({})
129
+ },
130
+ },
131
+ emits: ['update-settings', 'close'],
132
+ data() {
133
+ return {
134
+ draftSettings: mergeReaderSettings(this.settings),
135
+ themes: [
136
+ { value: 'day', label: '日间', background: '#f7f8fa', text: '#303133' },
137
+ { value: 'paper', label: '护眼', background: '#f3ead7', text: '#51483d' },
138
+ { value: 'green', label: '豆沙', background: '#e7f1e4', text: '#3f5140' },
139
+ { value: 'night', label: '夜间', background: '#202124', text: '#d6d7da' },
140
+ { value: 'dark', label: '深色', background: '#111214', text: '#e5e7eb' }
141
+ ]
142
+ }
143
+ },
144
+ computed: {
145
+ settingsStyle() {
146
+ return {
147
+ color: this.textColor,
148
+ backgroundColor: this.themeTokens.background || '#ffffff'
149
+ }
150
+ },
151
+ textColor() {
152
+ return this.themeTokens.text || '#303133'
153
+ },
154
+ mutedColor() {
155
+ return this.themeTokens.muted || '#909399'
156
+ },
157
+ contentWidthValue: {
158
+ get() {
159
+ const value = Number.parseFloat(this.draftSettings.contentWidth)
160
+ return Number.isFinite(value) ? value : 92
161
+ },
162
+ set(value) {
163
+ this.draftSettings.contentWidth = `${value}%`
164
+ }
165
+ },
166
+ contentWidthPercent() {
167
+ return Math.round(this.contentWidthValue)
168
+ }
169
+ },
170
+ watch: {
171
+ settings: {
172
+ handler(value) {
173
+ this.draftSettings = mergeReaderSettings(value)
174
+ },
175
+ deep: true
176
+ },
177
+ },
178
+ methods: {
179
+ themeOptionStyle(theme) {
180
+ return {
181
+ backgroundColor: theme.background,
182
+ color: theme.text
183
+ }
184
+ },
185
+ setTheme(theme) {
186
+ this.draftSettings.theme = theme
187
+ this.emitSettings()
188
+ },
189
+ toggleWeight() {
190
+ this.draftSettings.fontWeight = this.draftSettings.fontWeight >= 600 ? 400 : 600
191
+ this.emitSettings()
192
+ },
193
+ toggleAnimation() {
194
+ this.draftSettings.animation = !this.draftSettings.animation
195
+ this.emitSettings()
196
+ },
197
+ emitContentWidth() {
198
+ this.emitSettings()
199
+ },
200
+ emitSettings() {
201
+ this.draftSettings = mergeReaderSettings(this.draftSettings)
202
+ this.$emit('update-settings', { ...this.draftSettings })
203
+ },
204
+ close() {
205
+ this.$emit('close')
206
+ }
207
+ }
208
+ }
209
+ </script>
210
+
211
+ <style lang="scss" scoped>
212
+ .up-novel-reader__settings {
213
+ width: 100%;
214
+ max-height: 78vh;
215
+ overflow: hidden;
216
+ }
217
+
218
+ .up-novel-reader__settings-header {
219
+ display: flex;
220
+ flex-direction: row;
221
+ align-items: center;
222
+ justify-content: space-between;
223
+ padding: 18px 20px 12px;
224
+ }
225
+
226
+ .up-novel-reader__settings-title {
227
+ font-size: 18px;
228
+ font-weight: 600;
229
+ }
230
+
231
+ .up-novel-reader__settings-close {
232
+ display: flex;
233
+ flex-direction: row;
234
+ align-items: center;
235
+ justify-content: center;
236
+ width: 34px;
237
+ height: 34px;
238
+ }
239
+
240
+ .up-novel-reader__settings-scroll {
241
+ max-height: calc(78vh - 64px);
242
+ padding: 0 20px 24px;
243
+ box-sizing: border-box;
244
+ }
245
+
246
+ .up-novel-reader__settings-section {
247
+ padding: 14px 0;
248
+ }
249
+
250
+ .up-novel-reader__settings-row,
251
+ .up-novel-reader__settings-option {
252
+ display: flex;
253
+ flex-direction: row;
254
+ align-items: center;
255
+ justify-content: space-between;
256
+ gap: 12px;
257
+ }
258
+
259
+ .up-novel-reader__settings-row {
260
+ min-height: 34px;
261
+ }
262
+
263
+ .up-novel-reader__settings-label {
264
+ font-size: 14px;
265
+ }
266
+
267
+ .up-novel-reader__settings-value {
268
+ color: var(--up-novel-reader-muted, #909399);
269
+ font-size: 13px;
270
+ }
271
+
272
+ .up-novel-reader__settings-hint {
273
+ color: var(--up-novel-reader-muted, #909399);
274
+ font-size: 12px;
275
+ }
276
+
277
+ .up-novel-reader__theme-list {
278
+ display: flex;
279
+ flex-direction: row;
280
+ gap: 8px;
281
+ margin-top: 10px;
282
+ }
283
+
284
+ .up-novel-reader__theme-option {
285
+ display: flex;
286
+ flex-direction: row;
287
+ align-items: center;
288
+ justify-content: center;
289
+ flex: 1;
290
+ height: 42px;
291
+ border: 1px solid transparent;
292
+ border-radius: 8px;
293
+ font-size: 12px;
294
+ }
295
+
296
+ .up-novel-reader__theme-option.is-active {
297
+ border-color: var(--up-novel-reader-active, #2979ff);
298
+ box-shadow: 0 0 0 2px rgba(41, 121, 255, 0.18);
299
+ }
300
+
301
+ .up-novel-reader__settings-option {
302
+ padding: 10px 0;
303
+ }
304
+
305
+ .up-novel-reader__settings-hint {
306
+ display: block;
307
+ margin-top: 3px;
308
+ }
309
+
310
+ .up-novel-reader__settings-footer {
311
+ padding: 10px 0 18px;
312
+ }
313
+
314
+ .up-novel-reader__settings-done {
315
+ display: flex;
316
+ flex-direction: row;
317
+ align-items: center;
318
+ justify-content: center;
319
+ height: 42px;
320
+ border-radius: 8px;
321
+ color: #ffffff;
322
+ background-color: var(--up-novel-reader-active, #2979ff);
323
+ font-size: 14px;
324
+ }
325
+ </style>