diff --git a/pages/product-detail/product-detail.vue b/pages/product-detail/product-detail.vue index 8f6658c..f322931 100644 --- a/pages/product-detail/product-detail.vue +++ b/pages/product-detail/product-detail.vue @@ -1,5 +1,5 @@ @@ -56,15 +58,7 @@ import ParamsTable from './components/ParamsTable.vue' import SizeChart from './components/SizeChart.vue' import PackagingSpec from './components/PackagingSpec.vue' -import { - mockProduct, - mockImages, - mockColors, - mockSizes, - mockParams, - mockSizeChart, - mockPackaging -} from './mock.js' +import { getGoodsDetail } from '@/api/public.js' export default { components: { @@ -79,25 +73,182 @@ export default { data() { return { productId: null, - product: mockProduct, - productImages: mockImages, - colors: mockColors, - // 默认选中"灰色"(id=3),与 Figma 设计稿一致 - selectedColorId: 3, - sizes: mockSizes, - // 默认选中 L,与 Figma 设计稿一致 - selectedSize: 'L', - productParams: mockParams, - sizeChart: mockSizeChart, - packaging: mockPackaging + loading: false, + product: { + name: '', + sku: '', + country: '', + price: { symbol: '¥', integer: '0', decimal: '' }, + quickInfo: [] + }, + productImages: [], + colors: [], + selectedColorId: '', + sizes: [], + selectedSize: '', + productParams: [], + sizeChart: {}, + packaging: null } }, onLoad(options) { if (options && options.id) { this.productId = options.id + this.loadDetail(options.id) } }, methods: { + async loadDetail(goodId) { + if (!goodId) return + this.loading = true + try { + const res = await getGoodsDetail(goodId) + const detail = (res && res.data && res.data.data) || {} + this.applyDetail(detail) + } catch (err) { + console.error('[product-detail loadDetail Error]', err) + } finally { + this.loading = false + } + }, + + /** + * 将接口详情数据映射为各组件所需结构 + */ + applyDetail(detail) { + // 1. 产品信息:名称 / SKU / 国家 / 价格 / 4 列快捷信息 + this.product = { + name: detail.goodName || '—', + sku: detail.productCode || '—', + country: (detail.country && detail.country.countryName) || '—', + price: this.parsePrice(detail.price), + quickInfo: [ + { label: '生产工艺', value: (detail.details && detail.details.productionProcess) || '—' }, + { + label: '发货时效', + value: detail.productionCycleHours ? detail.productionCycleHours + '小时' : '—' + }, + { label: '印花', value: (detail.details && detail.details.designArea) || '—' }, + { label: '产品编码', value: detail.productCode || '—' } + ] + } + + // 2. 图片画廊:按 sortOrder 升序,首图置顶 + const media = detail.media || {} + let images = (media.images || []) + .slice() + .sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0)) + .map((img) => img.url) + .filter((url) => url) + if (media.primaryImageUrl && images.indexOf(media.primaryImageUrl) === -1) { + images.unshift(media.primaryImageUrl) + } + if (!images.length && detail.image) { + images.push(detail.image) + } + this.productImages = images + + // 3. 颜色:仅取启用项,按 sortOrder 升序 + const colors = ((detail.options && detail.options.colors) || []) + .filter((c) => c.enabled !== false) + .slice() + .sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0)) + .map((c) => ({ id: c.id, name: c.name, value: c.hex })) + this.colors = colors + this.selectedColorId = colors.length ? colors[0].id : '' + + // 4. 尺码:仅取启用项,按 sortOrder 升序 + const sizes = ((detail.options && detail.options.sizes) || []) + .filter((s) => s.enabled !== false) + .slice() + .sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0)) + .map((s) => s.name) + this.sizes = sizes + this.selectedSize = sizes.length ? sizes[0] : '' + + // 5. 产品参数(含可展开的"补充说明") + this.productParams = this.buildParams(detail) + + // 6. 尺码表 + this.sizeChart = this.buildSizeChart(detail.sizeChart) + + // 7. 包装规格:接口暂未提供,预留为空 + this.packaging = null + }, + + /** + * 价格拆分:'35' → { symbol:'¥', integer:'35', decimal:'' } + * '35.50' → { symbol:'¥', integer:'35', decimal:'.50' } + */ + parsePrice(price) { + const raw = price != null ? String(price) : '' + let integer = raw + let decimal = '' + const dot = raw.indexOf('.') + if (dot >= 0) { + integer = raw.slice(0, dot) || '0' + decimal = raw.slice(dot) + } + return { symbol: '¥', integer: integer || '0', decimal: decimal } + }, + + /** + * 构造产品参数列表:克重 / 材质 / 补充说明(可展开) + */ + buildParams(detail) { + const d = detail.details || {} + const params = [] + if (detail.minWeightG) { + params.push({ label: '克重', value: detail.minWeightG + 'g' }) + } + if (d.materialDescription) { + params.push({ label: '材质', value: d.materialDescription }) + } + const expandableText = this.buildExpandableText(d) + if (expandableText) { + params.push({ label: '补充说明', expandable: true, value: expandableText }) + } + return params + }, + + /** + * 拼接"补充说明"多段文本:材质说明/产品性能/适用情景/洗涤说明/图片要求/特殊说明/温馨提示 + */ + buildExpandableText(d) { + const lines = [] + if (d.materialDescription) lines.push('【 材质说明 】\n' + d.materialDescription) + if (d.productPerformance) lines.push('【 产品性能 】\n' + d.productPerformance) + if (d.applicableScenarios) lines.push('【 适用情景 】\n' + d.applicableScenarios) + if (d.washingInstructions) lines.push('【 洗涤说明 】\n' + d.washingInstructions) + if (d.pictureRequest) lines.push('【 图片要求 】\n' + d.pictureRequest) + if (d.specialDescription) lines.push('【 特殊说明 】\n' + d.specialDescription) + if (d.reminder) lines.push('【 温馨提醒 】\n' + d.reminder) + return lines.join('\n') + }, + + /** + * 构造尺码表:接口 measurements.key (chest/bodyLength/shoulder/sleeveLength) → 表格列 + */ + buildSizeChart(sizeChart) { + if (!sizeChart || !sizeChart.rows || !sizeChart.rows.length) return {} + const columns = ['尺码', '肩宽', '胸围', '衣长', '袖长'] + const rows = sizeChart.rows.map((row) => { + const measurements = row.measurements || [] + const get = (key) => { + const m = measurements.find((x) => x.key === key) + return m ? m.cm : '' + } + return { + size: row.sizeName || '', + shoulder: get('shoulder'), + chest: get('chest'), + length: get('bodyLength'), + sleeve: get('sleeveLength') + } + }) + return { unit: 'cm', columns, rows } + }, + selectColor(id) { this.selectedColorId = id }, diff --git a/pages/products/components/ProductGrid.vue b/pages/products/components/ProductGrid.vue index 6c93797..123e916 100644 --- a/pages/products/components/ProductGrid.vue +++ b/pages/products/components/ProductGrid.vue @@ -13,7 +13,7 @@ - + 1) this.$emit('page-change', this.current - 1) }, onNext() { if (this.current < this.totalPages) this.$emit('page-change', this.current + 1) }, - onGoPage(page) { this.$emit('page-change', page) }, - onLoadMore() { this.$emit('load-more') } + onGoPage(page) { this.$emit('page-change', page) } } } diff --git a/pages/products/components/SearchBar.vue b/pages/products/components/SearchBar.vue index 4dc380b..da47106 100644 --- a/pages/products/components/SearchBar.vue +++ b/pages/products/components/SearchBar.vue @@ -9,6 +9,7 @@ placeholder-class="search-placeholder" :value="keyword" @input="onInput" + @blur="onBlur" /> @@ -31,6 +32,9 @@ export default { onInput(e) { this.$emit('input', e.detail.value) }, + onBlur() { + this.$emit('blur') + }, onFilter() { this.$emit('filter') } diff --git a/pages/products/products.vue b/pages/products/products.vue index 8035690..20d6bd7 100644 --- a/pages/products/products.vue +++ b/pages/products/products.vue @@ -5,6 +5,7 @@ @@ -49,7 +50,6 @@ @sub-select="selectSubCategory" @product-click="goDetail" @page-change="loadGoods" - @load-more="loadMore" /> @@ -199,29 +199,16 @@ export default { }, /** - * 加载商品列表(分页 + 筛选) + * 统一加载商品列表(分页 + 全部筛选条件) + * 所有触发点(搜索失焦 / 分类点击 / 国家切换 / 标签点击 / 确认筛选 / 分页)都走这里 */ 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 - } - - if (this.activeCategory && this.activeCategory !== 'all') { - params.categoryId = this.activeCategory - } - - if (this.searchKeyword && this.searchKeyword.trim()) { - params.keyword = this.searchKeyword.trim() - } + // 统一参数对象:由 buildQueryParams 统一拼装 + const params = this.buildQueryParams(currentPage) console.log('[loadGoods] params:', JSON.stringify(params)) @@ -274,6 +261,40 @@ export default { } }, + /** + * 构造统一的查询参数对象 + * - countryId / categoryId / keyword / page / pageSize 直传后端 + * - categoryId:子分类优先,其次顶级分类 + * - tags:对象数组 [{ tagGroupId, tagIds }],同组 OR、跨组 AND(后端处理) + */ + buildQueryParams(page) { + const params = { + page: page || 1, + pageSize: this.pagination.pageSize || 10 + } + if (this.selectedCountry) { + params.countryId = this.selectedCountry + } + const catId = this.activeSubCategory || (this.activeCategory !== 'all' ? this.activeCategory : '') + if (catId) { + params.categoryId = catId + } + const kw = (this.searchKeyword || '').trim() + if (kw) { + params.keyword = kw + } + const tagsList = (this.filterGroups || []) + .filter((g) => g.selected && g.selected.length > 0) + .map((g) => ({ + tagGroupId: String(g.id), + tagIds: (g.selected || []).map((id) => String(id)) + })) + if (tagsList.length > 0) { + params.tags = JSON.stringify(tagsList) + } + return params + }, + codeToEmoji(iconPath) { if (!iconPath) return '🌐' var match = iconPath.match(/\/([^/]+)\.\w+$/) @@ -295,17 +316,16 @@ export default { } } this.subCategories = subCats - this.activeSubCategory = subCats.length > 0 ? subCats[0].id : '' + this.activeSubCategory = '' }, onSearchInput(val) { + // 仅更新关键词状态,搜索在失焦时触发 this.searchKeyword = val - // 防抖搜索 - if (this._searchTimer) clearTimeout(this._searchTimer) - var self = this - this._searchTimer = setTimeout(function () { - self.loadGoods(1) - }, 300) + }, + + onSearchBlur() { + this.loadGoods(1) }, async selectCountry(id) { @@ -339,8 +359,9 @@ export default { await this.loadGoods(1) }, - selectSubCategory(id) { + async selectSubCategory(id) { this.activeSubCategory = id + await this.loadGoods(1) }, toggleFilter() { @@ -421,7 +442,6 @@ export default { 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 @@ -444,18 +464,6 @@ export default { uni.navigateTo({ url: `/pages/product-detail/product-detail?id=${item.id}` }) - }, - - loadMore() { - if (this.loadingMore) { - return - } - const nextPage = this.pagination.current + 1 - if (nextPage > this.pagination.totalPages) { - return - } - this.loadingMore = true - this.loadGoods(nextPage) } } }