feat: persist ledger amount display locally

This commit is contained in:
openclaw 2026-07-25 02:03:04 +08:00
parent 758f061eb5
commit e155f09fe1
6 changed files with 51 additions and 51 deletions

View File

@ -1,4 +1,4 @@
import { ledgerThemeAccents, type CurrencyCode, type LedgerThemeId } from "@cents/domain"; import { ledgerThemeAccents, type CurrencyCode, type LedgerAmountDisplayMode, type LedgerThemeId } from "@cents/domain";
export const ledgerThemes: ReadonlyArray<{ export const ledgerThemes: ReadonlyArray<{
id: LedgerThemeId; id: LedgerThemeId;
@ -30,6 +30,7 @@ export type LedgerRecord = {
color: string; color: string;
theme: LedgerThemeId; theme: LedgerThemeId;
defaultCurrency: CurrencyCode; defaultCurrency: CurrencyCode;
amountDisplay: LedgerAmountDisplayMode;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
archivedAt: string | null; archivedAt: string | null;
@ -40,7 +41,7 @@ export type LedgerRecord = {
export type UserPreference = { export type UserPreference = {
id: string; id: string;
userId: string; userId: string;
key: "currentLedgerId"; key: "currentLedgerId" | "ledgerAmountDisplay";
value: string; value: string;
updatedAt: string; updatedAt: string;
}; };
@ -49,6 +50,10 @@ export function currentLedgerPreferenceId(userId: string) {
return `${userId}:currentLedgerId`; return `${userId}:currentLedgerId`;
} }
export function ledgerAmountDisplayPreferenceId(userId: string, ledgerId: string) {
return `${userId}:ledgerAmountDisplay:${ledgerId}`;
}
export function createDefaultLedgers(): LedgerRecord[] { export function createDefaultLedgers(): LedgerRecord[] {
const now = new Date().toISOString(); const now = new Date().toISOString();
return [ return [
@ -58,6 +63,7 @@ export function createDefaultLedgers(): LedgerRecord[] {
color: "#087f72", color: "#087f72",
theme: "jade", theme: "jade",
defaultCurrency: "CNY", defaultCurrency: "CNY",
amountDisplay: "base",
createdAt: now, createdAt: now,
updatedAt: now, updatedAt: now,
archivedAt: null, archivedAt: null,
@ -69,6 +75,7 @@ export function createDefaultLedgers(): LedgerRecord[] {
color: "#3478e5", color: "#3478e5",
theme: "ocean", theme: "ocean",
defaultCurrency: "CNY", defaultCurrency: "CNY",
amountDisplay: "base",
createdAt: now, createdAt: now,
updatedAt: now, updatedAt: now,
archivedAt: null, archivedAt: null,

View File

@ -1,7 +1,8 @@
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { apiRequest } from "../data/api"; import { apiRequest } from "../data/api";
import { db } from "../data/db"; import { db } from "../data/db";
import { currentLedgerPreferenceId, type LedgerRecord, type UserPreference } from "../data/ledgers"; import { currentLedgerPreferenceId, ledgerAmountDisplayPreferenceId, type LedgerRecord, type UserPreference } from "../data/ledgers";
import type { LedgerAmountDisplayMode } from "@cents/domain";
import { useAuthStore } from "./auth"; import { useAuthStore } from "./auth";
export type LedgerMember = { export type LedgerMember = {
@ -39,7 +40,18 @@ export const useLedgerStore = defineStore("ledgers", {
const auth = useAuthStore(); const auth = useAuthStore();
if (!auth.user) return; if (!auth.user) return;
const result = await apiRequest<{ ledgers: LedgerRecord[] }>("/api/ledgers"); const result = await apiRequest<{ ledgers: LedgerRecord[] }>("/api/ledgers");
const serverLedgers = result.ledgers.map((ledger) => ({ ...ledger })); const displayPreferences = await db.userPreferences
.where("userId")
.equals(auth.user.id)
.filter((preference) => preference.key === "ledgerAmountDisplay")
.toArray();
const displayByLedger = new Map(
displayPreferences.map((preference) => [preference.id.split(":").at(-1), preference.value as LedgerAmountDisplayMode]),
);
const serverLedgers = result.ledgers.map((ledger) => ({
...ledger,
amountDisplay: displayByLedger.get(ledger.id) ?? "base",
}));
this.ledgers = serverLedgers; this.ledgers = serverLedgers;
const allowedIds = new Set(this.ledgers.map((ledger) => ledger.id)); const allowedIds = new Set(this.ledgers.map((ledger) => ledger.id));
@ -118,8 +130,22 @@ export const useLedgerStore = defineStore("ledgers", {
await this.setCurrentLedger(result.ledger.id); await this.setCurrentLedger(result.ledger.id);
return result.ledger; return result.ledger;
}, },
async updateLedger(input: Pick<LedgerRecord, "id" | "name" | "color" | "theme" | "defaultCurrency">) { async updateLedger(input: Pick<LedgerRecord, "id" | "name" | "color" | "theme" | "defaultCurrency"> & { amountDisplay?: LedgerAmountDisplayMode }) {
await apiRequest(`/api/ledgers/${encodeURIComponent(input.id)}`, { method: "PATCH", body: input }); const { amountDisplay, ...serverInput } = input;
await apiRequest(`/api/ledgers/${encodeURIComponent(input.id)}`, { method: "PATCH", body: serverInput });
if (amountDisplay) {
const auth = useAuthStore();
if (auth.user) {
const preference: UserPreference = {
id: ledgerAmountDisplayPreferenceId(auth.user.id, input.id),
userId: auth.user.id,
key: "ledgerAmountDisplay",
value: amountDisplay,
updatedAt: new Date().toISOString(),
};
await db.userPreferences.put(preference);
}
}
await this.reloadLedgers(); await this.reloadLedgers();
return this.ledgers.find((ledger) => ledger.id === input.id)!; return this.ledgers.find((ledger) => ledger.id === input.id)!;
}, },

View File

@ -92,35 +92,11 @@ input:focus-visible {
.top-actions { .top-actions {
display: grid; display: grid;
grid-template-columns: 56px minmax(0, 1fr) auto; grid-template-columns: 56px 1fr 40px;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
} }
.header-right-actions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 6px;
}
.currency-display-button {
width: 34px;
height: 34px;
display: inline-grid;
place-items: center;
border: 1px solid rgba(255, 255, 255, 0.28);
border-radius: 7px;
padding: 0;
background: rgba(0, 0, 0, 0.12);
color: rgba(255, 255, 255, 0.82);
}
.currency-display-button.active {
background: rgba(255, 255, 255, 0.2);
color: #ffffff;
}
.participant-avatars { .participant-avatars {
width: 56px; width: 56px;
height: 40px; height: 40px;

View File

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { CurrencyCode, LedgerThemeId } from "@cents/domain"; import type { CurrencyCode, LedgerAmountDisplayMode, LedgerThemeId } from "@cents/domain";
import { ArrowLeft, Check } from "@lucide/vue"; import { ArrowLeft, Check } from "@lucide/vue";
import { onMounted, ref } from "vue"; import { onMounted, ref } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
@ -11,6 +11,7 @@ const ledgerStore = useLedgerStore();
const name = ref(""); const name = ref("");
const themeId = ref<LedgerThemeId>("jade"); const themeId = ref<LedgerThemeId>("jade");
const currency = ref<CurrencyCode>("CNY"); const currency = ref<CurrencyCode>("CNY");
const amountDisplay = ref<LedgerAmountDisplayMode>("base");
const saving = ref(false); const saving = ref(false);
onMounted(async () => { onMounted(async () => {
@ -23,13 +24,14 @@ onMounted(async () => {
name.value = ledgerStore.currentLedger.name; name.value = ledgerStore.currentLedger.name;
themeId.value = ledgerStore.currentLedger.theme; themeId.value = ledgerStore.currentLedger.theme;
currency.value = ledgerStore.currentLedger.defaultCurrency; currency.value = ledgerStore.currentLedger.defaultCurrency;
amountDisplay.value = ledgerStore.currentLedger.amountDisplay;
}); });
async function save() { async function save() {
if (!ledgerStore.currentLedger || !name.value.trim()) return; if (!ledgerStore.currentLedger || !name.value.trim()) return;
saving.value = true; saving.value = true;
const theme = ledgerTheme(themeId.value); const theme = ledgerTheme(themeId.value);
await ledgerStore.updateLedger({ id: ledgerStore.currentLedger.id, name: name.value, color: theme.accent, theme: theme.id, defaultCurrency: currency.value }); await ledgerStore.updateLedger({ id: ledgerStore.currentLedger.id, name: name.value, color: theme.accent, theme: theme.id, defaultCurrency: currency.value, amountDisplay: amountDisplay.value });
saving.value = false; saving.value = false;
await router.back(); await router.back();
} }
@ -45,6 +47,7 @@ async function save() {
<section> <section>
<label><span>账本名称</span><input v-model="name" maxlength="24" :disabled="ledgerStore.currentLedger?.isPersonal" required /></label> <label><span>账本名称</span><input v-model="name" maxlength="24" :disabled="ledgerStore.currentLedger?.isPersonal" required /></label>
<label><span>默认币种 · 暂不可修改</span><select v-model="currency" disabled title="默认币种暂不可修改"><option value="CNY">人民币 CNY</option><option value="USD">美元 USD</option><option value="EUR">欧元 EUR</option><option value="JPY">日元 JPY</option><option value="THB">泰铢 THB</option><option value="HKD">港币 HKD</option></select></label> <label><span>默认币种 · 暂不可修改</span><select v-model="currency" disabled title="默认币种暂不可修改"><option value="CNY">人民币 CNY</option><option value="USD">美元 USD</option><option value="EUR">欧元 EUR</option><option value="JPY">日元 JPY</option><option value="THB">泰铢 THB</option><option value="HKD">港币 HKD</option></select></label>
<label><span>流水金额显示</span><select v-model="amountDisplay"><option value="base">本币金额</option><option value="original">原币金额</option></select></label>
</section> </section>
<section class="color-setting"> <section class="color-setting">
<span>账本主题</span> <span>账本主题</span>

View File

@ -2,7 +2,6 @@
import { formatCurrencyAmount, type LedgerEntry } from "@cents/domain"; import { formatCurrencyAmount, type LedgerEntry } from "@cents/domain";
import { import {
BookOpen, BookOpen,
ArrowLeftRight,
ChevronLeft, ChevronLeft,
ChevronDown, ChevronDown,
ChevronRight, ChevronRight,
@ -33,7 +32,7 @@ const store = useEntryStore();
const ledgerStore = useLedgerStore(); const ledgerStore = useLedgerStore();
const router = useRouter(); const router = useRouter();
const moreMenuOpen = ref(false); const moreMenuOpen = ref(false);
const showBaseCurrency = ref(true); const showBaseCurrency = computed(() => ledgerStore.currentLedger?.amountDisplay !== "original");
const participantOpen = ref(false); const participantOpen = ref(false);
const shareOpen = ref(false); const shareOpen = ref(false);
const shareFeedback = ref(""); const shareFeedback = ref("");
@ -437,23 +436,10 @@ async function shareLedger() {
<span>{{ ledgerStore.currentLedger?.name ?? "选择账本" }}</span> <span>{{ ledgerStore.currentLedger?.name ?? "选择账本" }}</span>
<ChevronDown :size="16" /> <ChevronDown :size="16" />
</button> </button>
<div class="header-right-actions">
<button
class="currency-display-button"
type="button"
:class="{ active: showBaseCurrency }"
:aria-label="showBaseCurrency ? '当前显示本币,点击显示原币' : '当前显示原币,点击显示本币'"
:title="showBaseCurrency ? '当前显示本币,点击切换为原币' : '当前显示原币,点击切换为本币'"
:aria-pressed="showBaseCurrency"
@click="showBaseCurrency = !showBaseCurrency"
>
<ArrowLeftRight :size="17" />
</button>
<button v-if="ledgerStore.currentRole === 'owner'" class="icon-button header-icon" type="button" aria-label="更多" title="更多" @click.stop="moreMenuOpen = true"> <button v-if="ledgerStore.currentRole === 'owner'" class="icon-button header-icon" type="button" aria-label="更多" title="更多" @click.stop="moreMenuOpen = true">
<MoreHorizontal :size="21" /> <MoreHorizontal :size="21" />
</button> </button>
</div> </div>
</div>
<Transition name="more-menu"> <Transition name="more-menu">
<div v-if="moreMenuOpen" class="ledger-more-layer"> <div v-if="moreMenuOpen" class="ledger-more-layer">

View File

@ -68,6 +68,8 @@ export const ledgerThemeAccents = {
export type LedgerThemeId = keyof typeof ledgerThemeAccents; export type LedgerThemeId = keyof typeof ledgerThemeAccents;
export type LedgerAmountDisplayMode = "base" | "original";
export type LedgerEntry = { export type LedgerEntry = {
id: string; id: string;
ownerId: string; ownerId: string;