Refine category selection styling
This commit is contained in:
parent
c2bb2e63a2
commit
001847fa45
|
|
@ -25,6 +25,7 @@ const emit = defineEmits<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const FOCUS_ANGLE = 270;
|
const FOCUS_ANGLE = 270;
|
||||||
|
const RING_GAP = 9;
|
||||||
const wheelEntryTypes = [...entryTypes, ...entryTypes, ...entryTypes];
|
const wheelEntryTypes = [...entryTypes, ...entryTypes, ...entryTypes];
|
||||||
const selectorLevel = ref<SelectorLevel>("type");
|
const selectorLevel = ref<SelectorLevel>("type");
|
||||||
const wheelStage = ref<HTMLElement | null>(null);
|
const wheelStage = ref<HTMLElement | null>(null);
|
||||||
|
|
@ -58,18 +59,14 @@ const levelLabel = computed(() => {
|
||||||
return `选择${selectedParent.value?.label ?? ""}细类`;
|
return `选择${selectedParent.value?.label ?? ""}细类`;
|
||||||
});
|
});
|
||||||
const canGoBack = computed(() => selectorLevel.value !== "type");
|
const canGoBack = computed(() => selectorLevel.value !== "type");
|
||||||
const ringBackground = computed(() => {
|
const ringBackground = computed(() => createConicBackground((option) => option.color));
|
||||||
const options = selectorOptions.value;
|
const ringBorderBackground = computed(() =>
|
||||||
if (!options.length) return "transparent";
|
createConicBackground((option) => darkenHexColor(option.color)),
|
||||||
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 rotorStyle = computed(() => ({
|
const rotorStyle = computed(() => ({
|
||||||
"--wheel-rotation": `${wheelRotation.value}deg`,
|
"--wheel-rotation": `${wheelRotation.value}deg`,
|
||||||
"--ring-background": ringBackground.value,
|
"--ring-background": ringBackground.value,
|
||||||
|
"--ring-border-background": ringBorderBackground.value,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
|
|
@ -135,7 +132,13 @@ function chooseWheelSegment(event: MouseEvent) {
|
||||||
if (!bounds || !selectorOptions.value.length) return;
|
if (!bounds || !selectorOptions.value.length) return;
|
||||||
const offsetX = event.clientX - (bounds.left + bounds.width / 2);
|
const offsetX = event.clientX - (bounds.left + bounds.width / 2);
|
||||||
const offsetY = event.clientY - (bounds.top + bounds.height / 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 screenAngle = (Math.atan2(offsetY, offsetX) * 180) / Math.PI;
|
||||||
const conicAngle = normalizeCircleAngle(screenAngle + 90 - wheelRotation.value);
|
const conicAngle = normalizeCircleAngle(screenAngle + 90 - wheelRotation.value);
|
||||||
|
|
@ -260,6 +263,25 @@ function normalizeCircleAngle(value: number) {
|
||||||
return ((value % 360) + 360) % 360;
|
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() {
|
function resetShellScroll() {
|
||||||
const shell = wheelStage.value?.closest<HTMLElement>(".app-shell");
|
const shell = wheelStage.value?.closest<HTMLElement>(".app-shell");
|
||||||
if (shell) shell.scrollLeft = 0;
|
if (shell) shell.scrollLeft = 0;
|
||||||
|
|
|
||||||
|
|
@ -971,6 +971,7 @@ input:focus-visible {
|
||||||
.category-wheel-selector {
|
.category-wheel-selector {
|
||||||
--wheel-size: clamp(286px, 88vw, 344px);
|
--wheel-size: clamp(286px, 88vw, 344px);
|
||||||
--center-size: clamp(122px, 35vw, 140px);
|
--center-size: clamp(122px, 35vw, 140px);
|
||||||
|
--ring-gap: 9px;
|
||||||
--option-radius: calc((var(--wheel-size) + var(--center-size)) / 4);
|
--option-radius: calc((var(--wheel-size) + var(--center-size)) / 4);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|
@ -1027,13 +1028,32 @@ input:focus-visible {
|
||||||
box-shadow: 0 14px 34px rgba(29, 55, 48, 0.24);
|
box-shadow: 0 14px 34px rgba(29, 55, 48, 0.24);
|
||||||
-webkit-mask: radial-gradient(
|
-webkit-mask: radial-gradient(
|
||||||
circle,
|
circle,
|
||||||
transparent 0 calc(var(--center-size) / 2),
|
transparent 0 calc(var(--center-size) / 2 + var(--ring-gap)),
|
||||||
#000 calc(var(--center-size) / 2 + 1px)
|
#000 calc(var(--center-size) / 2 + var(--ring-gap) + 1px)
|
||||||
);
|
);
|
||||||
mask: radial-gradient(
|
mask: radial-gradient(
|
||||||
circle,
|
circle,
|
||||||
transparent 0 calc(var(--center-size) / 2),
|
transparent 0 calc(var(--center-size) / 2 + var(--ring-gap)),
|
||||||
#000 calc(var(--center-size) / 2 + 1px)
|
#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)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -130,6 +130,12 @@ function chooseParent(category: Category) {
|
||||||
categorySheetMode.value = null;
|
categorySheetMode.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearCategory() {
|
||||||
|
parentCategoryId.value = "";
|
||||||
|
childCategoryId.value = "";
|
||||||
|
categorySheetMode.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
function chooseChild(category: Category) {
|
function chooseChild(category: Category) {
|
||||||
childCategoryId.value = category.id;
|
childCategoryId.value = category.id;
|
||||||
categorySheetMode.value = null;
|
categorySheetMode.value = null;
|
||||||
|
|
@ -151,10 +157,6 @@ async function saveEntry() {
|
||||||
errorMessage.value = "请输入有效金额";
|
errorMessage.value = "请输入有效金额";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!parentCategoryId.value) {
|
|
||||||
errorMessage.value = "请选择大类";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const occurredDate = new Date(occurredAt.value);
|
const occurredDate = new Date(occurredAt.value);
|
||||||
if (timeInvalid.value) {
|
if (timeInvalid.value) {
|
||||||
errorMessage.value = "请选择时间";
|
errorMessage.value = "请选择时间";
|
||||||
|
|
@ -171,7 +173,7 @@ async function saveEntry() {
|
||||||
amount: cents,
|
amount: cents,
|
||||||
baseAmount: Math.round(cents * exchangeRate),
|
baseAmount: Math.round(cents * exchangeRate),
|
||||||
ledgerIds: [...ledgerIds.value],
|
ledgerIds: [...ledgerIds.value],
|
||||||
categoryId: childCategoryId.value || parentCategoryId.value,
|
categoryId: childCategoryId.value || parentCategoryId.value || entryType.value,
|
||||||
note: note.value.trim(),
|
note: note.value.trim(),
|
||||||
occurredAt: occurredDate.toISOString(),
|
occurredAt: occurredDate.toISOString(),
|
||||||
});
|
});
|
||||||
|
|
@ -317,6 +319,16 @@ async function deleteEntry() {
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div v-if="categorySheetMode === 'parent'" class="entry-category-grid">
|
<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
|
<button
|
||||||
v-for="category in parentCategories"
|
v-for="category in parentCategories"
|
||||||
:key="category.id"
|
:key="category.id"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue