Refine category selection styling

This commit is contained in:
openclaw 2026-07-24 00:43:57 +08:00
parent c2bb2e63a2
commit 001847fa45
3 changed files with 73 additions and 19 deletions

View File

@ -25,6 +25,7 @@ const emit = defineEmits<{
}>();
const FOCUS_ANGLE = 270;
const RING_GAP = 9;
const wheelEntryTypes = [...entryTypes, ...entryTypes, ...entryTypes];
const selectorLevel = ref<SelectorLevel>("type");
const wheelStage = ref<HTMLElement | null>(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<HTMLElement>(".app-shell");
if (shell) shell.scrollLeft = 0;

View File

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

View File

@ -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() {
</header>
<div v-if="categorySheetMode === 'parent'" class="entry-category-grid">
<button
type="button"
:class="{ active: !parentCategoryId }"
:style="{ '--category-color': '#6f7f83', '--category-tint': '#edf2f1' }"
@click="clearCategory"
>
<span><X :size="23" /></span>
<small>不选择</small>
<Check v-if="!parentCategoryId" :size="15" />
</button>
<button
v-for="category in parentCategories"
:key="category.id"