chenkainan 4 weeks ago
parent
commit
f00797ffe7
  1. 317
      src/components/common/AddressFormDialog.vue
  2. 16
      src/components/layout/HeaderNav.vue
  3. 34
      src/components/layout/HomeLayout.vue
  4. 36
      src/components/layout/Sidebar.vue
  5. 213
      src/components/product/AddToCartDialog.vue
  6. 14
      src/components/product/Evaluate.vue
  7. 47
      src/components/product/ProductList.vue
  8. 19
      src/libs/axios.js
  9. 3
      src/main.js
  10. 3
      src/router/index.js
  11. 157
      src/store/index.js
  12. 157
      src/views/Detail/Index.vue
  13. 4
      src/views/Index.vue
  14. 6
      src/views/Login.vue
  15. 405
      src/views/Order/Index.vue
  16. 340
      src/views/ProductPage/Index.vue
  17. 269
      src/views/User/UserAddress.vue

317
src/components/common/AddressFormDialog.vue

@ -0,0 +1,317 @@
<template>
<el-dialog
:title="`${type === 'add' ? '新增' : '编辑'}收货地址`"
:visible="visible"
width="700px"
@close="handleClose"
>
<el-form
:model="form"
:rules="rules"
ref="addressForm"
label-width="100px"
size="small"
>
<el-form-item label="收货人" prop="username">
<el-input
v-model="form.username"
placeholder="请填写收货人姓名,限制10个字符"
maxlength="10"
></el-input>
</el-form-item>
<el-form-item label="所在地区" prop="region">
<div class="region-selector">
<el-select
v-model="form.province_id"
placeholder="选择省份/直辖市"
@change="handleProvinceChange"
class="region-select"
>
<el-option
v-for="province in provinces"
:key="province.id"
:label="province.name"
:value="province.id"
></el-option>
</el-select>
<el-select
v-model="form.city_id"
placeholder="选择城市"
@change="handleCityChange"
class="region-select"
:disabled="!form.province_id"
>
<el-option
v-for="city in cities"
:key="city.id"
:label="city.name"
:value="city.id"
></el-option>
</el-select>
<el-select
v-model="form.district_id"
placeholder="选择县区"
class="region-select"
:disabled="!form.city_id"
>
<el-option
v-for="district in districts"
:key="district.id"
:label="district.name"
:value="district.id"
></el-option>
</el-select>
</div>
</el-form-item>
<el-form-item label="详细地址" prop="detail_addr">
<el-input
v-model="form.detail_addr"
placeholder="请填写详细地址,限制500个字符"
maxlength="500"
></el-input>
</el-form-item>
<el-form-item label="手机号码" prop="mobile">
<el-input
v-model="form.mobile"
placeholder="请填写手机号码,限制11位数字"
maxlength="11"
oninput="value=value.replace(/[^\d]/g,'')"
></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button size="small" @click="handleClose">取消</el-button>
<el-button size="small" type="primary" @click="saveAddress"
>保存</el-button
>
</div>
</el-dialog>
</template>
<script>
export default {
name: "AddressFormDialog",
props: {
visible: {
type: Boolean,
default: false,
},
type: {
type: String,
required: true,
validator: (val) => ["add", "edit"].includes(val),
},
initialData: {
type: Object,
default: () => ({}),
},
},
data() {
const validateMobile = (rule, value, callback) => {
if (!value) {
return callback(new Error("请输入手机号码"));
} else if (!/^1[3-9]\d{9}$/.test(value)) {
return callback(new Error("请输入正确的11位手机号码"));
} else {
callback();
}
};
const validateRegion = (rule, value, callback) => {
if (
!this.form.province_id ||
!this.form.city_id ||
!this.form.district_id
) {
return callback(new Error("请完整选择省市区"));
}
callback();
};
return {
form: {
id: null,
username: "",
detail_addr: "",
mobile: "",
province_id: null,
city_id: null,
district_id: null,
region: "",
},
rules: {
username: [
{ required: true, message: "请输入收货人姓名", trigger: "blur" },
{ max: 10, message: "姓名不能超过10个字符", trigger: "blur" },
],
region: [
{ required: true, validator: validateRegion, trigger: "change" },
],
detail_addr: [
{ required: true, message: "请输入详细地址", trigger: "blur" },
{ max: 500, message: "详细地址不能超过500个字符", trigger: "blur" },
],
mobile: [
{ required: true, validator: validateMobile, trigger: "blur" },
],
},
regionData: []
};
},
computed: {
provinces() {
return this.regionData.filter((item) => item.level === 1);
},
cities() {
if (!this.form.province_id) return [];
return this.regionData.filter(
(item) => item.level === 2 && item.pid === this.form.province_id
);
},
districts() {
if (!this.form.city_id) return [];
return this.regionData.filter(
(item) => item.level === 3 && item.pid === this.form.city_id
);
},
},
watch: {
visible(val) {
if (val) {
this.initForm();
}
},
initialData: {
handler() {
if (this.visible) {
this.initForm();
}
},
deep: true,
},
},
mounted() {
this.getRegionData();
},
methods: {
//
async getRegionData() {
try {
const res = await this.post({}, "/api/uservice/user/getAreas");
if (res && res.data) {
this.regionData = res.data;
} else {
this.$message.error("获取地区数据失败");
}
} catch (error) {
console.error("获取地区数据出错:", error);
this.$message.error("获取地区数据时发生错误");
}
},
initForm() {
this.$nextTick(() => {
if (this.$refs.addressForm) {
this.$refs.addressForm.resetFields();
}
this.form = {
id: this.type === "edit" ? this.initialData.id : null,
username: this.initialData.username || "",
detail_addr: this.initialData.detail_addr || "",
mobile: this.initialData.mobile || "",
province_id: this.initialData.province_id || null,
city_id: this.initialData.city_id || null,
district_id: this.initialData.district_id || null,
region: this.initialData.region || "",
};
// ID
if (
this.type === "edit" &&
this.initialData.region &&
!this.form.province_id
) {
this.echoRegionSelection(this.initialData.region);
}
});
},
echoRegionSelection(regionText) {
const regions = regionText.split(" ");
if (regions.length < 3) return;
const province = this.provinces.find((p) => p.name === regions[0]);
if (province) {
this.form.province_id = province.id;
this.$nextTick(() => {
const city = this.cities.find((c) => c.name === regions[1]);
if (city) {
this.form.city_id = city.id;
this.$nextTick(() => {
const district = this.districts.find(
(d) => d.name === regions[2]
);
if (district) {
this.form.district_id = district.id;
}
});
}
});
}
},
handleProvinceChange() {
this.form.city_id = null;
this.form.district_id = null;
},
handleCityChange() {
this.form.district_id = null;
},
saveAddress() {
this.$refs.addressForm.validate(async (valid) => {
if (!valid) return;
const province = this.provinces.find(
(p) => p.id === this.form.province_id
);
const city = this.cities.find((c) => c.id === this.form.city_id);
const district = this.districts.find(
(d) => d.id === this.form.district_id
);
if (!(province && city && district)) {
this.$message.warning("地区信息不完整");
return;
}
const formData = {
...this.form,
region: `${province.name} ${city.name} ${district.name}`,
};
this.$emit("save", formData);
this.handleClose();
});
},
handleClose() {
this.$emit("update:visible", false);
},
},
};
</script>
<style lang="scss" scoped>
.region-selector {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.region-select {
min-width: 180px;
flex: 1;
max-width: 30%;
}
</style>

16
src/components/layout/HeaderNav.vue

@ -94,7 +94,7 @@
<!-- 电话 --> <!-- 电话 -->
<div class="phone-entry"> <div class="phone-entry">
<router-link to="#"> <div>
<img <img
src="https://static.ticket.sz-trip.com/shiweisuzhou/pc/login/phone.png" src="https://static.ticket.sz-trip.com/shiweisuzhou/pc/login/phone.png"
alt="联系电话" alt="联系电话"
@ -103,7 +103,7 @@
<span class="cart-count" v-if="cartTotalCount > 0 && false">{{ <span class="cart-count" v-if="cartTotalCount > 0 && false">{{
cartTotalCount cartTotalCount
}}</span> }}</span>
</router-link> </div>
</div> </div>
</div> </div>
</div> </div>
@ -175,18 +175,14 @@ export default {
methods: { methods: {
...mapActions(["fetchCategories", "logout"]), ...mapActions(["fetchCategories", "logout"]),
handleSearch() { handleSearch() {
if (!this.searchText.trim()) return; // if (!this.searchText.trim()) return;
this.$store.commit("setSearchText", this.searchText); this.$store.commit("setSearchText", this.searchText);
// //
const isSearchPage = if (this.$route.path === "/ProductList") {
this.$route.path === "/ProductList" &&
this.$route.query.type === "search";
if (isSearchPage) {
// //
this.$emit("search", this.searchText); this.$bus.emit("search-product", this.searchText);
} else { } else {
// //
this.$router.push({ this.$router.push({
@ -206,6 +202,8 @@ export default {
.then(() => { .then(() => {
// logout // logout
this.$message.success("退出登录成功"); this.$message.success("退出登录成功");
//
localStorage.setItem("redirectPath", this.$route.path);
this.$router.push("/Login"); // 退 this.$router.push("/Login"); // 退
}) })
.catch((err) => { .catch((err) => {

34
src/components/layout/HomeLayout.vue

@ -1,13 +1,19 @@
<template> <template>
<div class="home-layout-container"> <div class="home-layout-container">
<!-- 左侧导航栏 --> <!-- 左侧导航栏 -->
<router-link to="/productList" class="left-nav"> <div class="left-nav">
<ul class="nav-list"> <ul class="nav-list">
<li v-for="item in tagList" :key="item.id" class="nav-item"> <router-link
:to="'/productList?id=' + item.id"
tag="li"
v-for="item in tagList"
:key="item.id"
class="nav-item"
>
<img :src="util.showImg(item.image)" alt="" />{{ item.name }} <img :src="util.showImg(item.image)" alt="" />{{ item.name }}
</li>
</ul>
</router-link> </router-link>
</ul>
</div>
<!-- 轮播图 --> <!-- 轮播图 -->
<div class="main-content"> <div class="main-content">
@ -40,28 +46,28 @@
</div> </div>
<div class="func-icons" v-if="$store.getters.isUserLogin"> <div class="func-icons" v-if="$store.getters.isUserLogin">
<div class="icon-item"> <router-link to="/User/UserCenter" class="icon-item">
<i class="icon el-icon-user"></i> <i class="icon el-icon-user"></i>
<span>个人中心</span> <span>个人中心</span>
</div> </router-link>
<div class="icon-item"> <router-link to="/User/OrderList" class="icon-item">
<i class="icon el-icon-goods"></i> <i class="icon el-icon-goods"></i>
<span>我的订单</span> <span>我的订单</span>
</div> </router-link>
<div class="icon-item"> <router-link to="/User/ViewHistory" class="icon-item">
<i class="icon el-icon-star-off"></i> <i class="icon el-icon-star-off"></i>
<span>我的收藏</span> <span>我的收藏</span>
</div> </router-link>
<div class="icon-item"> <router-link to="/User/UserCenter" class="icon-item">
<i class="icon el-icon-pie-chart"></i> <i class="icon el-icon-pie-chart"></i>
<span>议价单</span> <span>议价单</span>
</div> </router-link>
</div> </div>
<div class="announcement"> <!-- <div class="announcement">
<el-tag type="danger" size="mini">公告</el-tag> <el-tag type="danger" size="mini">公告</el-tag>
<span>2099年12月平台重要新规速递</span> <span>2099年12月平台重要新规速递</span>
</div> </div> -->
</div> </div>
</div> </div>
</template> </template>

36
src/components/layout/Sidebar.vue

@ -1,21 +1,15 @@
<template> <template>
<div class="sidebar-container"> <div class="sidebar-container">
<!-- 购物车 --> <!-- 购物车 -->
<div <div class="sidebar-item" @click="handleCartClick">
class="sidebar-item" <el-badge :value="$store.getters.getCartTotalCount" class="item-badge">
@click="handleCartClick"
>
<el-badge :value="cartCount" class="item-badge">
<i class="el-icon-shopping-cart-full"></i> <i class="el-icon-shopping-cart-full"></i>
</el-badge> </el-badge>
<div class="item-text">购物车</div> <div class="item-text">购物车</div>
</div> </div>
<!-- 在线客服 --> <!-- 在线客服 -->
<div <div class="sidebar-item" @click="handleServiceClick">
class="sidebar-item"
@click="handleServiceClick"
>
<i class="el-icon-headset"></i> <i class="el-icon-headset"></i>
<div class="item-text">在线客服</div> <div class="item-text">在线客服</div>
</div> </div>
@ -33,22 +27,28 @@
</template> </template>
<script> <script>
import { mapGetters } from "vuex";
export default { export default {
name: 'Sidebar', name: "Sidebar",
data() { data() {
return { return {
cartCount: 3, // cartCount: 3, //
isShowBackTop: false, // isShowBackTop: false, //
scrollTimer: null // scrollTimer: null, //
}; };
}, },
computed: {
// mapGetters
...mapGetters(["getCartTotalCount"]),
},
mounted() { mounted() {
// //
window.addEventListener('scroll', this.handleScroll); window.addEventListener("scroll", this.handleScroll);
this.$store.dispatch("fetchCartCount");
}, },
beforeDestroy() { beforeDestroy() {
// //
window.removeEventListener('scroll', this.handleScroll); window.removeEventListener("scroll", this.handleScroll);
if (this.scrollTimer) { if (this.scrollTimer) {
clearTimeout(this.scrollTimer); clearTimeout(this.scrollTimer);
} }
@ -57,18 +57,18 @@ export default {
// //
handleCartClick() { handleCartClick() {
// //
this.$router.push('/User/ShoppingCart'); this.$router.push("/User/ShoppingCart");
}, },
// 线 // 线
handleServiceClick() { handleServiceClick() {
// //
this.$message.info('正在唤起在线客服...'); this.$message.info("正在唤起在线客服...");
}, },
// //
handleBackToTop() { handleBackToTop() {
window.scrollTo({ top: 0, behavior: 'smooth' }); window.scrollTo({ top: 0, behavior: "smooth" });
}, },
// //
@ -81,8 +81,8 @@ export default {
// 300px // 300px
this.isShowBackTop = window.pageYOffset > 300; this.isShowBackTop = window.pageYOffset > 300;
}, 200); }, 200);
} },
} },
}; };
</script> </script>

213
src/components/product/AddToCartDialog.vue

@ -0,0 +1,213 @@
<template>
<!-- 加入购物车弹框 -->
<el-dialog
title="加入购物车"
:visible.sync="dialogVisible"
width="360px"
:close-on-click-modal="false"
:show-close="false"
>
<!-- 商品信息 -->
<div class="cart-dialog__product">
<img v-lazy="product.headimg" alt="商品图片" class="product-img" />
<div class="product-info">
<h4 class="product-name">{{ product.sku_name || "商品名称" }}</h4>
<p class="product-price">¥{{ product.price / 100 }}</p>
</div>
</div>
<!-- 数量选择器 -->
<div class="cart-dialog__quantity">
<label>购买数量</label>
<el-input-number
v-model="buyQuantity"
:min="1"
:max="product.stock"
:step="1"
class="quantity-input"
@change="handleQuantityChange"
></el-input-number>
<span class="stock-tip" v-if="product.stock">
库存仅剩 {{ product.stock }}
</span>
</div>
<!-- 底部按钮区 -->
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false" class="cancel-btn">
取消
</el-button>
<el-button
type="primary"
@click="handleAddToCart"
:loading="loading"
class="confirm-btn"
>
加入购物车
</el-button>
</span>
</el-dialog>
</template>
<script>
import { mapActions } from "vuex";
export default {
name: "AddToCartDialog",
props: {
//
product: {
type: Object,
required: true,
default: () => ({
id: "",
name: "",
price: 0,
avatar: "https://picsum.photos/100", //
stock: 99, //
}),
},
// /
visible: {
type: Boolean,
default: false,
},
buyQuantity: {
type: Number,
default: false,
},
},
data() {
return {
loading: false, //
};
},
computed: {
//
dialogVisible: {
get() {
return this.visible;
},
set(val) {
this.$emit("update:visible", val);
},
},
},
methods: {
// Vuex action
...mapActions(["addToCart"]),
//
handleQuantityChange(val) {
if (val > this.product.stock) {
this.buyQuantity = this.product.stock;
this.$message.warning(`库存不足,最多可购买 ${this.product.stock}`);
}
},
//
async handleAddToCart() {
this.loading = true;
try {
// Vuex action
await this.addToCart({
...this.product,
quantity: this.buyQuantity,
});
//
this.dialogVisible = false;
//
this.buyQuantity = 1;
} catch (err) {
this.$message.error(err.message || "加入购物车失败,请重试");
} finally {
this.loading = false;
}
},
},
};
</script>
<style lang="scss" scoped>
.cart-dialog__product {
display: flex;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #f5f5f5;
.product-img {
width: 80px;
height: 80px;
object-fit: cover;
border-radius: 4px;
margin-right: 15px;
}
.product-info {
flex: 1;
.product-name {
font-size: 16px;
color: #333;
margin: 0 0 8px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.product-price {
font-size: 18px;
color: #ff4400;
margin: 0;
font-weight: 500;
}
}
}
.cart-dialog__quantity {
display: flex;
align-items: center;
margin-bottom: 10px;
label {
font-size: 14px;
color: #666;
margin-right: 15px;
}
.quantity-input {
width: 120px;
}
.stock-tip {
font-size: 12px;
color: #999;
margin-left: 15px;
}
}
.dialog-footer {
display: flex;
justify-content: center;
padding-top: 10px;
.cancel-btn {
margin-right: 10px;
border-color: #e5e5e5;
color: #666;
}
.confirm-btn {
background-color: #6a8a27;
border-color: #6a8a27;
&:hover {
background-color: #5a7a1f;
border-color: #5a7a1f;
}
}
}
</style>

14
src/components/product/Evaluate.vue

@ -152,6 +152,7 @@ import { Empty, Pagination } from "element-ui";
export default { export default {
name: "ProductReviews", name: "ProductReviews",
props: ["id"],
components: { components: {
ElEmpty: Empty, ElEmpty: Empty,
ElPagination: Pagination, ElPagination: Pagination,
@ -394,7 +395,20 @@ export default {
return this.filteredReviews.slice(startIndex, endIndex); return this.filteredReviews.slice(startIndex, endIndex);
}, },
}, },
mounted() {
this.getList()
},
methods: { methods: {
//
getList() {
this.get({
product_id: this.id,
offset: 0,
limit: 999
}, '/api/product/product_comment_list').then(res => {
})
},
// //
handleFilterChange(filterType) { handleFilterChange(filterType) {
this.activeFilter = filterType; this.activeFilter = filterType;

47
src/components/product/ProductList.vue

@ -1,15 +1,18 @@
<template> <template>
<div class="product-list"> <div class="product-list">
<div class="product-grid"> <div class="product-grid">
<div v-for="product in products" :key="product.id" class="product-card"> <router-link
:to="`/Detail/${product.id}`"
v-for="product in products"
:key="product.id"
class="product-card"
>
<div class="product-img"> <div class="product-img">
<router-link :to="`/Detail/${product.id}`">
<img <img
v-lazy="product.headimg" v-lazy="product.headimg"
:alt="product.title" :alt="product.title"
class="product-pic" class="product-pic"
/> />
</router-link>
</div> </div>
<div class="product-info"> <div class="product-info">
<div class="flex-between"> <div class="flex-between">
@ -33,13 +36,12 @@
type="primary" type="primary"
size="small" size="small"
style="background-color: #6a8a27; border: none" style="background-color: #6a8a27; border: none"
@click="addToCart(product)"
> >
<el-icon name="el-icon-shopping-cart"></el-icon> <el-icon name="el-icon-shopping-cart"></el-icon>
</el-button> </el-button>
</div> </div>
</div> </div>
</div> </router-link>
</div> </div>
<!-- 无商品时显示 --> <!-- 无商品时显示 -->
@ -60,38 +62,7 @@ export default {
default: () => [], default: () => [],
}, },
}, },
methods: { methods: {},
...mapActions(["addToCart"]),
addToCart(product) {
//
if (!this.$store.getters.isUserLogin) {
this.$confirm("您尚未登录,是否前往登录?", "提示", {
confirmButtonText: "登录",
cancelButtonText: "取消",
type: "info",
})
.then(() => {
this.$router.push({
path: "/login",
query: { redirect: this.$route.fullPath },
});
})
.catch(() => {
//
});
return;
}
this.addToCart({
id: product.id,
name: product.name,
price: product.price,
image: product.image,
quantity: 1,
});
this.$message.success("已加入购物车");
},
},
}; };
</script> </script>

19
src/libs/axios.js

@ -2,6 +2,7 @@ import Vue from 'vue'
import axios from "axios"; import axios from "axios";
import { Message, MessageBox, Loading } from "element-ui"; // 引入 Element UI 组件 import { Message, MessageBox, Loading } from "element-ui"; // 引入 Element UI 组件
import store from '@/store'; import store from '@/store';
import router from "@/router"
const http = axios.create({ const http = axios.create({
timeout: 6000 // 请求超时时间 timeout: 6000 // 请求超时时间
@ -21,8 +22,9 @@ http.interceptors.request.use((config) => {
config.baseURL = process.env.VUE_APP_URL; config.baseURL = process.env.VUE_APP_URL;
} }
const token = store.state.user?.userInfo?.token || ''; const token = store.state.user?.info?.token || '';
config.headers['token'] = token || "745f2b4c-49b7-4393-bf58-bc380a87149a"; config.headers['token'] = token || ""
// config.headers['token'] = token || "745f2b4c-49b7-4393-bf58-bc380a87149a";
config.headers['Content-Type'] = 'application/json;charset=UTF-8'; config.headers['Content-Type'] = 'application/json;charset=UTF-8';
// 统一添加 platform_type = 2 参数 // 统一添加 platform_type = 2 参数
@ -73,6 +75,18 @@ http.interceptors.response.use(response => {
MessageBox.alert(errorMsg, '错误提示', { MessageBox.alert(errorMsg, '错误提示', {
confirmButtonText: '确定', confirmButtonText: '确定',
type: 'error' type: 'error'
}).then(() => {
if (resData.code === 401) {
// 存储当前路由(排除登录页自身,避免死循环)
const currentPath = router.currentRoute.fullPath;
if (currentPath !== '/Login') { // 假设登录页路径是 /Login
localStorage.setItem('redirectPath', currentPath); // 存到本地存储
}
// 登录操作
router.push("/Login")
}
}).catch(() => {
}); });
// return Promise.reject(new Error(errorMsg)); // return Promise.reject(new Error(errorMsg));
} }
@ -100,6 +114,7 @@ http.interceptors.response.use(response => {
type: 'warning' type: 'warning'
}).then(() => { }).then(() => {
// 登录操作 // 登录操作
router.push("/Login")
}).catch(() => { }).catch(() => {
// 取消登录回调 // 取消登录回调
}); });

3
src/main.js

@ -15,6 +15,9 @@ Vue.use(utils)
Vue.config.productionTip = false Vue.config.productionTip = false
Vue.use(ElementUI) Vue.use(ElementUI)
// 注册全局事件总线
Vue.prototype.$bus = new Vue()
// 配置图片懒加载 // 配置图片懒加载
Vue.use(VueLazyload, { Vue.use(VueLazyload, {
preLoad: 1.3, // 预加载高度比例 preLoad: 1.3, // 预加载高度比例

3
src/router/index.js

@ -1,5 +1,6 @@
import Vue from 'vue' import Vue from 'vue'
import Router from 'vue-router' import Router from 'vue-router'
import store from '@/store';
Vue.use(Router) Vue.use(Router)
@ -264,7 +265,7 @@ router.beforeEach((to, from, next) => {
// 验证登录状态 // 验证登录状态
if (to.meta.requireAuth) { if (to.meta.requireAuth) {
const token = localStorage.getItem('token') const token = store.state.user?.info?.token || '';
if (token) { if (token) {
next() next()
} else { } else {

157
src/store/index.js

@ -34,9 +34,19 @@ export default new Vuex.Store({
categories: [], categories: [],
// 全局加载状态 // 全局加载状态
loading: false, loading: false,
searchText: '' // 存储搜索词 searchText: '', // 存储搜索词
// 存储订单数据
orderData: null
}, },
mutations: { mutations: {
// 设置订单数据
setOrderData(state, data) {
state.orderData = data
},
// 清空订单数据
clearOrderData(state) {
state.orderData = null
},
// 更新分类数据 // 更新分类数据
UPDATE_CATEGORIES(state, categories) { UPDATE_CATEGORIES(state, categories) {
state.categories = categories state.categories = categories
@ -63,52 +73,9 @@ export default new Vuex.Store({
localStorage.removeItem('token') localStorage.removeItem('token')
}, },
// 添加商品到购物车 // 更新购物车总数
ADD_TO_CART(state, product) { UPDATE_CART_TOTAL_COUNT(state, count) {
const existingItem = state.cart.items.find(item => item.id === product.id) state.cart.totalCount = count
if (existingItem) {
existingItem.quantity += product.quantity || 1
} else {
state.cart.items.push({
...product,
quantity: product.quantity || 1
})
}
this.commit('UPDATE_CART_TOTAL')
},
// 从购物车移除商品
REMOVE_FROM_CART(state, productId) {
state.cart.items = state.cart.items.filter(item => item.id !== productId)
this.commit('UPDATE_CART_TOTAL')
},
// 更新购物车商品数量
UPDATE_CART_ITEM_QUANTITY(state, { productId, quantity }) {
const item = state.cart.items.find(item => item.id === productId)
if (item) {
item.quantity = quantity
this.commit('UPDATE_CART_TOTAL')
}
},
// 清空购物车
CLEAR_CART(state) {
state.cart.items = []
this.commit('UPDATE_CART_TOTAL')
},
// 更新购物车总计
UPDATE_CART_TOTAL(state) {
state.cart.totalCount = state.cart.items.reduce((total, item) => {
return total + item.quantity
}, 0)
state.cart.totalPrice = state.cart.items.reduce((total, item) => {
return total + (item.price * item.quantity)
}, 0)
}, },
setSearchText(state, text) { setSearchText(state, text) {
@ -116,6 +83,10 @@ export default new Vuex.Store({
} }
}, },
actions: { actions: {
// 提交订单数据
submitOrderData({ commit }, data) {
commit('setOrderData', data)
},
// 获取分类数据 // 获取分类数据
fetchCategories({ commit }) { fetchCategories({ commit }) {
commit('UPDATE_LOADING', true) commit('UPDATE_LOADING', true)
@ -143,26 +114,99 @@ export default new Vuex.Store({
}, },
// 添加商品到购物车 // 添加商品到购物车
addToCart({ commit }, product) { addToCart({ dispatch }, product) {
commit('ADD_TO_CART', product) // 调用接口,参数为商品id和数量
return Vue.prototype.post(
{
sku_id: product.id,
num: product.quantity || 1
},
"/api/cart/add_sku"
).then((res) => {
if (res) {
// 添加成功后重新获取购物车数量
return dispatch('fetchCartCount')
}
}).catch((err) => {
console.error('添加到购物车请求失败', err);
throw err
});
},
// 获取购物车数量
fetchCartCount({ commit, state }) {
// 检查是否存在token,不存在则直接返回0
if (!state.user.token) {
commit('UPDATE_CART_TOTAL_COUNT', 0)
return Promise.resolve(0)
}
return Vue.prototype.post(
{},
"/api/cart/get_count"
).then((res) => {
console.log('购物车数量接口返回', res)
if (res.code == 1) {
commit('UPDATE_CART_TOTAL_COUNT', res.data)
}
return res.data
}).catch((err) => {
console.error('获取购物车数量失败', err)
throw err
});
}, },
// 从购物车移除商品 // 从购物车移除商品
removeFromCart({ commit }, productId) { removeFromCart({ dispatch }, productId) {
commit('REMOVE_FROM_CART', productId) // 调用删除接口
return Vue.prototype.post(
{ sku_id: productId },
"/api/cart/remove_sku"
).then(() => {
// 删除成功后重新获取购物车数量
return dispatch('fetchCartCount')
}).catch(err => {
console.error('删除购物车商品失败', err)
throw err
})
}, },
// 更新购物车商品数量 // 更新购物车商品数量
updateCartItemQuantity({ commit }, payload) { updateCartItemQuantity({ dispatch }, { productId, quantity }) {
commit('UPDATE_CART_ITEM_QUANTITY', payload) // 调用更新数量接口
return Vue.prototype.post(
{
sku_id: productId,
num: quantity
},
"/api/cart/add_sku"
).then(() => {
// 更新成功后重新获取购物车数量
return dispatch('fetchCartCount')
}).catch(err => {
console.error('更新购物车商品数量失败', err)
throw err
})
}, },
// 清空购物车 // 清空购物车
clearCart({ commit }) { clearCart({ dispatch }) {
commit('CLEAR_CART') // 调用清空接口
return Vue.prototype.post(
{},
"/api/cart/clear"
).then(() => {
// 清空成功后重新获取购物车数量
return dispatch('fetchCartCount')
}).catch(err => {
console.error('清空购物车失败', err)
throw err
})
} }
}, },
getters: { getters: {
// 获取订单数据
getOrderData: state => state.orderData,
// 获取分类列表 // 获取分类列表
getCategories: state => state.categories, getCategories: state => state.categories,
@ -172,9 +216,6 @@ export default new Vuex.Store({
// 获取购物车商品总数 // 获取购物车商品总数
getCartTotalCount: state => state.cart.totalCount, getCartTotalCount: state => state.cart.totalCount,
// 获取购物车商品总价
getCartTotalPrice: state => state.cart.totalPrice,
// 获取用户登录状态 // 获取用户登录状态
isUserLogin: state => state.user.isLogin, isUserLogin: state => state.user.isLogin,

157
src/views/Detail/Index.vue

@ -43,6 +43,7 @@
</h2> </h2>
<div class="product-tags"> <div class="product-tags">
<div v-if="info.display_tags" style="display: inline-block">
<el-tag <el-tag
effect="dark" effect="dark"
size="mini" size="mini"
@ -51,24 +52,20 @@
style="margin-right: 5px" style="margin-right: 5px"
>{{ item }}</el-tag >{{ item }}</el-tag
> >
</div>
<span class="subtitle">{{ info.subtitle }}</span> <span class="subtitle">{{ info.subtitle }}</span>
</div> </div>
<div class="price-info"> <div class="price-info">
<span class="price-label">售价</span> <span class="price-label">售价</span>
<span class="price-amount">¥{{ info.price / 100 }}</span> <span class="price-amount">¥{{ info.sku[selectedSpec].price / 100 }}</span>
<span class="sales-volume">已售 {{ info.sales_number }}</span> <span class="sales-volume">已售 {{ info.sales_number }}</span>
</div> </div>
<div class="product-attr"> <div class="product-attr">
<div class="attr-item">
<span class="attr-label">起订量</span>
<span class="attr-value">{{ moq }}</span>
</div>
<div class="attr-item"> <div class="attr-item">
<span class="attr-label">收货方式</span> <span class="attr-label">收货方式</span>
<span class="attr-value">{{ deliveryMethod }}</span> <span class="attr-value">邮寄</span>
</div> </div>
<div class="attr-item spec-group"> <div class="attr-item spec-group">
@ -86,6 +83,13 @@
</div> </div>
</div> </div>
<div class="attr-item" v-if="currentMinQuantity > 1">
<span class="attr-label">起订量</span>
<span class="attr-value">{{
currentMinQuantity
}}</span>
</div>
<div class="attr-item"> <div class="attr-item">
<span class="attr-label">发货地</span> <span class="attr-label">发货地</span>
<span class="attr-value">{{ info.supplier_address }}</span> <span class="attr-value">{{ info.supplier_address }}</span>
@ -114,8 +118,10 @@
circle circle
@click="increaseQuantity" @click="increaseQuantity"
></el-button> ></el-button>
<el-button type="primary" class="buy-btn">一口价购买</el-button> <el-button type="primary" class="buy-btn" @click="buyNow">一口价购买</el-button>
<el-button type="success" class="cart-btn">加入购物车</el-button> <el-button type="success" class="cart-btn" @click="addCart"
>加入购物车</el-button
>
<el-button type="info" class="bargain-btn">议价</el-button> <el-button type="info" class="bargain-btn">议价</el-button>
<el-button <el-button
icon="el-icon-share" icon="el-icon-share"
@ -135,10 +141,14 @@
v-for="(item, index) in hotRecommendData" v-for="(item, index) in hotRecommendData"
:key="index" :key="index"
class="product-item" class="product-item"
@click="
id = item.id;
onReload();
"
> >
<img v-lazy="item.imgUrl" alt="" /> <img v-lazy="item.headimg" alt="" />
<div class="product-name">{{ item.title }}</div> <div class="product-name">{{ item.title }}</div>
<div class="product-price">{{ item.price }}</div> <div class="product-price">{{ item.price / 100 }}</div>
</div> </div>
</div> </div>
@ -165,21 +175,32 @@
></div> ></div>
<!-- 商品评价 --> <!-- 商品评价 -->
<Evaluate v-show="tabIndex" /> <Evaluate v-show="tabIndex" :id="id" />
</div> </div>
</div> </div>
<!-- 加入购物车弹框 -->
<AddToCartDialog
:product="sku"
:buyQuantity="quantity"
:visible.sync="showAddCartDialog"
/>
</div> </div>
</template> </template>
<script> <script>
import Evaluate from "@/components/product/Evaluate.vue"; import Evaluate from "@/components/product/Evaluate.vue";
import AddToCartDialog from "@/components/product/AddToCartDialog.vue";
import { mapActions } from 'vuex';
export default { export default {
name: "ProductDetail", name: "ProductDetail",
components: { components: {
Evaluate, Evaluate,
AddToCartDialog,
}, },
data() { data() {
return { return {
id: "",
info: {}, info: {},
activeIndex: 0, // activeIndex: 0, //
productCount: "52个", productCount: "52个",
@ -190,36 +211,42 @@ export default {
otherInfo: "下单填写留言,即免费赠送精美贺卡!", otherInfo: "下单填写留言,即免费赠送精美贺卡!",
deliveryRange: "全国(可配送至全国1000多个城市,苏州市区内免配送费)", deliveryRange: "全国(可配送至全国1000多个城市,苏州市区内免配送费)",
quantity: 1, quantity: 1,
hotRecommendData: [ hotRecommendData: [],
{ tabIndex: 0,
id: 1, showAddCartDialog: false,
imgUrl: "https://picsum.photos/id/103/500/500", // sku: {},
title: "北欧花艺素雅仿真花", };
price: 359,
},
{
id: 2,
imgUrl: "https://picsum.photos/id/103/500/500",
title: "生日玫瑰鲜花",
price: 359,
}, },
{ computed: {
id: 3, //
imgUrl: "https://picsum.photos/id/103/500/500", currentMinQuantity() {
title: "香雪兰小苍兰鲜花", if (!this.info?.sku || this.info.sku.length === 0) return 1;
price: 359, return this.info.sku[this.selectedSpec].start_number || 1;
}, },
{
id: 4,
imgUrl: "https://picsum.photos/id/103/500/500",
title: "现代创意简约仿真花艺",
price: 359,
}, },
], watch: {
tabIndex: 0, //
}; selectedSpec() {
this.quantity = this.currentMinQuantity;
}
}, },
methods: { methods: {
...mapActions(['submitOrderData']),
buyNow() {
const orderItems = [
{
product: this.info,
sku: this.info.sku[this.selectedSpec],
quantity: this.quantity
}
]
this.submitOrderData({
from: 'product',
items: orderItems //
});
this.$router.push('/Order');
},
// - activeIndex // - activeIndex
handleCarouselChange(index) { handleCarouselChange(index) {
this.activeIndex = index; this.activeIndex = index;
@ -240,7 +267,7 @@ export default {
}, },
decreaseQuantity() { decreaseQuantity() {
if (this.quantity > 1) { if (this.quantity > this.currentMinQuantity) {
this.quantity--; this.quantity--;
} }
}, },
@ -252,19 +279,57 @@ export default {
handleShare() { handleShare() {
this.$message.info("分享功能待实现"); this.$message.info("分享功能待实现");
}, },
//
addCart() {
this.sku = this.info.sku[this.selectedSpec];
this.showAddCartDialog = true;
},
//
getRecommend() {
this.post(
{
offset: 0,
limit: 6,
rand: true,
product_ids: this.id,
},
"/api/search/product_recommend"
).then((res) => {
this.hotRecommendData = res.data.list;
});
},
//
getDetail() {
this.get({ id: this.id }, "/api/product/get_product_detail").then(
(res) => {
this.info = res.data;
if (this.info.sku && this.info.sku.length > 0) {
//
if (this.selectedSpec >= this.info.sku.length) {
this.selectedSpec = 0;
}
//
this.quantity = this.currentMinQuantity;
}
}
);
},
onReload() {
this.getDetail();
this.getRecommend();
},
}, },
mounted() { mounted() {
// //
if (!this.$refs.carousel) { if (!this.$refs.carousel) {
console.warn("轮播组件未正确加载,请检查ref属性是否设置"); console.warn("轮播组件未正确加载,请检查ref属性是否设置");
} }
this.id = this.$route.params.id;
this.get( this.onReload();
{ id: this.$route.params.id },
"/api/product/get_product_detail"
).then((res) => {
this.info = res.data;
});
}, },
}; };
</script> </script>
@ -467,10 +532,12 @@ export default {
.hot-recommend-sidebar { .hot-recommend-sidebar {
min-width: 200px; min-width: 200px;
max-width: 200px;
padding: 10px; padding: 10px;
// border: 1px solid #eee; // border: 1px solid #eee;
background-color: #f7f9fa; background-color: #f7f9fa;
margin-right: 20px; margin-right: 20px;
cursor: pointer;
.hot-title { .hot-title {
border-left: 4px solid #ff4d4f; border-left: 4px solid #ff4d4f;

4
src/views/Index.vue

@ -97,7 +97,6 @@ export default {
this.getTopBanner(); this.getTopBanner();
this.getTags(); this.getTags();
this.getProductList(); this.getProductList();
this.post({}, "/api/uservice/user/getMyInfo").then((res) => {});
}, },
methods: { methods: {
// banner // banner
@ -118,11 +117,12 @@ export default {
getTags() { getTags() {
this.post( this.post(
{ {
pid: 662, pid: 683,
}, },
"/api/product/tag_list" "/api/product/tag_list"
).then((res) => { ).then((res) => {
this.tagList = res.data; this.tagList = res.data;
this.tagList[0].id = 662;
}); });
}, },
// //

6
src/views/Login.vue

@ -265,8 +265,10 @@ export default {
token: res.data.token, token: res.data.token,
}); });
// //
this.$router.push("/"); const redirectPath = localStorage.getItem('redirectPath');
this.$router.push(redirectPath || '/');
localStorage.removeItem('redirectPath');
} }
}); });

405
src/views/Order/Index.vue

@ -3,7 +3,11 @@
<!-- 页面标题 --> <!-- 页面标题 -->
<div class="page-header"> <div class="page-header">
<h2>填写收货人信息</h2> <h2>填写收货人信息</h2>
<el-button type="text" class="address-book-btn"> <el-button
type="text"
class="address-book-btn"
@click="openDialog('add', {})"
>
新增/修改地址 新增/修改地址
</el-button> </el-button>
</div> </div>
@ -12,7 +16,7 @@
<div class="info-section"> <div class="info-section">
<h3>收货人信息</h3> <h3>收货人信息</h3>
<div class="address-item"> <div class="address-item">
<span class="label">收货地址</span> <span class="label">收货地址</span>
<div class="address-content"> <div class="address-content">
<div <div
class="saved-address" class="saved-address"
@ -23,22 +27,32 @@
> >
<span class="name-tag">{{ addr.name }} {{ addr.province }}</span> <span class="name-tag">{{ addr.name }} {{ addr.province }}</span>
<span class="detail">{{ addr.address }} {{ addr.phone }}</span> <span class="detail">{{ addr.address }} {{ addr.phone }}</span>
<el-button type="text" class="set-default-btn">设为默认</el-button> <!-- <el-button type="text" class="set-default-btn">设为默认</el-button> -->
<el-button type="text" class="edit-btn">编辑</el-button> <el-button
<el-button type="text" class="delete-btn">删除</el-button> type="text"
class="edit-btn"
@click.stop="handleEdit(addr)"
>编辑</el-button
>
<el-button
type="text"
class="delete-btn"
@click.stop="handleDelete()"
>删除</el-button
>
</div> </div>
</div> </div>
</div> </div>
<div class="trade-type-item"> <!-- <div class="trade-type-item">
<span class="label">交易方式</span> <span class="label">交易方式</span>
<el-radio-group v-model="selectedTradeType"> <el-radio-group v-model="selectedTradeType">
<el-radio :label="1">先款后货</el-radio> <el-radio :label="1">先款后货</el-radio>
<el-radio :label="2">货到付款</el-radio> <el-radio :label="2">货到付款</el-radio>
</el-radio-group> </el-radio-group>
</div> </div> -->
<h3>发票信息</h3> <!-- <h3>发票信息</h3>
<div class="invoice-type-item"> <div class="invoice-type-item">
<span class="label">发票类型</span> <span class="label">发票类型</span>
<el-radio-group v-model="selectedInvoiceType"> <el-radio-group v-model="selectedInvoiceType">
@ -62,27 +76,7 @@
></el-option> ></el-option>
</el-select> </el-select>
<el-button type="text" class="add-title-btn">+ 添加</el-button> <el-button type="text" class="add-title-btn">+ 添加</el-button>
</div> </div> -->
<h3>收票地址</h3>
<div class="invoice-address-item">
<span class="label">收票地址</span>
<div class="address-content">
<div
class="saved-address"
v-for="(addr, index) in invoiceAddresses"
:key="index"
@click="selectInvoiceAddress(index)"
:class="{ selected: selectedInvoiceAddressIndex === index }"
>
<span class="name-tag">{{ addr.name }} {{ addr.province }}</span>
<span class="detail">{{ addr.address }} {{ addr.phone }}</span>
<el-button type="text" class="set-default-btn">设为默认</el-button>
<el-button type="text" class="edit-btn">编辑</el-button>
<el-button type="text" class="delete-btn">删除</el-button>
</div>
</div>
</div>
</div> </div>
<!-- 签署合同 --> <!-- 签署合同 -->
@ -188,34 +182,32 @@
</el-button> </el-button>
</div> </div>
</div> </div>
<!-- 引入地址编辑组件 -->
<AddressFormDialog
:visible="dialogVisible"
:type="dialogType"
:initial-data="dialogData"
@update:visible="dialogVisible = $event"
@save="handleSaveAddress"
/>
</div> </div>
</template> </template>
<script> <script>
import { mapGetters, mapMutations } from "vuex";
import AddressFormDialog from "@/components/common/AddressFormDialog.vue"; //
export default { export default {
name: "OrderPage", name: "OrderPage",
components: {
AddressFormDialog, //
},
data() { data() {
return { return {
// //
addresses: [ addresses: [],
{ selectedAddressIndex: -1,
id: 1,
name: "郑小工",
province: "江苏省",
address: "苏州市吴中区文化创意大厦16层",
phone: "13000000000",
isDefault: true,
},
{
id: 2,
name: "郑小工",
province: "江苏省",
address: "苏州市吴中区文化创意大厦16层",
phone: "13000000001",
isDefault: false,
},
],
selectedAddressIndex: 0,
// //
selectedTradeType: 1, // 1: 2: selectedTradeType: 1, // 1: 2:
@ -231,115 +223,260 @@ export default {
], ],
selectedInvoiceTitle: null, selectedInvoiceTitle: null,
//
invoiceAddresses: [
{
id: 1,
name: "郑小工",
province: "江苏省",
address: "苏州市吴中区文化创意大厦16层",
phone: "13000000000",
isDefault: true,
},
{
id: 2,
name: "郑小工",
province: "江苏省",
address: "苏州市吴中区文化创意大厦16层",
phone: "13000000001",
isDefault: false,
},
],
selectedInvoiceAddressIndex: 0,
// //
orderGroups: [ orderGroups: [],
{
supplier: "新疆特色农产品开发有限公司",
items: [
{
image: "https://picsum.photos/50/50?random=1",
name: "产品标题产品标题产品标题",
spec: "规格名称一样",
price: "¥449",
quantity: 1,
freight: "¥0.00",
deliveryType: "邮寄",
subtotal: "¥449.00",
},
{
image: "https://picsum.photos/50/50?random=2",
name: "产品标题产品标题",
spec: "规格名称一样",
price: "¥199",
quantity: 1,
freight: "¥0.00",
deliveryType: "快递",
subtotal: "¥199.00",
},
],
remark: "",
},
{
supplier: "新疆特色农产品开发有限公司",
items: [
{
image: "https://picsum.photos/50/50?random=3",
name: "产品标题产品标题产品标题",
spec: "规格名称一样",
price: "¥339",
quantity: 1,
freight: "¥0.00",
deliveryType: "邮寄",
subtotal: "¥339.00",
},
],
remark: "",
},
],
// //
totalGoodsCount: 3, totalGoodsCount: 3,
totalGoodsAmount: 973, totalGoodsAmount: 973,
totalFreight: 0, totalFreight: 0,
totalPayAmount: 973, totalPayAmount: 0,
dialogVisible: false,
dialogType: "add",
dialogData: {},
}; };
}, },
computed: { computed: {
...mapGetters(["getOrderData"]),
// //
selectedAddress() { selectedAddress() {
return this.addresses[this.selectedAddressIndex] || {}; return this.addresses[this.selectedAddressIndex] || {};
}, },
}, },
mounted() {
if (this.getOrderData) {
this.initOrderData(this.getOrderData);
// this.clearOrderData(); // Vuex
console.log(this.getOrderData);
} else {
this.$message.error("未获取到商品信息,请重新操作");
this.$router.go(-1);
}
this.getAddress();
},
methods: { methods: {
// ...mapMutations(["clearOrderData"]),
selectAddress(index) { //
this.selectedAddressIndex = index; openDialog(type, item) {
this.dialogType = type;
this.dialogData = { ...item };
this.dialogVisible = true;
}, },
//
// handleEdit(row) {
selectInvoiceAddress(index) { console.log(row);
this.selectedInvoiceAddressIndex = index; row.username = row.name;
row.mobile = row.tel;
this.openDialog("edit", row);
}, },
//
// handleDelete(row) {
submitOrder() { this.$confirm("此操作将永久删除该地址, 是否继续?", "提示", {
// confirmButtonText: "确定",
if (this.selectedAddressIndex === null) { cancelButtonText: "取消",
this.$message.warning("请选择收货地址"); type: "warning",
})
.then(() => {
this.post(
{
id: row.id,
},
"/api/uservice/user/delConsignee"
).then((res) => {
if (res.code == 1) {
this.getAddress();
this.$message.success("删除成功!");
}
});
})
.catch(() => {
this.$message.info("已取消删除");
});
},
//
initOrderData(data) {
if (data.from === "product") {
//
const item = data.items[0];
this.orderGroups = [
{
supplier: item.product.supplier_name,
items: this.formatItems([item]), //
},
];
} else if (data.from === "cart") {
//
this.orderGroups = Object.entries(data.groups).map(
([supplier, items]) => ({
supplier,
items: this.formatItems(items), //
})
);
}
this.calculateTotal(); //
},
//
formatItems(items) {
return items.map((item) => ({
id: item.sku.id,
productId: item.product.id,
name: item.product.title,
image: item.product.headimg,
spec: item.sku.sku_name,
price: (item.sku.price / 100).toFixed(2), //
quantity: item.quantity,
freight: "0.00", //
deliveryType: "邮寄",
//
// subtotal: ((item.sku.price / 100) * item.quantity).toFixed(2),
// + 0
subtotal: (
(item.sku.price / 100) * item.quantity +
parseFloat("0.00")
).toFixed(2),
}));
},
//
getPost() {
//
if (!this.selectedAddress.id) {
this.$message.warning("请先选择收货地址");
return; return;
} }
if (!this.selectedInvoiceTitle) { //
this.$message.warning("请选择发票抬头"); this.totalFreight = 0;
return;
// Promise
const freightPromises = [];
//
this.orderGroups.forEach((group, groupIndex) => {
group.items.forEach((item, itemIndex) => {
//
const params = {
consignee_id: this.selectedAddress.id,
sku_id: item.id,
num: item.quantity,
};
// Promise
const promise = this.post(params, "/api/order/get_post_price")
.then((res) => {
if (res.code === 1) {
//
const freight = (res.data.price / 100).toFixed(2);
item.freight = freight;
// +
item.subtotal = (
parseFloat(item.price) * item.quantity +
parseFloat(freight)
).toFixed(2);
//
this.totalFreight = (
parseFloat(this.totalFreight) + parseFloat(freight)
).toFixed(2);
}
})
.catch((err) => {
console.error(`获取商品 ${item.id} 邮费失败`, err);
this.$message.error(`获取商品 ${item.name} 邮费失败,请重试`);
});
freightPromises.push(promise);
});
});
//
Promise.all(freightPromises).then(() => {
this.calculateTotal();
});
},
//
calculateTotalFreight() {
let total = 0;
this.orderGroups.forEach((group) => {
group.items.forEach((item) => {
console.log(item)
total += parseFloat(item.freight);
});
});
this.totalFreight = total.toFixed(2);
},
//
calculateTotal() {
let totalCount = 0;
let totalAmount = 0;
this.orderGroups.forEach((group) => {
group.items.forEach((item) => {
totalCount += item.quantity;
totalAmount += parseFloat(item.subtotal);
});
});
this.totalGoodsCount = totalCount;
this.totalGoodsAmount = totalAmount.toFixed(2);
// 使
this.totalPayAmount = (
totalAmount + parseFloat(this.totalFreight)
).toFixed(2);
},
//
handleSaveAddress(formData) {
formData.is_default = 0;
if (this.dialogType === "add") {
this.post(formData, "/api/uservice/user/addNewConsignee").then(
(res) => {
if (res.code == 1) {
this.$message.success("保存成功");
this.getAddress();
this.dialogVisible = false;
}
}
);
} else {
this.post(formData, "/api/uservice/user/editConsignee").then((res) => {
if (res.code == 1) {
this.$message.success("保存成功");
this.getAddress();
this.dialogVisible = false;
}
});
}
},
//
getAddress() {
this.post(
{
contactType: "CONSIGNEE",
offset: "0",
limit: "100",
},
"/api/uservice/user/getContactOrConsignee"
).then((res) => {
if (res) {
this.addresses = res.data || [];
} }
});
},
//
selectAddress(index) {
this.selectedAddressIndex = index;
//
this.getPost();
},
if (this.selectedInvoiceAddressIndex === null) { //
this.$message.warning("请选择收票地址"); submitOrder() {
if (!this.selectedAddress.id) {
this.$message.warning("请先选择收货地址");
return; return;
} }
this.$message.success("订单提交成功!"); this.$message.success("订单提交成功!");
// //
// this.$router.push('/order/detail'); // this.$router.push('/order/detail');
@ -394,7 +531,7 @@ export default {
.invoice-address-item { .invoice-address-item {
margin-bottom: 15px; margin-bottom: 15px;
display: flex; display: flex;
align-items: flex-start; align-items: center;
.label { .label {
width: 120px; width: 120px;

340
src/views/ProductPage/Index.vue

@ -11,9 +11,9 @@
> >
<el-option <el-option
v-for="cat in categories" v-for="cat in categories"
:key="cat.value" :key="cat.id"
:label="cat.label" :label="cat.name"
:value="cat.value" :value="cat.id"
></el-option> ></el-option>
</el-select> </el-select>
@ -53,45 +53,33 @@
<div <div
class="product-card" class="product-card"
v-for="(product, index) in visibleProducts" v-for="(product, index) in visibleProducts"
:key="product.id" :key="index"
@click="goToDetail(product.id)" @click="goToDetail(product.id)"
> >
<el-image <img
v-lazy="product.image" v-lazy="product.headimg"
:alt="product.name" :alt="product.title"
class="product-img" class="product-img"
lazy
fit="cover" fit="cover"
> />
<div slot="placeholder" class="image-placeholder"> <!-- <div slot="placeholder" class="image-placeholder">
<i class="el-icon-loading"></i> <i class="el-icon-loading"></i>
</div> </div> -->
</el-image> <!-- </img> -->
<div
class="tag"
v-if="product.tag"
:style="{ backgroundColor: product.tagColor }"
>
{{ product.tag }}
</div>
<div class="product-info"> <div class="product-info">
<div class="product-name">{{ product.name }}</div> <div class="product-name">{{ product.title }}</div>
<div class="price-row"> <div class="price-row">
<span class="current-price">¥{{ product.price.toFixed(2) }}</span> <span class="current-price">¥{{ product.price / 100 }}</span>
<span class="original-price" v-if="product.originalPrice" <span class="original-price" v-if="product.market_price"
>¥{{ product.originalPrice.toFixed(2) }}</span >¥{{ product.market_price / 100 }}</span
> >
</div> </div>
<div class="sales-volume" v-if="product.sales > 0"> <div class="sales-volume">
<i class="el-icon-shopping-cart"></i> 已售 {{ product.sales }} <i class="el-icon-shopping-cart"></i> 已售
{{ product.sales_number }}
</div> </div>
<el-button <el-button type="primary" size="mini" class="cart-btn">
type="primary" 立即购买
size="mini"
class="cart-btn"
@click.prevent="addToCart(product)"
>
加入购物车
</el-button> </el-button>
</div> </div>
</div> </div>
@ -132,177 +120,19 @@ export default {
currentPage: 1, currentPage: 1,
pageSize: 12, pageSize: 12,
// //
products: [ visibleProducts: [],
{ totalProducts: 0,
id: 1,
name: "无线蓝牙耳机 主动降噪长续航",
image: "https://picsum.photos/id/101/300/300",
price: 359.0,
originalPrice: 499.0,
category: "digital",
tag: "限时折扣",
tagColor: "#FF6B6B",
sales: 120,
},
{
id: 2,
name: "智能手表 心率监测运动计步",
image: "https://picsum.photos/id/102/300/300",
price: 259.0,
originalPrice: 329.0,
category: "digital",
tag: "新品",
tagColor: "#4ECDC4",
sales: 86,
},
{
id: 3,
name: "纯棉短袖T恤 宽松休闲",
image: "https://picsum.photos/id/103/300/300",
price: 89.0,
originalPrice: 129.0,
category: "life",
sales: 320,
},
{
id: 4,
name: "新鲜水果礼盒 当季混合装",
image: "https://picsum.photos/id/104/300/300",
price: 159.0,
category: "food",
tag: "热销",
tagColor: "#FF9F1C",
sales: 215,
},
{
id: 5,
name: "全自动咖啡机 家用小型",
image: "https://picsum.photos/id/105/300/300",
price: 1299.0,
originalPrice: 1599.0,
category: "life",
sales: 45,
},
{
id: 6,
name: "高清投影仪 家用办公两用",
image: "https://picsum.photos/id/106/300/300",
price: 2499.0,
category: "digital",
tag: "推荐",
tagColor: "#2EC4B6",
sales: 78,
},
{
id: 7,
name: "进口红酒 赤霞珠干红",
image: "https://picsum.photos/id/107/300/300",
price: 199.0,
originalPrice: 258.0,
category: "food",
sales: 63,
},
{
id: 8,
name: "瑜伽垫 防滑专业健身垫",
image: "https://picsum.photos/id/108/300/300",
price: 129.0,
category: "life",
sales: 156,
},
{
id: 9,
name: "机械键盘 青轴游戏专用",
image: "https://picsum.photos/id/109/300/300",
price: 299.0,
originalPrice: 399.0,
category: "digital",
tag: "限时折扣",
tagColor: "#FF6B6B",
sales: 92,
},
{
id: 10,
name: "有机蔬菜礼盒 新鲜配送",
image: "https://picsum.photos/id/110/300/300",
price: 89.0,
category: "food",
sales: 205,
},
{
id: 11,
name: "北欧风落地灯 客厅卧室",
image: "https://picsum.photos/id/111/300/300",
price: 199.0,
category: "life",
sales: 57,
},
{
id: 12,
name: "便携式充电宝 20000mAh",
image: "https://picsum.photos/id/112/300/300",
price: 129.0,
originalPrice: 169.0,
category: "digital",
sales: 310,
},
],
// //
categories: [ categories: [],
{ label: "全部", value: "" }, id: "",
{ label: "美食", value: "food" }, keyword: "",
{ label: "生活", value: "life" },
{ label: "数码", value: "digital" },
],
}; };
}, },
computed: { computed: {
...mapGetters(["getSearchText"]), ...mapGetters(["getSearchText"]),
// // offset
filteredProducts() { currentOffset() {
return this.products.filter((product) => { return (this.currentPage - 1) * this.pageSize;
//
const categoryMatch = this.selectedCategory
? product.category === this.selectedCategory
: true;
//
const keywordMatch = this.searchKeyword
? product.name
.toLowerCase()
.includes(this.searchKeyword.toLowerCase())
: true;
return categoryMatch && keywordMatch;
});
},
//
sortedProducts() {
const sorted = [...this.filteredProducts];
console.log(...this.filteredProducts, sorted);
switch (this.selectedSort) {
case "price_asc":
return sorted.sort((a, b) => a.price - b.price);
case "price_desc":
return sorted.sort((a, b) => b.price - a.price);
case "sales_asc":
return sorted.sort((a, b) => a.sales - b.sales);
case "sales_desc":
return sorted.sort((a, b) => b.sales - a.sales);
default: // ID
return sorted.sort((a, b) => a.id - b.id);
}
},
//
visibleProducts() {
const start = (this.currentPage - 1) * this.pageSize;
const end = start + this.pageSize;
return this.sortedProducts.slice(start, end);
},
//
totalProducts() {
return this.sortedProducts.length;
}, },
}, },
watch: { watch: {
@ -317,58 +147,142 @@ export default {
created() { created() {
this.type = this.$route.query?.type; this.type = this.$route.query?.type;
this.searchKeyword = this.getSearchText; this.searchKeyword = this.getSearchText;
this.getTagList();
//
this.$bus.on("search-product", (keyword) => {
//
this.searchKeyword = keyword;
//
this.currentPage = 1;
//
this.fetchProducts(); this.fetchProducts();
});
},
//
beforeDestroy() {
this.$bus.off("search-product");
//
this.searchKeyword = "";
// Vuex
this.$store.commit("setSearchText", "");
//
this.currentPage = 1;
this.selectedCategory = "";
this.selectedSort = "default";
}, },
methods: { methods: {
// //
getTagList() {
this.post(
{
pid: 683,
},
"/api/product/tag_list"
).then((res) => {
this.categories = res.data;
this.categories[0].id = 662;
// id
const routeId = this.$route.query.id || 662;
if (routeId) {
// id
this.id = routeId;
//
const matchedCategory = this.categories.find(
(cat) => cat.id == routeId
);
console.log(routeId, matchedCategory);
if (matchedCategory) {
this.selectedCategory = matchedCategory.name;
}
}
this.fetchProducts();
});
},
// - 使offsetlimit
async fetchProducts() { async fetchProducts() {
console.log(this.getSearchText); //
// const response = await this.$axios.get('/api/products', { params }); const params = {
tag_id: this.id || this.selectedCategory,
offset: this.currentOffset,
limit: this.pageSize,
title: this.searchKeyword,
...this.parseSortParams(),
};
//
if (this.searchKeyword) {
params.keyword = this.searchKeyword;
}
const response = await this.post(
params,
"/api/product/get_product_by_tag"
);
// listtotal
this.visibleProducts = response.data.list || [];
this.totalProducts = Number(response.data.total) || 0;
}, },
// //
handleSearch() { parseSortParams() {
this.currentPage = 1; // if (this.selectedSort === "default") {
return {}; //
}
//
const [sortField, order] = this.selectedSort.split("_");
//
const sortMap = {
sales: "sales_number",
price: "price",
};
return {
sort: sortMap[sortField],
order: order,
};
}, },
// //
handleCategoryChange() { handleCategoryChange(event) {
this.id = event;
this.currentPage = 1; // this.currentPage = 1; //
this.fetchProducts();
}, },
// //
handleSortChange() { handleSortChange() {
this.currentPage = 1; // this.currentPage = 1; //
this.fetchProducts();
}, },
// //
handleSizeChange(val) { handleSizeChange(val) {
this.pageSize = val; this.pageSize = val;
this.currentPage = 1; this.currentPage = 1; //
this.fetchProducts();
}, },
// //
handleCurrentChange(val) { handleCurrentChange(val) {
this.currentPage = val; this.currentPage = val;
this.fetchProducts();
// //
window.scrollTo(0, 0); window.scrollTo(0, 0);
}, },
//
addToCart(product) {
this.$message.success(`${product.name} 已加入购物车`);
// API
},
// //
goToDetail(id) { goToDetail(id) {
this.$router.push(`/product/${id}`); this.$router.push(`/Detail/${id}`);
}, },
}, },
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
/* 样式部分保持不变 */
.product-grid-page { .product-grid-page {
padding: 20px; padding: 20px;
background-color: #f5f7fa; background-color: #f5f7fa;

269
src/views/User/UserAddress.vue

@ -1,32 +1,56 @@
<template> <template>
<div class="bg"> <div class="bg">
<el-card style="width: 100%;"> <el-card style="width: 100%">
<div slot="header" class="clearfix flex-between"> <div slot="header" class="clearfix flex-between">
<div> <div>
收货地址 收货地址
<span style="font-size: 12px;color:#666">收票人地址统一使用该地址管理设置</span> <span style="font-size: 12px; color: #666"
>收票人地址统一使用该地址管理设置</span
>
</div> </div>
<el-button size="small" style="float: right;" type="primary" @click="openDialog('add', {})">新增地址</el-button> <el-button
size="small"
style="float: right"
type="primary"
@click="openDialog('add', {})"
>新增地址</el-button
>
</div> </div>
<el-table :data="addresses" border style="width: 100%;"> <el-table :data="addresses" border style="width: 100%">
<el-table-column prop="receiver" label="收货人" width="120"></el-table-column> <el-table-column
<el-table-column prop="region" label="所在地区" width="120"></el-table-column> prop="name"
<el-table-column prop="detailAddress" label="详细地址" width="300"></el-table-column> label="收货人"
<el-table-column prop="phone" label="手机号码" width="150"></el-table-column> width="120"
<el-table-column prop="fixedPhone" label="固定电话" width="150"></el-table-column> ></el-table-column>
<el-table-column prop="alias" label="地区别名" width="120"></el-table-column> <el-table-column
prop="address"
label="收货地址"
width="auto"
></el-table-column>
<el-table-column
prop="tel"
label="手机号码"
width="150"
></el-table-column>
<el-table-column label="操作" width="200"> <el-table-column label="操作" width="200">
<template slot-scope="scope"> <template slot-scope="scope">
<el-button size="mini" @click="handleEdit(scope.row)">编辑</el-button> <el-button size="mini" @click="handleEdit(scope.row)"
<el-button size="mini" type="danger" @click="handleDelete(scope.row)">删除</el-button> >编辑</el-button
>
<el-button
size="mini"
type="danger"
@click="handleDelete(scope.row)"
>删除</el-button
>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<!-- 分页 --> <!-- 分页 -->
<div style="text-align: right; margin-top: 20px;"> <div style="text-align: right; margin-top: 20px">
<el-pagination <el-pagination
@size-change="handleSizeChange" @size-change="handleSizeChange"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
@ -37,165 +61,138 @@
:total="searchParam.total" :total="searchParam.total"
></el-pagination> ></el-pagination>
</div> </div>
</el-card> </el-card>
<!-- 引入地址编辑组件 -->
<el-dialog :title="`${address.type === 'add' ? '新增' : '编辑'}收货地址`" :visible.sync="dialogVisible" width="700px"> <AddressFormDialog
<el-form :model="address" label-width="100px" size="small"> :visible="dialogVisible"
<el-form-item label="*收货人"> :type="dialogType"
<el-input v-model="address.receiver" placeholder="请填写收货人姓名,限制40个字符"></el-input> :initial-data="dialogData"
</el-form-item> @update:visible="dialogVisible = $event"
<el-form-item label="*所在地区"> @save="handleSaveAddress"
<el-select v-model="address.region" placeholder="请选择所在地区"> />
<el-option label="北京" value="北京"></el-option>
<el-option label="上海" value="上海"></el-option>
<el-option label="广州" value="广州"></el-option>
<el-option label="深圳" value="深圳"></el-option>
</el-select>
</el-form-item>
<el-form-item label="*详细地址">
<el-input v-model="address.detailAddress" placeholder="请填写详细地址,限制500个字符"></el-input>
</el-form-item>
<el-form-item label="*手机号码">
<el-input v-model="address.phone" placeholder="请填写手机号码,限制11位数字"></el-input>
</el-form-item>
<el-form-item label="固定电话">
<el-input v-model="address.fixedPhone" placeholder="请填写固定号码,限制40个字符"></el-input>
</el-form-item>
<el-form-item label="地区别名">
<el-input v-model="address.alias" placeholder="请填写常用名称,限制40个字符"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button size="small" @click="dialogVisible = false">取消</el-button>
<el-button size="small" type="primary" @click="saveAddress">保存</el-button>
</div>
</el-dialog>
</div> </div>
</template> </template>
<script> <script>
import AddressFormDialog from "@/components/common/AddressFormDialog.vue"; //
export default { export default {
name: 'UserInfo', name: "AddressManagement",
components: {
AddressFormDialog, //
},
data() { data() {
return { return {
user: {}, user: {},
searchParam: {pageSize: 5, currentPage: 1,total: 10}, searchParam: { pageSize: 5, currentPage: 1, total: 0 },
dialogVisible: false, dialogVisible: false,
addresses: [ dialogType: "add",
{ dialogData: {},
id: 1, addresses: [],
receiver: '张三', };
region: '北京',
detailAddress: '北京市朝阳区xx街道xx小区xx号楼xx单元',
phone: '13800138000',
fixedPhone: '010-12345678',
alias: '家'
}, },
{ mounted() {
id: 2, this.getAddress();
receiver: '李四',
region: '上海',
detailAddress: '上海市浦东新区xx街道xx小区xx号楼xx单元',
phone: '13900139000',
fixedPhone: '021-12345678',
alias: '公司'
}, },
methods: {
//
getAddress() {
this.post(
{ {
id: 3, contactType: "CONSIGNEE",
receiver: '王五', offset: "0",
region: '广州', limit: "100",
detailAddress: '广州市天河区xx街道xx小区xx号楼xx单元', },
phone: '13700137000', "/api/uservice/user/getContactOrConsignee"
fixedPhone: '020-12345678', ).then((res) => {
alias: '朋友家' if (res) {
} this.addresses = res.data || [];
], this.searchParam.total = this.addresses.length;
address: {
type: 'add',
id: '',
receiver: '',
region: '',
detailAddress: '',
phone: '',
fixedPhone: '',
alias: ''
}
} }
});
}, },
methods:{
// //
handleSizeChange(val) { handleSizeChange(val) {
this.searchParam.pageSize = val; this.searchParam.pageSize = val;
this.searchParam.currentPage = 1;
}, },
//
handleCurrentChange(val) { handleCurrentChange(val) {
this.searchParam.currentPage = val; this.searchParam.currentPage = val;
}, },
//
openDialog(type, item) { openDialog(type, item) {
this.address = { this.dialogType = type;
type: type, this.dialogData = { ...item };
id: item.id || "",
receiver: item.receiver || "",
region: item.region || "",
detailAddress: item.detailAddress || "",
phone: item.phone || "",
fixedPhone: item.fixedPhone || "",
alias: item.alias || ""
}
this.dialogVisible = true; this.dialogVisible = true;
}, },
saveAddress() { //
if (this.address.id) { handleSaveAddress(formData) {
// formData.is_default = 0;
const index = this.addresses.findIndex(item => item.id === this.address.id); if (this.dialogType === "add") {
if (index !== -1) { this.post(formData, "/api/uservice/user/addNewConsignee").then(
this.addresses[index] = { ...this.address }; (res) => {
if (res.code == 1) {
this.$message.success("保存成功");
this.getAddress();
this.dialogVisible = false;
}
} }
);
} else { } else {
// this.post(formData, "/api/uservice/user/editConsignee").then((res) => {
this.address.id = Date.now(); if (res.code == 1) {
this.addresses.push({ ...this.address }); this.$message.success("保存成功");
this.getAddress();
this.dialogVisible = false;
} }
this.$message({
message: '保存成功',
type: 'success'
}); });
this.dialogVisible = false; }
}, },
//
handleEdit(row) { handleEdit(row) {
this.address = { ...row }; console.log(row);
this.dialogVisible = true; row.username = row.name;
row.mobile = row.tel;
this.openDialog("edit", row);
}, },
//
handleDelete(row) { handleDelete(row) {
this.$confirm('此操作将永久删除该地址, 是否继续?', '提示', { this.$confirm("此操作将永久删除该地址, 是否继续?", "提示", {
confirmButtonText: '确定', confirmButtonText: "确定",
cancelButtonText: '取消', cancelButtonText: "取消",
type: 'warning' type: "warning",
}).then(() => { })
const index = this.addresses.findIndex(item => item.id === row.id); .then(() => {
if (index !== -1) { this.post(
this.addresses.splice(index, 1); {
id: row.id,
},
"/api/uservice/user/delConsignee"
).then((res) => {
if (res.code == 1) {
this.getAddress();
this.$message.success("删除成功!");
} }
this.$message({
type: 'success',
message: '删除成功!'
}); });
}).catch(() => { })
this.$message({ .catch(() => {
type: 'info', this.$message.info("已取消删除");
message: '已取消删除'
}); });
}); },
} },
} };
}
</script> </script>
<style lang="scss" scoped>
<style lang="scss" scoped>
.flex-between {
display: flex;
justify-content: space-between;
align-items: center;
}
</style> </style>
Loading…
Cancel
Save