Files
inkreach-uni/pages/products/products.vue
T
2026-08-21 16:58:11 +08:00

653 lines
24 KiB
Vue
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.
<template>
<!-- 产品列表页 - 印美达 Inkreach -->
<view class="products-page">
<!-- 搜索栏 -->
<view class="search-bar">
<view class="search-input-wrap">
<text class="search-icon">🔍</text>
<input class="search-input" placeholder="搜索商品" placeholder-class="search-placeholder" @input="onSearchInput" :value="searchKeyword" />
</view>
<view class="filter-btn" @tap="toggleFilter">
<text class="filter-icon"></text>
<text class="filter-text">筛选</text>
</view>
</view>
<!-- 国家选择条 -->
<scroll-view class="country-bar" :scroll-x="true" :enhanced="true" :show-scrollbar="false">
<view :class="'country-item ' + (selectedCountry === item.id ? 'active' : '')" @tap="selectCountry" :data-id="item.id" v-for="(item, index) in countries" :key="index">
<text class="country-flag">{{ item.flag }}</text>
<text class="country-name">{{ item.name }}</text>
</view>
</scroll-view>
<!-- 已选筛选标签 -->
<view class="active-filters" v-if="activeFilters.length > 0">
<view class="active-filter-tag" v-for="(item, index) in activeFilters" :key="index">
<text>{{ item.label }}</text>
<text class="filter-remove" @tap="removeFilter" :data-key="item.key"></text>
</view>
<view class="clear-all" @tap="clearAllFilters">清除全部</view>
</view>
<!-- 主体内容侧边栏 + 产品列表 -->
<view class="content-body">
<!-- 左侧分类侧边栏 -->
<scroll-view class="sidebar" :scroll-y="true">
<view
:class="'sidebar-item ' + (activeCategory === item.id ? 'active' : '')"
@tap="selectCategory"
:data-id="item.id"
v-for="(item, index) in categories"
:key="index"
>
<view class="sidebar-indicator" v-if="activeCategory === item.id"></view>
<text>{{ item.name }}</text>
</view>
<!-- 促销Banner -->
<view class="sidebar-promo">
<text class="promo-title">0元拿样</text>
<text class="promo-desc">全国包邮</text>
</view>
</scroll-view>
<!-- 右侧产品列表 -->
<scroll-view class="product-list-area" :scroll-y="true" @scrolltolower="loadMore">
<!-- 子分类 -->
<view class="sub-categories" v-if="subCategories.length > 0">
<view
:class="'sub-cat-item ' + (activeSubCategory === item.id ? 'active' : '')"
@tap="selectSubCategory"
:data-id="item.id"
v-for="(item, index) in subCategories"
:key="index"
>
{{ item.name }}
</view>
</view>
<!-- 产品网格 -->
<view class="product-grid">
<view class="product-card" @tap="goDetail" :data-id="item.id" v-for="(item, index) in products" :key="index">
<view class="product-img">
<image class="product-image" :src="item.image" mode="aspectFill" v-if="item.image"></image>
<view class="product-badges">
<view class="product-badge" :style="'background: ' + tag.bg + '; color: ' + tag.color + ';'" v-for="(tag, index1) in item.tags" :key="index1">
{{ tag.text }}
</view>
</view>
</view>
<view class="product-info">
<text class="product-name">{{ item.name }}</text>
<view class="product-bottom">
<text class="product-price">
¥{{ item.price }}
<text class="product-price-unit"></text>
</text>
</view>
</view>
</view>
</view>
<!-- 分页 -->
<view class="pagination">
<text class="page-info">显示 {{ pagination.start }}-{{ pagination.end }} {{ pagination.total }} 个产品</text>
<view class="page-controls">
<view :class="'page-btn ' + (pagination.current === 1 ? 'disabled' : '')" @tap="changePage" data-action="prev">上一页</view>
<view
:class="'page-num ' + (pagination.current === item ? 'active' : '')"
@tap="goToPage"
:data-page="item"
v-for="(item, index) in pageNumbers"
:key="index"
>
{{ item }}
</view>
<view :class="'page-btn ' + (pagination.current === pagination.totalPages ? 'disabled' : '')" @tap="changePage" data-action="next">下一页</view>
</view>
<view class="page-size">
<text>{{ pagination.pageSize }}/</text>
</view>
</view>
<!-- 加载更多提示 -->
<view class="loading-more" v-if="loadingMore">
<text>加载中...</text>
</view>
</scroll-view>
</view>
<!-- 筛选弹窗 -->
<view :class="'filter-overlay ' + (showFilter ? 'show' : '')" @tap="closeFilter"></view>
<view :class="'filter-drawer ' + (showFilter ? 'show' : '')">
<view class="filter-drawer-header">
<text class="filter-drawer-title">筛选条件</text>
<text class="filter-drawer-close" @tap="closeFilter"></text>
</view>
<scroll-view class="filter-drawer-body" :scroll-y="true">
<!-- 动态标签筛选组 API 获取 -->
<view class="filter-group" v-for="(group, gIdx) in filterGroups" :key="gIdx">
<text class="filter-group-title">{{ group.title }}</text>
<view class="filter-options">
<view
:class="'filter-option ' + (group.selected.indexOf(tag.id) > -1 ? 'selected' : '')"
@tap="toggleFilterTag"
:data-group-idx="gIdx"
:data-tag-id="tag.id"
v-for="(tag, tIdx) in group.tags"
:key="tIdx"
>
{{ tag.name }}
</view>
</view>
</view>
<!-- 价格区间 -->
<view class="filter-group">
<text class="filter-group-title">价格区间</text>
<view class="filter-price-range">
<input class="price-input" placeholder="最低价" type="number" />
<text class="price-divider">-</text>
<input class="price-input" placeholder="最高价" type="number" />
</view>
</view>
</scroll-view>
<view class="filter-drawer-footer">
<view class="filter-reset" @tap="resetFilters">重置</view>
<view class="filter-confirm" @tap="confirmFilters">确定</view>
</view>
</view>
</view>
</template>
<script>
import { getCountries, getCategories, getTagGroups, getGoods } from '@/api/public.js';
export default {
data() {
return {
searchKeyword: '',
selectedCountry: '',
countries: [],
activeCategory: 'all',
categories: [
{
id: 'all',
name: '全部商品'
}
],
subCategories: [],
activeSubCategory: '',
activeFilters: [],
showFilter: false,
filterGroups: [],
pagination: {
start: 1,
end: 10,
total: 0,
current: 1,
pageSize: 10,
totalPages: 0
},
pageNumbers: [],
loadingMore: false,
products: []
};
},
async onLoad() {
await this.initData();
this.loadSubCategories('all');
await this.loadGoods(1);
},
methods: {
async initData() {
try {
// 1. 国家列表(始终全量)
const countriesRes = await getCountries();
const countriesData = (countriesRes.data && countriesRes.data.data) || [];
const countries = countriesData.map((c) => ({
id: c.id,
name: c.countryName,
flag: this.codeToEmoji(c.countryIcon)
}));
this.setData({
countries
});
// 2. 默认不选中任何国家,显示全部国家的数据
await this.loadCategoryData('');
// 简单检测结构 — 打印到 console 验证
console.log('=== [Products API Test] ===');
console.log('[1] countries:', countries.length, '条');
console.log('=== [Test End] ===');
} catch (err) {
console.error('[Products initData Error]', err);
}
},
/**
* 加载分类树和标签组(按国家过滤)
* @param {string} countryId 国家 ID,传空字符串表示全部国家
*/
async loadCategoryData(countryId) {
try {
const params = countryId ? { countryId: countryId } : {};
const [categoriesRes, tagGroupsRes] = await Promise.all([
getCategories(params),
getTagGroups(params)
]);
// 分类树 → 映射到侧边栏
const categoriesData = (categoriesRes.data && categoriesRes.data.data) || [];
const categories = [{ id: 'all', name: '全部商品', children: [], productCount: 0 }];
categoriesData.forEach((cat) => {
categories.push({
id: cat.id,
name: cat.categoryName,
children: cat.children || [],
productCount: cat.productCount || 0
});
});
// 标签组 → 映射到筛选弹窗
const tagGroupsData = (tagGroupsRes.data && tagGroupsRes.data.data) || [];
const filterGroups = tagGroupsData.map((g) => ({
id: g.id,
title: g.groupName,
tags: (g.tags || []).map((t) => ({
id: t.id,
name: t.tagName,
color: t.tagColor,
fontColor: t.tagFontColor,
productCount: t.productCount || 0
})),
selected: []
}));
this.setData({
categories,
filterGroups,
activeCategory: 'all',
subCategories: [],
activeSubCategory: ''
});
console.log('[loadCategoryData] countryId=' + (countryId || 'all') +
', categories=' + categories.length +
', filterGroups=' + filterGroups.length);
} catch (err) {
console.error('[loadCategoryData Error]', err);
}
},
/**
* 加载商品列表(分页 + 筛选)
* @param {number} [page] 页码,默认 1
*/
async loadGoods(page) {
try {
const currentPage = page || this.pagination.current || 1;
const pageSize = this.pagination.pageSize || 10;
var params = {
page: currentPage,
pageSize: pageSize
};
// 国家筛选
if (this.selectedCountry) {
params.countryId = this.selectedCountry;
}
// 分类筛选(排除 "all"
if (this.activeCategory && this.activeCategory !== 'all') {
params.categoryId = this.activeCategory;
}
// 关键词搜索
if (this.searchKeyword && this.searchKeyword.trim()) {
params.keyword = this.searchKeyword.trim();
}
console.log('[loadGoods] params:', JSON.stringify(params));
const res = await getGoods(params);
const data = (res.data && res.data.data) || { items: [], total: 0, page: 1, pageSize: 10 };
const items = data.items || [];
// 映射:API 字段 → 模板字段
const products = items.map((item) => ({
id: item.goodId,
name: item.goodName,
price: item.price,
image: item.image,
bgColor: '#F5F5F5',
emoji: '👕',
tags: (item.tags || []).map(function (t) {
return {
text: t.tagName,
bg: t.tagColor,
color: t.tagFontColor
};
})
}));
const total = data.total || 0;
const totalPages = Math.ceil(total / pageSize) || 1;
var pageNumbers = [];
for (var i = Math.max(1, currentPage - 2); i <= Math.min(totalPages, currentPage + 2); i++) {
pageNumbers.push(i);
}
this.setData({
products: products,
pagination: {
start: total === 0 ? 0 : (currentPage - 1) * pageSize + 1,
end: Math.min(currentPage * pageSize, total),
total: total,
current: currentPage,
pageSize: pageSize,
totalPages: totalPages
},
pageNumbers: pageNumbers,
loadingMore: false
});
console.log('[loadGoods] total=' + total +
', page=' + currentPage +
', items=' + products.length);
} catch (err) {
console.error('[loadGoods Error]', err);
this.setData({
products: [],
loadingMore: false
});
}
},
codeToEmoji(iconPath) {
if (!iconPath) return '🌐';
var match = iconPath.match(/\/([^/]+)\.\w+$/);
if (!match) return '🌐';
var code = match[1].toUpperCase();
if (code.length !== 2) return '🌐';
var A = 0x1f1e6;
return String.fromCodePoint(A + code.charCodeAt(0) - 65) + String.fromCodePoint(A + code.charCodeAt(1) - 65);
},
loadSubCategories(catId) {
var subCats = [];
if (catId !== 'all') {
var cat = this.categories.find(function (c) { return c.id === catId; });
if (cat && cat.children && cat.children.length > 0) {
subCats = cat.children.map(function (child) {
return { id: child.id, name: child.categoryName };
});
}
}
this.setData({
subCategories: subCats,
activeSubCategory: subCats.length > 0 ? subCats[0].id : ''
});
},
onSearchInput(e) {
this.setData({
searchKeyword: e.detail.value
});
// 防抖搜索
if (this._searchTimer) clearTimeout(this._searchTimer);
var self = this;
this._searchTimer = setTimeout(function () {
self.loadGoods(1);
}, 300);
},
async selectCountry(e) {
const id = e.currentTarget.dataset.id;
// 如果点击的是已选中的国家 → 取消选中,重置为全部国家数据
if (this.selectedCountry === id) {
this.setData({
selectedCountry: '',
activeFilters: this.activeFilters.filter((f) => f.key !== 'country')
});
await this.loadCategoryData('');
await this.loadGoods(1);
return;
}
// 选中新国家 → 重新请求分类和标签组
const country = this.countries.find((c) => c.id === id);
const filters = this.activeFilters.filter((f) => f.key !== 'country');
if (country) {
filters.push({
key: 'country',
label: country.name
});
}
this.setData({
selectedCountry: id,
activeFilters: filters
});
await this.loadCategoryData(id);
await this.loadGoods(1);
},
async selectCategory(e) {
const id = e.currentTarget.dataset.id;
this.setData({
activeCategory: id
});
this.loadSubCategories(id);
await this.loadGoods(1);
},
selectSubCategory(e) {
this.setData({
activeSubCategory: e.currentTarget.dataset.id
});
},
toggleFilter() {
this.setData({
showFilter: !this.showFilter
});
},
closeFilter() {
this.setData({
showFilter: false
});
},
toggleFilterTag(e) {
const gIdx = e.currentTarget.dataset.groupIdx;
const tagId = e.currentTarget.dataset.tagId;
var groups = [...this.filterGroups];
var group = groups[gIdx];
if (!group) return;
var selected = [...group.selected];
var idx = selected.indexOf(tagId);
if (idx > -1) {
selected.splice(idx, 1);
} else {
selected.push(tagId);
}
group.selected = selected;
this.setData({
filterGroups: groups
});
},
async removeFilter(e) {
const key = e.currentTarget.dataset.key;
const filters = this.activeFilters.filter((f) => f.key !== key);
// 如果清除的是国家筛选 → 重置分类和标签组为全部国家
if (key === 'country') {
this.setData({
selectedCountry: '',
activeFilters: filters
});
await this.loadCategoryData('');
await this.loadGoods(1);
return;
}
// 清除标签筛选 → 同步更新 filterGroups.selected
if (key.indexOf('tag_') === 0) {
var groupId = key.replace('tag_', '');
var groups = this.filterGroups.map(function (g) {
if (String(g.id) === String(groupId)) {
return Object.assign({}, g, { selected: [] });
}
return g;
});
this.setData({
activeFilters: filters,
filterGroups: groups
});
await this.loadGoods(1);
return;
}
this.setData({
activeFilters: filters
});
},
async clearAllFilters() {
// 是否有国家筛选
const hasCountry = this.activeFilters.some((f) => f.key === 'country');
this.setData({
activeFilters: []
});
if (hasCountry || this.selectedCountry) {
this.setData({
selectedCountry: ''
});
await this.loadCategoryData('');
}
// 清空标签选中
var groups = this.filterGroups.map(function (g) {
return Object.assign({}, g, { selected: [] });
});
this.setData({
filterGroups: groups
});
await this.loadGoods(1);
},
resetFilters() {
var groups = this.filterGroups.map(function (g) {
return Object.assign({}, g, { selected: [] });
});
this.setData({
filterGroups: groups
});
},
async confirmFilters() {
var filters = [];
// 保留国家筛选
var countryFilter = this.activeFilters.find(function (f) { return f.key === 'country'; });
if (countryFilter) filters.push(countryFilter);
this.filterGroups.forEach(function (group) {
if (group.selected.length > 0) {
var labels = group.tags
.filter(function (t) { return group.selected.indexOf(t.id) > -1; })
.map(function (t) { return t.name; });
if (labels.length > 0) {
filters.push({
key: 'tag_' + group.id,
label: labels.join(',')
});
}
}
});
this.setData({
activeFilters: filters,
showFilter: false
});
await this.loadGoods(1);
},
goDetail(e) {
const id = e.currentTarget.dataset.id;
uni.navigateTo({
url: `/pages/product-detail/product-detail?id=${id}`
});
},
async changePage(e) {
const action = e.currentTarget.dataset.action;
let current = this.pagination.current;
if (action === 'prev' && current > 1) {
current--;
}
if (action === 'next' && current < this.pagination.totalPages) {
current++;
}
await this.loadGoods(current);
},
async goToPage(e) {
await this.loadGoods(e.currentTarget.dataset.page);
},
updatePagination(current) {
const total = this.pagination.total;
const pageSize = this.pagination.pageSize;
const totalPages = Math.ceil(total / pageSize);
let pageNumbers = [];
for (let i = Math.max(1, current - 2); i <= Math.min(totalPages, current + 2); i++) {
pageNumbers.push(i);
}
this.setData({
pagination: {
...this.pagination,
current,
start: (current - 1) * pageSize + 1,
end: Math.min(current * pageSize, total),
totalPages
},
pageNumbers
});
},
loadMore() {
if (this.loadingMore) {
return;
}
const nextPage = this.pagination.current + 1;
if (nextPage > this.pagination.totalPages) {
return;
}
this.setData({
loadingMore: true
});
this.loadGoods(nextPage);
}
}
};
</script>
<style>
@import './products.css';
</style>