4 changed files with 560 additions and 0 deletions
@ -0,0 +1,429 @@ |
|||
<template> |
|||
<scroll-view scroll-y class="viewport"> |
|||
<!-- 基本信息 --> |
|||
<view class="goods"> |
|||
<!-- 商品主图 --> |
|||
<view class="preview"> |
|||
<swiper circular @change="onChange"> |
|||
<swiper-item v-for="item in goods?.mainPictures" :key="item"> |
|||
<image @tap="onTapImage(item)" mode="aspectFill" :src="item" /> |
|||
</swiper-item> |
|||
</swiper> |
|||
<view class="indicator"> |
|||
<text class="current">{{ currentIndex + 1 }}</text> |
|||
<text class="split">/</text> |
|||
<text class="total">{{ goods?.mainPictures.length }}</text> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 商品简介 --> |
|||
<view class="meta"> |
|||
<view class="price"> |
|||
<text class="symbol">¥</text> |
|||
<text class="number">{{ goods?.price }}</text> |
|||
</view> |
|||
<view class="name ellipsis">{{ goods?.name }}</view> |
|||
<view class="desc">{{ goods?.desc }}</view> |
|||
</view> |
|||
|
|||
<!-- 操作面板 --> |
|||
<view class="action"> |
|||
<view class="item arrow"> |
|||
<text class="label">选择</text> |
|||
<text class="text ellipsis"> 请选择商品规格 </text> |
|||
</view> |
|||
<view class="item arrow"> |
|||
<text class="label">送至</text> |
|||
<text class="text ellipsis"> 请选择收获地址 </text> |
|||
</view> |
|||
<view class="item arrow"> |
|||
<text class="label">服务</text> |
|||
<text class="text ellipsis"> 无忧退 快速退款 免费包邮 </text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 商品详情 --> |
|||
<view class="detail panel"> |
|||
<view class="title"> |
|||
<text>详情</text> |
|||
</view> |
|||
<view class="content"> |
|||
<view class="properties"> |
|||
<!-- 属性详情 --> |
|||
<view class="item" v-for="item in goods?.details.properties" :key="item.name"> |
|||
<text class="label">{{ item.name }}</text> |
|||
<text class="value">{{ item.value }}</text> |
|||
</view> |
|||
</view> |
|||
<!-- 图片详情 --> |
|||
<image |
|||
class="image" |
|||
v-for="item in goods?.details.pictures" |
|||
:key="item" |
|||
mode="widthFix" |
|||
:src="item" |
|||
></image> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 同类推荐 --> |
|||
<view class="similar panel"> |
|||
<view class="title"> |
|||
<text>同类推荐</text> |
|||
</view> |
|||
<view class="content"> |
|||
<navigator |
|||
v-for="item in goods?.similarProducts" |
|||
:key="item.id" |
|||
class="goods" |
|||
hover-class="none" |
|||
:url="`/pages/goods/goods?id=${item.id}`" |
|||
> |
|||
<image class="image" mode="aspectFill" :src="item.picture"></image> |
|||
<view class="name ellipsis">{{ item.name }}</view> |
|||
<view class="price"> |
|||
<text class="symbol">¥</text> |
|||
<text class="number">{{ item.price }}</text> |
|||
</view> |
|||
</navigator> |
|||
</view> |
|||
</view> |
|||
</scroll-view> |
|||
|
|||
<!-- 用户操作 --> |
|||
<view class="toolbar" :style="{ paddingBottom: safeAreaInsets?.bottom + 'px' }"> |
|||
<view class="icons"> |
|||
<button class="icons-button"><text class="icon-heart"></text>收藏</button> |
|||
<button class="icons-button" open-type="contact"> |
|||
<text class="icon-handset"></text>客服 |
|||
</button> |
|||
<navigator class="icons-button" url="/pages/cart/cart" open-type="switchTab"> |
|||
<text class="icon-cart"></text>购物车 |
|||
</navigator> |
|||
</view> |
|||
<view class="buttons"> |
|||
<view class="addcart"> 加入购物车 </view> |
|||
<view class="buynow"> 立即购买 </view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
import { onLoad } from '@dcloudio/uni-app' |
|||
import { computed, ref } from 'vue' |
|||
import type { GoodsResult } from '@/types/goods' |
|||
import { getGoodsByIdAPI } from '@/services/goods' |
|||
|
|||
// 获取屏幕边界到安全区域距离 |
|||
const { safeAreaInsets } = uni.getSystemInfoSync() |
|||
|
|||
// 接收页面参数 |
|||
const query = defineProps<{ |
|||
id: string |
|||
}>() |
|||
|
|||
// 获取商品详情信息 |
|||
const goods = ref<GoodsResult>() |
|||
const getGoodsByIdData = async () => { |
|||
const res = await getGoodsByIdAPI(query.id) |
|||
goods.value = res.result |
|||
} |
|||
|
|||
// 轮播图变化时 |
|||
const currentIndex = ref(0) |
|||
const onChange: UniHelper.SwiperOnChange = (ev) => { |
|||
currentIndex.value = ev.detail.current |
|||
} |
|||
|
|||
// 点击图片时 |
|||
const onTapImage = (url: string) => { |
|||
// 大图预览 |
|||
uni.previewImage({ |
|||
current: url, |
|||
urls: goods.value!.mainPictures, |
|||
}) |
|||
} |
|||
|
|||
// 页面加载 |
|||
onLoad(() => { |
|||
getGoodsByIdData() |
|||
}) |
|||
</script> |
|||
|
|||
<style lang="scss"> |
|||
page { |
|||
height: 100%; |
|||
overflow: hidden; |
|||
display: flex; |
|||
flex-direction: column; |
|||
} |
|||
|
|||
.viewport { |
|||
background-color: #f4f4f4; |
|||
} |
|||
|
|||
.panel { |
|||
margin-top: 20rpx; |
|||
background-color: #fff; |
|||
.title { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
height: 90rpx; |
|||
line-height: 1; |
|||
padding: 30rpx 60rpx 30rpx 6rpx; |
|||
position: relative; |
|||
text { |
|||
padding-left: 10rpx; |
|||
font-size: 28rpx; |
|||
color: #333; |
|||
font-weight: 600; |
|||
border-left: 4rpx solid #27ba9b; |
|||
} |
|||
navigator { |
|||
font-size: 24rpx; |
|||
color: #666; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.arrow { |
|||
&::after { |
|||
position: absolute; |
|||
top: 50%; |
|||
right: 30rpx; |
|||
content: '\e6c2'; |
|||
color: #ccc; |
|||
font-family: 'erabbit' !important; |
|||
font-size: 32rpx; |
|||
transform: translateY(-50%); |
|||
} |
|||
} |
|||
|
|||
/* 商品信息 */ |
|||
.goods { |
|||
background-color: #fff; |
|||
.preview { |
|||
height: 750rpx; |
|||
position: relative; |
|||
.image { |
|||
width: 750rpx; |
|||
height: 750rpx; |
|||
} |
|||
.indicator { |
|||
height: 40rpx; |
|||
padding: 0 24rpx; |
|||
line-height: 40rpx; |
|||
border-radius: 30rpx; |
|||
color: #fff; |
|||
font-family: Arial, Helvetica, sans-serif; |
|||
background-color: rgba(0, 0, 0, 0.3); |
|||
position: absolute; |
|||
bottom: 30rpx; |
|||
right: 30rpx; |
|||
.current { |
|||
font-size: 26rpx; |
|||
} |
|||
.split { |
|||
font-size: 24rpx; |
|||
margin: 0 1rpx 0 2rpx; |
|||
} |
|||
.total { |
|||
font-size: 24rpx; |
|||
} |
|||
} |
|||
} |
|||
.meta { |
|||
position: relative; |
|||
border-bottom: 1rpx solid #eaeaea; |
|||
.price { |
|||
height: 130rpx; |
|||
padding: 25rpx 30rpx 0; |
|||
color: #fff; |
|||
font-size: 34rpx; |
|||
box-sizing: border-box; |
|||
background-color: #35c8a9; |
|||
} |
|||
.number { |
|||
font-size: 56rpx; |
|||
} |
|||
.brand { |
|||
width: 160rpx; |
|||
height: 80rpx; |
|||
overflow: hidden; |
|||
position: absolute; |
|||
top: 26rpx; |
|||
right: 30rpx; |
|||
} |
|||
.name { |
|||
max-height: 88rpx; |
|||
line-height: 1.4; |
|||
margin: 20rpx; |
|||
font-size: 32rpx; |
|||
color: #333; |
|||
} |
|||
.desc { |
|||
line-height: 1; |
|||
padding: 0 20rpx 30rpx; |
|||
font-size: 24rpx; |
|||
color: #cf4444; |
|||
} |
|||
} |
|||
.action { |
|||
padding-left: 20rpx; |
|||
.item { |
|||
height: 90rpx; |
|||
padding-right: 60rpx; |
|||
border-bottom: 1rpx solid #eaeaea; |
|||
font-size: 26rpx; |
|||
color: #333; |
|||
position: relative; |
|||
display: flex; |
|||
align-items: center; |
|||
&:last-child { |
|||
border-bottom: 0 none; |
|||
} |
|||
} |
|||
.label { |
|||
width: 60rpx; |
|||
color: #898b94; |
|||
margin: 0 16rpx 0 10rpx; |
|||
} |
|||
.text { |
|||
flex: 1; |
|||
-webkit-line-clamp: 1; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/* 商品详情 */ |
|||
.detail { |
|||
padding-left: 20rpx; |
|||
.content { |
|||
margin-left: -20rpx; |
|||
.image { |
|||
width: 100%; |
|||
} |
|||
} |
|||
.properties { |
|||
padding: 0 20rpx; |
|||
margin-bottom: 30rpx; |
|||
.item { |
|||
display: flex; |
|||
line-height: 2; |
|||
padding: 10rpx; |
|||
font-size: 26rpx; |
|||
color: #333; |
|||
border-bottom: 1rpx dashed #ccc; |
|||
} |
|||
.label { |
|||
width: 200rpx; |
|||
} |
|||
.value { |
|||
flex: 1; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/* 同类推荐 */ |
|||
.similar { |
|||
.content { |
|||
padding: 0 20rpx 200rpx; |
|||
background-color: #f4f4f4; |
|||
display: flex; |
|||
flex-wrap: wrap; |
|||
.goods { |
|||
width: 340rpx; |
|||
padding: 24rpx 20rpx 20rpx; |
|||
margin: 20rpx 7rpx; |
|||
border-radius: 10rpx; |
|||
background-color: #fff; |
|||
} |
|||
.image { |
|||
width: 300rpx; |
|||
height: 260rpx; |
|||
} |
|||
.name { |
|||
height: 80rpx; |
|||
margin: 10rpx 0; |
|||
font-size: 26rpx; |
|||
color: #262626; |
|||
} |
|||
.price { |
|||
line-height: 1; |
|||
font-size: 20rpx; |
|||
color: #cf4444; |
|||
} |
|||
.number { |
|||
font-size: 26rpx; |
|||
margin-left: 2rpx; |
|||
} |
|||
} |
|||
navigator { |
|||
&:nth-child(even) { |
|||
margin-right: 0; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/* 底部工具栏 */ |
|||
.toolbar { |
|||
position: fixed; |
|||
left: 0; |
|||
right: 0; |
|||
bottom: 0; |
|||
z-index: 1; |
|||
background-color: #fff; |
|||
height: 100rpx; |
|||
padding: 0 20rpx var(--window-bottom); |
|||
border-top: 1rpx solid #eaeaea; |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
box-sizing: content-box; |
|||
.buttons { |
|||
display: flex; |
|||
& > view { |
|||
width: 220rpx; |
|||
text-align: center; |
|||
line-height: 72rpx; |
|||
font-size: 26rpx; |
|||
color: #fff; |
|||
border-radius: 72rpx; |
|||
} |
|||
.addcart { |
|||
background-color: #ffa868; |
|||
} |
|||
.buynow, |
|||
.payment { |
|||
background-color: #27ba9b; |
|||
margin-left: 20rpx; |
|||
} |
|||
} |
|||
.icons { |
|||
padding-right: 10rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
flex: 1; |
|||
.icons-button { |
|||
flex: 1; |
|||
text-align: center; |
|||
line-height: 1.4; |
|||
padding: 0; |
|||
margin: 0; |
|||
border-radius: 0; |
|||
font-size: 20rpx; |
|||
color: #333; |
|||
background-color: #fff; |
|||
&::after { |
|||
border: none; |
|||
} |
|||
} |
|||
text { |
|||
display: block; |
|||
font-size: 34rpx; |
|||
} |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,13 @@ |
|||
import { http } from '@/utils/http' |
|||
import type { GoodsResult } from '@/types/goods' |
|||
/** |
|||
* 商品详情 |
|||
* @param id 商品id |
|||
*/ |
|||
export const getGoodsByIdAPI = (id: string) => { |
|||
return http<GoodsResult>({ |
|||
method: 'GET', |
|||
url: '/goods', |
|||
data: { id }, |
|||
}) |
|||
} |
@ -0,0 +1,111 @@ |
|||
import type { GoodsItem } from './global' |
|||
|
|||
/** 商品信息 */ |
|||
export type GoodsResult = { |
|||
/** id */ |
|||
id: string |
|||
/** 商品名称 */ |
|||
name: string |
|||
/** 商品描述 */ |
|||
desc: string |
|||
/** 当前价格 */ |
|||
price: number |
|||
/** 原价 */ |
|||
oldPrice: number |
|||
/** 商品详情: 包含详情属性 + 详情图片 */ |
|||
details: Details |
|||
/** 主图图片集合[ 主图图片链接 ] */ |
|||
mainPictures: string[] |
|||
/** 同类商品[ 商品信息 ] */ |
|||
similarProducts: GoodsItem[] |
|||
/** sku集合[ sku信息 ] */ |
|||
skus: SkuItem[] |
|||
/** 可选规格集合备注[ 可选规格信息 ] */ |
|||
specs: SpecItem[] |
|||
/** 用户地址列表[ 地址信息 ] */ |
|||
userAddresses: AddressItem[] |
|||
} |
|||
|
|||
/** 商品详情: 包含详情属性 + 详情图片 */ |
|||
export type Details = { |
|||
/** 商品属性集合[ 属性信息 ] */ |
|||
properties: DetailsPropertyItem[] |
|||
/** 商品详情图片集合[ 图片链接 ] */ |
|||
pictures: string[] |
|||
} |
|||
|
|||
/** 属性信息 */ |
|||
export type DetailsPropertyItem = { |
|||
/** 属性名称 */ |
|||
name: string |
|||
/** 属性值 */ |
|||
value: string |
|||
} |
|||
|
|||
/** sku信息 */ |
|||
export type SkuItem = { |
|||
/** id */ |
|||
id: string |
|||
/** 库存 */ |
|||
inventory: number |
|||
/** 原价 */ |
|||
oldPrice: number |
|||
/** sku图片 */ |
|||
picture: string |
|||
/** 当前价格 */ |
|||
price: number |
|||
/** sku编码 */ |
|||
skuCode: string |
|||
/** 规格集合[ 规格信息 ] */ |
|||
specs: SkuSpecItem[] |
|||
} |
|||
|
|||
/** 规格信息 */ |
|||
export type SkuSpecItem = { |
|||
/** 规格名称 */ |
|||
name: string |
|||
/** 可选值名称 */ |
|||
valueName: string |
|||
} |
|||
|
|||
/** 可选规格信息 */ |
|||
export type SpecItem = { |
|||
/** 规格名称 */ |
|||
name: string |
|||
/** 可选值集合[ 可选值信息 ] */ |
|||
values: SpecValueItem[] |
|||
} |
|||
|
|||
/** 可选值信息 */ |
|||
export type SpecValueItem = { |
|||
/** 是否可售 */ |
|||
available: boolean |
|||
/** 可选值备注 */ |
|||
desc: string |
|||
/** 可选值名称 */ |
|||
name: string |
|||
/** 可选值图片链接 */ |
|||
picture: string |
|||
} |
|||
|
|||
/** 地址信息 */ |
|||
export type AddressItem = { |
|||
/** 收货人姓名 */ |
|||
receiver: string |
|||
/** 联系方式 */ |
|||
contact: string |
|||
/** 省份编码 */ |
|||
provinceCode: string |
|||
/** 城市编码 */ |
|||
cityCode: string |
|||
/** 区/县编码 */ |
|||
countyCode: string |
|||
/** 详细地址 */ |
|||
address: string |
|||
/** 默认地址,1为是,0为否 */ |
|||
isDefault: number |
|||
/** 收货地址 id */ |
|||
id: string |
|||
/** 省市区 */ |
|||
fullLocation: string |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue