hoony's web study

728x90
반응형

qCalendarDoc : https://qcalendar.netlify.app/

 

QCalendar Docs

 

qcalendar.netlify.app

 

qCalendar를 이용해 DatePicker를 만들어봤다.

 디자인은 tistory에서 사용하는 DatePicker를 참고해서 만들었다.

tistory에서 제공하는 DatePicker

 

 

사용 버전

"nuxt": "3.3.1",

"@quasar/quasar-ui-qcalendar": "^4.0.0-beta.15",

"quasar": "^2.12.0",

 

 

QCalendar Setting

 

아래 방법은 Nuxt3에서의 setting 방법입니다.
Vue3를 사용할 경우 https://qcalendar.netlify.app/all-about-qcalendar/installation-types#Vue-CLI-or-Vite 을 참고해주세요.~

 

/plugins/q-calendar.js

import Plugin from '@quasar/quasar-ui-qcalendar/src/QCalendarDay.js';
import '@quasar/quasar-ui-qcalendar/src/css/calendar-day.sass';

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.use(Plugin, {});
});

 

/nuxt.config.ts

export default defineNuxtConfig({

  ...
  
  plugins: [
    {
      src: '~/plugins/q-calendar',
      mode: 'client',
    },
  ],
})

 

 

DatePicker

 

 

한 컴포넌트로 만들기에 너무 크고, 나중에 내부달력부분은 다른 컴포넌트를 만들 때 또 사용할 수 있을 것 같아서 분리해서 작업했다.

 

사용 방법

 

DateSelectBox 

name 설명 default
(slot) DateSelectBox의 버튼 element 작성,
#default로 선택한날짜 시작과 끝, option 등을 { from, to, option, preview }로 받아올 수 있다.
(없음)
v-model 선택한 날짜의 시작, 끝을 가져올 수 있으며, 초기값을 세팅할 수 있다. (없음)
predoList 날짜 자동완성 List [
    {
      name: '오늘',
      from: new Date(),
      to: new Date(),
    },
]

 

<template>
  <ClientOnly>
    <DateSelectBox v-model="pickDate" :predo-list="predoList">
      <template #default="{ from, to, option, preview }">
        <div class="selected-date">
          <div class="option">{{ option.name }}</div>
          <div class="date">{{ preview }}<q-icon icon="mdi-calendar" class="ml-2"></q-icon></div>
        </div>
      </template>
    </DateSelectBox>
  </ClientOnly>
</template>

<script setup>
import DateSelectBox from '~/components/DateSelectBox.vue';
import { date } from 'quasar';

const newDate = new Date();
const yesterday = new Date();
const lastWeek = new Date();
const lastMonth = new Date();
yesterday.setDate(newDate.getDate() - 1);
lastWeek.setDate(newDate.getDate() - 6);
lastMonth.setDate(newDate.getDate() - 29);

const pickDate = ref({
  from: new Date(),
  to: new Date(),
});

const predoList = [
  {
    name: '오늘',
    from: date.formatDate(new Date(), 'YYYY-MM-DD'),
    to: date.formatDate(new Date(), 'YYYY-MM-DD'),
  },
  {
    name: '어제',
    from: date.formatDate(yesterday, 'YYYY-MM-DD'),
    to: date.formatDate(yesterday, 'YYYY-MM-DD'),
  },
  {
    name: '최근 7일',
    from: date.formatDate(lastWeek, 'YYYY-MM-DD'),
    to: date.formatDate(new Date(), 'YYYY-MM-DD'),
  },
  {
    name: '최근 30일',
    from: date.formatDate(lastMonth, 'YYYY-MM-DD'),
    to: date.formatDate(new Date(), 'YYYY-MM-DD'),
  },
  {
    name: '직접 입력',
    from: null,
    to: null,
  },
];
</script>

<style lang="scss" scoped>
.selected-date {
  display: inline-flex;
  border: 1px solid #000000;
  border-radius: 4px;
  line-height: 36px;
  cursor: pointer;
  .option {
    padding: 0 10px;
    min-width: 100px;
  }
  .date {
    padding: 0 10px;
    border-left: 1px solid #ccc;
  }
}
</style>

 

 

MultiMonthSelection.js 

https://qcalendar.netlify.app/examples/mini-mode-multi-month-selection 에서 스타일과 요일을 수정했다.

<template>
  <div class="multi-month-selection">
    <div class="btn-wrap">
      <button @click="onPrev"><q-icon name="chevron_left" size="2em" /></button>
      <button @click="onNext"><q-icon name="chevron_right" size="2em" /></button>
    </div>
    <div style="display: flex; justify-content: center; align-items: center; flex-wrap: nowrap">
      <div class="calender-wrap">
        <div>
          <p class="text-center">{{ selectedDate1.slice(0, 7).replace('-', '. ') }}</p>
          <q-calendar-month
            ref="calendar1"
            v-model="selectedDate1"
            mini-mode
            no-active-date
            :hover="canHover"
            :selected-start-end-dates="startEndDates"
            :min-weeks="6"
            animated
            :disabled-after="disabledAfter"
            no-outside-days
            @mousedown-day="onMouseDownDay"
            @mouseup-day="onMouseUpDay"
            @mousemove-day="onMouseMoveDay"
            @change="onChange"
            @moved="onMoved"
            @click-day="onClickDay"
            @click-workweek="onClickWorkweek"
            @click-head-workweek="onClickHeadWorkweek"
            @click-head-day="onClickHeadDay"
          >
            <template #head-day="{ scope }">
              {{ WEEKDAY[scope.weekday] }}
            </template>
          </q-calendar-month>
        </div>
        <div>
          <p class="text-center">{{ selectedDate2.slice(0, 7).replace('-', '. ') }}</p>
          <q-calendar-month
            ref="calendar2"
            v-model="selectedDate2"
            mini-mode
            no-active-date
            :hover="canHover"
            :selected-start-end-dates="startEndDates"
            :min-weeks="6"
            animated
            no-outside-days
            :disabled-after="disabledAfter"
            @mousedown-day="onMouseDownDay"
            @mouseup-day="onMouseUpDay"
            @mousemove-day="onMouseMoveDay"
            @change="onChange"
            @moved="onMoved"
            @click-date="onClickDate"
            @click-day="onClickDay"
            @click-workweek="onClickWorkweek"
            @click-head-workweek="onClickHeadWorkweek"
            @click-head-day="onClickHeadDay"
          >
            <template #head-day="{ scope }">
              {{ WEEKDAY[scope.weekday] }}
            </template>
          </q-calendar-month>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { QCalendarMonth, addToDate, getDayIdentifier, parseTimestamp, today } from '@quasar/quasar-ui-qcalendar/src/index.js';
import '@quasar/quasar-ui-qcalendar/src/QCalendarVariables.sass';
import '@quasar/quasar-ui-qcalendar/src/QCalendarTransitions.sass';
import '@quasar/quasar-ui-qcalendar/src/QCalendarMonth.sass';

function leftClick(e) {
  return e.button === 0;
}
const WEEKDAY = ['일', '월', '화', '수', '목', '금', '토'];
const emit = defineEmits(['update:modelValue', 'click-date']);
const props = defineProps({
  modelValue: Object,
});

const disabledAfter = computed(() => {
  let ts = parseTimestamp(today());
  ts = addToDate(ts, { day: 1 });
  return ts.date;
});

const selectedDate1 = ref(today()),
  selectedDate2 = ref(today()),
  calendar1 = ref(null),
  calendar2 = ref(null),
  anchorTimestamp = ref(null),
  otherTimestamp = ref(null),
  mouseDown = ref(false),
  mobile = ref(true),
  hover = ref(false);

const canHover = computed(() => {
  return hover.value === true && mouseDown.value === true;
});

const startEndDates = computed(() => {
  const dates = [];
  if (anchorDayIdentifier.value !== false && otherDayIdentifier.value !== false) {
    if (anchorDayIdentifier.value <= otherDayIdentifier.value) {
      dates.push(anchorTimestamp.value.date, otherTimestamp.value.date);
    } else {
      dates.push(otherTimestamp.value.date, anchorTimestamp.value.date);
    }
  }
  return dates;
});

const anchorDayIdentifier = computed(() => {
  if (anchorTimestamp.value !== null) {
    return getDayIdentifier(anchorTimestamp.value);
  }
  return false;
});

const otherDayIdentifier = computed(() => {
  if (otherTimestamp.value !== null) {
    return getDayIdentifier(otherTimestamp.value);
  }
  return false;
});

onBeforeMount(() => {
  selectedDate1.value = today();
  let tm = parseTimestamp(selectedDate1.value);
  tm = addToDate(tm, { month: 1 });
  selectedDate2.value = tm.date;
});

function onMouseDownDay({ scope, event }) {
  if (leftClick(event)) {
    if (mobile.value === true && anchorTimestamp.value !== null && otherTimestamp.value !== null && anchorTimestamp.value.date === otherTimestamp.value.date) {
      otherTimestamp.value = scope.timestamp;
      mouseDown.value = false;
      return;
    }
    // mouse is down, start selection and capture current
    mouseDown.value = true;
    anchorTimestamp.value = scope.timestamp;
    otherTimestamp.value = scope.timestamp;
  }
}

function onMouseUpDay({ scope, event }) {
  if (leftClick(event)) {
    // mouse is up, capture last and cancel selection
    otherTimestamp.value = scope.timestamp;
    mouseDown.value = false;
  }
}

function onMouseMoveDay({ scope, event }) {
  if (mouseDown.value === true && scope.outside !== true) {
    otherTimestamp.value = scope.timestamp;
  }
}

const setTimeStamp = () => {
  const { from, to } = props.modelValue;
  anchorTimestamp.value = from ? parseTimestamp(from) : null;
  otherTimestamp.value = to ? parseTimestamp(to) : null;
};

function onToday() {
  selectedDate1.value = today();
  let tm = parseTimestamp(selectedDate1.value);
  tm = addToDate(tm, { month: 1 });
  selectedDate2.value = tm.date;
}
function onPrev() {
  calendar1.value.prev();
  calendar2.value.prev();
}
function onNext() {
  calendar1.value.next();
  calendar2.value.next();
}
function onMoved(data) {
  // console.log('onMoved', data);
}
function onChange(data) {
  // console.log('onChange', data);
}
function onClickDate(data) {
  const { value } = startEndDates;
  emit('click-date', data);
  emit('update:modelValue', {
    from: value[0],
    to: value[1],
  });
}
function onClickDay(data) {
  // console.log('onClickDay', data);
}
function onClickWorkweek(data) {
  // console.log('onClickWorkweek', data);
}
function onClickHeadDay(data) {
  // console.log('onClickHeadDay', data);
}
function onClickHeadWorkweek(data) {
  // console.log('onClickHeadWorkweek', data);
}

watch(props, () => {
  setTimeStamp();
});

onMounted(() => {
  onPrev();
  setTimeStamp();
});
</script>
<style lang="scss" scoped>
.multi-month-selection {
  position: relative;
  .calender-wrap {
    display: flex;
    gap: 20px;
    width: 450px;
    margin-top: 10px;
    p {
      margin-bottom: 30px;
      font-weight: bold;
    }
  }
  .btn-wrap {
    position: absolute;
    display: flex;
    width: 100%;
    top: 2px;
    justify-content: space-between;
    button {
      border: none;
      background: transparent;
      cursor: pointer;
      width: 32px;
      height: 32px;
      padding: 0;
      border-radius: 50%;
      transition: 0.3s;
      &:hover {
        background: #ddd;
      }
    }
  }
}

// qCalendar에서 기본으로 제공되는 style 수정 
::v-deep .q-calendar {
  $btn-width: 30px;
  $btn-height: 30px;
  font-weight: bold;
  //요일
  .q-calendar-month__head--wrapper .q-calendar-month__head--weekdays div {
    font-size: 14px;
    color: #999;
  }
  .q-calendar-mini {
    color: #333;
  }
  .q-calendar-mini .q-calendar-month__day.q-range-first .q-calendar-month__day--label__wrapper .q-calendar__button,
  .q-calendar-mini .q-calendar-month__day.q-range-last .q-calendar-month__day--label__wrapper .q-calendar__button {
    border-radius: 0;
  }
  .q-calendar__button--round {
    border-radius: 0;
  }
  &.q-calendar__bordered {
    border: none;
  }
  .q-calendar-month__week--days {
    margin-bottom: 5px;
  }
  .q-calendar-mini .q-calendar-month__day {
    max-width: $btn-width !important;
    width: $btn-width !important;
    height: $btn-height !important;
  }
  // 오늘날짜
  .q-calendar-mini .q-calendar-month__day.q-current-day button {
    border: none !important;
    background: #333;
    color: #fff;
  }
  button {
    width: $btn-width !important;
    max-width: $btn-width !important;
    min-width: $btn-width !important;
    height: $btn-height !important;
  }
  .q-range.disabled .q-calendar-month__day--label__wrapper {
    min-height: 0;
    max-height: 0;
    height: 0;
  }
}
</style>

 

 

DateSelectBox.js

 

해당 컴포넌트에서는 MultiMonthSelection에서 선택한 날짜를 가져오고, '오늘','이번주','직접입력'과 같은 선택지를 준다.

<template>
  <div class="search-date">
    <slot :from="sendDate.from" :to="sendDate.to" :option="sendDate.option" :preview="preview" />
    <q-menu v-model="showing">
      <div class="date-dialog">
        <div class="period-setting">
          <p>기간 설정</p>
          <ul>
            <li v-for="(item, i) in predoList" :key="i" :class="selectedPeriod.name === item.name ? 'select' : null" @click="settingPeriod(item)">
              {{ item.name }}
            </li>
          </ul>
        </div>
        <div class="select-date">
          <div class="preview">{{ preview }}</div>
          <hr />
          <div>
            <MultiMonthSelection v-model="range" @click-date="clickDate" />
          </div>
          <hr />
          <div class="btn-wrap">
            <q-btn outline style="color: #333" variant="outlined" label="취소" @click="cancel" />
            <q-btn unelevated color="primary" label="적용" @click="getData" />
          </div>
        </div>
      </div>
    </q-menu>
  </div>
</template>

<script setup>
import { date } from 'quasar';

const WEEKDAY = ['일', '월', '화', '수', '목', '금', '토'];
const newDate = new Date();
const props = defineProps({
  modelValue: {
    type: Object,
  },
  predoList: {
    type: Array,
    default: [
      {
        name: '오늘',
        from: new Date(),
        to: new Date(),
      },
    ],
  },
});
const emit = defineEmits(['update:modelValue']);
const showing = ref(false);
const range = ref({
  from: newDate,
  to: newDate,
});
const sendDate = ref({
  from: props.modelValue.from,
  to: props.modelValue.to,
  option: props.predoList[0],
});
const selectedPeriod = ref(props.predoList[0]);

const preview = computed(() => {
  const { from, to } = range.value;
  const startDate = from ? date.formatDate(from, 'YYYY-MM-DD') : '';
  const startDated = from ? date.formatDate(from, 'd') : '';
  const endDate = to ? date.formatDate(to, 'YYYY-MM-DD') : '';
  const endDated = from ? date.formatDate(from, 'd') : '';
  return `${startDate} ${WEEKDAY[startDated]} ~ ${endDate} ${WEEKDAY[endDated]}`;
});

const settingPeriod = (date) => {
  selectedPeriod.value = date;
  range.value = date;
};

const getData = () => {
  sendDate.value = { ...range.value, option: selectedPeriod.value };
  emit('update:modelValue', {
    from: range.value.from,
    to: range.value.to,
    option: selectedPeriod.value,
  });
  showing.value = false;
};

const resetDate = () => {
  const { to, from, option } = sendDate.value;
  range.value = { to, from };
  selectedPeriod.value = option;
};

const cancel = () => {
  resetDate();
  showing.value = false;
};

const clickDate = () => {
  const { predoList } = props;
  if (selectedPeriod.value.name !== '직접 입력') {
    selectedPeriod.value = predoList[predoList.length - 1];
  }
};

onMounted(() => {
  const today = date.formatDate(newDate, 'MMMM-MM-DD');
  const { modelValue, predoList } = props;
  const { from, to } = modelValue;

  if (from !== today && to !== today) {
    selectedPeriod.value = predoList[predoList.length - 1];
  }
  range.value = { from, to };
  sendDate.value = { from, to, option: selectedPeriod.value };
});
</script>

<style lang="scss" scoped>
.date-dialog {
  left: 0;
  width: auto;
  --primary-color: #1867c0;
  display: inline-flex;
  border: 1px solid #000000;
  border-radius: 4px;
  background: #fff;
  overflow: hidden;
}
.period-setting {
  background: #eee;
  padding: 5px 0;
  font-size: 14px;
  p {
    padding: 0 20px;
    line-height: 3;
    font-weight: 900;
  }
  li {
    color: #333;
    padding: 0 20px;
    width: 180px;
    line-height: 2.5;
    cursor: pointer;
  }
  li.select {
    background: #fff;
    color: var(--primary-color);
  }
}
.search-date {
  position: relative;
  display: inline-flex;
  justify-content: center;
  gap: 10px;
  align-items: center;
  margin-top: 20px;
}
.select-date {
  padding: 20px 15px;
  .preview {
    text-align: center;
    font-weight: bold;
  }
  hr {
    background: #eee;
    height: 1px;
    border: 0;
    margin: 20px 0;
  }
  .btn-wrap {
    display: flex;
    justify-content: end;
    gap: 10px;
  }
}
input {
  padding: 4px 10px;
  border: 2px solid #1a237e;
  border-radius: 4px;
}
</style>

 

 

728x90

공유하기

facebook twitter kakaoTalk kakaostory naver band
loading