feat(productDetail): 新增商品详情页及相关组件
添加商品详情页面,包含图片轮播、基本信息展示、车辆评估、卖家信息等功能模块 引入支付组件payCard,实现订单支付流程 更新app.config.js添加新页面路由 优化多个页面的收藏图标统一使用Heart1组件 新增底部操作栏样式和深色模式适配
Showing
12 changed files
with
1145 additions
and
21 deletions
| ... | @@ -8,10 +8,12 @@ export {} | ... | @@ -8,10 +8,12 @@ export {} |
| 8 | declare module 'vue' { | 8 | declare module 'vue' { |
| 9 | export interface GlobalComponents { | 9 | export interface GlobalComponents { |
| 10 | NavBar: typeof import('./src/components/navBar.vue')['default'] | 10 | NavBar: typeof import('./src/components/navBar.vue')['default'] |
| 11 | + NutActionSheet: typeof import('@nutui/nutui-taro')['ActionSheet'] | ||
| 11 | NutButton: typeof import('@nutui/nutui-taro')['Button'] | 12 | NutButton: typeof import('@nutui/nutui-taro')['Button'] |
| 12 | NutCol: typeof import('@nutui/nutui-taro')['Col'] | 13 | NutCol: typeof import('@nutui/nutui-taro')['Col'] |
| 13 | NutConfigProvider: typeof import('@nutui/nutui-taro')['ConfigProvider'] | 14 | NutConfigProvider: typeof import('@nutui/nutui-taro')['ConfigProvider'] |
| 14 | NutDatePicker: typeof import('@nutui/nutui-taro')['DatePicker'] | 15 | NutDatePicker: typeof import('@nutui/nutui-taro')['DatePicker'] |
| 16 | + NutDialog: typeof import('@nutui/nutui-taro')['Dialog'] | ||
| 15 | NutForm: typeof import('@nutui/nutui-taro')['Form'] | 17 | NutForm: typeof import('@nutui/nutui-taro')['Form'] |
| 16 | NutFormItem: typeof import('@nutui/nutui-taro')['FormItem'] | 18 | NutFormItem: typeof import('@nutui/nutui-taro')['FormItem'] |
| 17 | NutImagePreview: typeof import('@nutui/nutui-taro')['ImagePreview'] | 19 | NutImagePreview: typeof import('@nutui/nutui-taro')['ImagePreview'] |
| ... | @@ -32,6 +34,7 @@ declare module 'vue' { | ... | @@ -32,6 +34,7 @@ declare module 'vue' { |
| 32 | NutTabs: typeof import('@nutui/nutui-taro')['Tabs'] | 34 | NutTabs: typeof import('@nutui/nutui-taro')['Tabs'] |
| 33 | NutTextarea: typeof import('@nutui/nutui-taro')['Textarea'] | 35 | NutTextarea: typeof import('@nutui/nutui-taro')['Textarea'] |
| 34 | NutToast: typeof import('@nutui/nutui-taro')['Toast'] | 36 | NutToast: typeof import('@nutui/nutui-taro')['Toast'] |
| 37 | + PayCard: typeof import('./src/components/payCard.vue')['default'] | ||
| 35 | Picker: typeof import('./src/components/time-picker-data/picker.vue')['default'] | 38 | Picker: typeof import('./src/components/time-picker-data/picker.vue')['default'] |
| 36 | PosterBuilder: typeof import('./src/components/PosterBuilder/index.vue')['default'] | 39 | PosterBuilder: typeof import('./src/components/PosterBuilder/index.vue')['default'] |
| 37 | RouterLink: typeof import('vue-router')['RouterLink'] | 40 | RouterLink: typeof import('vue-router')['RouterLink'] | ... | ... |
| 1 | /* | 1 | /* |
| 2 | * @Date: 2025-06-28 10:33:00 | 2 | * @Date: 2025-06-28 10:33:00 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2025-07-02 22:18:10 | 4 | + * @LastEditTime: 2025-07-03 09:34:59 |
| 5 | * @FilePath: /jgdl/src/app.config.js | 5 | * @FilePath: /jgdl/src/app.config.js |
| 6 | * @Description: 文件描述 | 6 | * @Description: 文件描述 |
| 7 | */ | 7 | */ |
| ... | @@ -18,6 +18,7 @@ export default { | ... | @@ -18,6 +18,7 @@ export default { |
| 18 | 'pages/setAuthCar/index', | 18 | 'pages/setAuthCar/index', |
| 19 | 'pages/newCarList/index', | 19 | 'pages/newCarList/index', |
| 20 | 'pages/goodCarList/index', | 20 | 'pages/goodCarList/index', |
| 21 | + 'pages/productDetail/index', | ||
| 21 | 'pages/auth/index', | 22 | 'pages/auth/index', |
| 22 | ], | 23 | ], |
| 23 | subpackages: [ // 配置在tabBar中的页面不能分包写到subpackages中去 | 24 | subpackages: [ // 配置在tabBar中的页面不能分包写到subpackages中去 | ... | ... |
| ... | @@ -13,3 +13,28 @@ | ... | @@ -13,3 +13,28 @@ |
| 13 | font-style: normal !important; | 13 | font-style: normal !important; |
| 14 | font-weight: normal !important; | 14 | font-weight: normal !important; |
| 15 | } | 15 | } |
| 16 | + | ||
| 17 | +button { | ||
| 18 | + margin: 0; | ||
| 19 | + padding: 0; | ||
| 20 | + background-color: inherit; | ||
| 21 | + position: static; | ||
| 22 | +} | ||
| 23 | + | ||
| 24 | +button:after { | ||
| 25 | + content: none; | ||
| 26 | +} | ||
| 27 | +button::after { | ||
| 28 | + border: none; | ||
| 29 | +} | ||
| 30 | + | ||
| 31 | +.bottom-actions { | ||
| 32 | + position: fixed; | ||
| 33 | + bottom: 0; | ||
| 34 | + left: 0; | ||
| 35 | + right: 0; | ||
| 36 | + background-color: #ffffff; | ||
| 37 | + padding: 24rpx 32rpx; | ||
| 38 | + border-top: 1rpx solid #f3f4f6; | ||
| 39 | + z-index: 100; | ||
| 40 | +} | ... | ... |
src/components/payCard.vue
0 → 100644
| 1 | +<!-- | ||
| 2 | + * @Date: 2023-12-20 14:11:11 | ||
| 3 | + * @LastEditors: hookehuyr hookehuyr@gmail.com | ||
| 4 | + * @LastEditTime: 2025-07-03 11:43:29 | ||
| 5 | + * @FilePath: /jgdl/src/components/payCard.vue | ||
| 6 | + * @Description: 文件描述 | ||
| 7 | +--> | ||
| 8 | +<template> | ||
| 9 | + <div class="pay-card"> | ||
| 10 | + <nut-action-sheet v-model:visible="props.visible" title="" @close="onClose"> | ||
| 11 | + <view style="padding: 2rem 1rem; text-align: center;"> | ||
| 12 | + <view style="font-size: 32rpx;">实付金额</view> | ||
| 13 | + <view style="color: red; margin: 10rpx 0;"><text style="font-size: 50rpx;">¥</text><text style="font-size: 80rpx;">{{ price }}</text></view> | ||
| 14 | + <view style="font-size: 28rpx; margin-bottom: 20rpx;">支付剩余时间 <text style="color: red;">{{ formatTime(remain_time) }}</text></view> | ||
| 15 | + <nut-button block color="#fb923c" @tap="goToPay">立即支付</nut-button> | ||
| 16 | + </view> | ||
| 17 | + </nut-action-sheet> | ||
| 18 | + </div> | ||
| 19 | +</template> | ||
| 20 | + | ||
| 21 | +<script setup> | ||
| 22 | +import Taro from '@tarojs/taro' | ||
| 23 | +import { ref, watch, onMounted, onUnmounted } from 'vue' | ||
| 24 | +import { getCurrentPageUrl } from "@/utils/weapp"; | ||
| 25 | +import { payAPI, payCheckAPI, orderSuccessAPI } from '@/api/index' | ||
| 26 | + | ||
| 27 | +/** | ||
| 28 | + * 格式化时间 | ||
| 29 | + * @param {*} seconds | ||
| 30 | + */ | ||
| 31 | +function formatTime(seconds) { | ||
| 32 | + const hours = Math.floor(seconds / 3600); // 计算小时数 | ||
| 33 | + const minutes = Math.floor((seconds % 3600) / 60); // 计算分钟数 | ||
| 34 | + const remainingSeconds = seconds % 60; // 计算剩余的秒数 | ||
| 35 | + | ||
| 36 | + const formattedHours = String(hours).padStart(2, "0"); // 格式化小时数,保证两位数 | ||
| 37 | + const formattedMinutes = String(minutes).padStart(2, "0"); // 格式化分钟数,保证两位数 | ||
| 38 | + const formattedSeconds = String(remainingSeconds).padStart(2, "0"); // 格式化剩余的秒数,保证两位数 | ||
| 39 | + | ||
| 40 | + return `${formattedHours}:${formattedMinutes}:${formattedSeconds}`; | ||
| 41 | +} | ||
| 42 | + | ||
| 43 | +const props = defineProps({ | ||
| 44 | + visible: { | ||
| 45 | + type: Boolean, | ||
| 46 | + default: false, | ||
| 47 | + }, | ||
| 48 | + data: { | ||
| 49 | + type: Object, | ||
| 50 | + default: {}, | ||
| 51 | + }, | ||
| 52 | +}); | ||
| 53 | + | ||
| 54 | +const emit = defineEmits(['close']); | ||
| 55 | + | ||
| 56 | +const onClose = () => { | ||
| 57 | + emit('close'); | ||
| 58 | +} | ||
| 59 | + | ||
| 60 | +const id = ref(''); | ||
| 61 | +const price = ref(''); | ||
| 62 | +const remain_time = ref(''); | ||
| 63 | + | ||
| 64 | +let timeId = null; | ||
| 65 | + | ||
| 66 | +watch( | ||
| 67 | + () => props.visible, | ||
| 68 | + (val) => { | ||
| 69 | + if (val) { | ||
| 70 | + id.value = props.data.id; | ||
| 71 | + price.value = props.data.price; | ||
| 72 | + remain_time.value = props.data.remain_time; | ||
| 73 | + } | ||
| 74 | + } | ||
| 75 | +) | ||
| 76 | + | ||
| 77 | +onMounted(() => { | ||
| 78 | + // 进入页面后,开始倒计时 | ||
| 79 | + timeId = setInterval(() => { | ||
| 80 | + remain_time.value ? remain_time.value -= 1 : 0; | ||
| 81 | + if (remain_time.value === 0) { // 倒计时结束 | ||
| 82 | + clearInterval(timeId); | ||
| 83 | + emit('close'); | ||
| 84 | + } | ||
| 85 | + }, 1000); | ||
| 86 | +}) | ||
| 87 | + | ||
| 88 | +onUnmounted(() => { | ||
| 89 | + timeId && clearInterval(timeId); | ||
| 90 | +}) | ||
| 91 | + | ||
| 92 | +const goToPay = async () => { | ||
| 93 | + if (price.value > 0) { // 金额大于0 | ||
| 94 | + // 获取支付参数 | ||
| 95 | + const { code, data } = await payAPI({ order_id: id.value }); | ||
| 96 | + if (code) { | ||
| 97 | + let pay = data; | ||
| 98 | + // 触发微信支付操作 | ||
| 99 | + wx.requestPayment({ | ||
| 100 | + timeStamp: pay.timeStamp, | ||
| 101 | + nonceStr: pay.nonceStr, | ||
| 102 | + package: pay.package, | ||
| 103 | + signType: pay.signType, | ||
| 104 | + paySign: pay.paySign, | ||
| 105 | + success: async (result) => { | ||
| 106 | + emit('close'); // 关闭支付弹框 | ||
| 107 | + Taro.showToast({ | ||
| 108 | + title: '支付成功', | ||
| 109 | + icon: 'success', | ||
| 110 | + duration: 1000 | ||
| 111 | + }); | ||
| 112 | + // 支付成功后,调用检查接口 | ||
| 113 | + const pay_success = await payCheckAPI({ order_id: id.value }); | ||
| 114 | + if (pay_success.code) { | ||
| 115 | + let current_page = getCurrentPageUrl(); | ||
| 116 | + if (current_page === 'pages/my/index') { // 我的页面打开 | ||
| 117 | + // 刷新当前页面 | ||
| 118 | + Taro.reLaunch({ | ||
| 119 | + url: '/pages/my/index?tab_index=5' | ||
| 120 | + }); | ||
| 121 | + } | ||
| 122 | + if (current_page === 'pages/detail/index') { // 订房确认页打开 | ||
| 123 | + // 跳转订单成功页 | ||
| 124 | + Taro.navigateTo({ | ||
| 125 | + url: '/pages/payInfo/index', | ||
| 126 | + }); | ||
| 127 | + } | ||
| 128 | + } | ||
| 129 | + } | ||
| 130 | + }); | ||
| 131 | + } | ||
| 132 | + } | ||
| 133 | +} | ||
| 134 | +</script> | ||
| 135 | + | ||
| 136 | +<style lang="less"> | ||
| 137 | + | ||
| 138 | +</style> |
| 1 | <!-- | 1 | <!-- |
| 2 | * @Date: 2022-09-19 14:11:06 | 2 | * @Date: 2022-09-19 14:11:06 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2025-07-02 18:15:19 | 4 | + * @LastEditTime: 2025-07-03 12:40:46 |
| 5 | * @FilePath: /jgdl/src/pages/authCar/index.vue | 5 | * @FilePath: /jgdl/src/pages/authCar/index.vue |
| 6 | * @Description: 认证车源 | 6 | * @Description: 认证车源 |
| 7 | --> | 7 | --> |
| ... | @@ -61,8 +61,8 @@ | ... | @@ -61,8 +61,8 @@ |
| 61 | </view> | 61 | </view> |
| 62 | <view class="flex-1 p-3 relative"> | 62 | <view class="flex-1 p-3 relative"> |
| 63 | <view class="absolute top-3 right-4" @tap.stop="() => toggleFavorite(car.id)"> | 63 | <view class="absolute top-3 right-4" @tap.stop="() => toggleFavorite(car.id)"> |
| 64 | - <Addfollow v-if="!favoriteIds.includes(car.id)" size="16" color="#9ca3af" /> | 64 | + <Heart1 v-if="!favoriteIds.includes(car.id)" size="16" :color="'#9ca3af'" /> |
| 65 | - <HeartFill v-else size="16" color="#ef4444" /> | 65 | + <HeartFill v-else size="16" :color="'#ef4444'" /> |
| 66 | </view> | 66 | </view> |
| 67 | <text class="font-medium text-sm block">{{ car.name }}</text> | 67 | <text class="font-medium text-sm block">{{ car.name }}</text> |
| 68 | <text class="text-xs text-gray-600 mt-1 block"> | 68 | <text class="text-xs text-gray-600 mt-1 block"> |
| ... | @@ -109,7 +109,7 @@ | ... | @@ -109,7 +109,7 @@ |
| 109 | <script setup> | 109 | <script setup> |
| 110 | import Taro from '@tarojs/taro' | 110 | import Taro from '@tarojs/taro' |
| 111 | import { ref, computed, onMounted } from 'vue' | 111 | import { ref, computed, onMounted } from 'vue' |
| 112 | -import { Check, RectRight, Addfollow, HeartFill } from '@nutui/icons-vue-taro' | 112 | +import { Check, Addfollow, Follow, Heart1, HeartFill } from '@nutui/icons-vue-taro' |
| 113 | import './index.less' | 113 | import './index.less' |
| 114 | 114 | ||
| 115 | // Banner图片数据 | 115 | // Banner图片数据 | ... | ... |
| ... | @@ -68,7 +68,7 @@ | ... | @@ -68,7 +68,7 @@ |
| 68 | </view> | 68 | </view> |
| 69 | <view class="flex-1 p-3 relative"> | 69 | <view class="flex-1 p-3 relative"> |
| 70 | <view class="absolute top-3 right-4" @tap.stop="() => toggleFavorite(car.id)"> | 70 | <view class="absolute top-3 right-4" @tap.stop="() => toggleFavorite(car.id)"> |
| 71 | - <Addfollow v-if="!favoriteIds.includes(car.id)" size="16" color="#9ca3af" /> | 71 | + <Heart1 v-if="!favoriteIds.includes(car.id)" size="16" color="#9ca3af" /> |
| 72 | <HeartFill v-else size="16" color="#ef4444" /> | 72 | <HeartFill v-else size="16" color="#ef4444" /> |
| 73 | </view> | 73 | </view> |
| 74 | <text class="font-medium text-sm block">{{ car.name }}</text> | 74 | <text class="font-medium text-sm block">{{ car.name }}</text> |
| ... | @@ -125,7 +125,7 @@ | ... | @@ -125,7 +125,7 @@ |
| 125 | 125 | ||
| 126 | <script setup> | 126 | <script setup> |
| 127 | import { ref, computed, onMounted } from 'vue' | 127 | import { ref, computed, onMounted } from 'vue' |
| 128 | -import { Search2, Addfollow, HeartFill } from '@nutui/icons-vue-taro' | 128 | +import { Search2, Addfollow, Follow, Heart1, HeartFill } from '@nutui/icons-vue-taro' |
| 129 | import TabBar from '@/components/TabBar.vue' | 129 | import TabBar from '@/components/TabBar.vue' |
| 130 | import './index.less' | 130 | import './index.less' |
| 131 | 131 | ... | ... |
| 1 | <!-- | 1 | <!-- |
| 2 | * @Date: 2025-06-28 10:33:00 | 2 | * @Date: 2025-06-28 10:33:00 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2025-07-02 22:58:32 | 4 | + * @LastEditTime: 2025-07-03 12:43:02 |
| 5 | * @FilePath: /jgdl/src/pages/index/index.vue | 5 | * @FilePath: /jgdl/src/pages/index/index.vue |
| 6 | * @Description: 捡个电驴首页 | 6 | * @Description: 捡个电驴首页 |
| 7 | --> | 7 | --> |
| ... | @@ -77,7 +77,7 @@ | ... | @@ -77,7 +77,7 @@ |
| 77 | <image :src="scooter.imageUrl" :alt="scooter.name" mode="aspectFill" | 77 | <image :src="scooter.imageUrl" :alt="scooter.name" mode="aspectFill" |
| 78 | class="w-full h-36 object-cover rounded-lg" /> | 78 | class="w-full h-36 object-cover rounded-lg" /> |
| 79 | <view class="absolute top-4 right-4 p-1" @tap.stop="() => toggleFavorite(scooter.id)"> | 79 | <view class="absolute top-4 right-4 p-1" @tap.stop="() => toggleFavorite(scooter.id)"> |
| 80 | - <Addfollow v-if="!favoriteIds.includes(scooter.id)" size="20" :color="'#ffffff'" /> | 80 | + <Heart1 v-if="!favoriteIds.includes(scooter.id)" size="20" :color="'#ffffff'" /> |
| 81 | <HeartFill v-else size="20" :color="'#ef4444'" /> | 81 | <HeartFill v-else size="20" :color="'#ef4444'" /> |
| 82 | </view> | 82 | </view> |
| 83 | <view v-if="scooter.isVerified" | 83 | <view v-if="scooter.isVerified" |
| ... | @@ -131,7 +131,7 @@ | ... | @@ -131,7 +131,7 @@ |
| 131 | </view> | 131 | </view> |
| 132 | <view class="flex-1 p-3 relative"> | 132 | <view class="flex-1 p-3 relative"> |
| 133 | <view class="absolute top-2 right-2" @tap.stop="() => toggleFavorite(scooter.id)"> | 133 | <view class="absolute top-2 right-2" @tap.stop="() => toggleFavorite(scooter.id)"> |
| 134 | - <Addfollow v-if="!favoriteIds.includes(scooter.id)" size="16" :color="'#ffffff'" /> | 134 | + <Heart1 v-if="!favoriteIds.includes(scooter.id)" size="16" :color="'#ffffff'" /> |
| 135 | <HeartFill v-else size="16" :color="'#ef4444'" /> | 135 | <HeartFill v-else size="16" :color="'#ef4444'" /> |
| 136 | </view> | 136 | </view> |
| 137 | <text class="font-medium text-sm block">{{ scooter.name }}</text> | 137 | <text class="font-medium text-sm block">{{ scooter.name }}</text> |
| ... | @@ -162,7 +162,7 @@ import Taro from '@tarojs/taro' | ... | @@ -162,7 +162,7 @@ import Taro from '@tarojs/taro' |
| 162 | import '@tarojs/taro/html5.css' //和 nutui组件居然有冲突? | 162 | import '@tarojs/taro/html5.css' //和 nutui组件居然有冲突? |
| 163 | import { ref, onMounted } from 'vue' | 163 | import { ref, onMounted } from 'vue' |
| 164 | import { useDidShow, useReady } from '@tarojs/taro' | 164 | import { useDidShow, useReady } from '@tarojs/taro' |
| 165 | -import { Clock, Star, RectRight, Addfollow, HeartFill, Check, Search2, Shop } from '@nutui/icons-vue-taro' | 165 | +import { Clock, Star, RectRight, Addfollow, Follow, Check, Search2, Shop, Heart1, HeartFill } from '@nutui/icons-vue-taro' |
| 166 | import TabBar from '@/components/TabBar.vue' | 166 | import TabBar from '@/components/TabBar.vue' |
| 167 | import "./index.less"; | 167 | import "./index.less"; |
| 168 | 168 | ||
| ... | @@ -280,9 +280,8 @@ const toggleFavorite = (scooterId) => { | ... | @@ -280,9 +280,8 @@ const toggleFavorite = (scooterId) => { |
| 280 | * @param {Object} scooter - 电动车信息 | 280 | * @param {Object} scooter - 电动车信息 |
| 281 | */ | 281 | */ |
| 282 | const onProductClick = (scooter) => { | 282 | const onProductClick = (scooter) => { |
| 283 | - Taro.showToast({ | 283 | + Taro.navigateTo({ |
| 284 | - title: `查看${scooter.name}`, | 284 | + url: `/pages/productDetail/index?id=${scooter.id}` |
| 285 | - icon: 'none' | ||
| 286 | }) | 285 | }) |
| 287 | } | 286 | } |
| 288 | 287 | ... | ... |
| ... | @@ -63,7 +63,7 @@ | ... | @@ -63,7 +63,7 @@ |
| 63 | </view> | 63 | </view> |
| 64 | <view class="flex-1 p-3 relative"> | 64 | <view class="flex-1 p-3 relative"> |
| 65 | <view class="absolute top-3 right-4" @tap.stop="() => toggleFavorite(car.id)"> | 65 | <view class="absolute top-3 right-4" @tap.stop="() => toggleFavorite(car.id)"> |
| 66 | - <Addfollow v-if="!favoriteIds.includes(car.id)" size="16" color="#9ca3af" /> | 66 | + <Heart1 v-if="!favoriteIds.includes(car.id)" size="16" color="#9ca3af" /> |
| 67 | <HeartFill v-else size="16" color="#ef4444" /> | 67 | <HeartFill v-else size="16" color="#ef4444" /> |
| 68 | </view> | 68 | </view> |
| 69 | <text class="font-medium text-sm block">{{ car.name }}</text> | 69 | <text class="font-medium text-sm block">{{ car.name }}</text> |
| ... | @@ -114,7 +114,7 @@ | ... | @@ -114,7 +114,7 @@ |
| 114 | 114 | ||
| 115 | <script setup> | 115 | <script setup> |
| 116 | import { ref, computed, onMounted } from 'vue' | 116 | import { ref, computed, onMounted } from 'vue' |
| 117 | -import { Search2, Addfollow, HeartFill } from '@nutui/icons-vue-taro' | 117 | +import { Search2, Heart1, HeartFill } from '@nutui/icons-vue-taro' |
| 118 | import TabBar from '@/components/TabBar.vue' | 118 | import TabBar from '@/components/TabBar.vue' |
| 119 | import './index.less' | 119 | import './index.less' |
| 120 | 120 | ||
| ... | @@ -234,12 +234,12 @@ const scrollStyle = computed(() => { | ... | @@ -234,12 +234,12 @@ const scrollStyle = computed(() => { |
| 234 | * @param {string} carId - 车辆ID | 234 | * @param {string} carId - 车辆ID |
| 235 | */ | 235 | */ |
| 236 | const toggleFavorite = (carId) => { | 236 | const toggleFavorite = (carId) => { |
| 237 | - const index = favoriteIds.value.indexOf(carId.toString()) | 237 | + const index = favoriteIds.value.indexOf(carId) |
| 238 | if (index > -1) { | 238 | if (index > -1) { |
| 239 | favoriteIds.value.splice(index, 1) | 239 | favoriteIds.value.splice(index, 1) |
| 240 | showToast('取消收藏', 'success') | 240 | showToast('取消收藏', 'success') |
| 241 | } else { | 241 | } else { |
| 242 | - favoriteIds.value.push(carId.toString()) | 242 | + favoriteIds.value.push(carId) |
| 243 | showToast('收藏成功', 'success') | 243 | showToast('收藏成功', 'success') |
| 244 | } | 244 | } |
| 245 | } | 245 | } | ... | ... |
| ... | @@ -45,7 +45,7 @@ | ... | @@ -45,7 +45,7 @@ |
| 45 | </view> | 45 | </view> |
| 46 | <view class="flex-1 p-3 relative"> | 46 | <view class="flex-1 p-3 relative"> |
| 47 | <view class="absolute top-3 right-4" @tap.stop="() => toggleFavorite(scooter.id)"> | 47 | <view class="absolute top-3 right-4" @tap.stop="() => toggleFavorite(scooter.id)"> |
| 48 | - <Addfollow v-if="!favoriteIds.includes(scooter.id)" size="16" color="#9ca3af" /> | 48 | + <Heart1 v-if="!favoriteIds.includes(scooter.id)" size="16" color="#9ca3af" /> |
| 49 | <HeartFill v-else size="16" color="#ef4444" /> | 49 | <HeartFill v-else size="16" color="#ef4444" /> |
| 50 | </view> | 50 | </view> |
| 51 | <text class="font-medium text-sm block">{{ scooter.name }}</text> | 51 | <text class="font-medium text-sm block">{{ scooter.name }}</text> |
| ... | @@ -96,7 +96,7 @@ | ... | @@ -96,7 +96,7 @@ |
| 96 | <image :src="scooter.imageUrl" :alt="scooter.name" mode="aspectFill" | 96 | <image :src="scooter.imageUrl" :alt="scooter.name" mode="aspectFill" |
| 97 | class="w-full h-36 object-cover rounded-lg" /> | 97 | class="w-full h-36 object-cover rounded-lg" /> |
| 98 | <view class="absolute top-4 right-4 p-1" @tap.stop="() => toggleFavorite(scooter.id)"> | 98 | <view class="absolute top-4 right-4 p-1" @tap.stop="() => toggleFavorite(scooter.id)"> |
| 99 | - <Addfollow v-if="!favoriteIds.includes(scooter.id)" size="20" color="#ffffff" /> | 99 | + <Heart1 v-if="!favoriteIds.includes(scooter.id)" size="20" color="#ffffff" /> |
| 100 | <HeartFill v-else size="20" color="#ef4444" /> | 100 | <HeartFill v-else size="20" color="#ef4444" /> |
| 101 | </view> | 101 | </view> |
| 102 | <view v-if="scooter.isVerified" | 102 | <view v-if="scooter.isVerified" |
| ... | @@ -132,7 +132,7 @@ | ... | @@ -132,7 +132,7 @@ |
| 132 | <script setup> | 132 | <script setup> |
| 133 | import { ref } from 'vue' | 133 | import { ref } from 'vue' |
| 134 | import Taro from '@tarojs/taro' | 134 | import Taro from '@tarojs/taro' |
| 135 | -import { Search2, RectRight, Check, Addfollow, HeartFill } from '@nutui/icons-vue-taro' | 135 | +import { Search2, RectRight, Check, Heart1, HeartFill } from '@nutui/icons-vue-taro' |
| 136 | import TabBar from '@/components/TabBar.vue' | 136 | import TabBar from '@/components/TabBar.vue' |
| 137 | 137 | ||
| 138 | // 响应式数据 | 138 | // 响应式数据 | ... | ... |
src/pages/productDetail/index.config.js
0 → 100755
| 1 | +/* | ||
| 2 | + * @Date: 2025-07-03 09:34:12 | ||
| 3 | + * @LastEditors: hookehuyr hookehuyr@gmail.com | ||
| 4 | + * @LastEditTime: 2025-07-03 09:34:43 | ||
| 5 | + * @FilePath: /jgdl/src/pages/productDetail/index.config.js | ||
| 6 | + * @Description: 文件描述 | ||
| 7 | + */ | ||
| 8 | +export default { | ||
| 9 | + navigationBarTitleText: '商品详情', | ||
| 10 | + usingComponents: { | ||
| 11 | + }, | ||
| 12 | +} |
src/pages/productDetail/index.less
0 → 100644
| 1 | +// 商品详情页样式 | ||
| 2 | +.product-detail-page { | ||
| 3 | + background-color: #f5f5f5; | ||
| 4 | + min-height: 100vh; | ||
| 5 | + padding-bottom: 120rpx; | ||
| 6 | +} | ||
| 7 | + | ||
| 8 | +// 图片轮播样式 | ||
| 9 | +.image-carousel { | ||
| 10 | + .nut-swiper { | ||
| 11 | + .nut-swiper-pagination { | ||
| 12 | + bottom: 20rpx; | ||
| 13 | + } | ||
| 14 | + } | ||
| 15 | +} | ||
| 16 | + | ||
| 17 | +// 商品信息区域 | ||
| 18 | +.product-info { | ||
| 19 | + .flex { | ||
| 20 | + display: flex; | ||
| 21 | + } | ||
| 22 | + | ||
| 23 | + .items-center { | ||
| 24 | + align-items: center; | ||
| 25 | + } | ||
| 26 | + | ||
| 27 | + .justify-between { | ||
| 28 | + justify-content: space-between; | ||
| 29 | + } | ||
| 30 | + | ||
| 31 | + .flex-col { | ||
| 32 | + flex-direction: column; | ||
| 33 | + } | ||
| 34 | + | ||
| 35 | + .space-x-4 > view:not(:first-child) { | ||
| 36 | + margin-left: 1rem; | ||
| 37 | + } | ||
| 38 | + | ||
| 39 | + .bg-gray-50 { | ||
| 40 | + background-color: #f9fafb; | ||
| 41 | + } | ||
| 42 | + | ||
| 43 | + .rounded-full { | ||
| 44 | + border-radius: 50%; | ||
| 45 | + } | ||
| 46 | + | ||
| 47 | + .p-2 { | ||
| 48 | + padding: 0.5rem; | ||
| 49 | + } | ||
| 50 | + | ||
| 51 | + .mt-1 { | ||
| 52 | + margin-top: 0.25rem; | ||
| 53 | + } | ||
| 54 | + | ||
| 55 | + .text-xs { | ||
| 56 | + font-size: 0.75rem; | ||
| 57 | + line-height: 1rem; | ||
| 58 | + } | ||
| 59 | + | ||
| 60 | + .text-gray-500 { | ||
| 61 | + color: #6b7280; | ||
| 62 | + } | ||
| 63 | +} | ||
| 64 | + | ||
| 65 | +// 基本信息网格布局 | ||
| 66 | +.grid { | ||
| 67 | + display: grid; | ||
| 68 | +} | ||
| 69 | + | ||
| 70 | +.grid-cols-2 { | ||
| 71 | + grid-template-columns: repeat(2, 1fr); | ||
| 72 | +} | ||
| 73 | + | ||
| 74 | +.gap-4 { | ||
| 75 | + gap: 1rem; | ||
| 76 | +} | ||
| 77 | + | ||
| 78 | +// 间距工具类 | ||
| 79 | +.space-x-3 > view:not(:first-child) { | ||
| 80 | + margin-left: 0.75rem; | ||
| 81 | +} | ||
| 82 | + | ||
| 83 | +.space-x-4 > view:not(:first-child) { | ||
| 84 | + margin-left: 1rem; | ||
| 85 | +} | ||
| 86 | + | ||
| 87 | +.space-y-3 > view:not(:first-child) { | ||
| 88 | + margin-top: 0.75rem; | ||
| 89 | +} | ||
| 90 | + | ||
| 91 | +// 富文本内容样式 | ||
| 92 | +.rich-content { | ||
| 93 | + line-height: 1.6; | ||
| 94 | + | ||
| 95 | + p { | ||
| 96 | + margin-bottom: 12rpx; | ||
| 97 | + } | ||
| 98 | + | ||
| 99 | + ul { | ||
| 100 | + margin: 12rpx 0; | ||
| 101 | + padding-left: 20rpx; | ||
| 102 | + | ||
| 103 | + li { | ||
| 104 | + margin-bottom: 6rpx; | ||
| 105 | + } | ||
| 106 | + } | ||
| 107 | + | ||
| 108 | + strong { | ||
| 109 | + font-weight: bold; | ||
| 110 | + } | ||
| 111 | + | ||
| 112 | + span { | ||
| 113 | + display: inline; | ||
| 114 | + } | ||
| 115 | +} | ||
| 116 | + | ||
| 117 | +// 联系卖家弹框样式 | ||
| 118 | +.contact-modal { | ||
| 119 | + .seller-card { | ||
| 120 | + background-color: #f9fafb; | ||
| 121 | + } | ||
| 122 | + | ||
| 123 | + .contact-options { | ||
| 124 | + .nut-button { | ||
| 125 | + margin-bottom: 12rpx; | ||
| 126 | + | ||
| 127 | + &:last-child { | ||
| 128 | + margin-bottom: 0; | ||
| 129 | + } | ||
| 130 | + } | ||
| 131 | + } | ||
| 132 | +} | ||
| 133 | + | ||
| 134 | +// 微信弹框样式 | ||
| 135 | +.nut-dialog { | ||
| 136 | + .nut-dialog-content { | ||
| 137 | + .text-center { | ||
| 138 | + text-align: center; | ||
| 139 | + } | ||
| 140 | + | ||
| 141 | + .p-4 { | ||
| 142 | + padding: 1rem; | ||
| 143 | + } | ||
| 144 | + | ||
| 145 | + .mb-2 { | ||
| 146 | + margin-bottom: 0.5rem; | ||
| 147 | + } | ||
| 148 | + | ||
| 149 | + .mb-4 { | ||
| 150 | + margin-bottom: 1rem; | ||
| 151 | + } | ||
| 152 | + | ||
| 153 | + .text-lg { | ||
| 154 | + font-size: 1.125rem; | ||
| 155 | + line-height: 1.75rem; | ||
| 156 | + } | ||
| 157 | + | ||
| 158 | + .font-medium { | ||
| 159 | + font-weight: 500; | ||
| 160 | + } | ||
| 161 | + | ||
| 162 | + .text-sm { | ||
| 163 | + font-size: 0.875rem; | ||
| 164 | + line-height: 1.25rem; | ||
| 165 | + } | ||
| 166 | + | ||
| 167 | + .block { | ||
| 168 | + display: block; | ||
| 169 | + } | ||
| 170 | + } | ||
| 171 | +} | ||
| 172 | + | ||
| 173 | +// 底部按钮区域 | ||
| 174 | +.fixed { | ||
| 175 | + position: fixed; | ||
| 176 | +} | ||
| 177 | + | ||
| 178 | +.bottom-0 { | ||
| 179 | + bottom: 0; | ||
| 180 | +} | ||
| 181 | + | ||
| 182 | +.left-0 { | ||
| 183 | + left: 0; | ||
| 184 | +} | ||
| 185 | + | ||
| 186 | +.right-0 { | ||
| 187 | + right: 0; | ||
| 188 | +} | ||
| 189 | + | ||
| 190 | +.bg-white { | ||
| 191 | + background-color: #ffffff; | ||
| 192 | +} | ||
| 193 | + | ||
| 194 | +.border-t { | ||
| 195 | + border-top-width: 1px; | ||
| 196 | +} | ||
| 197 | + | ||
| 198 | +.border-gray-200 { | ||
| 199 | + border-color: #e5e7eb; | ||
| 200 | +} | ||
| 201 | + | ||
| 202 | +.p-3 { | ||
| 203 | + padding: 0.75rem; | ||
| 204 | +} | ||
| 205 | + | ||
| 206 | +.flex-1 { | ||
| 207 | + flex: 1 1 0%; | ||
| 208 | +} | ||
| 209 | + | ||
| 210 | +// 响应式适配 | ||
| 211 | +@media (max-width: 768rpx) { | ||
| 212 | + .product-detail-page { | ||
| 213 | + padding-bottom: 140rpx; | ||
| 214 | + } | ||
| 215 | + | ||
| 216 | + .product-info { | ||
| 217 | + .space-x-4 > view:not(:first-child) { | ||
| 218 | + margin-left: 0.5rem; | ||
| 219 | + } | ||
| 220 | + | ||
| 221 | + .text-xs { | ||
| 222 | + font-size: 0.7rem; | ||
| 223 | + } | ||
| 224 | + } | ||
| 225 | + | ||
| 226 | + .grid-cols-2 { | ||
| 227 | + grid-template-columns: 1fr; | ||
| 228 | + gap: 0.5rem; | ||
| 229 | + } | ||
| 230 | +} | ||
| 231 | + | ||
| 232 | +// 深色模式适配 | ||
| 233 | +@media (prefers-color-scheme: dark) { | ||
| 234 | + .product-detail-page { | ||
| 235 | + background-color: #1f2937; | ||
| 236 | + color: #f9fafb; | ||
| 237 | + } | ||
| 238 | + | ||
| 239 | + .bg-white { | ||
| 240 | + background-color: #374151; | ||
| 241 | + } | ||
| 242 | + | ||
| 243 | + .text-gray-500 { | ||
| 244 | + color: #9ca3af; | ||
| 245 | + } | ||
| 246 | + | ||
| 247 | + .border-gray-200 { | ||
| 248 | + border-color: #4b5563; | ||
| 249 | + } | ||
| 250 | +} | ||
| 251 | + | ||
| 252 | +// 动画效果 | ||
| 253 | +@keyframes fadeInUp { | ||
| 254 | + from { | ||
| 255 | + opacity: 0; | ||
| 256 | + transform: translateY(30rpx); | ||
| 257 | + } | ||
| 258 | + to { | ||
| 259 | + opacity: 1; | ||
| 260 | + transform: translateY(0); | ||
| 261 | + } | ||
| 262 | +} | ||
| 263 | + | ||
| 264 | +.product-info, | ||
| 265 | +.basic-info, | ||
| 266 | +.vehicle-condition, | ||
| 267 | +.vehicle-description, | ||
| 268 | +.seller-info { | ||
| 269 | + animation: fadeInUp 0.6s ease-out; | ||
| 270 | +} | ||
| 271 | + | ||
| 272 | +// 卡片悬停效果 | ||
| 273 | +.product-info, | ||
| 274 | +.basic-info, | ||
| 275 | +.vehicle-condition, | ||
| 276 | +.vehicle-description, | ||
| 277 | +.seller-info { | ||
| 278 | + transition: all 0.3s ease; | ||
| 279 | + | ||
| 280 | + &:hover { | ||
| 281 | + transform: translateY(-2rpx); | ||
| 282 | + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1); | ||
| 283 | + } | ||
| 284 | +} | ||
| 285 | + | ||
| 286 | +// 按钮样式增强 | ||
| 287 | +.nut-button { | ||
| 288 | + transition: all 0.3s ease; | ||
| 289 | + | ||
| 290 | + &:active { | ||
| 291 | + transform: scale(0.98); | ||
| 292 | + } | ||
| 293 | +} | ||
| 294 | + | ||
| 295 | +// 图标按钮样式 | ||
| 296 | +.product-info { | ||
| 297 | + .bg-gray-50 { | ||
| 298 | + transition: all 0.3s ease; | ||
| 299 | + | ||
| 300 | + &:active { | ||
| 301 | + background-color: #e5e7eb; | ||
| 302 | + transform: scale(0.95); | ||
| 303 | + } | ||
| 304 | + } | ||
| 305 | +} |
src/pages/productDetail/index.vue
0 → 100644
| 1 | +<!-- | ||
| 2 | + * @Date: 2022-09-19 14:11:06 | ||
| 3 | + * @LastEditors: hookehuyr hookehuyr@gmail.com | ||
| 4 | + * @LastEditTime: 2025-07-03 12:45:36 | ||
| 5 | + * @FilePath: /jgdl/src/pages/productDetail/index.vue | ||
| 6 | + * @Description: 商品详情页 | ||
| 7 | +--> | ||
| 8 | +<template> | ||
| 9 | + <view class="product-detail-page"> | ||
| 10 | + <!-- 图片轮播 --> | ||
| 11 | + <view class="image-carousel"> | ||
| 12 | + <nut-swiper | ||
| 13 | + :init-page="currentImageIndex" | ||
| 14 | + :pagination-visible="true" | ||
| 15 | + pagination-color="#426543" | ||
| 16 | + auto-play="3000" | ||
| 17 | + @change="onSwiperChange" | ||
| 18 | + > | ||
| 19 | + <nut-swiper-item v-for="(image, index) in product.images" :key="index" style="height: 400rpx"> | ||
| 20 | + <image | ||
| 21 | + :src="image" | ||
| 22 | + :alt="product.name" | ||
| 23 | + mode="aspectFill" | ||
| 24 | + class="w-full h-full object-cover" | ||
| 25 | + @error="onImageError" | ||
| 26 | + @load="onImageLoad" | ||
| 27 | + /> | ||
| 28 | + </nut-swiper-item> | ||
| 29 | + </nut-swiper> | ||
| 30 | + </view> | ||
| 31 | + | ||
| 32 | + <!-- 商品信息 --> | ||
| 33 | + <view class="product-info bg-white p-4"> | ||
| 34 | + <text class="text-xl font-bold block mb-3">{{ product.name }}</text> | ||
| 35 | + <view class="flex items-center justify-between"> | ||
| 36 | + <view class="flex items-center"> | ||
| 37 | + <text class="text-2xl text-orange-500 font-bold"> | ||
| 38 | + ¥{{ product.price.toLocaleString() }} | ||
| 39 | + </text> | ||
| 40 | + <view class="ml-2 text-xs px-2 py-1 bg-orange-100 text-orange-600 rounded"> | ||
| 41 | + 低于市场价{{ product.discountPercent }}% | ||
| 42 | + </view> | ||
| 43 | + </view> | ||
| 44 | + <view class="flex space-x-4"> | ||
| 45 | + <button | ||
| 46 | + open-type="share" | ||
| 47 | + class="flex flex-col items-center" | ||
| 48 | + > | ||
| 49 | + <view style="height: 2.55rem;"> | ||
| 50 | + <Share size="18" color="#666" style="margin-bottom: 0.05rem;"/> | ||
| 51 | + </view> | ||
| 52 | + <text class="text-xs text-gray-500 mt-1">分享</text> | ||
| 53 | + </button> | ||
| 54 | + <view @tap="toggleFavorite" class="flex flex-col items-center ml-3"> | ||
| 55 | + <view class="p-2"> | ||
| 56 | + <HeartFill v-if="isFavorite" size="20" color="#ef4444" /> | ||
| 57 | + <Heart1 v-else size="20" color="#666" /> | ||
| 58 | + </view> | ||
| 59 | + <text class="text-xs text-gray-500 mt-1">{{ isFavorite ? '已收藏' : '收藏' }}</text> | ||
| 60 | + </view> | ||
| 61 | + </view> | ||
| 62 | + </view> | ||
| 63 | + </view> | ||
| 64 | + | ||
| 65 | + <!-- 基本信息 --> | ||
| 66 | + <view class="basic-info bg-white mt-2 p-4"> | ||
| 67 | + <text class="text-lg font-medium mb-3 block">基本信息</text> | ||
| 68 | + <view class="grid grid-cols-2 gap-4"> | ||
| 69 | + <view class="flex items-center"> | ||
| 70 | + <view class="w-8 h-8 bg-orange-100 rounded-full flex items-center justify-center mr-2"> | ||
| 71 | + <text class="text-orange-500">📅</text> | ||
| 72 | + </view> | ||
| 73 | + <view> | ||
| 74 | + <text class="text-xs text-gray-500 block">出厂年份</text> | ||
| 75 | + <text class="text-sm block">{{ product.year }}</text> | ||
| 76 | + </view> | ||
| 77 | + </view> | ||
| 78 | + <view class="flex items-center"> | ||
| 79 | + <view class="w-8 h-8 bg-orange-100 rounded-full flex items-center justify-center mr-2"> | ||
| 80 | + <text class="text-orange-500">🔋</text> | ||
| 81 | + </view> | ||
| 82 | + <view> | ||
| 83 | + <text class="text-xs text-gray-500 block">续航里程</text> | ||
| 84 | + <text class="text-sm block">{{ product.range }}</text> | ||
| 85 | + </view> | ||
| 86 | + </view> | ||
| 87 | + <view class="flex items-center"> | ||
| 88 | + <view class="w-8 h-8 bg-orange-100 rounded-full flex items-center justify-center mr-2"> | ||
| 89 | + <text class="text-orange-500">🛣️</text> | ||
| 90 | + </view> | ||
| 91 | + <view> | ||
| 92 | + <text class="text-xs text-gray-500 block">行驶里程</text> | ||
| 93 | + <text class="text-sm block">{{ product.mileage }}</text> | ||
| 94 | + </view> | ||
| 95 | + </view> | ||
| 96 | + <view class="flex items-center"> | ||
| 97 | + <view class="w-8 h-8 bg-orange-100 rounded-full flex items-center justify-center mr-2"> | ||
| 98 | + <text class="text-orange-500">⚡</text> | ||
| 99 | + </view> | ||
| 100 | + <view> | ||
| 101 | + <text class="text-xs text-gray-500 block">最高时速</text> | ||
| 102 | + <text class="text-sm block">{{ product.maxSpeed }}</text> | ||
| 103 | + </view> | ||
| 104 | + </view> | ||
| 105 | + </view> | ||
| 106 | + </view> | ||
| 107 | + | ||
| 108 | + <!-- 车辆评估 --> | ||
| 109 | + <view class="vehicle-condition bg-white mt-2 p-4"> | ||
| 110 | + <text class="text-lg font-medium mb-3 block">车辆评估</text> | ||
| 111 | + <view class="space-y-3"> | ||
| 112 | + <view class="flex justify-between items-center"> | ||
| 113 | + <text>车辆成色</text> | ||
| 114 | + <view class="flex"> | ||
| 115 | + <text v-for="star in 5" :key="star" class="text-orange-400">★</text> | ||
| 116 | + </view> | ||
| 117 | + </view> | ||
| 118 | + <view class="flex justify-between items-center"> | ||
| 119 | + <text>刹车磨损度</text> | ||
| 120 | + <view class="flex"> | ||
| 121 | + <text | ||
| 122 | + v-for="star in 5" | ||
| 123 | + :key="star" | ||
| 124 | + :class="star <= Math.round(product.brakeCondition) ? 'text-orange-400' : 'text-gray-300'" | ||
| 125 | + > | ||
| 126 | + ★ | ||
| 127 | + </text> | ||
| 128 | + </view> | ||
| 129 | + </view> | ||
| 130 | + <view class="flex justify-between items-center"> | ||
| 131 | + <text>轮胎磨损度</text> | ||
| 132 | + <view class="flex"> | ||
| 133 | + <text | ||
| 134 | + v-for="star in 5" | ||
| 135 | + :key="star" | ||
| 136 | + :class="star <= product.tireCondition ? 'text-orange-400' : 'text-gray-300'" | ||
| 137 | + > | ||
| 138 | + ★ | ||
| 139 | + </text> | ||
| 140 | + </view> | ||
| 141 | + </view> | ||
| 142 | + </view> | ||
| 143 | + </view> | ||
| 144 | + | ||
| 145 | + <!-- 车辆描述 --> | ||
| 146 | + <view class="vehicle-description bg-white mt-2 p-4"> | ||
| 147 | + <text class="text-lg font-medium mb-3 block">车辆描述</text> | ||
| 148 | + <rich-text :nodes="product.richDescription" class="rich-content"></rich-text> | ||
| 149 | + <image | ||
| 150 | + :src="product.images[1]" | ||
| 151 | + alt="车辆细节" | ||
| 152 | + mode="aspectFill" | ||
| 153 | + class="w-full h-40 object-cover rounded-lg mt-3" | ||
| 154 | + @error="onImageError" | ||
| 155 | + @load="onImageLoad" | ||
| 156 | + /> | ||
| 157 | + </view> | ||
| 158 | + | ||
| 159 | + <!-- 卖家信息 --> | ||
| 160 | + <view class="seller-info bg-white mt-2 p-4 mb-24"> | ||
| 161 | + <text class="text-lg font-medium mb-3 block">卖家信息</text> | ||
| 162 | + <view class="flex items-center justify-between"> | ||
| 163 | + <view class="flex items-center"> | ||
| 164 | + <image :src="product.seller.avatar" :alt="product.seller.name" mode="aspectFill" class="w-10 h-10 rounded-full object-cover mr-3" /> | ||
| 165 | + <view> | ||
| 166 | + <view class="flex items-center"> | ||
| 167 | + <text class="font-medium">{{ product.seller.name }}</text> | ||
| 168 | + <view v-if="product.seller.verified" class="ml-1 text-xs text-blue-500 bg-blue-50 px-1 py-0.5 rounded"> | ||
| 169 | + 已认证卖家 | ||
| 170 | + </view> | ||
| 171 | + </view> | ||
| 172 | + <text class="text-xs text-gray-500 block">{{ product.seller.school }}</text> | ||
| 173 | + </view> | ||
| 174 | + </view> | ||
| 175 | + <view @tap="showWechatModal" class="text-green-500 font-medium flex items-center"> | ||
| 176 | + <text>加微信</text> | ||
| 177 | + <Right size="16" /> | ||
| 178 | + </view> | ||
| 179 | + </view> | ||
| 180 | + </view> | ||
| 181 | + | ||
| 182 | + <!-- 底部按钮 --> | ||
| 183 | + <view class="bottom-actions"> | ||
| 184 | + <nut-row :gutter="10"> | ||
| 185 | + <nut-col :span="12"> | ||
| 186 | + <nut-button @click="handleContactSeller" | ||
| 187 | + block | ||
| 188 | + type="default" | ||
| 189 | + shape="round" | ||
| 190 | + style="border-color: #f97316; color: #f97316;" | ||
| 191 | + > | ||
| 192 | + 联系卖家 | ||
| 193 | + </nut-button> | ||
| 194 | + </nut-col> | ||
| 195 | + <nut-col :span="12"> | ||
| 196 | + <nut-button | ||
| 197 | + @click="handlePurchase" | ||
| 198 | + block | ||
| 199 | + type="primary" | ||
| 200 | + shape="round" | ||
| 201 | + style="background-color: #f97316; border-color: #f97316;" | ||
| 202 | + > | ||
| 203 | + 我想购买 | ||
| 204 | + </nut-button> | ||
| 205 | + </nut-col> | ||
| 206 | + </nut-row> | ||
| 207 | + </view> | ||
| 208 | + | ||
| 209 | + <!-- 微信号弹框 --> | ||
| 210 | + <nut-dialog | ||
| 211 | + v-model:visible="showWechat" | ||
| 212 | + title="卖家微信号" | ||
| 213 | + :close-on-click-overlay="true" | ||
| 214 | + > | ||
| 215 | + <view class="text-center p-4"> | ||
| 216 | + <text class="text-lg font-medium block mb-2">{{ product.seller.wechat }}</text> | ||
| 217 | + <text class="text-sm text-gray-500 block mb-4">长按复制微信号</text> | ||
| 218 | + <nut-button | ||
| 219 | + @click="copyWechat" | ||
| 220 | + type="primary" | ||
| 221 | + size="small" | ||
| 222 | + style="background-color: #f97316; border-color: #f97316;" | ||
| 223 | + > | ||
| 224 | + 复制微信号 | ||
| 225 | + </nut-button> | ||
| 226 | + </view> | ||
| 227 | + </nut-dialog> | ||
| 228 | + | ||
| 229 | + <!-- 联系卖家弹框 --> | ||
| 230 | + <nut-popup | ||
| 231 | + v-model:visible="showContactModal" | ||
| 232 | + position="bottom" | ||
| 233 | + :style="{ height: '60%' }" | ||
| 234 | + round | ||
| 235 | + > | ||
| 236 | + <view class="contact-modal p-4"> | ||
| 237 | + <view class="text-center mb-4"> | ||
| 238 | + <text class="text-lg font-medium">联系卖家</text> | ||
| 239 | + </view> | ||
| 240 | + <view class="seller-card bg-gray-50 rounded-lg p-3 mb-4"> | ||
| 241 | + <view class="flex items-center"> | ||
| 242 | + <image :src="product.seller.avatar" :alt="product.seller.name" mode="aspectFill" class="w-12 h-12 rounded-full object-cover mr-3" /> | ||
| 243 | + <view> | ||
| 244 | + <text class="font-medium block">{{ product.seller.name }}</text> | ||
| 245 | + <text class="text-sm text-gray-500 block">{{ product.seller.school }}</text> | ||
| 246 | + </view> | ||
| 247 | + </view> | ||
| 248 | + </view> | ||
| 249 | + | ||
| 250 | + <!-- 留言输入框 --> | ||
| 251 | + <view class="message-input mb-4"> | ||
| 252 | + <nut-textarea | ||
| 253 | + v-model="messageText" | ||
| 254 | + placeholder="请输入您想对卖家说的话..." | ||
| 255 | + :rows="4" | ||
| 256 | + :max-length="200" | ||
| 257 | + show-word-limit | ||
| 258 | + class="w-full" | ||
| 259 | + /> | ||
| 260 | + </view> | ||
| 261 | + | ||
| 262 | + <!-- 快捷标签 --> | ||
| 263 | + <view class="quick-tags mb-4"> | ||
| 264 | + <text class="text-sm text-gray-600 block mb-2">快捷输入:</text> | ||
| 265 | + <view class="flex flex-wrap gap-2"> | ||
| 266 | + <view | ||
| 267 | + v-for="tag in quickTags" | ||
| 268 | + :key="tag" | ||
| 269 | + @click="addQuickTag(tag)" | ||
| 270 | + class="quick-tag px-3 py-1 bg-orange-50 text-orange-600 rounded-full text-sm cursor-pointer" | ||
| 271 | + > | ||
| 272 | + {{ tag }} | ||
| 273 | + </view> | ||
| 274 | + </view> | ||
| 275 | + </view> | ||
| 276 | + | ||
| 277 | + <!-- 发送按钮 --> | ||
| 278 | + <nut-button | ||
| 279 | + @click="sendMessageToSeller" | ||
| 280 | + block | ||
| 281 | + type="primary" | ||
| 282 | + shape="round" | ||
| 283 | + style="background-color: #f97316; border-color: #f97316;" | ||
| 284 | + :disabled="!messageText.trim()" | ||
| 285 | + > | ||
| 286 | + 发送消息 | ||
| 287 | + </nut-button> | ||
| 288 | + </view> | ||
| 289 | + </nut-popup> | ||
| 290 | + | ||
| 291 | + <!-- 支付组件 --> | ||
| 292 | + <payCard :visible="show_pay" :data="payData" @close="onPayClose" /> | ||
| 293 | + </view> | ||
| 294 | +</template> | ||
| 295 | + | ||
| 296 | +<script setup> | ||
| 297 | +import { ref } from 'vue' | ||
| 298 | +import Taro from '@tarojs/taro' | ||
| 299 | +import { Share, Heart1, HeartFill, Right } from '@nutui/icons-vue-taro' | ||
| 300 | +import payCard from '@/components/payCard.vue' | ||
| 301 | +import avatarImg from '@/assets/images/avatar.png' | ||
| 302 | + | ||
| 303 | +// 分享功能 | ||
| 304 | +wx.showShareMenu({ | ||
| 305 | + withShareTicket: true, | ||
| 306 | + menus: ['shareAppMessage', 'shareTimeline'] | ||
| 307 | +}) | ||
| 308 | + | ||
| 309 | +// 响应式数据 | ||
| 310 | +const currentImageIndex = ref(0) | ||
| 311 | +const isFavorite = ref(false) | ||
| 312 | +const showWechat = ref(false) | ||
| 313 | +const showContactModal = ref(false) | ||
| 314 | +const show_pay = ref(false) | ||
| 315 | +const messageText = ref('') | ||
| 316 | +const payData = ref({ | ||
| 317 | + id: '', | ||
| 318 | + price: 0, | ||
| 319 | + remain_time: 0 | ||
| 320 | +}) | ||
| 321 | + | ||
| 322 | +// 快捷标签数据 | ||
| 323 | +const quickTags = ref([ | ||
| 324 | + '你好,我对这辆车很感兴趣', | ||
| 325 | + '请问车况怎么样?', | ||
| 326 | + '可以面谈吗?', | ||
| 327 | + '价格还能商量吗?', | ||
| 328 | + '什么时候方便看车?', | ||
| 329 | + '还有其他配件吗?' | ||
| 330 | +]) | ||
| 331 | + | ||
| 332 | +// 备用图片数组 | ||
| 333 | +const fallbackImages = ref([ | ||
| 334 | + avatarImg, | ||
| 335 | + avatarImg, | ||
| 336 | + avatarImg, | ||
| 337 | + avatarImg, | ||
| 338 | + avatarImg | ||
| 339 | +]) | ||
| 340 | +const imageLoadErrors = ref(new Set()) | ||
| 341 | + | ||
| 342 | +// 模拟商品数据 | ||
| 343 | +const product = ref({ | ||
| 344 | + id: '5', | ||
| 345 | + name: '雅迪 豪华版', | ||
| 346 | + price: 3200, | ||
| 347 | + discountPercent: 8, | ||
| 348 | + images: [ | ||
| 349 | + 'https://images.unsplash.com/photo-1558981806-ec527fa84c39?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60', | ||
| 350 | + 'https://images.unsplash.com/photo-1558981285-6f0c94958bb6?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60', | ||
| 351 | + 'https://images.unsplash.com/photo-1558981403-c5f9899a28bc?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60' | ||
| 352 | + ], | ||
| 353 | + year: '2023年', | ||
| 354 | + range: '60km', | ||
| 355 | + mileage: '1200公里', | ||
| 356 | + maxSpeed: '25km/h', | ||
| 357 | + batteryHealth: 98, | ||
| 358 | + brakeCondition: 4.5, | ||
| 359 | + tireCondition: 4, | ||
| 360 | + bodyCondition: 5, | ||
| 361 | + description: '这辆雅迪豪华版电动车是我去年购买的,一直很爱惜。电池健康度保持在98%,行驶里程仅1200公里,车身几乎无划痕,配置齐全,包括原装充电器、车锁、后视镜等。因为毕业需要离开学校,所以忍痛出售,价格比市场价低8%,非常划算。', | ||
| 362 | + richDescription: ` | ||
| 363 | + <div style="line-height: 1.6; color: #333;"> | ||
| 364 | + <p style="margin-bottom: 12px;">这辆<strong style="color: #f97316;">雅迪豪华版电动车</strong>是我去年购买的,一直很爱惜。</p> | ||
| 365 | + <p style="margin-bottom: 12px;">🔋 电池健康度保持在<span style="color: #10b981; font-weight: bold;">98%</span></p> | ||
| 366 | + <p style="margin-bottom: 12px;">🛣️ 行驶里程仅<span style="color: #3b82f6; font-weight: bold;">1200公里</span></p> | ||
| 367 | + <p style="margin-bottom: 12px;">✨ 车身几乎无划痕,配置齐全</p> | ||
| 368 | + <ul style="margin: 12px 0; padding-left: 20px;"> | ||
| 369 | + <li style="margin-bottom: 6px;">✅ 原装充电器</li> | ||
| 370 | + <li style="margin-bottom: 6px;">✅ 车锁</li> | ||
| 371 | + <li style="margin-bottom: 6px;">✅ 后视镜</li> | ||
| 372 | + <li style="margin-bottom: 6px;">✅ 脚踏板</li> | ||
| 373 | + </ul> | ||
| 374 | + <p style="margin-bottom: 12px; background: #fef3c7; padding: 8px; border-radius: 6px;">💰 因为毕业需要离开学校,所以忍痛出售,价格比市场价低8%,非常划算!</p> | ||
| 375 | + </div> | ||
| 376 | + `, | ||
| 377 | + seller: { | ||
| 378 | + name: '李同学', | ||
| 379 | + verified: true, | ||
| 380 | + school: '上海理工大学-本部', | ||
| 381 | + avatar: 'https://randomuser.me/api/portraits/men/32.jpg', | ||
| 382 | + wechat: 'li_student_2023', | ||
| 383 | + phone: '138****8888' | ||
| 384 | + } | ||
| 385 | +}) | ||
| 386 | + | ||
| 387 | +/** | ||
| 388 | + * 轮播图切换事件 | ||
| 389 | + * @param {number} index - 当前图片索引 | ||
| 390 | + */ | ||
| 391 | +const onSwiperChange = (index) => { | ||
| 392 | + currentImageIndex.value = index | ||
| 393 | +} | ||
| 394 | + | ||
| 395 | +/** | ||
| 396 | + * 切换收藏状态 | ||
| 397 | + */ | ||
| 398 | +const toggleFavorite = () => { | ||
| 399 | + isFavorite.value = !isFavorite.value | ||
| 400 | + Taro.showToast({ | ||
| 401 | + title: isFavorite.value ? '已收藏' : '已取消收藏', | ||
| 402 | + icon: 'none' | ||
| 403 | + }) | ||
| 404 | +} | ||
| 405 | + | ||
| 406 | +/** | ||
| 407 | + * 显示微信号弹框 | ||
| 408 | + */ | ||
| 409 | +const showWechatModal = () => { | ||
| 410 | + showWechat.value = true | ||
| 411 | +} | ||
| 412 | + | ||
| 413 | +/** | ||
| 414 | + * 复制微信号 | ||
| 415 | + */ | ||
| 416 | +const copyWechat = () => { | ||
| 417 | + Taro.setClipboardData({ | ||
| 418 | + data: product.value.seller.wechat, | ||
| 419 | + success: () => { | ||
| 420 | + Taro.showToast({ | ||
| 421 | + title: '微信号已复制', | ||
| 422 | + icon: 'success' | ||
| 423 | + }) | ||
| 424 | + showWechat.value = false | ||
| 425 | + }, | ||
| 426 | + }) | ||
| 427 | +} | ||
| 428 | + | ||
| 429 | +/** | ||
| 430 | + * 联系卖家 | ||
| 431 | + */ | ||
| 432 | +const handleContactSeller = () => { | ||
| 433 | + showContactModal.value = true | ||
| 434 | +} | ||
| 435 | + | ||
| 436 | +/** | ||
| 437 | + * 添加快捷标签到输入框 | ||
| 438 | + * @param {string} tag - 快捷标签文本 | ||
| 439 | + */ | ||
| 440 | +const addQuickTag = (tag) => { | ||
| 441 | + if (messageText.value.trim()) { | ||
| 442 | + messageText.value += ' ' + tag | ||
| 443 | + } else { | ||
| 444 | + messageText.value = tag | ||
| 445 | + } | ||
| 446 | +} | ||
| 447 | + | ||
| 448 | +/** | ||
| 449 | + * 发送消息给卖家 | ||
| 450 | + */ | ||
| 451 | +const sendMessageToSeller = () => { | ||
| 452 | + if (!messageText.value.trim()) { | ||
| 453 | + Taro.showToast({ | ||
| 454 | + title: '请输入留言内容', | ||
| 455 | + icon: 'none' | ||
| 456 | + }) | ||
| 457 | + return | ||
| 458 | + } | ||
| 459 | + | ||
| 460 | + Taro.showToast({ | ||
| 461 | + title: '消息发送成功', | ||
| 462 | + icon: 'success' | ||
| 463 | + }) | ||
| 464 | + | ||
| 465 | + // 清空输入框并关闭弹框 | ||
| 466 | + messageText.value = '' | ||
| 467 | + showContactModal.value = false | ||
| 468 | + | ||
| 469 | + // 这里可以调用API发送消息 | ||
| 470 | + // sendMessageAPI({ | ||
| 471 | + // sellerId: product.value.seller.id, | ||
| 472 | + // message: messageText.value, | ||
| 473 | + // productId: product.value.id | ||
| 474 | + // }) | ||
| 475 | +} | ||
| 476 | + | ||
| 477 | +/** | ||
| 478 | + * 购买商品 | ||
| 479 | + */ | ||
| 480 | +const handlePurchase = () => { | ||
| 481 | + onPay({ | ||
| 482 | + id: product.value.id, | ||
| 483 | + remain_time: 1800, // 30分钟 | ||
| 484 | + price: product.value.price | ||
| 485 | + }) | ||
| 486 | +} | ||
| 487 | + | ||
| 488 | +/** | ||
| 489 | + * 发送订单支付信息到支付组件 | ||
| 490 | + * @param {Object} payInfo - 支付信息 | ||
| 491 | + * @param {string} payInfo.id - 订单ID | ||
| 492 | + * @param {number} payInfo.remain_time - 剩余时间 | ||
| 493 | + * @param {number} payInfo.price - 价格 | ||
| 494 | + */ | ||
| 495 | +const onPay = ({ id, remain_time, price }) => { | ||
| 496 | + show_pay.value = true | ||
| 497 | + payData.value.id = id | ||
| 498 | + payData.value.price = price | ||
| 499 | + payData.value.remain_time = remain_time | ||
| 500 | +} | ||
| 501 | + | ||
| 502 | +/** | ||
| 503 | + * 关闭支付弹框 | ||
| 504 | + */ | ||
| 505 | +const onPayClose = () => { | ||
| 506 | + show_pay.value = false | ||
| 507 | +} | ||
| 508 | + | ||
| 509 | +/** | ||
| 510 | + * 图片加载成功 | ||
| 511 | + */ | ||
| 512 | +const onImageLoad = () => { | ||
| 513 | + // 图片加载成功 | ||
| 514 | +} | ||
| 515 | + | ||
| 516 | +/** | ||
| 517 | + * 图片加载失败处理 | ||
| 518 | + */ | ||
| 519 | +const onImageError = (e) => { | ||
| 520 | + const target = e.target || e.currentTarget | ||
| 521 | + const src = target.src | ||
| 522 | + | ||
| 523 | + // 记录加载失败的图片 | ||
| 524 | + imageLoadErrors.value.add(src) | ||
| 525 | + | ||
| 526 | + // 如果不是备用图片,则替换为备用图片 | ||
| 527 | + if (!src.includes('avatar.png')) { | ||
| 528 | + const imageIndex = product.value.images.findIndex(img => img === src) | ||
| 529 | + if (imageIndex !== -1 && fallbackImages.value[imageIndex]) { | ||
| 530 | + // 替换为备用图片 | ||
| 531 | + product.value.images[imageIndex] = fallbackImages.value[imageIndex] | ||
| 532 | + } | ||
| 533 | + } | ||
| 534 | + | ||
| 535 | + Taro.showToast({ | ||
| 536 | + title: '图片加载失败,已使用备用图片', | ||
| 537 | + icon: 'none', | ||
| 538 | + duration: 2000 | ||
| 539 | + }) | ||
| 540 | +} | ||
| 541 | +</script> | ||
| 542 | + | ||
| 543 | +<style lang="less"> | ||
| 544 | +.product-detail-page { | ||
| 545 | + background-color: #f5f5f5; | ||
| 546 | + min-height: 100vh; | ||
| 547 | + padding-bottom: 120rpx; | ||
| 548 | +} | ||
| 549 | + | ||
| 550 | +.image-carousel { | ||
| 551 | + .nut-swiper { | ||
| 552 | + .nut-swiper-pagination { | ||
| 553 | + bottom: 20rpx; | ||
| 554 | + } | ||
| 555 | + } | ||
| 556 | +} | ||
| 557 | + | ||
| 558 | +.space-x-3 > view:not(:first-child) { | ||
| 559 | + margin-left: 0.75rem; | ||
| 560 | +} | ||
| 561 | + | ||
| 562 | +.space-x-6 > view:not(:first-child) { | ||
| 563 | + margin-left: 1.5rem; | ||
| 564 | +} | ||
| 565 | + | ||
| 566 | +.space-y-3 > view:not(:first-child) { | ||
| 567 | + margin-top: 0.75rem; | ||
| 568 | +} | ||
| 569 | + | ||
| 570 | +.grid { | ||
| 571 | + display: grid; | ||
| 572 | +} | ||
| 573 | + | ||
| 574 | +.grid-cols-2 { | ||
| 575 | + grid-template-columns: repeat(2, 1fr); | ||
| 576 | +} | ||
| 577 | + | ||
| 578 | +.gap-4 { | ||
| 579 | + gap: 1rem; | ||
| 580 | +} | ||
| 581 | + | ||
| 582 | +.rich-content { | ||
| 583 | + line-height: 1.6; | ||
| 584 | + | ||
| 585 | + p { | ||
| 586 | + margin-bottom: 12rpx; | ||
| 587 | + } | ||
| 588 | + | ||
| 589 | + ul { | ||
| 590 | + margin: 12rpx 0; | ||
| 591 | + padding-left: 20rpx; | ||
| 592 | + | ||
| 593 | + li { | ||
| 594 | + margin-bottom: 6rpx; | ||
| 595 | + } | ||
| 596 | + } | ||
| 597 | +} | ||
| 598 | + | ||
| 599 | +.contact-modal { | ||
| 600 | + .seller-card { | ||
| 601 | + background-color: #f9fafb; | ||
| 602 | + } | ||
| 603 | + | ||
| 604 | + .message-input { | ||
| 605 | + .nut-textarea { | ||
| 606 | + border-radius: 12rpx; | ||
| 607 | + border: 1px solid #e5e7eb; | ||
| 608 | + | ||
| 609 | + &:focus { | ||
| 610 | + border-color: #f97316; | ||
| 611 | + box-shadow: 0 0 0 2px rgba(249, 115, 22, 0.1); | ||
| 612 | + } | ||
| 613 | + } | ||
| 614 | + } | ||
| 615 | + | ||
| 616 | + .quick-tags { | ||
| 617 | + .quick-tag { | ||
| 618 | + transition: all 0.2s ease; | ||
| 619 | + border: 1px solid #fed7aa; | ||
| 620 | + | ||
| 621 | + &:hover { | ||
| 622 | + background-color: #f97316; | ||
| 623 | + color: white; | ||
| 624 | + transform: translateY(-1px); | ||
| 625 | + } | ||
| 626 | + | ||
| 627 | + &:active { | ||
| 628 | + transform: translateY(0); | ||
| 629 | + } | ||
| 630 | + } | ||
| 631 | + } | ||
| 632 | + | ||
| 633 | + .gap-2 { | ||
| 634 | + gap: 8rpx; | ||
| 635 | + } | ||
| 636 | + | ||
| 637 | + .flex-wrap { | ||
| 638 | + flex-wrap: wrap; | ||
| 639 | + } | ||
| 640 | +} | ||
| 641 | +</style> |
-
Please register or login to post a comment