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.
157 lines
4.1 KiB
TypeScript
157 lines
4.1 KiB
TypeScript
import { useLocalStorage, createSharedComposable } from "@vueuse/core";
|
|
|
|
export type Item = {
|
|
id: number;
|
|
brand?: string;
|
|
name: string;
|
|
imageUrl?: string;
|
|
description?: string | null;
|
|
tags?: string[] | null;
|
|
createdAt?: Date;
|
|
updatedAt?: Date;
|
|
};
|
|
|
|
const _useItems = () => {
|
|
const stored = useLocalStorage<Item[]>("item-database", [], {
|
|
serializer: {
|
|
read: (v) => {
|
|
if (!v) return [];
|
|
try {
|
|
const parsed = JSON.parse(v);
|
|
return (parsed ?? []).map((item: any) => ({
|
|
...item,
|
|
createdAt: item.createdAt ? new Date(item.createdAt) : new Date(),
|
|
updatedAt: item.updatedAt ? new Date(item.updatedAt) : new Date(),
|
|
// ensure tags is an array or null
|
|
tags: item.tags
|
|
? Array.isArray(item.tags)
|
|
? item.tags
|
|
: String(item.tags)
|
|
.split(",")
|
|
.map((t: string) => t.trim())
|
|
: null,
|
|
}));
|
|
} catch (err) {
|
|
console.error("Failed to parse item-database from localStorage", err);
|
|
return [];
|
|
}
|
|
},
|
|
write: (v) => {
|
|
try {
|
|
return JSON.stringify(v ?? []);
|
|
} catch (err) {
|
|
console.error("Failed to stringify item-database", err);
|
|
return "[]";
|
|
}
|
|
},
|
|
},
|
|
});
|
|
|
|
const items = stored; // Ref<Item[]>
|
|
|
|
const stats = {
|
|
totalItems: computed(() => (items.value ?? []).length),
|
|
addedThisMonth: computed(() => {
|
|
const now = new Date();
|
|
const thisMonth = now.getMonth();
|
|
const thisYear = now.getFullYear();
|
|
return (items.value ?? []).filter(
|
|
(item) =>
|
|
item.createdAt?.getMonth() === thisMonth &&
|
|
item.createdAt?.getFullYear() === thisYear
|
|
).length;
|
|
}),
|
|
};
|
|
|
|
const utils = {
|
|
// Get all brandings as array of strings
|
|
allBrands: ref<string[]>(
|
|
(() => {
|
|
const brands = new Set<string>();
|
|
(items.value ?? []).forEach((item) => {
|
|
if (item.brand) {
|
|
brands.add(item.brand);
|
|
}
|
|
});
|
|
return Array.from(brands).sort((a, b) => a.localeCompare(b));
|
|
})()
|
|
),
|
|
// Get all tags as array of strings
|
|
getAllTags: () => {
|
|
const tags = new Set<string>();
|
|
(items.value ?? []).forEach((item) => {
|
|
if (item.tags) {
|
|
item.tags.forEach((tag) => tags.add(tag));
|
|
}
|
|
});
|
|
return Array.from(tags).sort((a, b) => a.localeCompare(b));
|
|
},
|
|
};
|
|
|
|
const latestId = computed(() => {
|
|
const arr = items.value ?? [];
|
|
if (arr.length === 0) return 1;
|
|
return Math.max(...arr.map((i) => i.id)) + 1;
|
|
});
|
|
|
|
const isNameAvailable = (id: number | null, name: string, brand?: string) => {
|
|
const list = items.value ?? [];
|
|
|
|
return !list.some((item) => {
|
|
// 跳过自己(编辑时)
|
|
if (id && item.id === id) return false;
|
|
|
|
// 有品牌时需同时匹配品牌
|
|
if (brand) {
|
|
return item.name === name && item.brand === brand;
|
|
}
|
|
|
|
// 没品牌时只比较名字
|
|
return item.name === name;
|
|
});
|
|
};
|
|
|
|
const addItem = (item: Item) => {
|
|
item.id = latestId.value;
|
|
item.createdAt = new Date();
|
|
item.updatedAt = new Date();
|
|
items.value = [...(items.value ?? []), item];
|
|
console.debug("[addItem] new length:", (items.value ?? []).length);
|
|
};
|
|
|
|
const editItem = (item: Item) => {
|
|
const index = (items.value ?? []).findIndex((i) => i.id === item.id);
|
|
if (index === -1) return;
|
|
const updatedItems = [...(items.value ?? [])];
|
|
updatedItems[index] = {
|
|
...updatedItems[index],
|
|
...item,
|
|
updatedAt: new Date(),
|
|
};
|
|
items.value = updatedItems;
|
|
console.debug("[editItem] updated id:", item.id);
|
|
};
|
|
|
|
const removeItem = (id: number) => {
|
|
items.value = (items.value ?? []).filter((i) => i.id !== id);
|
|
console.debug(
|
|
"[removeItem] removed id:",
|
|
id,
|
|
"new length:",
|
|
(items.value ?? []).length
|
|
);
|
|
};
|
|
|
|
return {
|
|
items,
|
|
stats,
|
|
utils,
|
|
isNameAvailable,
|
|
addItem,
|
|
editItem,
|
|
removeItem,
|
|
};
|
|
};
|
|
|
|
export const useItemsStore = createSharedComposable(_useItems);
|