fix: convert apps/website from submodule to regular directory
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
export interface RecommendItem {
|
||||
id: number;
|
||||
title: string;
|
||||
image: string;
|
||||
link: string;
|
||||
}
|
||||
|
||||
export interface RecommendCategory {
|
||||
id: number;
|
||||
title: string;
|
||||
items: RecommendItem[];
|
||||
}
|
||||
|
||||
export interface SolutionLink {
|
||||
id: number;
|
||||
title: string;
|
||||
link: string;
|
||||
tag?: 'hot' | 'new';
|
||||
}
|
||||
|
||||
export interface SolutionColumn {
|
||||
id: number;
|
||||
title: string;
|
||||
links: SolutionLink[];
|
||||
}
|
||||
|
||||
export interface NavBanner {
|
||||
image: string;
|
||||
link: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const MOCK_RECOMMEND_CATEGORIES: RecommendCategory[] = [
|
||||
{
|
||||
id: 1,
|
||||
title: '热卖推荐',
|
||||
items: [
|
||||
{ id: 101, title: 'T恤 热销爆款', image: '/水洗T恤.png', link: 'https://inkpod.vip/portal/search' },
|
||||
{ id: 102, title: '卫衣 秋冬必备', image: '/鸭舌帽.png', link: 'https://inkpod.vip/portal/search' },
|
||||
{ id: 103, title: '帆布袋 创意定制', image: '/帆布袋.png', link: 'https://inkpod.vip/portal/search' },
|
||||
{ id: 104, title: '手机壳 个性潮流', image: '/设计工具.png', link: 'https://inkpod.vip/portal/search' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '新品上架',
|
||||
items: [
|
||||
{ id: 201, title: '棒球帽 运动风潮', image: '/鸭舌帽.png', link: 'https://inkpod.vip/portal/search' },
|
||||
{ id: 202, title: '马克杯 定制礼物', image: '/设计工具.png', link: 'https://inkpod.vip/portal/search' },
|
||||
{ id: 203, title: '帆布画 艺术家居', image: '/帆布袋.png', link: 'https://inkpod.vip/portal/search' },
|
||||
{ id: 204, title: '抱枕 舒适生活', image: '/水洗T恤.png', link: 'https://inkpod.vip/portal/search' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '节日专区',
|
||||
items: [
|
||||
{ id: 301, title: '圣诞系列 定制好礼', image: '/帆布袋.png', link: 'https://inkpod.vip/portal/search' },
|
||||
{ id: 302, title: '情人节 浪漫定制', image: '/水洗T恤.png', link: 'https://inkpod.vip/portal/search' },
|
||||
{ id: 303, title: '万圣节 搞怪周边', image: '/鸭舌帽.png', link: 'https://inkpod.vip/portal/search' },
|
||||
{ id: 304, title: '春节 新年好物', image: '/设计工具.png', link: 'https://inkpod.vip/portal/search' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const MOCK_SOLUTION_COLUMNS: SolutionColumn[] = [
|
||||
{
|
||||
id: 1,
|
||||
title: '跨境电商',
|
||||
links: [
|
||||
{ id: 12, title: 'Temu 卖家方案', link: 'https://inkpod.vip/portal/search', tag: 'new' },
|
||||
{ id: 13, title: 'eBay 卖家方案', link: 'https://inkpod.vip/portal/search' },
|
||||
{ id: 14, title: '速卖通方案', link: 'https://inkpod.vip/portal/search' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '独立站',
|
||||
links: [
|
||||
{ id: 21, title: 'Shopify 方案', link: 'https://inkpod.vip/portal/search', tag: 'hot' },
|
||||
{ id: 22, title: 'WooCommerce 方案', link: 'https://inkpod.vip/portal/search' },
|
||||
{ id: 23, title: '品牌出海方案', link: 'https://inkpod.vip/portal/search' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '定制场景',
|
||||
links: [
|
||||
{ id: 31, title: '礼品定制', link: 'https://inkpod.vip/portal/search' },
|
||||
{ id: 32, title: '宠物定制', link: 'https://inkpod.vip/portal/search', tag: 'new' },
|
||||
{ id: 33, title: '二次元周边', link: 'https://inkpod.vip/portal/search' },
|
||||
{ id: 34, title: '婚庆定制', link: 'https://inkpod.vip/portal/search' },
|
||||
{ id: 35, title: '团体定制', link: 'https://inkpod.vip/portal/search' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: '服务支持',
|
||||
links: [
|
||||
{ id: 41, title: '在线设计工具', link: 'https://inkpod.vip/portal/search' },
|
||||
{ id: 42, title: '供应链服务', link: 'https://inkpod.vip/portal/search' },
|
||||
{ id: 43, title: '一件代发', link: 'https://inkpod.vip/portal/search' },
|
||||
{ id: 44, title: '售后保障', link: 'https://inkpod.vip/portal/search' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const MOCK_SOLUTION_BANNER: NavBanner = {
|
||||
image: '/主视觉.png',
|
||||
link: 'https://inkpod.vip/portal/search',
|
||||
title: 'InkReach 全球 POD 供应链',
|
||||
};
|
||||
|
||||
export function useNavData() {
|
||||
const recommendCategories = ref<RecommendCategory[]>(MOCK_RECOMMEND_CATEGORIES);
|
||||
const solutionColumns = ref<SolutionColumn[]>(MOCK_SOLUTION_COLUMNS);
|
||||
const solutionBanner = ref<NavBanner | null>(MOCK_SOLUTION_BANNER);
|
||||
const activeRecommendIdx = ref(0);
|
||||
|
||||
const activeRecommendCategory = computed(
|
||||
() => recommendCategories.value[activeRecommendIdx.value] ?? null,
|
||||
);
|
||||
|
||||
function setActiveRecommend(idx: number): void {
|
||||
if (idx >= 0 && idx < recommendCategories.value.length) {
|
||||
activeRecommendIdx.value = idx;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
recommendCategories,
|
||||
solutionColumns,
|
||||
solutionBanner,
|
||||
activeRecommendIdx,
|
||||
activeRecommendCategory,
|
||||
setActiveRecommend,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
interface CategoryChild {
|
||||
id: number;
|
||||
name: string;
|
||||
parentId: number;
|
||||
parentName: string;
|
||||
tag: string;
|
||||
productNum: number;
|
||||
children: CategoryChild[];
|
||||
}
|
||||
|
||||
interface Category {
|
||||
id: number;
|
||||
name: string;
|
||||
tag: string;
|
||||
productNum: number;
|
||||
children: CategoryChild[];
|
||||
}
|
||||
|
||||
interface ProductItem {
|
||||
id: number;
|
||||
name: string;
|
||||
psd_img_url: string;
|
||||
thumbImgUrl: string;
|
||||
currentPrice: number;
|
||||
color_str: string[];
|
||||
}
|
||||
|
||||
interface ProductPage {
|
||||
totalCount: number;
|
||||
items: ProductItem[];
|
||||
page: number;
|
||||
size: number;
|
||||
}
|
||||
|
||||
interface CountryGroup {
|
||||
id: number;
|
||||
name: string;
|
||||
products: CategoryChild[];
|
||||
}
|
||||
|
||||
const COUNTRY_NAME_MAP: Record<string, string> = {
|
||||
'美国工厂直发': '美国',
|
||||
'日本本地工厂直发': '日本',
|
||||
'墨西哥工厂本地直发': '墨西哥',
|
||||
'巴西本地工厂直发': '巴西',
|
||||
'中东本地工厂直发': '中东',
|
||||
'欧洲波兰工厂直发': '欧洲波兰',
|
||||
'欧洲西班牙工厂直发': '欧洲西班牙',
|
||||
'欧洲德国工厂本地直发': '欧洲德国',
|
||||
'欧洲意大利工厂直发': '欧洲意大利',
|
||||
'英国本地直发': '英国',
|
||||
'加拿大本地工厂直发': '加拿大',
|
||||
'澳大利亚本地工厂直发': '澳大利亚',
|
||||
'韩国本地直发': '韩国',
|
||||
};
|
||||
|
||||
export function usePodProducts() {
|
||||
const categories = ref<Category[]>([]);
|
||||
const countryGroups = ref<CountryGroup[]>([]);
|
||||
const activeCountryIdx = ref(0);
|
||||
const productImages = ref<Map<number, string>>(new Map());
|
||||
const loading = ref(true);
|
||||
const loadingProducts = ref(false);
|
||||
|
||||
const countryPage = ref(0);
|
||||
const visibleCountryCount = 6;
|
||||
|
||||
const fetchCategories = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
const data = await $fetch<Category[]>('/api/pod/categories', {
|
||||
method: 'POST',
|
||||
});
|
||||
categories.value = data;
|
||||
countryGroups.value = data.map((cat) => ({
|
||||
id: cat.id,
|
||||
name: COUNTRY_NAME_MAP[cat.name] ?? cat.name,
|
||||
products: cat.children,
|
||||
}));
|
||||
if (countryGroups.value.length > 0) {
|
||||
await fetchProductsForCountry(0);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch categories:', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchProductsForCountry = async (idx: number) => {
|
||||
const group = countryGroups.value[idx];
|
||||
if (!group) return;
|
||||
|
||||
loadingProducts.value = true;
|
||||
try {
|
||||
const children = group.products.slice(0, 10);
|
||||
const promises = children.map((child) =>
|
||||
$fetch<ProductPage>(`/api/pod/products/${child.id}`).then((res) => ({
|
||||
id: child.id,
|
||||
name: child.name,
|
||||
image: res.items?.[0]?.psd_img_url ?? res.items?.[0]?.thumbImgUrl ?? '',
|
||||
})).catch(() => ({
|
||||
id: child.id,
|
||||
name: child.name,
|
||||
image: '',
|
||||
})),
|
||||
);
|
||||
const results = await Promise.all(promises);
|
||||
const newMap = new Map(productImages.value);
|
||||
for (const r of results) {
|
||||
newMap.set(r.id, r.image);
|
||||
}
|
||||
productImages.value = newMap;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch products:', error);
|
||||
} finally {
|
||||
loadingProducts.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const selectCountry = (idx: number) => {
|
||||
activeCountryIdx.value = idx;
|
||||
countryPage.value = Math.floor(idx / visibleCountryCount);
|
||||
fetchProductsForCountry(idx);
|
||||
};
|
||||
|
||||
const visibleCountries = computed(() => {
|
||||
const start = countryPage.value * visibleCountryCount;
|
||||
return countryGroups.value.slice(start, start + visibleCountryCount);
|
||||
});
|
||||
|
||||
const canPrevCountry = computed(() => countryPage.value > 0);
|
||||
const canNextCountry = computed(
|
||||
() =>
|
||||
(countryPage.value + 1) * visibleCountryCount <
|
||||
countryGroups.value.length,
|
||||
);
|
||||
|
||||
const currentProducts = computed(() => {
|
||||
const group = countryGroups.value[activeCountryIdx.value];
|
||||
if (!group) return [];
|
||||
return group.products.slice(0, 10).map((p) => ({
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
image: productImages.value.get(p.id) ?? '',
|
||||
}));
|
||||
});
|
||||
|
||||
return {
|
||||
categories,
|
||||
countryGroups,
|
||||
activeCountryIdx,
|
||||
productImages,
|
||||
loading,
|
||||
loadingProducts,
|
||||
countryPage,
|
||||
visibleCountryCount,
|
||||
visibleCountries,
|
||||
canPrevCountry,
|
||||
canNextCountry,
|
||||
currentProducts,
|
||||
fetchCategories,
|
||||
selectCountry,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user