Fix:修复日期时间编辑器箭头按钮不立即更新 (#2310)

Co-authored-by: staff <staff@qimaos-MacBook-Pro.local>
This commit is contained in:
zipg 2026-07-01 15:21:25 +08:00 committed by GitHub
parent 74165cc045
commit 4780966d21
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 82 additions and 10 deletions

View File

@ -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) {

View File

@ -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();
}

View File

@ -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");
});