Files
bid-setup.tootaio.com/app/composables/items.ts
xiaomai 3909f614d2 feat(ui): implement dashboard layout and refactor pages
This commit replaces the previous simple layout with a comprehensive dashboard interface using Nuxt UI Pro components.

- Introduced a new default layout with `UDashboardSidebar` and `UDashboardPanel`.
- Refactored the main page (`index.vue`) by splitting it into `ItemManageStatistics` and `ItemManageTable` components.
- Added new computed stats for monthly growth and total images, and displayed them in new statistic cards.
- Reorganized components from `item/` to a new `itemManage/` directory.
- Added custom primary and secondary theme colors.
2025-10-21 14:16:27 +08:00

196 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}),
// Compared to last month
comparedToLastMonth: computed(() => {
const now = new Date();
const thisMonth = now.getMonth();
const thisYear = now.getFullYear();
const lastMonthDate = new Date(now);
lastMonthDate.setMonth(thisMonth - 1);
const lastMonth = lastMonthDate.getMonth();
const lastMonthYear = lastMonthDate.getFullYear();
const thisMonthCount = (items.value ?? []).filter(
(item) =>
item.createdAt?.getMonth() === thisMonth &&
item.createdAt?.getFullYear() === thisYear
).length;
const lastMonthCount = (items.value ?? []).filter(
(item) =>
item.createdAt?.getMonth() === lastMonth &&
item.createdAt?.getFullYear() === lastMonthYear
).length;
// if (lastMonthCount === 0) {
// return thisMonthCount === 0 ? 0 : 100;
// }
// return ((thisMonthCount - lastMonthCount) / lastMonthCount) * 100;
if (lastMonthCount === 0) {
// 上个月为 0本月不为 0 → 直接视为无限增长(返回 100 * thisMonthCount
return thisMonthCount === 0 ? 0 : thisMonthCount * 100;
}
// 允许超过 100% 的差值百分比
return ((thisMonthCount - lastMonthCount) / lastMonthCount) * 100;
}),
totalImages: computed(() => {
return (items.value ?? []).filter((item) => item.imageUrl).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);