export interface Country { id: string; name: string; icon: string | null; } export interface Category { id: string; name: string; icon: string | null; parentId: string | null; children: Category[]; } export interface TagGroup { id: string; name: string; icon: string | null; color: string | null; sortOrder: number; } export interface Tag { id: string; name: string; color: string | null; fontColor: string | null; group: { id: string; name: string; sortOrder: number } | null; } export interface Product { id: string; name: string; priority: number; image: string | null; price: string | null; country: Country; category: Category; tag: Tag | null; tags: Tag[]; } export interface PaginatedProducts { items: Product[]; total: number; page: number; pageSize: number; } export interface ProductQuery { countryId?: string; categoryId?: string; tagIds: string[]; keyword?: string; page: number; pageSize: number; } interface BackendCategoryNode { id: string; categoryName: string; categoryIcon: string | null; parentCategoryId: string | null; children: BackendCategoryNode[]; } interface BackendCountry { id: string; countryName: string; countryIcon: string | null; } interface BackendTagGroup { id: string; groupName: string; groupIcon: string | null; groupColor: string | null; sortOrder: number; } interface BackendTag { id: string; tagName: string; tagColor: string | null; tagFontColor: string | null; group: { id: string; groupName: string; sortOrder: number } | null; } interface BackendProduct { id: string; goodName: string; goodPriority: number; country: { id: string; countryName: string; countryIcon: string | null }; category: { id: string; categoryName: string; categoryIcon: string | null }; tag: { id: string; tagName: string; tagColor: string | null; tagFontColor: string | null; group: { id: string; groupName: string; sortOrder: number } | null } | null; tags: { id: string; tagName: string; tagColor: string | null; tagFontColor: string | null; group: { id: string; groupName: string; sortOrder: number } | null }[]; image: string | null; price: string | null; createdAt: string; } interface BackendPaginatedProducts { items: BackendProduct[]; total: number; page: number; pageSize: number; } interface BackendEnvelope { data: T; success: boolean; } function mapCategory(node: BackendCategoryNode): Category { return { id: node.id, name: node.categoryName, icon: node.categoryIcon, parentId: node.parentCategoryId, children: node.children.map(mapCategory), }; } function mapCountry(c: BackendCountry): Country { return { id: c.id, name: c.countryName, icon: c.countryIcon, }; } function mapTag(t: BackendTag): Tag { return { id: t.id, name: t.tagName, color: t.tagColor, fontColor: t.tagFontColor, group: t.group ? { id: t.group.id, name: t.group.groupName, sortOrder: t.group.sortOrder } : null, }; } function mapTagGroup(g: BackendTagGroup): TagGroup { return { id: g.id, name: g.groupName, icon: g.groupIcon, color: g.groupColor, sortOrder: g.sortOrder, }; } /** Sort tags by their group.order (admin-configured). Tags without a group go last. */ export function sortTagsByGroup(tags: Tag[]): Tag[] { return [...tags].sort((a, b) => { const ao = a.group?.sortOrder ?? Number.MAX_SAFE_INTEGER; const bo = b.group?.sortOrder ?? Number.MAX_SAFE_INTEGER; return ao - bo; }); } function mapProduct(p: BackendProduct): Product { return { id: p.id, name: p.goodName, priority: p.goodPriority, image: p.image, price: p.price, country: mapCountry(p.country), category: { id: p.category.id, name: p.category.categoryName, icon: p.category.categoryIcon, parentId: null, children: [], }, tag: p.tag ? mapTag(p.tag) : null, tags: p.tags ? p.tags.map(mapTag) : [], }; } export function useProductCenter() { const categories = useState('pc-categories', () => []); const countries = useState('pc-countries', () => []); const tags = useState('pc-tags', () => []); const tagGroups = useState('pc-tag-groups', () => []); const products = useState('pc-products', () => []); const total = useState('pc-total', () => 0); const loading = useState('pc-loading', () => false); const query = reactive({ countryId: undefined, categoryId: undefined, tagIds: [], keyword: undefined, page: 1, pageSize: 20, }); const selectedCategory = computed(() => query.categoryId ?? null); const selectedCountry = computed(() => query.countryId ?? null); const selectedTagIds = computed(() => query.tagIds); async function loadFilters(): Promise { try { const needCats = categories.value.length === 0; const [tgs, grs] = await Promise.all([ $fetch>('/api/backend/tags'), $fetch>('/api/backend/tag-groups'), ]); tags.value = tgs.data.map(mapTag); tagGroups.value = grs.data.map(mapTagGroup); if (needCats) { const [cats, cos] = await Promise.all([ $fetch>('/api/backend/categories'), $fetch>('/api/backend/countries'), ]); categories.value = cats.data.map(mapCategory); countries.value = cos.data.map(mapCountry); } } catch (error) { console.error('Failed to load filters:', error); } } async function fetchProducts(): Promise { loading.value = true; try { const params: Record = { page: query.page, pageSize: query.pageSize, }; if (query.countryId) params.countryId = query.countryId; if (query.categoryId) params.categoryId = query.categoryId; if (query.tagIds.length > 0) params.tagIds = query.tagIds.join(','); if (query.keyword?.trim()) params.keyword = query.keyword.trim(); const response = await $fetch>( '/api/backend/goods', { params }, ); products.value = response.data.items.map(mapProduct); total.value = response.data.total; } catch (error) { console.error('Failed to fetch products:', error); products.value = []; total.value = 0; } finally { loading.value = false; } } function setCategory(id: string | null): void { query.categoryId = id ?? undefined; query.page = 1; } function setCountry(id: string | null): void { query.countryId = id ?? undefined; query.page = 1; } function setTag(id: string): void { const idx = query.tagIds.indexOf(id); if (idx >= 0) { query.tagIds.splice(idx, 1); } else { query.tagIds.push(id); } query.page = 1; } function clearTags(): void { query.tagIds = []; query.page = 1; } function setKeyword(value: string): void { query.keyword = value; query.page = 1; } function setPage(page: number): void { query.page = Math.max(1, page); } function setPageSize(size: number): void { query.pageSize = size; query.page = 1; } return { categories, countries, tags, tagGroups, products, total, loading, query, selectedCategory, selectedCountry, selectedTagIds, loadFilters, fetchProducts, setCategory, setCountry, setTag, clearTags, setKeyword, setPage, setPageSize, }; }