v2体验版

This commit is contained in:
2026-08-31 13:35:50 +08:00
parent 3b4a2f9a78
commit 3bd58e05c5
28 changed files with 299 additions and 115 deletions
+3 -2
View File
@@ -222,7 +222,8 @@ export default {
line-height: 1.5;
}
/* 核心能力卡片 - 自适应2列(避免宽度+间距同时固定导致挤压) */
/* 核心能力卡片 - 固定2列(345rpx×2 + 间距20rpx + 容器内边距40rpx = 750rpx)
注意:不要用 flex-basis + calc(50%),部分手机浏览器解析失败会导致单列 */
.ability-grid {
padding: 0 20rpx;
display: flex;
@@ -231,7 +232,7 @@ export default {
margin-bottom: 60rpx;
}
.ability-card {
flex: 1 1 calc(50% - 10rpx);
width: 345rpx;
min-width: 0;
height: 180rpx;
background: #ffffff;
+1 -1
View File
@@ -165,7 +165,7 @@ export default {
display: block;
margin: 12rpx auto 0;
width: fit-content;
max-width: calc(100% - 80rpx);
max-width: 670rpx; /* 750 - 两侧留白 80rpx,避免用 calc(部分手机浏览器解析失败) */
font-size: 20rpx;
color: #000000;
line-height: 1.4;
+3 -2
View File
@@ -127,7 +127,8 @@ export default {
color: #ffffff;
}
/* 产品网格 - 自适应2列(避免宽度+间距同时固定导致挤压) */
/* 产品网格 - 固定2列(345rpx×2 + 间距20rpx + 容器内边距40rpx = 750rpx)
注意:不要用 flex-basis + calc(50%),部分手机浏览器解析失败会导致单列 */
.pod-grid {
padding: 0 20rpx;
display: flex;
@@ -135,7 +136,7 @@ export default {
gap: 20rpx;
}
.pod-card {
flex: 1 1 calc(50% - 10rpx);
width: 345rpx;
min-width: 0;
background: #ffffff;
border-radius: 16rpx;
+3 -2
View File
@@ -46,7 +46,8 @@ export default {
margin-bottom: 64rpx;
}
/* 2x2 网格 - 自适应2列(避免宽度+间距同时固定导致挤压) */
/* 2x2 网格 - 固定2列(345rpx×2 + 间距20rpx + 容器内边距40rpx = 750rpx)
注意:不要用 flex-basis + calc(50%),部分手机浏览器解析失败会导致单列 */
.why-grid {
padding: 0 20rpx;
display: flex;
@@ -54,7 +55,7 @@ export default {
gap: 20rpx;
}
.why-card {
flex: 1 1 calc(50% - 10rpx);
width: 345rpx;
min-width: 0;
height: 160rpx;
background: #ffffff;
@@ -0,0 +1,86 @@
<template>
<!-- 通用文本选项选择物流/印花/工艺胶囊样式选中橙色边框+橙色文字 -->
<view class="option-wrap">
<text class="title">{{ title }}</text>
<view class="option-list">
<view
v-for="(item, index) in list"
:key="index"
class="option-item"
:class="{ active: selected === item }"
@tap="selectOption(item)"
>
<text class="option-text">{{ item }}</text>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'OptionSelector',
props: {
title: {
type: String,
default: ''
},
list: {
type: Array,
default: () => []
},
selected: {
type: [String, Number],
default: ''
}
},
methods: {
selectOption(item) {
this.$emit('select', item)
}
}
}
</script>
<style scoped>
.option-wrap {
padding: 30rpx 20rpx; /* 15px 10px */
background: #ffffff;
}
/* 标题 */
.title {
display: block;
font-size: 29rpx; /* 15px */
color: #000000;
line-height: 33rpx;
margin-bottom: 30rpx; /* 15px */
}
/* 选项列表 */
.option-list {
display: flex;
flex-wrap: wrap;
gap: 16rpx; /* 8px */
}
/* 单个胶囊:与颜色选择样式一致 */
.option-item {
padding: 10rpx 24rpx; /* 5px 12px */
border: 1rpx solid #f2f2f2;
border-radius: 120rpx; /* 60px */
background: #ffffff;
}
.option-item.active {
border-color: #ff6800;
}
.option-text {
font-size: 27rpx; /* 14px */
color: #000000;
line-height: 33rpx;
}
.option-item.active .option-text {
color: #ff6800;
}
</style>
@@ -1,19 +1,19 @@
<template>
<!-- 产品信息名称 / SKU / 国家 / 价格 / 4 列快捷信息 -->
<!-- 产品信息名称 / 国家 / 发货时效 / 价格 -->
<view class="info-wrap">
<!-- 产品名称 18px Bold #1A1A1A -->
<text class="name">{{ info.name }}</text>
<!-- SKU + 国家 同行 12px #979797值黑色 -->
<!-- 国家 + 发货时效 同行 12px #979797值黑色 -->
<view class="meta-row">
<view class="meta-item">
<text class="meta-label">SKU: </text>
<text class="meta-value">{{ info.sku }}</text>
</view>
<view class="meta-item">
<text class="meta-label">国家: </text>
<text class="meta-value">{{ info.country }}</text>
</view>
<view class="meta-item">
<text class="meta-label">发货时效: </text>
<text class="meta-value">{{ info.shipTime }}</text>
</view>
</view>
<!-- 价格 .00 10px整数 24px全部红色 -->
@@ -22,17 +22,6 @@
<text class="price-integer">{{ info.price.integer }}</text>
<text class="price-decimal">{{ info.price.decimal }}</text>
</view>
<!-- 分隔线 0.5px #F0F0F0 -->
<view class="divider"></view>
<!-- 4 列快捷信息label 10px value 13px 居中 -->
<view class="quick-info">
<view class="quick-col" v-for="(item, index) in info.quickInfo" :key="index">
<text class="quick-label">{{ item.label }}</text>
<text class="quick-value">{{ item.value }}</text>
</view>
</view>
</view>
</template>
@@ -63,7 +52,7 @@ export default {
line-height: 50rpx; /* 26px */
}
/* SKU / 国家 同行 */
/* 国家 / 发货时效 同行 */
.meta-row {
display: flex;
margin-top: 10rpx;
@@ -104,34 +93,4 @@ export default {
font-weight: 700;
line-height: 1;
}
/* 分隔线 */
.divider {
height: 1rpx; /* 0.5px */
background: #f0f0f0;
margin-top: 26rpx; /* 13px 间隙 */
}
/* 4 列快捷信息 */
.quick-info {
display: flex;
margin-top: 20rpx; /* 10px */
}
.quick-col {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
}
.quick-label {
font-size: 19rpx; /* 10px */
color: #979797;
line-height: 27rpx;
}
.quick-value {
font-size: 25rpx; /* 13px */
color: #000000;
line-height: 33rpx;
}
</style>
@@ -84,8 +84,10 @@ export default {
/* 单个尺码按钮 44x34,圆角 4px,背景 #F7F7F7 */
.size-item {
width: 85rpx; /* 44px */
min-width: 85rpx; /* 44px */
height: 65rpx; /* 34px */
padding: 0 20rpx;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
+141 -24
View File
@@ -4,7 +4,7 @@
<!-- 产品图片轮播 + 指示点 -->
<DetailSwiper :images="productImages" />
<!-- 产品信息名称/SKU/国家/价格/4列快捷信息 -->
<!-- 产品信息名称/国家/发货时效/价格 -->
<ProductInfo :info="product" />
<!-- 灰色分隔条 -->
@@ -29,6 +29,42 @@
@select="selectSize"
/>
<!-- 灰色分隔条 -->
<view class="section-divider" v-if="logisticsOptions.length"></view>
<!-- 物流选择来自 family 价格矩阵维度 -->
<OptionSelector
v-if="logisticsOptions.length"
title="物流"
:list="logisticsOptions"
:selected="selectedLogistics"
@select="selectLogistics"
/>
<!-- 灰色分隔条 -->
<view class="section-divider" v-if="printCountOptions.length"></view>
<!-- 印花选择来自 family 价格矩阵维度 -->
<OptionSelector
v-if="printCountOptions.length"
title="印花"
:list="printCountOptions"
:selected="selectedPrintCount"
@select="selectPrintCount"
/>
<!-- 灰色分隔条 -->
<view class="section-divider" v-if="craftOptions.length"></view>
<!-- 工艺选择来自 family 价格矩阵维度 -->
<OptionSelector
v-if="craftOptions.length"
title="工艺"
:list="craftOptions"
:selected="selectedCraft"
@select="selectCraft"
/>
<!-- 灰色分隔条 -->
<view class="section-divider" v-if="productParams.length"></view>
@@ -54,6 +90,7 @@ import DetailSwiper from './components/DetailSwiper.vue'
import ProductInfo from './components/ProductInfo.vue'
import ColorSelector from './components/ColorSelector.vue'
import SizeSelector from './components/SizeSelector.vue'
import OptionSelector from './components/OptionSelector.vue'
import ParamsTable from './components/ParamsTable.vue'
import SizeChart from './components/SizeChart.vue'
import PackagingSpec from './components/PackagingSpec.vue'
@@ -66,6 +103,7 @@ export default {
ProductInfo,
ColorSelector,
SizeSelector,
OptionSelector,
ParamsTable,
SizeChart,
PackagingSpec
@@ -77,10 +115,9 @@ export default {
loading: false,
product: {
name: '',
sku: '',
country: '',
price: { symbol: '', integer: '0', decimal: '' },
quickInfo: []
shipTime: '',
price: { symbol: '¥', integer: '0', decimal: '' }
},
productImages: [],
colors: [],
@@ -95,7 +132,12 @@ export default {
mediaByColor: {},
// 默认轮播图(无颜色或 mediaByColor 缺失时使用,来自 detail.media.images
defaultImages: [],
packaging: null
packaging: null,
// 产品族价格矩阵(来自 detail.family,含 尺码×颜色×工艺×物流×印花 五维)
family: null,
selectedLogistics: '',
selectedPrintCount: '',
selectedCraft: ''
}
},
onLoad(options) {
@@ -129,21 +171,12 @@ export default {
* 将接口详情数据映射为各组件所需结构
*/
applyDetail(detail) {
// 1. 产品信息:名称 / SKU / 国家 / 价格 / 4 列快捷信息
// 1. 产品信息:名称 / 国家 / 发货时效 / 价格
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 || '—' }
]
shipTime: detail.productionCycleHours ? detail.productionCycleHours + '小时' : '—',
price: this.parsePrice(detail.price)
}
// 2. 图片画廊:
@@ -195,10 +228,17 @@ export default {
// 8. 包装规格(从 variants 按尺码去重生成)
this.packaging = this.buildPackaging(this.variants)
// 9. 初始价格(按默认选中的 尺码+颜色 匹配变体价格)
// 9. 产品族价格矩阵:解析 family 五维选项(物流/印花/工艺),默认选中第一项
const family = detail.family && detail.family.priceMatrix ? detail.family : null
this.family = family
this.selectedLogistics = family && family.logistics && family.logistics.length ? family.logistics[0] : ''
this.selectedPrintCount = family && family.printCounts && family.printCounts.length ? family.printCounts[0] : ''
this.selectedCraft = family && family.crafts && family.crafts.length ? family.crafts[0] : ''
// 10. 初始价格(family 优先按价格矩阵取,否则按默认选中的 尺码+颜色 匹配变体价格)
this.syncPriceByVariant()
// 10. 初始轮播图(按默认选中颜色取 mediaByColor,无则回退 defaultImages
// 11. 初始轮播图(按默认选中颜色取 mediaByColor,无则回退 defaultImages
this.syncImagesByColor()
},
@@ -372,7 +412,41 @@ export default {
) || null
)
},
/**
* 在 family.priceMatrix.rows 中查找当前选中组合(尺码+颜色+物流+印花+工艺)的价格行
* 未选全或无 family 时返回 null
*/
findMatrixRow() {
const f = this.family
if (!f || !f.priceMatrix || !Array.isArray(f.priceMatrix.rows) || !f.priceMatrix.rows.length) {
return null
}
const sizeName = this.selectedSize
const colorId = this.selectedColorId
return (
f.priceMatrix.rows.find(
(row) =>
row &&
(!sizeName || String(row.sizeName) === String(sizeName)) &&
(!colorId || String(row.colorId) === String(colorId)) &&
(!this.selectedLogistics || String(row.logistics) === String(this.selectedLogistics)) &&
(!this.selectedPrintCount || String(row.printCount) === String(this.selectedPrintCount)) &&
(!this.selectedCraft || String(row.craft) === String(this.selectedCraft))
) || null
)
},
/**
* 根据当前选中组合同步价格:
* - 有 family 价格矩阵:按 尺码+颜色+物流+印花+工艺 匹配矩阵行
* - 否则回退:按 尺码+颜色 匹配 variant
*/
syncPriceByVariant() {
const row = this.findMatrixRow()
if (row && row.price != null) {
this.$set(this.product, 'price', this.parsePrice(row.price))
return
}
const v = this.findVariant()
if (v && v.price != null) {
this.$set(this.product, 'price', this.parsePrice(v.price))
@@ -395,20 +469,62 @@ export default {
selectSize(size) {
this.selectedSize = size
this.syncPriceByVariant()
},
/**
* 物流/印花/工艺选择:来自 family 价格矩阵维度,选中后联动价格
*/
selectLogistics(value) {
this.selectedLogistics = value
this.syncPriceByVariant()
},
selectPrintCount(value) {
this.selectedPrintCount = value
this.syncPriceByVariant()
},
selectCraft(value) {
this.selectedCraft = value
this.syncPriceByVariant()
}
},
computed: {
/**
* 可用性判断数据源:有 family 价格矩阵时用矩阵行,否则用 variants
* 两者都含 sizeName / colorId 字段
*/
availabilitySource() {
const f = this.family
if (f && f.priceMatrix && Array.isArray(f.priceMatrix.rows) && f.priceMatrix.rows.length) {
return f.priceMatrix.rows
}
return this.variants
},
/**
* 物流/印花/工艺选项列表(来自 family)
*/
logisticsOptions() {
return this.family && Array.isArray(this.family.logistics) ? this.family.logistics : []
},
printCountOptions() {
return this.family && Array.isArray(this.family.printCounts) ? this.family.printCounts : []
},
craftOptions() {
return this.family && Array.isArray(this.family.crafts) ? this.family.crafts : []
},
/**
* 颜色列表 + 可用态标记(给 ColorSelector 用,用于 disabled 样式)
* - 如果没选尺码:所有已 enabled 的颜色都可选
* - 如果已选尺码:仅保留存在 (colorId + sizeName) 组合且 variant.enabled 的颜色
* - 如果已选尺码:仅保留存在 (colorId + sizeName) 组合的项
*/
colorListWithAvailability() {
const sizeName = this.selectedSize
const source = this.availabilitySource
return this.colors.map((c) => {
let enabled = c.enabled !== false
if (sizeName && this.variants.length) {
enabled = this.variants.some(
if (sizeName && source.length) {
enabled = source.some(
(v) =>
v &&
v.enabled !== false &&
@@ -425,10 +541,11 @@ export default {
*/
sizeListWithAvailability() {
const colorId = this.selectedColorId
const source = this.availabilitySource
return this.sizes.map((s) => {
let enabled = true
if (colorId && this.variants.length) {
enabled = this.variants.some(
if (colorId && source.length) {
enabled = source.some(
(v) =>
v &&
v.enabled !== false &&
+1 -22
View File
@@ -35,21 +35,13 @@
class="product-image"
:src="item.image"
mode="aspectFill"
lazy-load
v-if="item.image"
></image>
</view>
<!-- 商品信息 -->
<view class="product-info">
<text class="product-name">{{ item.name }}</text>
<!-- 标签 - 商品名下方价格上方 (Figma 1523:344) -->
<view class="product-badges" v-if="item.tags && item.tags.length > 0">
<view
class="product-badge"
:style="{ background: tag.bg, color: tag.color }"
v-for="(tag, tIdx) in item.tags"
:key="tIdx"
>{{ tag.text }}</view>
</view>
<!-- 价格 + "起" -->
<view class="price-row">
<text class="product-price">{{ item.price }}</text>
@@ -188,19 +180,6 @@ export default {
width: 100%;
height: 100%;
}
/* 标签 - 位于商品名下方、价格上方,横向排列 */
.product-badges {
display: flex;
flex-wrap: wrap;
gap: 8rpx;
margin-top: 8rpx;
}
.product-badge {
padding: 4rpx 12rpx;
border-radius: 8rpx;
font-size: 18rpx;
}
/* 商品信息 - 内边距 10px */
.product-info {
padding: 20rpx;
+5 -9
View File
@@ -145,9 +145,12 @@ export default {
} catch (e) {
this.windowHeight = 667
}
// 首屏商品请求与基础数据并行发出(goods 无筛选时不依赖国家/分类数据),
// 避免串行等待拖慢首屏(goods 是最慢的接口,应最先发出)
const goodsPromise = this.loadGoods(1)
await this.initData()
this.loadSubCategories('all')
await this.loadGoods(1)
await goodsPromise
// 首屏数据渲染完成后,子组件会通过 mounted 自行测量并 emit 高度
// 这里兜底触发一次重算(防止某些端 mounted 比父组件 onLoad 晚)
this.recomputeScrollHeight()
@@ -252,14 +255,7 @@ export default {
id: item.goodId,
name: truncateProductTitle(item.goodName),
price: item.price,
image: item.image,
tags: (item.tags || []).map(function (t) {
return {
text: t.tagName,
bg: t.tagColor,
color: t.tagFontColor
}
})
image: item.image
}))
const total = data.total || 0