1
0
Files
coconut-leaf/frontend/src/components/calendar-event/EventLoopEditor.vue

311 lines
9.5 KiB
Vue
Raw Normal View History

<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import DateTimePicker, { TabType } from '@/components/DateTimePicker.vue';
import { resolveLoopRules4UI, getDayInMonth } from '@/utils/datetime';
import { WEEK_NAMES } from '@/utils/calendar-names';
import { format } from '@/utils/utils';
type LoopMethod = 'never' | 'day' | 'week' | 'month' | 'year';
type StopMethod = 'forever' | 'datetime' | 'times';
const props = defineProps<{
modelValue: string;
startDate: Date;
}>();
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void
}>();
const stopPicker = ref<InstanceType<typeof DateTimePicker>>();
const loopMethod = ref<LoopMethod>('never');
const daySpan = ref<number>(1);
const weekSpan = ref<number>(1);
const monthSpan = ref<number>(1);
const yearSpan = ref<number>(1);
// default: today's weekday checked (Monday-first 0-6)
const initialWeekday = (new Date().getDay() + 6) % 7;
const weekChecks = ref<boolean[]>(Array.from({ length: 7 }, (_, i) => i === initialWeekday));
const monthMode = ref<'A' | 'B' | 'C' | 'D'>('A');
const isStrict = ref<boolean>(true);
const stopMethod = ref<StopMethod>('forever');
const stopTimes = ref<number>(1);
const stopDateTime = ref<Date>(new Date());
// region: Serialize / parse
const buildString = (): string => {
let loopPart = '';
switch (loopMethod.value) {
case 'never':
return '';
case 'day':
loopPart = `D${daySpan.value}`;
break;
case 'week': {
const checks = weekChecks.value.map(b => b ? 'T' : 'F').join('');
loopPart = `W${checks}${weekSpan.value}`;
break;
}
case 'month':
loopPart = `M${isStrict.value ? 'S' : 'R'}${monthMode.value}${monthSpan.value}`;
break;
case 'year':
loopPart = `Y${isStrict.value ? 'S' : 'R'}${yearSpan.value}`;
break;
}
let stopPart = '';
switch (stopMethod.value) {
case 'forever':
stopPart = '-F';
break;
case 'datetime':
stopPart = `-D${Math.floor(stopDateTime.value.getTime() / 60000)}`;
break;
case 'times':
stopPart = `-T${stopTimes.value}`;
break;
}
return loopPart + stopPart;
};
const serialized = computed<string>(() => buildString());
// Cast helpers: the parsed rule tuples are a union; index access is loose here.
const parseFromModel = (val: string): void => {
if (val === serialized.value) return;
if (val === '') {
loopMethod.value = 'never';
return;
}
const parsed = resolveLoopRules4UI(val);
if (typeof parsed === 'undefined') {
loopMethod.value = 'never';
return;
}
const loopRule = parsed[0];
const stopRule = parsed[1];
switch (loopRule[0]) {
case 0:
loopMethod.value = 'year';
isStrict.value = loopRule[1];
yearSpan.value = loopRule[2];
break;
case 1:
loopMethod.value = 'month';
isStrict.value = loopRule[1];
monthMode.value = loopRule[2];
monthSpan.value = loopRule[3];
break;
case 2:
loopMethod.value = 'week';
weekChecks.value = [loopRule[1], loopRule[2], loopRule[3], loopRule[4], loopRule[5], loopRule[6], loopRule[7]];
weekSpan.value = loopRule[8];
break;
case 3:
loopMethod.value = 'day';
daySpan.value = loopRule[1];
break;
}
switch (stopRule[0]) {
case 0:
stopMethod.value = 'forever';
break;
case 1:
stopMethod.value = 'datetime';
// NOTE: treated as a plain UTC instant (day-precision stop), deviating
// from the legacy timezone-shifted display. See migration notes.
stopDateTime.value = new Date(stopRule[1] * 60000);
break;
case 2:
stopMethod.value = 'times';
stopTimes.value = stopRule[1];
break;
}
};
watch(() => props.modelValue, (val) => parseFromModel(val), { immediate: true });
watch(serialized, (val) => {
if (val !== props.modelValue) emit('update:modelValue', val);
});
// endregion
const showLoopStop = computed(() => loopMethod.value !== 'never');
const showStrictMode = computed(() => loopMethod.value === 'month' || loopMethod.value === 'year');
const monthOptionTexts = computed(() => {
const d = getDayInMonth(
props.startDate.getFullYear(),
props.startDate.getMonth() + 1,
props.startDate.getDate()
);
return {
A: format('Day {0} in month', d[0]),
B: format('Day {0} from the end of the month', d[1]),
C: format('Day {1} in week {0}', d[2], d[3] + 1),
D: format('Day {1} in week {0} from the end of the month', d[4], d[5] + 1),
};
});
const stopDateTimeText = computed(() => stopDateTime.value.toLocaleDateString());
const openStopPicker = () => {
stopPicker.value?.modal(stopDateTime.value, TabType.Day);
};
const onStopPickerConfirm = (date: Date) => {
stopDateTime.value = date;
};
</script>
<template>
<section class="section">
<h2 class="subtitle">Event Loop</h2>
<div class="button-list">
<label class="radio">
<input type="radio" value="never" v-model="loopMethod">
<span>Never</span>
</label>
<label class="radio">
<input type="radio" value="day" v-model="loopMethod">
<span>Day</span>
</label>
<label class="radio">
<input type="radio" value="week" v-model="loopMethod">
<span>Week</span>
</label>
<label class="radio">
<input type="radio" value="month" v-model="loopMethod">
<span>Month</span>
</label>
<label class="radio">
<input type="radio" value="year" v-model="loopMethod">
<span>Year</span>
</label>
</div>
<div v-if="loopMethod === 'day'">
<div class="field">
<label class="label">Day span</label>
<div class="control">
<input v-model.number="daySpan" class="input spanpicker" type="number" min="1" max="100" step="1">
</div>
</div>
</div>
<div v-if="loopMethod === 'week'">
<div class="field">
<label class="label">Week span</label>
<div class="control">
<input v-model.number="weekSpan" class="input spanpicker" type="number" min="1" max="100" step="1">
</div>
</div>
<div class="field">
<label class="label">Week options</label>
<div class="button-list">
<label v-for="(name, i) in WEEK_NAMES" :key="i" class="checkbox">
<input type="checkbox" v-model="weekChecks[i]">
<span>{{ name }}</span>
</label>
</div>
</div>
</div>
<div v-if="loopMethod === 'month'">
<div class="field">
<label class="label">Month span</label>
<div class="control">
<input v-model.number="monthSpan" class="input spanpicker" type="number" min="1" max="100" step="1">
</div>
</div>
<div class="field">
<label class="label">Month mode</label>
<div class="button-list">
<label class="radio">
<input type="radio" value="A" v-model="monthMode">
<span>{{ monthOptionTexts.A }}</span>
</label>
<label class="radio">
<input type="radio" value="B" v-model="monthMode">
<span>{{ monthOptionTexts.B }}</span>
</label>
<label class="radio">
<input type="radio" value="C" v-model="monthMode">
<span>{{ monthOptionTexts.C }}</span>
</label>
<label class="radio">
<input type="radio" value="D" v-model="monthMode">
<span>{{ monthOptionTexts.D }}</span>
</label>
</div>
</div>
</div>
<div v-if="loopMethod === 'year'">
<div class="field">
<label class="label">Year span</label>
<div class="control">
<input v-model.number="yearSpan" class="input spanpicker" type="number" min="1" max="100" step="1">
</div>
</div>
</div>
</section>
<section v-if="showStrictMode" class="section">
<h2 class="subtitle">Strict Mode in Event Loop</h2>
<p>You can choose strict mode or rough mode in following content. This is only effect on looped event.</p>
<div class="button-list">
<label class="radio">
<input type="radio" :value="true" v-model="isStrict">
<span>Strict Mode. If ordered day is not existing, skip it.</span>
</label>
<label class="radio">
<input type="radio" :value="false" v-model="isStrict">
<span>Rough mode. If ordered day is not existing, choose the day closing with original day to arrange event.</span>
</label>
</div>
</section>
<section v-if="showLoopStop" class="section">
<h2 class="subtitle">Event Loop Stop</h2>
<div class="button-list">
<label class="radio">
<input type="radio" value="forever" v-model="stopMethod">
<span>Forever</span>
</label>
<label class="radio">
<input type="radio" value="datetime" v-model="stopMethod">
<span>Date Time</span>
</label>
<label class="radio">
<input type="radio" value="times" v-model="stopMethod">
<span>Times</span>
</label>
</div>
<div v-if="stopMethod === 'datetime'">
<a class="button" @click="openStopPicker">
<span>{{ stopDateTimeText }}</span>
</a>
</div>
<div v-if="stopMethod === 'times'">
<div class="field">
<div class="control">
<input v-model.number="stopTimes" class="input spanpicker" type="number" min="1" max="100" step="1">
</div>
</div>
</div>
</section>
<DateTimePicker ref="stopPicker" @confirm="onStopPickerConfirm" />
</template>
<style scoped>
.section {
border-top: 1px solid rgba(219, 219, 219, .5);
padding-top: 1.25rem;
margin-top: 1.25rem;
}
</style>