@tuya-miniapp/smart-ui 2.13.4 → 2.13.5-beta-0

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-zh_CN.md CHANGED
@@ -1,3 +1,10 @@
1
+ <!--
2
+ * @Author: mjh
3
+ * @Date: 2026-04-01 14:55:38
4
+ * @LastEditors: mjh
5
+ * @LastEditTime: 2026-08-24 11:44:37
6
+ * @Description:
7
+ -->
1
8
  [English](./README.md) | 简体中文
2
9
 
3
10
  # @tuya-miniapp/smart-ui
@@ -78,6 +85,7 @@ yarn dev
78
85
 
79
86
  > 本项目遵从 [Angular Style Commit Message Conventions](https://gist.github.com/stephenparish/9941e89d80e2bc58a153)
80
87
 
88
+
81
89
  [官网更新日志](https://developer.tuya.com/material/smartui?comId=help-changelog&appType=miniapp)
82
90
 
83
91
  ## 开源协议
@@ -1,5 +1,10 @@
1
1
  import { SmartComponent } from '../common/component';
2
2
  import { pickerProps } from './shared';
3
+ // 超过该数量且非 loop 的列启用「窗口化」:只把当前位置附近的一段数据交给子 picker-column,
4
+ // 其余保存在逻辑层实例属性中,滚动停稳后再回中,避免把超长数组整份 setData 到渲染层 / 子组件。
5
+ const WINDOW_THRESHOLD = 2000;
6
+ const WINDOW_SIZE = 1000;
7
+ const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
3
8
  SmartComponent({
4
9
  classes: ['hairline-class', 'active-class', 'toolbar-class', 'column-class'],
5
10
  props: Object.assign(Object.assign({}, pickerProps), { valueKey: {
@@ -16,6 +21,12 @@ SmartComponent({
16
21
  value: 0,
17
22
  }, activeIndex: {
18
23
  type: null,
24
+ observer() {
25
+ // 受控模式下 activeIndex(全局)变化时,若存在窗口化列需以新位置重建窗口
26
+ if (this._hasWindow) {
27
+ this.buildRenderColumns();
28
+ }
29
+ },
19
30
  }, unit: {
20
31
  type: String,
21
32
  value: '',
@@ -24,6 +35,7 @@ SmartComponent({
24
35
  value: [],
25
36
  observer(columns = []) {
26
37
  this.simple = columns.length && !columns[0].values;
38
+ this.buildRenderColumns();
27
39
  if (Array.isArray(this.children) && this.children.length) {
28
40
  this.setColumns().catch(() => { });
29
41
  }
@@ -31,8 +43,14 @@ SmartComponent({
31
43
  } }),
32
44
  data: {
33
45
  animating: false,
46
+ // 交给渲染层 / 子组件的列数据(窗口化列在此仅为切片,非窗口化列为完整数据)
47
+ renderColumns: [],
34
48
  },
35
49
  beforeCreate() {
50
+ // 每列的窗口状态:{ windowed, start, size, full },保存在逻辑层,不进入 data
51
+ this._windows = [];
52
+ this._hasWindow = false;
53
+ this._repositioning = false;
36
54
  Object.defineProperty(this, 'children', {
37
55
  get: () => this.selectAllComponents('.smart-picker__column') || [],
38
56
  });
@@ -44,10 +62,103 @@ SmartComponent({
44
62
  },
45
63
  methods: {
46
64
  noop() { },
65
+ // 归一化列数据:simple 模式(columns 为一维数组)包一层
66
+ normalizedColumns() {
67
+ const { columns } = this.data;
68
+ if (this.simple) {
69
+ return [{ values: columns }];
70
+ }
71
+ return Array.isArray(columns) ? columns : [];
72
+ },
73
+ // 解析某列的初始全局下标(优先 activeIndex,其次 defaultIndex)
74
+ resolveInitialIndex(column) {
75
+ const { defaultIndex, activeIndex } = this.data;
76
+ const columnActive = column.activeIndex === null || column.activeIndex === undefined
77
+ ? activeIndex
78
+ : column.activeIndex;
79
+ let index;
80
+ if (columnActive !== null && columnActive !== undefined) {
81
+ index = columnActive;
82
+ }
83
+ else if (column.defaultIndex !== undefined) {
84
+ index = column.defaultIndex;
85
+ }
86
+ else {
87
+ index = defaultIndex;
88
+ }
89
+ const { length } = column.values || [];
90
+ return clamp(index || 0, 0, Math.max(0, length - 1));
91
+ },
92
+ // 判断某列是否需要窗口化
93
+ shouldWindow(column) {
94
+ return (!this.data.loop &&
95
+ !column.loop &&
96
+ Array.isArray(column.values) &&
97
+ column.values.length > WINDOW_THRESHOLD);
98
+ },
99
+ // 基于当前 columns 计算 renderColumns 与每列窗口状态
100
+ buildRenderColumns() {
101
+ const columns = this.normalizedColumns();
102
+ const windows = [];
103
+ let hasWindow = false;
104
+ const renderColumns = columns.map((column) => {
105
+ const values = column.values || [];
106
+ if (!this.shouldWindow(column)) {
107
+ windows.push({ windowed: false, start: 0, size: values.length, full: values });
108
+ return column;
109
+ }
110
+ hasWindow = true;
111
+ const globalIndex = this.resolveInitialIndex(column);
112
+ const size = WINDOW_SIZE;
113
+ const start = clamp(globalIndex - Math.floor(size / 2), 0, Math.max(0, values.length - size));
114
+ const localIndex = globalIndex - start;
115
+ windows.push({ windowed: true, start, size, full: values });
116
+ return Object.assign(Object.assign({}, column), { values: values.slice(start, start + size), defaultIndex: localIndex, activeIndex: localIndex, _windowed: true });
117
+ });
118
+ this._windows = windows;
119
+ this._hasWindow = hasWindow;
120
+ this.setData({ renderColumns });
121
+ },
122
+ // 将窗口化列停稳后回中:使当前项回到窗口中心,纯坐标平移,静止态无感
123
+ recenterColumn(columnIndex) {
124
+ const win = this._windows[columnIndex];
125
+ if (!win || !win.windowed || this._repositioning)
126
+ return;
127
+ const column = this.getColumn(columnIndex);
128
+ if (!column)
129
+ return;
130
+ const localIndex = column.data.currentIndex;
131
+ const globalIndex = win.start + localIndex;
132
+ const { size } = win;
133
+ const newStart = clamp(globalIndex - Math.floor(size / 2), 0, Math.max(0, win.full.length - size));
134
+ if (newStart === win.start)
135
+ return;
136
+ this._repositioning = true;
137
+ win.start = newStart;
138
+ const newLocal = globalIndex - newStart;
139
+ // 同一次 setData 内同时更新切片与 active-index(局部),子组件原子接收,
140
+ // 配合 wxs 对大跳变强制 transition:none,实现无动画的静默回中
141
+ this.setData({
142
+ [`renderColumns[${columnIndex}].values`]: win.full.slice(newStart, newStart + size),
143
+ [`renderColumns[${columnIndex}].activeIndex`]: newLocal,
144
+ }, () => {
145
+ this._repositioning = false;
146
+ });
147
+ },
148
+ // 局部下标 -> 全局下标
149
+ toGlobalIndex(columnIndex, localIndex) {
150
+ const win = this._windows[columnIndex];
151
+ return win && win.windowed ? win.start + localIndex : localIndex;
152
+ },
47
153
  setColumns() {
48
- const { data } = this;
49
- const columns = this.simple ? [{ values: data.columns }] : data.columns;
50
- const stack = columns.map((column, index) => this.setColumnValues(index, column.values));
154
+ const columns = this.normalizedColumns();
155
+ const stack = columns.map((column, index) => {
156
+ // 窗口化列由 renderColumns(wxml)响应式驱动,不走命令式全量下发
157
+ if (this._windows[index] && this._windows[index].windowed) {
158
+ return Promise.resolve();
159
+ }
160
+ return this.setColumnValues(index, column.values);
161
+ });
51
162
  return Promise.all(stack);
52
163
  },
53
164
  emit(event) {
@@ -66,6 +177,7 @@ SmartComponent({
66
177
  }
67
178
  },
68
179
  onChange(event) {
180
+ const columnIndex = this.simple ? 0 : event.currentTarget.dataset.index;
69
181
  if (this.simple) {
70
182
  this.$emit('change', {
71
183
  picker: this,
@@ -80,6 +192,8 @@ SmartComponent({
80
192
  index: event.currentTarget.dataset.index,
81
193
  });
82
194
  }
195
+ // 停稳后再回中,保证切换发生在静止态
196
+ this.recenterColumn(columnIndex);
83
197
  },
84
198
  // get column instance by index
85
199
  getColumn(index) {
@@ -92,6 +206,19 @@ SmartComponent({
92
206
  },
93
207
  // set column value by index
94
208
  setColumnValue(index, value) {
209
+ const win = this._windows[index];
210
+ if (win && win.windowed) {
211
+ // 窗口化列:在完整数据中按 valueKey 查找全局下标后跳转
212
+ const { valueKey } = this.data;
213
+ for (let i = 0; i < win.full.length; i++) {
214
+ const option = win.full[i];
215
+ const text = option && typeof option === 'object' && valueKey in option ? option[valueKey] : option;
216
+ if (text === value) {
217
+ return this.setColumnIndex(index, i);
218
+ }
219
+ }
220
+ return Promise.resolve();
221
+ }
95
222
  const column = this.getColumn(index);
96
223
  if (column == null) {
97
224
  return Promise.reject(new Error('setColumnValue: The corresponding column does not exist'));
@@ -100,10 +227,27 @@ SmartComponent({
100
227
  },
101
228
  // get column option index by column index
102
229
  getColumnIndex(columnIndex) {
103
- return (this.getColumn(columnIndex) || {}).data.currentIndex;
230
+ var _a, _b;
231
+ const local = (_b = (_a = (this.getColumn(columnIndex) || {}).data) === null || _a === void 0 ? void 0 : _a.currentIndex) !== null && _b !== void 0 ? _b : 0;
232
+ return this.toGlobalIndex(columnIndex, local);
104
233
  },
105
234
  // set column option index by column index
106
235
  setColumnIndex(columnIndex, optionIndex) {
236
+ const win = this._windows[columnIndex];
237
+ if (win && win.windowed) {
238
+ // 以目标全局下标为中心重建窗口,子组件通过 active-index 定位
239
+ const { size } = win;
240
+ const globalIndex = clamp(optionIndex, 0, Math.max(0, win.full.length - 1));
241
+ const newStart = clamp(globalIndex - Math.floor(size / 2), 0, Math.max(0, win.full.length - size));
242
+ win.start = newStart;
243
+ this._repositioning = true;
244
+ return this.set({
245
+ [`renderColumns[${columnIndex}].values`]: win.full.slice(newStart, newStart + size),
246
+ [`renderColumns[${columnIndex}].activeIndex`]: globalIndex - newStart,
247
+ }).then(() => {
248
+ this._repositioning = false;
249
+ });
250
+ }
107
251
  const column = this.getColumn(columnIndex);
108
252
  if (column == null) {
109
253
  return Promise.reject(new Error('setColumnIndex: The corresponding column does not exist'));
@@ -112,15 +256,37 @@ SmartComponent({
112
256
  },
113
257
  // get options of column by index
114
258
  getColumnValues(index) {
259
+ const win = this._windows[index];
260
+ if (win && win.windowed) {
261
+ return win.full;
262
+ }
115
263
  return (this.children[index] || {}).data.options;
116
264
  },
117
265
  // set options of column by index
118
266
  setColumnValues(index, options, needReset = true) {
267
+ const win = this._windows[index];
268
+ if (win && win.windowed) {
269
+ // 替换完整数据并以头部为中心重建窗口
270
+ win.full = options || [];
271
+ const { size } = win;
272
+ const start = needReset ? 0 : clamp(win.start, 0, Math.max(0, win.full.length - size));
273
+ win.start = start;
274
+ this._repositioning = true;
275
+ return this.set({
276
+ [`renderColumns[${index}].values`]: win.full.slice(start, start + size),
277
+ [`renderColumns[${index}].activeIndex`]: 0,
278
+ }).then(() => {
279
+ this._repositioning = false;
280
+ });
281
+ }
119
282
  const column = this.children[index];
120
283
  if (column == null) {
121
284
  return Promise.reject(new Error('setColumnValues: The corresponding column does not exist'));
122
285
  }
123
- const isSame = JSON.stringify(column.data.options) === JSON.stringify(options);
286
+ const prevOptions = column.data.options || [];
287
+ // 先比长度(O(1))短路:长度不同必然不同,避免对超长数组做两次 JSON.stringify
288
+ const isSame = prevOptions.length === options.length &&
289
+ JSON.stringify(prevOptions) === JSON.stringify(options);
124
290
  if (isSame) {
125
291
  return Promise.resolve();
126
292
  }
@@ -141,7 +307,7 @@ SmartComponent({
141
307
  },
142
308
  // get indexes of all columns
143
309
  getIndexes() {
144
- return this.children.map(child => child.data.currentIndex);
310
+ return this.children.map((child, index) => this.toGlobalIndex(index, child.data.currentIndex));
145
311
  },
146
312
  // set indexes of all columns
147
313
  setIndexes(indexes) {
@@ -14,7 +14,7 @@
14
14
  >
15
15
  <picker-column
16
16
  class="smart-picker__column"
17
- wx:for="{{ computed.columns(columns) }}"
17
+ wx:for="{{ renderColumns }}"
18
18
  wx:key="index"
19
19
  data-index="{{ index }}"
20
20
  animation-time="{{ animationTime }}"
@@ -24,6 +24,7 @@
24
24
  active-style="{{ activeStyle }}"
25
25
  font-style="{{fontStyle ? fontStyle + ';' + computed.style(item.fontStyle): computed.style(item.fontStyle)}}"
26
26
  options="{{ item.values }}"
27
+ windowed="{{ item._windowed || false }}"
27
28
  disabled="{{ item.disabled || false }}"
28
29
  unit="{{ item.unit || unit }}"
29
30
  unit-gap="{{ item.unitGap }}"
@@ -16,6 +16,11 @@ SmartComponent({
16
16
  className: String,
17
17
  itemHeight: Number,
18
18
  disabled: Boolean,
19
+ // 是否为父级窗口化列:为 true 时对大幅度的 active-index 跳变强制关闭过渡(用于停稳回中的静默重定位)
20
+ windowed: {
21
+ type: Boolean,
22
+ value: false,
23
+ },
19
24
  visibleItemCount: Number,
20
25
  activeStyle: {
21
26
  type: String,
@@ -28,7 +33,12 @@ SmartComponent({
28
33
  if (!this.data.isInit)
29
34
  return;
30
35
  this.updateUint(value);
31
- this.updateCurrentIndex(this.data.currentIndex);
36
+ // 窗口化列:切片与 activeIndex(局部)在同一批 setData 到达,
37
+ // 此处直接采用新的 activeIndex,使「新切片 + 新下标」原子落地,避免差一帧的错位闪烁
38
+ const nextIndex = this.data.windowed && this.data.activeIndex !== null && this.data.activeIndex !== undefined
39
+ ? this.data.activeIndex
40
+ : this.data.currentIndex;
41
+ this.updateCurrentIndex(nextIndex);
32
42
  this.updateVisibleOptions();
33
43
  },
34
44
  },
@@ -8,15 +8,15 @@
8
8
  id="{{instanceId}}"
9
9
  data-isdestroy="{{isDestroy}}"
10
10
  data-changeanimation="{{changeAnimation}}"
11
- data-options="{{options}}"
12
11
  data-valuekey="{{valueKey}}"
13
12
  data-itemheight="{{itemHeight}}"
14
13
  data-visibleitemcount="{{visibleItemCount}}"
15
14
  data-activeindex="{{currentIndex}}"
16
15
  data-loop="{{loop && options.length > 1}}"
17
16
  data-animationtime="{{animationTime}}"
17
+ data-windowed="{{windowed}}"
18
18
  isdestroy="{{isDestroy}}"
19
- options="{{options}}"
19
+ options="{{options}}"
20
20
  activeindex="{{currentIndex}}"
21
21
  changeanimation="{{changeAnimation}}"
22
22
  loop="{{loop && options.length > 1}}"
@@ -24,15 +24,17 @@
24
24
  itemheight="{{itemHeight}}"
25
25
  visibleitemcount="{{visibleItemCount}}"
26
26
  animationtime="{{animationTime}}"
27
+ windowed="{{windowed}}"
27
28
  change:isdestroy="{{computed.updateValue(instanceId, 'isDestroy')}}"
28
- change:options="{{computed.updateValue(instanceId, 'options')}}"
29
- change:valuekey="{{computed.updateValue(instanceId,'valueKey')}}"
30
- change:itemheight="{{computed.updateValue(instanceId,'itemHeight')}}"
31
- change:visibleitemcount="{{computed.updateValue(instanceId, 'visibleItemCount')}}"
32
- change:activeindex="{{computed.updateValue(instanceId,'activeIndex')}}"
33
- change:loop="{{computed.updateValue(instanceId,'loop')}}"
34
- change:animationtime="{{computed.updateValue(instanceId,'animationTime')}}"
35
- change:instanceid="{{computed.updateValue(instanceId,'instanceId')}}"
29
+ change:options="{{computed.updateValue(instanceId, 'options')}}"
30
+ change:valuekey="{{computed.updateValue(instanceId,'valueKey')}}"
31
+ change:itemheight="{{computed.updateValue(instanceId,'itemHeight')}}"
32
+ change:visibleitemcount="{{computed.updateValue(instanceId, 'visibleItemCount')}}"
33
+ change:activeindex="{{computed.updateValue(instanceId,'activeIndex')}}"
34
+ change:loop="{{computed.updateValue(instanceId,'loop')}}"
35
+ change:animationtime="{{computed.updateValue(instanceId,'animationTime')}}"
36
+ change:windowed="{{computed.updateValue(instanceId,'windowed')}}"
37
+ change:instanceid="{{computed.updateValue(instanceId,'instanceId')}}"
36
38
  bind:touchstart="{{computed.touchStart(instanceId)}}"
37
39
  catch:touchmove="{{computed.touchMove(instanceId)}}"
38
40
  bind:touchend="{{computed.touchEnd(instanceId)}}"
@@ -226,7 +226,8 @@ function getDomState(ownerInstance) {
226
226
 
227
227
  function initDomState(instanceId, ownerInstance) {
228
228
  var state = getDomState(ownerInstance);
229
- updateState(instanceId, 'options', state.options)
229
+ // options 不再通过 data-options 传入(避免把全量数组塞进 dataset / getDataset),
230
+ // 由 change:options -> updateValue('options', ...) 单独维护 state.options
230
231
  updateState(instanceId, 'valueKey', state.valuekey)
231
232
  updateState(instanceId, 'itemHeight', state.itemheight)
232
233
  updateState(instanceId, 'visibleItemCount', state.visibleitemcount)
@@ -234,6 +235,7 @@ function initDomState(instanceId, ownerInstance) {
234
235
  updateState(instanceId, 'loop', state.loop)
235
236
  updateState(instanceId, 'animationTime', state.animationtime)
236
237
  updateState(instanceId, 'changeAnimation', state.changeanimation)
238
+ updateState(instanceId, 'windowed', state.windowed)
237
239
  }
238
240
 
239
241
 
@@ -262,10 +264,14 @@ function updateListView(instanceId, ownerInstance) {
262
264
  var animationIndex = state.animationIndex || 0;
263
265
  var newAnimationIndex = getNewAnimationIndex(animationIndex, activeIndex, length, state.loop);
264
266
  var isSame = newAnimationIndex === animationIndex;
267
+ // 窗口化列的「停稳回中/重定位」会产生大幅度的 activeIndex 跳变,此时强制关闭过渡:
268
+ // 因为切片内容与位置是同步平移的,直接吸附即像素级一致,避免整轮旋转动画造成抖动。
269
+ var vOptionLength = state.visibleItemCount * 4 - 2;
270
+ var isWindowReposition = state.windowed && Math.abs(newAnimationIndex - animationIndex) > vOptionLength;
265
271
  updateState(instanceId, 'animationIndex', newAnimationIndex)
266
272
  updateVisibleOptions(state.animationIndex, instanceId);
267
273
  updateWrapperStyle(instanceId, ownerInstance, {
268
- transition: (!state.changeAnimation || isSame) ? 'none' : 'transform ' + state.animationTime + 'ms cubic-bezier(0.2, 0.9, 0.25, 1)'
274
+ transition: (!state.changeAnimation || isSame || isWindowReposition) ? 'none' : 'transform ' + state.animationTime + 'ms cubic-bezier(0.2, 0.9, 0.25, 1)'
269
275
  });
270
276
  }
271
277
 
@@ -373,7 +379,8 @@ function touchEnd(instanceId) {
373
379
 
374
380
  // 惯性滚动参数配置
375
381
  var minVelocity = 0.1; // 最小速度阈值,低于此值停止滚动
376
- var maxInertiaDistance = state.itemHeight * Math.max(Math.floor(state.options.length/4), 6); // 最大惯性滚动距离
382
+ // 最大惯性滚动距离:下限 6 项,上限 100 项,避免超长列表单次快滑滚动上万项
383
+ var maxInertiaDistance = state.itemHeight * Math.min(Math.max(Math.floor(state.options.length/4), 6), 100);
377
384
  // 计算惯性滚动距离
378
385
  var inertiaDistance = 0;
379
386
  if (recentVelocity > minVelocity) {
@@ -13,6 +13,11 @@ var __assign = (this && this.__assign) || function () {
13
13
  Object.defineProperty(exports, "__esModule", { value: true });
14
14
  var component_1 = require("../common/component");
15
15
  var shared_1 = require("./shared");
16
+ // 超过该数量且非 loop 的列启用「窗口化」:只把当前位置附近的一段数据交给子 picker-column,
17
+ // 其余保存在逻辑层实例属性中,滚动停稳后再回中,避免把超长数组整份 setData 到渲染层 / 子组件。
18
+ var WINDOW_THRESHOLD = 2000;
19
+ var WINDOW_SIZE = 1000;
20
+ var clamp = function (value, min, max) { return Math.min(Math.max(value, min), max); };
16
21
  (0, component_1.SmartComponent)({
17
22
  classes: ['hairline-class', 'active-class', 'toolbar-class', 'column-class'],
18
23
  props: __assign(__assign({}, shared_1.pickerProps), { valueKey: {
@@ -29,6 +34,12 @@ var shared_1 = require("./shared");
29
34
  value: 0,
30
35
  }, activeIndex: {
31
36
  type: null,
37
+ observer: function () {
38
+ // 受控模式下 activeIndex(全局)变化时,若存在窗口化列需以新位置重建窗口
39
+ if (this._hasWindow) {
40
+ this.buildRenderColumns();
41
+ }
42
+ },
32
43
  }, unit: {
33
44
  type: String,
34
45
  value: '',
@@ -38,6 +49,7 @@ var shared_1 = require("./shared");
38
49
  observer: function (columns) {
39
50
  if (columns === void 0) { columns = []; }
40
51
  this.simple = columns.length && !columns[0].values;
52
+ this.buildRenderColumns();
41
53
  if (Array.isArray(this.children) && this.children.length) {
42
54
  this.setColumns().catch(function () { });
43
55
  }
@@ -45,9 +57,15 @@ var shared_1 = require("./shared");
45
57
  } }),
46
58
  data: {
47
59
  animating: false,
60
+ // 交给渲染层 / 子组件的列数据(窗口化列在此仅为切片,非窗口化列为完整数据)
61
+ renderColumns: [],
48
62
  },
49
63
  beforeCreate: function () {
50
64
  var _this = this;
65
+ // 每列的窗口状态:{ windowed, start, size, full },保存在逻辑层,不进入 data
66
+ this._windows = [];
67
+ this._hasWindow = false;
68
+ this._repositioning = false;
51
69
  Object.defineProperty(this, 'children', {
52
70
  get: function () { return _this.selectAllComponents('.smart-picker__column') || []; },
53
71
  });
@@ -59,11 +77,105 @@ var shared_1 = require("./shared");
59
77
  },
60
78
  methods: {
61
79
  noop: function () { },
80
+ // 归一化列数据:simple 模式(columns 为一维数组)包一层
81
+ normalizedColumns: function () {
82
+ var columns = this.data.columns;
83
+ if (this.simple) {
84
+ return [{ values: columns }];
85
+ }
86
+ return Array.isArray(columns) ? columns : [];
87
+ },
88
+ // 解析某列的初始全局下标(优先 activeIndex,其次 defaultIndex)
89
+ resolveInitialIndex: function (column) {
90
+ var _a = this.data, defaultIndex = _a.defaultIndex, activeIndex = _a.activeIndex;
91
+ var columnActive = column.activeIndex === null || column.activeIndex === undefined
92
+ ? activeIndex
93
+ : column.activeIndex;
94
+ var index;
95
+ if (columnActive !== null && columnActive !== undefined) {
96
+ index = columnActive;
97
+ }
98
+ else if (column.defaultIndex !== undefined) {
99
+ index = column.defaultIndex;
100
+ }
101
+ else {
102
+ index = defaultIndex;
103
+ }
104
+ var length = (column.values || []).length;
105
+ return clamp(index || 0, 0, Math.max(0, length - 1));
106
+ },
107
+ // 判断某列是否需要窗口化
108
+ shouldWindow: function (column) {
109
+ return (!this.data.loop &&
110
+ !column.loop &&
111
+ Array.isArray(column.values) &&
112
+ column.values.length > WINDOW_THRESHOLD);
113
+ },
114
+ // 基于当前 columns 计算 renderColumns 与每列窗口状态
115
+ buildRenderColumns: function () {
116
+ var _this = this;
117
+ var columns = this.normalizedColumns();
118
+ var windows = [];
119
+ var hasWindow = false;
120
+ var renderColumns = columns.map(function (column) {
121
+ var values = column.values || [];
122
+ if (!_this.shouldWindow(column)) {
123
+ windows.push({ windowed: false, start: 0, size: values.length, full: values });
124
+ return column;
125
+ }
126
+ hasWindow = true;
127
+ var globalIndex = _this.resolveInitialIndex(column);
128
+ var size = WINDOW_SIZE;
129
+ var start = clamp(globalIndex - Math.floor(size / 2), 0, Math.max(0, values.length - size));
130
+ var localIndex = globalIndex - start;
131
+ windows.push({ windowed: true, start: start, size: size, full: values });
132
+ return __assign(__assign({}, column), { values: values.slice(start, start + size), defaultIndex: localIndex, activeIndex: localIndex, _windowed: true });
133
+ });
134
+ this._windows = windows;
135
+ this._hasWindow = hasWindow;
136
+ this.setData({ renderColumns: renderColumns });
137
+ },
138
+ // 将窗口化列停稳后回中:使当前项回到窗口中心,纯坐标平移,静止态无感
139
+ recenterColumn: function (columnIndex) {
140
+ var _a;
141
+ var _this = this;
142
+ var win = this._windows[columnIndex];
143
+ if (!win || !win.windowed || this._repositioning)
144
+ return;
145
+ var column = this.getColumn(columnIndex);
146
+ if (!column)
147
+ return;
148
+ var localIndex = column.data.currentIndex;
149
+ var globalIndex = win.start + localIndex;
150
+ var size = win.size;
151
+ var newStart = clamp(globalIndex - Math.floor(size / 2), 0, Math.max(0, win.full.length - size));
152
+ if (newStart === win.start)
153
+ return;
154
+ this._repositioning = true;
155
+ win.start = newStart;
156
+ var newLocal = globalIndex - newStart;
157
+ // 同一次 setData 内同时更新切片与 active-index(局部),子组件原子接收,
158
+ // 配合 wxs 对大跳变强制 transition:none,实现无动画的静默回中
159
+ this.setData((_a = {},
160
+ _a["renderColumns[".concat(columnIndex, "].values")] = win.full.slice(newStart, newStart + size),
161
+ _a["renderColumns[".concat(columnIndex, "].activeIndex")] = newLocal,
162
+ _a), function () {
163
+ _this._repositioning = false;
164
+ });
165
+ },
166
+ // 局部下标 -> 全局下标
167
+ toGlobalIndex: function (columnIndex, localIndex) {
168
+ var win = this._windows[columnIndex];
169
+ return win && win.windowed ? win.start + localIndex : localIndex;
170
+ },
62
171
  setColumns: function () {
63
172
  var _this = this;
64
- var data = this.data;
65
- var columns = this.simple ? [{ values: data.columns }] : data.columns;
173
+ var columns = this.normalizedColumns();
66
174
  var stack = columns.map(function (column, index) {
175
+ // 窗口化列由 renderColumns(wxml)响应式驱动,不走命令式全量下发
176
+ if (_this._windows[index] && _this._windows[index].windowed) {
177
+ return Promise.resolve();
178
+ }
67
179
  return _this.setColumnValues(index, column.values);
68
180
  });
69
181
  return Promise.all(stack);
@@ -84,6 +196,7 @@ var shared_1 = require("./shared");
84
196
  }
85
197
  },
86
198
  onChange: function (event) {
199
+ var columnIndex = this.simple ? 0 : event.currentTarget.dataset.index;
87
200
  if (this.simple) {
88
201
  this.$emit('change', {
89
202
  picker: this,
@@ -98,6 +211,8 @@ var shared_1 = require("./shared");
98
211
  index: event.currentTarget.dataset.index,
99
212
  });
100
213
  }
214
+ // 停稳后再回中,保证切换发生在静止态
215
+ this.recenterColumn(columnIndex);
101
216
  },
102
217
  // get column instance by index
103
218
  getColumn: function (index) {
@@ -110,6 +225,19 @@ var shared_1 = require("./shared");
110
225
  },
111
226
  // set column value by index
112
227
  setColumnValue: function (index, value) {
228
+ var win = this._windows[index];
229
+ if (win && win.windowed) {
230
+ // 窗口化列:在完整数据中按 valueKey 查找全局下标后跳转
231
+ var valueKey = this.data.valueKey;
232
+ for (var i = 0; i < win.full.length; i++) {
233
+ var option = win.full[i];
234
+ var text = option && typeof option === 'object' && valueKey in option ? option[valueKey] : option;
235
+ if (text === value) {
236
+ return this.setColumnIndex(index, i);
237
+ }
238
+ }
239
+ return Promise.resolve();
240
+ }
113
241
  var column = this.getColumn(index);
114
242
  if (column == null) {
115
243
  return Promise.reject(new Error('setColumnValue: The corresponding column does not exist'));
@@ -118,10 +246,29 @@ var shared_1 = require("./shared");
118
246
  },
119
247
  // get column option index by column index
120
248
  getColumnIndex: function (columnIndex) {
121
- return (this.getColumn(columnIndex) || {}).data.currentIndex;
249
+ var _a, _b;
250
+ var local = (_b = (_a = (this.getColumn(columnIndex) || {}).data) === null || _a === void 0 ? void 0 : _a.currentIndex) !== null && _b !== void 0 ? _b : 0;
251
+ return this.toGlobalIndex(columnIndex, local);
122
252
  },
123
253
  // set column option index by column index
124
254
  setColumnIndex: function (columnIndex, optionIndex) {
255
+ var _a;
256
+ var _this = this;
257
+ var win = this._windows[columnIndex];
258
+ if (win && win.windowed) {
259
+ // 以目标全局下标为中心重建窗口,子组件通过 active-index 定位
260
+ var size = win.size;
261
+ var globalIndex = clamp(optionIndex, 0, Math.max(0, win.full.length - 1));
262
+ var newStart = clamp(globalIndex - Math.floor(size / 2), 0, Math.max(0, win.full.length - size));
263
+ win.start = newStart;
264
+ this._repositioning = true;
265
+ return this.set((_a = {},
266
+ _a["renderColumns[".concat(columnIndex, "].values")] = win.full.slice(newStart, newStart + size),
267
+ _a["renderColumns[".concat(columnIndex, "].activeIndex")] = globalIndex - newStart,
268
+ _a)).then(function () {
269
+ _this._repositioning = false;
270
+ });
271
+ }
125
272
  var column = this.getColumn(columnIndex);
126
273
  if (column == null) {
127
274
  return Promise.reject(new Error('setColumnIndex: The corresponding column does not exist'));
@@ -130,16 +277,40 @@ var shared_1 = require("./shared");
130
277
  },
131
278
  // get options of column by index
132
279
  getColumnValues: function (index) {
280
+ var win = this._windows[index];
281
+ if (win && win.windowed) {
282
+ return win.full;
283
+ }
133
284
  return (this.children[index] || {}).data.options;
134
285
  },
135
286
  // set options of column by index
136
287
  setColumnValues: function (index, options, needReset) {
288
+ var _a;
289
+ var _this = this;
137
290
  if (needReset === void 0) { needReset = true; }
291
+ var win = this._windows[index];
292
+ if (win && win.windowed) {
293
+ // 替换完整数据并以头部为中心重建窗口
294
+ win.full = options || [];
295
+ var size = win.size;
296
+ var start = needReset ? 0 : clamp(win.start, 0, Math.max(0, win.full.length - size));
297
+ win.start = start;
298
+ this._repositioning = true;
299
+ return this.set((_a = {},
300
+ _a["renderColumns[".concat(index, "].values")] = win.full.slice(start, start + size),
301
+ _a["renderColumns[".concat(index, "].activeIndex")] = 0,
302
+ _a)).then(function () {
303
+ _this._repositioning = false;
304
+ });
305
+ }
138
306
  var column = this.children[index];
139
307
  if (column == null) {
140
308
  return Promise.reject(new Error('setColumnValues: The corresponding column does not exist'));
141
309
  }
142
- var isSame = JSON.stringify(column.data.options) === JSON.stringify(options);
310
+ var prevOptions = column.data.options || [];
311
+ // 先比长度(O(1))短路:长度不同必然不同,避免对超长数组做两次 JSON.stringify
312
+ var isSame = prevOptions.length === options.length &&
313
+ JSON.stringify(prevOptions) === JSON.stringify(options);
143
314
  if (isSame) {
144
315
  return Promise.resolve();
145
316
  }
@@ -161,7 +332,10 @@ var shared_1 = require("./shared");
161
332
  },
162
333
  // get indexes of all columns
163
334
  getIndexes: function () {
164
- return this.children.map(function (child) { return child.data.currentIndex; });
335
+ var _this = this;
336
+ return this.children.map(function (child, index) {
337
+ return _this.toGlobalIndex(index, child.data.currentIndex);
338
+ });
165
339
  },
166
340
  // set indexes of all columns
167
341
  setIndexes: function (indexes) {
@@ -14,7 +14,7 @@
14
14
  >
15
15
  <picker-column
16
16
  class="smart-picker__column"
17
- wx:for="{{ computed.columns(columns) }}"
17
+ wx:for="{{ renderColumns }}"
18
18
  wx:key="index"
19
19
  data-index="{{ index }}"
20
20
  animation-time="{{ animationTime }}"
@@ -24,6 +24,7 @@
24
24
  active-style="{{ activeStyle }}"
25
25
  font-style="{{fontStyle ? fontStyle + ';' + computed.style(item.fontStyle): computed.style(item.fontStyle)}}"
26
26
  options="{{ item.values }}"
27
+ windowed="{{ item._windowed || false }}"
27
28
  disabled="{{ item.disabled || false }}"
28
29
  unit="{{ item.unit || unit }}"
29
30
  unit-gap="{{ item.unitGap }}"
@@ -21,6 +21,11 @@ var getId = function () {
21
21
  className: String,
22
22
  itemHeight: Number,
23
23
  disabled: Boolean,
24
+ // 是否为父级窗口化列:为 true 时对大幅度的 active-index 跳变强制关闭过渡(用于停稳回中的静默重定位)
25
+ windowed: {
26
+ type: Boolean,
27
+ value: false,
28
+ },
24
29
  visibleItemCount: Number,
25
30
  activeStyle: {
26
31
  type: String,
@@ -33,7 +38,12 @@ var getId = function () {
33
38
  if (!this.data.isInit)
34
39
  return;
35
40
  this.updateUint(value);
36
- this.updateCurrentIndex(this.data.currentIndex);
41
+ // 窗口化列:切片与 activeIndex(局部)在同一批 setData 到达,
42
+ // 此处直接采用新的 activeIndex,使「新切片 + 新下标」原子落地,避免差一帧的错位闪烁
43
+ var nextIndex = this.data.windowed && this.data.activeIndex !== null && this.data.activeIndex !== undefined
44
+ ? this.data.activeIndex
45
+ : this.data.currentIndex;
46
+ this.updateCurrentIndex(nextIndex);
37
47
  this.updateVisibleOptions();
38
48
  },
39
49
  },
@@ -8,15 +8,15 @@
8
8
  id="{{instanceId}}"
9
9
  data-isdestroy="{{isDestroy}}"
10
10
  data-changeanimation="{{changeAnimation}}"
11
- data-options="{{options}}"
12
11
  data-valuekey="{{valueKey}}"
13
12
  data-itemheight="{{itemHeight}}"
14
13
  data-visibleitemcount="{{visibleItemCount}}"
15
14
  data-activeindex="{{currentIndex}}"
16
15
  data-loop="{{loop && options.length > 1}}"
17
16
  data-animationtime="{{animationTime}}"
17
+ data-windowed="{{windowed}}"
18
18
  isdestroy="{{isDestroy}}"
19
- options="{{options}}"
19
+ options="{{options}}"
20
20
  activeindex="{{currentIndex}}"
21
21
  changeanimation="{{changeAnimation}}"
22
22
  loop="{{loop && options.length > 1}}"
@@ -24,15 +24,17 @@
24
24
  itemheight="{{itemHeight}}"
25
25
  visibleitemcount="{{visibleItemCount}}"
26
26
  animationtime="{{animationTime}}"
27
+ windowed="{{windowed}}"
27
28
  change:isdestroy="{{computed.updateValue(instanceId, 'isDestroy')}}"
28
- change:options="{{computed.updateValue(instanceId, 'options')}}"
29
- change:valuekey="{{computed.updateValue(instanceId,'valueKey')}}"
30
- change:itemheight="{{computed.updateValue(instanceId,'itemHeight')}}"
31
- change:visibleitemcount="{{computed.updateValue(instanceId, 'visibleItemCount')}}"
32
- change:activeindex="{{computed.updateValue(instanceId,'activeIndex')}}"
33
- change:loop="{{computed.updateValue(instanceId,'loop')}}"
34
- change:animationtime="{{computed.updateValue(instanceId,'animationTime')}}"
35
- change:instanceid="{{computed.updateValue(instanceId,'instanceId')}}"
29
+ change:options="{{computed.updateValue(instanceId, 'options')}}"
30
+ change:valuekey="{{computed.updateValue(instanceId,'valueKey')}}"
31
+ change:itemheight="{{computed.updateValue(instanceId,'itemHeight')}}"
32
+ change:visibleitemcount="{{computed.updateValue(instanceId, 'visibleItemCount')}}"
33
+ change:activeindex="{{computed.updateValue(instanceId,'activeIndex')}}"
34
+ change:loop="{{computed.updateValue(instanceId,'loop')}}"
35
+ change:animationtime="{{computed.updateValue(instanceId,'animationTime')}}"
36
+ change:windowed="{{computed.updateValue(instanceId,'windowed')}}"
37
+ change:instanceid="{{computed.updateValue(instanceId,'instanceId')}}"
36
38
  bind:touchstart="{{computed.touchStart(instanceId)}}"
37
39
  catch:touchmove="{{computed.touchMove(instanceId)}}"
38
40
  bind:touchend="{{computed.touchEnd(instanceId)}}"
@@ -226,7 +226,8 @@ function getDomState(ownerInstance) {
226
226
 
227
227
  function initDomState(instanceId, ownerInstance) {
228
228
  var state = getDomState(ownerInstance);
229
- updateState(instanceId, 'options', state.options)
229
+ // options 不再通过 data-options 传入(避免把全量数组塞进 dataset / getDataset),
230
+ // 由 change:options -> updateValue('options', ...) 单独维护 state.options
230
231
  updateState(instanceId, 'valueKey', state.valuekey)
231
232
  updateState(instanceId, 'itemHeight', state.itemheight)
232
233
  updateState(instanceId, 'visibleItemCount', state.visibleitemcount)
@@ -234,6 +235,7 @@ function initDomState(instanceId, ownerInstance) {
234
235
  updateState(instanceId, 'loop', state.loop)
235
236
  updateState(instanceId, 'animationTime', state.animationtime)
236
237
  updateState(instanceId, 'changeAnimation', state.changeanimation)
238
+ updateState(instanceId, 'windowed', state.windowed)
237
239
  }
238
240
 
239
241
 
@@ -262,10 +264,14 @@ function updateListView(instanceId, ownerInstance) {
262
264
  var animationIndex = state.animationIndex || 0;
263
265
  var newAnimationIndex = getNewAnimationIndex(animationIndex, activeIndex, length, state.loop);
264
266
  var isSame = newAnimationIndex === animationIndex;
267
+ // 窗口化列的「停稳回中/重定位」会产生大幅度的 activeIndex 跳变,此时强制关闭过渡:
268
+ // 因为切片内容与位置是同步平移的,直接吸附即像素级一致,避免整轮旋转动画造成抖动。
269
+ var vOptionLength = state.visibleItemCount * 4 - 2;
270
+ var isWindowReposition = state.windowed && Math.abs(newAnimationIndex - animationIndex) > vOptionLength;
265
271
  updateState(instanceId, 'animationIndex', newAnimationIndex)
266
272
  updateVisibleOptions(state.animationIndex, instanceId);
267
273
  updateWrapperStyle(instanceId, ownerInstance, {
268
- transition: (!state.changeAnimation || isSame) ? 'none' : 'transform ' + state.animationTime + 'ms cubic-bezier(0.2, 0.9, 0.25, 1)'
274
+ transition: (!state.changeAnimation || isSame || isWindowReposition) ? 'none' : 'transform ' + state.animationTime + 'ms cubic-bezier(0.2, 0.9, 0.25, 1)'
269
275
  });
270
276
  }
271
277
 
@@ -373,7 +379,8 @@ function touchEnd(instanceId) {
373
379
 
374
380
  // 惯性滚动参数配置
375
381
  var minVelocity = 0.1; // 最小速度阈值,低于此值停止滚动
376
- var maxInertiaDistance = state.itemHeight * Math.max(Math.floor(state.options.length/4), 6); // 最大惯性滚动距离
382
+ // 最大惯性滚动距离:下限 6 项,上限 100 项,避免超长列表单次快滑滚动上万项
383
+ var maxInertiaDistance = state.itemHeight * Math.min(Math.max(Math.floor(state.options.length/4), 6), 100);
377
384
  // 计算惯性滚动距离
378
385
  var inertiaDistance = 0;
379
386
  if (recentVelocity > minVelocity) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tuya-miniapp/smart-ui",
3
- "version": "2.13.4",
3
+ "version": "2.13.5-beta-0",
4
4
  "author": "MiniApp Team",
5
5
  "license": "MIT",
6
6
  "miniprogram": "lib",