feat: enhance UI styling, add date picker, and clean up inline CSS
This commit is contained in:
@@ -0,0 +1,291 @@
|
||||
/**
|
||||
* KamiDatePicker — 轻量日历弹窗,kami 暖纸风格。
|
||||
*
|
||||
* 用法:
|
||||
* new KamiDatePicker(triggerEl, {
|
||||
* value: '2025-06-09', // 初始日期
|
||||
* maxDate: '2025-06-09', // 最晚可选
|
||||
* minDate: '2024-01-01', // 最早可选
|
||||
* markedDates: ['2025-06-08'],// 有数据的日期(小圆点)
|
||||
* onChange: function(dateStr) {}, // 选择回调
|
||||
* });
|
||||
*/
|
||||
;(function () {
|
||||
'use strict';
|
||||
|
||||
var INSTANCES = [];
|
||||
|
||||
var WEEKDAYS = ['日', '一', '二', '三', '四', '五', '六'];
|
||||
|
||||
function pad(n) {
|
||||
return n < 10 ? '0' + n : '' + n;
|
||||
}
|
||||
function isoDate(y, m, d) {
|
||||
return y + '-' + pad(m + 1) + '-' + pad(d);
|
||||
}
|
||||
function parseDate(s) {
|
||||
if (!s) return null;
|
||||
var parts = s.split('-');
|
||||
if (parts.length !== 3) return null;
|
||||
return new Date(+parts[0], +parts[1] - 1, +parts[2]);
|
||||
}
|
||||
function sameDay(a, b) {
|
||||
return a && b && a.getFullYear() === b.getFullYear() &&
|
||||
a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
|
||||
}
|
||||
|
||||
function KamiDatePicker(triggerEl, opts) {
|
||||
opts = opts || {};
|
||||
this.trigger = triggerEl;
|
||||
this.value = opts.value || null;
|
||||
this.maxDate = opts.maxDate ? parseDate(opts.maxDate) : null;
|
||||
this.minDate = opts.minDate ? parseDate(opts.minDate) : null;
|
||||
this.markedSet = {};
|
||||
if (opts.markedDates) {
|
||||
for (var i = 0; i < opts.markedDates.length; i++) {
|
||||
this.markedSet[opts.markedDates[i]] = true;
|
||||
}
|
||||
}
|
||||
this.onChange = opts.onChange || function () {};
|
||||
this.opened = false;
|
||||
|
||||
// view state
|
||||
var initial = parseDate(this.value) || new Date();
|
||||
this.viewYear = initial.getFullYear();
|
||||
this.viewMonth = initial.getMonth();
|
||||
|
||||
this._build();
|
||||
this._bind();
|
||||
INSTANCES.push(this);
|
||||
}
|
||||
|
||||
KamiDatePicker.prototype._build = function () {
|
||||
// wrapper
|
||||
this.wrapper = document.createElement('div');
|
||||
this.wrapper.className = 'kami-date-picker-wrapper';
|
||||
this.trigger.parentNode.insertBefore(this.wrapper, this.trigger);
|
||||
this.wrapper.appendChild(this.trigger);
|
||||
|
||||
// popup
|
||||
this.popup = document.createElement('div');
|
||||
this.popup.className = 'kami-date-popup';
|
||||
this.popup.innerHTML = this._renderHTML();
|
||||
this.wrapper.appendChild(this.popup);
|
||||
|
||||
// make trigger look clickable
|
||||
this.trigger.style.cursor = 'pointer';
|
||||
};
|
||||
|
||||
KamiDatePicker.prototype._bind = function () {
|
||||
var self = this;
|
||||
|
||||
// toggle on trigger click
|
||||
this._triggerHandler = function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
self.toggle();
|
||||
};
|
||||
this.trigger.addEventListener('click', this._triggerHandler);
|
||||
|
||||
// popup internal clicks
|
||||
this.popup.addEventListener('click', function (e) {
|
||||
var target = e.target;
|
||||
|
||||
// prev month
|
||||
if (target.closest('.kami-date-prev')) {
|
||||
e.stopPropagation();
|
||||
self.viewMonth--;
|
||||
if (self.viewMonth < 0) { self.viewMonth = 11; self.viewYear--; }
|
||||
self._update();
|
||||
return;
|
||||
}
|
||||
// next month
|
||||
if (target.closest('.kami-date-next')) {
|
||||
e.stopPropagation();
|
||||
self.viewMonth++;
|
||||
if (self.viewMonth > 11) { self.viewMonth = 0; self.viewYear++; }
|
||||
self._update();
|
||||
return;
|
||||
}
|
||||
// today link
|
||||
if (target.closest('.kami-date-today-link')) {
|
||||
e.stopPropagation();
|
||||
self.select(new Date());
|
||||
return;
|
||||
}
|
||||
// day cell
|
||||
var dayEl = target.closest('.kami-date-day');
|
||||
if (dayEl && !dayEl.classList.contains('muted')) {
|
||||
e.stopPropagation();
|
||||
var y = +dayEl.dataset.year;
|
||||
var m = +dayEl.dataset.month;
|
||||
var d = +dayEl.dataset.day;
|
||||
self.select(new Date(y, m, d));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
KamiDatePicker.prototype._renderHTML = function () {
|
||||
var y = this.viewYear;
|
||||
var m = this.viewMonth;
|
||||
var today = new Date();
|
||||
var selected = parseDate(this.value);
|
||||
|
||||
var html = '';
|
||||
|
||||
// header
|
||||
html += '<div class="kami-date-header">';
|
||||
html += '<button type="button" class="kami-date-prev" title="上个月">‹</button>';
|
||||
html += '<span class="kami-date-label">' + y + ' 年 ' + (m + 1) + ' 月</span>';
|
||||
html += '<button type="button" class="kami-date-next" title="下个月">›</button>';
|
||||
html += '</div>';
|
||||
|
||||
// weekdays
|
||||
html += '<div class="kami-date-weekdays">';
|
||||
for (var w = 0; w < 7; w++) {
|
||||
html += '<span>' + WEEKDAYS[w] + '</span>';
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
// day grid
|
||||
var firstDay = new Date(y, m, 1).getDay(); // 0=Sun
|
||||
var daysInMonth = new Date(y, m + 1, 0).getDate();
|
||||
var daysInPrevMonth = new Date(y, m, 0).getDate();
|
||||
|
||||
html += '<div class="kami-date-grid">';
|
||||
|
||||
// prev month trailing days
|
||||
for (var p = firstDay - 1; p >= 0; p--) {
|
||||
var pd = daysInPrevMonth - p;
|
||||
var pm = m - 1;
|
||||
var py = y;
|
||||
if (pm < 0) { pm = 11; py--; }
|
||||
html += '<div class="kami-date-day muted" data-year="' + py + '" data-month="' + pm + '" data-day="' + pd + '">' + pd + '</div>';
|
||||
}
|
||||
|
||||
// current month days
|
||||
for (var d = 1; d <= daysInMonth; d++) {
|
||||
var date = new Date(y, m, d);
|
||||
var iso = isoDate(y, m, d);
|
||||
var cls = 'kami-date-day';
|
||||
if (sameDay(date, today)) cls += ' today';
|
||||
if (sameDay(date, selected)) cls += ' selected';
|
||||
if (this.markedSet[iso]) cls += ' marked';
|
||||
if (this.maxDate && date > this.maxDate) cls += ' disabled';
|
||||
if (this.minDate && date < this.minDate) cls += ' disabled';
|
||||
html += '<div class="' + cls + '" data-year="' + y + '" data-month="' + m + '" data-day="' + d + '">' + d + '</div>';
|
||||
}
|
||||
|
||||
// next month leading days
|
||||
var totalCells = firstDay + daysInMonth;
|
||||
var remaining = (7 - totalCells % 7) % 7;
|
||||
for (var n = 1; n <= remaining; n++) {
|
||||
var nm = m + 1;
|
||||
var ny = y;
|
||||
if (nm > 11) { nm = 0; ny++; }
|
||||
html += '<div class="kami-date-day muted" data-year="' + ny + '" data-month="' + nm + '" data-day="' + n + '">' + n + '</div>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
|
||||
// footer — "回到今天"
|
||||
html += '<div class="kami-date-footer">';
|
||||
html += '<a href="javascript:void(0)" class="kami-date-today-link">回到今天</a>';
|
||||
html += '</div>';
|
||||
|
||||
return html;
|
||||
};
|
||||
|
||||
KamiDatePicker.prototype._update = function () {
|
||||
this.popup.innerHTML = this._renderHTML();
|
||||
};
|
||||
|
||||
KamiDatePicker.prototype.open = function () {
|
||||
// close other instances
|
||||
for (var i = 0; i < INSTANCES.length; i++) {
|
||||
if (INSTANCES[i] !== this && INSTANCES[i].opened) {
|
||||
INSTANCES[i].close();
|
||||
}
|
||||
}
|
||||
|
||||
// sync view to current value
|
||||
var sel = parseDate(this.value);
|
||||
if (sel) {
|
||||
this.viewYear = sel.getFullYear();
|
||||
this.viewMonth = sel.getMonth();
|
||||
}
|
||||
this._update();
|
||||
this.popup.classList.add('open');
|
||||
this.opened = true;
|
||||
this._addGlobalListeners();
|
||||
};
|
||||
|
||||
KamiDatePicker.prototype.close = function () {
|
||||
this.popup.classList.remove('open');
|
||||
this.opened = false;
|
||||
this._removeGlobalListeners();
|
||||
};
|
||||
|
||||
KamiDatePicker.prototype.toggle = function () {
|
||||
if (this.opened) this.close();
|
||||
else this.open();
|
||||
};
|
||||
|
||||
KamiDatePicker.prototype.select = function (date) {
|
||||
var iso = isoDate(date.getFullYear(), date.getMonth(), date.getDate());
|
||||
this.value = iso;
|
||||
this.close();
|
||||
this.onChange(iso);
|
||||
};
|
||||
|
||||
KamiDatePicker.prototype.setValue = function (dateStr) {
|
||||
this.value = dateStr || null;
|
||||
var d = parseDate(dateStr);
|
||||
if (d) {
|
||||
this.viewYear = d.getFullYear();
|
||||
this.viewMonth = d.getMonth();
|
||||
}
|
||||
if (this.opened) this._update();
|
||||
};
|
||||
|
||||
KamiDatePicker.prototype.getValue = function () {
|
||||
return this.value;
|
||||
};
|
||||
|
||||
KamiDatePicker.prototype.destroy = function () {
|
||||
this.close();
|
||||
this.trigger.removeEventListener('click', this._triggerHandler);
|
||||
// move trigger back out of wrapper
|
||||
this.wrapper.parentNode.insertBefore(this.trigger, this.wrapper);
|
||||
this.wrapper.remove();
|
||||
var idx = INSTANCES.indexOf(this);
|
||||
if (idx > -1) INSTANCES.splice(idx, 1);
|
||||
};
|
||||
|
||||
// ── Global listeners ──
|
||||
|
||||
KamiDatePicker.prototype._addGlobalListeners = function () {
|
||||
var self = this;
|
||||
if (!this._docClickHandler) {
|
||||
this._docClickHandler = function (e) {
|
||||
if (self.wrapper.contains(e.target)) return;
|
||||
self.close();
|
||||
};
|
||||
}
|
||||
if (!this._keyHandler) {
|
||||
this._keyHandler = function (e) {
|
||||
if (e.key === 'Escape') self.close();
|
||||
};
|
||||
}
|
||||
document.addEventListener('click', this._docClickHandler);
|
||||
document.addEventListener('keydown', this._keyHandler);
|
||||
};
|
||||
|
||||
KamiDatePicker.prototype._removeGlobalListeners = function () {
|
||||
if (this._docClickHandler) document.removeEventListener('click', this._docClickHandler);
|
||||
if (this._keyHandler) document.removeEventListener('keydown', this._keyHandler);
|
||||
};
|
||||
|
||||
// expose
|
||||
window.KamiDatePicker = KamiDatePicker;
|
||||
})();
|
||||
Reference in New Issue
Block a user