'商品列表和详情页接上接口'
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- 产品详情页 - 严格按 Figma 1544:530 实现 -->
|
<!-- 产品详情页 - 接入 /public/goods/{goodId} 接口 -->
|
||||||
<view class="detail-page">
|
<view class="detail-page">
|
||||||
<!-- 产品图片轮播 + 指示点 -->
|
<!-- 产品图片轮播 + 指示点 -->
|
||||||
<DetailSwiper :images="productImages" />
|
<DetailSwiper :images="productImages" />
|
||||||
@@ -8,42 +8,44 @@
|
|||||||
<ProductInfo :info="product" />
|
<ProductInfo :info="product" />
|
||||||
|
|
||||||
<!-- 灰色分隔条 -->
|
<!-- 灰色分隔条 -->
|
||||||
<view class="section-divider"></view>
|
<view class="section-divider" v-if="colors.length"></view>
|
||||||
|
|
||||||
<!-- 颜色选择 -->
|
<!-- 颜色选择 -->
|
||||||
<ColorSelector
|
<ColorSelector
|
||||||
|
v-if="colors.length"
|
||||||
:list="colors"
|
:list="colors"
|
||||||
:selected-id="selectedColorId"
|
:selected-id="selectedColorId"
|
||||||
@select="selectColor"
|
@select="selectColor"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 灰色分隔条 -->
|
<!-- 灰色分隔条 -->
|
||||||
<view class="section-divider"></view>
|
<view class="section-divider" v-if="sizes.length"></view>
|
||||||
|
|
||||||
<!-- 尺码选择 -->
|
<!-- 尺码选择 -->
|
||||||
<SizeSelector
|
<SizeSelector
|
||||||
|
v-if="sizes.length"
|
||||||
:list="sizes"
|
:list="sizes"
|
||||||
:selected="selectedSize"
|
:selected="selectedSize"
|
||||||
@select="selectSize"
|
@select="selectSize"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 灰色分隔条 -->
|
<!-- 灰色分隔条 -->
|
||||||
<view class="section-divider"></view>
|
<view class="section-divider" v-if="productParams.length"></view>
|
||||||
|
|
||||||
<!-- 产品参数(含可展开的补充说明) -->
|
<!-- 产品参数(含可展开的补充说明) -->
|
||||||
<ParamsTable :list="productParams" />
|
<ParamsTable v-if="productParams.length" :list="productParams" />
|
||||||
|
|
||||||
<!-- 灰色分隔条 -->
|
<!-- 灰色分隔条 -->
|
||||||
<view class="section-divider"></view>
|
<view class="section-divider" v-if="sizeChart.rows && sizeChart.rows.length"></view>
|
||||||
|
|
||||||
<!-- 产品尺码表 -->
|
<!-- 产品尺码表 -->
|
||||||
<SizeChart :data="sizeChart" />
|
<SizeChart v-if="sizeChart.rows && sizeChart.rows.length" :data="sizeChart" />
|
||||||
|
|
||||||
<!-- 灰色分隔条 -->
|
<!-- 灰色分隔条 -->
|
||||||
<view class="section-divider"></view>
|
<view class="section-divider" v-if="packaging"></view>
|
||||||
|
|
||||||
<!-- 包装规格表 -->
|
<!-- 包装规格表(接口暂未提供,预留) -->
|
||||||
<PackagingSpec :data="packaging" />
|
<PackagingSpec v-if="packaging" :data="packaging" />
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -56,15 +58,7 @@ import ParamsTable from './components/ParamsTable.vue'
|
|||||||
import SizeChart from './components/SizeChart.vue'
|
import SizeChart from './components/SizeChart.vue'
|
||||||
import PackagingSpec from './components/PackagingSpec.vue'
|
import PackagingSpec from './components/PackagingSpec.vue'
|
||||||
|
|
||||||
import {
|
import { getGoodsDetail } from '@/api/public.js'
|
||||||
mockProduct,
|
|
||||||
mockImages,
|
|
||||||
mockColors,
|
|
||||||
mockSizes,
|
|
||||||
mockParams,
|
|
||||||
mockSizeChart,
|
|
||||||
mockPackaging
|
|
||||||
} from './mock.js'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -79,25 +73,182 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
productId: null,
|
productId: null,
|
||||||
product: mockProduct,
|
loading: false,
|
||||||
productImages: mockImages,
|
product: {
|
||||||
colors: mockColors,
|
name: '',
|
||||||
// 默认选中"灰色"(id=3),与 Figma 设计稿一致
|
sku: '',
|
||||||
selectedColorId: 3,
|
country: '',
|
||||||
sizes: mockSizes,
|
price: { symbol: '¥', integer: '0', decimal: '' },
|
||||||
// 默认选中 L,与 Figma 设计稿一致
|
quickInfo: []
|
||||||
selectedSize: 'L',
|
},
|
||||||
productParams: mockParams,
|
productImages: [],
|
||||||
sizeChart: mockSizeChart,
|
colors: [],
|
||||||
packaging: mockPackaging
|
selectedColorId: '',
|
||||||
|
sizes: [],
|
||||||
|
selectedSize: '',
|
||||||
|
productParams: [],
|
||||||
|
sizeChart: {},
|
||||||
|
packaging: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLoad(options) {
|
onLoad(options) {
|
||||||
if (options && options.id) {
|
if (options && options.id) {
|
||||||
this.productId = options.id
|
this.productId = options.id
|
||||||
|
this.loadDetail(options.id)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
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) {
|
selectColor(id) {
|
||||||
this.selectedColorId = id
|
this.selectedColorId = id
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 商品网格滚动区 -->
|
<!-- 商品网格滚动区 -->
|
||||||
<scroll-view scroll-y class="product-scroll" @scrolltolower="onLoadMore">
|
<scroll-view scroll-y class="product-scroll">
|
||||||
<view class="product-grid">
|
<view class="product-grid">
|
||||||
<view
|
<view
|
||||||
class="product-card"
|
class="product-card"
|
||||||
@@ -89,8 +89,7 @@ export default {
|
|||||||
onTapProduct(item) { this.$emit('product-click', item) },
|
onTapProduct(item) { this.$emit('product-click', item) },
|
||||||
onPrev() { if (this.current > 1) this.$emit('page-change', this.current - 1) },
|
onPrev() { if (this.current > 1) this.$emit('page-change', this.current - 1) },
|
||||||
onNext() { if (this.current < this.totalPages) 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) },
|
onGoPage(page) { this.$emit('page-change', page) }
|
||||||
onLoadMore() { this.$emit('load-more') }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
placeholder-class="search-placeholder"
|
placeholder-class="search-placeholder"
|
||||||
:value="keyword"
|
:value="keyword"
|
||||||
@input="onInput"
|
@input="onInput"
|
||||||
|
@blur="onBlur"
|
||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
<view class="filter-btn" @tap="onFilter">
|
<view class="filter-btn" @tap="onFilter">
|
||||||
@@ -31,6 +32,9 @@ export default {
|
|||||||
onInput(e) {
|
onInput(e) {
|
||||||
this.$emit('input', e.detail.value)
|
this.$emit('input', e.detail.value)
|
||||||
},
|
},
|
||||||
|
onBlur() {
|
||||||
|
this.$emit('blur')
|
||||||
|
},
|
||||||
onFilter() {
|
onFilter() {
|
||||||
this.$emit('filter')
|
this.$emit('filter')
|
||||||
}
|
}
|
||||||
|
|||||||
+47
-39
@@ -5,6 +5,7 @@
|
|||||||
<SearchBar
|
<SearchBar
|
||||||
:keyword="searchKeyword"
|
:keyword="searchKeyword"
|
||||||
@input="onSearchInput"
|
@input="onSearchInput"
|
||||||
|
@blur="onSearchBlur"
|
||||||
@filter="toggleFilter"
|
@filter="toggleFilter"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -49,7 +50,6 @@
|
|||||||
@sub-select="selectSubCategory"
|
@sub-select="selectSubCategory"
|
||||||
@product-click="goDetail"
|
@product-click="goDetail"
|
||||||
@page-change="loadGoods"
|
@page-change="loadGoods"
|
||||||
@load-more="loadMore"
|
|
||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -199,29 +199,16 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 加载商品列表(分页 + 筛选)
|
* 统一加载商品列表(分页 + 全部筛选条件)
|
||||||
|
* 所有触发点(搜索失焦 / 分类点击 / 国家切换 / 标签点击 / 确认筛选 / 分页)都走这里
|
||||||
*/
|
*/
|
||||||
async loadGoods(page) {
|
async loadGoods(page) {
|
||||||
try {
|
try {
|
||||||
const currentPage = page || this.pagination.current || 1
|
const currentPage = page || this.pagination.current || 1
|
||||||
const pageSize = this.pagination.pageSize || 10
|
const pageSize = this.pagination.pageSize || 10
|
||||||
|
|
||||||
var params = {
|
// 统一参数对象:由 buildQueryParams 统一拼装
|
||||||
page: currentPage,
|
const params = this.buildQueryParams(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()
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[loadGoods] params:', JSON.stringify(params))
|
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) {
|
codeToEmoji(iconPath) {
|
||||||
if (!iconPath) return '🌐'
|
if (!iconPath) return '🌐'
|
||||||
var match = iconPath.match(/\/([^/]+)\.\w+$/)
|
var match = iconPath.match(/\/([^/]+)\.\w+$/)
|
||||||
@@ -295,17 +316,16 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.subCategories = subCats
|
this.subCategories = subCats
|
||||||
this.activeSubCategory = subCats.length > 0 ? subCats[0].id : ''
|
this.activeSubCategory = ''
|
||||||
},
|
},
|
||||||
|
|
||||||
onSearchInput(val) {
|
onSearchInput(val) {
|
||||||
|
// 仅更新关键词状态,搜索在失焦时触发
|
||||||
this.searchKeyword = val
|
this.searchKeyword = val
|
||||||
// 防抖搜索
|
},
|
||||||
if (this._searchTimer) clearTimeout(this._searchTimer)
|
|
||||||
var self = this
|
onSearchBlur() {
|
||||||
this._searchTimer = setTimeout(function () {
|
this.loadGoods(1)
|
||||||
self.loadGoods(1)
|
|
||||||
}, 300)
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async selectCountry(id) {
|
async selectCountry(id) {
|
||||||
@@ -339,8 +359,9 @@ export default {
|
|||||||
await this.loadGoods(1)
|
await this.loadGoods(1)
|
||||||
},
|
},
|
||||||
|
|
||||||
selectSubCategory(id) {
|
async selectSubCategory(id) {
|
||||||
this.activeSubCategory = id
|
this.activeSubCategory = id
|
||||||
|
await this.loadGoods(1)
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleFilter() {
|
toggleFilter() {
|
||||||
@@ -421,7 +442,6 @@ export default {
|
|||||||
var filters = []
|
var filters = []
|
||||||
var countryFilter = this.activeFilters.find(function (f) { return f.key === 'country' })
|
var countryFilter = this.activeFilters.find(function (f) { return f.key === 'country' })
|
||||||
if (countryFilter) filters.push(countryFilter)
|
if (countryFilter) filters.push(countryFilter)
|
||||||
|
|
||||||
this.filterGroups.forEach(function (group) {
|
this.filterGroups.forEach(function (group) {
|
||||||
if (group.selected.length > 0) {
|
if (group.selected.length > 0) {
|
||||||
var labels = group.tags
|
var labels = group.tags
|
||||||
@@ -444,18 +464,6 @@ export default {
|
|||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: `/pages/product-detail/product-detail?id=${item.id}`
|
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user