hookehuyr

feat(productDetail): 新增商品详情页及相关组件

添加商品详情页面,包含图片轮播、基本信息展示、车辆评估、卖家信息等功能模块
引入支付组件payCard,实现订单支付流程
更新app.config.js添加新页面路由
优化多个页面的收藏图标统一使用Heart1组件
新增底部操作栏样式和深色模式适配
...@@ -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 +}
......
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 // 响应式数据
......
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 +}
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 +}
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>