feat(productDetail): 新增商品详情页及相关组件
添加商品详情页面,包含图片轮播、基本信息展示、车辆评估、卖家信息等功能模块 引入支付组件payCard,实现订单支付流程 更新app.config.js添加新页面路由 优化多个页面的收藏图标统一使用Heart1组件 新增底部操作栏样式和深色模式适配
Showing
12 changed files
with
504 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
This diff is collapsed. Click to expand it.
-
Please register or login to post a comment