316 lines
7.6 KiB
TypeScript
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,
|
|
};
|
|
}
|