311 lines
9.5 KiB
Vue
311 lines
9.5 KiB
Vue
<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>
|