学习unigui【42】UnimDatePicker的使用
场景:UnimDatePicker需要:
1 进入 Picker:默认日期会按控件当前值显示(2026-01-09) 2 年=2026(当年)时:月槽只剩 1..当月 3 且月=当月时:日槽只剩 1..当日(如果你要 1..昨天:把 limitToYesterday=true) 4 选 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 数据很容易被覆盖。我们之前反复卡在这里,就是这个原因。
所以最终正确姿势是:
不要改它内部实现,而是:
-
UnimDatePicker1只当一个“显示输入框”(显示日期字符串) -
点击它时,不弹出原生 picker
-
我们自己创建一个
Ext.picker.Picker(三个 slots:year/month/day) -
我们完全控制 month/day 的数据源(store),所以“只能显示合法项”天然成立
-
在
pick事件里实时重算并刷新数据源(动态联动) -
在
done事件里把最终 Date 写回到 field(这时才真正提交)
代码结构讲解(按模块拆给你)
A. 为什么用 afterCreate
afterCreate 时 field 已经是 Ext 组件实例,能安全挂钩 field.showPicker,还能创建我们的 picker 并缓存到 field 上。
B. 覆盖 showPicker(关键一刀)
这一刀的意义:
用户点日期框时,系统不再弹原生 DatePicker,而是弹我们自建 Picker。
C. 三列 slots(y-m-d)
slots 的数据就是你看到的“滚轮列表项”。
我们想让某些月份/日期“不出现”,只要把对应 slot 的 store setData 成更短的数组即可。
D. 动态联动:pick 事件里重算范围
rebuildByValue() 做的事就三步:
-
算 “当前应该允许的 monthMax / dayMax”
-
monthSlot.setData(1..monthMax),daySlot.setData(1..dayMax)
-
如果当前选中的 month/day 超界,夹回合法值,再 setValue
所以你想要的体验“列表里根本没有非法项”,在这里实现。
E. 写回:done 才 setValue 到 field
这保证了:
滚轮滚动过程只是“预览/选择”,不会把 field 的值乱改;只有点“确定”才提交。
浙公网安备 33010602011771号