jiazhipeng 3 months ago
parent
commit
04b37ad57d
  1. 6
      src/components/product/ProductList.vue
  2. 114
      src/libs/axios.js
  3. 128
      src/libs/utils.js
  4. 26
      src/main.js
  5. 9
      src/router/index.js
  6. 521
      src/views/Detail/Index.vue

6
src/components/product/ProductList.vue

@ -3,7 +3,7 @@
<div class="product-grid">
<div v-for="product in products" :key="product.id" class="product-card">
<div class="product-img">
<a :href="`/product/${product.id}`" :to="`/product/${product.id}`">
<a :href="`/detail/${product.id}`" :to="`/detail/${product.id}`">
<img
v-lazy="product.image"
:alt="product.name"
@ -24,7 +24,7 @@
</div>
</div>
<h3 class="product-name">
<a :href="`/product/${product.id}`" :to="`/product/${product.id}`">
<a :href="`/detail/${product.id}`" :to="`/detail/${product.id}`">
{{ product.name }}
</a>
</h3>
@ -32,7 +32,7 @@
<el-button
type="primary"
size="small"
style="background-color: #ff6e90; border: none"
style="background-color: #6a8a27; border: none"
@click="addToCart(product)"
>
<el-icon name="el-icon-shopping-cart"></el-icon>

114
src/libs/axios.js

@ -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);
});
})
}

128
src/libs/utils.js

@ -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("&nbsp;", "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;
// }
}
}
}
}

26
src/main.js

@ -4,9 +4,12 @@ import router from './router'
import store from './store'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import axios from 'axios'
import VueLazyload from 'vue-lazyload'
import '@/assets/css/common.css'
import '../src/libs/axios.js' // axios处理
import utils from './libs/utils.js'
Vue.use(utils)
// 全局配置
Vue.config.productionTip = false
@ -31,28 +34,7 @@ Vue.use(VueLazyload, {
}
})
// 请求拦截器设置
axios.interceptors.request.use(
config => {
// 可以在这里添加token等信息
return config
},
error => {
return Promise.reject(error)
}
)
// 响应拦截器设置
axios.interceptors.response.use(
response => {
return response.data
},
error => {
// 统一错误处理
ElementUI.Message.error('请求失败,请稍后重试')
return Promise.reject(error)
}
)
/* eslint-disable no-new */
new Vue({

9
src/router/index.js

@ -25,6 +25,15 @@ const router = new Router({
},
component: () => import('@/views/Home.vue')
},
{
path: '/detail/:id',
name: 'Detail',
meta: {
title: '首页 - 精品商城',
keepAlive: false
},
component: () => import('@/views/Detail/Index.vue')
},
// {
// path: '/category/:id?',
// name: 'Category',

521
src/views/Detail/Index.vue

@ -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…
Cancel
Save