feat: use AI to migrate DateTimePicker component
This commit is contained in:
@@ -33,7 +33,7 @@ const logout = async () => {
|
|||||||
// Check for click events on the navbar burger icon
|
// Check for click events on the navbar burger icon
|
||||||
const toggleBurger = () => {
|
const toggleBurger = () => {
|
||||||
// Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu"
|
// Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu"
|
||||||
isBurgerActive.value = !isBurgerActive
|
isBurgerActive.value = !isBurgerActive.value
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
678
frontend/src/components/DateTimePicker.vue
Normal file
678
frontend/src/components/DateTimePicker.vue
Normal file
@@ -0,0 +1,678 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export enum TabType {
|
||||||
|
Year,
|
||||||
|
Month,
|
||||||
|
Day,
|
||||||
|
Hour,
|
||||||
|
Minute
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||||
|
import {
|
||||||
|
MAX_DATETIME,
|
||||||
|
MIN_DATETIME,
|
||||||
|
MIN_YEAR,
|
||||||
|
MAX_YEAR,
|
||||||
|
MONTH_DAY_COUNT,
|
||||||
|
dayOfWeek,
|
||||||
|
isLeapYear
|
||||||
|
} from '@/utils/datetime';
|
||||||
|
|
||||||
|
const DIAL_PLATE_WIDTH: number = 200;
|
||||||
|
const DIAL_PLATE_RADIUS: number = DIAL_PLATE_WIDTH / 2;
|
||||||
|
const DIAL_PLATE_HOUR_INNER_PERCENT: number = 0.6;
|
||||||
|
const DIAL_PLATE_HOUR_OUTTER_PERCENT: number = 0.8;
|
||||||
|
const DIAL_PLATE_HOUR_DISTINGUISH_PERCENT: number = 0.7;
|
||||||
|
const DIAL_PLATE_MINUTE_PERCENT: number = 0.8;
|
||||||
|
const DIAL_PLATE_HOUR_RESOLUTION: number = Math.PI * 2 / 12;
|
||||||
|
const DIAL_PLATE_MINUTE_RESOLUTION: number = Math.PI * 2 / 60;
|
||||||
|
|
||||||
|
const MONTH_NAMES: string[] = [
|
||||||
|
'January', 'February', 'March', 'April', 'May', 'June',
|
||||||
|
'July', 'August', 'September', 'October', 'November', 'December'
|
||||||
|
];
|
||||||
|
const WEEK_NAMES: string[] = [
|
||||||
|
'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'
|
||||||
|
];
|
||||||
|
const NBSP: string = '\u00A0';
|
||||||
|
|
||||||
|
const isVisible = ref(false);
|
||||||
|
const currentTab = ref<TabType>(TabType.Year);
|
||||||
|
const mode = ref<TabType>(TabType.Day);
|
||||||
|
const svgFontScale = ref<number>(1);
|
||||||
|
|
||||||
|
let enableHourDrag = false;
|
||||||
|
let enableMinuteDrag = false;
|
||||||
|
|
||||||
|
const internalDateTime = ref<Date>(new Date());
|
||||||
|
const displayCacheDateTime = ref<Date>(new Date());
|
||||||
|
|
||||||
|
const pickerContainerRef = ref<HTMLElement>();
|
||||||
|
const hourSvgRef = ref<SVGSVGElement>();
|
||||||
|
const minuteSvgRef = ref<SVGSVGElement>();
|
||||||
|
|
||||||
|
// region: Exported API and Signals
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'confirm', date: Date): void
|
||||||
|
(e: 'cancel'): void
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const clampDateTime = (d: Date): void => {
|
||||||
|
if (d < MIN_DATETIME) d.setTime(MIN_DATETIME.getTime());
|
||||||
|
if (d >= MAX_DATETIME) d.setTime(MAX_DATETIME.getTime());
|
||||||
|
};
|
||||||
|
|
||||||
|
const switchTab = (newTab: TabType): void => {
|
||||||
|
displayCacheDateTime.value = new Date(internalDateTime.value);
|
||||||
|
currentTab.value = newTab;
|
||||||
|
};
|
||||||
|
|
||||||
|
const modal = (initialDate: Date, _mode: TabType): void => {
|
||||||
|
mode.value = _mode;
|
||||||
|
const d = new Date(initialDate);
|
||||||
|
clampDateTime(d);
|
||||||
|
internalDateTime.value = new Date(d);
|
||||||
|
displayCacheDateTime.value = new Date(d);
|
||||||
|
isVisible.value = true;
|
||||||
|
switchTab(_mode);
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirm = (): void => {
|
||||||
|
isVisible.value = false;
|
||||||
|
emit('confirm', new Date(internalDateTime.value));
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancel = (): void => {
|
||||||
|
isVisible.value = false;
|
||||||
|
emit('cancel');
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({ modal });
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region: Mutator helpers
|
||||||
|
// Vue does not track in-place Date mutation, so every change clones, mutates,
|
||||||
|
// clamps, and reassigns the ref to trigger reactivity.
|
||||||
|
|
||||||
|
const updateDisplay = (mutator: (d: Date) => void): void => {
|
||||||
|
const d = new Date(displayCacheDateTime.value);
|
||||||
|
mutator(d);
|
||||||
|
clampDateTime(d);
|
||||||
|
displayCacheDateTime.value = d;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateInternal = (mutator: (d: Date) => void): void => {
|
||||||
|
const d = new Date(internalDateTime.value);
|
||||||
|
mutator(d);
|
||||||
|
clampDateTime(d);
|
||||||
|
internalDateTime.value = d;
|
||||||
|
};
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region: Computed views (replaces legacy imperative RefreshDisplay)
|
||||||
|
|
||||||
|
const visibleTabs = computed<Set<TabType>>(() => {
|
||||||
|
const s = new Set<TabType>();
|
||||||
|
if (mode.value >= TabType.Year) s.add(TabType.Year);
|
||||||
|
if (mode.value >= TabType.Month) s.add(TabType.Month);
|
||||||
|
if (mode.value >= TabType.Day) s.add(TabType.Day);
|
||||||
|
if (mode.value >= TabType.Hour) s.add(TabType.Hour);
|
||||||
|
if (mode.value >= TabType.Minute) s.add(TabType.Minute);
|
||||||
|
return s;
|
||||||
|
});
|
||||||
|
|
||||||
|
const headerParts = computed(() => ({
|
||||||
|
year: internalDateTime.value.getFullYear(),
|
||||||
|
month: internalDateTime.value.getMonth() + 1,
|
||||||
|
day: internalDateTime.value.getDate(),
|
||||||
|
hour: internalDateTime.value.getHours(),
|
||||||
|
minute: internalDateTime.value.getMinutes(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const yearPageStart = computed<number>(() => {
|
||||||
|
const y = displayCacheDateTime.value.getFullYear();
|
||||||
|
return Math.floor((y - MIN_YEAR) / 12) * 12 + MIN_YEAR;
|
||||||
|
});
|
||||||
|
|
||||||
|
const yearCells = computed<{ value: number | undefined; picked: boolean }[]>(() => {
|
||||||
|
const start = yearPageStart.value;
|
||||||
|
const internalYear = internalDateTime.value.getFullYear();
|
||||||
|
const cells: { value: number | undefined; picked: boolean }[] = [];
|
||||||
|
for (let i = 0; i < 12; i++) {
|
||||||
|
const counter = start + i;
|
||||||
|
if (counter < MAX_YEAR) {
|
||||||
|
cells.push({ value: counter, picked: counter === internalYear });
|
||||||
|
} else {
|
||||||
|
cells.push({ value: undefined, picked: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cells;
|
||||||
|
});
|
||||||
|
|
||||||
|
const yearRows = computed<{ value: number | undefined; picked: boolean }[][]>(() => {
|
||||||
|
const cells = yearCells.value;
|
||||||
|
const rows: { value: number | undefined; picked: boolean }[][] = [];
|
||||||
|
for (let i = 0; i < 3; i++) rows.push(cells.slice(i * 4, i * 4 + 4));
|
||||||
|
return rows;
|
||||||
|
});
|
||||||
|
|
||||||
|
const yearTitle = computed<string>(() => {
|
||||||
|
const start = yearPageStart.value;
|
||||||
|
return `${start} - ${Math.min(start + 12, MAX_YEAR)}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const pickedMonthIndex = computed<number>(() => {
|
||||||
|
if (internalDateTime.value.getFullYear() === displayCacheDateTime.value.getFullYear()) {
|
||||||
|
return internalDateTime.value.getMonth();
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
});
|
||||||
|
|
||||||
|
const monthCells = computed<{ index: number; name: string; picked: boolean }[]>(() => {
|
||||||
|
const picked = pickedMonthIndex.value;
|
||||||
|
const cells: { index: number; name: string; picked: boolean }[] = [];
|
||||||
|
for (let i = 0; i < 12; i++) {
|
||||||
|
cells.push({ index: i, name: MONTH_NAMES[i]!, picked: i === picked });
|
||||||
|
}
|
||||||
|
return cells;
|
||||||
|
});
|
||||||
|
|
||||||
|
const monthRows = computed<{ index: number; name: string; picked: boolean }[][]>(() => {
|
||||||
|
const cells = monthCells.value;
|
||||||
|
const rows: { index: number; name: string; picked: boolean }[][] = [];
|
||||||
|
for (let i = 0; i < 3; i++) rows.push(cells.slice(i * 4, i * 4 + 4));
|
||||||
|
return rows;
|
||||||
|
});
|
||||||
|
|
||||||
|
const monthTitle = computed<number>(() => displayCacheDateTime.value.getFullYear());
|
||||||
|
|
||||||
|
const dayGrid = computed<{ value: number | undefined; picked: boolean }[][]>(() => {
|
||||||
|
const y = displayCacheDateTime.value.getFullYear();
|
||||||
|
const m = displayCacheDateTime.value.getMonth() + 1;
|
||||||
|
const firstDow = dayOfWeek(y, m, 1);
|
||||||
|
const daysInMonth = MONTH_DAY_COUNT[m - 1]! + (m === 2 && isLeapYear(y) ? 1 : 0);
|
||||||
|
const internalDay = internalDateTime.value.getDate();
|
||||||
|
const rows: { value: number | undefined; picked: boolean }[][] = [];
|
||||||
|
let counter = -firstDow;
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
const row: { value: number | undefined; picked: boolean }[] = [];
|
||||||
|
for (let j = 0; j < 7; j++, counter++) {
|
||||||
|
if (counter < 0 || counter >= daysInMonth) {
|
||||||
|
row.push({ value: undefined, picked: false });
|
||||||
|
} else {
|
||||||
|
const dayNum = counter + 1;
|
||||||
|
row.push({ value: dayNum, picked: dayNum === internalDay });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rows.push(row);
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
});
|
||||||
|
|
||||||
|
const dayTitle = computed<string>(() => {
|
||||||
|
const y = displayCacheDateTime.value.getFullYear();
|
||||||
|
const m = displayCacheDateTime.value.getMonth();
|
||||||
|
return `${y} - ${MONTH_NAMES[m]!}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const hourHand = computed(() => {
|
||||||
|
const h = displayCacheDateTime.value.getHours();
|
||||||
|
const angle = (3 - h) * DIAL_PLATE_HOUR_RESOLUTION;
|
||||||
|
const radius = DIAL_PLATE_RADIUS * (h < 12 ? DIAL_PLATE_HOUR_OUTTER_PERCENT : DIAL_PLATE_HOUR_INNER_PERCENT);
|
||||||
|
const x = Math.cos(angle) * radius + DIAL_PLATE_RADIUS;
|
||||||
|
const y = (-Math.sin(angle) * radius) + DIAL_PLATE_RADIUS;
|
||||||
|
return { x2: x, y2: y, cx: x, cy: y };
|
||||||
|
});
|
||||||
|
|
||||||
|
const minuteHand = computed(() => {
|
||||||
|
const mi = displayCacheDateTime.value.getMinutes();
|
||||||
|
const angle = (15 - mi) * DIAL_PLATE_MINUTE_RESOLUTION;
|
||||||
|
const radius = DIAL_PLATE_RADIUS * DIAL_PLATE_MINUTE_PERCENT;
|
||||||
|
const x = Math.cos(angle) * radius + DIAL_PLATE_RADIUS;
|
||||||
|
const y = (-Math.sin(angle) * radius) + DIAL_PLATE_RADIUS;
|
||||||
|
return { x2: x, y2: y, cx: x, cy: y };
|
||||||
|
});
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region: Navigation and click handlers
|
||||||
|
|
||||||
|
const prevNextYear = (isPrev: boolean): void => {
|
||||||
|
updateDisplay((d) => d.setFullYear(d.getFullYear() + (isPrev ? -12 : 12)));
|
||||||
|
};
|
||||||
|
|
||||||
|
// NOTE: legacy PrevNextMonth actually shifts the YEAR (the month panel browses years).
|
||||||
|
const prevNextMonth = (isPrev: boolean): void => {
|
||||||
|
updateDisplay((d) => d.setFullYear(d.getFullYear() + (isPrev ? -1 : 1)));
|
||||||
|
};
|
||||||
|
|
||||||
|
// NOTE: legacy PrevNextDay actually shifts the MONTH (the day panel browses months).
|
||||||
|
const prevNextDay = (isPrev: boolean): void => {
|
||||||
|
updateDisplay((d) => d.setMonth(d.getMonth() + (isPrev ? -1 : 1)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const clickYear = (value: number | undefined): void => {
|
||||||
|
if (value === undefined) return;
|
||||||
|
updateInternal((d) => d.setFullYear(value));
|
||||||
|
if (mode.value !== TabType.Year) switchTab(TabType.Month);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clickMonth = (monthIndex: number): void => {
|
||||||
|
updateInternal((d) => {
|
||||||
|
d.setFullYear(displayCacheDateTime.value.getFullYear());
|
||||||
|
d.setMonth(monthIndex);
|
||||||
|
});
|
||||||
|
if (mode.value !== TabType.Month) switchTab(TabType.Day);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clickDay = (day: number | undefined): void => {
|
||||||
|
if (day === undefined) return;
|
||||||
|
updateInternal((d) => {
|
||||||
|
d.setFullYear(displayCacheDateTime.value.getFullYear());
|
||||||
|
d.setMonth(displayCacheDateTime.value.getMonth());
|
||||||
|
d.setDate(day);
|
||||||
|
});
|
||||||
|
if (mode.value !== TabType.Day) switchTab(TabType.Hour);
|
||||||
|
};
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region: Dial drag interaction
|
||||||
|
|
||||||
|
const getUniformedXY = (e: MouseEvent | TouchEvent, el: Element): { x: number; y: number } => {
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
const halfWidth = rect.width / 2;
|
||||||
|
const halfHeight = rect.height / 2;
|
||||||
|
const halfSquare = Math.min(rect.width, rect.height) / 2;
|
||||||
|
let clientX = 0;
|
||||||
|
let clientY = 0;
|
||||||
|
const te = e as TouchEvent;
|
||||||
|
const me = e as MouseEvent;
|
||||||
|
if (te.targetTouches && te.targetTouches.length >= 1) {
|
||||||
|
clientX = te.targetTouches[0]!.clientX;
|
||||||
|
clientY = te.targetTouches[0]!.clientY;
|
||||||
|
} else {
|
||||||
|
clientX = me.clientX;
|
||||||
|
clientY = me.clientY;
|
||||||
|
}
|
||||||
|
const x = ((clientX - rect.left - halfWidth) / halfSquare) * DIAL_PLATE_RADIUS;
|
||||||
|
const y = -(((clientY - rect.top - halfHeight) / halfSquare) * DIAL_PLATE_RADIUS);
|
||||||
|
return { x, y };
|
||||||
|
};
|
||||||
|
|
||||||
|
const startDragHour = (): void => { enableHourDrag = true; };
|
||||||
|
const draggingHour = (e: MouseEvent | TouchEvent): void => {
|
||||||
|
if (!enableHourDrag || !hourSvgRef.value) return;
|
||||||
|
const { x, y } = getUniformedXY(e, hourSvgRef.value);
|
||||||
|
const distance = Math.sqrt(x * x + y * y);
|
||||||
|
let angle = Math.acos(x / distance);
|
||||||
|
if (y < 0) angle = Math.PI * 2 - angle;
|
||||||
|
angle += DIAL_PLATE_HOUR_RESOLUTION / 2;
|
||||||
|
if (angle > Math.PI * 2) angle -= Math.PI * 2;
|
||||||
|
let number = Math.floor(angle / DIAL_PLATE_HOUR_RESOLUTION);
|
||||||
|
if (number >= 12) number = 11;
|
||||||
|
number = (15 - number) % 12;
|
||||||
|
if (distance < DIAL_PLATE_RADIUS * DIAL_PLATE_HOUR_DISTINGUISH_PERCENT) number += 12;
|
||||||
|
if (displayCacheDateTime.value.getHours() !== number) {
|
||||||
|
updateDisplay((d) => d.setHours(number));
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
};
|
||||||
|
const stopDragHour = (): void => {
|
||||||
|
enableHourDrag = false;
|
||||||
|
updateInternal((d) => d.setHours(displayCacheDateTime.value.getHours()));
|
||||||
|
if (mode.value !== TabType.Hour) switchTab(TabType.Minute);
|
||||||
|
};
|
||||||
|
|
||||||
|
const startDragMinute = (): void => { enableMinuteDrag = true; };
|
||||||
|
const draggingMinute = (e: MouseEvent | TouchEvent): void => {
|
||||||
|
if (!enableMinuteDrag || !minuteSvgRef.value) return;
|
||||||
|
const { x, y } = getUniformedXY(e, minuteSvgRef.value);
|
||||||
|
const distance = Math.sqrt(x * x + y * y);
|
||||||
|
let angle = Math.acos(x / distance);
|
||||||
|
if (y < 0) angle = Math.PI * 2 - angle;
|
||||||
|
angle += DIAL_PLATE_MINUTE_RESOLUTION / 2;
|
||||||
|
if (angle > Math.PI * 2) angle -= Math.PI * 2;
|
||||||
|
let number = Math.floor(angle / DIAL_PLATE_MINUTE_RESOLUTION);
|
||||||
|
if (number >= 60) number = 59;
|
||||||
|
number = (75 - number) % 60;
|
||||||
|
if (displayCacheDateTime.value.getMinutes() !== number) {
|
||||||
|
updateDisplay((d) => d.setMinutes(number));
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
};
|
||||||
|
const stopDragMinute = (): void => {
|
||||||
|
enableMinuteDrag = false;
|
||||||
|
updateInternal((d) => d.setMinutes(displayCacheDateTime.value.getMinutes()));
|
||||||
|
};
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region: Lifecycle - SVG font-size scaling
|
||||||
|
|
||||||
|
let resizeObserver: ResizeObserver | null = null;
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
resizeObserver = new ResizeObserver((entries) => {
|
||||||
|
for (const entry of entries) {
|
||||||
|
const w = entry.contentRect.width;
|
||||||
|
const h = entry.contentRect.height;
|
||||||
|
if (w > 0 && h > 0) svgFontScale.value = 200 / Math.min(w, h);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (pickerContainerRef.value) resizeObserver.observe(pickerContainerRef.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
resizeObserver?.disconnect();
|
||||||
|
resizeObserver = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="modal" :class="{ 'is-active': isVisible }"
|
||||||
|
style="float: left; position: fixed; top: 0; bottom: 0; left: 0; right: 0;">
|
||||||
|
<div class="modal-background"></div>
|
||||||
|
<div class="modal-card" style="height: 70%;">
|
||||||
|
<header class="modal-card-head pickerHeader">
|
||||||
|
<div v-show="visibleTabs.has(TabType.Year)" @click="switchTab(TabType.Year)">
|
||||||
|
<small>Year</small><span>{{ headerParts.year }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-show="visibleTabs.has(TabType.Month)" @click="switchTab(TabType.Month)">
|
||||||
|
<small>Month</small><span>{{ headerParts.month }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-show="visibleTabs.has(TabType.Day)" @click="switchTab(TabType.Day)">
|
||||||
|
<small>Day</small><span>{{ headerParts.day }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-show="visibleTabs.has(TabType.Hour)" @click="switchTab(TabType.Hour)">
|
||||||
|
<small>Hour</small><span>{{ headerParts.hour }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-show="visibleTabs.has(TabType.Minute)" @click="switchTab(TabType.Minute)">
|
||||||
|
<small>Minute</small><span>{{ headerParts.minute }}</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div class="modal-card-body pickerContainer" ref="pickerContainerRef">
|
||||||
|
<div v-show="currentTab === TabType.Year">
|
||||||
|
<nav class="level is-mobile">
|
||||||
|
<div class="level-left">
|
||||||
|
<div class="level-item control">
|
||||||
|
<a class="button" @click="prevNextYear(true)">
|
||||||
|
<span class="icon is-small"><i class="fas fa-chevron-circle-left"></i></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="level-item">{{ yearTitle }}</div>
|
||||||
|
<div class="level-right">
|
||||||
|
<div class="level-item control">
|
||||||
|
<a class="button" @click="prevNextYear(false)">
|
||||||
|
<span class="icon is-small"><i class="fas fa-chevron-circle-right"></i></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="perfectTable">
|
||||||
|
<div v-for="(row, ri) in yearRows" :key="ri">
|
||||||
|
<div v-for="(cell, ci) in row" :key="ci"
|
||||||
|
:class="{ picked: cell.picked }"
|
||||||
|
@click="clickYear(cell.value)">{{ cell.value ?? NBSP }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="currentTab === TabType.Month">
|
||||||
|
<nav class="level is-mobile">
|
||||||
|
<div class="level-left">
|
||||||
|
<div class="level-item control">
|
||||||
|
<a class="button" @click="prevNextMonth(true)">
|
||||||
|
<span class="icon is-small"><i class="fas fa-chevron-circle-left"></i></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="level-item">{{ monthTitle }}</div>
|
||||||
|
<div class="level-right">
|
||||||
|
<div class="level-item control">
|
||||||
|
<a class="button" @click="prevNextMonth(false)">
|
||||||
|
<span class="icon is-small"><i class="fas fa-chevron-circle-right"></i></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="perfectTable">
|
||||||
|
<div v-for="(row, ri) in monthRows" :key="ri">
|
||||||
|
<div v-for="(cell, ci) in row" :key="ci"
|
||||||
|
:class="{ picked: cell.picked }"
|
||||||
|
@click="clickMonth(cell.index)">{{ cell.name }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="currentTab === TabType.Day">
|
||||||
|
<nav class="level is-mobile">
|
||||||
|
<div class="level-left">
|
||||||
|
<div class="level-item control">
|
||||||
|
<a class="button" @click="prevNextDay(true)">
|
||||||
|
<span class="icon is-small"><i class="fas fa-chevron-circle-left"></i></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="level-item">{{ dayTitle }}</div>
|
||||||
|
<div class="level-right">
|
||||||
|
<div class="level-item control">
|
||||||
|
<a class="button" @click="prevNextDay(false)">
|
||||||
|
<span class="icon is-small"><i class="fas fa-chevron-circle-right"></i></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="perfectTable">
|
||||||
|
<div>
|
||||||
|
<div v-for="(name, wi) in WEEK_NAMES" :key="wi">{{ name }}</div>
|
||||||
|
</div>
|
||||||
|
<div v-for="(row, ri) in dayGrid" :key="ri">
|
||||||
|
<div v-for="(cell, ci) in row" :key="ci"
|
||||||
|
:class="{ picked: cell.picked }"
|
||||||
|
@click="clickDay(cell.value)">{{ cell.value ?? NBSP }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<svg v-show="currentTab === TabType.Hour" ref="hourSvgRef"
|
||||||
|
xmlns="http://www.w3.org/2000/svg" version="1.1"
|
||||||
|
preserveAspectRatio="xMidYMid" viewBox="0 0 200 200"
|
||||||
|
:style="{ fontSize: svgFontScale + 'em' }"
|
||||||
|
@mousedown="startDragHour" @mousemove="draggingHour" @mouseup="stopDragHour"
|
||||||
|
@touchstart="startDragHour" @touchmove="draggingHour" @touchend="stopDragHour">
|
||||||
|
<circle cx="100.000000" cy="100.000000" r="100.000000" type="background"></circle>
|
||||||
|
<line x1="100" y1="100" :x2="hourHand.x2" :y2="hourHand.y2"></line>
|
||||||
|
<circle :cx="hourHand.cx" :cy="hourHand.cy" r="1em" type="symbol"></circle>
|
||||||
|
|
||||||
|
<text x="100.000000" y="20.000000">0</text>
|
||||||
|
<text x="140.000000" y="30.717968">1</text>
|
||||||
|
<text x="169.282032" y="60.000000">2</text>
|
||||||
|
<text x="180.000000" y="100.000000">3</text>
|
||||||
|
<text x="169.282032" y="140.000000">4</text>
|
||||||
|
<text x="140.000000" y="169.282032">5</text>
|
||||||
|
<text x="100.000000" y="180.000000">6</text>
|
||||||
|
<text x="60.000000" y="169.282032">7</text>
|
||||||
|
<text x="30.717968" y="140.000000">8</text>
|
||||||
|
<text x="20.000000" y="100.000000">9</text>
|
||||||
|
<text x="30.717968" y="60.000000">10</text>
|
||||||
|
<text x="60.000000" y="30.717968">11</text>
|
||||||
|
<text x="100.000000" y="40.000000">12</text>
|
||||||
|
<text x="130.000000" y="48.038476">13</text>
|
||||||
|
<text x="151.961524" y="70.000000">14</text>
|
||||||
|
<text x="160.000000" y="100.000000">15</text>
|
||||||
|
<text x="151.961524" y="130.000000">16</text>
|
||||||
|
<text x="130.000000" y="151.961524">17</text>
|
||||||
|
<text x="100.000000" y="160.000000">18</text>
|
||||||
|
<text x="70.000000" y="151.961524">19</text>
|
||||||
|
<text x="48.038476" y="130.000000">20</text>
|
||||||
|
<text x="40.000000" y="100.000000">21</text>
|
||||||
|
<text x="48.038476" y="70.000000">22</text>
|
||||||
|
<text x="70.000000" y="48.038476">23</text>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<svg v-show="currentTab === TabType.Minute" ref="minuteSvgRef"
|
||||||
|
xmlns="http://www.w3.org/2000/svg" version="1.1"
|
||||||
|
preserveAspectRatio="xMidYMid" viewBox="0 0 200 200"
|
||||||
|
:style="{ fontSize: svgFontScale + 'em' }"
|
||||||
|
@mousedown="startDragMinute" @mousemove="draggingMinute" @mouseup="stopDragMinute"
|
||||||
|
@touchstart="startDragMinute" @touchmove="draggingMinute" @touchend="stopDragMinute">
|
||||||
|
<circle cx="100.000000" cy="100.000000" r="100.000000" type="background"></circle>
|
||||||
|
<line x1="100" y1="100" :x2="minuteHand.x2" :y2="minuteHand.y2"></line>
|
||||||
|
<circle :cx="minuteHand.cx" :cy="minuteHand.cy" r="1em" type="symbol"></circle>
|
||||||
|
|
||||||
|
<text x="100.000000" y="20.000000">0</text>
|
||||||
|
<text x="140.000000" y="30.717968">5</text>
|
||||||
|
<text x="169.282032" y="60.000000">10</text>
|
||||||
|
<text x="180.000000" y="100.000000">15</text>
|
||||||
|
<text x="169.282032" y="140.000000">20</text>
|
||||||
|
<text x="140.000000" y="169.282032">25</text>
|
||||||
|
<text x="100.000000" y="180.000000">30</text>
|
||||||
|
<text x="60.000000" y="169.282032">35</text>
|
||||||
|
<text x="30.717968" y="140.000000">40</text>
|
||||||
|
<text x="20.000000" y="100.000000">45</text>
|
||||||
|
<text x="30.717968" y="60.000000">50</text>
|
||||||
|
<text x="60.000000" y="30.717968">55</text>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<footer class="modal-card-foot">
|
||||||
|
<a class="button is-success" @click="confirm">
|
||||||
|
<span>OK</span>
|
||||||
|
</a>
|
||||||
|
<a class="button" @click="cancel">
|
||||||
|
<span>Cancel</span>
|
||||||
|
</a>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
div.perfectTable {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
div.perfectTable > div > div:nth-child(1) {
|
||||||
|
border-left: 1px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.perfectTable > div:nth-child(1) > div {
|
||||||
|
border-top: 1px solid black;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
div.perfectTable>div>div {
|
||||||
|
/*border-top: 0 solid black;
|
||||||
|
border-left: 0 solid black;
|
||||||
|
border-right: 1px solid black;
|
||||||
|
border-bottom: 1px solid black;
|
||||||
|
|
||||||
|
padding: 0.75em;*/
|
||||||
|
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-basis: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
padding-top: 1rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.perfectTable>div {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.perfectTable>div>div.picked {
|
||||||
|
background: hsl(171, 100%, 41%);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
div.pickerContainer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-flow: row;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.pickerContainer>div {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
flex-basis: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.pickerContainer>svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.pickerContainer>svg>text {
|
||||||
|
dominant-baseline: middle;
|
||||||
|
text-anchor: middle;
|
||||||
|
user-select: none;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.pickerContainer>svg>circle[type=background] {
|
||||||
|
stroke-width: 0;
|
||||||
|
fill: #d0d0d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.pickerContainer>svg>circle[type=symbol] {
|
||||||
|
stroke-width: 0;
|
||||||
|
fill: hsl(171, 100%, 41%);
|
||||||
|
/* $primary */
|
||||||
|
}
|
||||||
|
|
||||||
|
div.pickerContainer>svg>line {
|
||||||
|
stroke-width: 0.125em;
|
||||||
|
stroke: hsl(171, 100%, 41%);
|
||||||
|
/* $primary */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
header.pickerHeader {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row;
|
||||||
|
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-basis: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
header.pickerHeader>div {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column;
|
||||||
|
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-basis: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -5,14 +5,24 @@ const isVisible = ref(false);
|
|||||||
const title = ref<string>("");
|
const title = ref<string>("");
|
||||||
const content = ref<string>("");
|
const content = ref<string>("");
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'closed', isOk: boolean): void
|
||||||
|
}>()
|
||||||
|
|
||||||
const show = (_content: string, _title?: string) => {
|
const show = (_content: string, _title?: string) => {
|
||||||
title.value = _title ?? "Notification";
|
title.value = _title ?? "Notification";
|
||||||
content.value = _content;
|
content.value = _content;
|
||||||
isVisible.value = true;
|
isVisible.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hide = () => {
|
const close = () => {
|
||||||
isVisible.value = false;
|
isVisible.value = false;
|
||||||
|
emit('closed', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ok = () => {
|
||||||
|
isVisible.value = false;
|
||||||
|
emit('closed', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
@@ -26,13 +36,13 @@ defineExpose({
|
|||||||
<div class="modal-card">
|
<div class="modal-card">
|
||||||
<header class="modal-card-head">
|
<header class="modal-card-head">
|
||||||
<p class="modal-card-title">{{ title }}</p>
|
<p class="modal-card-title">{{ title }}</p>
|
||||||
<button class="delete" aria-label="close" @click="hide"></button>
|
<button class="delete" aria-label="close" @click="close"></button>
|
||||||
</header>
|
</header>
|
||||||
<div class="modal-card-body">
|
<div class="modal-card-body">
|
||||||
<p>{{ content }}</p>
|
<p>{{ content }}</p>
|
||||||
</div>
|
</div>
|
||||||
<footer class="modal-card-foot">
|
<footer class="modal-card-foot">
|
||||||
<button class="button is-success" @click="hide">OK</button>
|
<button class="button is-success" @click="ok">OK</button>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ const deleteItem = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const updateItem = () => {
|
const updateItem = () => {
|
||||||
let new_name = editingName.value;
|
const new_name = editingName.value;
|
||||||
editingName.value = "";
|
editingName.value = "";
|
||||||
isEditing.value = false;
|
isEditing.value = false;
|
||||||
emit('update', props.uuid, new_name);
|
emit('update', props.uuid, new_name);
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ type LoopStopRule = InfinityStopRule | DatetimeStopRule | TimesStopRule;
|
|||||||
export function resolveLoopRules4UI(strl: string): [LoopRule, LoopStopRule] | undefined {
|
export function resolveLoopRules4UI(strl: string): [LoopRule, LoopStopRule] | undefined {
|
||||||
if (strl == '') return undefined;
|
if (strl == '') return undefined;
|
||||||
|
|
||||||
let sp = strl.split('-');
|
const sp = strl.split('-');
|
||||||
if (sp.length != 2) return undefined;
|
if (sp.length != 2) return undefined;
|
||||||
let loopRules: LoopRule | undefined = undefined;
|
let loopRules: LoopRule | undefined = undefined;
|
||||||
let loopStopRules: LoopStopRule | undefined = undefined;
|
let loopStopRules: LoopStopRule | undefined = undefined;
|
||||||
@@ -130,35 +130,35 @@ export function resolveLoopRules2Event(
|
|||||||
Math.max(loopDateTimeEnd, eventDateTimeEnd)]
|
Math.max(loopDateTimeEnd, eventDateTimeEnd)]
|
||||||
];
|
];
|
||||||
|
|
||||||
let sp = fullLoopRules.split('-');
|
const sp = fullLoopRules.split('-');
|
||||||
if (sp.length != 2) return undefined;
|
if (sp.length != 2) return undefined;
|
||||||
let loopRules = sp[0]!; // we don't need consider stop flag
|
const loopRules = sp[0]!; // we don't need consider stop flag
|
||||||
let result: TimeRange[] = new Array();
|
const result: TimeRange[] = [];
|
||||||
|
|
||||||
// compute offset and duration
|
// compute offset and duration
|
||||||
let eventDateTime = new Date((eventDateTimeStart + timezoneOffset) * 60000);
|
const eventDateTime = new Date((eventDateTimeStart + timezoneOffset) * 60000);
|
||||||
eventDateTime.setUTCHours(0, 0, 0, 0);
|
eventDateTime.setUTCHours(0, 0, 0, 0);
|
||||||
let eventOffset = eventDateTimeStart - (Math.floor(eventDateTime.getTime() / 60000) - timezoneOffset);
|
const eventOffset = eventDateTimeStart - (Math.floor(eventDateTime.getTime() / 60000) - timezoneOffset);
|
||||||
let eventDuration = eventDateTimeEnd - eventDateTimeStart;
|
const eventDuration = eventDateTimeEnd - eventDateTimeStart;
|
||||||
|
|
||||||
let detectDateTime = new Date(loopDateTimeStart * 60000);
|
const detectDateTime = new Date(loopDateTimeStart * 60000);
|
||||||
detectDateTime.setUTCHours(0, 0, 0, 0);
|
detectDateTime.setUTCHours(0, 0, 0, 0);
|
||||||
let originalYear = eventDateTime.getUTCFullYear();
|
const originalYear = eventDateTime.getUTCFullYear();
|
||||||
let originalMonth = eventDateTime.getUTCMonth() + 1;
|
const originalMonth = eventDateTime.getUTCMonth() + 1;
|
||||||
let originalDay = eventDateTime.getUTCDate();
|
const originalDay = eventDateTime.getUTCDate();
|
||||||
|
|
||||||
// compute event
|
// compute event
|
||||||
let match: RegExpExecArray | null;
|
let match: RegExpExecArray | null;
|
||||||
if ((match = PRECOMPILED_LOOP_RULES.year.exec(loopRules)) !== null) {
|
if ((match = PRECOMPILED_LOOP_RULES.year.exec(loopRules)) !== null) {
|
||||||
let isStrict = match[1]! == 'S';
|
const isStrict = match[1]! == 'S';
|
||||||
let loopSpan = parseInt(match[2]!);
|
const loopSpan = parseInt(match[2]!);
|
||||||
|
|
||||||
let yearCount = detectDateTime.getFullYear() - originalYear;
|
const yearCount = detectDateTime.getFullYear() - originalYear;
|
||||||
let isSpecial = (originalMonth == 2 && originalDay == 29);
|
const isSpecial = (originalMonth == 2 && originalDay == 29);
|
||||||
let realLoopSpan = (isSpecial && isStrict) ? lcm(4, loopSpan) : loopSpan;
|
const realLoopSpan = (isSpecial && isStrict) ? lcm(4, loopSpan) : loopSpan;
|
||||||
|
|
||||||
//let fullSpanCount = Math.floor(yearCount / realLoopSpan);
|
//let fullSpanCount = Math.floor(yearCount / realLoopSpan);
|
||||||
let remainYear = yearCount % realLoopSpan;
|
const remainYear = yearCount % realLoopSpan;
|
||||||
//detectDateTime.setUTCFullYear(fullSpanCount + detectDateTime.getUTCFullYear(), 1, 1);
|
//detectDateTime.setUTCFullYear(fullSpanCount + detectDateTime.getUTCFullYear(), 1, 1);
|
||||||
if (remainYear != 0)
|
if (remainYear != 0)
|
||||||
detectDateTime.setUTCFullYear(realLoopSpan - remainYear + detectDateTime.getUTCFullYear(), 1 - 1, 1);
|
detectDateTime.setUTCFullYear(realLoopSpan - remainYear + detectDateTime.getUTCFullYear(), 1 - 1, 1);
|
||||||
@@ -189,22 +189,22 @@ export function resolveLoopRules2Event(
|
|||||||
}
|
}
|
||||||
|
|
||||||
} else if ((match = PRECOMPILED_LOOP_RULES.month.exec(loopRules)) !== null) {
|
} else if ((match = PRECOMPILED_LOOP_RULES.month.exec(loopRules)) !== null) {
|
||||||
let isStrict = match[1]! == 'S';
|
const isStrict = match[1]! == 'S';
|
||||||
let loopMethod = match[2]!;
|
const loopMethod = match[2]!;
|
||||||
let loopSpan = parseInt(match[3]!);
|
const loopSpan = parseInt(match[3]!);
|
||||||
|
|
||||||
let monthsCountValue = monthsCount(detectDateTime.getUTCFullYear(), detectDateTime.getUTCMonth() + 1) -
|
const monthsCountValue = monthsCount(detectDateTime.getUTCFullYear(), detectDateTime.getUTCMonth() + 1) -
|
||||||
monthsCount(originalYear, originalMonth);
|
monthsCount(originalYear, originalMonth);
|
||||||
|
|
||||||
//let fullSpanCount = Math.floor(monthsCountValue / loopSpan);
|
//let fullSpanCount = Math.floor(monthsCountValue / loopSpan);
|
||||||
let remainMonth = monthsCountValue % loopSpan;
|
const remainMonth = monthsCountValue % loopSpan;
|
||||||
//detectDateTime.setUTCMonth(fullSpanCount * loopSpan + detectDateTime.getUTCMonth(), 1);
|
//detectDateTime.setUTCMonth(fullSpanCount * loopSpan + detectDateTime.getUTCMonth(), 1);
|
||||||
detectDateTime.setUTCDate(1);
|
detectDateTime.setUTCDate(1);
|
||||||
if (remainMonth != 0)
|
if (remainMonth != 0)
|
||||||
detectDateTime.setUTCMonth(loopSpan - remainMonth + detectDateTime.getUTCMonth(), 1);
|
detectDateTime.setUTCMonth(loopSpan - remainMonth + detectDateTime.getUTCMonth(), 1);
|
||||||
|
|
||||||
while (Math.floor(detectDateTime.getTime() / 60000) + eventOffset - timezoneOffset <= loopDateTimeEnd) {
|
while (Math.floor(detectDateTime.getTime() / 60000) + eventOffset - timezoneOffset <= loopDateTimeEnd) {
|
||||||
let data = getRemanagedDayInMonth(originalYear, originalMonth, originalDay, detectDateTime.getUTCFullYear(), detectDateTime.getUTCMonth() + 1, isStrict);
|
const data = getRemanagedDayInMonth(originalYear, originalMonth, originalDay, detectDateTime.getUTCFullYear(), detectDateTime.getUTCMonth() + 1, isStrict);
|
||||||
let predictedDay: number | undefined = undefined;
|
let predictedDay: number | undefined = undefined;
|
||||||
switch (loopMethod) {
|
switch (loopMethod) {
|
||||||
case 'A':
|
case 'A':
|
||||||
@@ -233,15 +233,15 @@ export function resolveLoopRules2Event(
|
|||||||
|
|
||||||
|
|
||||||
} else if ((match = PRECOMPILED_LOOP_RULES.week.exec(loopRules)) !== null) {
|
} else if ((match = PRECOMPILED_LOOP_RULES.week.exec(loopRules)) !== null) {
|
||||||
let loopSpan = parseInt(match[2]!);
|
const loopSpan = parseInt(match[2]!);
|
||||||
let weekOption: boolean[] = [];
|
const weekOption: boolean[] = [];
|
||||||
let weekEventCount = 0
|
let weekEventCount = 0
|
||||||
for (let i = 0; i < 7; i++) {
|
for (let i = 0; i < 7; i++) {
|
||||||
weekOption.push(match[1]![i] == 'T');
|
weekOption.push(match[1]![i] == 'T');
|
||||||
if (match[1]![i] == 'T') weekEventCount++;
|
if (match[1]![i] == 'T') weekEventCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
let originalWeek = dayOfWeek(originalYear, originalMonth, originalDay);
|
const originalWeek = dayOfWeek(originalYear, originalMonth, originalDay);
|
||||||
|
|
||||||
// try insert original event
|
// try insert original event
|
||||||
if (!weekOption[originalWeek]) {
|
if (!weekOption[originalWeek]) {
|
||||||
@@ -250,11 +250,11 @@ export function resolveLoopRules2Event(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let daysCountValue = daysCount(detectDateTime.getUTCFullYear(), detectDateTime.getUTCMonth() + 1, detectDateTime.getDate()) -
|
const daysCountValue = daysCount(detectDateTime.getUTCFullYear(), detectDateTime.getUTCMonth() + 1, detectDateTime.getDate()) -
|
||||||
daysCount(originalYear, originalMonth, originalDay);
|
daysCount(originalYear, originalMonth, originalDay);
|
||||||
//let fullSpanCount = Math.floor(daysCountValue / (7 * loopSpan));
|
//let fullSpanCount = Math.floor(daysCountValue / (7 * loopSpan));
|
||||||
let remainFullSpanCount = Math.floor((daysCountValue % (7 * loopSpan)) / 7);
|
const remainFullSpanCount = Math.floor((daysCountValue % (7 * loopSpan)) / 7);
|
||||||
let remainDays = (daysCountValue % (7 * loopSpan)) % 7;
|
const remainDays = (daysCountValue % (7 * loopSpan)) % 7;
|
||||||
|
|
||||||
//detectDateTime.setUTCDate((7 * loopSpan * fullSpanCount) + detectDateTime.getUTCDate());
|
//detectDateTime.setUTCDate((7 * loopSpan * fullSpanCount) + detectDateTime.getUTCDate());
|
||||||
if (remainFullSpanCount != 0) {
|
if (remainFullSpanCount != 0) {
|
||||||
@@ -276,12 +276,12 @@ export function resolveLoopRules2Event(
|
|||||||
}
|
}
|
||||||
|
|
||||||
} else if ((match = PRECOMPILED_LOOP_RULES.day.exec(loopRules)) !== null) {
|
} else if ((match = PRECOMPILED_LOOP_RULES.day.exec(loopRules)) !== null) {
|
||||||
let loopSpan = parseInt(match[1]!);
|
const loopSpan = parseInt(match[1]!);
|
||||||
|
|
||||||
let daysCountValue = daysCount(detectDateTime.getUTCFullYear(), detectDateTime.getUTCMonth() + 1, detectDateTime.getUTCDate()) -
|
const daysCountValue = daysCount(detectDateTime.getUTCFullYear(), detectDateTime.getUTCMonth() + 1, detectDateTime.getUTCDate()) -
|
||||||
daysCount(originalYear, originalMonth, originalDay);
|
daysCount(originalYear, originalMonth, originalDay);
|
||||||
//let fullSpanCount = Math.floor(daysCountValue / loopSpan);
|
//let fullSpanCount = Math.floor(daysCountValue / loopSpan);
|
||||||
let remainDays = daysCountValue % loopSpan;
|
const remainDays = daysCountValue % loopSpan;
|
||||||
//detectDateTime.setUTCDate(fullSpanCount * loopSpan + detectDateTime.getUTCDate());
|
//detectDateTime.setUTCDate(fullSpanCount * loopSpan + detectDateTime.getUTCDate());
|
||||||
if (remainDays != 0)
|
if (remainDays != 0)
|
||||||
detectDateTime.setUTCDate(loopSpan - remainDays + detectDateTime.getUTCDate());
|
detectDateTime.setUTCDate(loopSpan - remainDays + detectDateTime.getUTCDate());
|
||||||
@@ -296,10 +296,10 @@ export function resolveLoopRules2Event(
|
|||||||
} else return undefined;
|
} else return undefined;
|
||||||
|
|
||||||
// clamp item
|
// clamp item
|
||||||
let realResult: TimeRange[] = new Array();
|
const realResult: TimeRange[] = [];
|
||||||
for (let i of result) {
|
for (const i of result) {
|
||||||
let start = i[0];
|
const start = i[0];
|
||||||
let end = i[1];
|
const end = i[1];
|
||||||
if (end > clampStartDateTime && start <= loopDateTimeEnd)
|
if (end > clampStartDateTime && start <= loopDateTimeEnd)
|
||||||
realResult.push([Math.max(start, clampStartDateTime), Math.min(end, loopDateTimeEnd)]);
|
realResult.push([Math.max(start, clampStartDateTime), Math.min(end, loopDateTimeEnd)]);
|
||||||
}
|
}
|
||||||
@@ -319,11 +319,11 @@ export function resolveLoopRules2Event(
|
|||||||
export function resolveLoopRules4Text(strl: string, startDateTime: number, timezoneOffset: number): string {
|
export function resolveLoopRules4Text(strl: string, startDateTime: number, timezoneOffset: number): string {
|
||||||
if (strl == '') return "";
|
if (strl == '') return "";
|
||||||
|
|
||||||
let sp = strl.split('-');
|
const sp = strl.split('-');
|
||||||
if (sp.length != 2) return "";
|
if (sp.length != 2) return "";
|
||||||
let loopRules: string;
|
let loopRules: string;
|
||||||
let loopStopRules: string;
|
let loopStopRules: string;
|
||||||
let datetimeInstance = new Date((startDateTime + timezoneOffset) * 60000)
|
const datetimeInstance = new Date((startDateTime + timezoneOffset) * 60000)
|
||||||
|
|
||||||
let match: RegExpExecArray | null;
|
let match: RegExpExecArray | null;
|
||||||
if ((match = PRECOMPILED_LOOP_RULES.year.exec(sp[0]!)) !== null) {
|
if ((match = PRECOMPILED_LOOP_RULES.year.exec(sp[0]!)) !== null) {
|
||||||
@@ -340,7 +340,7 @@ export function resolveLoopRules4Text(strl: string, startDateTime: number, timez
|
|||||||
else
|
else
|
||||||
loopRules = "宽松模式。";
|
loopRules = "宽松模式。";
|
||||||
|
|
||||||
let dayInMonth = getDayInMonth(
|
const dayInMonth = getDayInMonth(
|
||||||
datetimeInstance.getUTCFullYear(),
|
datetimeInstance.getUTCFullYear(),
|
||||||
datetimeInstance.getUTCMonth() + 1,
|
datetimeInstance.getUTCMonth() + 1,
|
||||||
datetimeInstance.getUTCDate());
|
datetimeInstance.getUTCDate());
|
||||||
@@ -367,7 +367,7 @@ export function resolveLoopRules4Text(strl: string, startDateTime: number, timez
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else if ((match = PRECOMPILED_LOOP_RULES.week.exec(sp[0]!)) !== null) {
|
} else if ((match = PRECOMPILED_LOOP_RULES.week.exec(sp[0]!)) !== null) {
|
||||||
let weekOfDayCache = [];
|
const weekOfDayCache = [];
|
||||||
for (let i = 0; i < 7; i++) {
|
for (let i = 0; i < 7; i++) {
|
||||||
if (match[1]![i] == 'T')
|
if (match[1]![i] == 'T')
|
||||||
weekOfDayCache.push(universalGetDayOfWeek(i));
|
weekOfDayCache.push(universalGetDayOfWeek(i));
|
||||||
@@ -414,7 +414,7 @@ function leapYearCountEx(endYear: number, includeThis: boolean, baseYear: number
|
|||||||
}
|
}
|
||||||
|
|
||||||
function daysCount(year: number, month: number, day: number): number {
|
function daysCount(year: number, month: number, day: number): number {
|
||||||
let ly = leapYearCountEx(year, false, 1, true);
|
const ly = leapYearCountEx(year, false, 1, true);
|
||||||
let days = 365 * (year - 1);
|
let days = 365 * (year - 1);
|
||||||
days += ly;
|
days += ly;
|
||||||
|
|
||||||
@@ -432,7 +432,7 @@ function monthsCount(year: number, month: number): number {
|
|||||||
return (year - 1) * 12 + (month - 1);
|
return (year - 1) * 12 + (month - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function dayOfWeek(year: number, month: number, day: number): number {
|
export function dayOfWeek(year: number, month: number, day: number): number {
|
||||||
return daysCount(year, month, day) % 7;
|
return daysCount(year, month, day) % 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -452,15 +452,15 @@ type DayInMonthInfo = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
function getDayInMonth(year: number, month: number, day: number): DayInMonthInfo {
|
function getDayInMonth(year: number, month: number, day: number): DayInMonthInfo {
|
||||||
let days = MONTH_DAY_COUNT[month - 1]! + ((month == 2 && isLeapYear(year)) ? 1 : 0);
|
const days = MONTH_DAY_COUNT[month - 1]! + ((month == 2 && isLeapYear(year)) ? 1 : 0);
|
||||||
let firstDayOfWeek = dayOfWeek(year, month, 1);
|
const firstDayOfWeek = dayOfWeek(year, month, 1);
|
||||||
let bilateralDayOfWeek = (firstDayOfWeek + day - 1) % 7;
|
const bilateralDayOfWeek = (firstDayOfWeek + day - 1) % 7;
|
||||||
|
|
||||||
let dayForwards = day;
|
const dayForwards = day;
|
||||||
let dayBackwards = days - day + 1;
|
const dayBackwards = days - day + 1;
|
||||||
|
|
||||||
let weeksForward = Math.floor((dayForwards - 1) / 7) + 1;
|
const weeksForward = Math.floor((dayForwards - 1) / 7) + 1;
|
||||||
let weeksBackwards = Math.floor((dayBackwards - 1) / 7) + 1;
|
const weeksBackwards = Math.floor((dayBackwards - 1) / 7) + 1;
|
||||||
|
|
||||||
return [dayForwards, dayBackwards, weeksForward, bilateralDayOfWeek, weeksBackwards, bilateralDayOfWeek];
|
return [dayForwards, dayBackwards, weeksForward, bilateralDayOfWeek, weeksBackwards, bilateralDayOfWeek];
|
||||||
}
|
}
|
||||||
@@ -493,10 +493,10 @@ type RemanagedDayInMonth = [
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
function getRemanagedDayInMonth(oldYear: number, oldMonth: number, oldDay: number, newYear: number, newMonth: number, isStrict: boolean): RemanagedDayInMonth {
|
function getRemanagedDayInMonth(oldYear: number, oldMonth: number, oldDay: number, newYear: number, newMonth: number, isStrict: boolean): RemanagedDayInMonth {
|
||||||
let ddata = getDayInMonth(oldYear, oldMonth, oldDay);
|
const ddata = getDayInMonth(oldYear, oldMonth, oldDay);
|
||||||
let mdata = getMonthWeekStatistics(newYear, newMonth);
|
const mdata = getMonthWeekStatistics(newYear, newMonth);
|
||||||
let days = MONTH_DAY_COUNT[newMonth - 1]! + ((newMonth == 2 && isLeapYear(newYear)) ? 1 : 0);
|
const days = MONTH_DAY_COUNT[newMonth - 1]! + ((newMonth == 2 && isLeapYear(newYear)) ? 1 : 0);
|
||||||
let firstDayOfWeek = dayOfWeek(newYear, newMonth, 1);
|
const firstDayOfWeek = dayOfWeek(newYear, newMonth, 1);
|
||||||
//let lastDayOfWeek = (firstDayOfWeek + days - 1) % 7;
|
//let lastDayOfWeek = (firstDayOfWeek + days - 1) % 7;
|
||||||
|
|
||||||
let methodA = undefined;
|
let methodA = undefined;
|
||||||
@@ -511,14 +511,14 @@ function getRemanagedDayInMonth(oldYear: number, oldMonth: number, oldDay: numbe
|
|||||||
|
|
||||||
let methodC = undefined;
|
let methodC = undefined;
|
||||||
if (ddata[2] <= mdata[ddata[3]]! || !isStrict) {
|
if (ddata[2] <= mdata[ddata[3]]! || !isStrict) {
|
||||||
let targetWeek = Math.min(ddata[2], mdata[ddata[3]]!);
|
const targetWeek = Math.min(ddata[2], mdata[ddata[3]]!);
|
||||||
methodC = 1 + (targetWeek - 1) * 7 + ((ddata[3] + 7 - firstDayOfWeek) % 7);
|
methodC = 1 + (targetWeek - 1) * 7 + ((ddata[3] + 7 - firstDayOfWeek) % 7);
|
||||||
}
|
}
|
||||||
|
|
||||||
let methodD = undefined;
|
let methodD = undefined;
|
||||||
if (ddata[4] <= mdata[ddata[5]]! || !isStrict) {
|
if (ddata[4] <= mdata[ddata[5]]! || !isStrict) {
|
||||||
// convert to type c and calc
|
// convert to type c and calc
|
||||||
let targetWeek = mdata[ddata[5]]! - Math.min(ddata[4], mdata[ddata[5]]!) + 1;
|
const targetWeek = mdata[ddata[5]]! - Math.min(ddata[4], mdata[ddata[5]]!) + 1;
|
||||||
methodD = 1 + (targetWeek - 1) * 7 + ((ddata[5] + 7 - firstDayOfWeek) % 7);
|
methodD = 1 + (targetWeek - 1) * 7 + ((ddata[5] + 7 - firstDayOfWeek) % 7);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -543,10 +543,10 @@ type MonthWeekStatistics = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
function getMonthWeekStatistics(year: number, month: number): MonthWeekStatistics {
|
function getMonthWeekStatistics(year: number, month: number): MonthWeekStatistics {
|
||||||
let days = MONTH_DAY_COUNT[month - 1]! + ((month == 2 && isLeapYear(year)) ? 1 : 0);
|
const days = MONTH_DAY_COUNT[month - 1]! + ((month == 2 && isLeapYear(year)) ? 1 : 0);
|
||||||
let firstDayOfWeek = dayOfWeek(year, month, 1);
|
const firstDayOfWeek = dayOfWeek(year, month, 1);
|
||||||
|
|
||||||
let result: MonthWeekStatistics = [4, 4, 4, 4, 4, 4, 4];
|
const result: MonthWeekStatistics = [4, 4, 4, 4, 4, 4, 4];
|
||||||
let remain = days % 7;
|
let remain = days % 7;
|
||||||
let week = firstDayOfWeek;
|
let week = firstDayOfWeek;
|
||||||
while (remain > 0) {
|
while (remain > 0) {
|
||||||
@@ -558,7 +558,7 @@ function getMonthWeekStatistics(year: number, month: number): MonthWeekStatistic
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isLeapYear(year: number): boolean {
|
export function isLeapYear(year: number): boolean {
|
||||||
let isLeap = false;
|
let isLeap = false;
|
||||||
if (year % 4 == 0) isLeap = true;
|
if (year % 4 == 0) isLeap = true;
|
||||||
if (year % 100 == 0) isLeap = false;
|
if (year % 100 == 0) isLeap = false;
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export function format(str: string, ...args: any[]): string {
|
|||||||
* @returns The zero-based weekday. 0 stands for Monday.
|
* @returns The zero-based weekday. 0 stands for Monday.
|
||||||
*/
|
*/
|
||||||
export function getWeekday(date: Date): number {
|
export function getWeekday(date: Date): number {
|
||||||
let day = date.getDay();
|
const day = date.getDay();
|
||||||
if (day == 0) return 6;
|
if (day == 0) return 6;
|
||||||
else return day - 1;
|
else return day - 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,13 +37,13 @@ const refreshOwnedItem = async () => {
|
|||||||
ownedItems.value.clear();
|
ownedItems.value.clear();
|
||||||
|
|
||||||
// Fetch data
|
// Fetch data
|
||||||
let rv = await apiCollectionGetFullOwn(token.currentToken);
|
const rv = await apiCollectionGetFullOwn(token.currentToken);
|
||||||
if (typeof rv === 'undefined') {
|
if (typeof rv === 'undefined') {
|
||||||
console.error("fail to fetch owned collection");
|
console.error("fail to fetch owned collection");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Add into dict for rendering
|
// Add into dict for rendering
|
||||||
for (let item of rv) {
|
for (const item of rv) {
|
||||||
ownedItems.value.set(item[0], item);
|
ownedItems.value.set(item[0], item);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,20 +53,20 @@ const refreshOwnedItem = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const addOwnedItem = async () => {
|
const addOwnedItem = async () => {
|
||||||
let new_name = pendingOwnedItemName.value;
|
const new_name = pendingOwnedItemName.value;
|
||||||
if (new_name === '') {
|
if (new_name === '') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// first add it
|
// first add it
|
||||||
let rv = await apiCollectionAddOwn(token.currentToken, new_name);
|
const rv = await apiCollectionAddOwn(token.currentToken, new_name);
|
||||||
if (typeof rv === 'undefined') {
|
if (typeof rv === 'undefined') {
|
||||||
messagebox.value?.show("An add operation failed. It may caused by wrong arguments. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data.");
|
messagebox.value?.show("An add operation failed. It may caused by wrong arguments. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// second get its detail
|
// second get its detail
|
||||||
let rv2 = await apiCollectionGetDetailOwn(token.currentToken, rv);
|
const rv2 = await apiCollectionGetDetailOwn(token.currentToken, rv);
|
||||||
if (typeof rv2 === 'undefined') {
|
if (typeof rv2 === 'undefined') {
|
||||||
messagebox.value?.show("A get operation failed. It may caused by server internal error or your limited permission. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data.");
|
messagebox.value?.show("A get operation failed. It may caused by server internal error or your limited permission. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data.");
|
||||||
return;
|
return;
|
||||||
@@ -77,7 +77,7 @@ const addOwnedItem = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const deleteOwnedItem = async (uuid: string) => {
|
const deleteOwnedItem = async (uuid: string) => {
|
||||||
let rv = await apiCollectionDeleteOwn(token.currentToken, uuid, ownedItems.value.get(uuid)![2]);
|
const rv = await apiCollectionDeleteOwn(token.currentToken, uuid, ownedItems.value.get(uuid)![2]);
|
||||||
if (rv) {
|
if (rv) {
|
||||||
messagebox.value?.show("A delete operation failed. It may caused by no matched item. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data.");
|
messagebox.value?.show("A delete operation failed. It may caused by no matched item. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data.");
|
||||||
return;
|
return;
|
||||||
@@ -97,7 +97,7 @@ const deleteOwnedItem = async (uuid: string) => {
|
|||||||
const shareOwnedItem = async (uuid: string) => {
|
const shareOwnedItem = async (uuid: string) => {
|
||||||
// if there is no editing sharing target, set it.
|
// if there is no editing sharing target, set it.
|
||||||
// otherwise override it or toggle it according to its old value.
|
// otherwise override it or toggle it according to its old value.
|
||||||
let old_value = sharingItemUuid.value;
|
const old_value = sharingItemUuid.value;
|
||||||
if (typeof old_value === 'string') {
|
if (typeof old_value === 'string') {
|
||||||
if (old_value === uuid) {
|
if (old_value === uuid) {
|
||||||
sharingItemUuid.value = null;
|
sharingItemUuid.value = null;
|
||||||
@@ -112,15 +112,15 @@ const shareOwnedItem = async (uuid: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const updateOwnedItem = async (uuid: string, name: string) => {
|
const updateOwnedItem = async (uuid: string, name: string) => {
|
||||||
let lastChange = ownedItems.value.get(uuid)![2];
|
const lastChange = ownedItems.value.get(uuid)![2];
|
||||||
let rv = await apiCollectionUpdateOwn(token.currentToken, uuid, name, lastChange);
|
const rv = await apiCollectionUpdateOwn(token.currentToken, uuid, name, lastChange);
|
||||||
if (typeof rv === 'undefined') {
|
if (typeof rv === 'undefined') {
|
||||||
messagebox.value?.show("An update operation failed. It may caused by wrong arguments or lost target. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data.");
|
messagebox.value?.show("An update operation failed. It may caused by wrong arguments or lost target. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// return value is new lastChange, so we update it.
|
// return value is new lastChange, so we update it.
|
||||||
let row = ownedItems.value.get(uuid)!;
|
const row = ownedItems.value.get(uuid)!;
|
||||||
row[1] = name;
|
row[1] = name;
|
||||||
row[2] = rv;
|
row[2] = rv;
|
||||||
|
|
||||||
@@ -140,8 +140,8 @@ const refreshSharingItem = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// fetch data
|
// fetch data
|
||||||
let uuid = sharingItemUuid.value!;
|
const uuid = sharingItemUuid.value!;
|
||||||
let rv = await apiCollectionGetSharing(token.currentToken, uuid);
|
const rv = await apiCollectionGetSharing(token.currentToken, uuid);
|
||||||
if (typeof rv === 'undefined') {
|
if (typeof rv === 'undefined') {
|
||||||
console.error(`fail to fetch sharing target for collection ${uuid}`);
|
console.error(`fail to fetch sharing target for collection ${uuid}`);
|
||||||
return;
|
return;
|
||||||
@@ -152,15 +152,15 @@ const refreshSharingItem = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const addSharingItem = async () => {
|
const addSharingItem = async () => {
|
||||||
let uuid = sharingItemUuid.value!;
|
const uuid = sharingItemUuid.value!;
|
||||||
let lastChange = ownedItems.value.get(uuid)![2];
|
const lastChange = ownedItems.value.get(uuid)![2];
|
||||||
let new_username = pendingSharingItemUsername.value;
|
const new_username = pendingSharingItemUsername.value;
|
||||||
if (new_username === '') {
|
if (new_username === '') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// order adding
|
// order adding
|
||||||
let rv = await apiCollectionAddSharing(token.currentToken, uuid, new_username, lastChange);
|
const rv = await apiCollectionAddSharing(token.currentToken, uuid, new_username, lastChange);
|
||||||
if (typeof rv === 'undefined') {
|
if (typeof rv === 'undefined') {
|
||||||
messagebox.value?.show("An add operation failed. It may caused by wrong arguments. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data.");
|
messagebox.value?.show("An add operation failed. It may caused by wrong arguments. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data.");
|
||||||
return;
|
return;
|
||||||
@@ -173,11 +173,11 @@ const addSharingItem = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const deleteSharingItem = async (username: string) => {
|
const deleteSharingItem = async (username: string) => {
|
||||||
let uuid = sharingItemUuid.value!;
|
const uuid = sharingItemUuid.value!;
|
||||||
let lastChange = ownedItems.value.get(uuid)![2];
|
const lastChange = ownedItems.value.get(uuid)![2];
|
||||||
|
|
||||||
// order deleting
|
// order deleting
|
||||||
let rv = await apiCollectionDeleteSharing(token.currentToken, uuid, username, lastChange);
|
const rv = await apiCollectionDeleteSharing(token.currentToken, uuid, username, lastChange);
|
||||||
if (typeof rv === 'undefined') {
|
if (typeof rv === 'undefined') {
|
||||||
messagebox.value?.show("A delete operation failed. It may caused by no matched item. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data.");
|
messagebox.value?.show("A delete operation failed. It may caused by no matched item. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data.");
|
||||||
return;
|
return;
|
||||||
|
|||||||
Reference in New Issue
Block a user