uview-plus 3.8.126 → 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.
- package/README.md +13 -0
- package/changelog.md +39 -0
- package/components/u-calendar/month.vue +71 -35
- package/components/u-calendar/u-calendar.vue +67 -31
- package/components/u-car-keyboard/u-car-keyboard.vue +21 -8
- package/components/u-cate-tab/u-cate-tab.vue +56 -11
- package/components/u-cell/u-cell.vue +20 -16
- package/components/u-collapse-item/u-collapse-item.vue +34 -11
- package/components/u-datetime-picker/u-datetime-picker.vue +16 -0
- package/components/u-dropdown/u-dropdown.vue +15 -0
- package/components/u-grid/u-grid.vue +9 -0
- package/components/u-grid-item/u-grid-item.vue +8 -0
- package/components/u-icon/util.js +16 -1
- package/components/u-list/u-list.vue +59 -8
- package/components/u-list-item/u-list-item.vue +13 -3
- package/components/u-number-keyboard/u-number-keyboard.vue +18 -6
- package/components/u-parse/parser.js +4 -3
- package/components/u-picker/u-picker.vue +6 -3
- package/components/u-search/u-search.vue +15 -5
- package/components/u-select/u-select.vue +206 -31
- package/components/u-slider/u-slider.vue +21 -12
- package/components/u-steps-item/u-steps-item.vue +9 -0
- package/components/u-tabbar-item/u-tabbar-item.vue +12 -1
- package/components/u-tabs/u-tabs.vue +5 -1
- package/components/u-text/u-text.vue +34 -32
- package/components/u-textarea/props.js +5 -0
- package/components/u-textarea/textarea.js +4 -1
- package/components/u-textarea/u-textarea.vue +3 -1
- package/components/u-upload/u-upload.vue +15 -4
- package/components/u-upload/utils.js +4 -1
- package/components/u-video/danmaku.js +97 -0
- package/components/u-video/props.js +205 -0
- package/components/u-video/u-video.vue +1427 -0
- package/components/u-video/video-danmaku.vue +273 -0
- package/components/u-video/video-slider.vue +179 -0
- package/components/u-video/video-utils.js +98 -0
- package/components/u-video/video.js +72 -0
- package/libs/config/props.js +2 -1
- package/libs/function/test.js +2 -0
- package/libs/i18n/locales/de.js +14 -1
- package/libs/i18n/locales/en.js +14 -1
- package/libs/i18n/locales/es.js +14 -1
- package/libs/i18n/locales/fr.js +14 -1
- package/libs/i18n/locales/ja.js +14 -1
- package/libs/i18n/locales/ko.js +14 -1
- package/libs/i18n/locales/ru.js +14 -1
- package/libs/i18n/locales/th.js +14 -1
- package/libs/i18n/locales/zh-Hans.js +14 -1
- package/libs/i18n/locales/zh-Hant.js +14 -1
- package/libs/mixin/mixin.js +12 -7
- package/package.json +1 -1
- package/skills/README.md +66 -0
- package/skills/uview-plus/SKILL.md +95 -0
- package/skills/uview-plus/references/api.md +105 -0
- package/skills/uview-plus/references/components.md +156 -0
- package/skills/uview-plus/references/icons.md +223 -0
- package/skills/uview-plus/references/pitfalls.md +77 -0
- package/skills/uview-plus/references/setup.md +168 -0
- package/types/comps/form.d.ts +44 -8
- package/types/comps/list.d.ts +1 -1
- package/types/comps/listItem.d.ts +1 -1
- package/types/comps/numberBox.d.ts +85 -21
- package/types/comps/pagination.d.ts +96 -0
- package/types/comps/swipeActionItem.d.ts +3 -4
- package/types/comps/textarea.d.ts +9 -3
- package/types/comps.d.ts +1 -0
- 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,42 @@
|
|
|
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
|
+
|
|
30
|
+
## 3.8.127
|
|
31
|
+
fix: 修复抖音小程序 tabbar-item / steps-item 因父子生命周期顺序导致的崩溃
|
|
32
|
+
|
|
33
|
+
抖音小程序中,子组件的 created 可能早于父组件的 created 执行,此时父组件实例已可通过 $parent 找到,但父组件的 children 数组尚未初始化(u-tabbar / u-steps / u-list 都在自己的 created 里才赋值 children=[])。子组件 init() 直接访问 this.parent.children.indexOf(this) 时 children 仍为 undefined,抛出 TypeError: Cannot read properties of undefined (reading 'indexOf'),并伴随「up-tabbar-item必须搭配up-tabbar组件使用」的误报警告(该分支只 log 未 return,继续执行才崩溃)。对照 u-radio / u-checkbox:它们在 mounted() 调 init() 且不访问 parent.children,所以不受影响。
|
|
34
|
+
|
|
35
|
+
- u-tabbar-item:init() 在 error() 后补 return,并加 !this.parent.children 守卫;mounted() 新增 init() 补偿重试(created 阶段被跳过时补齐激活态);clickHandler() 补 this.parent / children 守卫
|
|
36
|
+
- u-steps-item:init() 加 !this.parent.children 守卫;mounted() 新增 init() 补偿重试,确保自身已注册进父组件 children
|
|
37
|
+
- u-list-item:init() 加 !this.parent / !this.parent.children 守卫,避免 $parent 返回 false 时报错
|
|
38
|
+
- 共享 mixin getParentData():访问 children 前先判断 this.parent 存在,避免 $parent 返回 false 时读取 false.children 崩溃,影响所有父子联动组件
|
|
39
|
+
|
|
1
40
|
## 3.8.126
|
|
2
41
|
feat: 新增 u-flex 通用弹性布局容器组件
|
|
3
42
|
|
|
@@ -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 =
|
|
232
|
+
let date = this.dateOf(item),
|
|
208
233
|
style = {}
|
|
209
|
-
//
|
|
210
|
-
if (this.
|
|
234
|
+
// selected已归一化为YYYY-MM-DD(月份可能需要补0),故可直接比较字符串
|
|
235
|
+
if (this.selectedDateMap[date]) {
|
|
211
236
|
style.backgroundColor = this.color
|
|
212
237
|
}
|
|
213
|
-
if (this.
|
|
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.
|
|
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.
|
|
227
|
-
const len = this.selected.length - 1
|
|
251
|
+
if (this.selectedDates.length >= 2) {
|
|
228
252
|
// 第一个日期设置左上角和左下角的圆角
|
|
229
|
-
if (
|
|
253
|
+
if (date === this.selectedStart) {
|
|
230
254
|
style.borderTopLeftRadius = '3px'
|
|
231
255
|
style.borderBottomLeftRadius = '3px'
|
|
232
256
|
}
|
|
233
257
|
// 最后一个日期设置右上角和右下角的圆角
|
|
234
|
-
if (
|
|
258
|
+
if (date === this.selectedEnd) {
|
|
235
259
|
style.borderTopRightRadius = '3px'
|
|
236
260
|
style.borderBottomRightRadius = '3px'
|
|
237
261
|
}
|
|
238
262
|
// 处于第一和最后一个之间的日期,背景色设置为浅色,通过将对应颜色进行等分,再取其尾部的颜色值
|
|
239
|
-
if (
|
|
240
|
-
|
|
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.
|
|
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.
|
|
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 =
|
|
297
|
+
const date = this.dateOf(item),
|
|
270
298
|
style = {}
|
|
271
299
|
// 选中的日期,提示文字设置白色
|
|
272
|
-
if (this.
|
|
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 (
|
|
279
|
-
.selected[len]))) {
|
|
305
|
+
if (date > this.selectedStart && date < this.selectedEnd) {
|
|
280
306
|
style.color = this.color
|
|
281
307
|
}
|
|
282
308
|
}
|
|
283
|
-
if (this.
|
|
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 =
|
|
318
|
+
const date = this.dateOf(item)
|
|
293
319
|
const bottomInfo = item.bottomInfo
|
|
294
320
|
// 当为日期范围模式时,且选择的日期个数大于0时
|
|
295
|
-
if (this.mode === 'range' && this.
|
|
296
|
-
if (this.
|
|
321
|
+
if (this.mode === 'range' && this.selectedDates.length > 0) {
|
|
322
|
+
if (this.selectedDates.length === 1) {
|
|
297
323
|
// 选择了一个日期时,如果当前日期为数组中的第一个日期,则显示底部文字为“开始”
|
|
298
|
-
if (
|
|
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.
|
|
304
|
-
|
|
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 (
|
|
332
|
+
} else if (date === this.selectedStart) {
|
|
308
333
|
return this.startText
|
|
309
|
-
} else if (
|
|
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
|
-
|
|
347
|
-
|
|
348
|
-
return true
|
|
371
|
+
if (this.mode === 'range' || !this.forbidDays.length) {
|
|
372
|
+
return false
|
|
349
373
|
}
|
|
350
|
-
return
|
|
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.
|
|
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 =
|
|
635
|
-
|
|
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
|
-
|
|
647
|
-
|
|
648
|
-
|
|
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
|
-
|
|
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:
|
|
685
|
+
month: monthValue,
|
|
676
686
|
// 当前年份
|
|
677
|
-
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
|
-
|
|
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
|
-
|
|
772
|
-
|
|
773
|
-
|
|
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
|
-
@
|
|
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,
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
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
|
-
|
|
200
|
-
this
|
|
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
|
-
|
|
247
|
-
|
|
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 =
|
|
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
|
}
|