From 99933da2c89bbbe951b47a1fd303763b64bbadd3 Mon Sep 17 00:00:00 2001 From: openclaw Date: Sat, 8 Aug 2026 00:32:06 +0800 Subject: [PATCH] feat: refine reimbursements and add parenting categories --- apps/web/src/data/categories.ts | 23 +++++++++++++++++++++++ apps/web/src/styles.css | 26 ++++++++++++++++++++++++++ apps/web/src/views/EntryDetailView.vue | 20 +++++++++++++++++--- apps/web/src/views/LedgerView.vue | 6 +++++- docs/分类体系.md | 14 ++++++++------ tests/e2e/07-报销/01-报销流程.spec.ts | 26 ++++++++++++++++++++++++++ 6 files changed, 105 insertions(+), 10 deletions(-) diff --git a/apps/web/src/data/categories.ts b/apps/web/src/data/categories.ts index 091ff0f..f302ad0 100644 --- a/apps/web/src/data/categories.ts +++ b/apps/web/src/data/categories.ts @@ -2,6 +2,7 @@ import type { EntryType } from "@cents/domain"; import { Ambulance, Apple, + Baby, Banknote, BatteryCharging, Bike, @@ -34,6 +35,7 @@ import { Hospital, Hotel, House, + Milk, ParkingCircle, Pencil, Pill, @@ -44,6 +46,7 @@ import { ShoppingBag, ShoppingBasket, Shirt, + SoapDispenserDroplet, Smartphone, Soup, Sparkles, @@ -55,6 +58,8 @@ import { UsersRound, WalletCards, Zap, + CircleEllipsis, + PackageOpen, type LucideIcon, } from "@lucide/vue"; @@ -130,6 +135,24 @@ export const categories: Record = { { id: "toys", label: "玩具", color: "#7559d9", tint: "#f2efff", icon: Puzzle }, ], }, + { + id: "parenting", + label: "育儿", + color: "#de6b79", + tint: "#fff0f2", + icon: Baby, + children: [ + { id: "formula", label: "奶粉", color: "#c88945", tint: "#fff5e8", icon: Milk }, + { id: "maternal-baby", label: "母婴", color: "#de6b79", tint: "#fff0f2", icon: Baby }, + { id: "children-clothing", label: "童装", color: "#b14e79", tint: "#faeef4", icon: Shirt }, + { id: "children-toys", label: "玩具", color: "#7559d9", tint: "#f2efff", icon: Puzzle }, + { id: "children-tuition", label: "学费", color: "#4869b1", tint: "#edf2fb", icon: GraduationCap }, + { id: "children-supplements", label: "补剂", color: "#2f8768", tint: "#eaf7f1", icon: Pill }, + { id: "children-care", label: "洗护", color: "#2d8f9e", tint: "#eaf8fa", icon: SoapDispenserDroplet }, + { id: "children-paper", label: "纸品", color: "#7a8a86", tint: "#f0f4f2", icon: PackageOpen }, + { id: "children-other", label: "其他", color: "#526a78", tint: "#edf2f4", icon: CircleEllipsis }, + ], + }, { id: "entertainment", label: "娱乐", diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index 3ef0a3f..557db91 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -696,6 +696,32 @@ input:focus-visible { color: #0d8b67; } +.entry-reimbursement-value { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 5px; +} + +.entry-value .entry-reimbursement-tag { + display: inline-block; + margin: 0; + padding: 1px 4px; + border: 1px solid #c5d6ea; + border-radius: 3px; + color: #5e7895; + font-size: 9px; + line-height: 1.25; + white-space: nowrap; +} + +.entry-value .reimbursement { + color: #3478b8; + text-decoration: line-through; + text-decoration-thickness: 1px; + text-decoration-color: #78a5cc; +} + .empty-state { min-height: 260px; display: grid; diff --git a/apps/web/src/views/EntryDetailView.vue b/apps/web/src/views/EntryDetailView.vue index 6fae35e..ddbf4a8 100644 --- a/apps/web/src/views/EntryDetailView.vue +++ b/apps/web/src/views/EntryDetailView.vue @@ -20,7 +20,7 @@ import { WalletCards, X, } from "@lucide/vue"; -import { computed, onMounted, ref } from "vue"; +import { computed, onMounted, ref, watch } from "vue"; import { useRoute, useRouter } from "vue-router"; import { categories, @@ -151,11 +151,25 @@ const canRecordReimbursement = computed(() => onMounted(async () => { await Promise.all([store.loadEntries(), ledgerStore.loadLedgers()]); - const found = store.entries.find((item) => item.id === String(route.params.id)); - if (found) hydrate(found); + hydrateCurrentEntry(); loading.value = false; }); +watch(() => route.params.id, () => { + hydrateCurrentEntry(); +}); + +function hydrateCurrentEntry() { + const found = store.entries.find((item) => item.id === String(route.params.id)); + if (!found) return; + categorySheetMode.value = null; + ledgerPickerOpen.value = false; + currencyPickerOpen.value = false; + reimbursementOpen.value = false; + errorMessage.value = ""; + hydrate(found); +} + function hydrate(value: LedgerEntry) { entry.value = value; currency.value = value.currency; diff --git a/apps/web/src/views/LedgerView.vue b/apps/web/src/views/LedgerView.vue index 1e9745f..e89a829 100644 --- a/apps/web/src/views/LedgerView.vue +++ b/apps/web/src/views/LedgerView.vue @@ -999,7 +999,11 @@ async function shareLedger() {
−{{ entryDisplayAmount(entry) }} - {{ entry.type === "expense" ? "−" : "+" }}{{ entryDisplayAmount(entry, true) }} +
+ 不计入 + {{ entryDisplayAmount(entry, true) }} +
+ {{ entry.type === "expense" ? "−" : "+" }}{{ entryDisplayAmount(entry, true) }} {{ formatTime(entry.occurredAt) }}
diff --git a/docs/分类体系.md b/docs/分类体系.md index 4cc48f1..fed58ed 100644 --- a/docs/分类体系.md +++ b/docs/分类体系.md @@ -17,18 +17,19 @@ ### 支出 -共 8 个。 +共 9 个。 | 顺序 | 分类 | ID | | --- | --- | --- | | 1 | 餐饮 | `food` | | 2 | 交通 | `transport` | | 3 | 购物 | `shopping` | -| 4 | 娱乐 | `entertainment` | -| 5 | 社交 | `social` | -| 6 | 医疗 | `health` | -| 7 | 教育 | `education` | -| 8 | 缴费 | `bills` | +| 4 | 育儿 | `parenting` | +| 5 | 娱乐 | `entertainment` | +| 6 | 社交 | `social` | +| 7 | 医疗 | `health` | +| 8 | 教育 | `education` | +| 9 | 缴费 | `bills` | ### 收入 @@ -54,6 +55,7 @@ | 餐饮 | 外卖 `takeout`、堂食 `restaurant`、咖啡 `coffee`、零食 `snacks`、饮品 `drinks`、水果 `fruit` | | 交通 | 打车 `taxi`、公交 `public-transit`、地铁 `subway`、火车 `train`、骑行 `cycling`、加油 `fuel`、充电 `charging`、停车 `parking`、高速 `highway` | | 购物 | 服饰 `clothing`、食品 `shopping-food`、杂货 `groceries`、数码 `digital`、家居 `home`、玩具 `toys` | +| 育儿 | 奶粉 `formula`、母婴 `maternal-baby`、童装 `children-clothing`、玩具 `children-toys`、学费 `children-tuition`、补剂 `children-supplements`、洗护 `children-care`、纸品 `children-paper`、其他 `children-other` | | 娱乐 | 游戏 `games`、电影 `movies`、按摩 `massage`、运动 `sports`、门票 `tickets`、酒店 `hotel` | | 社交 | 红包 `red-packet`、礼物 `gift`、请客 `treating`、聚会 `gathering`、捐赠 `donation`、人情 `social-obligation` | | 医疗 | 药店 `pharmacy`、门诊 `outpatient`、急诊 `emergency`、疫苗 `vaccine`、住院 `hospitalization` | diff --git a/tests/e2e/07-报销/01-报销流程.spec.ts b/tests/e2e/07-报销/01-报销流程.spec.ts index 91150e8..1510c29 100644 --- a/tests/e2e/07-报销/01-报销流程.spec.ts +++ b/tests/e2e/07-报销/01-报销流程.spec.ts @@ -29,4 +29,30 @@ test.describe("报销:从支出详情记录", () => { await page.goto("/stats"); await expect(page.getByText("报销到账")).not.toBeVisible(); }); + + test("04-流水中的报销到账标记为不计入并使用蓝色删除线金额", async ({ page }) => { + await mockApi(page, { initialEntries: [makeEntry(), makeEntry({ id: "reimbursement", type: "income", amount: 1000, categoryId: "restaurant", reimbursementOfEntryId: "entry-lunch", note: "午餐报销" })] }); + await 登录(page); + + const reimbursement = page.locator(".entry-row", { hasText: "午餐报销" }); + await expect(reimbursement.getByText("不计入")).toBeVisible(); + await expect(reimbursement.locator(".entry-value .reimbursement")).toHaveText("¥32.00"); + await expect(reimbursement.locator(".entry-value .reimbursement")).not.toContainText("+"); + await expect(reimbursement.locator(".entry-value .reimbursement")).toHaveCSS("color", "rgb(52, 120, 184)"); + await expect(reimbursement.locator(".entry-value .reimbursement")).toHaveCSS("text-decoration-line", "line-through"); + }); + + test("05-报销详情可以跳转到关联原支出", async ({ page }) => { + await mockApi(page, { initialEntries: [ + makeEntry({ note: "午餐支出" }), + makeEntry({ id: "reimbursement", type: "income", amount: 1000, categoryId: "restaurant", reimbursementOfEntryId: "entry-lunch", note: "午餐报销" }), + ] }); + await 登录(page); + await page.goto("/entries/reimbursement"); + + await page.getByRole("link", { name: /关联原支出/ }).click(); + await expect(page).toHaveURL(/\/entries\/entry-lunch$/); + await expect(page.getByRole("button", { name: "记录报销" })).toBeVisible(); + await expect(page.locator("textarea")).toHaveValue("午餐支出"); + }); });