uview-plus 3.8.127 → 3.8.128

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 (65) hide show
  1. package/README.md +13 -0
  2. package/changelog.md +29 -0
  3. package/components/u-calendar/month.vue +71 -35
  4. package/components/u-calendar/u-calendar.vue +67 -31
  5. package/components/u-car-keyboard/u-car-keyboard.vue +21 -8
  6. package/components/u-cate-tab/u-cate-tab.vue +56 -11
  7. package/components/u-cell/u-cell.vue +20 -16
  8. package/components/u-collapse-item/u-collapse-item.vue +34 -11
  9. package/components/u-datetime-picker/u-datetime-picker.vue +16 -0
  10. package/components/u-dropdown/u-dropdown.vue +15 -0
  11. package/components/u-grid/u-grid.vue +9 -0
  12. package/components/u-grid-item/u-grid-item.vue +8 -0
  13. package/components/u-icon/util.js +16 -1
  14. package/components/u-list/u-list.vue +59 -8
  15. package/components/u-list-item/u-list-item.vue +9 -3
  16. package/components/u-number-keyboard/u-number-keyboard.vue +18 -6
  17. package/components/u-parse/parser.js +4 -3
  18. package/components/u-picker/u-picker.vue +6 -3
  19. package/components/u-search/u-search.vue +15 -5
  20. package/components/u-select/u-select.vue +206 -31
  21. package/components/u-slider/u-slider.vue +21 -12
  22. package/components/u-tabs/u-tabs.vue +5 -1
  23. package/components/u-text/u-text.vue +34 -32
  24. package/components/u-textarea/props.js +5 -0
  25. package/components/u-textarea/textarea.js +4 -1
  26. package/components/u-textarea/u-textarea.vue +3 -1
  27. package/components/u-upload/u-upload.vue +15 -4
  28. package/components/u-upload/utils.js +4 -1
  29. package/components/u-video/danmaku.js +97 -0
  30. package/components/u-video/props.js +205 -0
  31. package/components/u-video/u-video.vue +1427 -0
  32. package/components/u-video/video-danmaku.vue +273 -0
  33. package/components/u-video/video-slider.vue +179 -0
  34. package/components/u-video/video-utils.js +98 -0
  35. package/components/u-video/video.js +72 -0
  36. package/libs/config/props.js +2 -1
  37. package/libs/function/test.js +2 -0
  38. package/libs/i18n/locales/de.js +14 -1
  39. package/libs/i18n/locales/en.js +14 -1
  40. package/libs/i18n/locales/es.js +14 -1
  41. package/libs/i18n/locales/fr.js +14 -1
  42. package/libs/i18n/locales/ja.js +14 -1
  43. package/libs/i18n/locales/ko.js +14 -1
  44. package/libs/i18n/locales/ru.js +14 -1
  45. package/libs/i18n/locales/th.js +14 -1
  46. package/libs/i18n/locales/zh-Hans.js +14 -1
  47. package/libs/i18n/locales/zh-Hant.js +14 -1
  48. package/libs/mixin/mixin.js +11 -6
  49. package/package.json +1 -1
  50. package/skills/README.md +66 -0
  51. package/skills/uview-plus/SKILL.md +95 -0
  52. package/skills/uview-plus/references/api.md +105 -0
  53. package/skills/uview-plus/references/components.md +156 -0
  54. package/skills/uview-plus/references/icons.md +223 -0
  55. package/skills/uview-plus/references/pitfalls.md +77 -0
  56. package/skills/uview-plus/references/setup.md +168 -0
  57. package/types/comps/form.d.ts +44 -8
  58. package/types/comps/list.d.ts +1 -1
  59. package/types/comps/listItem.d.ts +1 -1
  60. package/types/comps/numberBox.d.ts +85 -21
  61. package/types/comps/pagination.d.ts +96 -0
  62. package/types/comps/swipeActionItem.d.ts +3 -4
  63. package/types/comps/textarea.d.ts +9 -3
  64. package/types/comps.d.ts +1 -0
  65. package/types/index.d.ts +5 -0
package/README.md CHANGED
@@ -84,6 +84,19 @@ uview-plus现已推出免费可视化设计,可以方便的进行页面可视
84
84
  </template>
85
85
  ```
86
86
 
87
+ ## AI 编码助手
88
+
89
+ 包内 `skills/` 目录提供了给 AI 编码助手用的 uview-plus 使用说明(Agent Skills 的 `SKILL.md` 格式),包含组件与图标清单、easycom 接入、`uni.$u` 工具库和多端常见坑,让 AI 少猜少写错。
90
+
91
+ Claude Code 等工具把它拷到项目的 `.claude/skills/` 下即可生效:
92
+
93
+ ```bash
94
+ mkdir -p .claude/skills
95
+ cp -r node_modules/uview-plus/skills/uview-plus .claude/skills/
96
+ ```
97
+
98
+ 其他工具的接法与 uni_modules 装法见 `skills/README.md`。
99
+
87
100
  ## 版权信息
88
101
  uview-plus遵循[MIT](https://en.wikipedia.org/wiki/MIT_License)开源协议,意味着您无需支付任何费用,也无需授权,即可将uview-plus应用到您的产品中。
89
102
 
package/changelog.md CHANGED
@@ -1,3 +1,32 @@
1
+ ## 3.8.128
2
+ feat: 新增 u-video 视频播放器组件,并集中修复列表滚动、日历副标题、下拉菜单等 20+ 个问题
3
+
4
+ 本次为大版本迭代:新增 u-video 视频播放器组件(含弹幕、进度条、自定义控制层),随包提供面向 AI 编码助手的 uview-plus skill,并集中修复了列表锚点滚动、日历副标题同步、select 面板、App 端字体加载等一批跨组件问题。
5
+
6
+ - 新增 u-video:统一封装 video 播放能力,支持弹幕(danmaku)、自定义进度条与控制层、常见播放属性透传,覆盖 H5 / 小程序 / App
7
+ - 随包提供 skills/uview-plus:面向 AI 编码助手的技能包,含 setup、components、api、icons、pitfalls 参考文档
8
+ - u-list / u-list-item:新增 anchor 锚点滚动定位,修复 scrollIntoViewById 调用后不滚动的问题(#671、#721),并合并 anchor 定位实现
9
+ - u-calendar:修复关闭再打开后副标题与月份内容不一致;优化大 monthNum 下的渲染性能
10
+ - u-select:完善面板指示器、自适应宽度与多选回显逻辑
11
+ - u-icon:字体加载失败后改由定时器重试,修复 u-search 输入后无法再次聚焦的问题(#844)
12
+ - u-dropdown:修复 nvue 下菜单宽度被压缩的问题(#814)
13
+ - u-collapse / u-collapse-item:修复展开后内容变高被裁切;value 为 null 时不再重置展开状态
14
+ - u-cell:必填星号改用真实节点渲染,修复小程序与 nvue 下不显示
15
+ - u-slider:修复 border-box 下的滑块错位;点击轨道时直接触发 change
16
+ - u-textarea:回车默认换行并支持 confirmHold
17
+ - u-text:透传 data-* 属性,修复分享回调拿不到数据的问题
18
+ - u-upload:透传 H5 端视频真实的 File 对象
19
+ - u-search:修复小程序端占位符颜色首屏不生效
20
+ - u-cate-tab:优化大量图片场景的首屏渲染与滚动联动
21
+ - u-tabs:去除 scroll-view 上无效的 flex 布局
22
+ - u-picker:修复滚动过上级列后的 columnIndex 错位
23
+ - u-datetime-picker:修复空值回填并让确认返回当前选中值
24
+ - u-grid:兼容旧版 WebView 的网格布局
25
+ - u-parse:支持 quill 的对齐 class
26
+ - u-number-keyboard:修复 PC 浏览器退格键不触发;u-car-keyboard 支持警车车牌输入
27
+ - 类型声明:补齐步进器、分页器与表单相关类型;补齐 u-video 与 icon 字体相关配置
28
+ - 合入任务分支 task/71、task/82
29
+
1
30
  ## 3.8.127
2
31
  fix: 修复抖音小程序 tabbar-item / steps-item 因父子生命周期顺序导致的崩溃
3
32
 
@@ -39,6 +39,8 @@
39
39
  import defProps from '../../libs/config/props';
40
40
  import dayjs from '../u-datetime-picker/dayjs.esm.min.js';
41
41
  import { t } from '../../libs/i18n'
42
+ // 已经是YYYY-MM-DD形式的日期字符串,无需再交给dayjs解析
43
+ const DATE_STR_REG = /^\d{4}-\d{2}-\d{2}$/
42
44
  export default {
43
45
  name: 'u-calendar-month',
44
46
  mixins: [mpMixin, mixin],
@@ -169,6 +171,29 @@
169
171
  selectedChange() {
170
172
  return [this.minDate, this.maxDate, this.defaultDate]
171
173
  },
174
+ // selected中的元素可能是用户通过defaultDate传入的原始值(未补零的字符串、
175
+ // Date对象、时间戳等),这里统一归一化一次,避免在每个日期格子里反复解析
176
+ selectedDates() {
177
+ return this.selected.map((item) => this.formatDate(item))
178
+ },
179
+ // 供渲染期做O(1)的选中判断,取代原先每个格子都要遍历selected的dayjs比较
180
+ selectedDateMap() {
181
+ const map = {}
182
+ this.selectedDates.forEach((date) => {
183
+ map[date] = true
184
+ })
185
+ return map
186
+ },
187
+ // 区间模式的起止日期
188
+ selectedStart() {
189
+ return this.selectedDates[0]
190
+ },
191
+ selectedEnd() {
192
+ return this.selectedDates[this.selectedDates.length - 1]
193
+ },
194
+ normalizedTodayDate() {
195
+ return this.todayDate ? this.formatDate(this.todayDate) : ''
196
+ },
172
197
  dayStyle(index1, index2, item) {
173
198
  return (index1, index2, item) => {
174
199
  const style = {}
@@ -204,18 +229,18 @@
204
229
  },
205
230
  daySelectStyle() {
206
231
  return (index1, index2, item) => {
207
- let date = dayjs(item.date).format("YYYY-MM-DD"),
232
+ let date = this.dateOf(item),
208
233
  style = {}
209
- // 判断date是否在selected数组中,因为月份可能会需要补0,所以使用dateSame判断,而不用数组的includes判断
210
- if (this.selected.some(item => this.dateSame(item, date))) {
234
+ // selected已归一化为YYYY-MM-DD(月份可能需要补0),故可直接比较字符串
235
+ if (this.selectedDateMap[date]) {
211
236
  style.backgroundColor = this.color
212
237
  }
213
- if (this.todayDate && this.dateSame(date, this.todayDate)) {
238
+ if (this.normalizedTodayDate && date === this.normalizedTodayDate) {
214
239
  style.border = `1px solid ${this.resolvedTodayColor}`
215
240
  style.boxSizing = 'border-box'
216
241
  }
217
242
  if (this.mode === 'single') {
218
- if (date === this.selected[0]) {
243
+ if (date === this.selectedStart) {
219
244
  // 因为需要对nvue的兼容,只能这么写,无法缩写,也无法通过类名控制等等
220
245
  style.borderTopLeftRadius = '3px'
221
246
  style.borderBottomLeftRadius = '3px'
@@ -223,34 +248,31 @@
223
248
  style.borderBottomRightRadius = '3px'
224
249
  }
225
250
  } else if (this.mode === 'range') {
226
- if (this.selected.length >= 2) {
227
- const len = this.selected.length - 1
251
+ if (this.selectedDates.length >= 2) {
228
252
  // 第一个日期设置左上角和左下角的圆角
229
- if (this.dateSame(date, this.selected[0])) {
253
+ if (date === this.selectedStart) {
230
254
  style.borderTopLeftRadius = '3px'
231
255
  style.borderBottomLeftRadius = '3px'
232
256
  }
233
257
  // 最后一个日期设置右上角和右下角的圆角
234
- if (this.dateSame(date, this.selected[len])) {
258
+ if (date === this.selectedEnd) {
235
259
  style.borderTopRightRadius = '3px'
236
260
  style.borderBottomRightRadius = '3px'
237
261
  }
238
262
  // 处于第一和最后一个之间的日期,背景色设置为浅色,通过将对应颜色进行等分,再取其尾部的颜色值
239
- if (dayjs(date).isAfter(dayjs(this.selected[0])) && dayjs(date).isBefore(dayjs(this
240
- .selected[len]))) {
241
- const rangeEndColor = this.upThemeVar('--up-card-bg-color', this.upThemeIsDark ? '#1c1c1e' : '#ffffff')
242
- style.backgroundColor = colorGradient(this.color, rangeEndColor, 100)[90]
263
+ if (date > this.selectedStart && date < this.selectedEnd) {
264
+ style.backgroundColor = this.rangeMiddleColor
243
265
  // 增加一个透明度,让范围区间的背景色也能看到底部的mark水印字符
244
266
  style.opacity = this.upThemeIsDark ? 0.85 : 0.7
245
267
  }
246
- } else if (this.selected.length === 1) {
268
+ } else if (this.selectedDates.length === 1) {
247
269
  // 之所以需要这么写,是因为uni-app的iOS客户端的bug
248
270
  // 进行还原操作,否则在nvue的iOS,uni-app有bug,会导致诡异的表现
249
271
  style.borderTopLeftRadius = '3px'
250
272
  style.borderBottomLeftRadius = '3px'
251
273
  }
252
274
  } else {
253
- if (this.selected.some(item => this.dateSame(item, date))) {
275
+ if (this.selectedDateMap[date]) {
254
276
  style.borderTopLeftRadius = '3px'
255
277
  style.borderBottomLeftRadius = '3px'
256
278
  style.borderTopRightRadius = '3px'
@@ -260,27 +282,31 @@
260
282
  return style
261
283
  }
262
284
  },
285
+ // 区间中间日期的背景色与格子无关,按整体算一次即可,
286
+ // 否则区间内每个格子都要做一遍100等分的颜色渐变计算
287
+ rangeMiddleColor() {
288
+ const rangeEndColor = this.upThemeVar('--up-card-bg-color', this.upThemeIsDark ? '#1c1c1e' : '#ffffff')
289
+ return colorGradient(this.color, rangeEndColor, 100)[90]
290
+ },
263
291
  resolvedTodayColor() {
264
292
  return this.todayColor || this.color
265
293
  },
266
294
  // 某个日期是否被选中
267
295
  textStyle() {
268
296
  return (item) => {
269
- const date = dayjs(item.date).format("YYYY-MM-DD"),
297
+ const date = this.dateOf(item),
270
298
  style = {}
271
299
  // 选中的日期,提示文字设置白色
272
- if (this.selected.some(item => this.dateSame(item, date))) {
300
+ if (this.selectedDateMap[date]) {
273
301
  style.color = '#ffffff'
274
302
  }
275
303
  if (this.mode === 'range') {
276
- const len = this.selected.length - 1
277
304
  // 如果是范围选择模式,第一个和最后一个之间的日期,文字颜色设置为高亮的主题色
278
- if (dayjs(date).isAfter(dayjs(this.selected[0])) && dayjs(date).isBefore(dayjs(this
279
- .selected[len]))) {
305
+ if (date > this.selectedStart && date < this.selectedEnd) {
280
306
  style.color = this.color
281
307
  }
282
308
  }
283
- if (this.todayDate && this.dateSame(date, this.todayDate) && !this.isSelectedDate(date)) {
309
+ if (this.normalizedTodayDate && date === this.normalizedTodayDate && !this.selectedDateMap[date]) {
284
310
  style.color = this.resolvedTodayColor
285
311
  }
286
312
  return style
@@ -289,24 +315,23 @@
289
315
  // 获取底部的提示文字
290
316
  getBottomInfo() {
291
317
  return (index1, index2, item) => {
292
- const date = dayjs(item.date).format("YYYY-MM-DD")
318
+ const date = this.dateOf(item)
293
319
  const bottomInfo = item.bottomInfo
294
320
  // 当为日期范围模式时,且选择的日期个数大于0时
295
- if (this.mode === 'range' && this.selected.length > 0) {
296
- if (this.selected.length === 1) {
321
+ if (this.mode === 'range' && this.selectedDates.length > 0) {
322
+ if (this.selectedDates.length === 1) {
297
323
  // 选择了一个日期时,如果当前日期为数组中的第一个日期,则显示底部文字为“开始”
298
- if (this.dateSame(date, this.selected[0])) return this.startText
324
+ if (date === this.selectedStart) return this.startText
299
325
  else return bottomInfo
300
326
  } else {
301
- const len = this.selected.length - 1
302
327
  // 如果数组中的日期大于2个时,第一个和最后一个显示为开始和结束日期
303
- if (this.dateSame(date, this.selected[0]) && this.dateSame(date, this.selected[1]) &&
304
- len === 1) {
328
+ if (this.selectedDates.length === 2 && date === this.selectedStart && date === this
329
+ .selectedEnd) {
305
330
  // 如果长度为2,且第一个等于第二个日期,则提示语放在同一个item中
306
331
  return `${this.startText}/${this.endText}`
307
- } else if (this.dateSame(date, this.selected[0])) {
332
+ } else if (date === this.selectedStart) {
308
333
  return this.startText
309
- } else if (this.dateSame(date, this.selected[len])) {
334
+ } else if (date === this.selectedEnd) {
310
335
  return this.endText
311
336
  } else {
312
337
  return bottomInfo
@@ -343,18 +368,29 @@
343
368
  }
344
369
  },
345
370
  isForbid(item) {
346
- let date = dayjs(item.date).format("YYYY-MM-DD")
347
- if (this.mode !== 'range' && this.forbidDays.includes(date)) {
348
- return true
371
+ if (this.mode === 'range' || !this.forbidDays.length) {
372
+ return false
349
373
  }
350
- return false
374
+ return this.forbidDays.includes(this.dateOf(item))
375
+ },
376
+ // 归一化为YYYY-MM-DD,已经是该形式的字符串则直接返回,省掉一次dayjs解析
377
+ formatDate(value) {
378
+ if (typeof value === 'string' && DATE_STR_REG.test(value)) {
379
+ return value
380
+ }
381
+ return dayjs(value).format("YYYY-MM-DD")
382
+ },
383
+ // 取日期格子的日期字符串,父组件生成数据时已经算好;
384
+ // 兼容自定义formatter返回了不带dateStr的新对象的情况
385
+ dateOf(item) {
386
+ return item.dateStr || dayjs(item.date).format("YYYY-MM-DD")
351
387
  },
352
388
  // 判断两个日期是否相等
353
389
  dateSame(date1, date2) {
354
390
  return dayjs(date1).isSame(dayjs(date2))
355
391
  },
356
392
  isSelectedDate(date) {
357
- return this.selected.some(item => this.dateSame(item, date))
393
+ return !!this.selectedDateMap[this.formatDate(date)]
358
394
  },
359
395
  // 获取月份数据区域的宽度,因为nvue不支持百分比,所以无法通过css设置每个日期item的宽度
360
396
  getWrapperWidth() {
@@ -300,6 +300,12 @@ export default {
300
300
  handler(n) {
301
301
  if (n) {
302
302
  this.setMonth()
303
+ // 弹层每次打开都会重新挂载并滚动回默认月份,副标题指向的月份索引要一起复位,
304
+ // 否则会沿用上一次关闭前滚动到的月份,导致副标题与日历内容对应不上。
305
+ // 页面内模式的内容一直挂载着,滚动位置不会重置,所以不做处理
306
+ if (!this.pageInline) {
307
+ this.monthIndex = this.getDefaultMonthIndex()
308
+ }
303
309
  } else {
304
310
  // 关闭时重置scrollIntoView,否则会出现二次打开日历,当前月份数据显示不正确。
305
311
  // scrollIntoView需要有一个值变动过程,才会产生作用。
@@ -619,33 +625,42 @@ export default {
619
625
  this.monthNum,
620
626
  this.getMonths(minDate, maxDate)
621
627
  )
628
+ // 记录上一次测量到的各月份top值,月份未发生变化时高度也不会变,
629
+ // 复用后可避免重新生成数据后onScroll没有判断依据
630
+ const topCache = {}
631
+ this.months.forEach(item => {
632
+ if (typeof item.top === 'number') {
633
+ topCache[`${item.year}-${item.month}`] = item.top
634
+ }
635
+ })
622
636
  // 先清空数组
623
637
  this.months = []
638
+ // 最大最小日期只需格式化一次,YYYY-MM-DD的字典序即时间序,可直接比较字符串
639
+ const minDateStr = dayjs(minDate).format('YYYY-MM-DD')
640
+ const maxDateStr = dayjs(maxDate).format('YYYY-MM-DD')
641
+ const formatter = this.formatter || this.innerFormatter
624
642
  for (let i = 0; i < months; i++) {
643
+ // 每个月只构造一个dayjs对象,当月的日期数据都由它派生,
644
+ // 避免按天重复解析(monthNum较大时这里是主要开销)
645
+ const monthStart = dayjs(minDate).add(i, 'month')
646
+ const monthValue = monthStart.month() + 1
625
647
  this.months.push({
626
- date: new Array(
627
- dayjs(minDate).add(i, 'month').daysInMonth()
628
- )
648
+ date: new Array(monthStart.daysInMonth())
629
649
  .fill(1)
630
650
  .map((item, index) => {
631
651
  // 日期,取值1-31
632
652
  let day = index + 1
653
+ const current = monthStart.date(day)
633
654
  // 星期,0-6,0为周日
634
- const week = dayjs(minDate)
635
- .add(i, 'month')
636
- .date(day)
637
- .day()
638
- const date = dayjs(minDate)
639
- .add(i, 'month')
640
- .date(day)
641
- .format('YYYY-MM-DD')
655
+ const week = current.day()
656
+ const date = current.format('YYYY-MM-DD')
642
657
  let bottomInfo = ''
643
658
  if (this.showLunar) {
644
659
  // 将日期转为农历格式
645
660
  const lunar = Calendar.solar2lunar(
646
- dayjs(date).year(),
647
- dayjs(date).month() + 1,
648
- dayjs(date).date()
661
+ current.year(),
662
+ monthValue,
663
+ day
649
664
  )
650
665
  bottomInfo = lunar.IDayCn
651
666
  }
@@ -654,29 +669,30 @@ export default {
654
669
  week,
655
670
  // 小于最小允许的日期,或者大于最大的日期,则设置为disabled状态
656
671
  disabled:
657
- dayjs(date).isBefore(
658
- dayjs(minDate).format('YYYY-MM-DD')
659
- ) ||
660
- dayjs(date).isAfter(
661
- dayjs(maxDate).format('YYYY-MM-DD')
662
- ),
672
+ date < minDateStr || date > maxDateStr,
663
673
  // 返回一个日期对象,供外部的formatter获取当前日期的年月日等信息,进行加工处理
664
674
  date: new Date(date),
675
+ // 已格式化好的日期字符串,供month组件比较使用,
676
+ // 省掉渲染期对每个日期格子的重复解析
677
+ dateStr: date,
665
678
  bottomInfo,
666
679
  dot: false,
667
- month:
668
- dayjs(minDate).add(i, 'month').month() + 1
680
+ month: monthValue
669
681
  }
670
- const formatter =
671
- this.formatter || this.innerFormatter
672
682
  return formatter(config)
673
683
  }),
674
684
  // 当前所属的月份
675
- month: dayjs(minDate).add(i, 'month').month() + 1,
685
+ month: monthValue,
676
686
  // 当前年份
677
- year: dayjs(minDate).add(i, 'month').year()
687
+ year: monthStart.year()
678
688
  })
679
689
  }
690
+ this.months.forEach(item => {
691
+ const cachedTop = topCache[`${item.year}-${item.month}`]
692
+ if (typeof cachedTop === 'number') {
693
+ item.top = cachedTop
694
+ }
695
+ })
680
696
  if (this.monthSwitch) {
681
697
  this.monthIndex = this.getDefaultMonthIndex()
682
698
  }
@@ -750,6 +766,9 @@ export default {
750
766
  return `${year}-${month}` === selected
751
767
  })
752
768
  if (_index !== -1) {
769
+ // 滚动位置是程序主动设置的,同步更新副标题指向的月份索引。
770
+ // 只依赖scroll事件的话,滚动位置没有真正变化时收不到事件,副标题就会停留在上一次的月份
771
+ this.monthIndex = _index
753
772
  // #ifndef MP-WEIXIN
754
773
  this.$nextTick(() => {
755
774
  this.scrollIntoView = ''
@@ -758,7 +777,15 @@ export default {
758
777
  })
759
778
  // #endif
760
779
  // #ifdef MP-WEIXIN
761
- this.scrollTop = this.months[_index].top || 0;
780
+ // scrollTop与scrollIntoView同理,需要有一个值变动过程才会生效,
781
+ // 否则二次打开日历时沿用上次的数值,scroll-view不会滚动到默认月份
782
+ const _top = this.months[_index].top || 0;
783
+ this.scrollTop = 0;
784
+ if (_top !== 0) {
785
+ this.$nextTick(() => {
786
+ this.scrollTop = _top;
787
+ })
788
+ }
762
789
  // #endif
763
790
  }
764
791
  },
@@ -766,13 +793,22 @@ export default {
766
793
  onScroll(event) {
767
794
  // 不允许小于0的滚动值,如果scroll-view到顶了,继续下拉,会出现负数值
768
795
  const scrollTop = Math.max(0, event.detail.scrollTop)
769
- // 将当前滚动条数值,除以滚动区域的高度,可以得出当前滚动到了哪一个月份的索引
796
+ // 月份的top值尚未测量出来时不做判断,否则所有月份的判断阈值相同,
797
+ // 会把副标题固定到最后一个月份上
798
+ if (!this.months.some(item => typeof item.top === 'number')) return
799
+ // 将当前滚动条数值,与各月份的top值比较,可以得出当前滚动到了哪一个月份的索引
800
+ let monthIndex = 0
770
801
  for (let i = 0; i < this.months.length; i++) {
771
- if (scrollTop >= (this.months[i].top || this.listHeight)) {
772
- this.monthIndex = i
773
- this.scrollIntoViewScroll = `month-${i}`
802
+ const top = this.months[i].top
803
+ if (typeof top !== 'number') continue
804
+ if (scrollTop >= top) {
805
+ monthIndex = i
806
+ } else {
807
+ break
774
808
  }
775
809
  }
810
+ this.monthIndex = monthIndex
811
+ this.scrollIntoViewScroll = `month-${monthIndex}`
776
812
  },
777
813
  // 更新月份的top值
778
814
  onUpdateMonthTop(topArr = []) {
@@ -47,7 +47,8 @@
47
47
  </view>
48
48
  <view
49
49
  v-if="i === 3"
50
- @touchstart="backspaceClick"
50
+ @tap="backspaceClick"
51
+ @touchstart="backspaceTouchStart"
51
52
  @touchend="clearTimer"
52
53
  class="u-keyboard__button__inner-wrapper"
53
54
  >
@@ -128,7 +129,8 @@
128
129
  '澳',
129
130
  '新',
130
131
  '使',
131
- '学'
132
+ '学',
133
+ '警'
132
134
  ];
133
135
  let tmp = [];
134
136
  // 打乱顺序
@@ -137,7 +139,7 @@
137
139
  tmp[0] = data.slice(0, 10);
138
140
  tmp[1] = data.slice(10, 20);
139
141
  tmp[2] = data.slice(20, 30);
140
- tmp[3] = data.slice(30, 36);
142
+ tmp[3] = data.slice(30, 37);
141
143
  return tmp;
142
144
  },
143
145
  engKeyBoardList() {
@@ -204,17 +206,28 @@
204
206
  changeCarInputMode() {
205
207
  this.abc = !this.abc;
206
208
  },
207
- // 点击退格键
209
+ // 点击退格键,PC端浏览器(如电脑版微信内置浏览器)只有鼠标事件不触发touch,故由tap触发删除
208
210
  backspaceClick() {
209
211
  this.$emit('backspace');
210
- clearInterval(this.timer); //再次清空定时器,防止重复注册定时器
211
- this.timer = null;
212
- this.timer = setInterval(() => {
212
+ },
213
+ // 触摸退格键,一定时间后进入长按连续删除状态,短按时会在touchend前被清除
214
+ backspaceTouchStart() {
215
+ this.clearTimer(); //再次清空定时器,防止重复注册定时器
216
+ this.timer = setTimeout(() => {
217
+ this.$emit('backspace');
218
+ this.backspaceLongPress();
219
+ }, 600);
220
+ },
221
+ // 长按状态下每隔一段时间删除一次
222
+ backspaceLongPress() {
223
+ this.clearTimer();
224
+ this.timer = setTimeout(() => {
213
225
  this.$emit('backspace');
226
+ this.backspaceLongPress();
214
227
  }, 250);
215
228
  },
216
229
  clearTimer() {
217
- clearInterval(this.timer);
230
+ clearTimeout(this.timer);
218
231
  this.timer = null;
219
232
  },
220
233
  }
@@ -20,7 +20,7 @@
20
20
  </view>
21
21
  <view class="u-cate-tab__page-view">
22
22
  <template :key="index" v-for="(item , index) in tabList">
23
- <view v-if="mode == 'follow' || ( mode == 'tab' && index == innerCurrent)"
23
+ <view v-if="(mode == 'follow' && index < renderLimit) || ( mode == 'tab' && index == innerCurrent)"
24
24
  class="u-cate-tab__page-item" :id="'item' + index">
25
25
  <slot name="itemList" :item="item">
26
26
  </slot>
@@ -76,6 +76,16 @@
76
76
  current: {
77
77
  type: Number,
78
78
  default: 0
79
+ },
80
+ // 右侧内容是否随滚动渐进渲染,分类很多时可避免首屏一次性渲染全部图片
81
+ lazyRender: {
82
+ type: Boolean,
83
+ default: true
84
+ },
85
+ // 渐进渲染时首屏渲染的分类数量,之后每次追加也是该数量
86
+ lazyRenderCount: {
87
+ type: Number,
88
+ default: 8
79
89
  }
80
90
  },
81
91
  watch: {
@@ -83,6 +93,10 @@
83
93
  deep: true,
84
94
  handler(newVal, oldVal) {
85
95
  // this.observer();
96
+ // 整体换了一份数据时重新从首屏开始渐进渲染;仅内容变化则保留已渲染范围
97
+ if (newVal !== oldVal) {
98
+ this.renderCount = 0;
99
+ }
86
100
  sleep(30).then(() => {
87
101
  this.getMenuItemTop();
88
102
  this.leftMenuStatus(this.innerCurrent);
@@ -103,6 +117,16 @@
103
117
  }
104
118
  },
105
119
  emits: ['update:current'],
120
+ computed: {
121
+ // 渐进渲染时右侧最多渲染到第几个分类(不含该下标)
122
+ renderLimit() {
123
+ if (!this.lazyRender || this.mode != 'follow') {
124
+ return this.tabList.length;
125
+ }
126
+ // 当前选中项必须在渲染范围内,否则联动滚动找不到目标节点
127
+ return Math.min(this.tabList.length, Math.max(this.renderCount, this.lazyRenderCount, this.innerCurrent + 1));
128
+ }
129
+ },
106
130
  data() {
107
131
  return {
108
132
  scrollTop: 0, //tab标题的滚动条位置
@@ -117,6 +141,7 @@
117
141
  arr: [],
118
142
  scrollRightTop: 0, // 右边栏目scroll-view的滚动条高度
119
143
  timer: null, // 定时器
144
+ renderCount: 0, // 渐进渲染已放开的分类数量
120
145
  }
121
146
  },
122
147
  mounted() {
@@ -134,6 +159,8 @@
134
159
  // 点击左边的栏目切换
135
160
  async swichMenu(index) {
136
161
  if (this.mode == 'follow') {
162
+ // 渐进渲染下目标分类可能还没渲染,先补齐再滚动,否则scroll-into-view找不到节点
163
+ await this.growRender(index + 1);
137
164
  if(this.arr.length == 0) {
138
165
  await this.getMenuItemTop();
139
166
  }
@@ -148,6 +175,19 @@
148
175
  this.$emit('update:current', index);
149
176
  })
150
177
  },
178
+ // 渐进渲染:把右侧渲染范围放开到count个分类,并重新测量各分类位置
179
+ async growRender(count) {
180
+ if (!this.lazyRender || this.mode != 'follow') return false;
181
+ // 已经全部渲染完就不用再测量,否则滚到底部时每次节流都会白跑一次查询
182
+ if (this.renderLimit >= this.tabList.length) return false;
183
+ if (count <= this.renderLimit) return false;
184
+ this.renderCount = Math.min(this.tabList.length, count);
185
+ await this.$nextTick();
186
+ // 小程序端节点布局晚于nextTick,等一拍再测量,否则arr拿到的还是旧位置
187
+ await sleep(30);
188
+ await this.getMenuItemTop();
189
+ return true;
190
+ },
151
191
  // 获取一个目标元素的高度
152
192
  getElRect(elClass, dataVal) {
153
193
  return new Promise((resolve, reject) => {
@@ -196,8 +236,11 @@
196
236
  },
197
237
  // 设置左边菜单的滚动状态
198
238
  async leftMenuStatus(index) {
199
- this.innerCurrent = index;
200
- this.$emit('update:current', index);
239
+ // 仅在选中项真正变化时才同步父组件,否则滚动过程中每次都会重复通知
240
+ if (this.innerCurrent != index) {
241
+ this.innerCurrent = index;
242
+ this.$emit('update:current', index);
243
+ }
201
244
  // 如果为0,意味着尚未初始化
202
245
  if (this.menuHeight == 0 || this.menuItemHeight == 0) {
203
246
  await this.getElRect('u-cate-tab__menu-scroll-view', 'menuHeight');
@@ -239,17 +282,15 @@
239
282
  this.oldScrollTop = e.detail.scrollTop;
240
283
  // console.log(e.detail.scrollTop)
241
284
  // console.log(JSON.stringify(this.arr))
242
- if(this.arr.length == 0) {
243
- await this.getMenuItemTop();
244
- }
245
285
  if(this.timer) return ;
246
- if(!this.menuHeight) {
247
- await this.getElRect('u-cate-tab__menu-scroll-view', 'menuHeight');
248
- }
249
- setTimeout(() => { // 节流
286
+ // 节流:定时器必须挂到this上,否则上面的拦截永远不生效,每个滚动事件都会跑一遍联动
287
+ this.timer = setTimeout(async () => {
250
288
  this.timer = null;
289
+ if(this.arr.length == 0) {
290
+ await this.getMenuItemTop();
291
+ }
251
292
  // scrollHeight为右边菜单垂直中点位置
252
- let scrollHeight = e.detail.scrollTop + 1;
293
+ let scrollHeight = this.oldScrollTop + 1;
253
294
  // console.log(e.detail.scrollTop)
254
295
  for (let i = 0; i < this.arr.length; i++) {
255
296
  let height1 = this.arr[i];
@@ -263,6 +304,10 @@
263
304
  // console.log('height1', height1)
264
305
  // console.log('height2', height2)
265
306
  this.leftMenuStatus(i);
307
+ // 快滚到已渲染的最后一个分类时追加下一批
308
+ if (i >= this.renderLimit - 2) {
309
+ this.growRender(this.renderLimit + this.lazyRenderCount);
310
+ }
266
311
  return ;
267
312
  }
268
313
  }