Fix:修复日期时间编辑器箭头按钮不立即更新 (#2310)
Co-authored-by: staff <staff@qimaos-MacBook-Pro.local>
This commit is contained in:
parent
74165cc045
commit
4780966d21
|
|
@ -1,10 +1,10 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, nextTick, onMounted, ref } from "vue";
|
||||
import { computed, nextTick, onMounted, ref, watch } from "vue";
|
||||
import type { FocusOutsideEvent, PointerDownOutsideEvent } from "reka-ui";
|
||||
import { CalendarClock, ChevronDown, ChevronUp, CircleSlash } from "@lucide/vue";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { formatTemporalInputValue, type TemporalCellEditorKind } from "@/lib/dataGridTemporalEditor";
|
||||
import { formatTemporalInputValue, stepTemporalInputValue, type TemporalCellEditorKind } from "@/lib/dataGridTemporalEditor";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
|
@ -29,19 +29,20 @@ const emit = defineEmits<{
|
|||
|
||||
const open = ref(true);
|
||||
const triggerRef = ref<HTMLButtonElement | null>(null);
|
||||
const localValue = ref(props.modelValue);
|
||||
let closeHandled = false;
|
||||
let isCommitting = false;
|
||||
|
||||
const hasDate = computed(() => props.kind !== "time");
|
||||
const hasTime = computed(() => props.kind !== "date");
|
||||
const displayValue = computed(() => props.modelValue || "NULL");
|
||||
const displayValue = computed(() => localValue.value || "NULL");
|
||||
const triggerClass = computed(() =>
|
||||
props.variant === "inline"
|
||||
? "cell-edit-input flex h-9 w-full items-center gap-2 rounded border bg-background px-2 text-left text-xs outline-none hover:border-primary/60 focus:border-primary"
|
||||
: ["cell-edit-input absolute inset-0 z-10 flex items-center gap-1 border-2 border-primary bg-background py-0 text-left text-xs outline-none", props.cellLayout === "transpose" ? "px-1.5" : "px-2.5"],
|
||||
);
|
||||
const dateParts = computed(() => {
|
||||
const text = formatTemporalInputValue(props.modelValue, "date");
|
||||
const text = formatTemporalInputValue(localValue.value, "date");
|
||||
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(text);
|
||||
if (!match) {
|
||||
const now = new Date();
|
||||
|
|
@ -51,8 +52,8 @@ const dateParts = computed(() => {
|
|||
});
|
||||
|
||||
const timeValue = computed(() => {
|
||||
if (props.kind === "time") return formatTemporalInputValue(props.modelValue, "time") || "00:00:00";
|
||||
return formatTemporalInputValue(props.modelValue, "datetime").split("T")[1] || "00:00:00";
|
||||
if (props.kind === "time") return formatTemporalInputValue(localValue.value, "time") || "00:00:00";
|
||||
return formatTemporalInputValue(localValue.value, "datetime").split("T")[1] || "00:00:00";
|
||||
});
|
||||
|
||||
const timeParts = computed(() => {
|
||||
|
|
@ -64,12 +65,20 @@ onMounted(() => {
|
|||
nextTick(() => triggerRef.value?.focus());
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(value) => {
|
||||
localValue.value = value;
|
||||
},
|
||||
);
|
||||
|
||||
function setOpen(value: boolean) {
|
||||
open.value = value;
|
||||
if (!value && props.commitOnClose && !closeHandled) finishCommit();
|
||||
}
|
||||
|
||||
function setModelValue(value: string) {
|
||||
localValue.value = value;
|
||||
emit("update:modelValue", value);
|
||||
}
|
||||
|
||||
|
|
@ -90,7 +99,7 @@ function updateDateFromInput(part: "day" | "month" | "year", event: Event) {
|
|||
}
|
||||
|
||||
function stepDate(part: "day" | "month" | "year", delta: number) {
|
||||
updateDate(part, dateParts.value[part] + delta);
|
||||
setModelValue(stepTemporalInputValue(localValue.value, props.kind, part, delta));
|
||||
}
|
||||
|
||||
function updateTime(part: "hour" | "minute" | "second", rawValue: string | number) {
|
||||
|
|
@ -108,9 +117,7 @@ function updateTimeFromInput(part: "hour" | "minute" | "second", event: Event) {
|
|||
}
|
||||
|
||||
function stepTime(part: "hour" | "minute" | "second", delta: number) {
|
||||
const max = part === "hour" ? 23 : 59;
|
||||
const current = Number(timeParts.value[part]) || 0;
|
||||
updateTime(part, (current + delta + max + 1) % (max + 1));
|
||||
setModelValue(stepTemporalInputValue(localValue.value, props.kind, part, delta));
|
||||
}
|
||||
|
||||
function flushInputValue(target: EventTarget | null) {
|
||||
|
|
|
|||
|
|
@ -43,6 +43,29 @@ export function parseTemporalInputValue(value: string, kind: TemporalCellEditorK
|
|||
return date && normalizedTime ? `${date} ${normalizedTime}` : text.replace("T", " ");
|
||||
}
|
||||
|
||||
export type TemporalCellEditorPart = "year" | "month" | "day" | "hour" | "minute" | "second";
|
||||
|
||||
export function stepTemporalInputValue(value: string, kind: TemporalCellEditorKind, part: TemporalCellEditorPart, delta: number): string {
|
||||
const dateParts = temporalDateParts(value);
|
||||
const timeParts = temporalTimeParts(value, kind);
|
||||
|
||||
if (part === "year") dateParts.year = Math.max(1, Math.min(9999, dateParts.year + delta));
|
||||
else if (part === "month") dateParts.month = Math.max(1, Math.min(12, dateParts.month + delta));
|
||||
else if (part === "day") dateParts.day = Math.max(1, Math.min(31, dateParts.day + delta));
|
||||
else {
|
||||
const max = part === "hour" ? 23 : 59;
|
||||
timeParts[part] = (timeParts[part] + delta + max + 1) % (max + 1);
|
||||
}
|
||||
|
||||
dateParts.day = Math.max(1, Math.min(daysInMonth(dateParts.year, dateParts.month), dateParts.day));
|
||||
const dateText = [String(dateParts.year).padStart(4, "0"), String(dateParts.month).padStart(2, "0"), String(dateParts.day).padStart(2, "0")].join("-");
|
||||
const timeText = [timeParts.hour, timeParts.minute, timeParts.second].map((item) => String(item).padStart(2, "0")).join(":");
|
||||
|
||||
if (kind === "date") return dateText;
|
||||
if (kind === "time") return timeText;
|
||||
return `${dateText} ${timeText}`;
|
||||
}
|
||||
|
||||
function normalizeTemporalType(dataType: string | undefined): string {
|
||||
return (dataType ?? "").toLowerCase().replace(/_/g, "").replace(/[(),]/g, " ").replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
|
@ -52,3 +75,23 @@ function normalizeTimeInput(text: string): string {
|
|||
if (!match) return "";
|
||||
return `${match[1]}:${match[2] ?? "00"}`;
|
||||
}
|
||||
|
||||
function temporalDateParts(value: string): { year: number; month: number; day: number } {
|
||||
const text = formatTemporalInputValue(value, "date");
|
||||
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(text);
|
||||
if (!match) {
|
||||
const now = new Date();
|
||||
return { year: now.getFullYear(), month: now.getMonth() + 1, day: now.getDate() };
|
||||
}
|
||||
return { year: Number(match[1]), month: Number(match[2]), day: Number(match[3]) };
|
||||
}
|
||||
|
||||
function temporalTimeParts(value: string, kind: TemporalCellEditorKind): { hour: number; minute: number; second: number } {
|
||||
const time = kind === "time" ? formatTemporalInputValue(value, "time") || "00:00:00" : formatTemporalInputValue(value, "datetime").split("T")[1] || "00:00:00";
|
||||
const [hour = "00", minute = "00", second = "00"] = time.split(":");
|
||||
return { hour: Number(hour) || 0, minute: Number(minute) || 0, second: Number(second) || 0 };
|
||||
}
|
||||
|
||||
function daysInMonth(year: number, month: number): number {
|
||||
return new Date(year, month, 0).getDate();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
import { strict as assert } from "node:assert";
|
||||
import { test } from "vitest";
|
||||
import { stepTemporalInputValue } from "../../apps/desktop/src/lib/dataGridTemporalEditor.ts";
|
||||
|
||||
test("steps datetime date and time parts", () => {
|
||||
assert.equal(stepTemporalInputValue("2025-07-01 13:41:49", "datetime", "day", 1), "2025-07-02 13:41:49");
|
||||
assert.equal(stepTemporalInputValue("2025-07-01 13:41:49", "datetime", "month", 1), "2025-08-01 13:41:49");
|
||||
assert.equal(stepTemporalInputValue("2025-07-01 13:41:49", "datetime", "hour", 1), "2025-07-01 14:41:49");
|
||||
assert.equal(stepTemporalInputValue("2025-07-01 13:41:49", "datetime", "minute", -1), "2025-07-01 13:40:49");
|
||||
assert.equal(stepTemporalInputValue("2025-07-01 13:41:49", "datetime", "second", 1), "2025-07-01 13:41:50");
|
||||
});
|
||||
|
||||
test("clamps stepped dates to the target month", () => {
|
||||
assert.equal(stepTemporalInputValue("2025-01-31", "date", "month", 1), "2025-02-28");
|
||||
assert.equal(stepTemporalInputValue("2024-01-31", "date", "month", 1), "2024-02-29");
|
||||
});
|
||||
|
||||
test("wraps stepped time values", () => {
|
||||
assert.equal(stepTemporalInputValue("23:59:59", "time", "hour", 1), "00:59:59");
|
||||
assert.equal(stepTemporalInputValue("23:59:59", "time", "minute", 1), "23:00:59");
|
||||
assert.equal(stepTemporalInputValue("23:59:59", "time", "second", 1), "23:59:00");
|
||||
});
|
||||
Loading…
Reference in New Issue