detail.vue 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843
  1. <template>
  2. <view :style="themeColor()">
  3. <view class="bg-[var(--page-bg-color)] min-h-[100vh] relative" v-if="Object.keys(goodsDetail).length">
  4. <!-- 自定义头部 -->
  5. <view class="flex items-center fixed left-0 right-0 z-10 bg-transparent detail-head" :class="{'!bg-[#fff]' :detailHeadBgChange}" :style="navbarInnerStyle">
  6. <text class="nc-iconfont nc-icon-zuoV6xx" :style="navbarInnerArrowStyle" @click="goback()"></text>
  7. <view class="ml-auto !pt-[12rpx] !pb-[8rpx] px-[10rpx] bg-[rgba(255,255,255,.4)] rounded-full border-[2rpx] border-solid border-transparent box-border nc-iconfont nc-icon-fenxiangV6xx font-bold text-[#303133] text-[36rpx]" :class="{'border-[#d8d8d8]': detailHeadBgChange}" @click="openShareFn"></view>
  8. </view>
  9. <view class="swiper-box">
  10. <u-swiper :list="goodsDetail.goods.goods_image" :indicator="goodsDetail.goods.goods_image.length" :indicatorStyle="{'bottom': '50rpx'}" :autoplay="true" height="100vw" radius="0" @click="swiperClick"></u-swiper>
  11. </view>
  12. <view v-if="priceType == 'discount_price'" class="rounded-t-[40rpx] -mt-[36rpx] relative flex items-center justify-between !bg-cover box-border pb-[26rpx] h-[136rpx] px-[30rpx]" :style="{ background: 'url(' + img('addon/mall/detail/discount_price_bg.png') + ') no-repeat'}">
  13. <view class="text-[#fff]">
  14. <text class="text-[28rpx] mr-[10rpx] font-500">折扣价</text>
  15. <view class="inline-block">
  16. <text class="text-[32rpx] price-font mr-[4rpx]">¥</text>
  17. <text class="text-[56rpx] -mb-[4rpx] price-font">{{ parseFloat(goodsPrice).toFixed(2).split('.')[0] }}</text>
  18. <text class="text-[32rpx] price-font">.{{ parseFloat(goodsPrice).toFixed(2).split('.')[1] }}</text>
  19. </view>
  20. <text class="text-[32rpx] ml-[14rpx] line-through price-font" v-if="goodsDetail.market_price && parseFloat(goodsDetail.market_price)">
  21. ¥{{ goodsDetail.market_price }}
  22. </text>
  23. </view>
  24. <view class="flex flex-col text-[#fff] items-end">
  25. <image class="h-[28rpx] mr-[2rpx]" :src="img('addon/mall/detail/discount_price.png')" mode="heightFix"></image>
  26. <view class="flex items-center text-[24rpx] -mb-[10rpx]">
  27. <text class="mr-[4rpx]">距结束</text>
  28. <up-count-down class="!text-[#fff] text-[28rpx]" :time="discountTime" format="HH:mm:ss"></up-count-down>
  29. </view>
  30. </view>
  31. </view>
  32. <view class="bg-[var(--page-bg-color)] rounded-[40rpx] overflow-hidden -mt-[28rpx] relative">
  33. <view class="datail-title relative px-[30rpx]" :class="{'pt-[40rpx]': priceType == 'discount_price','pt-[20rpx]': priceType != 'discount_price'}">
  34. <view class="text-[var(--price-text-color)] flex items-baseline mb-[12rpx]" v-if="priceType != 'discount_price'">
  35. <view class="inline-block">
  36. <text class="text-[32rpx] font-500 price-font">¥</text>
  37. <text class="text-[48rpx] font-500 price-font">{{ parseFloat(goodsPrice).toFixed(2).split('.')[0] }}</text>
  38. <text class="text-[32rpx] font-500 mr-[10rpx] price-font">.{{ parseFloat(goodsPrice).toFixed(2).split('.')[1] }}</text>
  39. </view>
  40. <image v-if="priceType == 'member_price'" class="h-[34rpx] mr-[12rpx] w-[80rpx]" :src="img('addon/mall/VIP.png')" mode="heightFix" />
  41. <text class="text-[26rpx] text-[var(--text-color-light9)] line-through price-font" v-if="goodsDetail.market_price && parseFloat(goodsDetail.market_price)">
  42. ¥{{ goodsDetail.market_price }}
  43. </text>
  44. </view>
  45. <view class="text-[#333] font-500 text-[30rpx] multi-hidden leading-[40rpx]">
  46. {{ goodsDetail.goods.goods_name }}
  47. </view>
  48. <view class="flex items-start mt-[24rpx]">
  49. <view class="flex flex-wrap" v-if="goodsDetail.label_info && goodsDetail.label_info.length">
  50. <view v-for="item in goodsDetail.label_info" :key="item.label_id"
  51. class="tag-item text-[#FA6400] mb-[10rpx] h-[36rpx] text-[20rpx] px-[12rpx] border-[2rpx] border-solid border-[#FA6400] mr-[15rpx] truncate">
  52. {{ item.label_name }}
  53. </view>
  54. </view>
  55. <view class="text-[22rpx] mb-[10rpx] text-[var(--text-color-light9)] flex items-baseline ml-auto">
  56. <text class="whitespace-nowrap">销量</text>
  57. <text class="mx-[2rpx]">{{ goodsDetail.goods.sale_num }}</text>
  58. <text>{{ goodsDetail.goods.unit }}</text>
  59. </view>
  60. </view>
  61. </view>
  62. </view>
  63. <view class="mt-[var(--top-m)] sidebar-marign card-template" v-if="isGoodsPropertyTemp">
  64. <view @click="servicesDataShow = !servicesDataShow" v-if="goodsDetail.service && goodsDetail.service.length" class="card-template-item">
  65. <text class="text-[#333] text-[26rpx] leading-[30rpx] font-400 flex-shrink-0">服务</text>
  66. <view class="text-[#343434] text-[26rpx] leading-[30rpx] font-400 truncate ml-auto">
  67. {{ goodsDetail.service[0].service_name }}
  68. </view>
  69. <text class="nc-iconfont nc-icon-youV6xx text-[26rpx] text-[var(--text-color-light6)] ml-[8rpx]"></text>
  70. </view>
  71. <view @click="buyFn" v-if="goodsDetail.goodsSpec && goodsDetail.goodsSpec.length" class="card-template-item">
  72. <text class="text-[#333] text-[26rpx] leading-[30rpx] font-400 flex-shrink-0 mr-[20rpx]">已选</text>
  73. <view class="ml-auto text-right truncate flex-1 text-[#343434] text-[26rpx] leading-[30rpx] font-400">
  74. {{ goodsDetail.sku_spec_format }}
  75. </view>
  76. <text class="nc-iconfont nc-icon-youV6xx text-[26rpx] text-[var(--text-color-light6)] ml-[8rpx]"></text>
  77. </view>
  78. <view class="card-template-item" @click="distributionDataOpen" v-if="goodsDetail.goods.goods_type == 'real'&&goodsDetail.delivery_type_list&&goodsDetail.delivery_type_list.length" >
  79. <text class="text-[#333] text-[26rpx] leading-[30rpx] font-400 flex-shrink-0">配送</text>
  80. <view class="ml-auto flex items-center text-[#343434] text-[26rpx] leading-[30rpx] font-400">
  81. {{goodsDetail.delivery_type_list[selectDeliveryType]}}
  82. </view>
  83. <text class="nc-iconfont nc-icon-youV6xx text-[26rpx] text-[var(--text-color-light6)] ml-[8rpx]"></text>
  84. </view>
  85. <view @click="couponListShow = true" v-if="couponList.length" class="card-template-item">
  86. <text class="text-[#333] text-[26rpx] leading-[30rpx] font-400 flex-shrink-0 mr-[20rpx]">领券</text>
  87. <view class="ml-auto flex-1 flex-nowrap flex items-center overflow-hidden h-[44rpx] content-between">
  88. <block v-for="(item, index) in couponList" :key="index">
  89. <text v-if="index < 3" class="tag-item whitespace-nowrap border-[2rpx] px-[6rpx] h-[40rpx] border-solid border-[var(--primary-color)] text-[var(--primary-color)] mt-[4rpx]" :class="{'mr-[12rpx]': couponList.length != (index+1) && index < 2, 'ml-auto': index == 0}">
  90. {{ item.title }}
  91. </text>
  92. </block>
  93. </view>
  94. <text class="nc-iconfont nc-icon-youV6xx text-[26rpx] text-[var(--text-color-light6)] ml-[8rpx]"></text>
  95. </view>
  96. </view>
  97. <view class="mt-[var(--top-m)] sidebar-marign card-template">
  98. <view class="flex justify-between items-center">
  99. <view class="flex flex-1">
  100. <u--image class="rounded-[6rpx] overflow-hidden" width="86rpx" height="86rpx" :src="img( goodsDetail.shop_info.icon? goodsDetail.shop_info.icon : '')" model="aspectFill">
  101. <template #error>
  102. <image :src="img('addon/mall/shop/shop_default.png')" class="w-[86rpx] h-[86rpx]"></image>
  103. </template>
  104. </u--image>
  105. <view class="ml-[20rpx] flex flex-col justify-center">
  106. <view class="flex items-center">
  107. <text class="text-[28rpx] text-[#282828] font-600 leading-[38rpx] max-w-[200rpx] truncate">{{goodsDetail.shop_info.site_name}}</text>
  108. <text class="ml-[8rpx] bg-[var(--primary-color)] text-[#fff] text-[20rpx] rounded-[4rpx] px-[4rpx] leading-[28rpx]" v-if="goodsDetail.shop_info.is_self">{{goodsDetail.shop_info.is_self ? '自营' : ''}}</text>
  109. </view>
  110. <view class="text-[22rpx] text-[var(--text-color-light6)] mt-[8rpx] leading-[30rpx]">{{goodsDetail.shop_info.follow_number}}人关注</view>
  111. </view>
  112. </view>
  113. <view class="w-[114rpx] h-[50rpx] leading-[50rpx] bg-[var(--primary-color)] text-[#fff] text-center rounded-[25rpx] text-[28rpx]" @click="toShopDetail(goodsDetail.shop_info.site_id)">进店</view>
  114. </view>
  115. </view>
  116. <view class="mt-[var(--top-m)] sidebar-marign card-template">
  117. <view class="flex items-center justify-between min-h-[40rpx]" :class="{'mb-[30rpx]': evaluate && evaluate.list && evaluate.list.length}">
  118. <text class="title">宝贝评价({{ evaluate.count }})</text>
  119. <view v-if="evaluate.count" class="h-[40rpx] flex items-center" @click="toLink(goodsDetail.goods_id)">
  120. <text class="text-[24rpx] text-[var(--text-color-light9)]">查看全部</text>
  121. <text class="nc-iconfont nc-icon-youV6xx text-[26rpx] text-[var(--text-color-light9)]"></text>
  122. </view>
  123. <text v-if="!evaluate.count" class="text-[24rpx] text-[var(--text-color-light9)]">暂无评价</text>
  124. </view>
  125. <view>
  126. <view :class="{'pb-[30rpx]': index != (evaluate.list.length-1)}" v-for="(item, index) in evaluate.list" :key="index">
  127. <view class="flex items-center w-full">
  128. <u-avatar :default-url="img('static/resource/images/default_headimg.png')" :src="img(item.member_head)" :size="'50rpx'" leftIcon="none"></u-avatar>
  129. <text class="ml-[10rpx] text-[28rpx] text-[#333]">{{ item.member_name }}</text>
  130. </view>
  131. <view class="flex justify-between w-full mt-[16rpx]">
  132. <view class="flex-1 w-[540rpx] text-[26rpx] text-[#333] max-h-[72rpx] leading-[36rpx] multi-hidden mr-[50rpx]">{{ item.content }}</view>
  133. </view>
  134. <view class="w-[80rpx] flex-shrink-0">
  135. <u--image v-if="item.image_mid && item.image_mid.length" width="80rpx" height="80rpx" radius="16rpx" :src="img(item.image_mid[0])" mode="aspectFill" @click="imgListPreview(item.images[0])">
  136. <template #error>
  137. <u-icon name="photo" color="#999" size="50"></u-icon>
  138. </template>
  139. </u--image>
  140. </view>
  141. </view>
  142. </view>
  143. </view>
  144. <view class="my-[var(--top-m)] goods-sku sidebar-marign card-template" v-if="goodsDetail.goods && goodsDetail.goods.attr_format && Object.keys(goodsDetail.goods.attr_format).length">
  145. <view class="title mb-[30rpx]">商品属性</view>
  146. <view>
  147. <block v-for="(item,index) in goodsDetail.goods.attr_format" :key="index">
  148. <view v-if="index < 4 || isAttrFormatShow" class="card-template-item">
  149. <view class="text-[26rpx] leading-[30rpx] w-[160rpx] font-400 shrink-0 text-[var(--text-color-light9)]">{{item.attr_value_name}}</view>
  150. <view class="text-[#333] box-border value-wid text-[26rpx] leading-[30rpx] font-400 truncate pl-[20rpx]">{{Array.isArray(item.attr_child_value_name) ? item.attr_child_value_name.join(',') : item.attr_child_value_name }}</view>
  151. </view>
  152. </block>
  153. <view v-if="goodsDetail.goods.attr_format.length > 4" class="flex items-center justify-center" @click="isAttrFormatShow = !isAttrFormatShow">
  154. <text class="text-[24rpx] mr-[10rpx] text-[var(--text-color-light9)]">{{!isAttrFormatShow ? '展开' : '收起'}}</text>
  155. <text class="nc-iconfont !text-[22rpx] text-[var(--text-color-light9)]" :class="{'nc-icon-xiaV6xx': !isAttrFormatShow, 'nc-icon-shangV6xx-1': isAttrFormatShow}"></text>
  156. </view>
  157. </view>
  158. </view>
  159. <view class="my-[var(--top-m)] sidebar-marign card-template px-[var(--pad-sidebar-m)]">
  160. <view class="title pb-[30rpx]">商品详情</view>
  161. <view class="u-content">
  162. <u-parse :content="goodsDetail.goods.goods_desc" :tagStyle="{img: 'vertical-align: top;',p:'overflow: hidden;word-break:break-word;' }"></u-parse>
  163. </view>
  164. </view>
  165. <!-- tabber -->
  166. <view class="tab-bar-placeholder"></view>
  167. <view class="border-[0] border-t-[2rpx] border-solid border-[#f5f5f5] w-[100%] flex justify-between pl-[32rpx] pr-[4rpx] bg-[#fff] box-border fixed left-0 bottom-0 tab-bar z-1 items-center">
  168. <view class="flex items-center">
  169. <view class="flex flex-col justify-center items-center mr-[38rpx]" @click="redirect({ url: '/app/pages/index/index', mode: 'reLaunch' })">
  170. <view class="nc-iconfont nc-icon-shouyeV6xx text-[36rpx]"></view>
  171. <text class="text-[20rpx] mt-[10rpx]">首页</text>
  172. </view>
  173. <view class="flex flex-col justify-center items-center mr-[38rpx]" @click="openShareFn">
  174. <view class="nc-iconfont nc-icon-fenxiangV6xx text-[36rpx]"></view>
  175. <text class="text-[20rpx] mt-[10rpx]">分享</text>
  176. </view>
  177. <view class="flex flex-col justify-center items-center mr-[38rpx]" @click="collectFn">
  178. <text class="nc-iconfont text-[36rpx]" :class="{'text-[#ff0000] nc-icon-xihuanV6mm': isCollect, 'text-[#303133] nc-icon-guanzhuV6xx' : !isCollect}"></text>
  179. <text class="text-[20rpx] mt-[10rpx]">收藏</text>
  180. </view>
  181. </view>
  182. <view class="flex flex-1" v-if="goodsDetail.goods.status == 1">
  183. <button v-if="goodsDetail.goods.goods_type == 'real' || (goodsDetail.goods.goods_type == 'virtual' && goodsDetail.goods.virtual_receive_type != 'verify')"
  184. class="flex-1 !h-[70rpx] font-500 text-[26rpx] !text-[#fff] !m-0 !mr-[16rpx] leading-[70rpx] rounded-full remove-border"
  185. style="background: linear-gradient(127deg, #FFB000 0%, #FFA029 100%);" @click="buyFn('join_cart')">
  186. 加入购物车</button>
  187. <button
  188. v-if="isShowSingleSku"
  189. :style="{ width : (goodsDetail.goods.goods_type == 'real' || (goodsDetail.goods.goods_type == 'virtual' && goodsDetail.goods.virtual_receive_type != 'verify')) ? '200rpx' : '420rpx' + '!important' }"
  190. class="flex-1 !h-[70rpx] font-500 text-[26rpx] !text-[#fff] primary-btn-bg !m-0 !mr-[16rpx] leading-[70rpx] rounded-full remove-border"
  191. @click="buyFn('buy_now')">立即购买</button>
  192. <button
  193. v-else :style="{ width : (goodsDetail.goods.goods_type == 'real' || (goodsDetail.goods.goods_type == 'virtual' && goodsDetail.goods.virtual_receive_type != 'verify')) ? '200rpx' : '420rpx' + '!important' }"
  194. class="flex-1 !h-[70rpx] font-500 text-[26rpx] !text-[#fff] !bg-[#ccc] !m-0 !mr-[16rpx] leading-[70rpx] rounded-full remove-border"
  195. >已售罄</button>
  196. </view>
  197. <view class="flex flex-1" v-else>
  198. <button class="w-[100%] !h-[70rpx] font-500 text-[26rpx] !text-[#fff] !bg-[#ccc] !m-0 leading-[70rpx] rounded-full remove-border">该商品已下架</button>
  199. </view>
  200. </view>
  201. <!-- 服务 -->
  202. <view @touchmove.prevent.stop>
  203. <u-popup class="popup-type" :show="servicesDataShow" @close="servicesDataShow = false">
  204. <view class="min-h-[480rpx]" @touchmove.prevent.stop>
  205. <view class="flex items-center justify-center py-[34rpx] relative">
  206. <text class="text-[32rpx] leading-[36rpx] font-500">商品服务</text>
  207. </view>
  208. <scroll-view class="h-[520rpx]" scroll-y="true">
  209. <view class="pl-[22rpx] py-[28rpx] pr-[37rpx]">
  210. <view class="flex mb-[28rpx]" v-for="(item, index) in goodsDetail.service">
  211. <image class="mt-[4rpx] w-[32rpx] h-[32rpx] mr-[14rpx]" :src="img(item.image || 'addon/mall/icon_service.png')" mode="aspectFit" />
  212. <view class="flex-1">
  213. <view class="text-[30rpx] leading-[36rpx] text-[#333] mb-[8rpx]">{{ item.service_name }}</view>
  214. <view class="text-[24rpx] leading-[36rpx] text-[var(--text-color-light9)]">{{ item.desc }}</view>
  215. </view>
  216. </view>
  217. </view>
  218. </scroll-view>
  219. </view>
  220. </u-popup>
  221. </view>
  222. <!-- 配送 -->
  223. <view @touchmove.prevent.stop>
  224. <u-popup class="popup-type" :show="distributionDataShow" @close="distributionDataShow = false">
  225. <view class="min-h-[360rpx]" @touchmove.prevent.stop>
  226. <view class="flex items-center justify-center py-[34rpx] relative">
  227. <text class="text-[32rpx] leading-[36rpx] font-500">配送方式</text>
  228. </view>
  229. <scroll-view class="h-[520rpx]" scroll-y="true">
  230. <view class="px-[var(--popup-sidebar-m)] pt-[28rpx]">
  231. <view class="flex mb-[40rpx]" v-for="(item, index) in goodsDetail.delivery_type_list" @click="distributionListFn(item,index)">
  232. <image class="mt-[4rpx] w-[32rpx] h-[32rpx] mr-[14rpx]" :src="img('addon/mall/icon_service.png')" mode="aspectFit" />
  233. <view class="flex-1">
  234. <view class="text-[30rpx] leading-[36rpx] text-[#333] mb-[8rpx]">{{ item }}</view>
  235. <view class="text-[24rpx] leading-[36rpx] text-[var(--text-color-light9)]">{{ item }}</view>
  236. </view>
  237. </view>
  238. </view>
  239. </scroll-view>
  240. </view>
  241. </u-popup>
  242. </view>
  243. <!-- 优惠券 -->
  244. <view @touchmove.prevent.stop>
  245. <u-popup class="popup-type" :show="couponListShow" @close="couponListShow = false">
  246. <view class="min-h-[480rpx]" @touchmove.prevent.stop>
  247. <view class="flex items-center justify-center py-[34rpx] relative">
  248. <text class="text-[32rpx] leading-[36rpx] font-500">优惠券</text>
  249. </view>
  250. <scroll-view class="h-[520rpx]" :scroll-y="true">
  251. <view class="px-[32rpx]">
  252. <view
  253. class="mb-[30rpx] flex items-center border-[2rpx] border-solid border-[rgba(0,0,0,.1)] rounded-[var(--rounded-small)]"
  254. v-for="(item, index) in couponList" :key="index">
  255. <view
  256. class="flex flex-col items-center my-[20rpx] w-[200rpx] border-0 border-r-[2rpx] border-dashed border-[rgba(0,0,0,.1)]">
  257. <view class="text-xs price-font">
  258. <text class="text-[28rpx]">¥</text>
  259. <text class="text-[48rpx]">{{ item.price }}</text>
  260. </view>
  261. <text class="text-xs mt-[12rpx]">{{ Number(item.min_condition_money) ? ('满' + item.min_condition_money + '元可用') : '无门槛' }}</text>
  262. </view>
  263. <view class="ml-[20rpx] flex-1 flex flex-col py-[20rpx]">
  264. <text class="text-xs font-500">{{ item.title }}</text>
  265. <text class="text-xs text-[var(--text-color-light6)] mt-[12rpx]">{{ item.valid_type == 1 &&
  266. ('领取之日起' + item.length + '天内有效') || item.valid_type == 2 &&
  267. ('领取之日起至' + item.valid_end_time) }}</text>
  268. </view>
  269. <text v-if="item.btnType === 'collecting'"
  270. class="bg-[var(--primary-color)] mr-[20rpx] w-[106rpx] box-border text-center text-[#fff] h-[50rpx] text-[22rpx] px-[20rpx] leading-[50rpx] rounded-[100rpx]"
  271. @click="getCouponFn(item, index)">领取</text>
  272. <text v-else
  273. class="!bg-[#FFB4B1] mr-[20rpx] text-[#fff] mr-[20rpx] h-[50rpx] text-[22rpx] px-[20rpx] leading-[50rpx] rounded-[100rpx]">{{
  274. item.btnType
  275. === 'collected' ? '已领完' : '已领取' }}</text>
  276. </view>
  277. </view>
  278. </scroll-view>
  279. <view class="px-[20rpx] pb-[32rpx] pt-[42rpx]">
  280. <button
  281. class="font-500 !w-[100%] !h-[80rpx] primary-btn-bg text-[28rpx] !text-[#fff] !m-0 rounded-full leading-[80rpx]"
  282. @click="couponListShow = false">确定</button>
  283. </view>
  284. </view>
  285. </u-popup>
  286. </view>
  287. <ns-goods-sku ref="goodsSkuRef" :goods-detail="goodsDetail" @change="specSelectFn"></ns-goods-sku>
  288. <share-poster ref="sharePosterRef" posterType="shop_goods" :posterId="goodsDetail.goods.poster_id" :posterParam="posterParam" :copyUrlParam="copyUrlParam" />
  289. </view>
  290. <u-loading-page bg-color="rgb(248,248,248)" :loading="loading" loadingText=""></u-loading-page>
  291. <!-- #ifdef MP-WEIXIN -->
  292. <!-- 小程序隐私协议 -->
  293. <wx-privacy-popup ref="wxPrivacyPopupRef"></wx-privacy-popup>
  294. <!-- #endif -->
  295. <!-- 强制绑定手机号 -->
  296. <bind-mobile ref="bindMobileRef" />
  297. </view>
  298. </template>
  299. <script setup lang="ts">
  300. import { ref, computed, getCurrentInstance, nextTick } from 'vue';
  301. import { onLoad, onShow,onUnload, onPageScroll } from '@dcloudio/uni-app'
  302. import { img, redirect, handleOnloadParams,getToken, deepClone} from '@/utils/common';
  303. import { t } from '@/locale';
  304. import { getGoodsDetail, collect, cancelCollect, getEvaluateList, addGoodsBrowse } from '@/addon/mall/api/goods';
  305. import { getMallGoodsCoupon, getCoupon } from '@/addon/mall/api/coupon';
  306. import nsGoodsSku from '@/addon/mall/components/ns-goods-sku/ns-goods-sku.vue';
  307. import bindMobile from '@/components/bind-mobile/bind-mobile.vue';
  308. import useCartStore from '@/addon/mall/stores/cart'
  309. import { useLogin } from '@/hooks/useLogin'
  310. import useMemberStore from '@/stores/member'
  311. import { useShare }from '@/hooks/useShare'
  312. import sharePoster from '@/components/share-poster/share-poster.vue'
  313. // 分享
  314. const{setShare} = useShare()
  315. // 会员信息
  316. const memberStore = useMemberStore()
  317. const userInfo = computed(() => memberStore.info)
  318. // 购物车数量
  319. const cartStore = useCartStore();
  320. let cartTotalNum = computed(() => cartStore.totalNum)
  321. let goodsSkuRef = ref(null);
  322. let goodsDetail = ref({});
  323. let isAttrFormatShow = ref(false); //控制属性是否展开
  324. let loading = ref<boolean>(false);
  325. let servicesDataShow = ref<boolean>(false)
  326. let distributionDataShow = ref<boolean>(false) //配送
  327. let couponListShow = ref<boolean>(false) //优惠券
  328. let discountTime = ref(0)
  329. const sendMessageTitle = ref('')
  330. const sendMessagePath = ref('')
  331. const sendMessageImg = ref('')
  332. onLoad((option) => {
  333. // #ifdef MP-WEIXIN
  334. // 处理小程序场景值参数
  335. option = handleOnloadParams(option);
  336. // #endif
  337. getGoodsDetail({
  338. goods_id: option.goods_id || '',
  339. sku_id: option.sku_id || '',
  340. }).then(res => {
  341. if (!res.data.goods || JSON.stringify(res.data) === '[]') {
  342. uni.showToast({ title: '找不到该商品', icon: 'none' })
  343. setTimeout(() => {
  344. redirect({ url: '/app/pages/index/index', mode: 'reLaunch' })
  345. }, 600)
  346. return false
  347. }
  348. goodsDetail.value = deepClone(res.data);
  349. isCollect.value = goodsDetail.value.goods.is_collect;
  350. goodsDetail.value.delivery_type_list = goodsDetail.value.goods.delivery_type_list ? Object.values(goodsDetail.value.goods.delivery_type_list).map(el => el.name) : [];
  351. goodsDetail.value.goods.goods_image = goodsDetail.value.goods.goods_image_thumb_big;
  352. goodsDetail.value.goods.goods_image.forEach((item, index) => {
  353. goodsDetail.value.goods.goods_image[index] = img(item);
  354. })
  355. // 商品属性
  356. if(goodsDetail.value.goods.attr_format){
  357. goodsDetail.value.goods.attr_format = goodsDetail.value.goods.attr_format.filter((item, index) => {
  358. return Array.isArray(item.attr_child_value_name) ? item.attr_child_value_name.length : item.attr_child_value_name
  359. })
  360. }
  361. sendMessageTitle.value = goodsDetail.value.goods.goods_name
  362. sendMessagePath.value = '/addon/mall/pages/goods/detail?sku_id=' + goodsDetail.value.sku_id;
  363. sendMessageImg.value = img(goodsDetail.value.goods.goods_cover_thumb_mid)
  364. // 分享 - start
  365. if(res.data.goods){
  366. let share = {
  367. title: goodsDetail.value.goods.goods_name,
  368. desc: goodsDetail.value.goods.sub_title,
  369. url: goodsDetail.value.goods.goods_cover_thumb_mid
  370. }
  371. uni. setNavigationBarTitle({
  372. title: goodsDetail.value.goods.goods_name
  373. })
  374. setShare({
  375. wechat:{
  376. ...share
  377. },
  378. weapp:{
  379. ...share
  380. }
  381. });
  382. }
  383. // 分享 - end
  384. // 折扣信息
  385. if(Object.keys(goodsDetail.value.goods).length && goodsDetail.value.goods.is_discount && Object.keys(goodsDetail.value.discount_info).length){
  386. let now = new Date();
  387. let timestamp = now.getTime();
  388. discountTime.value = goodsDetail.value.discount_info.active.end_time*1000 - timestamp.toFixed(0)
  389. }
  390. // 获取优惠券列表
  391. getMallCouponListFn();
  392. // 获取评价
  393. getEvaluateListFn();
  394. copyUrlFn();
  395. addGoodsBrowseFn();
  396. nextTick(() => {
  397. setTimeout(()=>{
  398. const query = uni.createSelectorQuery().in(instance);
  399. query.select('.swiper-box').boundingClientRect(data => {
  400. swiperHeight = data ? data.height : 0;
  401. }).exec();
  402. query.select('.detail-head').boundingClientRect(data => {
  403. if(data) {
  404. detailHead = data.height ? data.height : 0;
  405. }
  406. }).exec();
  407. }, 400)
  408. })
  409. })
  410. })
  411. onShow(() => {
  412. // 删除配送方式
  413. uni.removeStorageSync('distributionType');
  414. cartStore.getList();
  415. })
  416. const specSelectFn = (id) => {
  417. goodsDetail.value.skuList.forEach((item, index) => {
  418. if (item.sku_id == id) {
  419. Object.assign(goodsDetail.value, item);
  420. }
  421. })
  422. }
  423. // 判断单规格库存是否为0
  424. const isShowSingleSku = computed(() => {
  425. let isSingleSpec = false // 是否为单规格,true:多规格,false:单规格
  426. goodsDetail.value.skuList.forEach((item,index)=>{
  427. if(item.sku_spec_format){
  428. isSingleSpec = true
  429. }
  430. })
  431. // 单规格,库存为0,显示已售罄
  432. if(!isSingleSpec && goodsDetail.value.stock <= 0){
  433. return false;
  434. }else if(!isSingleSpec && goodsDetail.value.stock > 0){
  435. // 单规格,库存大于0,可以购买
  436. return true;
  437. }
  438. return true;
  439. })
  440. // 判断商品属性模块是否展示
  441. const isGoodsPropertyTemp = computed(() => {
  442. let bool = false;
  443. if(goodsDetail.value.service && goodsDetail.value.service.length ||
  444. goodsDetail.value.goodsSpec && goodsDetail.value.goodsSpec.length ||
  445. goodsDetail.value.goods.goods_type == 'real'&&goodsDetail.value.delivery_type_list&&goodsDetail.value.delivery_type_list.length ||
  446. couponList.value.length){
  447. bool = true;
  448. }
  449. return bool;
  450. })
  451. const buyFn = (type) => {
  452. goodsSkuRef.value.open(type)
  453. }
  454. //强制绑定手机号
  455. const bindMobileRef = ref(null)
  456. // 收藏
  457. let isCollect = ref(0);
  458. const collectFn = () => {
  459. // 检测是否登录
  460. if (!userInfo.value) {
  461. useLogin().setLoginBack({ url: '/addon/mall/pages/goods/detail', param: { sku_id: goodsDetail.value.sku_id } })
  462. return false
  463. }
  464. // 绑定手机号
  465. if(uni.getStorageSync('isbindmobile')){
  466. bindMobileRef.value.open()
  467. return false
  468. }
  469. let api = isCollect.value ? cancelCollect(goodsDetail.value.goods_id) : collect(goodsDetail.value.goods_id);
  470. api.then(res => {
  471. isCollect.value = !isCollect.value;
  472. if (isCollect.value) {
  473. uni.showToast({
  474. title: '收藏成功',
  475. icon: 'none'
  476. });
  477. } else {
  478. uni.showToast({
  479. title: '取消收藏',
  480. icon: 'none'
  481. });
  482. }
  483. })
  484. }
  485. // 优惠券
  486. let couponList = ref([]);
  487. const getMallCouponListFn = () => {
  488. getMallGoodsCoupon({
  489. site_id: goodsDetail.value.shop_info.site_id,
  490. category_id: goodsDetail.value.goods.goods_category || '',
  491. mall_category_id: goodsDetail.value.goods.goods_mall_category || '',
  492. goods_id: goodsDetail.value.goods_id || '',
  493. brand_id: goodsDetail.value.goods.brand_id || ''
  494. }).then(res => {
  495. couponList.value = res.data.data.map((el: any) => {
  496. if (!userInfo.value) {
  497. if (el.sum_count != -1 && el.receive_count === el.sum_count) {
  498. el.btnType = 'collected'//已领完
  499. } else {
  500. el.btnType = 'collecting'//领用
  501. }
  502. } else {
  503. if (el.is_receive) {
  504. if (el.member_receive_count < el.limit_count) {
  505. if (el.need_receive) {
  506. el.btnType = 'collecting'//领用
  507. } else {
  508. el.btnType = 'using'//去使用
  509. }
  510. } else {
  511. if (el.need_receive) {
  512. el.btnType = 'used'//已使用
  513. } else {
  514. el.btnType = 'using'//去使用
  515. }
  516. }
  517. } else {
  518. if (el.sum_count != -1 && el.receive_count === el.sum_count) {
  519. el.btnType = 'collected'//已领完
  520. } else {
  521. el.btnType = 'collecting'//领用
  522. }
  523. }
  524. }
  525. return el
  526. });
  527. })
  528. }
  529. // 领取优惠券
  530. const getCouponFn = (data, index) => {
  531. // 检测是否登录
  532. if (!userInfo.value) {
  533. useLogin().setLoginBack({ url: '/addon/mall/pages/goods/detail', param: { sku_id: goodsDetail.value.sku_id } })
  534. return false
  535. }
  536. // 绑定手机号
  537. if(uni.getStorageSync('isbindmobile')){
  538. bindMobileRef.value.open()
  539. return false
  540. }
  541. getCoupon({
  542. coupon_id: data.id || '',
  543. number: 1,
  544. }).then(res => {
  545. // couponList.value[index].btnType = 'using'
  546. getMallCouponListFn();
  547. })
  548. }
  549. // 获取评价
  550. const evaluate = ref({
  551. count : 0
  552. })
  553. const getEvaluateListFn = () => {
  554. getEvaluateList(goodsDetail.value.goods_id).then(res => {
  555. evaluate.value = res.data
  556. })
  557. }
  558. //进入评论
  559. const toLink = () => {
  560. redirect({ url: '/addon/mall/pages/evaluate/list', param: { goods_id: goodsDetail.value.goods_id } })
  561. }
  562. //浏览记录
  563. const addGoodsBrowseFn = () => {
  564. if (userInfo.value && userInfo.value.member_id) {
  565. addGoodsBrowse({goods_id:goodsDetail.value.goods_id,sku_id:goodsDetail.value.sku_id}).then(res => {})
  566. }
  567. }
  568. //进入店铺
  569. const toShopDetail = (site_id) => {
  570. redirect({ url: '/app/pages/site/index', param: { site_id } })
  571. }
  572. //预览图片
  573. const imgListPreview = (item:any,index:any) => {
  574. if(Array.isArray(item)){
  575. if (!item.length) return false
  576. var urlList =item;
  577. uni.previewImage({
  578. indicator: "number",
  579. current:index,
  580. loop: true,
  581. urls: urlList
  582. })
  583. }else{
  584. if (item === '') return false
  585. var urlList = []
  586. urlList.push(img(item)) //push中的参数为 :src="item.img_url" 中的图片地址
  587. uni.previewImage({
  588. indicator: "number",
  589. loop: true,
  590. urls: urlList
  591. })
  592. }
  593. }
  594. // 返回上一页
  595. const goback=()=> {
  596. if(getCurrentPages().length > 1){
  597. uni.navigateBack({
  598. delta: 1
  599. });
  600. }else{
  601. redirect({
  602. url: '/app/pages/index/index',
  603. mode: 'reLaunch'
  604. });
  605. }
  606. }
  607. /************ 选择配送方式-start ****************/
  608. const selectDeliveryType = ref(0);
  609. const distributionDataOpen = (()=>{
  610. distributionDataShow.value = true;
  611. });
  612. const distributionListFn = ((data,index)=>{
  613. selectDeliveryType.value = index;
  614. distributionDataShow.value = false;
  615. uni.setStorageSync('distributionType', data);
  616. });
  617. /************ 选择配送方式-end ****************/
  618. /************ 自定义头部-start ****************/
  619. // 获取系统状态栏的高度
  620. let systemInfo = uni.getSystemInfoSync();
  621. let platform = systemInfo.platform;
  622. let menuButtonInfo = {};
  623. // 如果是小程序,获取右上角胶囊的尺寸信息,避免导航栏右侧内容与胶囊重叠(支付宝小程序非本API,尚未兼容)
  624. // #ifdef MP-WEIXIN || MP-BAIDU || MP-TOUTIAO || MP-QQ
  625. menuButtonInfo = uni.getMenuButtonBoundingClientRect();
  626. // #endif
  627. // 导航栏内部盒子的样式
  628. const navbarInnerStyle = computed(() => {
  629. let style = '';
  630. // 导航栏宽度,如果在小程序下,导航栏宽度为胶囊的左边到屏幕左边的距离
  631. // #ifdef MP
  632. let rightButtonWidth = menuButtonInfo.width ? menuButtonInfo.width * 2 + 'rpx' : '70rpx';
  633. style += 'height:' + menuButtonInfo.height + 'px;';
  634. style += 'padding-right:calc(' + rightButtonWidth + ' + 30rpx);';
  635. style += 'padding-left:calc(' + rightButtonWidth + ' + 30rpx);';
  636. style += 'padding-top:' + menuButtonInfo.top + 'px;';
  637. style += 'padding-bottom: 8px;';
  638. style += 'font-size: 32rpx;';
  639. if (platform === 'ios') {
  640. // 苹果(iOS)设备
  641. style += 'font-weight: 500;';
  642. } else if (platform === 'android') {
  643. // 安卓(Android)设备
  644. style += 'font-size: 36rpx;';
  645. }
  646. // #endif
  647. // #ifdef H5
  648. style += 'height: 100rpx;';
  649. style += 'padding-right: 30rpx;';
  650. style += 'padding-left: 30rpx;';
  651. style += 'font-size: 32rpx;';
  652. if (platform === 'ios') {
  653. // 苹果(iOS)设备
  654. style += 'font-weight: 500;';
  655. } else if (platform === 'android') {
  656. // 安卓(Android)设备
  657. style += 'font-size: 36rpx;';
  658. }
  659. // #endif
  660. return style;
  661. })
  662. // 导航栏内部盒子的样式
  663. const navbarInnerArrowStyle = computed(() => {
  664. let style = '';
  665. // 导航栏宽度,如果在小程序下,导航栏宽度为胶囊的左边到屏幕左边的距离
  666. // #ifdef MP
  667. style += "padding-left: 10rpx;"
  668. style += "padding-right: 10rpx;"
  669. style += 'position: absolute;';
  670. style += 'left:calc( 100vw - ' + menuButtonInfo.right + 'px);';
  671. style += 'font-size: 26px;';
  672. if (platform === 'ios') {
  673. // 苹果(iOS)设备
  674. style += 'font-weight: 700;';
  675. } else if (platform === 'android') {
  676. // 安卓(Android)设备
  677. }
  678. // #endif
  679. // #ifdef H5
  680. style += 'font-size: 26px;';
  681. // #endif
  682. return style;
  683. })
  684. // 头部滚动
  685. const instance = getCurrentInstance();
  686. let swiperHeight = 0
  687. let detailHead = 0
  688. let detailHeadBgChange = ref(false)
  689. onPageScroll((e)=>{
  690. if (swiperHeight == 0 || detailHead == 0) return;
  691. let height = swiperHeight - detailHead - 20;
  692. detailHeadBgChange.value = false;
  693. if (e.scrollTop >= height) {
  694. detailHeadBgChange.value = true;
  695. }
  696. })
  697. /************ 自定义头部-end ****************/
  698. const swiperClick = (index)=>{
  699. if(typeof index == 'number')
  700. imgListPreview(goodsDetail.value.goods.goods_image,index)
  701. }
  702. /************* 分享海报-start **************/
  703. let sharePosterRef = ref(null);
  704. let copyUrlParam = ref('');
  705. let posterParam = {};
  706. // 分享海报链接
  707. const copyUrlFn = ()=>{
  708. copyUrlParam.value = '?sku_id='+goodsDetail.value.sku_id;
  709. if (userInfo.value && userInfo.value.member_id) copyUrlParam.value += '&mid=' + userInfo.value.member_id;
  710. }
  711. const openShareFn = ()=>{
  712. posterParam.sku_id = goodsDetail.value.sku_id;
  713. if (userInfo.value && userInfo.value.member_id) posterParam.member_id = userInfo.value.member_id;
  714. sharePosterRef.value.openShare()
  715. }
  716. /************* 分享海报-end **************/
  717. // 价格类型
  718. let priceType = ref('') //''=>原价,discount_price=>折扣价,member_price=>会员价
  719. // 商品价格
  720. let goodsPrice = computed(() =>{
  721. let price = "0.00";
  722. if(Object.keys(goodsDetail.value).length && Object.keys(goodsDetail.value.goods).length && goodsDetail.value.goods.is_discount && goodsDetail.value.sale_price != goodsDetail.value.price){
  723. // 折扣价
  724. price = goodsDetail.value.sale_price ? goodsDetail.value.sale_price : goodsDetail.value.price;
  725. priceType.value = 'discount_price'
  726. }else if(Object.keys(goodsDetail.value).length && Object.keys(goodsDetail.value.goods).length && goodsDetail.value.goods.member_discount && getToken() && goodsDetail.value.member_price != goodsDetail.value.price){
  727. // 会员价
  728. price = goodsDetail.value.member_price ? goodsDetail.value.member_price : goodsDetail.value.price;
  729. priceType.value = 'member_price'
  730. }else{
  731. price = goodsDetail.value.price
  732. priceType.value = ''
  733. }
  734. return price;
  735. })
  736. // 关闭预览图片
  737. onUnload(()=>{
  738. // #ifdef H5 || APP
  739. uni.closePreviewImage()
  740. // #endif
  741. })
  742. </script>
  743. <style lang="scss" scoped>
  744. .remove-border {
  745. &::after {
  746. border: none;
  747. }
  748. }
  749. :deep(.u-cell-group__wrapper) {
  750. .u-cell__body {
  751. padding: 23rpx 32rpx;
  752. }
  753. }
  754. .tab-bar-placeholder {
  755. padding-bottom: calc(constant(safe-area-inset-bottom) + 100rpx);
  756. padding-bottom: calc(env(safe-area-inset-bottom) + 100rpx);
  757. }
  758. .tab-bar {
  759. padding-top: 16rpx;
  760. padding-bottom: calc(constant(safe-area-inset-bottom) + 16rpx);
  761. padding-bottom: calc(env(safe-area-inset-bottom) + 16rpx);
  762. }
  763. :deep(.u-count-down) .u-count-down__text{
  764. color: #fff;
  765. font-size: 28rpx;
  766. }
  767. :deep(.u-swiper-indicator__wrapper--line__bar){
  768. height: 5rpx !important;
  769. }
  770. :deep(.u-swiper-indicator__wrapper--line){
  771. height: 5rpx !important;
  772. }
  773. .datail-title{
  774. background: linear-gradient(#fff , rgba(255,255,255,0));
  775. }
  776. .goods-sku .value-wid{
  777. width: calc(100% - 160rpx);
  778. }
  779. </style>