6 changed files with 779 additions and 25 deletions
@ -0,0 +1,114 @@ |
|||
import Vue from 'vue' |
|||
import axios from "axios"; |
|||
import { Message, MessageBox, Loading } from "element-ui"; // 引入 Element UI 组件
|
|||
import store from '@/store'; |
|||
|
|||
const http = axios.create({ |
|||
timeout: 6000 // 请求超时时间
|
|||
}) |
|||
|
|||
// 添加请求拦截器
|
|||
http.interceptors.request.use((config) => { |
|||
const { customBaseURL } = config.params || {}; |
|||
if (customBaseURL) { |
|||
config.baseURL = customBaseURL; |
|||
delete config.params.customBaseURL; |
|||
} else { |
|||
config.baseURL = process.env.VUE_APP_URL; |
|||
} |
|||
|
|||
const token = store.state.user.userInfo.token; |
|||
config.headers['token'] = token |
|||
config.headers['Content-Type'] = 'application/json;charset=UTF-8'; |
|||
|
|||
// 显示加载中状态(Element UI 的 Loading)
|
|||
if (config.loading !== false) { // 默认显示,可通过参数关闭
|
|||
config.loadingInstance = Loading.service({ |
|||
lock: true, |
|||
text: '加载中...', |
|||
background: 'rgba(0, 0, 0, 0.7)' |
|||
}); |
|||
} |
|||
|
|||
return config; |
|||
}, (error) => { |
|||
return Promise.reject(error); |
|||
}); |
|||
|
|||
// 添加响应拦截器
|
|||
http.interceptors.response.use(response => { |
|||
// 关闭加载状态
|
|||
if (response.config.loadingInstance) { |
|||
response.config.loadingInstance.close(); |
|||
} |
|||
|
|||
if (response.status === 200 || response.status === 1) { |
|||
return response.data; |
|||
} |
|||
}, error => { |
|||
// 关闭加载状态
|
|||
if (error.config && error.config.loadingInstance) { |
|||
error.config.loadingInstance.close(); |
|||
} |
|||
|
|||
if (error.response && error.response.status) { |
|||
switch (error.response.status) { |
|||
case 401: |
|||
MessageBox.confirm('请登录后操作', '提示', { |
|||
confirmButtonText: '去登录', |
|||
cancelButtonText: '取消', |
|||
type: 'warning' |
|||
}).then(() => { |
|||
// 登录操作
|
|||
}).catch(() => { |
|||
// 取消登录回调
|
|||
}); |
|||
break; |
|||
case 404: |
|||
Message({ |
|||
message: '网络繁忙,请刷新再试', |
|||
type: 'error', |
|||
duration: 2000 |
|||
}); |
|||
break; |
|||
default: |
|||
Message({ |
|||
message: '网络繁忙,请刷新再试', |
|||
type: 'error', |
|||
duration: 2000 |
|||
}); |
|||
break; |
|||
} |
|||
} |
|||
return Promise.reject(error); |
|||
}); |
|||
|
|||
// 请求方法挂载
|
|||
Vue.prototype.get = (params, url, loading = true) => { |
|||
return new Promise((resolve, reject) => { |
|||
http.get(url, { |
|||
params, |
|||
loading // 传递加载状态参数
|
|||
}) |
|||
.then(res => { |
|||
resolve(res); |
|||
}) |
|||
.catch(err => { |
|||
reject(err); |
|||
}); |
|||
}) |
|||
} |
|||
|
|||
Vue.prototype.post = (data, url, loading = true) => { |
|||
return new Promise((resolve, reject) => { |
|||
http.post(url, data, { |
|||
loading // 传递加载状态参数
|
|||
}) |
|||
.then(res => { |
|||
resolve(res); |
|||
}) |
|||
.catch(err => { |
|||
reject(err); |
|||
}); |
|||
}) |
|||
} |
@ -0,0 +1,128 @@ |
|||
export default { |
|||
install(Vue) { |
|||
Vue.prototype.util = { |
|||
// 格式化富文本
|
|||
formateRichText(str) { |
|||
if (!str) return ""; |
|||
var reg = new RegExp("<img", "g"); |
|||
str = str.replace(reg, "<img class='sz-xcx-fwb-img' width='100%'") |
|||
reg = new RegExp("<IMG", "g"); |
|||
str = str.replace(reg, "<img class='sz-xcx-fwb-img' width='100%'") |
|||
reg = new RegExp(" ", "g"); |
|||
str = str.replace(reg, '<span style="width: 8rpx;display: inline-block;"></span>') |
|||
reg = new RegExp("section", "g"); |
|||
str = str.replace(reg, 'div'); |
|||
reg = new RegExp("↵", "g"); |
|||
str = str.replace(reg, '<br />'); |
|||
str = str.replace(/<table/g, '<table border="1" cellspacing="0" style="border-collapse:collapse"') |
|||
return str; |
|||
}, |
|||
// 手机号验证规则
|
|||
mobileValid(val) { |
|||
return /^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$/.test(val); |
|||
}, |
|||
// 身份证验证规则
|
|||
idNumberValid(val) { |
|||
return /^\d{17}(\d{1}|[X|x])$/.test(val); |
|||
}, |
|||
// 护照验证正则
|
|||
passportValid(val) { |
|||
return /^([a-zA-z]|[0-9]){5,17}$/.test(val); |
|||
}, |
|||
// 台胞证正则
|
|||
taiwanValid(val) { |
|||
return /^\d{8}|^[a-zA-Z0-9]{10}|^\d{18}$/.test(val); |
|||
}, |
|||
// 港澳通行证正则
|
|||
gangaoValid(val) { |
|||
return /^([A-Z]\d{6,10}(\(\w{1}\))?)$/.test(val); |
|||
}, |
|||
// 外国人永久居留证正则
|
|||
foreignerValid(val) { |
|||
return /(^[A-Za-z]{3})([0-9]{12}$)/.test(val); |
|||
}, |
|||
// 军官证正则
|
|||
officerValid(val) { |
|||
return /^[\u4E00-\u9FA5](字第)([0-9a-zA-Z]{4,8})(号?)$/.test(val); |
|||
}, |
|||
// 邮箱验证正则
|
|||
emailValid(val) { |
|||
return /^[A-Za-z0-9\u4e00-\u9fa5]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/.test(val) |
|||
}, |
|||
// 获取路径参数
|
|||
getUrlPara(url) { |
|||
let arrUrl = url.split("?"); |
|||
let para = arrUrl[1]; |
|||
return para ? para.split('&') : false; |
|||
}, |
|||
openMap(item) { |
|||
let data = { |
|||
type: 'map', |
|||
lon: item.scene_lon, |
|||
lat: item.scene_lat, |
|||
name: item.title, |
|||
address: item.address |
|||
} |
|||
uni.navigateTo({ |
|||
url: '/subPackages/h5Web/h5Web?data=' + JSON.stringify(data) |
|||
}) |
|||
}, |
|||
showImg(img) { |
|||
if(!img) return; |
|||
if (img.indexOf('https://') != -1 || img.indexOf('http://') != -1) { |
|||
return img; |
|||
} else { |
|||
return 'https://changshu.js-dyyj.com' + img; |
|||
} |
|||
}, |
|||
// 跳回小程序
|
|||
gotoDetailMini(item) { |
|||
console.log(item) |
|||
if(item.link_type == 1) { |
|||
// 外部小程序
|
|||
let data = { |
|||
type: 'xcx', |
|||
url: item.ext_link |
|||
} |
|||
uni.navigateTo({ |
|||
url: '/subPackages/h5Web/h5Web?data=' + JSON.stringify(data) |
|||
}) |
|||
return |
|||
}else if(item.link_type == 2){ |
|||
// 外部H5
|
|||
// window.location.href = item.ext_link
|
|||
window.location.href = 'https://m.cloud.sz-trip.com/MailMerchandiseDetail?type=ticket&platform=changshu&id=' + item.id |
|||
return |
|||
} |
|||
// switch (item.genre){
|
|||
// // 景点
|
|||
// case 'ticket':
|
|||
// uni.navigateTo({
|
|||
// url: '/subPackages/ticketBooking/detail?id=' + item.id
|
|||
// })
|
|||
// break;
|
|||
// // 酒店
|
|||
// case 'hotel':
|
|||
// uni.navigateTo({
|
|||
// url: '/subPackages/hotelHomestay/detail?id=' + item.id
|
|||
// })
|
|||
// break;
|
|||
// // 美食
|
|||
// case 'food':
|
|||
// uni.navigateTo({
|
|||
// url: '/subPackages/food/foodDetail?id=' + item.id
|
|||
// })
|
|||
// break;
|
|||
// // 攻略
|
|||
// case 'article':
|
|||
// uni.navigateTo({
|
|||
// url: '/subPackages/travelGuide/detail?id=' + item.id
|
|||
// })
|
|||
// break;
|
|||
// default:
|
|||
// break;
|
|||
// }
|
|||
} |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,521 @@ |
|||
<template> |
|||
<div> |
|||
<div class="product-detail-container"> |
|||
<!-- 左侧图片轮播区域 --> |
|||
<div class="left-section"> |
|||
<el-carousel |
|||
ref="carousel" |
|||
height="500px" |
|||
class="product-carousel" |
|||
@change="handleCarouselChange" |
|||
indicator-position="none" |
|||
> |
|||
<el-carousel-item v-for="(img, index) in productImages" :key="index"> |
|||
<img |
|||
:src="img" |
|||
:alt="`商品图片${index + 1}`" |
|||
class="carousel-img" |
|||
/> |
|||
</el-carousel-item> |
|||
</el-carousel> |
|||
<div class="hint-text"> |
|||
温馨提示:以上图片仅供参考,若图片与实物有所不同,则以实物为准。 |
|||
</div> |
|||
<!-- 自定义图片指示器 --> |
|||
<div class="image-indicators"> |
|||
<div |
|||
v-for="(img, index) in productImages" |
|||
:key="index" |
|||
class="indicator-item" |
|||
:class="{ active: activeIndex === index }" |
|||
@click="handleIndicatorClick(index)" |
|||
> |
|||
<img :src="img" :alt="`缩略图${index + 1}`" class="indicator-img" /> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 右侧商品信息区域保持不变 --> |
|||
<div class="right-section"> |
|||
<h2 class="product-title"> |
|||
{{ productTitle }} |
|||
<span class="product-count">[{{ productCount }}]</span> |
|||
</h2> |
|||
|
|||
<div class="product-tags"> |
|||
<el-tag type="info" size="mini">[产品标签]</el-tag> |
|||
<span class="subtitle">{{ productSubtitle }}</span> |
|||
</div> |
|||
|
|||
<div class="price-info"> |
|||
<span class="price-label">售价</span> |
|||
<span class="price-amount">¥{{ productPrice }}</span> |
|||
<span class="sales-volume">已售 {{ salesVolume }}万</span> |
|||
</div> |
|||
|
|||
<div class="product-attr"> |
|||
<div class="attr-item"> |
|||
<span class="attr-label">起订量</span> |
|||
<span class="attr-value">{{ moq }}</span> |
|||
</div> |
|||
|
|||
<div class="attr-item"> |
|||
<span class="attr-label">收货方式</span> |
|||
<span class="attr-value">{{ deliveryMethod }}</span> |
|||
</div> |
|||
|
|||
<div class="attr-item spec-group"> |
|||
<span class="attr-label">商品规格</span> |
|||
<div class="custom-radio-group"> |
|||
<label |
|||
v-for="(spec, idx) in productSpecs" |
|||
:key="idx" |
|||
class="custom-radio" |
|||
:class="{ 'is-checked': selectedSpec === spec }" |
|||
@click="selectedSpec = spec" |
|||
> |
|||
<span class="radio-text">{{ spec }}</span> |
|||
</label> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="attr-item"> |
|||
<span class="attr-label">发货地</span> |
|||
<span class="attr-value">{{ origin }}</span> |
|||
</div> |
|||
|
|||
<div class="attr-item"> |
|||
<span class="attr-label">其他</span> |
|||
<span class="attr-value">{{ otherInfo }}</span> |
|||
</div> |
|||
|
|||
<div class="attr-item"> |
|||
<span class="attr-label">配送范围</span> |
|||
<span class="attr-value">{{ deliveryRange }}</span> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="quantity-control"> |
|||
<el-button |
|||
icon="el-icon-minus" |
|||
circle |
|||
@click="decreaseQuantity" |
|||
></el-button> |
|||
<span class="quantity-value">{{ quantity }}</span> |
|||
<el-button |
|||
icon="el-icon-plus" |
|||
circle |
|||
@click="increaseQuantity" |
|||
></el-button> |
|||
<el-button type="primary" class="buy-btn">一口价购买</el-button> |
|||
<el-button type="success" class="cart-btn">加入购物车</el-button> |
|||
<el-button type="info" class="bargain-btn">议价</el-button> |
|||
<el-button |
|||
icon="el-icon-share" |
|||
circle |
|||
class="share-btn" |
|||
@click="handleShare" |
|||
></el-button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="product-bottom"> |
|||
<!-- 左侧热销模块 --> |
|||
<div class="hot-recommend-sidebar"> |
|||
<div class="hot-title">热销推荐</div> |
|||
<div |
|||
v-for="(item, index) in hotRecommendData" |
|||
:key="index" |
|||
class="product-item" |
|||
> |
|||
<img v-lazy="item.imgUrl" alt="" /> |
|||
<div class="product-name">{{ item.title }}</div> |
|||
<div class="product-price">¥{{ item.price }}</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 右侧 --> |
|||
<div class="product-right"> |
|||
<div class="product-tabs"> |
|||
<span class="tab-item active">商品详情</span> |
|||
<span class="tab-item">商品评价(125)</span> |
|||
</div> |
|||
|
|||
<!-- 商品详情 --> |
|||
<div class="product-detail-main"></div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: "ProductDetail", |
|||
data() { |
|||
return { |
|||
productImages: [ |
|||
"https://picsum.photos/id/102/500/500", |
|||
"https://picsum.photos/id/103/500/500", |
|||
"https://picsum.photos/id/104/500/500", |
|||
"https://picsum.photos/id/105/500/500", |
|||
], |
|||
activeIndex: 0, // 当前激活的图片索引 |
|||
productTitle: "面包", |
|||
productCount: "52个", |
|||
productSubtitle: "副标题", |
|||
productPrice: 509, |
|||
salesVolume: 1.22, |
|||
moq: 1, |
|||
deliveryMethod: "邮寄", |
|||
productSpecs: ["规格一", "规格二", "规格三", "规格四", "规格五"], |
|||
selectedSpec: "规格一", |
|||
origin: "江苏省苏州市吴中区", |
|||
otherInfo: "下单填写留言,即免费赠送精美贺卡!", |
|||
deliveryRange: "全国(可配送至全国1000多个城市,苏州市区内免配送费)", |
|||
quantity: 1, |
|||
hotRecommendData: [ |
|||
{ |
|||
id: 1, |
|||
imgUrl: "https://picsum.photos/id/103/500/500", // 替换成实际图片地址,也可用本地相对路径 |
|||
title: "北欧花艺素雅仿真花", |
|||
price: 359, |
|||
}, |
|||
{ |
|||
id: 2, |
|||
imgUrl: "https://picsum.photos/id/103/500/500", |
|||
title: "生日玫瑰鲜花", |
|||
price: 359, |
|||
}, |
|||
{ |
|||
id: 3, |
|||
imgUrl: "https://picsum.photos/id/103/500/500", |
|||
title: "香雪兰小苍兰鲜花", |
|||
price: 359, |
|||
}, |
|||
{ |
|||
id: 4, |
|||
imgUrl: "https://picsum.photos/id/103/500/500", |
|||
title: "现代创意简约仿真花艺", |
|||
price: 359, |
|||
}, |
|||
], |
|||
}; |
|||
}, |
|||
methods: { |
|||
// 处理轮播图切换事件 - 同步更新activeIndex |
|||
handleCarouselChange(index) { |
|||
this.activeIndex = index; |
|||
}, |
|||
|
|||
// 处理指示器点击事件 - 修复切换功能 |
|||
handleIndicatorClick(index) { |
|||
// 1. 更新当前激活索引 |
|||
this.activeIndex = index; |
|||
|
|||
// 2. 关键修复:确保轮播组件已加载,再调用切换方法 |
|||
this.$nextTick(() => { |
|||
if (this.$refs.carousel) { |
|||
// 调用Element UI轮播组件的官方方法切换图片 |
|||
this.$refs.carousel.setActiveItem(index); |
|||
} |
|||
}); |
|||
}, |
|||
|
|||
decreaseQuantity() { |
|||
if (this.quantity > 1) { |
|||
this.quantity--; |
|||
} |
|||
}, |
|||
|
|||
increaseQuantity() { |
|||
this.quantity++; |
|||
}, |
|||
|
|||
handleShare() { |
|||
this.$message.info("分享功能待实现"); |
|||
}, |
|||
}, |
|||
mounted() { |
|||
// 初始化检查轮播组件是否存在 |
|||
if (!this.$refs.carousel) { |
|||
console.warn("轮播组件未正确加载,请检查ref属性是否设置"); |
|||
} |
|||
}, |
|||
}; |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.product-detail-container { |
|||
display: flex; |
|||
padding: 20px; |
|||
background-color: #fff; |
|||
border: 1px solid #eaeaea; |
|||
border-radius: 4px; |
|||
|
|||
.left-section { |
|||
width: 40%; |
|||
margin-right: 20px; |
|||
display: flex; |
|||
flex-direction: column; |
|||
|
|||
.product-carousel { |
|||
border: 1px solid #eaeaea; |
|||
border-radius: 4px; |
|||
margin-bottom: 15px; |
|||
|
|||
.carousel-img { |
|||
width: 100%; |
|||
height: 100%; |
|||
object-fit: cover; |
|||
} |
|||
} |
|||
|
|||
.hint-text { |
|||
color: #ccc; |
|||
font-size: 12px; |
|||
margin-bottom: 10px; |
|||
} |
|||
|
|||
.image-indicators { |
|||
display: flex; |
|||
gap: 10px; |
|||
justify-content: center; |
|||
padding: 5px 0; |
|||
|
|||
.indicator-item { |
|||
width: 80px; |
|||
height: 80px; |
|||
cursor: pointer; |
|||
border: 2px solid transparent; |
|||
border-radius: 4px; |
|||
transition: all 0.3s ease; |
|||
overflow: hidden; |
|||
|
|||
&.active { |
|||
border-color: #409eff; |
|||
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2); |
|||
} |
|||
|
|||
.indicator-img { |
|||
width: 100%; |
|||
height: 100%; |
|||
object-fit: cover; |
|||
display: block; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.right-section { |
|||
width: 60%; |
|||
|
|||
.product-title { |
|||
font-size: 20px; |
|||
font-weight: bold; |
|||
margin-bottom: 10px; |
|||
|
|||
.product-count { |
|||
font-size: 14px; |
|||
color: #666; |
|||
} |
|||
} |
|||
|
|||
.product-tags { |
|||
margin-bottom: 15px; |
|||
|
|||
.subtitle { |
|||
margin-left: 5px; |
|||
color: #666; |
|||
} |
|||
} |
|||
|
|||
.price-info { |
|||
display: flex; |
|||
align-items: center; |
|||
margin-bottom: 15px; |
|||
|
|||
.price-label { |
|||
font-weight: bold; |
|||
margin-right: 10px; |
|||
} |
|||
|
|||
.price-amount { |
|||
font-size: 24px; |
|||
color: #ff4d4f; |
|||
margin-right: 20px; |
|||
} |
|||
|
|||
.sales-volume { |
|||
color: #999; |
|||
} |
|||
} |
|||
|
|||
.product-attr { |
|||
margin-bottom: 20px; |
|||
|
|||
.attr-item { |
|||
display: flex; |
|||
flex-direction: column; |
|||
margin-bottom: 15px; |
|||
|
|||
.attr-label { |
|||
font-weight: bold; |
|||
margin-bottom: 8px; |
|||
color: #333; |
|||
} |
|||
|
|||
.attr-value { |
|||
color: #666; |
|||
} |
|||
|
|||
&.spec-group { |
|||
margin-top: 20px; |
|||
margin-bottom: 20px; |
|||
|
|||
.custom-radio-group { |
|||
display: flex; |
|||
flex-wrap: wrap; |
|||
gap: 10px; |
|||
margin-top: 5px; |
|||
|
|||
.custom-radio { |
|||
display: inline-block; |
|||
padding: 8px 15px; |
|||
border: 1px solid #ddd; |
|||
border-radius: 4px; |
|||
cursor: pointer; |
|||
transition: all 0.3s ease; |
|||
background-color: #fff; |
|||
position: relative; |
|||
|
|||
&:hover { |
|||
border-color: #409eff; |
|||
} |
|||
|
|||
&.is-checked { |
|||
border-color: #409eff; |
|||
background-color: #f0f7ff; |
|||
color: #409eff; |
|||
font-weight: 500; |
|||
|
|||
&::after { |
|||
/* content: "✓"; */ |
|||
position: absolute; |
|||
right: 5px; |
|||
bottom: 2px; |
|||
font-size: 12px; |
|||
color: #409eff; |
|||
} |
|||
} |
|||
|
|||
.radio-text { |
|||
user-select: none; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.quantity-control { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 10px; |
|||
margin-top: 30px; |
|||
|
|||
.quantity-value { |
|||
width: 40px; |
|||
text-align: center; |
|||
} |
|||
|
|||
.buy-btn, |
|||
.cart-btn, |
|||
.bargain-btn { |
|||
margin-right: 10px; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.product-bottom { |
|||
margin-top: 50px; |
|||
display: flex; |
|||
|
|||
.hot-recommend-sidebar { |
|||
min-width: 200px; |
|||
padding: 10px; |
|||
// border: 1px solid #eee; |
|||
background-color: #f7f9fa; |
|||
margin-right: 20px; |
|||
|
|||
.hot-title { |
|||
border-left: 4px solid #ff4d4f; |
|||
padding-left: 10px; |
|||
margin-bottom: 20px; |
|||
} |
|||
|
|||
.product-item { |
|||
margin-bottom: 20px; |
|||
text-align: center; |
|||
|
|||
img { |
|||
width: 100%; |
|||
height: auto; |
|||
margin-bottom: 8px; |
|||
} |
|||
|
|||
.product-name { |
|||
font-size: 14px; |
|||
margin-bottom: 4px; |
|||
} |
|||
|
|||
.product-price { |
|||
font-size: 12px; |
|||
color: #f40; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.product-right { |
|||
width: 1000%; |
|||
} |
|||
.product-tabs { |
|||
display: flex; |
|||
// border-bottom: 1px solid #eee; |
|||
margin-bottom: 10px; |
|||
background-color: #f7f9fa; |
|||
padding: 10px; |
|||
|
|||
.tab-item { |
|||
padding: 10px 20px; |
|||
cursor: pointer; |
|||
margin-right: 10px; |
|||
color: #333; |
|||
|
|||
&.active { |
|||
color: #ff4d4f; |
|||
border-bottom: 2px solid #ff4d4f; |
|||
} |
|||
} |
|||
} |
|||
.product-detail-main { |
|||
flex: 1; |
|||
padding: 10px; |
|||
background-color: #fff; |
|||
|
|||
.product-banner { |
|||
width: 100%; |
|||
height: auto; |
|||
margin-bottom: 20px; |
|||
} |
|||
|
|||
.product-desc { |
|||
font-size: 14px; |
|||
line-height: 1.6; |
|||
color: #666; |
|||
} |
|||
} |
|||
} |
|||
</style> |
|||
|
Loading…
Reference in new issue