hookehuyr

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

添加商品详情页面,包含图片轮播、基本信息展示、车辆评估、卖家信息等功能模块
引入支付组件payCard,实现订单支付流程
更新app.config.js添加新页面路由
优化多个页面的收藏图标统一使用Heart1组件
新增底部操作栏样式和深色模式适配
......@@ -8,10 +8,12 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
NavBar: typeof import('./src/components/navBar.vue')['default']
NutActionSheet: typeof import('@nutui/nutui-taro')['ActionSheet']
NutButton: typeof import('@nutui/nutui-taro')['Button']
NutCol: typeof import('@nutui/nutui-taro')['Col']
NutConfigProvider: typeof import('@nutui/nutui-taro')['ConfigProvider']
NutDatePicker: typeof import('@nutui/nutui-taro')['DatePicker']
NutDialog: typeof import('@nutui/nutui-taro')['Dialog']
NutForm: typeof import('@nutui/nutui-taro')['Form']
NutFormItem: typeof import('@nutui/nutui-taro')['FormItem']
NutImagePreview: typeof import('@nutui/nutui-taro')['ImagePreview']
......@@ -32,6 +34,7 @@ declare module 'vue' {
NutTabs: typeof import('@nutui/nutui-taro')['Tabs']
NutTextarea: typeof import('@nutui/nutui-taro')['Textarea']
NutToast: typeof import('@nutui/nutui-taro')['Toast']
PayCard: typeof import('./src/components/payCard.vue')['default']
Picker: typeof import('./src/components/time-picker-data/picker.vue')['default']
PosterBuilder: typeof import('./src/components/PosterBuilder/index.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
......
/*
* @Date: 2025-06-28 10:33:00
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-07-02 22:18:10
* @LastEditTime: 2025-07-03 09:34:59
* @FilePath: /jgdl/src/app.config.js
* @Description: 文件描述
*/
......@@ -18,6 +18,7 @@ export default {
'pages/setAuthCar/index',
'pages/newCarList/index',
'pages/goodCarList/index',
'pages/productDetail/index',
'pages/auth/index',
],
subpackages: [ // 配置在tabBar中的页面不能分包写到subpackages中去
......
......@@ -13,3 +13,28 @@
font-style: normal !important;
font-weight: normal !important;
}
button {
margin: 0;
padding: 0;
background-color: inherit;
position: static;
}
button:after {
content: none;
}
button::after {
border: none;
}
.bottom-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #ffffff;
padding: 24rpx 32rpx;
border-top: 1rpx solid #f3f4f6;
z-index: 100;
}
......
<!--
* @Date: 2023-12-20 14:11:11
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-07-03 11:43:29
* @FilePath: /jgdl/src/components/payCard.vue
* @Description: 文件描述
-->
<template>
<div class="pay-card">
<nut-action-sheet v-model:visible="props.visible" title="" @close="onClose">
<view style="padding: 2rem 1rem; text-align: center;">
<view style="font-size: 32rpx;">实付金额</view>
<view style="color: red; margin: 10rpx 0;"><text style="font-size: 50rpx;">¥</text><text style="font-size: 80rpx;">{{ price }}</text></view>
<view style="font-size: 28rpx; margin-bottom: 20rpx;">支付剩余时间 <text style="color: red;">{{ formatTime(remain_time) }}</text></view>
<nut-button block color="#fb923c" @tap="goToPay">立即支付</nut-button>
</view>
</nut-action-sheet>
</div>
</template>
<script setup>
import Taro from '@tarojs/taro'
import { ref, watch, onMounted, onUnmounted } from 'vue'
import { getCurrentPageUrl } from "@/utils/weapp";
import { payAPI, payCheckAPI, orderSuccessAPI } from '@/api/index'
/**
* 格式化时间
* @param {*} seconds
*/
function formatTime(seconds) {
const hours = Math.floor(seconds / 3600); // 计算小时数
const minutes = Math.floor((seconds % 3600) / 60); // 计算分钟数
const remainingSeconds = seconds % 60; // 计算剩余的秒数
const formattedHours = String(hours).padStart(2, "0"); // 格式化小时数,保证两位数
const formattedMinutes = String(minutes).padStart(2, "0"); // 格式化分钟数,保证两位数
const formattedSeconds = String(remainingSeconds).padStart(2, "0"); // 格式化剩余的秒数,保证两位数
return `${formattedHours}:${formattedMinutes}:${formattedSeconds}`;
}
const props = defineProps({
visible: {
type: Boolean,
default: false,
},
data: {
type: Object,
default: {},
},
});
const emit = defineEmits(['close']);
const onClose = () => {
emit('close');
}
const id = ref('');
const price = ref('');
const remain_time = ref('');
let timeId = null;
watch(
() => props.visible,
(val) => {
if (val) {
id.value = props.data.id;
price.value = props.data.price;
remain_time.value = props.data.remain_time;
}
}
)
onMounted(() => {
// 进入页面后,开始倒计时
timeId = setInterval(() => {
remain_time.value ? remain_time.value -= 1 : 0;
if (remain_time.value === 0) { // 倒计时结束
clearInterval(timeId);
emit('close');
}
}, 1000);
})
onUnmounted(() => {
timeId && clearInterval(timeId);
})
const goToPay = async () => {
if (price.value > 0) { // 金额大于0
// 获取支付参数
const { code, data } = await payAPI({ order_id: id.value });
if (code) {
let pay = data;
// 触发微信支付操作
wx.requestPayment({
timeStamp: pay.timeStamp,
nonceStr: pay.nonceStr,
package: pay.package,
signType: pay.signType,
paySign: pay.paySign,
success: async (result) => {
emit('close'); // 关闭支付弹框
Taro.showToast({
title: '支付成功',
icon: 'success',
duration: 1000
});
// 支付成功后,调用检查接口
const pay_success = await payCheckAPI({ order_id: id.value });
if (pay_success.code) {
let current_page = getCurrentPageUrl();
if (current_page === 'pages/my/index') { // 我的页面打开
// 刷新当前页面
Taro.reLaunch({
url: '/pages/my/index?tab_index=5'
});
}
if (current_page === 'pages/detail/index') { // 订房确认页打开
// 跳转订单成功页
Taro.navigateTo({
url: '/pages/payInfo/index',
});
}
}
}
});
}
}
}
</script>
<style lang="less">
</style>
<!--
* @Date: 2022-09-19 14:11:06
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-07-02 18:15:19
* @LastEditTime: 2025-07-03 12:40:46
* @FilePath: /jgdl/src/pages/authCar/index.vue
* @Description: 认证车源
-->
......@@ -61,8 +61,8 @@
</view>
<view class="flex-1 p-3 relative">
<view class="absolute top-3 right-4" @tap.stop="() => toggleFavorite(car.id)">
<Addfollow v-if="!favoriteIds.includes(car.id)" size="16" color="#9ca3af" />
<HeartFill v-else size="16" color="#ef4444" />
<Heart1 v-if="!favoriteIds.includes(car.id)" size="16" :color="'#9ca3af'" />
<HeartFill v-else size="16" :color="'#ef4444'" />
</view>
<text class="font-medium text-sm block">{{ car.name }}</text>
<text class="text-xs text-gray-600 mt-1 block">
......@@ -109,7 +109,7 @@
<script setup>
import Taro from '@tarojs/taro'
import { ref, computed, onMounted } from 'vue'
import { Check, RectRight, Addfollow, HeartFill } from '@nutui/icons-vue-taro'
import { Check, Addfollow, Follow, Heart1, HeartFill } from '@nutui/icons-vue-taro'
import './index.less'
// Banner图片数据
......
......@@ -68,7 +68,7 @@
</view>
<view class="flex-1 p-3 relative">
<view class="absolute top-3 right-4" @tap.stop="() => toggleFavorite(car.id)">
<Addfollow v-if="!favoriteIds.includes(car.id)" size="16" color="#9ca3af" />
<Heart1 v-if="!favoriteIds.includes(car.id)" size="16" color="#9ca3af" />
<HeartFill v-else size="16" color="#ef4444" />
</view>
<text class="font-medium text-sm block">{{ car.name }}</text>
......@@ -125,7 +125,7 @@
<script setup>
import { ref, computed, onMounted } from 'vue'
import { Search2, Addfollow, HeartFill } from '@nutui/icons-vue-taro'
import { Search2, Addfollow, Follow, Heart1, HeartFill } from '@nutui/icons-vue-taro'
import TabBar from '@/components/TabBar.vue'
import './index.less'
......
<!--
* @Date: 2025-06-28 10:33:00
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-07-02 22:58:32
* @LastEditTime: 2025-07-03 12:43:02
* @FilePath: /jgdl/src/pages/index/index.vue
* @Description: 捡个电驴首页
-->
......@@ -77,7 +77,7 @@
<image :src="scooter.imageUrl" :alt="scooter.name" mode="aspectFill"
class="w-full h-36 object-cover rounded-lg" />
<view class="absolute top-4 right-4 p-1" @tap.stop="() => toggleFavorite(scooter.id)">
<Addfollow v-if="!favoriteIds.includes(scooter.id)" size="20" :color="'#ffffff'" />
<Heart1 v-if="!favoriteIds.includes(scooter.id)" size="20" :color="'#ffffff'" />
<HeartFill v-else size="20" :color="'#ef4444'" />
</view>
<view v-if="scooter.isVerified"
......@@ -131,7 +131,7 @@
</view>
<view class="flex-1 p-3 relative">
<view class="absolute top-2 right-2" @tap.stop="() => toggleFavorite(scooter.id)">
<Addfollow v-if="!favoriteIds.includes(scooter.id)" size="16" :color="'#ffffff'" />
<Heart1 v-if="!favoriteIds.includes(scooter.id)" size="16" :color="'#ffffff'" />
<HeartFill v-else size="16" :color="'#ef4444'" />
</view>
<text class="font-medium text-sm block">{{ scooter.name }}</text>
......@@ -162,7 +162,7 @@ import Taro from '@tarojs/taro'
import '@tarojs/taro/html5.css' //和 nutui组件居然有冲突?
import { ref, onMounted } from 'vue'
import { useDidShow, useReady } from '@tarojs/taro'
import { Clock, Star, RectRight, Addfollow, HeartFill, Check, Search2, Shop } from '@nutui/icons-vue-taro'
import { Clock, Star, RectRight, Addfollow, Follow, Check, Search2, Shop, Heart1, HeartFill } from '@nutui/icons-vue-taro'
import TabBar from '@/components/TabBar.vue'
import "./index.less";
......@@ -280,9 +280,8 @@ const toggleFavorite = (scooterId) => {
* @param {Object} scooter - 电动车信息
*/
const onProductClick = (scooter) => {
Taro.showToast({
title: `查看${scooter.name}`,
icon: 'none'
Taro.navigateTo({
url: `/pages/productDetail/index?id=${scooter.id}`
})
}
......
......@@ -63,7 +63,7 @@
</view>
<view class="flex-1 p-3 relative">
<view class="absolute top-3 right-4" @tap.stop="() => toggleFavorite(car.id)">
<Addfollow v-if="!favoriteIds.includes(car.id)" size="16" color="#9ca3af" />
<Heart1 v-if="!favoriteIds.includes(car.id)" size="16" color="#9ca3af" />
<HeartFill v-else size="16" color="#ef4444" />
</view>
<text class="font-medium text-sm block">{{ car.name }}</text>
......@@ -114,7 +114,7 @@
<script setup>
import { ref, computed, onMounted } from 'vue'
import { Search2, Addfollow, HeartFill } from '@nutui/icons-vue-taro'
import { Search2, Heart1, HeartFill } from '@nutui/icons-vue-taro'
import TabBar from '@/components/TabBar.vue'
import './index.less'
......@@ -234,12 +234,12 @@ const scrollStyle = computed(() => {
* @param {string} carId - 车辆ID
*/
const toggleFavorite = (carId) => {
const index = favoriteIds.value.indexOf(carId.toString())
const index = favoriteIds.value.indexOf(carId)
if (index > -1) {
favoriteIds.value.splice(index, 1)
showToast('取消收藏', 'success')
} else {
favoriteIds.value.push(carId.toString())
favoriteIds.value.push(carId)
showToast('收藏成功', 'success')
}
}
......
......@@ -45,7 +45,7 @@
</view>
<view class="flex-1 p-3 relative">
<view class="absolute top-3 right-4" @tap.stop="() => toggleFavorite(scooter.id)">
<Addfollow v-if="!favoriteIds.includes(scooter.id)" size="16" color="#9ca3af" />
<Heart1 v-if="!favoriteIds.includes(scooter.id)" size="16" color="#9ca3af" />
<HeartFill v-else size="16" color="#ef4444" />
</view>
<text class="font-medium text-sm block">{{ scooter.name }}</text>
......@@ -96,7 +96,7 @@
<image :src="scooter.imageUrl" :alt="scooter.name" mode="aspectFill"
class="w-full h-36 object-cover rounded-lg" />
<view class="absolute top-4 right-4 p-1" @tap.stop="() => toggleFavorite(scooter.id)">
<Addfollow v-if="!favoriteIds.includes(scooter.id)" size="20" color="#ffffff" />
<Heart1 v-if="!favoriteIds.includes(scooter.id)" size="20" color="#ffffff" />
<HeartFill v-else size="20" color="#ef4444" />
</view>
<view v-if="scooter.isVerified"
......@@ -132,7 +132,7 @@
<script setup>
import { ref } from 'vue'
import Taro from '@tarojs/taro'
import { Search2, RectRight, Check, Addfollow, HeartFill } from '@nutui/icons-vue-taro'
import { Search2, RectRight, Check, Heart1, HeartFill } from '@nutui/icons-vue-taro'
import TabBar from '@/components/TabBar.vue'
// 响应式数据
......
/*
* @Date: 2025-07-03 09:34:12
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-07-03 09:34:43
* @FilePath: /jgdl/src/pages/productDetail/index.config.js
* @Description: 文件描述
*/
export default {
navigationBarTitleText: '商品详情',
usingComponents: {
},
}
// 商品详情页样式
.product-detail-page {
background-color: #f5f5f5;
min-height: 100vh;
padding-bottom: 120rpx;
}
// 图片轮播样式
.image-carousel {
.nut-swiper {
.nut-swiper-pagination {
bottom: 20rpx;
}
}
}
// 商品信息区域
.product-info {
.flex {
display: flex;
}
.items-center {
align-items: center;
}
.justify-between {
justify-content: space-between;
}
.flex-col {
flex-direction: column;
}
.space-x-4 > view:not(:first-child) {
margin-left: 1rem;
}
.bg-gray-50 {
background-color: #f9fafb;
}
.rounded-full {
border-radius: 50%;
}
.p-2 {
padding: 0.5rem;
}
.mt-1 {
margin-top: 0.25rem;
}
.text-xs {
font-size: 0.75rem;
line-height: 1rem;
}
.text-gray-500 {
color: #6b7280;
}
}
// 基本信息网格布局
.grid {
display: grid;
}
.grid-cols-2 {
grid-template-columns: repeat(2, 1fr);
}
.gap-4 {
gap: 1rem;
}
// 间距工具类
.space-x-3 > view:not(:first-child) {
margin-left: 0.75rem;
}
.space-x-4 > view:not(:first-child) {
margin-left: 1rem;
}
.space-y-3 > view:not(:first-child) {
margin-top: 0.75rem;
}
// 富文本内容样式
.rich-content {
line-height: 1.6;
p {
margin-bottom: 12rpx;
}
ul {
margin: 12rpx 0;
padding-left: 20rpx;
li {
margin-bottom: 6rpx;
}
}
strong {
font-weight: bold;
}
span {
display: inline;
}
}
// 联系卖家弹框样式
.contact-modal {
.seller-card {
background-color: #f9fafb;
}
.contact-options {
.nut-button {
margin-bottom: 12rpx;
&:last-child {
margin-bottom: 0;
}
}
}
}
// 微信弹框样式
.nut-dialog {
.nut-dialog-content {
.text-center {
text-align: center;
}
.p-4 {
padding: 1rem;
}
.mb-2 {
margin-bottom: 0.5rem;
}
.mb-4 {
margin-bottom: 1rem;
}
.text-lg {
font-size: 1.125rem;
line-height: 1.75rem;
}
.font-medium {
font-weight: 500;
}
.text-sm {
font-size: 0.875rem;
line-height: 1.25rem;
}
.block {
display: block;
}
}
}
// 底部按钮区域
.fixed {
position: fixed;
}
.bottom-0 {
bottom: 0;
}
.left-0 {
left: 0;
}
.right-0 {
right: 0;
}
.bg-white {
background-color: #ffffff;
}
.border-t {
border-top-width: 1px;
}
.border-gray-200 {
border-color: #e5e7eb;
}
.p-3 {
padding: 0.75rem;
}
.flex-1 {
flex: 1 1 0%;
}
// 响应式适配
@media (max-width: 768rpx) {
.product-detail-page {
padding-bottom: 140rpx;
}
.product-info {
.space-x-4 > view:not(:first-child) {
margin-left: 0.5rem;
}
.text-xs {
font-size: 0.7rem;
}
}
.grid-cols-2 {
grid-template-columns: 1fr;
gap: 0.5rem;
}
}
// 深色模式适配
@media (prefers-color-scheme: dark) {
.product-detail-page {
background-color: #1f2937;
color: #f9fafb;
}
.bg-white {
background-color: #374151;
}
.text-gray-500 {
color: #9ca3af;
}
.border-gray-200 {
border-color: #4b5563;
}
}
// 动画效果
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.product-info,
.basic-info,
.vehicle-condition,
.vehicle-description,
.seller-info {
animation: fadeInUp 0.6s ease-out;
}
// 卡片悬停效果
.product-info,
.basic-info,
.vehicle-condition,
.vehicle-description,
.seller-info {
transition: all 0.3s ease;
&:hover {
transform: translateY(-2rpx);
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
}
}
// 按钮样式增强
.nut-button {
transition: all 0.3s ease;
&:active {
transform: scale(0.98);
}
}
// 图标按钮样式
.product-info {
.bg-gray-50 {
transition: all 0.3s ease;
&:active {
background-color: #e5e7eb;
transform: scale(0.95);
}
}
}
This diff is collapsed. Click to expand it.