detail.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633
  1. <template>
  2. <view class="product-detail">
  3. <!-- 商品图片轮播 -->
  4. <view class="product-images">
  5. <swiper
  6. class="image-swiper"
  7. :indicator-dots="true"
  8. :autoplay="false"
  9. indicator-color="rgba(255,255,255,0.5)"
  10. indicator-active-color="#FF6600"
  11. @change="onImageChange"
  12. >
  13. <swiper-item v-for="(image, index) in productImages" :key="index">
  14. <image :src="image.url" class="product-image" mode="aspectFill"></image>
  15. </swiper-item>
  16. </swiper>
  17. <!-- 图片指示器 -->
  18. <view class="image-indicator">{{ currentImage + 1 }}/{{ productImages.length }}</view>
  19. </view>
  20. <!-- 商品基本信息 -->
  21. <view class="product-info">
  22. <view class="product-price">
  23. <text class="price-symbol">¥</text>
  24. <text class="price-value">{{ productDetail.price }}</text>
  25. <view class="points-badge">
  26. <text class="points-text">+{{ productDetail.points }}工分</text>
  27. </view>
  28. </view>
  29. <view class="product-name">{{ productDetail.name }}</view>
  30. <view class="delivery-info">
  31. <text class="delivery-text">送至: {{ deliveryAddress }}</text>
  32. <text class="delivery-arrow">></text>
  33. </view>
  34. </view>
  35. <!-- 商品特点模块 -->
  36. <view class="product-features">
  37. <view class="section-header">
  38. <text class="section-title">商品特点</text>
  39. <text class="section-subtitle">品质用心展示</text>
  40. </view>
  41. <!-- 商品展示图 -->
  42. <view class="feature-showcase">
  43. <view class="showcase-left">
  44. <image :src="productDetail.packageImage" class="package-image" mode="aspectFit"></image>
  45. </view>
  46. <view class="showcase-right">
  47. <image :src="productDetail.mainImage" class="main-product-image" mode="aspectFit"></image>
  48. </view>
  49. </view>
  50. <!-- 产品信息表格 -->
  51. <view class="product-specs">
  52. <view class="spec-item" v-for="(spec, key) in productSpecs" :key="key">
  53. <text class="spec-label">{{ spec.label }}</text>
  54. <text class="spec-value">{{ spec.value }}</text>
  55. </view>
  56. </view>
  57. </view>
  58. <!-- 品质与价值模块 -->
  59. <view class="quality-section">
  60. <view class="section-header">
  61. <text class="section-title">品质与价值</text>
  62. <text class="quality-arrow">↓</text>
  63. </view>
  64. <view class="quality-content">
  65. <view class="quality-text">
  66. <text class="quality-title">茅台品质</text>
  67. <text class="quality-description">{{ productDetail.qualityDescription }}</text>
  68. </view>
  69. <view class="quality-image">
  70. <image :src="productDetail.qualityImage" class="quality-product-image" mode="aspectFit"></image>
  71. </view>
  72. </view>
  73. </view>
  74. <!-- 底部购买按钮 -->
  75. <view class="bottom-actions">
  76. <view class="action-buttons">
  77. <button class="action-btn add-to-cart" @click="addToCart">
  78. <text class="btn-text">加入购物车</text>
  79. </button>
  80. <button class="action-btn buy-now" @click="buyNow">
  81. <text class="btn-text">立即购买</text>
  82. </button>
  83. </view>
  84. </view>
  85. <!-- 加载状态 -->
  86. <view v-if="loading" class="loading-container">
  87. <view class="loading-content">
  88. <view class="loading-spinner"></view>
  89. <text class="loading-text">{{ loadingText }}</text>
  90. </view>
  91. </view>
  92. </view>
  93. </template>
  94. <script>
  95. import { isLoggedIn, requireAuth, PROTECTED_ACTIONS } from '@/utils/auth.js'
  96. import { getProductDetail, getProductImages } from '@/api/product.js'
  97. export default {
  98. name: 'ProductDetail',
  99. data() {
  100. return {
  101. productId: '',
  102. loading: true,
  103. loadingText: '加载中...',
  104. currentImage: 0,
  105. // 商品图片
  106. productImages: [],
  107. // 商品详情
  108. productDetail: {
  109. name: '',
  110. price: 0,
  111. points: 0,
  112. mainImage: '',
  113. packageImage: '',
  114. qualityImage: '',
  115. qualityDescription: ''
  116. },
  117. // 商品规格信息
  118. productSpecs: {},
  119. // 配送地址
  120. deliveryAddress: '北京市大兴区上德中心'
  121. }
  122. },
  123. onLoad(options) {
  124. if (options.id) {
  125. this.productId = options.id
  126. this.loadProductDetail()
  127. } else {
  128. uni.showToast({
  129. title: '商品ID无效',
  130. icon: 'none'
  131. })
  132. setTimeout(() => {
  133. uni.navigateBack()
  134. }, 1500)
  135. }
  136. },
  137. methods: {
  138. /**
  139. * 加载商品详情
  140. */
  141. async loadProductDetail() {
  142. try {
  143. this.loading = true
  144. this.loadingText = '加载商品详情...'
  145. console.log('🔄 开始加载商品详情:', this.productId)
  146. // 并行加载商品详情和图片
  147. const [detailResponse, imagesResponse] = await Promise.all([
  148. getProductDetail(this.productId),
  149. getProductImages(this.productId, 2) // 图片分类==2
  150. ])
  151. if (detailResponse.success) {
  152. this.productDetail = {
  153. name: detailResponse.data.name,
  154. price: detailResponse.data.price,
  155. points: detailResponse.data.points || 400,
  156. mainImage: detailResponse.data.mainImage,
  157. packageImage: detailResponse.data.packageImage,
  158. qualityImage: detailResponse.data.qualityImage,
  159. qualityDescription: detailResponse.data.qualityDescription || '采用高温制曲、二次投料,一年为一个生产周期,具有独特的酿造工艺。'
  160. }
  161. // 构建商品规格信息
  162. this.productSpecs = {
  163. name: {
  164. label: '产品名称',
  165. value: detailResponse.data.name
  166. },
  167. material: {
  168. label: '原料',
  169. value: detailResponse.data.material || '水、高粱、小麦'
  170. },
  171. alcohol: {
  172. label: '酒精度数',
  173. value: detailResponse.data.alcohol || '53%vol.'
  174. },
  175. origin: {
  176. label: '厂址',
  177. value: detailResponse.data.origin || '贵州省仁怀市茅台镇'
  178. },
  179. type: {
  180. label: '酒品类型',
  181. value: detailResponse.data.type || '酱香型白酒'
  182. },
  183. capacity: {
  184. label: '容量',
  185. value: detailResponse.data.capacity || '500ml'
  186. },
  187. storage: {
  188. label: '储存条件',
  189. value: detailResponse.data.storage || '干燥、通风、阴凉的环境条件下存储'
  190. },
  191. company: {
  192. label: '公司',
  193. value: detailResponse.data.company || '贵州茅台酒股份有限公司'
  194. }
  195. }
  196. console.log('✅ 商品详情加载成功')
  197. } else {
  198. throw new Error(detailResponse.message || '商品详情加载失败')
  199. }
  200. if (imagesResponse.success) {
  201. // 限制最多6张图片
  202. this.productImages = imagesResponse.data.images.slice(0, 6).map(img => ({
  203. url: img.url,
  204. id: img.id
  205. }))
  206. console.log('✅ 商品图片加载成功,共', this.productImages.length, '张')
  207. } else {
  208. console.warn('⚠️ 商品图片加载失败,使用默认图片')
  209. // 使用默认图片
  210. this.productImages = [{
  211. url: this.productDetail.mainImage,
  212. id: 'default'
  213. }]
  214. }
  215. } catch (error) {
  216. console.error('❌ 商品详情加载失败:', error)
  217. uni.showToast({
  218. title: '商品详情加载失败',
  219. icon: 'none',
  220. duration: 2000
  221. })
  222. } finally {
  223. this.loading = false
  224. }
  225. },
  226. /**
  227. * 图片轮播切换事件
  228. */
  229. onImageChange(e) {
  230. this.currentImage = e.detail.current
  231. },
  232. /**
  233. * 加入购物车
  234. */
  235. addToCart() {
  236. requireAuth(() => {
  237. console.log('🛒 加入购物车:', this.productId)
  238. // TODO: 实现加入购物车逻辑
  239. uni.showToast({
  240. title: '已加入购物车',
  241. icon: 'success'
  242. })
  243. }, {
  244. action: PROTECTED_ACTIONS.ADD_TO_CART,
  245. returnUrl: `/pages/product/detail?id=${this.productId}`
  246. })
  247. },
  248. /**
  249. * 立即购买
  250. */
  251. buyNow() {
  252. requireAuth(() => {
  253. console.log('🛒 立即购买:', this.productId)
  254. // TODO: 实现立即购买逻辑
  255. uni.navigateTo({
  256. url: `/pages/order/confirm?productId=${this.productId}`
  257. })
  258. }, {
  259. action: PROTECTED_ACTIONS.BUY_NOW,
  260. returnUrl: `/pages/product/detail?id=${this.productId}`
  261. })
  262. }
  263. }
  264. }
  265. </script>
  266. <style lang="scss" scoped>
  267. .product-detail {
  268. min-height: 100vh;
  269. background: #f8f8f8;
  270. padding-bottom: 120rpx; /* 为底部按钮留出空间 */
  271. }
  272. /* 商品图片轮播 */
  273. .product-images {
  274. position: relative;
  275. height: 600rpx;
  276. background: #ffffff;
  277. .image-swiper {
  278. width: 100%;
  279. height: 100%;
  280. .product-image {
  281. width: 100%;
  282. height: 100%;
  283. object-fit: cover;
  284. }
  285. }
  286. .image-indicator {
  287. position: absolute;
  288. bottom: 30rpx;
  289. right: 30rpx;
  290. background: rgba(0, 0, 0, 0.7);
  291. color: #ffffff;
  292. padding: 8rpx 16rpx;
  293. border-radius: 20rpx;
  294. font-size: 24rpx;
  295. z-index: 10;
  296. }
  297. }
  298. /* 商品基本信息 */
  299. .product-info {
  300. background: #ffffff;
  301. padding: 30rpx;
  302. margin-bottom: 20rpx;
  303. .product-price {
  304. display: flex;
  305. align-items: center;
  306. margin-bottom: 20rpx;
  307. .price-symbol {
  308. font-size: 32rpx;
  309. color: #ff4757;
  310. font-weight: bold;
  311. margin-right: 8rpx;
  312. }
  313. .price-value {
  314. font-size: 48rpx;
  315. color: #ff4757;
  316. font-weight: bold;
  317. margin-right: 20rpx;
  318. }
  319. .points-badge {
  320. background: #FF6600;
  321. padding: 8rpx 16rpx;
  322. border-radius: 20rpx;
  323. .points-text {
  324. color: #ffffff;
  325. font-size: 24rpx;
  326. font-weight: bold;
  327. }
  328. }
  329. }
  330. .product-name {
  331. font-size: 36rpx;
  332. color: #333;
  333. font-weight: bold;
  334. margin-bottom: 20rpx;
  335. line-height: 1.4;
  336. }
  337. .delivery-info {
  338. display: flex;
  339. align-items: center;
  340. justify-content: space-between;
  341. .delivery-text {
  342. font-size: 28rpx;
  343. color: #666;
  344. }
  345. .delivery-arrow {
  346. font-size: 24rpx;
  347. color: #999;
  348. }
  349. }
  350. }
  351. /* 商品特点模块 */
  352. .product-features {
  353. background: #ffffff;
  354. margin-bottom: 20rpx;
  355. .section-header {
  356. padding: 30rpx 30rpx 20rpx;
  357. border-bottom: 1rpx solid #f0f0f0;
  358. .section-title {
  359. display: block;
  360. font-size: 36rpx;
  361. color: #333;
  362. font-weight: bold;
  363. margin-bottom: 10rpx;
  364. }
  365. .section-subtitle {
  366. font-size: 26rpx;
  367. color: #999;
  368. }
  369. }
  370. .feature-showcase {
  371. display: flex;
  372. padding: 30rpx;
  373. gap: 30rpx;
  374. .showcase-left,
  375. .showcase-right {
  376. flex: 1;
  377. height: 200rpx;
  378. background: #f8f8f8;
  379. border-radius: 12rpx;
  380. overflow: hidden;
  381. .package-image,
  382. .main-product-image {
  383. width: 100%;
  384. height: 100%;
  385. object-fit: contain;
  386. }
  387. }
  388. }
  389. .product-specs {
  390. padding: 0 30rpx 30rpx;
  391. .spec-item {
  392. display: flex;
  393. padding: 20rpx 0;
  394. border-bottom: 1rpx solid #f5f5f5;
  395. &:last-child {
  396. border-bottom: none;
  397. }
  398. .spec-label {
  399. width: 200rpx;
  400. font-size: 28rpx;
  401. color: #666;
  402. flex-shrink: 0;
  403. }
  404. .spec-value {
  405. flex: 1;
  406. font-size: 28rpx;
  407. color: #333;
  408. line-height: 1.4;
  409. }
  410. }
  411. }
  412. }
  413. /* 品质与价值模块 */
  414. .quality-section {
  415. background: #ffffff;
  416. margin-bottom: 20rpx;
  417. .section-header {
  418. padding: 30rpx 30rpx 20rpx;
  419. border-bottom: 1rpx solid #f0f0f0;
  420. display: flex;
  421. align-items: center;
  422. justify-content: space-between;
  423. .section-title {
  424. font-size: 36rpx;
  425. color: #333;
  426. font-weight: bold;
  427. }
  428. .quality-arrow {
  429. font-size: 24rpx;
  430. color: #999;
  431. }
  432. }
  433. .quality-content {
  434. display: flex;
  435. padding: 30rpx;
  436. gap: 30rpx;
  437. .quality-text {
  438. flex: 1;
  439. .quality-title {
  440. display: block;
  441. font-size: 32rpx;
  442. color: #333;
  443. font-weight: bold;
  444. margin-bottom: 20rpx;
  445. }
  446. .quality-description {
  447. font-size: 28rpx;
  448. color: #666;
  449. line-height: 1.6;
  450. }
  451. }
  452. .quality-image {
  453. width: 200rpx;
  454. height: 200rpx;
  455. background: #f8f8f8;
  456. border-radius: 12rpx;
  457. overflow: hidden;
  458. .quality-product-image {
  459. width: 100%;
  460. height: 100%;
  461. object-fit: contain;
  462. }
  463. }
  464. }
  465. }
  466. /* 底部购买按钮 - 重新设计布局 */
  467. .bottom-actions {
  468. position: fixed;
  469. bottom: 0;
  470. left: 0;
  471. right: 0;
  472. background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
  473. padding: 20rpx 24rpx;
  474. box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.12);
  475. border-top: 1rpx solid #dee2e6;
  476. .action-buttons {
  477. display: flex;
  478. justify-content: flex-end;
  479. align-items: center;
  480. width: 100%;
  481. gap: 5%;
  482. }
  483. .action-btn {
  484. width: 45%;
  485. height: 80rpx;
  486. border: 2rpx solid transparent;
  487. border-radius: 40rpx;
  488. font-size: 28rpx;
  489. font-weight: 700;
  490. position: relative;
  491. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  492. flex-shrink: 0;
  493. &.add-to-cart {
  494. background: linear-gradient(135deg, #dc3545 0%, #dc3545 100%);
  495. color: #ffffff;
  496. border-color: #dc3545;
  497. &:active {
  498. background: linear-gradient(135deg, #218838 0%, #1ea085 100%);
  499. transform: scale(0.95);
  500. }
  501. }
  502. &.buy-now {
  503. background: linear-gradient(135deg, #dc3545 0%, #fd7e14 100%);
  504. color: #ffffff;
  505. border-color: #dc3545;
  506. &:active {
  507. background: linear-gradient(135deg, #c82333 0%, #e55a00 100%);
  508. transform: scale(0.95);
  509. }
  510. }
  511. &:active {
  512. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
  513. }
  514. .btn-text {
  515. position: relative;
  516. z-index: 1;
  517. text-shadow: 0 1rpx 2rpx rgba(0, 0, 0, 0.2);
  518. }
  519. }
  520. }
  521. /* 加载状态 */
  522. .loading-container {
  523. position: fixed;
  524. top: 0;
  525. left: 0;
  526. right: 0;
  527. bottom: 0;
  528. background: rgba(255, 255, 255, 0.95);
  529. display: flex;
  530. align-items: center;
  531. justify-content: center;
  532. z-index: 9999;
  533. .loading-content {
  534. display: flex;
  535. flex-direction: column;
  536. align-items: center;
  537. .loading-spinner {
  538. width: 60rpx;
  539. height: 60rpx;
  540. border: 4rpx solid #f3f3f3;
  541. border-top: 4rpx solid #FF6600;
  542. border-radius: 50%;
  543. animation: loading-spin 1s linear infinite;
  544. margin-bottom: 20rpx;
  545. }
  546. .loading-text {
  547. font-size: 28rpx;
  548. color: #666;
  549. }
  550. }
  551. }
  552. @keyframes loading-spin {
  553. 0% { transform: rotate(0deg); }
  554. 100% { transform: rotate(360deg); }
  555. }
  556. </style>