Files
inkreach-official-website/apps/website/app/composables/useProductCenter.ts
T

316 lines
7.6 KiB
TypeScript

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<T> {
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<Category[]>('pc-categories', () => []);
const countries = useState<Country[]>('pc-countries', () => []);
const tags = useState<Tag[]>('pc-tags', () => []);
const tagGroups = useState<TagGroup[]>('pc-tag-groups', () => []);
const products = useState<Product[]>('pc-products', () => []);
const total = useState<number>('pc-total', () => 0);
const loading = useState<boolean>('pc-loading', () => false);
const query = reactive<ProductQuery>({
countryId: undefined,
categoryId: undefined,
tagIds: [],
keyword: undefined,
page: 1,
pageSize: 20,
});
const selectedCategory = computed<string | null>(() => query.categoryId ?? null);
const selectedCountry = computed<string | null>(() => query.countryId ?? null);
const selectedTagIds = computed<string[]>(() => query.tagIds);
async function loadFilters(): Promise<void> {
try {
const needCats = categories.value.length === 0;
const [tgs, grs] = await Promise.all([
$fetch<BackendEnvelope<BackendTag[]>>('/api/backend/tags'),
$fetch<BackendEnvelope<BackendTagGroup[]>>('/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<BackendEnvelope<BackendCategoryNode[]>>('/api/backend/categories'),
$fetch<BackendEnvelope<BackendCountry[]>>('/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<void> {
loading.value = true;
try {
const params: Record<string, string | number> = {
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<BackendEnvelope<BackendPaginatedProducts>>(
'/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,
};
}