278 lines
9.0 KiB
Svelte
278 lines
9.0 KiB
Svelte
<script lang="ts">
|
|
import type { Snippet } from 'svelte';
|
|
|
|
import * as Drawer from '$lib/components/ui/drawer';
|
|
import * as Select from '$lib/components/ui/select';
|
|
import { Button } from '$lib/components/ui/button';
|
|
import { Input } from '$lib/components/ui/input';
|
|
import { Label } from '$lib/components/ui/label';
|
|
|
|
import Receipt from '@lucide/svelte/icons/receipt';
|
|
import Pencil from '@lucide/svelte/icons/pencil';
|
|
import Save from '@lucide/svelte/icons/save';
|
|
import X from '@lucide/svelte/icons/x';
|
|
import Calendar from '@lucide/svelte/icons/calendar';
|
|
import Store from '@lucide/svelte/icons/store';
|
|
import Tag from '@lucide/svelte/icons/tag';
|
|
import FileText from '@lucide/svelte/icons/file-text';
|
|
import CreditCard from '@lucide/svelte/icons/credit-card';
|
|
|
|
import { updateBill } from '$lib/api';
|
|
import { cleanedBillToUIBill, type UIBill } from '$lib/models/bill';
|
|
|
|
interface Props {
|
|
open?: boolean;
|
|
record?: UIBill | null;
|
|
categories?: string[];
|
|
|
|
title?: string;
|
|
viewDescription?: string;
|
|
editDescription?: string;
|
|
|
|
titleExtra?: Snippet<[{ isEditing: boolean }]>;
|
|
|
|
contentClass?: string;
|
|
|
|
onUpdate?: (updated: UIBill, original: UIBill) => void;
|
|
}
|
|
|
|
let {
|
|
open = $bindable(false),
|
|
record = $bindable<UIBill | null>(null),
|
|
categories = [],
|
|
title = '账单详情',
|
|
viewDescription = '查看这笔支出的完整信息',
|
|
editDescription = '修改这笔支出的信息',
|
|
titleExtra,
|
|
contentClass,
|
|
onUpdate
|
|
}: Props = $props();
|
|
|
|
let isEditing = $state(false);
|
|
let isSaving = $state(false);
|
|
|
|
let editForm = $state({
|
|
amount: '',
|
|
merchant: '',
|
|
category: '',
|
|
description: '',
|
|
payment_method: ''
|
|
});
|
|
|
|
$effect(() => {
|
|
if (!open) return;
|
|
isEditing = false;
|
|
});
|
|
|
|
function startEdit() {
|
|
if (!record) return;
|
|
editForm = {
|
|
amount: String(record.amount),
|
|
merchant: record.merchant,
|
|
category: record.category,
|
|
description: record.description || '',
|
|
payment_method: record.paymentMethod || ''
|
|
};
|
|
isEditing = true;
|
|
}
|
|
|
|
function cancelEdit() {
|
|
isEditing = false;
|
|
}
|
|
|
|
function handleCategoryChange(value: string | undefined) {
|
|
if (value) editForm.category = value;
|
|
}
|
|
|
|
async function saveEdit() {
|
|
if (!record) return;
|
|
if (isSaving) return;
|
|
|
|
isSaving = true;
|
|
const original = { ...record };
|
|
|
|
const updated: UIBill = {
|
|
...record,
|
|
amount: Number(editForm.amount),
|
|
merchant: editForm.merchant,
|
|
category: editForm.category,
|
|
description: editForm.description,
|
|
paymentMethod: editForm.payment_method
|
|
};
|
|
|
|
try {
|
|
const billId = (record as unknown as { id?: string }).id;
|
|
if (billId) {
|
|
const resp = await updateBill(billId, {
|
|
merchant: editForm.merchant,
|
|
category: editForm.category,
|
|
amount: Number(editForm.amount),
|
|
description: editForm.description,
|
|
pay_method: editForm.payment_method
|
|
});
|
|
|
|
if (resp.result && resp.data) {
|
|
const persisted = cleanedBillToUIBill(resp.data);
|
|
updated.id = persisted.id;
|
|
updated.amount = persisted.amount;
|
|
updated.merchant = persisted.merchant;
|
|
updated.category = persisted.category;
|
|
updated.description = persisted.description;
|
|
updated.paymentMethod = persisted.paymentMethod;
|
|
updated.time = persisted.time;
|
|
updated.incomeExpense = persisted.incomeExpense;
|
|
updated.status = persisted.status;
|
|
updated.remark = persisted.remark;
|
|
updated.reviewLevel = persisted.reviewLevel;
|
|
}
|
|
}
|
|
|
|
record = updated;
|
|
isEditing = false;
|
|
onUpdate?.(updated, original);
|
|
} finally {
|
|
isSaving = false;
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<Drawer.Root bind:open>
|
|
<Drawer.Content class={`md:max-w-4xl ${contentClass ?? ''}`.trim()}>
|
|
<Drawer.Header>
|
|
<Drawer.Title class="flex items-center gap-2">
|
|
<Receipt class="h-5 w-5" />
|
|
{isEditing ? '编辑账单' : title}
|
|
{@render titleExtra?.({ isEditing })}
|
|
</Drawer.Title>
|
|
<Drawer.Description>
|
|
{isEditing ? editDescription : viewDescription}
|
|
</Drawer.Description>
|
|
</Drawer.Header>
|
|
|
|
<div class="flex-1 overflow-auto px-4 py-4 md:px-0">
|
|
{#if record}
|
|
{#if isEditing}
|
|
<div class="space-y-4">
|
|
<div class="space-y-2">
|
|
<Label>金额</Label>
|
|
<div class="relative">
|
|
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground">¥</span>
|
|
<Input type="number" bind:value={editForm.amount} class="pl-8" step="0.01" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-2">
|
|
<Label>商家</Label>
|
|
<Input bind:value={editForm.merchant} />
|
|
</div>
|
|
|
|
<div class="space-y-2">
|
|
<Label>分类</Label>
|
|
{#if categories.length > 0}
|
|
<Select.Root type="single" value={editForm.category} onValueChange={handleCategoryChange}>
|
|
<Select.Trigger class="w-full">
|
|
<span>{editForm.category || '选择分类'}</span>
|
|
</Select.Trigger>
|
|
<Select.Portal>
|
|
<Select.Content>
|
|
{#each categories as category}
|
|
<Select.Item value={category}>{category}</Select.Item>
|
|
{/each}
|
|
</Select.Content>
|
|
</Select.Portal>
|
|
</Select.Root>
|
|
{:else}
|
|
<Input bind:value={editForm.category} />
|
|
{/if}
|
|
</div>
|
|
|
|
<div class="space-y-2">
|
|
<Label>描述</Label>
|
|
<Input bind:value={editForm.description} />
|
|
</div>
|
|
|
|
<div class="space-y-2">
|
|
<Label>支付方式</Label>
|
|
<Input bind:value={editForm.payment_method} />
|
|
</div>
|
|
</div>
|
|
{:else}
|
|
<div>
|
|
<div class="text-center mb-6">
|
|
<div class="text-3xl font-bold text-red-600 dark:text-red-400 font-mono">¥{record.amount.toFixed(2)}</div>
|
|
<div class="text-sm text-muted-foreground mt-1">支出金额</div>
|
|
</div>
|
|
|
|
<div class="space-y-3">
|
|
<div class="flex items-center gap-3 p-3 rounded-lg bg-muted/50">
|
|
<Store class="h-4 w-4 text-muted-foreground shrink-0" />
|
|
<div class="min-w-0">
|
|
<div class="text-xs text-muted-foreground">商家</div>
|
|
<div class="font-medium truncate">{record.merchant}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-3 p-3 rounded-lg bg-muted/50">
|
|
<Tag class="h-4 w-4 text-muted-foreground shrink-0" />
|
|
<div class="min-w-0">
|
|
<div class="text-xs text-muted-foreground">分类</div>
|
|
<div class="font-medium">{record.category}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-3 p-3 rounded-lg bg-muted/50">
|
|
<Calendar class="h-4 w-4 text-muted-foreground shrink-0" />
|
|
<div class="min-w-0">
|
|
<div class="text-xs text-muted-foreground">时间</div>
|
|
<div class="font-medium">{record.time}</div>
|
|
</div>
|
|
</div>
|
|
|
|
{#if record.description}
|
|
<div class="flex items-center gap-3 p-3 rounded-lg bg-muted/50">
|
|
<FileText class="h-4 w-4 text-muted-foreground shrink-0" />
|
|
<div class="min-w-0">
|
|
<div class="text-xs text-muted-foreground">描述</div>
|
|
<div class="font-medium">{record.description}</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if record.paymentMethod}
|
|
<div class="flex items-center gap-3 p-3 rounded-lg bg-muted/50">
|
|
<CreditCard class="h-4 w-4 text-muted-foreground shrink-0" />
|
|
<div class="min-w-0">
|
|
<div class="text-xs text-muted-foreground">支付方式</div>
|
|
<div class="font-medium">{record.paymentMethod}</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
{/if}
|
|
</div>
|
|
|
|
<Drawer.Footer>
|
|
{#if isEditing}
|
|
<Button variant="outline" onclick={cancelEdit}>
|
|
<X class="h-4 w-4 mr-2" />
|
|
取消
|
|
</Button>
|
|
<Button onclick={saveEdit} disabled={isSaving}>
|
|
<Save class="h-4 w-4 mr-2" />
|
|
{isSaving ? '保存中…' : '保存'}
|
|
</Button>
|
|
{:else}
|
|
<Button variant="outline" onclick={() => (open = false)}>
|
|
关闭
|
|
</Button>
|
|
<Button onclick={startEdit}>
|
|
<Pencil class="h-4 w-4 mr-2" />
|
|
编辑
|
|
</Button>
|
|
{/if}
|
|
</Drawer.Footer>
|
|
</Drawer.Content>
|
|
</Drawer.Root>
|