Refine category wheel and taxonomy
This commit is contained in:
parent
00e5e035a9
commit
fff1ba9c2e
|
|
@ -25,6 +25,7 @@ const emit = defineEmits<{
|
|||
}>();
|
||||
|
||||
const FOCUS_ANGLE = 270;
|
||||
const wheelEntryTypes = [...entryTypes, ...entryTypes];
|
||||
const selectorLevel = ref<SelectorLevel>("type");
|
||||
const wheelStage = ref<HTMLElement | null>(null);
|
||||
const wheelRotation = ref(0);
|
||||
|
|
@ -37,7 +38,7 @@ let inertiaFrame: number | null = null;
|
|||
|
||||
const selectedParent = computed(() => props.categoryPath[0]);
|
||||
const selectorOptions = computed<SelectorOption[]>(() => {
|
||||
if (selectorLevel.value === "type") return entryTypes;
|
||||
if (selectorLevel.value === "type") return wheelEntryTypes;
|
||||
if (selectorLevel.value === "category") return categories[props.entryType];
|
||||
return selectedParent.value?.children ?? [];
|
||||
});
|
||||
|
|
@ -172,12 +173,16 @@ function moveWheelDrag(event: PointerEvent) {
|
|||
const delta = normalizeAngle(angle - pointerLastAngle);
|
||||
const now = performance.now();
|
||||
const elapsed = Math.max(1, now - pointerLastTime);
|
||||
const instantaneousVelocity = delta / elapsed;
|
||||
wheelRotation.value += delta;
|
||||
pointerTravel += Math.abs(delta);
|
||||
if (pointerTravel > 5 && wheelStage.value && !wheelStage.value.hasPointerCapture(event.pointerId)) {
|
||||
wheelStage.value.setPointerCapture(event.pointerId);
|
||||
}
|
||||
pointerVelocity = pointerVelocity * 0.62 + (delta / elapsed) * 0.38;
|
||||
pointerVelocity = Math.max(
|
||||
-1.2,
|
||||
Math.min(1.2, pointerVelocity * 0.62 + instantaneousVelocity * 0.38),
|
||||
);
|
||||
pointerLastAngle = angle;
|
||||
pointerLastTime = now;
|
||||
}
|
||||
|
|
@ -188,8 +193,10 @@ function endWheelDrag(event: PointerEvent) {
|
|||
if (wheelStage.value?.hasPointerCapture(event.pointerId)) {
|
||||
wheelStage.value.releasePointerCapture(event.pointerId);
|
||||
}
|
||||
if (pointerTravel > 5 && Math.abs(pointerVelocity) > 0.015) startInertia();
|
||||
else snapWheel();
|
||||
const releaseDelay = Math.max(0, performance.now() - pointerLastTime - 16);
|
||||
pointerVelocity *= Math.exp(-releaseDelay / 90);
|
||||
if (pointerTravel > 5 && Math.abs(pointerVelocity) > 0.008) startInertia();
|
||||
else pointerVelocity = 0;
|
||||
}
|
||||
|
||||
function cancelOptionClick(event: MouseEvent) {
|
||||
|
|
@ -205,10 +212,10 @@ function startInertia() {
|
|||
const elapsed = Math.min(32, now - previousTime);
|
||||
previousTime = now;
|
||||
wheelRotation.value += pointerVelocity * elapsed;
|
||||
pointerVelocity *= Math.pow(0.92, elapsed / 16);
|
||||
if (Math.abs(pointerVelocity) < 0.012) {
|
||||
pointerVelocity *= Math.exp(-elapsed / 280);
|
||||
if (Math.abs(pointerVelocity) < 0.002) {
|
||||
inertiaFrame = null;
|
||||
snapWheel();
|
||||
pointerVelocity = 0;
|
||||
return;
|
||||
}
|
||||
inertiaFrame = requestAnimationFrame(step);
|
||||
|
|
@ -216,12 +223,6 @@ function startInertia() {
|
|||
inertiaFrame = requestAnimationFrame(step);
|
||||
}
|
||||
|
||||
function snapWheel() {
|
||||
const step = segmentAngle.value;
|
||||
const target = Math.round((wheelRotation.value - FOCUS_ANGLE) / step) * step + FOCUS_ANGLE;
|
||||
wheelRotation.value = target;
|
||||
}
|
||||
|
||||
function stopInertia() {
|
||||
if (inertiaFrame !== null) cancelAnimationFrame(inertiaFrame);
|
||||
inertiaFrame = null;
|
||||
|
|
@ -257,7 +258,7 @@ function resetShellScroll() {
|
|||
<div class="category-wheel-disc"></div>
|
||||
<button
|
||||
v-for="(option, index) in selectorOptions"
|
||||
:key="option.id"
|
||||
:key="`${selectorLevel}-${option.id}-${index}`"
|
||||
type="button"
|
||||
class="wheel-option"
|
||||
:class="{ selected: isOptionSelected(option) }"
|
||||
|
|
|
|||
|
|
@ -20,9 +20,7 @@ import {
|
|||
HandCoins,
|
||||
HeartPulse,
|
||||
House,
|
||||
KeyRound,
|
||||
Music2,
|
||||
Package,
|
||||
ParkingCircle,
|
||||
Plane,
|
||||
ReceiptText,
|
||||
|
|
@ -34,13 +32,10 @@ import {
|
|||
Shirt,
|
||||
Smartphone,
|
||||
Soup,
|
||||
Sparkles,
|
||||
Ticket,
|
||||
TrainFront,
|
||||
Utensils,
|
||||
Wifi,
|
||||
Wrench,
|
||||
Zap,
|
||||
type LucideIcon,
|
||||
} from "@lucide/vue";
|
||||
|
||||
|
|
@ -109,17 +104,14 @@ export const categories: Record<EntryType, Category[]> = {
|
|||
{ id: "home", label: "家居", color: "#2b8a74", tint: "#e8f7f2", icon: House },
|
||||
],
|
||||
},
|
||||
{ id: "shopping", label: "购物", color: "#d84486", tint: "#fff0f7", icon: ShoppingBag },
|
||||
{
|
||||
id: "housing",
|
||||
label: "住房",
|
||||
color: "#387b70",
|
||||
tint: "#eaf6f3",
|
||||
icon: House,
|
||||
id: "shopping",
|
||||
label: "购物",
|
||||
color: "#d84486",
|
||||
tint: "#fff0f7",
|
||||
icon: ShoppingBag,
|
||||
children: [
|
||||
{ id: "rent", label: "房租房贷", color: "#387b70", tint: "#eaf6f3", icon: KeyRound },
|
||||
{ id: "utilities", label: "水电燃气", color: "#b27a22", tint: "#fff7e7", icon: Zap },
|
||||
{ id: "maintenance", label: "维修", color: "#697970", tint: "#f0f4f2", icon: Wrench },
|
||||
{ id: "clothing", label: "服饰", color: "#b14e79", tint: "#faeef4", icon: Shirt },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -158,17 +150,14 @@ export const categories: Record<EntryType, Category[]> = {
|
|||
{ id: "insurance", label: "保险", color: "#477866", tint: "#edf6f2", icon: ShieldCheck },
|
||||
],
|
||||
},
|
||||
{ id: "clothing", label: "服饰", color: "#b14e79", tint: "#faeef4", icon: Shirt },
|
||||
{ id: "beauty", label: "美容", color: "#b75e91", tint: "#fbf0f6", icon: Sparkles },
|
||||
{ id: "delivery", label: "快递", color: "#9a6b35", tint: "#f8f1e8", icon: Package },
|
||||
{ id: "travel", label: "旅行", color: "#7559d9", tint: "#f2efff", icon: Plane },
|
||||
],
|
||||
income: [
|
||||
{ id: "salary", label: "工资", color: "#15956d", tint: "#e8faf3", icon: BriefcaseBusiness },
|
||||
{ id: "bonus", label: "奖金", color: "#b57a0e", tint: "#fff7dd", icon: Gift },
|
||||
{ id: "part-time", label: "兼职", color: "#3577c9", tint: "#edf5ff", icon: HandCoins },
|
||||
{ id: "investment", label: "投资收益", color: "#28805d", tint: "#eaf7f1", icon: ChartNoAxesCombined },
|
||||
{ id: "refund", label: "退款报销", color: "#3c7d9b", tint: "#edf6fa", icon: RotateCcw },
|
||||
{ id: "investment", label: "投资", color: "#28805d", tint: "#eaf7f1", icon: ChartNoAxesCombined },
|
||||
{ id: "refund", label: "报销", color: "#3c7d9b", tint: "#edf6fa", icon: RotateCcw },
|
||||
{ id: "other-income", label: "其他", color: "#8559cf", tint: "#f4efff", icon: Banknote },
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
# 分类体系
|
||||
|
||||
本文记录快速记账当前使用的一级、二级和三级分类。二级、三级分类的排列顺序与分类轮盘一致。
|
||||
|
||||
## 一级分类
|
||||
|
||||
| 顺序 | 分类 | ID |
|
||||
| --- | --- | --- |
|
||||
| 1 | 支出 | `expense` |
|
||||
| 2 | 收入 | `income` |
|
||||
|
||||
一级逻辑分类仍然只有两个。为了缩小单个扇区,轮盘按“支出、收入、支出、收入”重复显示为四个象限。
|
||||
|
||||
## 二级分类
|
||||
|
||||
### 支出
|
||||
|
||||
共 9 个。
|
||||
|
||||
| 顺序 | 分类 | ID |
|
||||
| --- | --- | --- |
|
||||
| 1 | 餐饮 | `food` |
|
||||
| 2 | 交通 | `transport` |
|
||||
| 3 | 日用 | `daily` |
|
||||
| 4 | 购物 | `shopping` |
|
||||
| 5 | 娱乐 | `entertainment` |
|
||||
| 6 | 医疗 | `health` |
|
||||
| 7 | 教育 | `education` |
|
||||
| 8 | 账单 | `bills` |
|
||||
| 9 | 旅行 | `travel` |
|
||||
|
||||
### 收入
|
||||
|
||||
共 6 个。
|
||||
|
||||
| 顺序 | 分类 | ID |
|
||||
| --- | --- | --- |
|
||||
| 1 | 工资 | `salary` |
|
||||
| 2 | 奖金 | `bonus` |
|
||||
| 3 | 兼职 | `part-time` |
|
||||
| 4 | 投资 | `investment` |
|
||||
| 5 | 报销 | `refund` |
|
||||
| 6 | 其他 | `other-income` |
|
||||
|
||||
## 三级分类
|
||||
|
||||
代码中的 `children` 即三级分类,也就是二级分类下的次级选项。
|
||||
|
||||
### 支出
|
||||
|
||||
| 二级分类 | 三级分类 |
|
||||
| --- | --- |
|
||||
| 餐饮 | 外卖 `takeout`、堂食 `restaurant`、咖啡 `coffee`、零食 `snacks`、饮品 `drinks` |
|
||||
| 交通 | 打车 `taxi`、公交地铁 `public-transit`、火车 `train`、骑行 `cycling`、加油 `fuel`、停车 `parking` |
|
||||
| 日用 | 杂货 `groceries`、数码 `digital`、家居 `home` |
|
||||
| 购物 | 服饰 `clothing` |
|
||||
| 娱乐 | 游戏 `games`、电影 `movies`、音乐 `music` |
|
||||
| 医疗 | 无 |
|
||||
| 教育 | 书籍 `books`、课程 `courses` |
|
||||
| 账单 | 话费 `mobile`、宽带网络 `internet`、保险 `insurance` |
|
||||
| 旅行 | 无 |
|
||||
|
||||
### 收入
|
||||
|
||||
当前收入分类均无三级分类。
|
||||
|
||||
## 层级说明
|
||||
|
||||
- 一级分类决定账目类型,即支出或收入。
|
||||
- 二级分类是账目所属的大类。
|
||||
- 三级分类是部分二级分类下的细分类。
|
||||
Loading…
Reference in New Issue