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

View File

@ -1,7 +1,8 @@
import { defineStore } from "pinia";
import { apiRequest } from "../data/api";
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";
export type LedgerMember = {
@ -39,7 +40,18 @@ export const useLedgerStore = defineStore("ledgers", {
const auth = useAuthStore();
if (!auth.user) return;
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;
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);
return result.ledger;
},
async updateLedger(input: Pick<LedgerRecord, "id" | "name" | "color" | "theme" | "defaultCurrency">) {
await apiRequest(`/api/ledgers/${encodeURIComponent(input.id)}`, { method: "PATCH", body: input });
async updateLedger(input: Pick<LedgerRecord, "id" | "name" | "color" | "theme" | "defaultCurrency"> & { amountDisplay?: LedgerAmountDisplayMode }) {
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();
return this.ledgers.find((ledger) => ledger.id === input.id)!;
},

View File

@ -92,35 +92,11 @@ input:focus-visible {
.top-actions {
display: grid;
grid-template-columns: 56px minmax(0, 1fr) auto;
grid-template-columns: 56px 1fr 40px;
align-items: center;
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 {
width: 56px;
height: 40px;

View File

@ -1,5 +1,5 @@
<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 { onMounted, ref } from "vue";
import { useRouter } from "vue-router";
@ -11,6 +11,7 @@ const ledgerStore = useLedgerStore();
const name = ref("");
const themeId = ref<LedgerThemeId>("jade");
const currency = ref<CurrencyCode>("CNY");
const amountDisplay = ref<LedgerAmountDisplayMode>("base");
const saving = ref(false);
onMounted(async () => {
@ -23,13 +24,14 @@ onMounted(async () => {
name.value = ledgerStore.currentLedger.name;
themeId.value = ledgerStore.currentLedger.theme;
currency.value = ledgerStore.currentLedger.defaultCurrency;
amountDisplay.value = ledgerStore.currentLedger.amountDisplay;
});
async function save() {
if (!ledgerStore.currentLedger || !name.value.trim()) return;
saving.value = true;
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;
await router.back();
}
@ -45,6 +47,7 @@ async function save() {
<section>
<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="amountDisplay"><option value="base">本币金额</option><option value="original">原币金额</option></select></label>
</section>
<section class="color-setting">
<span>账本主题</span>

View File

@ -2,7 +2,6 @@
import { formatCurrencyAmount, type LedgerEntry } from "@cents/domain";
import {
BookOpen,
ArrowLeftRight,
ChevronLeft,
ChevronDown,
ChevronRight,
@ -33,7 +32,7 @@ const store = useEntryStore();
const ledgerStore = useLedgerStore();
const router = useRouter();
const moreMenuOpen = ref(false);
const showBaseCurrency = ref(true);
const showBaseCurrency = computed(() => ledgerStore.currentLedger?.amountDisplay !== "original");
const participantOpen = ref(false);
const shareOpen = ref(false);
const shareFeedback = ref("");
@ -437,22 +436,9 @@ async function shareLedger() {
<span>{{ ledgerStore.currentLedger?.name ?? "选择账本" }}</span>
<ChevronDown :size="16" />
</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">
<MoreHorizontal :size="21" />
</button>
</div>
<button v-if="ledgerStore.currentRole === 'owner'" class="icon-button header-icon" type="button" aria-label="更多" title="更多" @click.stop="moreMenuOpen = true">
<MoreHorizontal :size="21" />
</button>
</div>
<Transition name="more-menu">

View File

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