USEGEAR

导航

学习unigui【42】UnimDatePicker的使用

场景:UnimDatePicker需要:

1 进入 Picker:默认日期会按控件当前值显示(2026-01-092 年=2026(当年)时:月槽只剩 1..当月
3 且月=当月时:日槽只剩 1..当日(如果你要 1..昨天:把 limitToYesterday=true4 选 2025:月槽恢复 1..12,日槽恢复该月正常天数
5 再选回 2026:再次自动裁剪月/日(动态链式)

实现代码,加入UnimDatePicker.uniEnents的afterCreate

  1 function afterCreate(sender) {
  2   var field = sender;
  3   if (field.__smartPickerInstalled) return;
  4   field.__smartPickerInstalled = true;
  5 
  6   // true=只能选到昨天;false=可选到今天
  7   var EXCLUDE_TODAY = false;
  8 
  9   function pad2(n){ return (n < 10 ? '0' : '') + n; }
 10   function todayParts(){
 11     var n = new Date();
 12     return { y:n.getFullYear(), m:n.getMonth()+1, d:n.getDate() };
 13   }
 14   function daysInMonth(y, m1){ return new Date(y, m1, 0).getDate(); }
 15 
 16   function buildYears(yMin, yMax){
 17     var a=[];
 18     for(var y=yMin; y<=yMax; y++) a.push({ text: y + ' 年', value: y });
 19     return a;
 20   }
 21   function buildMonths(maxM){
 22     var a=[];
 23     for(var m=1; m<=maxM; m++) a.push({ text: pad2(m) + ' 月', value: m });
 24     return a;
 25   }
 26 
 27   function valOf(x){
 28     if(x === null || x === undefined) return null;
 29     if(typeof x === 'number') return isFinite(x) ? parseInt(x,10) : null;
 30     if(typeof x === 'string'){
 31       var n = parseInt(x, 10);
 32       return isNaN(n) ? null : n;
 33     }
 34     if(typeof x === 'object'){
 35       if(typeof x.value !== 'undefined') return valOf(x.value);
 36       if(x.data && typeof x.data.value !== 'undefined') return valOf(x.data.value);
 37       if(typeof x.get === 'function'){
 38         var v = x.get('value');
 39         if(typeof v !== 'undefined') return valOf(v);
 40       }
 41     }
 42     return null;
 43   }
 44 
 45   function parseFieldDate(){
 46     var v = field.getValue ? field.getValue() : null;
 47     if(v instanceof Date) return v;
 48 
 49     if(typeof v === 'string' && v){
 50       if(window.Ext && Ext.Date && Ext.Date.parse){
 51         var d = Ext.Date.parse(v, 'Y-m-d');
 52         if(d) return d;
 53       }
 54       var d2 = new Date(v);
 55       if(!isNaN(+d2)) return d2;
 56     }
 57     return new Date();
 58   }
 59 
 60   // -------- slot helpers (原生 picker) --------
 61   function getSlotByName(pkr, name){
 62     try{
 63       var slots = pkr.getSlots ? pkr.getSlots() : null;
 64       if(slots){
 65         var arr = slots.items || slots;
 66         for(var i=0; i<arr.length; i++){
 67           var s = arr[i];
 68           if(!s) continue;
 69           if(s.getName && s.getName() === name) return s;
 70           if(s._name === name) return s;
 71           if(s.config && s.config.name === name) return s;
 72         }
 73       }
 74     }catch(e){}
 75     var items = (pkr.getItems && pkr.getItems().items) || [];
 76     for(var j=0; j<items.length; j++){
 77       var it = items[j];
 78       if(it && it.isSlot && it.getName && it.getName() === name) return it;
 79     }
 80     return null;
 81   }
 82 
 83   // ✅ 年/月:继续用你现在的“换 store”
 84   function resetSlotStore(slot, data){
 85     if(!slot) return;
 86 
 87     try{
 88       if(slot.setItemTpl) slot.setItemTpl('{text}');
 89       if(slot.setDisplayField) slot.setDisplayField('text');
 90       if(slot.setValueField)   slot.setValueField('value');
 91     }catch(e){}
 92 
 93     try{
 94       // 优先用 setData(某些版本 setStore 不稳定)
 95       if(slot.setData){
 96         slot.setData(data);
 97         return;
 98       }
 99     }catch(e){}
100 
101     var stNew = null;
102     try{
103       stNew = Ext.create('Ext.data.Store', { fields: ['text','value'], data: data });
104     }catch(e){
105       stNew = null;
106     }
107 
108     try{
109       if(stNew && slot.setStore){
110         slot.setStore(stNew);
111       } else {
112         var st = slot.getStore && slot.getStore();
113         if(st){
114           if(st.removeAll) st.removeAll(true);
115           if(st.loadData) st.loadData(data, false);
116         }
117       }
118     }catch(e){}
119 
120     try{ if(slot.refresh) slot.refresh(); }catch(e){}
121     try{ if(slot.updateLayout) slot.updateLayout(); }catch(e){}
122   }
123 
124   function setSlotValue(slot, val){
125     if(!slot) return;
126     if(slot.setValue) slot.setValue(val);
127   }
128 
129   function getPickerValueSafe(pkr){
130     var ys = getSlotByName(pkr,'year');
131     var ms = getSlotByName(pkr,'month');
132     var ds = getSlotByName(pkr,'day');
133 
134     var y = valOf(ys && ys.getValue ? ys.getValue() : null);
135     var m = valOf(ms && ms.getValue ? ms.getValue() : null);
136     var d = valOf(ds && ds.getValue ? ds.getValue() : null);
137 
138     if(!y || !m || !d){
139       var v = (pkr.getValue && pkr.getValue()) || {};
140       y = y || valOf(v.year);
141       m = m || valOf(v.month);
142       d = d || valOf(v.day);
143     }
144     return { year:y, month:m, day:d };
145   }
146 
147   // 年范围读取:field + initialConfig + picker.yearFrom/yearTo
148   function getYearRange(pkr){
149     var yMin = null, yMax = null;
150 
151     yMin = valOf(field.minYear) || yMin;
152     yMax = valOf(field.maxYear) || yMax;
153     yMin = yMin !== null ? yMin : valOf(field.MinYear);
154     yMax = yMax !== null ? yMax : valOf(field.MaxYear);
155 
156     var ic = field.initialConfig || null;
157     if(ic){
158       yMin = yMin !== null ? yMin : valOf(ic.minYear);
159       yMax = yMax !== null ? yMax : valOf(ic.maxYear);
160       yMin = yMin !== null ? yMin : valOf(ic.MinYear);
161       yMax = yMax !== null ? yMax : valOf(ic.MaxYear);
162     }
163 
164     if(pkr){
165       yMin = yMin !== null ? yMin : valOf(pkr.yearFrom);
166       yMax = yMax !== null ? yMax : valOf(pkr.yearTo);
167       yMin = yMin !== null ? yMin : (pkr.getYearFrom ? valOf(pkr.getYearFrom()) : null);
168       yMax = yMax !== null ? yMax : (pkr.getYearTo ? valOf(pkr.getYearTo()) : null);
169     }
170 
171     // 兜底:如果你看到 2024,说明上面没取到 Min/MaxYear,落到这里了
172     var yNow = (new Date()).getFullYear();
173     if(yMin === null) yMin = yNow - 2;
174     if(yMax === null) yMax = yNow;
175 
176     if(yMin > yMax){ var t=yMin; yMin=yMax; yMax=t; }
177     return { yMin:yMin, yMax:yMax };
178   }
179 
180   // ====== ✅ 日列专用修复:模板 + 过滤 ======
181   function ensureDayTpl(daySlot){
182     if(!daySlot || daySlot.__dayTplFixed) return;
183     daySlot.__dayTplFixed = true;
184 
185     try{
186       if(daySlot.setDisplayField) daySlot.setDisplayField('value');
187       if(daySlot.setValueField)   daySlot.setValueField('value');
188 
189       // 无论 record 有没有 text,都按 value 渲染成 “XX 日”
190       if(window.Ext && Ext.XTemplate && daySlot.setItemTpl){
191         daySlot.setItemTpl(new Ext.XTemplate(
192           '{[Ext.String.leftPad(values.value,2,"0")]} 日'
193         ));
194       }else if(daySlot.setItemTpl){
195         daySlot.setItemTpl('{value} 日');
196       }
197     }catch(e){}
198   }
199 
200   function filterDayRange(daySlot, maxDay){
201     if(!daySlot) return;
202     var st = daySlot.getStore && daySlot.getStore();
203     if(!st) return;
204 
205     try{
206       if(st.clearFilter) st.clearFilter();
207       if(st.filterBy){
208         st.filterBy(function(rec){
209           var v = null;
210           try{ v = rec.get ? rec.get('value') : (rec.data ? rec.data.value : null); }catch(e){}
211           v = parseInt(v, 10);
212           if(isNaN(v)) return false;
213           return v >= 1 && v <= maxDay;
214         });
215       }
216     }catch(e){}
217 
218     try{ if(daySlot.refresh) daySlot.refresh(); }catch(e){}
219     try{ if(daySlot.updateLayout) daySlot.updateLayout(); }catch(e){}
220   }
221 
222   // 业务逻辑:当年限月,当月限日,排除今天
223   function rebuildByValue(pkr, value){
224     var yr = getYearRange(pkr);
225     var yMin = yr.yMin, yMax = yr.yMax;
226 
227     var now = todayParts();
228     var yNow = now.y, mNow = now.m, dNow = now.d;
229     if(EXCLUDE_TODAY) dNow = Math.max(1, dNow - 1);
230 
231     var y = valOf(value.year);
232     var m = valOf(value.month);
233     var d = valOf(value.day);
234 
235     if(!y || !m || !d){
236       var d0 = parseFieldDate();
237       y = d0.getFullYear(); m = d0.getMonth()+1; d = d0.getDate();
238     }
239 
240     if(y < yMin) y = yMin;
241     if(y > yMax) y = yMax;
242 
243     var maxMonth = (y === yNow) ? mNow : 12;
244     if(m < 1) m = 1;
245     if(m > maxMonth) m = maxMonth;
246 
247     var maxDay = daysInMonth(y, m);
248     if(y === yNow && m === mNow) maxDay = Math.min(maxDay, dNow);
249     if(d < 1) d = 1;
250     if(d > maxDay) d = maxDay;
251 
252     // 年/月照旧
253     resetSlotStore(getSlotByName(pkr,'year'),  buildYears(yMin, yMax));
254     resetSlotStore(getSlotByName(pkr,'month'), buildMonths(maxMonth));
255 
256     // ✅ 日:不再“换 store”,直接模板+过滤(能真正卡住今天/昨天)
257     var daySlot = getSlotByName(pkr,'day');
258     ensureDayTpl(daySlot);
259     filterDayRange(daySlot, maxDay);
260 
261     Ext.defer(function(){
262       setSlotValue(getSlotByName(pkr,'year'),  y);
263       setSlotValue(getSlotByName(pkr,'month'), m);
264       setSlotValue(daySlot, d);
265     }, 0);
266   }
267 
268   function patchNativePicker(){
269     var pkr = field.getPicker && field.getPicker();
270     if(!pkr) return;
271     if(pkr.__smartPatched) return;
272     pkr.__smartPatched = true;
273 
274     try{
275       if(pkr.getDoneButton)   pkr.getDoneButton().setText('确定');
276       if(pkr.getCancelButton) pkr.getCancelButton().setText('取消');
277     }catch(e){}
278 
279     pkr.on('show', function(){
280       var d0 = parseFieldDate();
281       var v0 = { year:d0.getFullYear(), month:d0.getMonth()+1, day:d0.getDate() };
282 
283       try{
284         pkr.__updating = true;
285         if(pkr.setValue) pkr.setValue(v0);
286       } finally {
287         pkr.__updating = false;
288       }
289 
290       try{
291         pkr.__updating = true;
292         rebuildByValue(pkr, getPickerValueSafe(pkr));
293       } finally {
294         pkr.__updating = false;
295       }
296     });
297 
298     var onMove = function(){
299       if(pkr.__updating) return;
300       pkr.__updating = true;
301       try{
302         rebuildByValue(pkr, getPickerValueSafe(pkr));
303       } finally {
304         pkr.__updating = false;
305       }
306     };
307     pkr.on('pick', onMove);
308     pkr.on('change', onMove);
309 
310     pkr.on('done', function(){
311       var v = getPickerValueSafe(pkr);
312       if(!v.year || !v.month || !v.day) return;
313 
314       var dt = new Date(v.year, v.month-1, v.day);
315 
316       Ext.defer(function(){
317         if(field.setValue) field.setValue(dt);
318         if(field.setRawValue && window.Ext && Ext.Date){
319           field.setRawValue(Ext.Date.format(dt, 'Y-m-d'));
320         }
321         if(field.fireEvent){
322           field.fireEvent('change', field, dt);
323           field.fireEvent('changevalue', field, dt);
324         }
325       }, 30);
326     });
327   }
328 
329   Ext.defer(patchNativePicker, 0);
330   Ext.defer(patchNativePicker, 200);
331 }

 


 


看效果:

图片

 

实现思路 + 代码讲解(核心抓住 3 点)

思路总览:弃用 UnimDatePicker 内置滚轮,自己接管三列 Picker

你原需求其实是:

  • :由控件自身 min/max 控制(你已经有)

  • 月/日:不是禁用,而是“列表里就不出现非法项”

  • 并且是动态链式:选 year → 立刻重算 month/day;选 month → 立刻重算 day

  • 还要“打开时就按默认值立刻裁剪”

Ext.field.Date(UnimDatePicker)自带的 Ext.picker.Date 内部会反复重建 slots / store,导致你在它内部改 month/day 数据很容易被覆盖。我们之前反复卡在这里,就是这个原因。

所以最终正确姿势是:
不要改它内部实现,而是:

  1. UnimDatePicker1 只当一个“显示输入框”(显示日期字符串)

  2. 点击它时,不弹出原生 picker

  3. 我们自己创建一个 Ext.picker.Picker(三个 slots:year/month/day)

  4. 我们完全控制 month/day 的数据源(store),所以“只能显示合法项”天然成立

  5. pick 事件里实时重算并刷新数据源(动态联动)

  6. done 事件里把最终 Date 写回到 field(这时才真正提交)


代码结构讲解(按模块拆给你)

A. 为什么用 afterCreate

afterCreate 时 field 已经是 Ext 组件实例,能安全挂钩 field.showPicker,还能创建我们的 picker 并缓存到 field 上。

B. 覆盖 showPicker(关键一刀)

 
field.__origShowPicker = field.showPicker; field.showPicker = function(){ // 构造默认值 value // rebuildByValue() 先裁剪列表 // picker.setValue(value) // picker.show() };

这一刀的意义:
用户点日期框时,系统不再弹原生 DatePicker,而是弹我们自建 Picker。

C. 三列 slots(y-m-d)

 
slots: [ { name:'year', data: buildYears(yMin, yMax) }, { name:'month', data: buildMonths(12) }, { name:'day', data: buildDays(31) } ]

slots 的数据就是你看到的“滚轮列表项”。
我们想让某些月份/日期“不出现”,只要把对应 slot 的 store setData 成更短的数组即可。

D. 动态联动:pick 事件里重算范围

 
pick: function(pkr, value){ rebuildByValue(pkr, value, false); }

rebuildByValue() 做的事就三步:

  1. 算 “当前应该允许的 monthMax / dayMax”

  2. monthSlot.setData(1..monthMax),daySlot.setData(1..dayMax)

  3. 如果当前选中的 month/day 超界,夹回合法值,再 setValue

所以你想要的体验“列表里根本没有非法项”,在这里实现。

E. 写回:done 才 setValue 到 field

 
done: function(pkr, value){ field.setValue(new Date(y, m-1, d)); }

这保证了:
滚轮滚动过程只是“预览/选择”,不会把 field 的值乱改;只有点“确定”才提交。

 

posted on 2026-01-09 16:22  USEGEAR  阅读(6)  评论(0)    收藏  举报