diff --git a/devui-cli/index.js b/devui-cli/index.js old mode 100644 new mode 100755 diff --git a/devui/date-picker/components/calendar/index.scss b/devui/date-picker/components/calendar/index.scss new file mode 100644 index 0000000000000000000000000000000000000000..72fe3cf172c218dce785940809b68ca9be68a449 --- /dev/null +++ b/devui/date-picker/components/calendar/index.scss @@ -0,0 +1,8 @@ +.devui-calendar-container { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: flex-start; + user-select: none; + position: relative; +} diff --git a/devui/date-picker/components/calendar/index.tsx b/devui/date-picker/components/calendar/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2834b42ab8de6c8e8fd1e749de20a5b3a962c38b --- /dev/null +++ b/devui/date-picker/components/calendar/index.tsx @@ -0,0 +1,36 @@ +import { TProps } from '../types' +import CalendarDatePanel from '../panel' +import TimePicker from '../timepicker' + +import './index.scss' + +const Calendar = (props: TProps) => { + const { showTime = false } = props + let { current } = props + if (!(current instanceof Date)) { + current = new Date + } + if (props.type === 'range') { + let { next } = props + if (!(next instanceof Date)) { + next = new Date(current.getFullYear(), current.getMonth() + 1, 1) + } + return ( +
+ + { showTime ? : null } + + { showTime ? : null } +
+ ) + } else { + return ( +
+ + { showTime ? : null } +
+ ) + } +} + +export default Calendar \ No newline at end of file diff --git a/devui/date-picker/components/helper.ts b/devui/date-picker/components/helper.ts new file mode 100644 index 0000000000000000000000000000000000000000..a756d082c922766301f6912bc2d86f4843a7eb39 --- /dev/null +++ b/devui/date-picker/components/helper.ts @@ -0,0 +1,81 @@ +import { invokeCallback, subDateDay } from './utils' +import { TDateCell, TDatePanelDataProps, TDatePanelProps } from './types' + +export const getDateKey = (date: Date): string => { + return date.toDateString() +} + +export const cellClassName = (props: TDatePanelDataProps, day: TDateCell, base = 'cell'): string => { + if(day.current !== 0) { + return `${base} disabled` + } + const { dateMin, dateMax } = props + if(dateMin && subDateDay(day.date, dateMin) < 0) { + return `${base} disabled` + } + if(dateMax && subDateDay(dateMax, day.date) < 0) { + return `${base} disabled` + } + const key = getDateKey(day.date) + if (props.type === 'range') { + if (props.dateStart) { + if (getDateKey(props.dateStart) === key) { + return `${base} selected` + } + if (props.dateEnd && getDateKey(props.dateEnd) === key) { + return `${base} selected` + } + const innerEnd = props.dateEnd || props.dateHover + if (innerEnd) { + const range = innerEnd > props.dateStart ? [props.dateStart, innerEnd] : [innerEnd, props.dateStart] + if (day.date > range[0] && day.date < range[1]) { + return `${base} innerday` + } + } + } + return base + } else { + return props.dateStart && getDateKey(props.dateStart) === key ? `${base} selected` : base + } +} + +export const trigEvent = (props: TDatePanelProps, day: TDateCell): void => { + if(day.current !== 0) { + return + } + if (props.type === 'range') { + if (!props.dateStart) { + invokeCallback(props.onSelectStart, day.date) + } else if (!props.dateEnd) { + if(subDateDay(props.dateStart, day.date) !== 0) { + invokeCallback(props.onSelectEnd, day.date) + typeof props.onChange === 'function' && props.onChange(props.type, props) + } + } else { + invokeCallback(props.onReset, day.date) + } + } else { + invokeCallback(props.onSelected, day.date) + typeof props.onChange === 'function' && props.onChange(props.type, props) + } +} + +export const handleDateEnter = (props: TDatePanelProps, day: TDateCell): void => { + if(day.current !== 0) { + return + } + const { dateMin, dateMax } = props + if(dateMin && subDateDay(day.date, dateMin) < 0) { + return + } + if(dateMax && subDateDay(dateMax, day.date) < 0) { + return + } + if (props.type === 'range') { + const key = getDateKey(day.date) + if (!props.dateStart || getDateKey(props.dateStart) === key || props.dateEnd) { + return + } + invokeCallback(props.onSelecting, day.date) + } +} \ No newline at end of file diff --git a/devui/date-picker/components/panel/index.scss b/devui/date-picker/components/panel/index.scss new file mode 100644 index 0000000000000000000000000000000000000000..57ae53ec08f1f14132b3877b3b4ce868480e154b --- /dev/null +++ b/devui/date-picker/components/panel/index.scss @@ -0,0 +1,78 @@ +@import '../../../style/devui.scss'; + +$panel-width: 230px; +$panel-height: 210px; +$panel-padding: 5px; +$panel-row-height: 24px; +$panel-cell-bg: #ffffff; +$panel-cell-color: #000000; +$panel-cell-active-bg: #0066cc; +$panel-cell-active-color: #f1f1f1; +$panel-cell-active-hover-bg: #0088dd; +$panel-cell-active-hover-color: #ffffff; + +.devui-calendar-panel { + width: $panel-width; + height: $panel-height; + padding: $panel-padding; + box-sizing: border-box; + overflow: hidden; + + .row { + display: flex; + flex-direction: row; + justify-content: space-between; + height: $panel-row-height; + + .cell { + width: 100%; + text-align: center; + flex-grow: 1; + flex-shrink: 1; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + border-radius: $devui-border-radius; + background-color: $panel-cell-bg; + color: $panel-cell-color; + + &:hover { + background-color: $devui-disabled-bg; + } + + &.selected { + background-color: $panel-cell-active-bg; + color: $panel-cell-active-color; + + &:hover { + background-color: $panel-cell-active-hover-bg; + color: $panel-cell-active-hover-color; + } + } + + &.innerday { + background-color: $devui-disabled-bg; + } + + &.disabled { + color: $devui-disabled-text; + } + } + } + + .head { + cursor: default; + padding: 0; + margin: 0; + } + + .body { + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + cursor: pointer; + list-style: none; + } +} diff --git a/devui/date-picker/components/panel/index.tsx b/devui/date-picker/components/panel/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..fdef8292063c881f5d933a38e360176e4f3790a1 --- /dev/null +++ b/devui/date-picker/components/panel/index.tsx @@ -0,0 +1,43 @@ +import { TDatePanelProps } from '../types' +import { getMonthWeeklyDays, WEEK_DAYS } from '../utils' +import { handleDateEnter, cellClassName, trigEvent } from '../helper' +import Toolbar from '../toolbar' +import './index.scss' + +const CalendarDatePanel = (props: TDatePanelProps) => { + return ( +
+ +
    { + WEEK_DAYS.map(day =>
  1. {day}
  2. ) + }
+ +
+ ) +} + +export default CalendarDatePanel \ No newline at end of file diff --git a/devui/date-picker/components/timepicker/index.scss b/devui/date-picker/components/timepicker/index.scss new file mode 100644 index 0000000000000000000000000000000000000000..139b9aaaa27d6fa03304af43b56307ddd428eba8 --- /dev/null +++ b/devui/date-picker/components/timepicker/index.scss @@ -0,0 +1,69 @@ +@import '../../../style/devui.scss'; +@import '../panel/index.scss'; + +$calendar-timepicker-width: 100px; +$head-height: 32px; + +.devui-calendar-timepicker { + width: $calendar-timepicker-width; + height: $panel-height; + overflow: hidden; + position: relative; + + .head { + height: $head-height; + line-height: $head-height; + background-color: #f1f1f1; + text-align: center; + display: flex; + justify-content: center; + // position: absolute; + // width: 100%; + // top: 50%; + // margin-top: -($head-height / 2); + + // z-index: 20; + + // .chars { + // width: 38%; + // display: flex; + // justify-content: space-between; + + // span { + // color: #000; + // // font-size: 16px; + // // margin-top: -2px + // } + // } + } + + .select { + display: flex; + flex-direction: row; + justify-content: space-around; + height: $panel-height - $head-height; + // height: $panel-height; + .column { + cursor: default; + overflow: auto; + flex-grow: 1; + + span { + display: block; + font-size: 11px; + width: 24px; + height: 24px; + line-height: 24px; + text-align: center; + overflow: hidden; + border-radius: 100%; + background-color: #f6f6f6; + + &.selected { + background-color: #00aaff; + color: #ffffff; + } + } + } + } +} diff --git a/devui/date-picker/components/timepicker/index.tsx b/devui/date-picker/components/timepicker/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..df874f8722b7cd3aab97d05af21e93f090414960 --- /dev/null +++ b/devui/date-picker/components/timepicker/index.tsx @@ -0,0 +1,59 @@ +import { defineComponent, onMounted, ref, reactive } from 'vue' +import VerticalSliderFunction from '../vertical-slider' + +import './index.scss' + +const TimePicker = defineComponent({ + props: { + time: { type: Date } + }, + setup(props) { + + const { time = new Date() } = props || {} + const state = reactive({ + hour: time.getHours(), + minute: time.getMinutes(), + second: time.getSeconds() + }) + + const hours = Array(24).fill(0).map((_, i) => `${i}`.padStart(2, '0')) + const minutes = Array(60).fill(0).map((_, i) => `${i}`.padStart(2, '0')) + + return () => { + return ( +
+
+
+ {/* {`:`} + {`:`} */} + + {state.hour.toString().padStart(2, '0')}: + {state.minute.toString().padStart(2, '0')}: + {state.second.toString().padStart(2, '0')} + +
+
+
+ state.hour = idx} + /> + state.minute = idx} + /> + state.second = idx} + /> +
+
+ ) + } + } +}) + +export default TimePicker \ No newline at end of file diff --git a/devui/date-picker/components/toolbar/index.scss b/devui/date-picker/components/toolbar/index.scss new file mode 100644 index 0000000000000000000000000000000000000000..d73f4afc503a6b0d568452bb30e28d1322d1b681 --- /dev/null +++ b/devui/date-picker/components/toolbar/index.scss @@ -0,0 +1,45 @@ +@import '../../../style/devui.scss'; + +$toolbar-height: 32px; +$cell-size: 28px; + +.devui-calendar-toolbar { + height: $toolbar-height; + font-weight: $font-title-weight; + display: flex; + justify-content: space-between; + align-items: center; + user-select: none; + + a { + width: $cell-size; + height: $cell-size; + line-height: $cell-size; + color: $text-color; + display: block; + text-align: center; + flex-shrink: 1; + flex-grow: 0; + cursor: pointer; + text-decoration: none; + + &:hover { + color: $text-color; + text-decoration: none; + } + + &.disabled { + color: $devui-disabled-text; + cursor: not-allowed; + + &:hover { + color: $devui-disabled-text; + } + } + + &.title { + flex-grow: 1; + flex-shrink: 1; + } + } +} diff --git a/devui/date-picker/components/toolbar/index.tsx b/devui/date-picker/components/toolbar/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..802ba1672b213cdb26a7f13a46bd01b00c2094c1 --- /dev/null +++ b/devui/date-picker/components/toolbar/index.tsx @@ -0,0 +1,76 @@ +import { compareDate, invokeCallback, subDateMonth } from '../utils' +import { Year, Month } from './svg-icon' +import { TCalendarToolbarItemProps, TDateToolbarProps } from '../types' +import './index.scss' + +const Item = (props: TCalendarToolbarItemProps) => { + const { + button: Btn, + disabled = false, + rotate = 0, + date, + pos, + cb, + } = props + const color = disabled ? '#cfd0d3' : '#585d6b' + const className = `${disabled ? 'disabled' : ''}` + const handleClick = disabled ? undefined : () => invokeCallback(props.cb, date, pos) + return ( + + + + ) +} + +export const Title = (props: { date: Date; }) => { + const { date } = props + return ( + { + `${date.getFullYear()}年${(date.getMonth() + 1 + '').padStart(2, '0')}月` + } + ) +} + +const CalendarToolbar = (props: TDateToolbarProps) => { + const { + type, current, compare, pos, + dateMax, dateMin, + onPreviousYear, + onPreviousMonth, + onNextMonth, + onNextYear, + } = props + + const dis = [false, false, false, false] + + if (type === 'range') { + if (pos === 1) { + dis[0] = !compareDate(compare, current, 'year', 1) + dis[1] = !compareDate(compare, current, 'month', 1) + dis[2] = !compareDate(current, dateMax, 'month', 0) + dis[3] = !compareDate(current, dateMax, 'year', 0) + } else { + dis[0] = !compareDate(dateMin, current, 'year', 0) + dis[1] = !compareDate(dateMin, current, 'month', 0) + dis[2] = !compareDate(current, compare, 'month', 1) + dis[3] = !compareDate(current, compare, 'year', 1) + } + } else { + dis[0] = !compareDate(dateMin, current, 'year', 0) + dis[1] = !compareDate(dateMin, current, 'month', 0) + dis[2] = !compareDate(current, dateMax, 'month', 0) + dis[3] = !compareDate(current, dateMax, 'year', 0) + } + + return ( +
+ + + + <Item disabled={dis[2]} date={current} pos={pos} button={Month} rotate={90} cb={onNextMonth} /> + <Item disabled={dis[3]} date={current} pos={pos} button={Year} rotate={180} cb={onNextYear} /> + </div> + ) +} + +export default CalendarToolbar \ No newline at end of file diff --git a/devui/date-picker/components/toolbar/svg-icon.tsx b/devui/date-picker/components/toolbar/svg-icon.tsx new file mode 100644 index 0000000000000000000000000000000000000000..01d9a31a3e853397497af16d85d073773a3128d4 --- /dev/null +++ b/devui/date-picker/components/toolbar/svg-icon.tsx @@ -0,0 +1,23 @@ +import { TIconSvgProps } from '../types' + +export const Year = (props: TIconSvgProps) => { + const { color = '#585d6b', rotate = 0 } = props + return ( + <svg style={{ transform: `rotate(${rotate}deg)` }} width="10px" height="10px" viewBox="0 0 10 10" version="1.1" xmlns="http://www.w3.org/2000/svg"> + <g fill={color} transform="translate(-1.000000, -1.000000)"> + <path d="M11,1.83333333 L11,10.1666667 L7,7.38833333 L7,10.1666667 L1,6 L7,1.83333333 L7,4.61033333 L11,1.83333333 Z" /> + </g> + </svg> + ) +} + +export const Month = (props: TIconSvgProps) => { + const { color = '#585d6b', rotate = 0 } = props + return ( + <svg style={{ transform: `rotate(${rotate}deg)` }} width="6px" height="10px" viewBox="0 0 6 10" version="1.1" xmlns="http://www.w3.org/2000/svg"> + <g fill={color} transform="translate(-3.000000, -1.000000)"> + <polygon points="6 3 10.1666667 9 1.83333333 9" /> + </g> + </svg> + ) +} \ No newline at end of file diff --git a/devui/date-picker/components/types.ts b/devui/date-picker/components/types.ts new file mode 100644 index 0000000000000000000000000000000000000000..66db15209623c554c4ba609bdfb04080a869fd4e --- /dev/null +++ b/devui/date-picker/components/types.ts @@ -0,0 +1,69 @@ +export type TDateCell = { date: Date; current: -1 | 0 | 1; } +export type TDatePanelMode = 'month' | 'year' +export type TDatePanelType = 'select' | 'range' +export type TEventCallback = (date: Date, position?: number) => void + +export type TDateConfig = { + type?: TDatePanelType + mode?: TDatePanelMode + current: Date + showTime: boolean + dateMin?: Date + dateMax?: Date +} + +export type TDateSelectingBase = { + dateStart?: Date + dateEnd?: Date + dateHover?: Date +} + +export type TDateToolbarEventProps = { + onPreviousYear?: TEventCallback + onPreviousMonth?: TEventCallback + onNextMonth?: TEventCallback + onNextYear?: TEventCallback +} + +export type TDateToolbarDataProps = TDateConfig & { + pos?: number + compare?: Date +} + +export type TDateToolbarProps = TDateToolbarDataProps & TDateToolbarEventProps + +export type TDatePanelEventProps = TDateToolbarEventProps & { + onSelected?: TEventCallback + onReset?: TEventCallback + onSelectStart?: TEventCallback + onSelectEnd?: TEventCallback + onSelecting?: TEventCallback + onChange?: (type: TDatePanelType, config: TDateSelectingBase) => void +} + +export type TDatePanelDataProps = TDateToolbarDataProps & TDateSelectingBase + +export type TDatePanelProps = TDatePanelDataProps & TDatePanelEventProps + + +export type TProps = ({ + type: 'select' +} | { + type: 'range' + next: Date +}) & TDateConfig & TDateSelectingBase & TDatePanelEventProps + +export type TIconSvgProps = { + color?: string + rotate?: number +} +export type TIconSvg = (props: TIconSvgProps) => any + +export type TCalendarToolbarItemProps = { + disabled?: boolean + rotate?: number + cb?: (...args: any[]) => void + pos: number + date: Date + button: TIconSvg +} \ No newline at end of file diff --git a/devui/date-picker/components/utils.ts b/devui/date-picker/components/utils.ts new file mode 100644 index 0000000000000000000000000000000000000000..cb2c02a57e3d9f1f25dc11efe0b2c125adddc389 --- /dev/null +++ b/devui/date-picker/components/utils.ts @@ -0,0 +1,149 @@ +import { TDateCell } from './types' + +const getHumanDate = (d: Date) => { + const year = d.getFullYear() + const month = d.getMonth() + 1 + const date = d.getDate() + const day = d.getDay() + const hour = d.getHours() + const minute = d.getMinutes() + const second = d.getSeconds() + const ms = d.getMilliseconds() + + return { + year, y: year, month, M: month, date, d: date, day, + hour, H: hour, h: hour, + minute, m: minute, + second, s: second, + ms, + } +} + +const getMonthDays = (year: number, month: number) => { + const first = new Date(year, month - 1, 1) + const last = new Date(year, month, 0) + const dates: TDateCell[] = [] + + let day = first.getDay() + while (day > 0) { + day -= 1 + dates.push({ date: new Date(year, month - 1, -day), current: -1 }) + } + + day = last.getDate() - first.getDate() + for (let i = 0; i <= day; i++) { + const date = new Date(first) + date.setDate(i + 1) + dates.push({ date, current: 0 }) + } + + day = last.getDay() + let tail: Date = last + for (let i = day; i < 6; i++) { + tail = new Date(year, month, i - day + 1) + dates.push({ date: tail, current: 1 }) + } + if(dates.length < 42) { + day = tail.getDate() + for (let i = 1; i <= 7; i++) { + tail = new Date(year, month, day + i) + dates.push({ date: tail, current: 1 }) + } + } + return dates +} + +export const getMonthWeeklyDays = (date: any = new Date()) => { + if (!(date instanceof Date)) { + date = new Date() + } + const { year, month } = getHumanDate(date) + const days = getMonthDays(year, month) + const dayRows: TDateCell[][] = [] + while (days.length > 0) { + dayRows.push(days.splice(0, 7)) + } + return dayRows +} + +export const WEEK_DAYS = ['日', '一', '二', '三', '四', '五', '六'] + +export const invokeCallback = (cb: any, ...args: any[]) => { + typeof cb === 'function' && cb(...args) +} + +/** + * a - b 的月数 + */ +export const subDateMonth = (a: Date, b: Date) => { + const am = a.getFullYear() * 12 + a.getMonth() + const bm = b.getFullYear() * 12 + b.getMonth() + return am - bm +} + +const ONE_DAY = 1000 * 60 * 60 * 24 + +/** + * a - b 的天数 + * @param a + * @param b + * @returns + */ +export const subDateDay = (a: Date, b: Date) => { + const ad = new Date(a.getFullYear(), a.getMonth(), a.getDate()).getTime() + const bd = new Date(b.getFullYear(), b.getMonth(), b.getDate()).getTime() + return (ad - bd) / ONE_DAY +} + +/** +* 比较日期单位 +* @param small 相对早的日期 +* @param big 相对晚的日期 +* @param mode 比较单位 +* @param min 不能小于这个值 +* @returns +*/ +export const compareDate = (small: Date | undefined, big: Date | undefined, mode: 'year' | 'month', min: number) => { + if (!small || !big) { + return true + } + if (mode === 'year') { + return big.getFullYear() - small.getFullYear() > min + } else { + return subDateMonth(big, small) > min + } +} + +export const parseDate = (str?: string) : Date | null => { + if(!str || typeof str !== 'string') { + return null + } + + const [dateStr = '', timeStr = ''] = str.split(/([ ]|T)+/) + if(!dateStr) { + return null + } + const [y, m, d] = dateStr.split(/[^\d]+/) + const year = _parseInt(y), month = _parseInt(m), date = _parseInt(d) || 1 + if(!year || !month) { + return null + } + const time = parseTime(timeStr) + return new Date(year, month - 1, date, ...time) +} + +const _parseInt = (str: any, dftVal?: number) => { + if(!str || typeof str !== 'string') { + return dftVal + } + const n = parseInt(str) + if(isNaN(n)) { + return dftVal + } + return n +} + +export const parseTime = (str?: string) : [number, number, number, number] => { + const [h, m, s, ms] = str.split(/[\:\.]+/) + return [_parseInt(h, 0), _parseInt(m, 0), _parseInt(s, 0), _parseInt(ms, 0)] +} \ No newline at end of file diff --git a/devui/date-picker/components/vertical-slider/index.scss b/devui/date-picker/components/vertical-slider/index.scss new file mode 100644 index 0000000000000000000000000000000000000000..8e2160d290fe997a19a13e2b7162b28e66ae16e9 --- /dev/null +++ b/devui/date-picker/components/vertical-slider/index.scss @@ -0,0 +1,45 @@ +.devui-vertical-slider { + display: flex; + flex-direction: column; + min-width: 24px; + text-align: center; + position: relative; + overflow: hidden; + flex-grow: 1; + + .movable-bar { + position: relative; + overflow: visible; + + .slider-item { + display: block; + flex-grow: 0; + flex-shrink: 0; + box-sizing: border-box; + + // &.selected { + // background-color: #06c; + // color: #fff; + // } + } + } + + .forcus { + position: absolute; + box-sizing: border-box; + width: 100%; + height: 50%; + top: 25%; + } + + .slider-mask { + position: absolute; + left: 0; + top: 0; + bottom: 0; + right: 0; + background-color: rgba(0, 0, 0, 0); + background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.3), transparent, rgba(0, 0, 0, 0.3)); + z-index: 99; + } +} diff --git a/devui/date-picker/components/vertical-slider/index.tsx b/devui/date-picker/components/vertical-slider/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..07f0265daaf2461e59afabae6730e5b86c625465 --- /dev/null +++ b/devui/date-picker/components/vertical-slider/index.tsx @@ -0,0 +1,120 @@ +import { defineComponent, reactive, onMounted, ref } from 'vue' + +import './index.scss' + +const VerticalSlider = defineComponent({ + props: { + size: { type: Number, default: 26 }, + items: { type: Array }, + selectedIndex: { type: Number }, + className: { type: String }, + itemClassNormal: { type: String }, + itemClassSelected: { type: String }, + onChange: { type: Function } + }, + setup(props) { + + const { + items = [0,1,2,3,4,5,6,7,8,9], + selectedIndex = 0, + size = 26, + className = '', + itemClassNormal = '', + itemClassSelected = 'selected', + onChange, + } = props || {} + + let max_y = 0, min_y = 0 + const container = ref<Element>() + const movbar = ref<Element>() + + let pos_start: [number, number] | null = null + let pos_cache: [number, number] | null = null + + const state = reactive<{ + selectedIndex: number + barOpacity: number + x: number + y: number + transition: string + }>({ + selectedIndex, + barOpacity: 0, + x: 0, y: 0, + transition: 'none', + }) + + const handleMouseDown = (e: MouseEvent) => { + e.stopPropagation() + e.preventDefault() + pos_start = [e.clientX, e.clientY] + state.transition = 'none' + } + + const handleMouseMove = (e: MouseEvent) => { + e.stopPropagation() + e.preventDefault() + if(!pos_start || !pos_cache) { + return + } + state.x = pos_cache[0] + e.clientX - pos_start[0] + state.y = Math.min(max_y, Math.max(min_y, pos_cache[1] + e.clientY - pos_start[1])) + state.selectedIndex = (max_y - state.y + size / 2) / size >> 0 + } + const handleMouseUp = (e: MouseEvent) => { + e.stopPropagation() + e.preventDefault() + pos_start = null + state.y = max_y - state.selectedIndex * size; + state.transition = 'transform 0.1s' + pos_cache[0] = state.x + pos_cache[1] = state.y + if(typeof onChange === 'function') { + const idx = state.selectedIndex + const val = items[idx] + onChange(val, idx) + } + } + + onMounted(() => { + const { height: ch } = container.value.getBoundingClientRect() + const { height: mh } = movbar.value.getBoundingClientRect() + max_y = (ch - size) / 2 + min_y = (ch + size) / 2 - mh + pos_cache = [0, max_y - state.selectedIndex * size] + state.x = pos_cache[0] + state.y = pos_cache[1] + state.barOpacity = 1 + state.transition = 'transform 0.1s' + // console.log(ch, mh) + }) + + return () => { + return ( + <div ref={container} class={`devui-vertical-slider ${className}`}> + <div ref={movbar} class="movable-bar" style={{ + opacity: state.barOpacity, + transform: `translateY(${state.y}px)`, + transition: state.transition, + }}> + { + items.map((c, i) => { + const className = i === state.selectedIndex ? itemClassSelected : itemClassNormal + return <span class={`slider-item ${className}`} style={{ height: `${size}px`, lineHeight: `${size}px` }}>{c}</span> + }) + } + </div> + <div + class="slider-mask" + onMousedown={handleMouseDown} + onMousemove={handleMouseMove} + onMouseup={handleMouseUp} + onMouseout={handleMouseUp} + ></div> + </div> + ) + } + } +}) + +export default VerticalSlider \ No newline at end of file diff --git a/devui/date-picker/date-picker.scss b/devui/date-picker/date-picker.scss new file mode 100644 index 0000000000000000000000000000000000000000..26d48111a251df99f3aacb48adcc067654f0aa39 --- /dev/null +++ b/devui/date-picker/date-picker.scss @@ -0,0 +1,31 @@ +@import '../style/devui.scss'; + +$cell-font-size: 13px; +$border-width: 1px; +$border-style: solid; +$border-color: #dddddd; + +.devui-datepicker-global-viewport { + position: fixed; + left: 0; + top: 0; + width: 0; + height: 0; + background-color: rgba(0, 0, 0, 0.2); + z-index: 99; + overflow: visible; +} + +.devui-datepicker-container { + margin: 0; + padding: 0; + position: relative; + display: inline-block; + border-width: $border-width; + border-style: $border-style; + border-color: $border-color; + border-radius: $devui-border-radius-card; + box-shadow: $devui-shadow-length-base $devui-shadow; + background-color: $devui-base-bg; + font-size: $cell-font-size; +} diff --git a/devui/date-picker/date-picker.tsx b/devui/date-picker/date-picker.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c8285aca5db6294a46a03b07d711d75162b0eaab --- /dev/null +++ b/devui/date-picker/date-picker.tsx @@ -0,0 +1,167 @@ +import { defineComponent, reactive, onMounted, onUnmounted, ref } from 'vue' +import { + EventManager, isIn, + traceNode, invokeFunction, +} from './utils' + +import { + TState, + handlePositionFactory, + handleCalendarSwitchState, + formatValue, + formatPlaceholder, + getAttachInputDom, +} from './helper' + +import Calendar from './components/calendar' + +import './date-picker.scss' +import { parseDate } from './components/utils' + +export default defineComponent({ + name: 'DDatepicker', + props: { + selectedDateChange: { type: Function }, + autoClose: { type: Boolean, default: false }, + range: { type: Boolean, default: false }, + showTime: { type: Boolean, default: false }, + format: { type: String, default: 'y/MM/dd' }, + rangeSpliter: { type: String, default: '-' }, + attachInputDom: { type: String }, + dateMin: { type: String }, + dateMax: { type: String }, + }, + setup(props, ctx) { + + const container = ref<Element>() + const evtman = new EventManager() + const current = new Date() + + const state = reactive<TState>({ + range: !!props.range, + current, + next: new Date(current.getFullYear(), current.getMonth() + 1, 1), + show: false, + input: props.attachInputDom, + st: true, + x: '0', + y: '0', + }) + + // 弹出层跟踪 + const handlePosition = handlePositionFactory(state, props, container) + + // 绑定层显示值、placehoder值设置 + const setBindingDom = (el: any = getAttachInputDom(state, props)) => { + + const value = formatValue(state, props) + const placeholder = formatPlaceholder(props) + + // 判断节点原生DOM类型 + // 对input节点的值处理 + if (el instanceof HTMLInputElement) { + // 设置水印文字 + el.placeholder = placeholder + // 设置显示值 + el.value = value + return el.value + } + return value + } + + const reset = () => { + state.hover = null + state.current = null + state.next = null + if (state.start) { + if (state.end && Math.abs(state.end.getMonth() - state.start.getMonth()) > 0) { + state.next = state.end + } + } else { + state.end = null + } + } + + onMounted(() => { + // 获取绑定节点(默认input) + const el = getAttachInputDom(state, props) + // 绑定节点不存在,作为普通组件展示。 + if (!el) { + // 显示组件 + state.show = true + return + } else { + // 作为弹出层,先隐藏 + state.show = false + } + + setBindingDom(el) + + // 绑定节点click事件处理弹出层显示 + evtman.append(el, 'click', () => !state.show && (state.show = true)) + // document层处理`点击其他区域隐藏` + evtman.append(document, 'click', (e: MouseEvent) => { + if (!state.show || e.target === el || isIn(e.target as Node, container.value)) { + return + } + state.show = false + reset() + }) + // 对绑定节点做scroll跟踪,并绑定跟踪事件 + traceNode(el).forEach(node => { + evtman.append(node, 'scroll', handlePosition) + }) + }) + + onUnmounted(() => { + evtman.dispose() + }) + + return () => { + handlePosition() + setBindingDom() + return ( + <div class={state.st ? `` : `devui-datepicker-global-viewport`}> + <div + ref={container} + class="devui-datepicker-container" + style={{ + transform: state.st ? '' : `translateX(${state.x}) translateY(${state.y})` + }} + > + <Calendar + type={props.range ? 'range' : 'select'} + showTime={props.showTime} + current={state.current} + next={state.next} + dateMin={parseDate(props.dateMin)} + dateMax={parseDate(props.dateMax)} + dateStart={state.start} + dateEnd={state.end} + dateHover={state.hover} + onReset={(date: Date) => { + state.current = state.end = state.hover = undefined + state.start = date + }} + onChange={() => { + const output = setBindingDom() + invokeFunction(props.selectedDateChange, output) + if (props.autoClose) { + state.show = false + } + }} + onSelected={(date: Date) => state.start = date} + onSelectStart={(date: Date) => state.start = date} + onSelectEnd={(date: Date) => state.end = date} + onSelecting={(date: Date) => state.hover = date} + onPreviousYear={(date: Date, pos: number) => handleCalendarSwitchState(state, 0, pos, date)} + onPreviousMonth={(date: Date, pos: number) => handleCalendarSwitchState(state, 1, pos, date)} + onNextMonth={(date: Date, pos: number) => handleCalendarSwitchState(state, 2, pos, date)} + onNextYear={(date: Date, pos: number) => handleCalendarSwitchState(state, 3, pos, date)} + /> + </div> + </div> + ) + } + } +}) \ No newline at end of file diff --git a/devui/date-picker/helper.ts b/devui/date-picker/helper.ts new file mode 100644 index 0000000000000000000000000000000000000000..0bc57373451e73cf5ea77621be18a878322136fb --- /dev/null +++ b/devui/date-picker/helper.ts @@ -0,0 +1,151 @@ +import type { Ref } from 'vue' +import { formatDate, formatRange } from './utils' + +export type TState = { + range?: boolean + current?: Date + next?: Date + start?: Date + end?: Date + hover?: Date + show?: boolean + input?: string + st?: boolean + x?: string + y?: string +} + +/** + * Calendar 面板年月切换逻辑 + * @param state + * @param index + * @param pos + * @param date + */ +export const handleCalendarSwitchState = (state: TState, index: number, pos: number, date: Date) => { + switch (index) { + case 0: // previous year + const preYear = new Date(date) + preYear.setFullYear(preYear.getFullYear() - 1) + pos === 0 ? (state.current = preYear) : (state.next = preYear) + break + case 1: // previous month + const preMonth = new Date(date) + preMonth.setMonth(preMonth.getMonth() - 1) + pos === 0 ? (state.current = preMonth) : (state.next = preMonth) + break + case 2: // next month + const nextMonth = new Date(date) + nextMonth.setMonth(nextMonth.getMonth() + 1) + pos === 0 ? (state.current = nextMonth) : (state.next = nextMonth) + break + case 3: // next year + const nextYear = new Date(date) + nextYear.setFullYear(nextYear.getFullYear() + 1) + pos === 0 ? (state.current = nextYear) : (state.next = nextYear) + break + } +} + +/** + * 格式化输入日期字符串 + * @param state + * @param props + * @returns + */ +export const formatValue = (state: TState, props: any) => { + const { format = 'y/MM/dd', range, rangeSpliter = '-' } = props || {} + if (range) { + if (!state.start) { + return '' + } else if(!state.end) { + return formatDate(format, state.start) + } + if(state.end < state.start) { + const end = state.end + state.end = state.start + state.start = end + } + return formatRange(format, + state.start, + state.end, + rangeSpliter + ) + } else { + if (!state.start) { + return '' + } + return formatDate(format, state.start) + } +} + +/** + * 格式化placeholder显示 + * @param props + * @returns + */ +export const formatPlaceholder = (props: any) => { + if (!props) return '' + const format = props.format || `y/MM/dd` + const sp = props.rangeSpliter || '-' + return props.range ? `${format} ${sp} ${format}` : format +} + +/** + * 输出日期选择结果 + * @param id + * @param output + */ +export const handleValue = (id: string | undefined, output: string) => { + if (id && typeof id === 'string') { + const el = document.querySelector(id) + if (el instanceof HTMLInputElement) { + el.value = output + } + } +} + +/** + * 获取绑定节点 + * @returns + */ +export const getAttachInputDom = (state: TState, props: any) => { + const { attachInputDom } = props || {} + if (!attachInputDom || typeof attachInputDom !== 'string') { + return null + } + const el = document.querySelector(attachInputDom) + if (!el) { + return null + } + state.st = false + return el +} + +/** + * 绑定弹出层场景,计算弹出层位置。 + * @param state + * @param props + * @param container + * @returns + */ +export const handlePositionFactory = (state: TState, props: any, container: Ref<Element>) => () => { + if (!state.show) { + state.x = `-100%` + state.y = `-100%` + return + } + const el = getAttachInputDom(state, props) + if (!el) { + return + } + const { left, top, width, height } = el.getBoundingClientRect() + const { width: _width, height: _height } = container.value.getBoundingClientRect() + const bottom = window.innerHeight - top - height + state.x = `${left}px` + if (bottom > top) { + state.y = `${top + height}px` + } else { + state.y = `${top - _height}px` + } +} \ No newline at end of file diff --git a/devui/date-picker/index.ts b/devui/date-picker/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..9f3c7f0e55874512d53c1609984164ead4439a84 --- /dev/null +++ b/devui/date-picker/index.ts @@ -0,0 +1,13 @@ +import { App } from 'vue' +import DatePicker from './date-picker' +import StickSlider from './stick-slider' + +export { DatePicker, StickSlider } + +export default { + install(app: App) { + DatePicker.version = '0.0.1' + app.component(DatePicker.name, DatePicker) + app.component(StickSlider.name, StickSlider) + } +} diff --git a/devui/date-picker/stick-slider/index.scss b/devui/date-picker/stick-slider/index.scss new file mode 100644 index 0000000000000000000000000000000000000000..46436f2fcd563c269f1001f3c4947253ba2d2116 --- /dev/null +++ b/devui/date-picker/stick-slider/index.scss @@ -0,0 +1,47 @@ +.devui-stick-slider { + border: 1px solid #000000; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + border-radius: 100%; + position: relative; + width: 36px; + height: 36px; + + .main-button { + width: 36px; + height: 36px; + cursor: pointer; + user-select: none; + background-color: red; + border-radius: 100%; + position: relative; + z-index: 10; + } + + .sub-buttons { + border: 1px solid #0000ff; + width: 240px; + height: 240px; + position: absolute; + display: flex; + flex-direction: row; + flex-wrap: wrap; + + .button { + width: 48px; + height: 48px; + line-height: 48px; + text-align: center; + background-color: #ffaa00; + position: relative; + margin: 5px; + cursor: pointer; + + &.selected { + background-color: #ff3300; + } + } + } +} diff --git a/devui/date-picker/stick-slider/index.tsx b/devui/date-picker/stick-slider/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d51ef64d84ea6e4c0bd6780f039cddd2da8cc951 --- /dev/null +++ b/devui/date-picker/stick-slider/index.tsx @@ -0,0 +1,58 @@ +import { defineComponent, reactive } from 'vue' + +import './index.scss' + +const StickSlider = defineComponent({ + name: 'DStickSlider', + props: {}, + setup(props) { + + const state = reactive({ + showButtons: false, + selectedIndex: 0, + }) + + const reset = () => { + state.showButtons = false + } + + const handleMainButtonMouseDown = (e: MouseEvent) => { + e.stopPropagation() + state.showButtons = true + } + const handleMainButtonMouseUp = (e: MouseEvent) => { + e.stopPropagation() + reset() + } + + return () => { + return ( + <div + class="devui-stick-slider" + onMousedown={handleMainButtonMouseDown} + onMouseup={handleMainButtonMouseUp} + onMouseleave={handleMainButtonMouseUp} + > + <div + class="sub-buttons" + style={{ display: state.showButtons ? '' : 'none' }} + > + { + Array(16).fill(null).map((_, i) => { + return (<div + class={`button ${i === state.selectedIndex ? 'selected' : ''}`} + onMouseenter={() => state.selectedIndex = i} + >{i}</div>) + }) + } + </div> + <div class="main-button"></div> + </div> + ) + } + } +}) + + + +export default StickSlider \ No newline at end of file diff --git a/devui/date-picker/utils.ts b/devui/date-picker/utils.ts new file mode 100644 index 0000000000000000000000000000000000000000..0c671520c74364cfdda997c9d3796597c2c27008 --- /dev/null +++ b/devui/date-picker/utils.ts @@ -0,0 +1,110 @@ +const getDateTime = (d: Date) => { + const year = d.getFullYear() + const month = d.getMonth() + 1 + const date = d.getDate() + const day = d.getDay() + const hour = d.getHours() + const minute = d.getMinutes() + const second = d.getSeconds() + const ms = d.getMilliseconds() + return [year, month, date, day, hour, minute, second, ms] +} + +const fixStart = (n: number, m: string, max = 2, ch = '0') => { + return (n + '').padStart(Math.min(m.length, max), ch) +} + +/** + * - y: year yy 取后2位,其他情况取4位 + * - M: month 最多取2位补0 + * @param fmt + * @param d + */ +export const formatDate = (fmt: string, d: Date) => { + const usage = getDateTime(d) + let res = fmt + res = res.replace(/y+/g, m => { + const year = usage[0] + '' + if (m.length === 2) { + return year.substring(2) + } + return year + }) + res = res.replace(/M+/g, m => fixStart(usage[1], m)) + res = res.replace(/d+/g, m => fixStart(usage[2], m)) + res = res.replace(/h+/g, m => fixStart(usage[4], m)) + res = res.replace(/m+/g, m => fixStart(usage[5], m)) + res = res.replace(/s+/g, m => fixStart(usage[6], m)) + return res +} + +export const formatRange = (fmt: string, a: Date, b: Date, conn = '-') => { + const ab = [a, b] + if(a.getTime() > b.getTime()) { + ab.reverse() + } + return `${formatDate(fmt, ab[0])} ${conn} ${formatDate(fmt, ab[1])}` +} + +/** + * 判断节点a是否在节点b中 + * @param a + * @param b + * @returns + */ +export const isIn = (a: Node | null, b: Node | null) => { + if (!b) { + return false + } + while (a) { + if (a === b) { + return true + } + a = a.parentNode + } + return false +} + +type EventItem = { el: Node | Window; cb: (...args: any[]) => any; name: string; capture: boolean; } +export class EventManager { + private readonly items: EventItem[] + constructor() { + this.items = [] + } + + append(el: Node | Window, name: string, cb: (...args: any[]) => any, capture = false) { + el.addEventListener(name, cb, capture) + this.items.push({ el, name, cb, capture }) + } + + dispose() { + this.items.splice(0, this.items.length).forEach(({ el, name, cb, capture }) => { + el.removeEventListener(name, cb, capture) + }) + } +} + +export const traceNode = (el: Node) => { + const els: Node[] = [], name = 'scroll' + while (el.parentNode) { + els.push(el.parentNode) + el = el.parentNode + } + return els +} + +/** + * 函数安全调用 + */ +export const invokeFunction = (fn: any, ...args: any[]) => { + if (typeof fn === 'function') { + fn(...args) + } +} + +export const getMinDate = (a?: Date, b?: Date) => { + if(a && b) { + return a > b ? b : a + } + return a || b || undefined +} \ No newline at end of file diff --git a/devui/loading/__tests__/loading.spec.ts b/devui/loading/__tests__/loading.spec.ts index be8b2b5fb731299bc7b61c1752d13d77916ae039..bc4ad4faf14cd2746d06d4ca9040c53affc2cc80 100644 --- a/devui/loading/__tests__/loading.spec.ts +++ b/devui/loading/__tests__/loading.spec.ts @@ -57,8 +57,9 @@ describe('Loading as directive', () => { const loadingPType = wrapper.find('#testLoading') expect(loadingPType).toBeTruthy() - // @ts-ignore - const targetEle = loadingPType.wrapperElement.instance.vnode.el + // @_ts-ignore + // 不支持`ts-ignore`,强行修改确保eslint通过。@mrundef-210810 + const targetEle = (loadingPType as any).wrapperElement.instance.vnode.el expect(targetEle.parentNode.style.position).toEqual('absolute') }) @@ -175,11 +176,11 @@ describe('Loading as directive', () => { </div> `, setup() { - let promises: any = shallowReactive({ + const promises: any = shallowReactive({ value: [] }) const fetchMutiplePromise = () => { - let list = [] + const list = [] for (let i = 0; i < 3; i++) { list.push(new Promise((res: any) => { res(true) @@ -213,13 +214,13 @@ describe('Loading as Service', () => { const loading = LoadingService.open() await nextTick() - let ele = document.querySelector('.devui-loading-contanier') + const ele = document.querySelector('.devui-loading-contanier') expect(ele).toBeTruthy() expect(ele.parentNode == document.body).toBe(true) loading.loadingInstance.close() await nextTick() - let ele2 = document.querySelector('.devui-loading-contanier') + const ele2 = document.querySelector('.devui-loading-contanier') expect(ele2).toBe(null) }) @@ -232,7 +233,7 @@ describe('Loading as Service', () => { }) await nextTick() - let ele = document.querySelector('.devui-loading-contanier') + const ele = document.querySelector('.devui-loading-contanier') expect(ele).toBeTruthy() expect(ele.parentNode === div).toBe(true) @@ -245,7 +246,7 @@ describe('Loading as Service', () => { }) await nextTick() - let ele = document.querySelector('.devui-loading-contanier') + const ele = document.querySelector('.devui-loading-contanier') expect(ele).toBeTruthy() expect(ele.textContent).toBe('正在加载中...') @@ -263,14 +264,16 @@ describe('Loading as Service', () => { }) await nextTick() - let ele = document.querySelector('.devui-loading-contanier') + const ele = document.querySelector('.devui-loading-contanier') expect(ele).toBeTruthy() - // @ts-ignore - expect(ele.parentNode.style.position).toBe('absolute') - - let loadingEle = ele.querySelector('.devui-loading-area') - // @ts-ignore - const style = loadingEle.style + // @_ts-ignore + // 不支持`ts-ignore`,强行修改确保eslint通过。@mrundef-210810 + expect((ele.parentNode as any).style.position).toBe('absolute') + + const loadingEle = ele.querySelector('.devui-loading-area') + // @_ts-ignore + // 不支持`ts-ignore`,强行修改确保eslint通过。@mrundef-210810 + const style = (loadingEle as any).style expect(style.top).toBe('40%') expect(style.left).toBe('60%') expect(style.zIndex).toBe('1000') diff --git a/devui/loading/src/directive.ts b/devui/loading/src/directive.ts index a99daebe5bd1907060acae378e30d4124c20ea88..5a2b02ad5139543c4d5135f00f4c80dddc096604 100644 --- a/devui/loading/src/directive.ts +++ b/devui/loading/src/directive.ts @@ -66,8 +66,11 @@ const toggleLoading = (el: TargetHTMLElement, binding: BindingType) => { cacheInstance.add(el) if (vals) { - Promise.all(vals).then((res: Array<boolean>) => { - }).catch((err: any) => { + Promise.all(vals) + // eslint不允许空的then执行体 @mrundef-210810 + // .then((res: Array<boolean>) => { + // }) + .catch((err: any) => { console.error(new Error('Promise handling errors'), err) }).finally(() => { unmount(el) @@ -93,7 +96,7 @@ const handleProps = (el: TargetHTMLElement, vprops: LoadingProps) => { ...vprops } - let loadingTemplateRef = props.loadingTemplateRef + const loadingTemplateRef = props.loadingTemplateRef const loadingInstance = createComponent( loadingConstructor, @@ -128,8 +131,8 @@ const loadingDirective = { toggleLoading(el, binding) }, - - unmounted: function () { } + // eslint不允许控的unmounted执行体 + // unmounted: function () { } } export default loadingDirective diff --git a/devui/loading/src/service.ts b/devui/loading/src/service.ts index 2c8642de85c211ba0d2dafe2d2a8b111c9d19e01..2794dc1f6fbc9690a27eaf7689d058b750e4b4cf 100644 --- a/devui/loading/src/service.ts +++ b/devui/loading/src/service.ts @@ -40,9 +40,12 @@ const loading = { const close = instance.proxy.close instance.loadingInstance = instance.proxy - instance.loadingInstance.close = () => { + instance.loadingInstance.close = (...args: any[]) => { cacheTarget.delete(parent) - close.apply(null, arguments) + // 1. 箭头函数内部并没有内置arguments对象 @mrundef-210810 + // 2. 如果没有上下文要求`apply(null)`,不必使用apply/call + // close.apply(null, arguments) + close(...args) } return instance diff --git a/sites/components/date-picker/index.md b/sites/components/date-picker/index.md new file mode 100644 index 0000000000000000000000000000000000000000..bd1253b0fba24592c058fea584016ae1a81a53dd --- /dev/null +++ b/sites/components/date-picker/index.md @@ -0,0 +1,177 @@ +<style lang="scss"> +.devui-datepicker-demo { + margin: 10px 0px; + padding: 10px 0px; + + label { + border: 1px solid #aaa; + padding: 0px 5px; + height: 2em; + line-height: 2em; + display: inline-block; + margin: 5px; + font-size: 14px; + border-radius: 5px; + user-select: none; + cursor: pointer; + + input[type=checkbox] { + transform: translateY(1px); + margin-left: 3px; + } + } + + .input-binder { + width: 300px; + padding: 5px; + font-size: 16px; + border-radius: 5px; + } +} +</style> + +<script lang="ts"> +import { defineComponent, ref } from 'vue' +export default defineComponent({ + setup() { + const range = ref<boolean>(false) + const rangeSwitch = () => range.value = !range.value + + const range2 = ref<boolean>(false) + const rangeSwitch2 = () => range2.value = !range2.value + + const showTime = ref<boolean>(false) + const showTimeSwitch = () => showTime.value = !showTime.value + + const spliter = ref<boolean>('-') + const setSpliter = (v: string) => spliter.value = v + + const handleRangeChange = (e: Event) => { + const { selectedIndex, value } = e.target + setSpliter(value) + } + + return { + range, + rangeSwitch, + range2, + rangeSwitch2, + showTime, + showTimeSwitch, + spliter, + setSpliter, + handleRangeChange, + } + } +}) +</script> + +# 扩展 + +<div style="margin:100px;"> + <d-stick-slider /> +</div> + + +# DatePicker 日期选择器 + +日期、时间可视化输入。 + +### 作为UI组件 + +--------- + + +#### 最简 + +<section class="devui-datepicker-demo"> + <d-datepicker /> +</section> + +```jsx +<d-datepicker /> +``` +#### 区域选择 + +<section class="devui-datepicker-demo"> + <d-datepicker range /> +</section> + +```jsx +<d-datepicker range /> +``` + +### 区间限制 + +<section class="devui-datepicker-demo"> + <d-datepicker date-min="2021-8-9" date-max="2021-9-20" /> +</section> + +```jsx +<d-datepicker date-min="2021-8-9" date-max="2021-9-20" /> +``` + +<section class="devui-datepicker-demo"> + <d-datepicker range date-min="2021-8-9" date-max="2022-3-20" /> +</section> + +```jsx +<d-datepicker range date-min="2021-8-9" date-max="2022-3-20" /> +``` + + +### 绑定原生`<input>` + +暂定通过`querySelector`查找节点,绑定真实`dom`节点。此方案待定。 + +```jsx +<section class="devui-datepicker-demo"> + <input class="input-binder" id="datepicker-input" /> + <d-datepicker attach-input-dom="#datepicker-input" /> +</section> +``` + +<section class="devui-datepicker-demo"> + <input class="input-binder" id="datepicker-input" /> + <label>分隔符 + <select @change="handleRangeChange" :disabled="!range2"> + <option>-</option> + <option>~</option> + <option>--</option> + <option>~</option> + <option>***</option> + </select> + </label> + <d-datepicker attach-input-dom="#datepicker-input" range :range-spliter="spliter" /> +</section> + +### Scroll位置跟踪 + +在对宿主`<input>`绑定的过程中,对宿主的父级节点做了`scroll`追踪;当任何一级发生`scroll`时,都会更新弹出层位置,确保能让弹出层与`<input>`正确贴合。 + +这个做法是根据`ng`的组件效果实现的。 + +TODO: 跟踪节流。 + +<br /> +<br /> +<br /> +<br /> +<br /> +<br /> +<br /> +<br /> +<br /> +<br /> +<br /> +<br /> +<br /> +<br /> +<br /> +<br /> +<br /> +<br /> +<br /> +<br /> +<br /> + diff --git a/yarn.lock b/yarn.lock index 95a75658aeb3c155f5d5e256be4ace1ff82357c8..2e22957cdff1b5a09aac2426ec8de02eefaa98ad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1202,8 +1202,8 @@ "@devui-design/icons@^1.3.0": version "1.3.0" - resolved "https://registry.nlark.com/@devui-design/icons/download/@devui-design/icons-1.3.0.tgz#5a3006a31ee4f62e3f9837b68c031898ff148b88" - integrity sha1-WjAGox7k9i4/mDe2jAMYmP8Ui4g= + resolved "https://registry.npmjs.org/@devui-design/icons/-/icons-1.3.0.tgz#5a3006a31ee4f62e3f9837b68c031898ff148b88" + integrity sha512-eg9PcDXYn1BaUCo6qP7dszFJ4uRycxASLdKzpz27AW34HbHx4kFce36Fhfr2niRjGKYllcMgYe7hgP5KYVOZ8Q== "@docsearch/css@^1.0.0-alpha.28": version "1.0.0-alpha.28"