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:
79
app/components/export/EditModal.vue
Normal file
79
app/components/export/EditModal.vue
Normal 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>
|
||||||
@@ -100,7 +100,7 @@ const { utils, isNameAvailable, addItem, editItem, items } = useItemsStore();
|
|||||||
const baseSchema = z.object({
|
const baseSchema = z.object({
|
||||||
brand: z.string().optional(),
|
brand: z.string().optional(),
|
||||||
name: z.string().min(1, "名称不能为空"),
|
name: z.string().min(1, "名称不能为空"),
|
||||||
imageUrl: z.string().nullable().optional(),
|
imageUrl: z.string().optional(),
|
||||||
description: z.string().nullable().optional(),
|
description: z.string().nullable().optional(),
|
||||||
tags: z.string().nullable().optional(),
|
tags: z.string().nullable().optional(),
|
||||||
});
|
});
|
||||||
@@ -121,7 +121,7 @@ type ItemSchema = z.output<typeof baseSchema>;
|
|||||||
const itemState = reactive<ItemSchema>({
|
const itemState = reactive<ItemSchema>({
|
||||||
brand: undefined,
|
brand: undefined,
|
||||||
name: "",
|
name: "",
|
||||||
imageUrl: null,
|
imageUrl: "",
|
||||||
description: null,
|
description: null,
|
||||||
tags: null,
|
tags: null,
|
||||||
});
|
});
|
||||||
@@ -134,7 +134,7 @@ const openModal = (item?: Item) => {
|
|||||||
currentId.value = item.id;
|
currentId.value = item.id;
|
||||||
itemState.brand = item.brand;
|
itemState.brand = item.brand;
|
||||||
itemState.name = item.name;
|
itemState.name = item.name;
|
||||||
itemState.imageUrl = item.imageUrl ?? null;
|
itemState.imageUrl = item.imageUrl;
|
||||||
itemState.description = item.description ?? null;
|
itemState.description = item.description ?? null;
|
||||||
itemState.tags = item.tags?.join(", ") ?? null;
|
itemState.tags = item.tags?.join(", ") ?? null;
|
||||||
} else {
|
} else {
|
||||||
@@ -143,7 +143,7 @@ const openModal = (item?: Item) => {
|
|||||||
itemState.brand = undefined;
|
itemState.brand = undefined;
|
||||||
currentId.value = null;
|
currentId.value = null;
|
||||||
itemState.name = "";
|
itemState.name = "";
|
||||||
itemState.imageUrl = null;
|
itemState.imageUrl = "";
|
||||||
itemState.description = null;
|
itemState.description = null;
|
||||||
itemState.tags = null;
|
itemState.tags = null;
|
||||||
}
|
}
|
||||||
|
|||||||
50
app/composables/biddingItems.ts
Normal file
50
app/composables/biddingItems.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { createSharedComposable } from "@vueuse/core";
|
||||||
|
|
||||||
|
export type BiddingItem = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
startPrice: number;
|
||||||
|
remarks: string;
|
||||||
|
imageUrl?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const _useBiddingItems = () => {
|
||||||
|
const biddingItems = ref<BiddingItem[]>([]);
|
||||||
|
|
||||||
|
const biddingItemsLatestId = computed(() => {
|
||||||
|
const arr = biddingItems.value ?? [];
|
||||||
|
if (arr.length === 0) return 1;
|
||||||
|
return Math.max(...arr.map((i) => i.id)) + 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
const addBiddingItem = (item: Item, startPrice: number) => {
|
||||||
|
biddingItems.value.push({
|
||||||
|
id: biddingItemsLatestId.value,
|
||||||
|
name: `${item.brand} ${item.name}`,
|
||||||
|
startPrice: startPrice,
|
||||||
|
remarks: "",
|
||||||
|
imageUrl: item.imageUrl,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeBiddingItem = (id: number) => {
|
||||||
|
biddingItems.value = (biddingItems.value ?? []).filter((i) => i.id !== id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const editBiddingItem = (biddingItem: BiddingItem) => {
|
||||||
|
const idx = (biddingItems.value ?? []).findIndex(
|
||||||
|
(i) => i.id === biddingItem.id
|
||||||
|
);
|
||||||
|
if (idx === -1) return;
|
||||||
|
const updatedBiddingItems = [...(biddingItems.value ?? [])];
|
||||||
|
updatedBiddingItems[idx] = {
|
||||||
|
...updatedBiddingItems[idx],
|
||||||
|
...biddingItem,
|
||||||
|
};
|
||||||
|
biddingItems.value = updatedBiddingItems;
|
||||||
|
};
|
||||||
|
|
||||||
|
return { biddingItems, addBiddingItem, removeBiddingItem, editBiddingItem };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useBiddingItems = createSharedComposable(_useBiddingItems);
|
||||||
@@ -4,7 +4,7 @@ export type Item = {
|
|||||||
id: number;
|
id: number;
|
||||||
brand?: string;
|
brand?: string;
|
||||||
name: string;
|
name: string;
|
||||||
imageUrl?: string | null;
|
imageUrl?: string;
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
tags?: string[] | null;
|
tags?: string[] | null;
|
||||||
createdAt?: Date;
|
createdAt?: Date;
|
||||||
|
|||||||
@@ -1,13 +1,349 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
|
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<!-- 物品列表 -->
|
||||||
|
<div class="w-1/2 min-w-[600px] overflow-x-auto">
|
||||||
|
<h2 class="text-2xl font-medium">物品列表</h2>
|
||||||
|
<div class="flex flex-col flex-1 w-full">
|
||||||
|
<div
|
||||||
|
class="flex justify-between items-center gap-2 overflow-x-auto px-4 py-3.5 border-b border-accented"
|
||||||
|
>
|
||||||
|
<div class="space-x-2">
|
||||||
|
<UInput
|
||||||
|
v-model="globalFilter"
|
||||||
|
class="max-w-sm"
|
||||||
|
placeholder="Search..."
|
||||||
|
icon="lucide:search"
|
||||||
|
/>
|
||||||
|
<UInputNumber
|
||||||
|
v-model="startPriceInput"
|
||||||
|
:min="0"
|
||||||
|
increment-icon="lucide:banknote-arrow-up"
|
||||||
|
decrement-icon="lucide:banknote-arrow-down"
|
||||||
|
class="max-w-sm"
|
||||||
|
placeholder="Starting price"
|
||||||
|
icon="lucide:banknote"
|
||||||
|
:format-options="{
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'MYR',
|
||||||
|
currencyDisplay: 'narrowSymbol',
|
||||||
|
currencySign: 'accounting',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<UTable
|
||||||
|
sticky
|
||||||
|
v-model:global-filter="globalFilter"
|
||||||
|
:data="itemsList"
|
||||||
|
:columns="itemColumns"
|
||||||
|
class="max-h-[640px]"
|
||||||
|
v-model:row-selection="itemListRowSelection"
|
||||||
|
@select="onitemListSelect"
|
||||||
|
>
|
||||||
|
<template #name-cell="{ row }">
|
||||||
|
<div class="flex items-center gap-3 select-none">
|
||||||
|
<UAvatar
|
||||||
|
:src="row.original.imageUrl"
|
||||||
|
size="lg"
|
||||||
|
:alt="row.original.name"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<p class="font-medium text-highlighted">
|
||||||
|
<strong>{{ row.original.brand }}</strong>
|
||||||
|
{{ row.original.name }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UTable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 竞标清单 -->
|
||||||
|
<div class="w-1/2 min-w-[600px] overflow-x-auto">
|
||||||
|
<h2 class="text-2xl font-medium">竞标清单</h2>
|
||||||
|
<div class="flex flex-col flex-1 w-full">
|
||||||
|
<div
|
||||||
|
class="flex justify-between items-center gap-2 overflow-x-auto px-4 py-3.5 border-b border-accented"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<UInputNumber
|
||||||
|
v-model="batchStartPriceInput"
|
||||||
|
:min="0"
|
||||||
|
increment-icon="lucide:banknote-arrow-up"
|
||||||
|
decrement-icon="lucide:banknote-arrow-down"
|
||||||
|
class="max-w-sm"
|
||||||
|
placeholder="Starting price"
|
||||||
|
icon="lucide:banknote"
|
||||||
|
:format-options="{
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'MYR',
|
||||||
|
currencyDisplay: 'narrowSymbol',
|
||||||
|
currencySign: 'accounting',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<UButton
|
||||||
|
icon="lucide:check"
|
||||||
|
:disabled="!isSomeBiddingItemSelected"
|
||||||
|
@click="applyStartPriceButtonClicked"
|
||||||
|
>应用到选中</UButton
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<UTable
|
||||||
|
ref="biddingItemsTableRef"
|
||||||
|
sticky
|
||||||
|
:data="biddingItemList"
|
||||||
|
:columns="biddingItemColumns"
|
||||||
|
class="max-h-[640px] no-wrap-header"
|
||||||
|
:ui="{
|
||||||
|
tbody: 'bidding-item-table-tbody',
|
||||||
|
}"
|
||||||
|
v-model:column-pinning="biddingItemColumnPinning"
|
||||||
|
>
|
||||||
|
<template #select-cell="{ row }">
|
||||||
|
<UCheckbox
|
||||||
|
:model-value="row.getIsSelected()"
|
||||||
|
@update:model-value="(value: boolean | 'indeterminate') => row.toggleSelected(!!value)"
|
||||||
|
aria-label="Select row"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #name-cell="{ row }">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<UAvatar
|
||||||
|
:src="row.original.imageUrl"
|
||||||
|
size="lg"
|
||||||
|
:alt="row.original.name"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<p class="font-medium text-highlighted">
|
||||||
|
{{ row.original.name }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #startPrice-cell="{ row }">
|
||||||
|
<div class="">
|
||||||
|
{{ formatCurrency(row.original.startPrice) }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #button-cell="{ row }">
|
||||||
|
<div class="space-x-2">
|
||||||
|
<UButton
|
||||||
|
icon="lucide:pencil-line"
|
||||||
|
@click="editItemButtonClicked(row.original)"
|
||||||
|
/>
|
||||||
|
<UButton
|
||||||
|
icon="lucide:trash-2"
|
||||||
|
color="error"
|
||||||
|
variant="outline"
|
||||||
|
@click="deleteItemButtonClicked(row.original.id)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UTable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 导出预览 -->
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h2 class="text-2xl font-medium">导出预览</h2>
|
||||||
|
<div class="bg-gray-200 p-6 rounded-md">
|
||||||
|
<pre>{{ exportCsv }}</pre>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<UButton
|
||||||
|
class="flex-1 justify-center"
|
||||||
|
icon="lucide:file-output"
|
||||||
|
@click="downloadCsv"
|
||||||
|
>导出 CSV</UButton
|
||||||
|
>
|
||||||
|
<UButton class="flex-1 justify-center" icon="lucide:download"
|
||||||
|
>下载图片集</UButton
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<ExportEditModal ref="biddingItemEditModal" />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import type { TableColumn, TableRow } from "@nuxt/ui";
|
||||||
|
import type { Table, Row } from "@tanstack/table-core";
|
||||||
|
import { useSortable } from "@vueuse/integrations/useSortable";
|
||||||
|
|
||||||
|
const UCheckbox = resolveComponent("UCheckbox");
|
||||||
|
|
||||||
|
const { items } = useItemsStore();
|
||||||
|
const itemsList = computed(() => items.value ?? []);
|
||||||
|
|
||||||
|
const { biddingItems, addBiddingItem, removeBiddingItem } = useBiddingItems();
|
||||||
|
const biddingItemList = biddingItems
|
||||||
|
|
||||||
|
const biddingItemEditModal = ref<any>(null);
|
||||||
|
|
||||||
|
const formatCurrency = (value: number) => {
|
||||||
|
return new Intl.NumberFormat("en-MY", {
|
||||||
|
style: "currency",
|
||||||
|
currency: "MYR",
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
currencyDisplay: "narrowSymbol",
|
||||||
|
}).format(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Left Table (Item List)
|
||||||
|
const globalFilter = ref<string>("");
|
||||||
|
const startPriceInput = ref<number>(0);
|
||||||
|
|
||||||
|
const itemListRowSelection = ref<Record<string, boolean>>({});
|
||||||
|
|
||||||
|
function onitemListSelect(row: TableRow<Item>, e?: Event) {
|
||||||
|
/* If you decide to also select the column you can do this */
|
||||||
|
// row.toggleSelected(!row.getIsSelected());
|
||||||
|
addBiddingItem(row.original, startPriceInput.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemColumns: TableColumn<Item>[] = [
|
||||||
|
{
|
||||||
|
accessorKey: "id",
|
||||||
|
header: "#",
|
||||||
|
cell: ({ row }) => `#${row.getValue("id")}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "name",
|
||||||
|
header: "标品名称",
|
||||||
|
},
|
||||||
|
// 新增一个 brand 列 —— 让它参与全局搜索,但不实际渲染内容(brand 已在 name-cell 中显示)
|
||||||
|
{
|
||||||
|
accessorKey: "brand",
|
||||||
|
header: "", // 留空,避免影响布局
|
||||||
|
// 明确允许全局过滤(通常是默认 true,但写上更明确)
|
||||||
|
enableGlobalFilter: true,
|
||||||
|
// 不在表格中重复显示 brand(因为你在 name-cell 已经显示了)
|
||||||
|
cell: () => null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
// Right Table (Bidding Item List)
|
||||||
|
const biddingItemsTable = useTemplateRef<{
|
||||||
|
tableRef: HTMLTableElement;
|
||||||
|
tableApi: Table<BiddingItem>;
|
||||||
|
}>("biddingItemsTableRef");
|
||||||
|
const batchStartPriceInput = ref<number>(0);
|
||||||
|
|
||||||
|
const isSomeBiddingItemSelected = computed<boolean>(
|
||||||
|
() =>
|
||||||
|
(biddingItemsTable.value?.tableApi.getIsSomeRowsSelected() ||
|
||||||
|
biddingItemsTable.value?.tableApi.getIsAllRowsSelected()) ??
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
const biddingItemColumns: TableColumn<BiddingItem>[] = [
|
||||||
|
{
|
||||||
|
id: "select",
|
||||||
|
header: ({ table }) =>
|
||||||
|
h(UCheckbox, {
|
||||||
|
modelValue: table.getIsSomePageRowsSelected()
|
||||||
|
? "indeterminate"
|
||||||
|
: table.getIsAllPageRowsSelected(),
|
||||||
|
"onUpdate:modelValue": (value: boolean | "indeterminate") =>
|
||||||
|
table.toggleAllPageRowsSelected(!!value),
|
||||||
|
ariaLabel: "Select all",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "id",
|
||||||
|
header: "#",
|
||||||
|
cell: ({ row }) => `${row.index + 1}`, // 动态行号,从 1 开始
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "name",
|
||||||
|
header: "标品名称",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "startPrice",
|
||||||
|
header: "起拍价",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "remarks",
|
||||||
|
header: "备注",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "button",
|
||||||
|
header: "",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const biddingItemColumnPinning = ref({
|
||||||
|
left: [],
|
||||||
|
right: ["button"],
|
||||||
|
});
|
||||||
|
|
||||||
|
useSortable(".bidding-item-table-tbody", biddingItemList, {
|
||||||
|
animation: 150,
|
||||||
|
});
|
||||||
|
|
||||||
|
const applyStartPriceButtonClicked = () => {
|
||||||
|
const selectedBiddingItems: Row<BiddingItem>[] =
|
||||||
|
biddingItemsTable.value?.tableApi.getFilteredSelectedRowModel().rows ?? [];
|
||||||
|
selectedBiddingItems.forEach((r) => {
|
||||||
|
r.original.startPrice = batchStartPriceInput.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
biddingItemsTable.value?.tableApi.resetRowSelection();
|
||||||
|
};
|
||||||
|
|
||||||
|
const editItemButtonClicked = (biddingItem: BiddingItem) => {
|
||||||
|
biddingItemEditModal.value?.openModal(biddingItem);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteItemButtonClicked = (id: number) => {
|
||||||
|
removeBiddingItem(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
function toUnderscore(name: string) {
|
||||||
|
return name.trim().toLowerCase().replace(/\s+/g, "_");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export Preview
|
||||||
|
const exportCsv = computed<string>(() => {
|
||||||
|
return [
|
||||||
|
...biddingItems.value.map(
|
||||||
|
(biddingItem) =>
|
||||||
|
`${biddingItem.name},${biddingItem.startPrice},${
|
||||||
|
biddingItem.remarks
|
||||||
|
},${toUnderscore(biddingItem.name)}`
|
||||||
|
),
|
||||||
|
].join("\n");
|
||||||
|
});
|
||||||
|
|
||||||
|
const downloadCsv = () => {
|
||||||
|
const blob = new Blob([exportCsv.value], { type: "text/csv;charset=utf-8;" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = `物品清单-${new Date().toISOString().split("T")[0]}.csv`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
/* 阻止表头换行 */
|
||||||
|
.no-wrap-header thead th {
|
||||||
|
white-space: nowrap !important;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxt/ui": "4.0.1",
|
"@nuxt/ui": "4.0.1",
|
||||||
"nuxt": "^4.1.3",
|
"nuxt": "^4.1.3",
|
||||||
|
"sortablejs": "^1.15.6",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vue": "^3.5.22",
|
"vue": "^3.5.22",
|
||||||
"vue-router": "^4.5.1"
|
"vue-router": "^4.5.1"
|
||||||
|
|||||||
17
pnpm-lock.yaml
generated
17
pnpm-lock.yaml
generated
@@ -10,10 +10,13 @@ importers:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@nuxt/ui':
|
'@nuxt/ui':
|
||||||
specifier: 4.0.1
|
specifier: 4.0.1
|
||||||
version: 4.0.1(@babel/parser@7.28.4)(db0@0.3.4)(embla-carousel@8.6.0)(ioredis@5.8.1)(magicast@0.3.5)(typescript@5.9.3)(vite@7.1.9(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-router@4.5.1(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3))(zod@4.1.12)
|
version: 4.0.1(@babel/parser@7.28.4)(db0@0.3.4)(embla-carousel@8.6.0)(ioredis@5.8.1)(magicast@0.3.5)(sortablejs@1.15.6)(typescript@5.9.3)(vite@7.1.9(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-router@4.5.1(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3))(zod@4.1.12)
|
||||||
nuxt:
|
nuxt:
|
||||||
specifier: ^4.1.3
|
specifier: ^4.1.3
|
||||||
version: 4.1.3(@parcel/watcher@2.5.1)(@vue/compiler-sfc@3.5.22)(db0@0.3.4)(ioredis@5.8.1)(lightningcss@1.30.1)(magicast@0.3.5)(rollup@4.52.4)(terser@5.44.0)(typescript@5.9.3)(vite@7.1.9(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1)
|
version: 4.1.3(@parcel/watcher@2.5.1)(@vue/compiler-sfc@3.5.22)(db0@0.3.4)(ioredis@5.8.1)(lightningcss@1.30.1)(magicast@0.3.5)(rollup@4.52.4)(terser@5.44.0)(typescript@5.9.3)(vite@7.1.9(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1)
|
||||||
|
sortablejs:
|
||||||
|
specifier: ^1.15.6
|
||||||
|
version: 1.15.6
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.9.3
|
specifier: ^5.9.3
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
@@ -3152,6 +3155,9 @@ packages:
|
|||||||
smob@1.5.0:
|
smob@1.5.0:
|
||||||
resolution: {integrity: sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==}
|
resolution: {integrity: sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==}
|
||||||
|
|
||||||
|
sortablejs@1.15.6:
|
||||||
|
resolution: {integrity: sha512-aNfiuwMEpfBM/CN6LY0ibyhxPfPbyFeBTYJKCvzkJ2GkUpazIt3H+QIPAMHwqQ7tMKaHz1Qj+rJJCqljnf4p3A==}
|
||||||
|
|
||||||
source-map-js@1.2.1:
|
source-map-js@1.2.1:
|
||||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -4439,7 +4445,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- magicast
|
- magicast
|
||||||
|
|
||||||
'@nuxt/ui@4.0.1(@babel/parser@7.28.4)(db0@0.3.4)(embla-carousel@8.6.0)(ioredis@5.8.1)(magicast@0.3.5)(typescript@5.9.3)(vite@7.1.9(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-router@4.5.1(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3))(zod@4.1.12)':
|
'@nuxt/ui@4.0.1(@babel/parser@7.28.4)(db0@0.3.4)(embla-carousel@8.6.0)(ioredis@5.8.1)(magicast@0.3.5)(sortablejs@1.15.6)(typescript@5.9.3)(vite@7.1.9(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-router@4.5.1(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3))(zod@4.1.12)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ai-sdk/vue': 2.0.68(vue@3.5.22(typescript@5.9.3))(zod@4.1.12)
|
'@ai-sdk/vue': 2.0.68(vue@3.5.22(typescript@5.9.3))(zod@4.1.12)
|
||||||
'@iconify/vue': 5.0.0(vue@3.5.22(typescript@5.9.3))
|
'@iconify/vue': 5.0.0(vue@3.5.22(typescript@5.9.3))
|
||||||
@@ -4456,7 +4462,7 @@ snapshots:
|
|||||||
'@tanstack/vue-table': 8.21.3(vue@3.5.22(typescript@5.9.3))
|
'@tanstack/vue-table': 8.21.3(vue@3.5.22(typescript@5.9.3))
|
||||||
'@unhead/vue': 2.0.19(vue@3.5.22(typescript@5.9.3))
|
'@unhead/vue': 2.0.19(vue@3.5.22(typescript@5.9.3))
|
||||||
'@vueuse/core': 13.9.0(vue@3.5.22(typescript@5.9.3))
|
'@vueuse/core': 13.9.0(vue@3.5.22(typescript@5.9.3))
|
||||||
'@vueuse/integrations': 13.9.0(fuse.js@7.1.0)(vue@3.5.22(typescript@5.9.3))
|
'@vueuse/integrations': 13.9.0(fuse.js@7.1.0)(sortablejs@1.15.6)(vue@3.5.22(typescript@5.9.3))
|
||||||
colortranslator: 5.0.0
|
colortranslator: 5.0.0
|
||||||
consola: 3.4.2
|
consola: 3.4.2
|
||||||
defu: 6.1.4
|
defu: 6.1.4
|
||||||
@@ -5292,13 +5298,14 @@ snapshots:
|
|||||||
'@vueuse/shared': 13.9.0(vue@3.5.22(typescript@5.9.3))
|
'@vueuse/shared': 13.9.0(vue@3.5.22(typescript@5.9.3))
|
||||||
vue: 3.5.22(typescript@5.9.3)
|
vue: 3.5.22(typescript@5.9.3)
|
||||||
|
|
||||||
'@vueuse/integrations@13.9.0(fuse.js@7.1.0)(vue@3.5.22(typescript@5.9.3))':
|
'@vueuse/integrations@13.9.0(fuse.js@7.1.0)(sortablejs@1.15.6)(vue@3.5.22(typescript@5.9.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vueuse/core': 13.9.0(vue@3.5.22(typescript@5.9.3))
|
'@vueuse/core': 13.9.0(vue@3.5.22(typescript@5.9.3))
|
||||||
'@vueuse/shared': 13.9.0(vue@3.5.22(typescript@5.9.3))
|
'@vueuse/shared': 13.9.0(vue@3.5.22(typescript@5.9.3))
|
||||||
vue: 3.5.22(typescript@5.9.3)
|
vue: 3.5.22(typescript@5.9.3)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fuse.js: 7.1.0
|
fuse.js: 7.1.0
|
||||||
|
sortablejs: 1.15.6
|
||||||
|
|
||||||
'@vueuse/metadata@10.11.1': {}
|
'@vueuse/metadata@10.11.1': {}
|
||||||
|
|
||||||
@@ -7237,6 +7244,8 @@ snapshots:
|
|||||||
|
|
||||||
smob@1.5.0: {}
|
smob@1.5.0: {}
|
||||||
|
|
||||||
|
sortablejs@1.15.6: {}
|
||||||
|
|
||||||
source-map-js@1.2.1: {}
|
source-map-js@1.2.1: {}
|
||||||
|
|
||||||
source-map-support@0.5.21:
|
source-map-support@0.5.21:
|
||||||
|
|||||||
Reference in New Issue
Block a user