Q3学习计划
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

540 lines
12 KiB

1 month ago
1 month ago
  1. <template>
  2. <scroll-view scroll-y class="viewport">
  3. <!-- 基本信息 -->
  4. <view class="goods">
  5. <!-- 商品主图 -->
  6. <view class="preview">
  7. <swiper circular @change="onChange">
  8. <swiper-item v-for="item in goods?.mainPictures" :key="item">
  9. <image @tap="onTapImage(item)" mode="aspectFill" :src="item" />
  10. </swiper-item>
  11. </swiper>
  12. <view class="indicator">
  13. <text class="current">{{ currentIndex + 1 }}</text>
  14. <text class="split">/</text>
  15. <text class="total">{{ goods?.mainPictures.length }}</text>
  16. </view>
  17. </view>
  18. <!-- 商品简介 -->
  19. <view class="meta">
  20. <view class="price">
  21. <text class="symbol">¥</text>
  22. <text class="number">{{ goods?.price }}</text>
  23. </view>
  24. <view class="name ellipsis">{{ goods?.name }}</view>
  25. <view class="desc">{{ goods?.desc }}</view>
  26. </view>
  27. <!-- 操作面板 -->
  28. <view class="action">
  29. <view class="item arrow" @tap="isShowSku = true">
  30. <text class="label">选择</text>
  31. <text class="text ellipsis"> {{ selectArrText }} </text>
  32. </view>
  33. <view @tap="openPopup('address')" class="item arrow">
  34. <text class="label">送至</text>
  35. <text class="text ellipsis"> 请选择收获地址 </text>
  36. </view>
  37. <view @tap="openPopup('service')" class="item arrow">
  38. <text class="label">服务</text>
  39. <text class="text ellipsis"> 无忧退 快速退款 免费包邮 </text>
  40. </view>
  41. </view>
  42. </view>
  43. <!-- 商品详情 -->
  44. <view class="detail panel">
  45. <view class="title">
  46. <text>详情</text>
  47. </view>
  48. <view class="content">
  49. <view class="properties">
  50. <!-- 属性详情 -->
  51. <view class="item" v-for="item in goods?.details.properties" :key="item.name">
  52. <text class="label">{{ item.name }}</text>
  53. <text class="value">{{ item.value }}</text>
  54. </view>
  55. </view>
  56. <!-- 图片详情 -->
  57. <image
  58. class="image"
  59. v-for="item in goods?.details.pictures"
  60. :key="item"
  61. mode="widthFix"
  62. :src="item"
  63. ></image>
  64. </view>
  65. </view>
  66. <!-- 同类推荐 -->
  67. <view class="similar panel">
  68. <view class="title">
  69. <text>同类推荐</text>
  70. </view>
  71. <view class="content">
  72. <navigator
  73. v-for="item in goods?.similarProducts"
  74. :key="item.id"
  75. class="goods"
  76. hover-class="none"
  77. :url="`/pages/goods/goods?id=${item.id}`"
  78. >
  79. <image class="image" mode="aspectFill" :src="item.picture"></image>
  80. <view class="name ellipsis">{{ item.name }}</view>
  81. <view class="price">
  82. <text class="symbol">¥</text>
  83. <text class="number">{{ item.price }}</text>
  84. </view>
  85. </navigator>
  86. </view>
  87. </view>
  88. </scroll-view>
  89. <!-- 用户操作 -->
  90. <view class="toolbar" :style="{ paddingBottom: safeAreaInsets?.bottom + 'px' }">
  91. <view class="icons">
  92. <button class="icons-button"><text class="icon-heart"></text>收藏</button>
  93. <!-- #ifdef MP-WEIXIN -->
  94. <button class="icons-button" open-type="contact">
  95. <text class="icon-handset"></text>客服
  96. </button>
  97. <!-- #endif -->
  98. <navigator class="icons-button" url="/pages/cart/cart2">
  99. <text class="icon-cart"></text>购物车
  100. </navigator>
  101. </view>
  102. <view class="buttons">
  103. <view class="addcart" @tap="openSkuPopup(SkuMode.Cart)"> 加入购物车 </view>
  104. <view class="buynow" @tap="openSkuPopup(SkuMode.Buy)"> 立即购买 </view>
  105. </view>
  106. </view>
  107. <!-- uni-ui 弹出层 -->
  108. <uni-popup ref="popup" type="bottom" background-color="#fff">
  109. <AddressPanel v-if="popupName === 'address'" @close="popup?.close()" />
  110. <ServicePanel v-if="popupName === 'service'" @close="popup?.close()" />
  111. </uni-popup>
  112. <!-- SKU弹窗组件 -->
  113. <vk-data-goods-sku-popup
  114. v-model="isShowSku"
  115. :localdata="localdata"
  116. :mode="mode"
  117. add-cart-background-color="#FFA868"
  118. buy-now-background-color="#27BA9B"
  119. ref="skuPopupRef"
  120. :actived-style="{
  121. color: '#27BA9B',
  122. borderColor: '#27BA9B',
  123. backgroundColor: '#E9F8F5',
  124. }"
  125. @add-cart="onAddCart"
  126. @buy-now="onBuyNow"
  127. />
  128. </template>
  129. <script setup lang="ts">
  130. import { onLoad } from '@dcloudio/uni-app'
  131. import { computed, ref } from 'vue'
  132. import type { GoodsResult } from '@/types/goods'
  133. import { getGoodsByIdAPI } from '@/services/goods'
  134. import { postMemberCartAPI } from '@/services/cart'
  135. import AddressPanel from './components/AddressPanel.vue'
  136. import ServicePanel from './components/ServicePanel.vue'
  137. import type {
  138. SkuPopupEvent,
  139. SkuPopupInstance,
  140. SkuPopupLocaldata,
  141. } from '@/components/vk-data-goods-sku-popup/vk-data-goods-sku-popup'
  142. // 获取屏幕边界到安全区域距离
  143. const { safeAreaInsets } = uni.getSystemInfoSync()
  144. // 接收页面参数
  145. const query = defineProps<{
  146. id: string
  147. }>()
  148. // 获取商品详情信息
  149. const goods = ref<GoodsResult>()
  150. const getGoodsByIdData = async () => {
  151. const res = await getGoodsByIdAPI(query.id)
  152. goods.value = res.result
  153. // SKU组件所需格式
  154. localdata.value = {
  155. _id: res.result.id,
  156. name: res.result.name,
  157. goods_thumb: res.result.mainPictures[0],
  158. spec_list: res.result.specs.map((v) => ({
  159. name: v.name,
  160. list: v.values,
  161. })),
  162. sku_list: res.result.skus.map((v) => ({
  163. _id: v.id,
  164. goods_id: res.result.id,
  165. goods_name: res.result.name,
  166. image: v.picture,
  167. price: v.price * 100, // 注意:需要乘以 100
  168. stock: v.inventory,
  169. sku_name_arr: v.specs.map((vv) => vv.valueName),
  170. })),
  171. }
  172. }
  173. // 轮播图变化时
  174. const currentIndex = ref(0)
  175. const onChange: UniHelper.SwiperOnChange = (ev) => {
  176. currentIndex.value = ev.detail.current
  177. }
  178. // 点击图片时
  179. const onTapImage = (url: string) => {
  180. // 大图预览
  181. uni.previewImage({
  182. current: url,
  183. urls: goods.value!.mainPictures,
  184. })
  185. }
  186. // uni-ui 弹出层组件 ref
  187. const popup = ref<{
  188. open: (type?: UniHelper.UniPopupType) => void
  189. close: () => void
  190. }>()
  191. // 弹出层条件渲染
  192. const popupName = ref<'address' | 'service'>()
  193. const openPopup = (name: typeof popupName.value) => {
  194. // 修改弹出层名称
  195. popupName.value = name
  196. // 打开弹出层
  197. popup.value?.open()
  198. }
  199. // 页面加载
  200. onLoad(() => {
  201. getGoodsByIdData()
  202. })
  203. // 是否显示SKU组件
  204. const isShowSku = ref(false)
  205. // 商品信息
  206. const localdata = ref({} as SkuPopupLocaldata)
  207. // 按钮模式
  208. enum SkuMode {
  209. Both = 1,
  210. Cart = 2,
  211. Buy = 3,
  212. }
  213. const mode = ref<SkuMode>(SkuMode.Both)
  214. // 打开SKU弹窗修改按钮模式
  215. const openSkuPopup = (val: SkuMode) => {
  216. // 显示SKU弹窗
  217. isShowSku.value = true
  218. // 修改按钮模式
  219. mode.value = val
  220. }
  221. // SKU组件实例
  222. const skuPopupRef = ref<SkuPopupInstance>()
  223. // 计算被选中的值
  224. const selectArrText = computed(() => {
  225. return skuPopupRef.value?.selectArr?.join(' ').trim() || '请选择商品规格'
  226. })
  227. const onAddCart = async (ev: SkuPopupEvent) => {
  228. await postMemberCartAPI({ skuId: ev._id, count: ev.buy_num })
  229. uni.showToast({ title: '添加成功' })
  230. isShowSku.value = false
  231. }
  232. // 立即购买
  233. const onBuyNow = (ev: SkuPopupEvent) => {
  234. uni.navigateTo({ url: `/pagesOrder/create/create?skuId=${ev._id}&count=${ev.buy_num}` })
  235. isShowSku.value = false
  236. }
  237. </script>
  238. <style lang="scss">
  239. page {
  240. height: 100%;
  241. overflow: hidden;
  242. display: flex;
  243. flex-direction: column;
  244. }
  245. .viewport {
  246. background-color: #f4f4f4;
  247. }
  248. .panel {
  249. margin-top: 20rpx;
  250. background-color: #fff;
  251. .title {
  252. display: flex;
  253. justify-content: space-between;
  254. align-items: center;
  255. height: 90rpx;
  256. line-height: 1;
  257. padding: 30rpx 60rpx 30rpx 6rpx;
  258. position: relative;
  259. text {
  260. padding-left: 10rpx;
  261. font-size: 28rpx;
  262. color: #333;
  263. font-weight: 600;
  264. border-left: 4rpx solid #27ba9b;
  265. }
  266. navigator {
  267. font-size: 24rpx;
  268. color: #666;
  269. }
  270. }
  271. }
  272. .arrow {
  273. &::after {
  274. position: absolute;
  275. top: 50%;
  276. right: 30rpx;
  277. content: '\e6c2';
  278. color: #ccc;
  279. font-family: 'erabbit' !important;
  280. font-size: 32rpx;
  281. transform: translateY(-50%);
  282. }
  283. }
  284. /* 商品信息 */
  285. .goods {
  286. background-color: #fff;
  287. .preview {
  288. height: 750rpx;
  289. position: relative;
  290. .image {
  291. width: 750rpx;
  292. height: 750rpx;
  293. }
  294. .indicator {
  295. height: 40rpx;
  296. padding: 0 24rpx;
  297. line-height: 40rpx;
  298. border-radius: 30rpx;
  299. color: #fff;
  300. font-family: Arial, Helvetica, sans-serif;
  301. background-color: rgba(0, 0, 0, 0.3);
  302. position: absolute;
  303. bottom: 30rpx;
  304. right: 30rpx;
  305. .current {
  306. font-size: 26rpx;
  307. }
  308. .split {
  309. font-size: 24rpx;
  310. margin: 0 1rpx 0 2rpx;
  311. }
  312. .total {
  313. font-size: 24rpx;
  314. }
  315. }
  316. }
  317. .meta {
  318. position: relative;
  319. border-bottom: 1rpx solid #eaeaea;
  320. .price {
  321. height: 130rpx;
  322. padding: 25rpx 30rpx 0;
  323. color: #fff;
  324. font-size: 34rpx;
  325. box-sizing: border-box;
  326. background-color: #35c8a9;
  327. }
  328. .number {
  329. font-size: 56rpx;
  330. }
  331. .brand {
  332. width: 160rpx;
  333. height: 80rpx;
  334. overflow: hidden;
  335. position: absolute;
  336. top: 26rpx;
  337. right: 30rpx;
  338. }
  339. .name {
  340. max-height: 88rpx;
  341. line-height: 1.4;
  342. margin: 20rpx;
  343. font-size: 32rpx;
  344. color: #333;
  345. }
  346. .desc {
  347. line-height: 1;
  348. padding: 0 20rpx 30rpx;
  349. font-size: 24rpx;
  350. color: #cf4444;
  351. }
  352. }
  353. .action {
  354. padding-left: 20rpx;
  355. .item {
  356. height: 90rpx;
  357. padding-right: 60rpx;
  358. border-bottom: 1rpx solid #eaeaea;
  359. font-size: 26rpx;
  360. color: #333;
  361. position: relative;
  362. display: flex;
  363. align-items: center;
  364. &:last-child {
  365. border-bottom: 0 none;
  366. }
  367. }
  368. .label {
  369. width: 60rpx;
  370. color: #898b94;
  371. margin: 0 16rpx 0 10rpx;
  372. }
  373. .text {
  374. flex: 1;
  375. -webkit-line-clamp: 1;
  376. }
  377. }
  378. }
  379. /* 商品详情 */
  380. .detail {
  381. padding-left: 20rpx;
  382. .content {
  383. margin-left: -20rpx;
  384. .image {
  385. width: 100%;
  386. }
  387. }
  388. .properties {
  389. padding: 0 20rpx;
  390. margin-bottom: 30rpx;
  391. .item {
  392. display: flex;
  393. line-height: 2;
  394. padding: 10rpx;
  395. font-size: 26rpx;
  396. color: #333;
  397. border-bottom: 1rpx dashed #ccc;
  398. }
  399. .label {
  400. width: 200rpx;
  401. }
  402. .value {
  403. flex: 1;
  404. }
  405. }
  406. }
  407. /* 同类推荐 */
  408. .similar {
  409. .content {
  410. padding: 0 20rpx 20rpx;
  411. background-color: #f4f4f4;
  412. display: flex;
  413. flex-wrap: wrap;
  414. .goods {
  415. width: 340rpx;
  416. padding: 24rpx 20rpx 20rpx;
  417. margin: 20rpx 7rpx;
  418. border-radius: 10rpx;
  419. background-color: #fff;
  420. }
  421. .image {
  422. width: 300rpx;
  423. height: 260rpx;
  424. }
  425. .name {
  426. height: 80rpx;
  427. margin: 10rpx 0;
  428. font-size: 26rpx;
  429. color: #262626;
  430. }
  431. .price {
  432. line-height: 1;
  433. font-size: 20rpx;
  434. color: #cf4444;
  435. }
  436. .number {
  437. font-size: 26rpx;
  438. margin-left: 2rpx;
  439. }
  440. }
  441. navigator {
  442. &:nth-child(even) {
  443. margin-right: 0;
  444. }
  445. }
  446. }
  447. /* 底部工具栏 */
  448. .toolbar {
  449. position: fixed;
  450. left: 0;
  451. right: 0;
  452. bottom: calc((var(--window-bottom)));
  453. z-index: 1;
  454. background-color: #fff;
  455. height: 100rpx;
  456. padding: 0 20rpx;
  457. border-top: 1rpx solid #eaeaea;
  458. display: flex;
  459. justify-content: space-between;
  460. align-items: center;
  461. box-sizing: content-box;
  462. .buttons {
  463. display: flex;
  464. & > view {
  465. width: 220rpx;
  466. text-align: center;
  467. line-height: 72rpx;
  468. font-size: 26rpx;
  469. color: #fff;
  470. border-radius: 72rpx;
  471. }
  472. .addcart {
  473. background-color: #ffa868;
  474. }
  475. .buynow,
  476. .payment {
  477. background-color: #27ba9b;
  478. margin-left: 20rpx;
  479. }
  480. }
  481. .icons {
  482. padding-right: 20rpx;
  483. display: flex;
  484. align-items: center;
  485. flex: 1;
  486. // 兼容 H5 端和 App 端的导航链接样式
  487. .navigator-wrap,
  488. .icons-button {
  489. flex: 1;
  490. text-align: center;
  491. line-height: 1.4;
  492. padding: 0;
  493. margin: 0;
  494. border-radius: 0;
  495. font-size: 20rpx;
  496. color: #333;
  497. background-color: #fff;
  498. &::after {
  499. border: none;
  500. }
  501. }
  502. text {
  503. display: block;
  504. font-size: 34rpx;
  505. }
  506. }
  507. }
  508. </style>