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<{
|
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,
|
||||||
|
|
|
||||||
|
|
@ -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)!;
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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,22 +436,9 @@ 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 v-if="ledgerStore.currentRole === 'owner'" class="icon-button header-icon" type="button" aria-label="更多" title="更多" @click.stop="moreMenuOpen = true">
|
||||||
<button
|
<MoreHorizontal :size="21" />
|
||||||
class="currency-display-button"
|
</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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Transition name="more-menu">
|
<Transition name="more-menu">
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue