Przeglądaj źródła

feat: add calendar code

yhhu 5 lat temu
rodzic
commit
83adb7fcd3

+ 1 - 0
.eslintrc

@@ -34,6 +34,7 @@
       "error",
       "never"
     ],
+    "no-plusplus": ["error", { "allowForLoopAfterthoughts": true }],
     "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }],
     "react/require-extension": "off",
     "arrow-parens": ["error", "as-needed"],

+ 17 - 0
src/components/calendar/Header.js

@@ -0,0 +1,17 @@
+import React from 'react'
+import Styles from './header.css'
+
+const Header = () => (
+  <div className={Styles.wrapper}>
+    <i className={Styles.prevYear} role="button" title="上一年" />
+    <i className={Styles.prevMonth} role="button" title="上一月" />
+    <div className={Styles.text}>
+      <span className={Styles.link}>2018年</span>
+      <span className={Styles.link}>9月</span>
+    </div>
+    <i className={Styles.nextMonth} role="button" title="下一月" />
+    <i className={Styles.nextYear} role="button" title="下一年" />
+  </div>
+)
+
+export default Header

+ 97 - 0
src/components/calendar/Index.js

@@ -0,0 +1,97 @@
+import React from 'react'
+import classNames from 'classnames'
+import PropTypes from 'prop-types'
+import Styles from './index.css'
+import {
+  getDaysOfMonth,
+  getWeekSort,
+  selectDayByIndex,
+} from '../../helper'
+import {
+  PREV_DAY, NEXT_DAY, _,
+} from '../../const'
+
+class Index extends React.Component {
+  constructor(props) {
+    super(props)
+
+    const { value } = this.props
+    this.state = {
+      weekTags: [],
+      days: [],
+      /* eslint-disable react/no-unused-state */
+      selectedDay: value,
+    }
+  }
+
+  static getDerivedStateFromProps(props, state) {
+    if (props.model !== state.prevModel) {
+      return {
+        prevModel: props.model,
+        weekTags: getWeekSort(props.model),
+        days: getDaysOfMonth(_, _, props.model),
+      }
+    }
+
+    if (props.value !== state.selectedDay) {
+      return { ...state, selectedDay: props.selectedDay }
+    }
+
+    return state
+  }
+
+  selectDay(day, index) {
+    const { onSelectDay } = this.props
+    const { days } = this.state
+    const selectedDays = selectDayByIndex(days, index)
+    // console.log(days)
+    // console.log(index)
+    // console.log(selectedDays)
+    this.setState({ days: selectedDays }, () => {
+      onSelectDay(day)
+    })
+  }
+
+  render() {
+    const { weekTags, days } = this.state
+
+    return (
+      <div className={Styles.wrapper}>
+        { weekTags.map(weekName => (
+          <span
+            className={`${Styles.normal} ${Styles.week}`}
+            title={`星期${weekName}`}
+            key={weekName}
+          >
+            { weekName }
+          </span>
+        )) }
+        {
+          days.map((day, index) => (
+            <span
+              className={classNames(Styles.normal, {
+                [Styles.prev]: day.tag === PREV_DAY,
+                [Styles.next]: day.tag === NEXT_DAY,
+                [Styles.current]: day.current,
+                [Styles.selected]: day.selected,
+              })}
+              title={day.full}
+              key={day.full}
+              onClick={() => this.selectDay(day, index)}
+              role="presentation"
+            >
+              { day.day }
+            </span>
+          ))
+        }
+      </div>
+    )
+  }
+}
+
+Index.propTypes = {
+  value: PropTypes.string.isRequired,
+  onSelectDay: PropTypes.func.isRequired,
+}
+
+export default Index

+ 71 - 0
src/components/calendar/header.css

@@ -0,0 +1,71 @@
+.wrapper {
+  width: 100%;
+  display: grid;
+  grid-template-columns: 25px 18px auto 18px 25px;
+  grid-auto-rows: 34px;
+  border-bottom: 1px solid #e8e8e8;
+}
+.wrapper > i {
+  cursor: pointer;
+}
+.prev-year {
+  margin-left: 7px;
+}
+.prev-year::after {
+  content: '\AB';
+  display: block;
+  width: 100%;
+  height: 100%;
+  font-style: normal;
+  line-height: 34px;
+  text-align: center;
+  color: rgba(0, 0, 0, 0.45);
+  font-size: 16px;
+}
+.prev-month::after {
+  content: '\2039';
+  display: block;
+  width: 100%;
+  height: 100%;
+  font-style: normal;
+  line-height: 34px;
+  text-align: center;
+  color: rgba(0, 0, 0, 0.45);
+  font-size: 16px;
+}
+.next-year {
+  margin-right: 7px;
+}
+.next-year::after {
+  content: '\BB';
+  display: block;
+  width: 100%;
+  height: 100%;
+  font-style: normal;
+  line-height: 34px;
+  text-align: center;
+  color: rgba(0, 0, 0, 0.45);
+  font-size: 16px;
+}
+.next-month::after {
+  content: '\203A';
+  display: block;
+  width: 100%;
+  height: 100%;
+  font-style: normal;
+  line-height: 34px;
+  text-align: center;
+  color: rgba(0, 0, 0, 0.45);
+  font-size: 16px;
+}
+.text {
+  font-weight: 500;
+  line-height: 34px;
+  text-align: center;
+}
+.link {
+  display: inline-block;
+  cursor: pointer;
+  margin: 0 3px;
+  letter-spacing: 1px;
+}

+ 41 - 0
src/components/calendar/index.css

@@ -0,0 +1,41 @@
+.wrapper {
+  padding: 7px;
+  display: grid;
+  grid-template-columns: repeat(7, 1fr);
+  grid-template-rows: repeat(7, 30px);
+  border-bottom: 1px solid #e8e8e8;
+}
+.normal {
+  line-height: 24px;
+  text-align: center;
+  cursor: pointer;
+  color: rgba(0, 0, 0, 0.65);
+  margin: 3px 7px;
+  border: 1px solid transparent;
+}
+.normal:hover {
+  background-color: #e6f7ff;
+}
+.week:hover {
+  background-color: #fff;
+  cursor: default;
+}
+.next, .prev {
+  color: rgba(0, 0, 0, 0.25);
+}
+.current {
+  border-color: #1890ff;
+  font-weight: bold;
+  color: #1890ff;
+  border-radius: 2px;
+}
+.selected {
+  background-color: #1890ff;
+  color: #fff;
+  border: 1px solid transparent;
+  font-weight: bold;
+  border-radius: 2px;
+}
+.selected:hover {
+  background-color: #1890ff;
+}

+ 21 - 0
src/components/footer/Footer.js

@@ -0,0 +1,21 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import Styles from './footer.css'
+import { CHINESE_MODEL } from '../../const'
+
+const Footer = ({ model, onChangeModel }) => (
+  <div className={Styles.wrapper}>
+    <div />
+    <div className={Styles.today}><span>今天</span></div>
+    <div role="presentation" className={Styles.lang} onClick={() => onChangeModel(model)}>
+      <span>{ model === CHINESE_MODEL ? '中' : '西' }</span>
+    </div>
+  </div>
+)
+
+Footer.propTypes = {
+  model: PropTypes.string.isRequired,
+  onChangeModel: PropTypes.func.isRequired,
+}
+
+export default Footer

+ 28 - 0
src/components/footer/footer.css

@@ -0,0 +1,28 @@
+.wrapper {
+  display: grid;
+  grid-template-columns: 50px auto 50px;
+  grid-auto-rows: 38px;
+}
+.lang {
+  text-align: center;
+}
+.lang > span {
+  display: inline-block;
+  width: 20px;
+  height: 20px;
+  color: #fff;
+  background-color: #1890ff;
+  border-radius: 2px;
+  font-size: 12px;
+  line-height: 22px;
+  margin-top: 8px;
+  cursor: pointer;
+}
+.today {
+  line-height: 38px;
+  text-align: center;
+}
+.today > span {
+  color: #1890ff;
+  cursor: pointer;
+}

+ 30 - 7
src/components/index/DatePicker.js

@@ -2,7 +2,8 @@ import React, { Component } from 'react'
 import PropTypes from 'prop-types'
 import Styles from './picker.css'
 import { getDateFormatFromSepecificDate } from '../../utils'
-import Modal from './Modal'
+import Modal from '../modal/Modal'
+import { CHINESE_MODEL, WESTERN_MODEL } from '../../const'
 
 class DatePicker extends Component {
   constructor(props) {
@@ -11,12 +12,15 @@ class DatePicker extends Component {
     this.state = {
       value: defaultDate,
       showModal: false,
+      model: CHINESE_MODEL,
     }
 
     this.onModalOpen = this.onModalOpen.bind(this)
     this.onInputChange = this.onInputChange.bind(this)
     this.onInputClear = this.onInputClear.bind(this)
     this.onModalClose = this.onModalClose.bind(this)
+    this.onChangeModel = this.onChangeModel.bind(this)
+    this.onSelectDay = this.onSelectDay.bind(this)
   }
 
   onModalOpen() {
@@ -24,17 +28,29 @@ class DatePicker extends Component {
   }
 
   onModalClose() {
-    this.setState({ showModal: false })
+    // this.setState({ showModal: false })
   }
 
-  onInputChange() {
-    console.log('onInputChange')
+  onInputChange(event) {
+    this.setState({ value: event.target.value })
   }
 
   onInputClear() {
     this.setState({ value: '', showModal: false })
   }
 
+  onChangeModel(model) {
+    if (model === CHINESE_MODEL) {
+      this.setState({ model: WESTERN_MODEL })
+    } else {
+      this.setState({ model: CHINESE_MODEL })
+    }
+  }
+
+  onSelectDay(day) {
+    this.setState({ value: day.full })
+  }
+
   render() {
     const { inline } = this.props
     const { value, showModal } = this.state
@@ -47,10 +63,10 @@ class DatePicker extends Component {
         >
           <input
             type="text"
-            placeholder="选择日期"
+            placeholder="选择日期"
             className={Styles.input}
             value={value}
-            onChange={this.onInputChange}
+            onChange={e => this.onInputChange(e)}
             onFocus={this.onModalOpen}
             onBlur={this.onModalClose}
           />
@@ -62,7 +78,14 @@ class DatePicker extends Component {
           />
           <div className={Styles.line} />
         </div>
-        <Modal isMounted={showModal} delayTime={200} />
+        <Modal
+          isMounted={showModal}
+          delayTime={200}
+          onInputChange={this.onInputChange}
+          onChangeModel={this.onChangeModel}
+          onSelectDay={this.onSelectDay}
+          {...this.state}
+        />
       </div>
     )
   }

+ 0 - 27
src/components/index/Modal.js

@@ -1,27 +0,0 @@
-import React from 'react'
-import classNames from 'classnames'
-import PropTypes from 'prop-types'
-import Styles from './modal.css'
-import delayUnmounting from '../delayUnmounting'
-
-const Modal = ({ isMounted }) => (
-  <React.Fragment>
-    <div className={classNames(Styles.container, {
-      [Styles.in]: isMounted,
-      [Styles.out]: !isMounted,
-    })}
-    >
-      modal
-    </div>
-  </React.Fragment>
-)
-
-Modal.defaultProps = {
-  isMounted: false,
-}
-
-Modal.propTypes = {
-  isMounted: PropTypes.bool,
-}
-
-export default delayUnmounting(Modal)

+ 3 - 0
src/components/index/picker.css

@@ -1,3 +1,6 @@
+* {
+  box-sizing: border-box;
+}
 .wrapper {
   font-family: "Chinese Quote", -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
   min-width: 170px;

+ 38 - 0
src/components/input/Input.js

@@ -0,0 +1,38 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import Styles from './input.css'
+
+class Input extends React.Component {
+  constructor(props) {
+    super(props)
+    this.textInput = React.createRef()
+  }
+
+  componentDidMount() {
+    this.textInput.current.focus()
+  }
+
+  render() {
+    const { value, onInputChange } = this.props
+
+    return (
+      <div className={Styles.wrapper}>
+        <input
+          ref={this.textInput}
+          className={Styles.input}
+          type="text"
+          placeholder="请选择日期"
+          value={value}
+          onChange={e => onInputChange(e)}
+        />
+      </div>
+    )
+  }
+}
+
+Input.propTypes = {
+  value: PropTypes.string.isRequired,
+  onInputChange: PropTypes.func.isRequired,
+}
+
+export default Input

+ 14 - 0
src/components/input/input.css

@@ -0,0 +1,14 @@
+.wrapper {
+  width: 100%;
+}
+.input {
+  display: inline-block;
+  outline: none;
+  border: none;
+  height: 34px;
+  width: 100%;
+  padding: 6px 10px;
+  font-size: 14px;
+  color: rgba(0, 0, 0, 0.65);
+  border-bottom: 1px solid #e8e8e8;
+}

+ 57 - 0
src/components/modal/Modal.js

@@ -0,0 +1,57 @@
+import React from 'react'
+import classNames from 'classnames'
+import PropTypes from 'prop-types'
+import Styles from './modal.css'
+import delayUnmounting from '../delayUnmounting'
+import Input from '../input/Input'
+import Header from '../calendar/Header'
+import Body from '../calendar/Index'
+import Footer from '../footer/Footer'
+
+const Modal = ({
+  isMounted,
+  value,
+  // showModal,
+  onInputChange,
+  onChangeModel,
+  onSelectDay,
+  model,
+}) => (
+  <React.Fragment>
+    <div className={classNames(Styles.container, {
+      [Styles.in]: isMounted,
+      [Styles.out]: !isMounted,
+    })}
+    >
+      <Input
+        value={value}
+        onInputChange={onInputChange}
+      />
+      <div className="calendar">
+        <Header />
+        <Body
+          model={model}
+          value={value}
+          onSelectDay={onSelectDay}
+        />
+      </div>
+      <Footer model={model} onChangeModel={onChangeModel} />
+    </div>
+  </React.Fragment>
+)
+
+Modal.defaultProps = {
+  isMounted: false,
+}
+
+Modal.propTypes = {
+  isMounted: PropTypes.bool,
+  value: PropTypes.string.isRequired,
+  // showModal: PropTypes.bool.isRequired,
+  onInputChange: PropTypes.func.isRequired,
+  model: PropTypes.string.isRequired,
+  onChangeModel: PropTypes.func.isRequired,
+  onSelectDay: PropTypes.func.isRequired,
+}
+
+export default delayUnmounting(Modal)

+ 2 - 2
src/components/index/modal.css

@@ -50,12 +50,12 @@
 
 .container {
   width: 280px;
-  height: 200px;
   position: absolute;
   top: 0;
   left: 0;
-  background-color: aqua;
+  background-color: white;
   z-index: 1;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
 }
 .in {
   animation: slideDownIn 0.2s;

+ 8 - 0
src/const/index.js

@@ -1 +1,9 @@
 export * from './week'
+
+export const CHINESE_MODEL = 'CHINESE_MODEL'
+export const WESTERN_MODEL = 'WESTERN_MODEL'
+export const _ = undefined
+
+export const PREV_DAY = 'PREV_DAY'
+export const NEXT_DAY = 'NEXT_DAY'
+export const CURRENT_DAY = 'CURRENT_DAY'

+ 107 - 0
src/helper.js

@@ -0,0 +1,107 @@
+import {
+  weekMap, CHINESE_MODEL, PREV_DAY, CURRENT_DAY, NEXT_DAY,
+} from './const'
+import {
+  getWeekOfMonth,
+  getCurrentYear,
+  getCurrentMonth,
+  getDaysCountOfMonth,
+  formatMonthOrDay,
+  isCurrentDay,
+} from './utils'
+
+export const getWeekSort = (model = CHINESE_MODEL) => {
+  const values = [...weekMap.values()]
+
+  if (model === CHINESE_MODEL) {
+    values.splice(0, 1)
+    values.push(weekMap.get(0))
+  }
+
+  return values
+}
+
+const getPrevLeftDays = (firstDay, model) => {
+  let leftCount = firstDay
+  // 如果是星期日
+  // 正常中方日历以周日结尾
+  // 西方以星期日作为第一天
+  if (+leftCount === 0) {
+    leftCount = model !== CHINESE_MODEL ? 0 : 6
+  } else {
+    leftCount = model !== CHINESE_MODEL ? leftCount : leftCount - 1
+  }
+
+  return leftCount
+}
+
+const getFullDays = (year, month, day, tag = CURRENT_DAY) => {
+  const current = isCurrentDay(year, month, day)
+  return {
+    tag: tag,
+    day: day,
+    full: `${year}-${formatMonthOrDay(month)}-${formatMonthOrDay(day)}`,
+    current: current,
+    selected: current,
+  }
+}
+
+const getPrevMonthLeftDays = (year, month, firstDay, model) => {
+  let prevYear = year
+  let prevMonth = month
+  const leftCount = getPrevLeftDays(firstDay, model)
+  if (+month === 1) {
+    prevYear -= 1
+    prevMonth = 12
+  }
+
+  prevMonth -= 1
+
+  const prevDays = []
+  const prevMonthDays = getDaysCountOfMonth(prevMonth, prevYear)
+
+  for (let i = 0; i < leftCount; i++) {
+    prevDays.unshift(getFullDays(prevYear, prevMonth, prevMonthDays - i, PREV_DAY))
+  }
+
+  return prevDays
+}
+
+const getNextMonthLeftDays = (year, month, days, firstDay, model) => {
+  let nextYear = year
+  let nextMonth = month
+  const leftCount = getPrevLeftDays(firstDay, model)
+  if (+month === 12) {
+    nextYear += 1
+    nextMonth = 1
+  }
+
+  nextMonth += 1
+  const nextDays = []
+  const nextLefts = 6 * 7 - (leftCount + days)
+  for (let i = 0; i < nextLefts; i++) {
+    nextDays.push(getFullDays(nextYear, nextMonth, i + 1, NEXT_DAY))
+  }
+
+  return nextDays
+}
+
+export const getDaysOfMonth = (year = getCurrentYear(),
+  month = getCurrentMonth(), model = CHINESE_MODEL) => {
+  const firstDayOfMonth = getWeekOfMonth(month, year)
+  const days = getDaysCountOfMonth(month, year)
+  const currentDaysArr = []
+  const prevDaysArr = getPrevMonthLeftDays(year, month, firstDayOfMonth, model)
+  const nextDaysArr = getNextMonthLeftDays(year, month, days, firstDayOfMonth, model)
+
+  for (let i = 1; i <= days; i++) {
+    currentDaysArr.push(getFullDays(year, month, i))
+  }
+  return prevDaysArr.concat(currentDaysArr).concat(nextDaysArr)
+}
+
+export const selectDayByIndex = (days, index) => days.map((day, idx) => {
+  const tempDay = day
+  tempDay.selected = index === idx
+  return tempDay
+})

+ 19 - 4
src/utils/date.js

@@ -37,8 +37,8 @@ export const getCurrentDate = () => `${getCurrentYear()}-${getCurrentMonth()}-${
  * 格式化月份或者天数
  * @param {String} dateStr 月份或者天数
  */
-const formatMonthOrDay = dateStr => {
-  if (dateStr.length === 2) {
+export const formatMonthOrDay = dateStr => {
+  if ((`${dateStr}`).length === 2) {
     return dateStr
   }
 
@@ -123,8 +123,9 @@ export const getDaysCountOfMonth = (month = getCurrentMonth(),
  * @param {String} month 月份
  * @param {String} year 年份
  */
-export const getWeekOfMonth = (month = getCurrentMonth(),
-  year = getCurrentYear()) => new Date(year, month).getDay()
+export const getWeekOfMonth = (month = getCurrentMonth(), year = getCurrentYear()) => (
+  new Date(`${year}, ${month}, 01`).getDay()
+)
 
 /**
  * 获取一月中的第一天是星期几
@@ -135,3 +136,17 @@ export const getWeekNameOfMonth = (month = getCurrentMonth(), year = getCurrentY
   const dayNumber = getWeekOfMonth(month, year)
   return weekMap.get(dayNumber)
 }
+
+/**
+ * 是否为当前天
+ * @param {Sring/Number} year 年份
+ * @param {Sring/Number} month 月份
+ * @param {Sring/Number} day 天
+ */
+export const isCurrentDay = (year, month, day) => {
+  const currentYear = getCurrentYear()
+  const currentMonth = getCurrentMonth()
+  const currentDay = getCurrentDay()
+  /* eslint-disable eqeqeq */
+  return (currentYear == year) && (currentMonth == month) && (currentDay == day)
+}