qCalendarDoc : https://qcalendar.netlify.app/
qCalendar를 이용해 DatePicker를 만들어봤다.
디자인은 tistory에서 사용하는 DatePicker를 참고해서 만들었다.
"nuxt": "3.3.1",
"@quasar/quasar-ui-qcalendar": "^4.0.0-beta.15",
"quasar": "^2.12.0",
아래 방법은 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',
},
],
})
한 컴포넌트로 만들기에 너무 크고, 나중에 내부달력부분은 다른 컴포넌트를 만들 때 또 사용할 수 있을 것 같아서 분리해서 작업했다.
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>
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>
해당 컴포넌트에서는 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>
[Nuxt] command not found : nuxt (1) | 2023.08.21 |
---|---|
[qCalendar] MonthSlotDay 예제 오류 (0) | 2023.06.01 |
[Nuxt3] i18n 과 Google Spread Sheet를 통한 다국어지원 자동화 (0) | 2023.05.23 |
[Vue] 컴포넌트 분리 (CompositionAPI 사용, emit, slot, props) (0) | 2023.05.17 |
[Nuxt3] 빌드방식 3가지(ssr,csr,generate) (0) | 2023.04.28 |