'首页和商品详情页初稿'

This commit is contained in:
2026-08-21 19:02:35 +08:00
parent 75135effde
commit d53bde57b6
29 changed files with 3694 additions and 3085 deletions
@@ -0,0 +1,95 @@
<template>
<!-- 颜色选择第一行 4 第二行 2 胶囊样式选中橙色边框+橙色文字 -->
<view class="color-wrap">
<text class="title">颜色</text>
<view class="color-list">
<view
v-for="(item, index) in list"
:key="index"
class="color-pill"
:class="{ active: selectedId === item.id }"
@tap="selectColor(item.id)"
>
<view class="color-dot" :style="{ background: item.value }"></view>
<text class="color-name">{{ item.name }}</text>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'ColorSelector',
props: {
list: {
type: Array,
default: () => []
},
selectedId: {
type: [String, Number],
default: ''
}
},
methods: {
selectColor(id) {
this.$emit('select', id)
}
}
}
</script>
<style scoped>
.color-wrap {
padding: 30rpx 20rpx; /* 15px 10px */
background: #ffffff;
}
/* 标题 */
.title {
display: block;
font-size: 29rpx; /* 15px */
color: #000000;
line-height: 33rpx;
margin-bottom: 30rpx; /* 15px */
}
/* 胶囊列表 */
.color-list {
display: flex;
flex-wrap: wrap;
gap: 16rpx; /* 8px 横向 / 纵向 */
}
/* 单个胶囊:padding 5px 12px,圆角 60px */
.color-pill {
display: flex;
align-items: center;
gap: 8rpx; /* 4px */
padding: 10rpx 24rpx; /* 5px 12px */
border: 1rpx solid #f2f2f2;
border-radius: 120rpx; /* 60px */
background: #ffffff;
}
.color-pill.active {
border-color: #ff6800;
}
/* 颜色圆点 15x151px 灰色边框 */
.color-dot {
width: 30rpx; /* 15px */
height: 30rpx;
border-radius: 50%;
border: 1rpx solid #cdcdcd;
}
/* 颜色名称 14px 黑,选中橙色 */
.color-name {
font-size: 27rpx; /* 14px */
color: #000000;
line-height: 33rpx;
}
.color-pill.active .color-name {
color: #ff6800;
}
</style>
@@ -0,0 +1,91 @@
<template>
<!-- 产品图片轮播 + 自定义指示点 -->
<view class="detail-swiper">
<swiper
class="swiper"
:circular="true"
:indicator-dots="false"
@change="onChange"
>
<swiper-item v-for="(item, index) in images" :key="index">
<view class="swiper-item">
<image class="swiper-image" :src="item" mode="aspectFill"></image>
</view>
</swiper-item>
</swiper>
<!-- 自定义指示点active 16x8 橙色胶囊inactive 8x8 黑色 20% -->
<view class="dots">
<view
v-for="(item, index) in images"
:key="index"
class="dot"
:class="{ active: current === index }"
></view>
</view>
</view>
</template>
<script>
export default {
name: 'DetailSwiper',
props: {
images: {
type: Array,
default: () => []
}
},
data() {
return {
current: 0
}
},
methods: {
onChange(e) {
this.current = e.detail.current
}
}
}
</script>
<style scoped>
.detail-swiper {
position: relative;
width: 100%;
background: #f2f2f2;
}
.swiper {
width: 100%;
height: 500rpx; /* 260px */
}
.swiper-item {
width: 100%;
height: 100%;
}
.swiper-image {
width: 100%;
height: 100%;
}
/* 指示点:覆盖在图片底部 */
.dots {
position: absolute;
bottom: 20rpx; /* 距图片底部 10px */
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 12rpx; /* 6px */
}
.dot {
width: 16rpx; /* 8px */
height: 16rpx;
background: rgba(0, 0, 0, 0.2);
border-radius: 8rpx; /* 4px */
}
.dot.active {
width: 32rpx; /* 16px */
height: 16rpx; /* 8px */
background: #ff6800;
}
</style>
@@ -0,0 +1,146 @@
<template>
<!-- 包装规格表4 × 4 每个数据单元格双单位cm + in -->
<view class="pack-wrap">
<view class="header-row">
<text class="title">包装规格</text>
<text class="unit">单位{{ data.unit }}</text>
</view>
<view class="table">
<!-- 表头bg #DFDFDF圆角 6px -->
<view class="row header">
<text
class="cell"
v-for="(col, index) in data.columns"
:key="index"
>{{ col }}</text>
</view>
<!-- 数据行偶数行 bg #F7F7F7行高 60px双行内容 -->
<view
class="row data-row"
:class="{ alt: (index + 1) % 2 === 0 }"
v-for="(row, index) in data.rows"
:key="index"
>
<!-- 尺码列单行居中 -->
<view class="cell size-cell">
<text class="size-text">{{ row.size }}</text>
</view>
<!-- 尺寸列cm + in 双行 -->
<view class="cell dual-cell">
<text class="dual-top">{{ row.dimension.cm }}</text>
<text class="dual-bottom">{{ row.dimension.in }}</text>
</view>
<!-- 体积列cm + in 双行 -->
<view class="cell dual-cell">
<text class="dual-top">{{ row.volume.cm }}</text>
<text class="dual-bottom">{{ row.volume.in }}</text>
</view>
<!-- 重量列cm + in 双行 -->
<view class="cell dual-cell">
<text class="dual-top">{{ row.weight.cm }}</text>
<text class="dual-bottom">{{ row.weight.in }}</text>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'PackagingSpec',
props: {
data: {
type: Object,
default: () => ({})
}
}
}
</script>
<style scoped>
.pack-wrap {
padding: 30rpx 20rpx; /* 15px 10px */
background: #ffffff;
}
/* 标题行 */
.header-row {
display: flex;
align-items: center;
margin-bottom: 20rpx; /* 10px */
}
.title {
font-size: 29rpx; /* 15px */
color: #000000;
line-height: 33rpx;
}
.unit {
margin-left: 16rpx;
font-size: 21rpx; /* 11px */
color: rgba(0, 0, 0, 0.5);
line-height: 33rpx;
}
/* 表格 */
.table {
display: flex;
flex-direction: column;
gap: 8rpx; /* 4px 行间距 */
}
/* 单行 */
.row {
display: flex;
border-radius: 12rpx; /* 6px */
align-items: center;
}
.row.header {
height: 77rpx; /* 40px */
background: #dfdfdf;
}
.row.data-row {
height: 115rpx; /* 60px 双行内容更高 */
}
.row.alt {
background: #f7f7f7;
}
/* 单元格 */
.cell {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
/* 表头文字 */
.row.header .cell {
font-size: 25rpx; /* 13px */
color: #000000;
line-height: 33rpx;
}
/* 尺码列:单行 13px */
.size-text {
font-size: 25rpx; /* 13px */
color: #000000;
line-height: 33rpx;
}
/* 双单位单元格:cm 在上,in 在下 */
.dual-cell {
gap: 4rpx; /* 2px */
}
.dual-top,
.dual-bottom {
font-size: 19rpx; /* 10px */
color: rgba(0, 0, 0, 0.6);
line-height: 33rpx;
}
</style>
@@ -0,0 +1,179 @@
<template>
<!-- 产品参数普通参数行 + 可展开的"补充说明" -->
<view class="params-wrap">
<text class="title">产品参数</text>
<!-- 普通参数行 -->
<view
class="param-row"
v-for="(item, index) in normalParams"
:key="index"
>
<text class="param-label">{{ item.label }}</text>
<text class="param-value">{{ item.value }}</text>
</view>
<!-- 可展开的"补充说明" -->
<view class="param-row expandable-row" v-if="expandableParam">
<text class="param-label">{{ expandableParam.label }}</text>
<view class="expandable-content">
<text class="param-value" :class="{ 'value-collapsed': !expanded }">{{ expandableParam.value }}</text>
<!-- 渐变遮罩 + 展开按钮仅收起态显示 -->
<view class="expand-overlay" v-if="!expanded">
<view class="gradient-mask"></view>
<view class="expand-btn" @tap="toggleExpand">
<text class="expand-text">展开</text>
<view class="arrow-down"></view>
</view>
</view>
<!-- 展开后的"收起"按钮 -->
<view class="collapse-btn" v-if="expanded" @tap="toggleExpand">
<text class="expand-text">收起</text>
<view class="arrow-up"></view>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'ParamsTable',
props: {
list: {
type: Array,
default: () => []
}
},
data() {
return {
expanded: false
}
},
computed: {
normalParams() {
return this.list.filter((item) => !item.expandable)
},
expandableParam() {
return this.list.find((item) => item.expandable)
}
},
methods: {
toggleExpand() {
this.expanded = !this.expanded
}
}
}
</script>
<style scoped>
.params-wrap {
padding: 30rpx 20rpx; /* 15px 10px */
background: #ffffff;
}
/* 标题 */
.title {
display: block;
font-size: 29rpx; /* 15px */
color: #000000;
line-height: 33rpx;
margin-bottom: 20rpx; /* 10px */
}
/* 普通参数行 */
.param-row {
display: flex;
align-items: flex-start;
padding: 10rpx 0; /* 5px 上下 */
}
.param-label {
width: 170rpx; /* 给 label 固定宽度对齐 */
flex-shrink: 0;
font-size: 25rpx; /* 13px */
color: rgba(0, 0, 0, 0.5); /* 黑色 50% 透明度 */
line-height: 33rpx;
}
.param-value {
flex: 1;
font-size: 25rpx; /* 13px */
color: #000000;
line-height: 33rpx;
}
/* 可展开的"补充说明"行 */
.expandable-row {
align-items: flex-start;
}
.expandable-content {
position: relative;
flex: 1;
}
/* 收起态:限制高度(约 5 行,留出渐变遮罩空间) */
.value-collapsed {
display: block;
max-height: 165rpx; /* 约 5 行 33rpx */
overflow: hidden;
}
/* 渐变遮罩:绝对定位在容器底部 60px,从透明到白 */
.expand-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 120rpx; /* 60px */
pointer-events: none;
}
.gradient-mask {
position: absolute;
inset: 0;
background: linear-gradient(
180deg,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 1) 52%
);
}
/* 展开/收起按钮:居中显示,可点击 */
.expand-btn,
.collapse-btn {
position: absolute;
bottom: 10rpx;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 8rpx; /* 4px */
pointer-events: auto;
}
/* 展开态的"收起"按钮:在文本下方居中 */
.collapse-btn {
position: relative;
margin-top: 16rpx;
left: auto;
transform: none;
justify-content: center;
}
.expand-text {
font-size: 23rpx; /* 12px */
color: #000000;
line-height: 33rpx;
}
/* CSS 箭头:12x12,下/上 */
.arrow-down,
.arrow-up {
width: 12rpx;
height: 12rpx;
border-right: 2rpx solid #000;
border-bottom: 2rpx solid #000;
transform: rotate(45deg);
}
.arrow-up {
transform: rotate(-135deg);
}
</style>
@@ -0,0 +1,137 @@
<template>
<!-- 产品信息名称 / SKU / 国家 / 价格 / 4 列快捷信息 -->
<view class="info-wrap">
<!-- 产品名称 18px Bold #1A1A1A -->
<text class="name">{{ info.name }}</text>
<!-- SKU + 国家 同行 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>
<!-- 价格 .00 10px整数 24px全部红色 -->
<view class="price-row">
<text class="price-symbol">{{ info.price.symbol }}</text>
<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>
<script>
export default {
name: 'ProductInfo',
props: {
info: {
type: Object,
default: () => ({})
}
}
}
</script>
<style scoped>
.info-wrap {
padding: 20rpx 20rpx 24rpx; /* 10px 10px 12px */
background: #ffffff;
}
/* 产品名称 */
.name {
display: block;
font-size: 35rpx; /* 18px */
font-weight: 700;
color: #1a1a1a;
line-height: 50rpx; /* 26px */
}
/* SKU / 国家 同行 */
.meta-row {
display: flex;
margin-top: 10rpx;
}
.meta-item {
display: flex;
align-items: center;
}
.meta-item + .meta-item {
margin-left: 20rpx;
}
.meta-label {
font-size: 23rpx; /* 12px */
color: #979797;
line-height: 33rpx;
}
.meta-value {
font-size: 23rpx;
color: #000000;
line-height: 33rpx;
}
/* 价格 */
.price-row {
display: flex;
align-items: baseline;
margin-top: 16rpx;
}
.price-symbol,
.price-decimal {
font-size: 19rpx; /* 10px */
color: #ff0000;
font-weight: 700;
}
.price-integer {
font-size: 46rpx; /* 24px */
color: #ff0000;
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>
@@ -0,0 +1,114 @@
<template>
<!-- 产品尺码表5 × 4 交替行背景 -->
<view class="chart-wrap">
<view class="header-row">
<text class="title">产品尺码</text>
<text class="unit">单位{{ data.unit }}</text>
</view>
<view class="table">
<!-- 表头bg #DFDFDF圆角 6px -->
<view class="row header">
<text
class="cell"
v-for="(col, index) in data.columns"
:key="index"
>{{ col }}</text>
</view>
<!-- 数据行偶数行 24 bg #F7F7F7圆角 6px -->
<view
class="row"
:class="{ alt: (index + 1) % 2 === 0 }"
v-for="(row, index) in data.rows"
:key="index"
>
<text class="cell">{{ row.size }}</text>
<text class="cell">{{ row.shoulder }}</text>
<text class="cell">{{ row.chest }}</text>
<text class="cell">{{ row.length }}</text>
<text class="cell">{{ row.sleeve }}</text>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'SizeChart',
props: {
data: {
type: Object,
default: () => ({})
}
}
}
</script>
<style scoped>
.chart-wrap {
padding: 30rpx 20rpx; /* 15px 10px */
background: #ffffff;
}
/* 标题行:产品尺码 + 单位 */
.header-row {
display: flex;
align-items: center;
margin-bottom: 20rpx; /* 10px */
}
.title {
font-size: 29rpx; /* 15px */
color: #000000;
line-height: 33rpx;
}
.unit {
margin-left: 16rpx; /* 8px */
font-size: 21rpx; /* 11px */
color: rgba(0, 0, 0, 0.5);
line-height: 33rpx;
}
/* 表格 */
.table {
display: flex;
flex-direction: column;
gap: 8rpx; /* 行间距 4px */
}
/* 单行 */
.row {
display: flex;
height: 77rpx; /* 40px */
align-items: center;
border-radius: 12rpx; /* 6px */
}
/* 表头行 */
.row.header {
background: #dfdfdf;
}
/* 偶数行(交替背景) */
.row.alt {
background: #f7f7f7;
}
/* 单元格:等宽分布 */
.cell {
flex: 1;
text-align: center;
font-size: 25rpx; /* 13px */
color: #000000;
line-height: 33rpx;
}
.row.header .cell {
color: #000000;
}
.row:not(.header) .cell {
opacity: 0.6;
}
.row:not(.header) .cell:first-child {
opacity: 1; /* 尺码列不透明 */
}
</style>
@@ -0,0 +1,87 @@
<template>
<!-- 尺码选择5 个按钮横排选中态橙色边框+橙色文字背景仍为浅灰 -->
<view class="size-wrap">
<text class="title">尺码</text>
<view class="size-list">
<view
v-for="(item, index) in list"
:key="index"
class="size-item"
:class="{ active: selected === item }"
@tap="selectSize(item)"
>
<text class="size-text">{{ item }}</text>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'SizeSelector',
props: {
list: {
type: Array,
default: () => []
},
selected: {
type: String,
default: ''
}
},
methods: {
selectSize(size) {
this.$emit('select', size)
}
}
}
</script>
<style scoped>
.size-wrap {
padding: 30rpx 20rpx; /* 15px 10px */
background: #ffffff;
}
/* 标题 */
.title {
display: block;
font-size: 29rpx; /* 15px */
color: #000000;
line-height: 33rpx;
margin-bottom: 30rpx; /* 15px */
}
/* 尺码列表横排 */
.size-list {
display: flex;
gap: 12rpx; /* 6px */
}
/* 单个尺码按钮 44x34,圆角 4px,背景 #F7F7F7 */
.size-item {
width: 85rpx; /* 44px */
height: 65rpx; /* 34px */
display: flex;
align-items: center;
justify-content: center;
background: #f7f7f7;
border: 1rpx solid transparent;
border-radius: 8rpx; /* 4px */
}
/* 选中:橙色边框 + 橙色文字(背景保持 #F7F7F7) */
.size-item.active {
border-color: #ff6800;
}
.size-text {
font-size: 25rpx; /* 13px */
color: #000000;
line-height: 33rpx;
}
.size-item.active .size-text {
color: #ff6800;
}
</style>