AntDesign老版本没有月份和年份区间选择组件?我封装了一个完美版,附源码!

这两天帮公司运维一个老的OA系统,说是老的但也不算很老,大概是23年前后的项目吧。

项目背景

整个项目技术栈还算比较主流,前端部分用的是老几样:

vue2 + vue-router + vuex + vue-cli + ant-design-vue@1.7.8

后端部分也是老几样:

SpringBoot + Radis + ‌MyBatis + hangao

这个项目升级根本上不上来,之前领导还说过要不要升AntDesign的高版本,因为高版本有换肤功能。

我直截了当的告诉他:升不了!

AntDesign在Vue2中最后一个版本就是 @1.7.8,还问我工作量大不大。

我告诉他:比天还大!

有兄弟说可以用AI做升级,但是非常抱歉,内网离线环境开发,没有AI。

而且公司也没有资源部署本地AI,所以纯古法编程,难搞的很。

月份区间选择

现在需求这边提出来需要一个月份区间选择 和年份区间选择。

但是用过的人都知道,AngDesignVue的Vue2版本只有日期区间选择,也就是a-range-picker组件。

于是乎我自己封装了一下,下面是月份区间选择组件的源码:

html 复制代码
<template>
  <a-range-picker
    ref="picker"
    class="adv-month-range-picker"
    :mode="panelMode"
    :value="innerValue"
    :open="sOpen"
    :format="format"
    :placeholder="placeholder"
    :separator="separator"
    :disabled="disabled"
    :allow-clear="allowClear"
    :size="size"
    :disabled-date="disabledDate"
    :get-calendar-container="getCalendarContainer"
    :popup-style="popupStyle"
    :show-today="showToday"
    :input-read-only="inputReadOnly"
    @openChange="handleOpenChange"
    @panelChange="handlePanelChange"
    @calendarChange="handleCalendarChange"
    @change="handleChange"
  />
</template>

<script>
import moment from 'moment';

const PANEL_MODE = ['month', 'month']; // 必须传数组,RangeCalendar 内部按 mode[0] / mode[1] 取用
const UNIT = 'month';
const PANEL_CELL_SUFFIX = '-panel-cell';

function resolveClickedSide(node) {
  let cur = node;
  let depth = 0;
  while (cur && depth++ < 24) {
    const cls = typeof cur.className === 'string' ? cur.className : '';
    if (cls) {
      if (/(^|\s)[\w-]*-range-left(\s|$)/.test(cls)) return 'start';
      if (/(^|\s)[\w-]*-range-right(\s|$)/.test(cls)) return 'end';
    }
    cur = cur.parentNode;
  }
  return null;
}

function findUp(node, matches, maxDepth) {
  let cur = node;
  let depth = 0;
  const limit = maxDepth || 24;
  while (cur && depth++ < limit) {
    if (cur.nodeType === 1 && matches(cur)) return cur;
    cur = cur.parentNode;
  }
  return null;
}

function panelCellBaseCls(el) {
  const list = String(el.className || '').split(/\s+/);
  for (let i = 0; i < list.length; i++) {
    const c = list[i];
    if (!c) continue;
    if (/-panel-cell$/.test(c) || /-panel-cell-/.test(c)) {
      return c.slice(0, c.indexOf(PANEL_CELL_SUFFIX) + PANEL_CELL_SUFFIX.length);
    }
  }
  return null;
}

function closestPanelCell(node) {
  return findUp(node, (el) => !!panelCellBaseCls(el), 12);
}

function panelCellDisabledCls(cell) {
  const base = panelCellBaseCls(cell);
  return base ? base + '-disabled' : null;
}

function panelCellMoment(cell) {
  const base = panelCellBaseCls(cell);
  if (!base) return null;

  if (base.indexOf('-month-panel-cell') > -1) {
    const panel = findUp(cell, (el) => !!el.querySelector('[class*="-month-panel-year-select-content"]'), 8);
    if (!panel) return null;
    const yearEl = panel.querySelector('[class*="-month-panel-year-select-content"]');
    const year = yearEl ? parseInt(yearEl.textContent, 10) : NaN;
    const table = findUp(cell, (el) => el.tagName === 'TABLE', 6);
    const cells = table ? table.querySelectorAll('[class*="-month-panel-cell"]') : [];
    const index = Array.prototype.indexOf.call(cells, cell);
    if (isNaN(year) || index < 0) return null;
    return moment([year, index, 1]);
  }

  if (base.indexOf('-year-panel-cell') > -1) {
    const year = parseInt(cell.getAttribute('title') || cell.textContent, 10);
    return isNaN(year) ? null : moment([year, 0, 1]);
  }

  return null;
}

export default {
  name: 'MonthRangePicker',
  model: { prop: 'value', event: 'change' },
  props: {
    /** 区间值,['2026-01','2026-12'];valueType=moment 时传 [moment, moment] */
    value: { type: Array, default: () => [] },
    /** 值类型:'string'(默认,便于直接塞进接口参数)| 'moment' */
    valueType: { type: String, default: 'string' },
    /** 展示与字符串格式 */
    format: { type: String, default: 'YYYY-MM' },
    placeholder: { type: Array, default: () => ['开始月份', '结束月份'] },
    separator: { type: String, default: '~' },
    disabled: { type: Boolean, default: false },
    allowClear: { type: Boolean, default: true },
    size: { type: String, default: 'default' },
    showToday: { type: Boolean, default: false },
    inputReadOnly: { type: Boolean, default: true },
    disabledDate: { type: Function, default: null },
    getCalendarContainer: { type: Function, default: null },
    popupStyle: { type: Object, default: () => ({}) },
    open: { type: Boolean, default: undefined },
    defaultPickerValue: { type: Array, default: () => [] },
  },
  data() {
    return {
      sOpen: false,
      pickedStart: null, // 已确认/待确认的开始
      pickedEnd: null, // 已确认的结束;为 null 表示"只选了开始"
      anchor: null, // 上一次面板显示的 [左, 右],用于兜底判断点击发生在哪一侧
      clickedSide: null, // 'start' | 'end' | null(本次点击落在哪个面板)
    };
  },
  computed: {
    panelMode() {
      return PANEL_MODE;
    },
    innerValue() {
      if (!this.pickedStart) return [];
      return this.pickedEnd ? [this.pickedStart, this.pickedEnd] : [this.pickedStart, undefined];
    },
  },
  watch: {
    value: {
      immediate: true,
      handler() {
        this.syncFromProp();
      },
    },
    open(val) {
      if (val !== undefined && val !== this.sOpen) this.sOpen = !!val;
    },
    disabledDate() {
      if (this.sOpen) this.decoratePanel();
    },
  },
  mounted() {
    this.__captureClick = (e) => {
      this.clickedSide = resolveClickedSide(e.target);
    };
    document.addEventListener('click', this.__captureClick, true);
    this.__blockDisabledClick = (e) => {
      if (!this.disabledDate || !this.sOpen) return;
      const cell = closestPanelCell(e.target);
      if (!cell || !this.isDisabledCell(cell)) return;
      e.preventDefault();
      e.stopPropagation();
    };
    document.addEventListener('click', this.__blockDisabledClick, true);
    this.__paintOnHover = (e) => {
      if (!this.disabledDate || !this.sOpen) return;
      this.paintCell(closestPanelCell(e.target));
    };
    document.addEventListener('mouseover', this.__paintOnHover, true);
    if (typeof MutationObserver !== 'undefined') {
      this.__observer = new MutationObserver(() => this.decoratePanel());
      this.__observeTarget = null;
    }
    this.__decorating = false;
  },
  beforeDestroy() {
    document.removeEventListener('click', this.__captureClick, true);
    document.removeEventListener('click', this.__blockDisabledClick, true);
    document.removeEventListener('mouseover', this.__paintOnHover, true);
    this.stopObserve();
  },
  methods: {
    fire(name, ...args) {
      this.$emit(name, ...args);
      const kebab = name.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
      if (kebab !== name) this.$emit(kebab, ...args);
    },
    isDisabledByUser(v) {
      if (!this.disabledDate || !v) return false;
      const m = moment.isMoment(v) ? v.clone() : moment(v);
      if (!m.isValid()) return false;
      try {
        return !!this.disabledDate(m.startOf(UNIT));
      } catch (e) {
        return false;
      }
    },
    isDisabledCell(cell) {
      if (!this.disabledDate || !cell) return false;
      return this.isDisabledByUser(panelCellMoment(cell));
    },
    paintCell(cell) {
      if (!this.disabledDate || !cell) return;
      const disabledCls = panelCellDisabledCls(cell);
      if (!disabledCls) return;
      const mine = cell.getAttribute('data-adv-disabled') === '1';
      if (this.isDisabledCell(cell)) {
        if (!mine) {
          cell.setAttribute('data-adv-disabled', '1');
          cell.classList.add(disabledCls);
        }
      } else if (mine) {
        cell.removeAttribute('data-adv-disabled');
        cell.classList.remove(disabledCls);
      }
    },
    popupNode() {
      const stack = [this.$refs.picker];
      let guard = 0;
      while (stack.length && guard++ < 200) {
        const cur = stack.shift();
        if (!cur) continue;
        if (typeof cur.getPopupDomNode === 'function') {
          let node = null;
          try {
            node = cur.getPopupDomNode();
          } catch (e) {
            node = null;
          }
          if (node && node.nodeType === 1) return node;
        }
        const kids = cur.$children;
        if (kids && kids.length) for (let i = 0; i < kids.length; i++) stack.push(kids[i]);
      }
      return null;
    },
    visiblePopup() {
      const nodes = document.querySelectorAll('[class*="-calendar-picker-container"]');
      for (let i = nodes.length - 1; i >= 0; i--) {
        const rect = nodes[i].getBoundingClientRect();
        if (rect.width > 0 && rect.height > 0) return nodes[i];
      }
      return null;
    },
    decoratePanel() {
      if (!this.disabledDate) return;
      this.$nextTick(() => this.paintPanel(0));
    },
    paintPanel(attempt) {
      if (!this.disabledDate || !this.sOpen || this.__decorating) return;
      const popup = this.popupNode() || this.visiblePopup();
      if (!popup) {
        if (attempt < 5) setTimeout(() => this.paintPanel(attempt + 1), 60);
        return;
      }
      this.__decorating = true;
      try {
        if (this.__observer && this.__observeTarget !== popup) {
          this.__observer.disconnect();
          this.__observer.observe(popup, {
            subtree: true,
            childList: true,
            attributes: true,
            attributeFilter: ['class'],
          });
          this.__observeTarget = popup;
        }
        const cells = popup.querySelectorAll('[class*="-panel-cell"]');
        Array.prototype.forEach.call(cells, (c) => this.paintCell(c));
      } finally {
        this.__decorating = false;
      }
    },
    stopObserve() {
      if (this.__observer) {
        this.__observer.disconnect();
        this.__observeTarget = null;
      }
    },

    parseOne(v) {
      if (!v) return null;
      let m = moment.isMoment(v) ? v.clone() : moment(v, this.format, true);
      if (!m.isValid()) m = moment(v);
      return m.isValid() ? m.startOf(UNIT) : null;
    },
    syncFromProp() {
      const arr = Array.isArray(this.value) ? this.value : [];
      this.pickedStart = this.parseOne(arr[0]);
      this.pickedEnd = this.parseOne(arr[1]);
      this.anchor = this.currentDisplay();
    },
    currentDisplay() {
      if (this.pickedStart) return [this.pickedStart, this.pickedEnd];
      const base = this.parseOne((this.defaultPickerValue || [])[0]) || moment();
      return [base, base.clone().add(1, UNIT)];
    },
    sameUnit(a, b) {
      if (!a && !b) return true;
      if (!a || !b) return false;
      return a.isSame(b, UNIT);
    },
    toOut(m) {
      if (!m) return null;
      return this.valueType === 'moment' ? m.clone() : m.format(this.format);
    },
    emitValue(s, e) {
      if (this.valueType === 'moment') return s || e ? [s, e] : [];
      return s || e ? [this.toOut(s), this.toOut(e)] : [];
    },
    isCellPick(mode) {
      const arr = Array.isArray(mode) ? mode : [mode];
      return arr.some((m) => m !== 'month' && m !== 'year' && m !== 'decade');
    },
    detectClicked(v) {
      const prev = this.anchor;
      if (!prev || !v) return v && (v[0] || v[1]);
      if (!this.sameUnit(v[0], prev[0])) return v[0];
      if (!this.sameUnit(v[1], prev[1])) return v[1];
      return v[0] || v[1];
    },
    handleOpenChange(open) {
      this.sOpen = open;
      if (!open && this.pickedStart && !this.pickedEnd) this.syncFromProp();
      this.anchor = this.currentDisplay();
      if (open) this.decoratePanel();
      else this.stopObserve();
      this.fire('openChange', open);
    },
    handlePanelChange(v, mode) {
      this.fire('panelChange', v ? [v[0], v[1]] : v, mode);
      this.decoratePanel(); // 切面板 / 翻页后重新上色
      if (!this.isCellPick(mode)) return;

      const side = this.clickedSide;
      const clicked = side === 'start' ? v[0] : side === 'end' ? v[1] : this.detectClicked(v);
      this.clickedSide = null;
      if (!clicked) return;
      if (this.isDisabledByUser(clicked)) return;

      const start = this.pickedStart;
      const end = this.pickedEnd;
      if (!start || end) {
        this.pickedStart = clicked;
        this.pickedEnd = null;
        this.anchor = v;
        return;
      }
      const s = clicked.isAfter(start, UNIT) ? start : clicked;
      const e = clicked.isAfter(start, UNIT) ? clicked : start;
      this.pickedStart = s;
      this.pickedEnd = e;
      this.anchor = [s, e];
      this.sOpen = false;
      this.$emit('change', this.emitValue(s, e), [s.format(this.format), e.format(this.format)]);
    },
    handleCalendarChange(v) {
      this.fire('calendarChange', v ? [v[0], v[1]] : v);
    },
    handleChange(v) {
      const s = v && v[0];
      const e = v && v[1];
      if (s && e) {
        if (this.sameUnit(s, this.pickedStart) && this.sameUnit(e, this.pickedEnd)) return; // 去重
        this.pickedStart = s;
        this.pickedEnd = e;
        this.anchor = [s, e];
        this.sOpen = false;
        this.$emit('change', this.emitValue(s, e), [s.format(this.format), e.format(this.format)]);
        return;
      }
      this.pickedStart = null;
      this.pickedEnd = null;
      this.anchor = this.currentDisplay();
      this.$emit('change', [], []);
    },
    focus() {
      const p = this.$refs.picker;
      if (p && p.focus) p.focus();
    },
    blur() {
      const p = this.$refs.picker;
      if (p && p.blur) p.blur();
    },
    clear() {
      this.pickedStart = null;
      this.pickedEnd = null;
      this.sOpen = false;
      this.anchor = this.currentDisplay();
      this.$emit('change', [], []);
    },
  },
};
</script>

年份区间选择

html 复制代码
<template>
  <a-range-picker
    ref="picker"
    class="adv-year-range-picker"
    :mode="panelMode"
    :value="innerValue"
    :open="sOpen"
    :format="format"
    :placeholder="placeholder"
    :separator="separator"
    :disabled="disabled"
    :allow-clear="allowClear"
    :size="size"
    :disabled-date="disabledDate"
    :get-calendar-container="getCalendarContainer"
    :popup-style="popupStyle"
    :show-today="showToday"
    :input-read-only="inputReadOnly"
    @openChange="handleOpenChange"
    @panelChange="handlePanelChange"
    @calendarChange="handleCalendarChange"
    @change="handleChange"
  />
</template>

<script>
import moment from 'moment';

const PANEL_MODE = ['year', 'year']; // 必须传数组
const UNIT = 'year';
const PANEL_CELL_SUFFIX = '-panel-cell';

function resolveClickedSide(node) {
  let cur = node;
  let depth = 0;
  while (cur && depth++ < 24) {
    const cls = typeof cur.className === 'string' ? cur.className : '';
    if (cls) {
      if (/(^|\s)[\w-]*-range-left(\s|$)/.test(cls)) return 'start';
      if (/(^|\s)[\w-]*-range-right(\s|$)/.test(cls)) return 'end';
    }
    cur = cur.parentNode;
  }
  return null;
}

function findUp(node, matches, maxDepth) {
  let cur = node;
  let depth = 0;
  const limit = maxDepth || 24;
  while (cur && depth++ < limit) {
    if (cur.nodeType === 1 && matches(cur)) return cur;
    cur = cur.parentNode;
  }
  return null;
}

function panelCellBaseCls(el) {
  const list = String(el.className || '').split(/\s+/);
  for (let i = 0; i < list.length; i++) {
    const c = list[i];
    if (!c) continue;
    if (/-panel-cell$/.test(c) || /-panel-cell-/.test(c)) {
      return c.slice(0, c.indexOf(PANEL_CELL_SUFFIX) + PANEL_CELL_SUFFIX.length);
    }
  }
  return null;
}

function closestPanelCell(node) {
  return findUp(node, (el) => !!panelCellBaseCls(el), 12);
}

function panelCellDisabledCls(cell) {
  const base = panelCellBaseCls(cell);
  return base ? base + '-disabled' : null;
}

function panelCellMoment(cell) {
  const base = panelCellBaseCls(cell);
  if (!base) return null;

  if (base.indexOf('-year-panel-cell') > -1) {
    const year = parseInt(cell.getAttribute('title') || cell.textContent, 10);
    return isNaN(year) ? null : moment([year, 0, 1]);
  }

  if (base.indexOf('-month-panel-cell') > -1) {
    const panel = findUp(cell, (el) => !!el.querySelector('[class*="-month-panel-year-select-content"]'), 8);
    if (!panel) return null;
    const yearEl = panel.querySelector('[class*="-month-panel-year-select-content"]');
    const year = yearEl ? parseInt(yearEl.textContent, 10) : NaN;
    const table = findUp(cell, (el) => el.tagName === 'TABLE', 6);
    const cells = table ? table.querySelectorAll('[class*="-month-panel-cell"]') : [];
    const index = Array.prototype.indexOf.call(cells, cell);
    if (isNaN(year) || index < 0) return null;
    return moment([year, index, 1]);
  }

  return null;
}

export default {
  name: 'YearRangePicker',
  model: { prop: 'value', event: 'change' },
  props: {
    value: { type: Array, default: () => [] },
    valueType: { type: String, default: 'string' },
    format: { type: String, default: 'YYYY' },
    placeholder: { type: Array, default: () => ['开始年份', '结束年份'] },
    separator: { type: String, default: '~' },
    disabled: { type: Boolean, default: false },
    allowClear: { type: Boolean, default: true },
    size: { type: String, default: 'default' },
    showToday: { type: Boolean, default: false },
    inputReadOnly: { type: Boolean, default: true },
    disabledDate: { type: Function, default: null },
    getCalendarContainer: { type: Function, default: null },
    popupStyle: { type: Object, default: () => ({}) },
    open: { type: Boolean, default: undefined },
    defaultPickerValue: { type: Array, default: () => [] },
  },
  data() {
    return {
      sOpen: false,
      pickedStart: null,
      pickedEnd: null,
      anchor: null,
      clickedSide: null,
    };
  },
  computed: {
    panelMode() {
      return PANEL_MODE;
    },
    innerValue() {
      if (!this.pickedStart) return [];
      return this.pickedEnd ? [this.pickedStart, this.pickedEnd] : [this.pickedStart, undefined];
    },
  },
  watch: {
    value: {
      immediate: true,
      handler() {
        this.syncFromProp();
      },
    },
    open(val) {
      if (val !== undefined && val !== this.sOpen) this.sOpen = !!val;
    },
    disabledDate() {
      if (this.sOpen) this.decoratePanel();
    },
  },
  mounted() {
    this.__captureClick = (e) => {
      this.clickedSide = resolveClickedSide(e.target);
    };
    document.addEventListener('click', this.__captureClick, true);
    this.__blockDisabledClick = (e) => {
      if (!this.disabledDate || !this.sOpen) return;
      const cell = closestPanelCell(e.target);
      if (!cell || !this.isDisabledCell(cell)) return;
      e.preventDefault();
      e.stopPropagation();
    };
    document.addEventListener('click', this.__blockDisabledClick, true);
    this.__paintOnHover = (e) => {
      if (!this.disabledDate || !this.sOpen) return;
      this.paintCell(closestPanelCell(e.target));
    };
    document.addEventListener('mouseover', this.__paintOnHover, true);
    if (typeof MutationObserver !== 'undefined') {
      this.__observer = new MutationObserver(() => this.decoratePanel());
      this.__observeTarget = null;
    }
    this.__decorating = false;
  },
  beforeDestroy() {
    document.removeEventListener('click', this.__captureClick, true);
    document.removeEventListener('click', this.__blockDisabledClick, true);
    document.removeEventListener('mouseover', this.__paintOnHover, true);
    this.stopObserve();
  },
  methods: {
    fire(name, ...args) {
      this.$emit(name, ...args);
      const kebab = name.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
      if (kebab !== name) this.$emit(kebab, ...args);
    },
    isDisabledByUser(v) {
      if (!this.disabledDate || !v) return false;
      const m = moment.isMoment(v) ? v.clone() : moment(v);
      if (!m.isValid()) return false;
      try {
        return !!this.disabledDate(m.startOf(UNIT));
      } catch (e) {
        return false;
      }
    },
    isDisabledCell(cell) {
      if (!this.disabledDate || !cell) return false;
      return this.isDisabledByUser(panelCellMoment(cell));
    },
    paintCell(cell) {
      if (!this.disabledDate || !cell) return;
      const disabledCls = panelCellDisabledCls(cell);
      if (!disabledCls) return;
      const mine = cell.getAttribute('data-adv-disabled') === '1';
      if (this.isDisabledCell(cell)) {
        if (!mine) {
          cell.setAttribute('data-adv-disabled', '1');
          cell.classList.add(disabledCls);
        }
      } else if (mine) {
        cell.removeAttribute('data-adv-disabled');
        cell.classList.remove(disabledCls);
      }
    },
    popupNode() {
      const stack = [this.$refs.picker];
      let guard = 0;
      while (stack.length && guard++ < 200) {
        const cur = stack.shift();
        if (!cur) continue;
        if (typeof cur.getPopupDomNode === 'function') {
          let node = null;
          try {
            node = cur.getPopupDomNode();
          } catch (e) {
            node = null;
          }
          if (node && node.nodeType === 1) return node;
        }
        const kids = cur.$children;
        if (kids && kids.length) for (let i = 0; i < kids.length; i++) stack.push(kids[i]);
      }
      return null;
    },
    visiblePopup() {
      const nodes = document.querySelectorAll('[class*="-calendar-picker-container"]');
      for (let i = nodes.length - 1; i >= 0; i--) {
        const rect = nodes[i].getBoundingClientRect();
        if (rect.width > 0 && rect.height > 0) return nodes[i];
      }
      return null;
    },
    decoratePanel() {
      if (!this.disabledDate) return;
      this.$nextTick(() => this.paintPanel(0));
    },
    paintPanel(attempt) {
      if (!this.disabledDate || !this.sOpen || this.__decorating) return;
      const popup = this.popupNode() || this.visiblePopup();
      if (!popup) {
        if (attempt < 5) setTimeout(() => this.paintPanel(attempt + 1), 60);
        return;
      }
      this.__decorating = true;
      try {
        if (this.__observer && this.__observeTarget !== popup) {
          this.__observer.disconnect();
          this.__observer.observe(popup, {
            subtree: true,
            childList: true,
            attributes: true,
            attributeFilter: ['class'],
          });
          this.__observeTarget = popup;
        }
        const cells = popup.querySelectorAll('[class*="-panel-cell"]');
        Array.prototype.forEach.call(cells, (c) => this.paintCell(c));
      } finally {
        this.__decorating = false;
      }
    },
    stopObserve() {
      if (this.__observer) {
        this.__observer.disconnect();
        this.__observeTarget = null;
      }
    },
    parseOne(v) {
      if (!v) return null;
      let m = moment.isMoment(v) ? v.clone() : moment(v, this.format, true);
      if (!m.isValid()) m = moment(v);
      return m.isValid() ? m.startOf(UNIT) : null;
    },
    syncFromProp() {
      const arr = Array.isArray(this.value) ? this.value : [];
      this.pickedStart = this.parseOne(arr[0]);
      this.pickedEnd = this.parseOne(arr[1]);
      this.anchor = this.currentDisplay();
    },
    currentDisplay() {
      if (this.pickedStart) return [this.pickedStart, this.pickedEnd];
      const base = this.parseOne((this.defaultPickerValue || [])[0]) || moment();
      return [base, base.clone().add(1, UNIT)];
    },
    sameUnit(a, b) {
      if (!a && !b) return true;
      if (!a || !b) return false;
      return a.isSame(b, UNIT);
    },
    toOut(m) {
      if (!m) return null;
      return this.valueType === 'moment' ? m.clone() : m.format(this.format);
    },
    emitValue(s, e) {
      if (this.valueType === 'moment') return s || e ? [s, e] : [];
      return s || e ? [this.toOut(s), this.toOut(e)] : [];
    },
    isCellPick(mode) {
      const arr = Array.isArray(mode) ? mode : [mode];
      return arr.some((m) => m !== 'month' && m !== 'year' && m !== 'decade');
    },
    detectClicked(v) {
      const prev = this.anchor;
      if (!prev || !v) return v && (v[0] || v[1]);
      if (!this.sameUnit(v[0], prev[0])) return v[0];
      if (!this.sameUnit(v[1], prev[1])) return v[1];
      return v[0] || v[1];
    },
    handleOpenChange(open) {
      this.sOpen = open;
      if (!open && this.pickedStart && !this.pickedEnd) this.syncFromProp();
      this.anchor = this.currentDisplay();
      if (open) this.decoratePanel();
      else this.stopObserve();
      this.fire('openChange', open);
    },
    handlePanelChange(v, mode) {
      this.fire('panelChange', v ? [v[0], v[1]] : v, mode);
      this.decoratePanel(); // 切面板 / 翻页后重新上色
      if (!this.isCellPick(mode)) return;

      const side = this.clickedSide;
      const clicked = side === 'start' ? v[0] : side === 'end' ? v[1] : this.detectClicked(v);
      this.clickedSide = null;
      if (!clicked) return;

      // 双保险:捕获阶段万一没拦住,也不允许写进禁用值
      if (this.isDisabledByUser(clicked)) return;

      const start = this.pickedStart;
      const end = this.pickedEnd;

      if (!start || end) {
        this.pickedStart = clicked;
        this.pickedEnd = null;
        this.anchor = v;
        return;
      }

      const s = clicked.isAfter(start, UNIT) ? start : clicked;
      const e = clicked.isAfter(start, UNIT) ? clicked : start;
      this.pickedStart = s;
      this.pickedEnd = e;
      this.anchor = [s, e];
      this.sOpen = false;
      this.$emit('change', this.emitValue(s, e), [s.format(this.format), e.format(this.format)]);
    },
    handleCalendarChange(v) {
      this.fire('calendarChange', v ? [v[0], v[1]] : v);
    },
    handleChange(v) {
      const s = v && v[0];
      const e = v && v[1];
      if (s && e) {
        if (this.sameUnit(s, this.pickedStart) && this.sameUnit(e, this.pickedEnd)) return; // 去重
        this.pickedStart = s;
        this.pickedEnd = e;
        this.anchor = [s, e];
        this.sOpen = false;
        this.$emit('change', this.emitValue(s, e), [s.format(this.format), e.format(this.format)]);
        return;
      }
      this.pickedStart = null;
      this.pickedEnd = null;
      this.anchor = this.currentDisplay();
      this.$emit('change', [], []);
    },
    focus() {
      const p = this.$refs.picker;
      if (p && p.focus) p.focus();
    },
    blur() {
      const p = this.$refs.picker;
      if (p && p.blur) p.blur();
    },
    clear() {
      this.pickedStart = null;
      this.pickedEnd = null;
      this.sOpen = false;
      this.anchor = this.currentDisplay();
      this.$emit('change', [], []);
    },
  },
};
</script>
相关推荐
牧艺1 小时前
浏览器端 3D 看车重磅升级:从三款展车到可试驾的八车展厅
前端·three.js·交互设计
老男孩游戏时光1 小时前
Electron 桌面应用开发入门,前端也能写客户端
前端
默_笙1 小时前
🚊 别再让 AI 的记忆写两遍了:PostgreSQL 把关系查询和语义检索装进一张表
前端·javascript
进击的明明1 小时前
TypeScript速通笔记(下)
前端·面试·typescript
lzhdim1 小时前
C#不为人知的10个魔法特性:资深开发者也会震惊的底层奥秘
java·前端·javascript·算法·c#
计算机毕设定制辅导-无忧学长2 小时前
《基于Spring Boot的减脂轻食购物网站》
java·vue.js·spring boot·后端·减脂轻食购物网站
zach2 小时前
前端落地实战:Next.js 项目 Nginx + PM2 生产部署全流程(附踩坑总结)
前端·pm2·next.js
overmind2 小时前
oeasy教h5前端001 修改的乐趣_直接修改在线网页
前端