feat: persist ledger amount display locally
This commit is contained in:
parent
758f061eb5
commit
e155f09fe1
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)!;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue