feat(export): implement export page for bidding items

Introduces a new export page for creating and managing bidding lists. Key features include: selecting items from a
master list, adding them to a bidding list, editing start price/remarks, batch price updates, and drag-and-drop
reordering. The final list can be previewed and exported as a CSV. This change adds the `useBiddingItems` composable and
the `sortablejs` dependency. Also refactors `imageUrl` to be a non-nullable string for type consistency.
This commit is contained in:
xiaomai
2025-10-20 17:33:48 +08:00
parent b00a130114
commit 802c4460a7
7 changed files with 487 additions and 12 deletions

View File

@@ -0,0 +1,79 @@
<template>
<UModal v-model:open="open" title="编辑标品">
<template #body>
<UForm
@submit="onSubmit"
:state="biddingItemEditState"
:schema="biddingItemEditSchema"
>
<UFormField label="起拍价" name="startPrice">
<UInput
v-model.number="biddingItemEditState.startPrice"
type="number"
/>
</UFormField>
<UFormField label="备注" name="remarks">
<UTextarea v-model="biddingItemEditState.remarks" />
</UFormField>
<UButton type="submit" label="保存" color="primary" class="mt-4" />
</UForm>
</template>
</UModal>
</template>
<script lang="ts" setup>
import type { FormSubmitEvent } from "@nuxt/ui";
import * as z from "zod";
const biddingItemEditForm = ref();
const currentId = ref<number>(0);
const open = ref<boolean>(false);
const toast = useToast();
const { biddingItems, editBiddingItem } = useBiddingItems();
const biddingItemEditSchema = z.object({
startPrice: z.number().min(0, "起拍价不能少于 RM 0").default(0),
remarks: z.string().optional(),
});
type BiddingItemEditSchema = z.output<typeof biddingItemEditSchema>;
const biddingItemEditState = reactive<BiddingItem & BiddingItemEditSchema>({
id: 0,
name: "",
imageUrl: "",
startPrice: 0,
remarks: "",
});
const openModal = (biddingItem: BiddingItem) => {
Object.assign(biddingItemEditState, biddingItem); // 👈 一次性复制所有字段
currentId.value = biddingItem.id;
open.value = true;
};
const onSubmit = async (event: FormSubmitEvent<BiddingItemEditSchema>) => {
const updatedItem: BiddingItem = {
...biddingItemEditState, // 👈 这里直接复用已有字段
startPrice: event.data.startPrice,
remarks: event.data.remarks ?? "",
};
editBiddingItem(updatedItem);
toast.add({
title: "已保存修改",
description: `${updatedItem.name} 的信息已更新`,
color: "success",
});
open.value = false;
};
defineExpose({ openModal });
</script>
<style></style>

View File

@@ -100,7 +100,7 @@ const { utils, isNameAvailable, addItem, editItem, items } = useItemsStore();
const baseSchema = z.object({
brand: z.string().optional(),
name: z.string().min(1, "名称不能为空"),
imageUrl: z.string().nullable().optional(),
imageUrl: z.string().optional(),
description: z.string().nullable().optional(),
tags: z.string().nullable().optional(),
});
@@ -121,7 +121,7 @@ type ItemSchema = z.output<typeof baseSchema>;
const itemState = reactive<ItemSchema>({
brand: undefined,
name: "",
imageUrl: null,
imageUrl: "",
description: null,
tags: null,
});
@@ -134,7 +134,7 @@ const openModal = (item?: Item) => {
currentId.value = item.id;
itemState.brand = item.brand;
itemState.name = item.name;
itemState.imageUrl = item.imageUrl ?? null;
itemState.imageUrl = item.imageUrl;
itemState.description = item.description ?? null;
itemState.tags = item.tags?.join(", ") ?? null;
} else {
@@ -143,7 +143,7 @@ const openModal = (item?: Item) => {
itemState.brand = undefined;
currentId.value = null;
itemState.name = "";
itemState.imageUrl = null;
itemState.imageUrl = "";
itemState.description = null;
itemState.tags = null;
}