product-detail.vue 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. <template>
  2. <view class="product-detail">
  3. <!-- 顶部导航栏 -->
  4. <view class="header">
  5. <view class="back-icon" @click="handleBack"></view>
  6. <text class="header-title">商品详情</text>
  7. <view class="header-right"></view>
  8. </view>
  9. <!-- 商品图片 -->
  10. <view class="product-image-section">
  11. <image :src="product.imageUrl || '/static/default-product.png'" mode="aspectFit" class="product-image"></image>
  12. </view>
  13. <!-- 商品信息 -->
  14. <view class="product-info-section">
  15. <view class="product-name">{{ product.name }}</view>
  16. <view class="product-price">
  17. <text class="price-value">{{ product.pointsPrice }}</text>
  18. <text class="price-unit">积分</text>
  19. </view>
  20. <view class="product-stock">库存: {{ product.stock || 0 }} 件</view>
  21. </view>
  22. <!-- 商品描述 -->
  23. <view class="product-desc-section">
  24. <view class="section-title">商品描述</view>
  25. <view class="desc-content">{{ product.description || '暂无描述' }}</view>
  26. </view>
  27. <!-- 联系信息 -->
  28. <view class="receiver-section">
  29. <view class="section-title">联系信息</view>
  30. <view class="form-item">
  31. <text class="label">联系电话</text>
  32. <input class="input" v-model="contactPhone" placeholder="请输入联系电话" type="number" maxlength="11" />
  33. </view>
  34. <view class="tips">提交后将由后台审核,审核通过后会与您联系</view>
  35. </view>
  36. <!-- 底部操作栏 -->
  37. <view class="bottom-bar">
  38. <view class="points-info">
  39. <text class="label">我的积分:</text>
  40. <text class="value">{{ userPoints }}</text>
  41. </view>
  42. <view class="exchange-btn" :class="{ disabled: !canExchange }" @click="handleExchange">
  43. 立即兑换
  44. </view>
  45. </view>
  46. </view>
  47. </template>
  48. <script>
  49. import api from '@/utils/api.js'
  50. export default {
  51. name: 'product-detail',
  52. data() {
  53. return {
  54. productId: null,
  55. product: {},
  56. userPoints: 0,
  57. makerId: null,
  58. contactPhone: '',
  59. loading: false
  60. }
  61. },
  62. computed: {
  63. canExchange() {
  64. if (this.loading) return false
  65. if (!this.product.pointsPrice) return false
  66. if (this.userPoints < this.product.pointsPrice) return false
  67. if (this.product.stock <= 0) return false
  68. // 需要填写联系电话
  69. if (!this.contactPhone || this.contactPhone.length !== 11) {
  70. return false
  71. }
  72. return true
  73. }
  74. },
  75. onLoad(options) {
  76. if (options.id) {
  77. this.productId = options.id
  78. }
  79. this.initData()
  80. },
  81. methods: {
  82. async initData() {
  83. // 获取红娘ID
  84. const userInfo = uni.getStorageSync('userInfo')
  85. if (userInfo && userInfo.matchmakerId) {
  86. this.makerId = userInfo.matchmakerId
  87. } else if (userInfo && userInfo.userId) {
  88. this.makerId = userInfo.userId
  89. }
  90. await Promise.all([
  91. this.loadProduct(),
  92. this.loadBalance()
  93. ])
  94. },
  95. async loadProduct() {
  96. if (!this.productId) return
  97. try {
  98. const res = await api.pointsMall.getProductDetail(this.productId)
  99. this.product = res || {}
  100. } catch (e) {
  101. console.error('获取商品详情失败:', e)
  102. uni.showToast({ title: '获取商品详情失败', icon: 'none' })
  103. }
  104. },
  105. async loadBalance() {
  106. if (!this.makerId) return
  107. try {
  108. const res = await api.pointsMall.getBalance(this.makerId)
  109. this.userPoints = res.balance || 0
  110. } catch (e) {
  111. console.error('获取积分余额失败:', e)
  112. }
  113. },
  114. handleBack() {
  115. uni.navigateBack()
  116. },
  117. async handleExchange() {
  118. if (!this.canExchange) {
  119. if (this.userPoints < this.product.pointsPrice) {
  120. uni.showToast({ title: '积分不足', icon: 'none' })
  121. } else if (this.product.stock <= 0) {
  122. uni.showToast({ title: '库存不足', icon: 'none' })
  123. } else if (!this.contactPhone || this.contactPhone.length !== 11) {
  124. uni.showToast({ title: '请输入正确的11位手机号', icon: 'none' })
  125. }
  126. return
  127. }
  128. uni.showModal({
  129. title: '确认兑换',
  130. content: `确定使用 ${this.product.pointsPrice} 积分兑换「${this.product.name}」吗?`,
  131. success: async (res) => {
  132. if (res.confirm) {
  133. await this.doExchange()
  134. }
  135. }
  136. })
  137. },
  138. async doExchange() {
  139. this.loading = true
  140. try {
  141. const data = {
  142. makerId: this.makerId,
  143. productId: this.productId,
  144. quantity: 1,
  145. contactPhone: this.contactPhone
  146. }
  147. await api.pointsMall.exchange(data)
  148. uni.showToast({ title: '提交成功,等待审核', icon: 'success' })
  149. // 刷新积分余额
  150. await this.loadBalance()
  151. // 延迟返回
  152. setTimeout(() => {
  153. uni.navigateBack()
  154. }, 1500)
  155. } catch (e) {
  156. console.error('兑换失败:', e)
  157. uni.showToast({ title: e.message || '兑换失败', icon: 'none' })
  158. } finally {
  159. this.loading = false
  160. }
  161. }
  162. }
  163. }
  164. </script>
  165. <style lang="scss" scoped>
  166. .product-detail {
  167. min-height: 100vh;
  168. background: #F5F5F5;
  169. padding-bottom: 120rpx;
  170. }
  171. .header {
  172. display: flex;
  173. align-items: center;
  174. justify-content: space-between;
  175. padding: 25rpx 30rpx;
  176. padding-top: calc(25rpx + env(safe-area-inset-top));
  177. background: #FFFFFF;
  178. .back-icon {
  179. width: 44rpx;
  180. height: 44rpx;
  181. display: flex;
  182. align-items: center;
  183. justify-content: center;
  184. font-size: 32rpx;
  185. color: #333;
  186. &::before { content: '‹'; }
  187. }
  188. .header-title {
  189. font-size: 36rpx;
  190. font-weight: bold;
  191. color: #333;
  192. }
  193. .header-right {
  194. width: 44rpx;
  195. }
  196. }
  197. .product-image-section {
  198. width: 100%;
  199. height: 600rpx;
  200. background: #FFFFFF;
  201. display: flex;
  202. align-items: center;
  203. justify-content: center;
  204. .product-image {
  205. width: 100%;
  206. height: 100%;
  207. object-fit: contain;
  208. }
  209. }
  210. .product-info-section {
  211. background: #FFFFFF;
  212. padding: 30rpx;
  213. margin-top: 20rpx;
  214. .product-name {
  215. font-size: 36rpx;
  216. font-weight: bold;
  217. color: #333;
  218. margin-bottom: 20rpx;
  219. }
  220. .product-price {
  221. display: flex;
  222. align-items: baseline;
  223. margin-bottom: 15rpx;
  224. .price-value {
  225. font-size: 48rpx;
  226. font-weight: bold;
  227. color: #9C27B0;
  228. }
  229. .price-unit {
  230. font-size: 28rpx;
  231. color: #9C27B0;
  232. margin-left: 10rpx;
  233. }
  234. }
  235. .product-stock {
  236. font-size: 26rpx;
  237. color: #999;
  238. }
  239. }
  240. .product-desc-section {
  241. background: #FFFFFF;
  242. padding: 30rpx;
  243. margin-top: 20rpx;
  244. .section-title {
  245. font-size: 30rpx;
  246. font-weight: bold;
  247. color: #333;
  248. margin-bottom: 20rpx;
  249. }
  250. .desc-content {
  251. font-size: 28rpx;
  252. color: #666;
  253. line-height: 1.6;
  254. }
  255. }
  256. .receiver-section {
  257. background: #FFFFFF;
  258. padding: 30rpx;
  259. margin-top: 20rpx;
  260. .section-title {
  261. font-size: 30rpx;
  262. font-weight: bold;
  263. color: #333;
  264. margin-bottom: 20rpx;
  265. }
  266. .form-item {
  267. display: flex;
  268. align-items: center;
  269. margin-bottom: 20rpx;
  270. .label {
  271. width: 150rpx;
  272. font-size: 28rpx;
  273. color: #333;
  274. }
  275. .input {
  276. flex: 1;
  277. height: 70rpx;
  278. background: #F5F5F5;
  279. border-radius: 10rpx;
  280. padding: 0 20rpx;
  281. font-size: 28rpx;
  282. }
  283. }
  284. .tips {
  285. font-size: 24rpx;
  286. color: #999;
  287. margin-top: 10rpx;
  288. padding: 15rpx;
  289. background: #FFF8E1;
  290. border-radius: 10rpx;
  291. line-height: 1.5;
  292. }
  293. }
  294. .bottom-bar {
  295. position: fixed;
  296. bottom: 0;
  297. left: 0;
  298. right: 0;
  299. height: 100rpx;
  300. background: #FFFFFF;
  301. display: flex;
  302. align-items: center;
  303. justify-content: space-between;
  304. padding: 0 30rpx;
  305. padding-bottom: env(safe-area-inset-bottom);
  306. box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
  307. .points-info {
  308. display: flex;
  309. align-items: center;
  310. .label {
  311. font-size: 26rpx;
  312. color: #666;
  313. }
  314. .value {
  315. font-size: 32rpx;
  316. font-weight: bold;
  317. color: #9C27B0;
  318. margin-left: 10rpx;
  319. }
  320. }
  321. .exchange-btn {
  322. padding: 20rpx 60rpx;
  323. background: linear-gradient(135deg, #9C27B0 0%, #E91E63 100%);
  324. color: #FFFFFF;
  325. font-size: 30rpx;
  326. font-weight: bold;
  327. border-radius: 50rpx;
  328. &.disabled {
  329. background: #CCCCCC;
  330. }
  331. }
  332. }
  333. </style>