From 001847fa45809c6e7b01c183ade8c49a55fcfc23 Mon Sep 17 00:00:00 2001 From: openclaw Date: Fri, 24 Jul 2026 00:43:57 +0800 Subject: [PATCH] Refine category selection styling --- .../components/CategoryCoordinateSelector.vue | 42 ++++++++++++++----- apps/web/src/styles.css | 28 +++++++++++-- apps/web/src/views/EntryDetailView.vue | 22 +++++++--- 3 files changed, 73 insertions(+), 19 deletions(-) diff --git a/apps/web/src/components/CategoryCoordinateSelector.vue b/apps/web/src/components/CategoryCoordinateSelector.vue index b2390f4..20089b7 100644 --- a/apps/web/src/components/CategoryCoordinateSelector.vue +++ b/apps/web/src/components/CategoryCoordinateSelector.vue @@ -25,6 +25,7 @@ const emit = defineEmits<{ }>(); const FOCUS_ANGLE = 270; +const RING_GAP = 9; const wheelEntryTypes = [...entryTypes, ...entryTypes, ...entryTypes]; const selectorLevel = ref("type"); const wheelStage = ref(null); @@ -58,18 +59,14 @@ const levelLabel = computed(() => { return `选择${selectedParent.value?.label ?? ""}细类`; }); const canGoBack = computed(() => selectorLevel.value !== "type"); -const ringBackground = computed(() => { - const options = selectorOptions.value; - if (!options.length) return "transparent"; - const step = segmentAngle.value; - const stops = options - .map((option, index) => `${option.color} ${index * step}deg ${(index + 1) * step}deg`) - .join(","); - return `conic-gradient(from ${-step / 2}deg,${stops})`; -}); +const ringBackground = computed(() => createConicBackground((option) => option.color)); +const ringBorderBackground = computed(() => + createConicBackground((option) => darkenHexColor(option.color)), +); const rotorStyle = computed(() => ({ "--wheel-rotation": `${wheelRotation.value}deg`, "--ring-background": ringBackground.value, + "--ring-border-background": ringBorderBackground.value, })); watch( @@ -135,7 +132,13 @@ function chooseWheelSegment(event: MouseEvent) { if (!bounds || !selectorOptions.value.length) return; const offsetX = event.clientX - (bounds.left + bounds.width / 2); const offsetY = event.clientY - (bounds.top + bounds.height / 2); - if (Math.hypot(offsetX, offsetY) > bounds.width / 2) return; + const radius = Math.hypot(offsetX, offsetY); + const centerBounds = wheelStage.value + ?.closest(".category-wheel-selector") + ?.querySelector(".wheel-center-controls") + ?.getBoundingClientRect(); + const innerRadius = (centerBounds?.width ?? 0) / 2 + RING_GAP; + if (radius < innerRadius || radius > bounds.width / 2) return; const screenAngle = (Math.atan2(offsetY, offsetX) * 180) / Math.PI; const conicAngle = normalizeCircleAngle(screenAngle + 90 - wheelRotation.value); @@ -260,6 +263,25 @@ function normalizeCircleAngle(value: number) { return ((value % 360) + 360) % 360; } +function createConicBackground(colorFor: (option: SelectorOption) => string) { + const options = selectorOptions.value; + if (!options.length) return "transparent"; + const step = segmentAngle.value; + const stops = options + .map((option, index) => `${colorFor(option)} ${index * step}deg ${(index + 1) * step}deg`) + .join(","); + return `conic-gradient(from ${-step / 2}deg,${stops})`; +} + +function darkenHexColor(hex: string) { + const normalized = hex.replace("#", ""); + if (!/^[\da-f]{6}$/i.test(normalized)) return hex; + const channels = [0, 2, 4].map((offset) => + Math.round(Number.parseInt(normalized.slice(offset, offset + 2), 16) * 0.82), + ); + return `#${channels.map((channel) => channel.toString(16).padStart(2, "0")).join("")}`; +} + function resetShellScroll() { const shell = wheelStage.value?.closest(".app-shell"); if (shell) shell.scrollLeft = 0; diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index d281f97..4ad9e59 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -971,6 +971,7 @@ input:focus-visible { .category-wheel-selector { --wheel-size: clamp(286px, 88vw, 344px); --center-size: clamp(122px, 35vw, 140px); + --ring-gap: 9px; --option-radius: calc((var(--wheel-size) + var(--center-size)) / 4); position: absolute; top: 0; @@ -1027,13 +1028,32 @@ input:focus-visible { box-shadow: 0 14px 34px rgba(29, 55, 48, 0.24); -webkit-mask: radial-gradient( circle, - transparent 0 calc(var(--center-size) / 2), - #000 calc(var(--center-size) / 2 + 1px) + transparent 0 calc(var(--center-size) / 2 + var(--ring-gap)), + #000 calc(var(--center-size) / 2 + var(--ring-gap) + 1px) ); mask: radial-gradient( circle, - transparent 0 calc(var(--center-size) / 2), - #000 calc(var(--center-size) / 2 + 1px) + transparent 0 calc(var(--center-size) / 2 + var(--ring-gap)), + #000 calc(var(--center-size) / 2 + var(--ring-gap) + 1px) + ); +} + +.category-wheel-disc::after { + position: absolute; + inset: 0; + border-radius: inherit; + background: var(--ring-border-background); + pointer-events: none; + content: ""; + -webkit-mask: radial-gradient( + circle closest-side, + transparent 0 calc(100% - 2px), + #000 calc(100% - 2px) + ); + mask: radial-gradient( + circle closest-side, + transparent 0 calc(100% - 2px), + #000 calc(100% - 2px) ); } diff --git a/apps/web/src/views/EntryDetailView.vue b/apps/web/src/views/EntryDetailView.vue index 9135eca..ba686f7 100644 --- a/apps/web/src/views/EntryDetailView.vue +++ b/apps/web/src/views/EntryDetailView.vue @@ -130,6 +130,12 @@ function chooseParent(category: Category) { categorySheetMode.value = null; } +function clearCategory() { + parentCategoryId.value = ""; + childCategoryId.value = ""; + categorySheetMode.value = null; +} + function chooseChild(category: Category) { childCategoryId.value = category.id; categorySheetMode.value = null; @@ -151,10 +157,6 @@ async function saveEntry() { errorMessage.value = "请输入有效金额"; return; } - if (!parentCategoryId.value) { - errorMessage.value = "请选择大类"; - return; - } const occurredDate = new Date(occurredAt.value); if (timeInvalid.value) { errorMessage.value = "请选择时间"; @@ -171,7 +173,7 @@ async function saveEntry() { amount: cents, baseAmount: Math.round(cents * exchangeRate), ledgerIds: [...ledgerIds.value], - categoryId: childCategoryId.value || parentCategoryId.value, + categoryId: childCategoryId.value || parentCategoryId.value || entryType.value, note: note.value.trim(), occurredAt: occurredDate.toISOString(), }); @@ -317,6 +319,16 @@ async function deleteEntry() {