基本实现了订单、购物车、商品等模块,商城v1.0
This commit is contained in:
25
src/apis/order.ts
Normal file
25
src/apis/order.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import http from "../utils/http";
|
||||||
|
import type {
|
||||||
|
// OrderListItem,
|
||||||
|
OrderDetail,
|
||||||
|
OrderPageParams,
|
||||||
|
OrderPageResponse,
|
||||||
|
} from "@/types/order";
|
||||||
|
|
||||||
|
// 获取订单列表
|
||||||
|
export const getOrderList = (params: OrderPageParams) => {
|
||||||
|
return http<OrderPageResponse>({
|
||||||
|
url: "/order/page",
|
||||||
|
method: "get",
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取订单详情
|
||||||
|
export const getOrderDetail = (id: number, userId: number) => {
|
||||||
|
return http<OrderDetail>({
|
||||||
|
url: `/order/${id}`,
|
||||||
|
method: "get",
|
||||||
|
params: { userId },
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,14 +1,41 @@
|
|||||||
import http from "../utils/http";
|
import http from "../utils/http";
|
||||||
|
import type {
|
||||||
|
// ProductListItem,
|
||||||
|
ProductCategory,
|
||||||
|
ProductDetail,
|
||||||
|
ProductPageParams,
|
||||||
|
ProductPageResponse,
|
||||||
|
} from "@/types/product";
|
||||||
|
|
||||||
export const addProduct = (data: {
|
// 获取商品列表
|
||||||
name: string;
|
export const getProductList = (params: ProductPageParams) => {
|
||||||
price: number;
|
return http<ProductPageResponse>({
|
||||||
description: string;
|
url: "/product/page",
|
||||||
image: string;
|
method: "get",
|
||||||
}) => {
|
params,
|
||||||
return http({
|
});
|
||||||
url: "/product",
|
};
|
||||||
method: "post",
|
|
||||||
data,
|
// 获取商品分类
|
||||||
|
export const getProductCategories = () => {
|
||||||
|
return http<ProductCategory[]>({
|
||||||
|
url: "/product/category",
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取子分类
|
||||||
|
export const getProductSubCategories = (id: number) => {
|
||||||
|
return http<ProductCategory[]>({
|
||||||
|
url: `/product/category/${id}`,
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取商品详情
|
||||||
|
export const getProductDetail = (id: number) => {
|
||||||
|
return http<ProductDetail>({
|
||||||
|
url: `/product/${id}`,
|
||||||
|
method: "get",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
47
src/apis/shoppingcart.ts
Normal file
47
src/apis/shoppingcart.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import http from "../utils/http";
|
||||||
|
import type { ShoppingCartItem, ShoppingCartDTO } from "@/types/shoppingcart";
|
||||||
|
|
||||||
|
// 获取用户购物车列表
|
||||||
|
export const getUserCartList = (userId: number) => {
|
||||||
|
return http<ShoppingCartItem[]>({
|
||||||
|
url: "/cart",
|
||||||
|
method: "get",
|
||||||
|
params: { userId },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加商品到购物车
|
||||||
|
export const addCart = (data: ShoppingCartDTO) => {
|
||||||
|
return http({
|
||||||
|
url: "/cart",
|
||||||
|
method: "post",
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新购物车商品数量
|
||||||
|
export const updateCartCount = (data: ShoppingCartDTO) => {
|
||||||
|
return http({
|
||||||
|
url: "/cart/count",
|
||||||
|
method: "put",
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新购物车商品勾选状态
|
||||||
|
export const updateCartChecked = (data: ShoppingCartDTO) => {
|
||||||
|
return http({
|
||||||
|
url: "/cart/checked",
|
||||||
|
method: "put",
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除购物车商品
|
||||||
|
export const deleteCart = (id: number, userId: number) => {
|
||||||
|
return http({
|
||||||
|
url: `/cart/${id}`,
|
||||||
|
method: "delete",
|
||||||
|
params: { userId },
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -66,16 +66,19 @@ const goToProfile = () => {
|
|||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="text-gray-600 hover:text-blue-600 transition-colors"
|
class="text-gray-600 hover:text-blue-600 transition-colors"
|
||||||
>商品分类</a
|
@click.prevent="$router.push('/product/product')"
|
||||||
|
>购买商品</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="text-gray-600 hover:text-blue-600 transition-colors"
|
class="text-gray-600 hover:text-blue-600 transition-colors"
|
||||||
|
@click.prevent="$router.push('/shoppingcart/content')"
|
||||||
>购物车</a
|
>购物车</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="text-gray-600 hover:text-blue-600 transition-colors"
|
class="text-gray-600 hover:text-blue-600 transition-colors"
|
||||||
|
@click.prevent="$router.push('/order/content')"
|
||||||
>我的订单</a
|
>我的订单</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
@@ -149,11 +152,13 @@ const goToProfile = () => {
|
|||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="py-2 text-gray-600 hover:text-blue-600 transition-colors"
|
class="py-2 text-gray-600 hover:text-blue-600 transition-colors"
|
||||||
>商品分类</a
|
@click.prevent="$router.push('/product/product')"
|
||||||
|
>购买商品</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="py-2 text-gray-600 hover:text-blue-600 transition-colors"
|
class="py-2 text-gray-600 hover:text-blue-600 transition-colors"
|
||||||
|
@click.prevent="$router.push('/shoppingcart/content')"
|
||||||
>购物车</a
|
>购物车</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
|
|||||||
334
src/pages/order/ContentPage.vue
Normal file
334
src/pages/order/ContentPage.vue
Normal file
@@ -0,0 +1,334 @@
|
|||||||
|
<template>
|
||||||
|
<MainLayout>
|
||||||
|
<div class="order-page">
|
||||||
|
<el-card class="order-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="order-header">
|
||||||
|
<span>我的订单</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-if="orderList.length === 0" class="empty-orders">
|
||||||
|
<el-empty description="暂无订单" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<!-- 订单列表 -->
|
||||||
|
<div class="order-list">
|
||||||
|
<el-table :data="orderList" style="width: 100%" row-key="id">
|
||||||
|
<el-table-column prop="orderNo" label="订单号" width="300" />
|
||||||
|
<el-table-column prop="createTime" label="下单时间" width="200">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ formatDate(scope.row.createTime) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
prop="productCount"
|
||||||
|
label="商品数量"
|
||||||
|
width="120"
|
||||||
|
/>
|
||||||
|
<el-table-column prop="totalAmount" label="订单总额" width="138">
|
||||||
|
<template #default="scope">
|
||||||
|
¥{{ scope.row.totalAmount.toFixed(2) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="statusText" label="订单状态" width="120" />
|
||||||
|
<el-table-column label="操作" width="150">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
link
|
||||||
|
@click="viewOrderDetail(scope.row)"
|
||||||
|
>
|
||||||
|
查看详情
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination-container">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="currentPage"
|
||||||
|
v-model:page-size="pageSize"
|
||||||
|
:page-sizes="[5, 10, 20, 50]"
|
||||||
|
:total="total"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 订单详情模态框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
title="订单详情"
|
||||||
|
width="900px"
|
||||||
|
class="order-detail-dialog"
|
||||||
|
>
|
||||||
|
<div v-if="currentOrder" class="order-detail">
|
||||||
|
<h3 class="section-title">订单信息</h3>
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="订单号">
|
||||||
|
{{ currentOrder.orderNo }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="下单时间">
|
||||||
|
{{ formatDate(currentOrder.createTime) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="订单状态">
|
||||||
|
{{ getOrderStatusText(currentOrder.status) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="支付方式">
|
||||||
|
{{ getPayTypeText(currentOrder.payType) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="订单总额">
|
||||||
|
¥{{ currentOrder.totalAmount.toFixed(2) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="实付金额">
|
||||||
|
¥{{ currentOrder.payAmount.toFixed(2) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
|
||||||
|
<h3 class="section-title">收货信息</h3>
|
||||||
|
<el-descriptions :column="1" border>
|
||||||
|
<el-descriptions-item label="收货人">
|
||||||
|
{{ currentOrder.receiverName }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="联系电话">
|
||||||
|
{{ currentOrder.receiverPhone }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="收货地址">
|
||||||
|
{{ currentOrder.receiverAddress }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
|
||||||
|
<h3 class="section-title">商品信息</h3>
|
||||||
|
<div class="order-items">
|
||||||
|
<el-table :data="currentOrder.orderItems" style="width: 100%">
|
||||||
|
<el-table-column prop="productImage" label="商品" width="300">
|
||||||
|
<template #default="scope">
|
||||||
|
<div class="product-info">
|
||||||
|
<el-image
|
||||||
|
:src="scope.row.productImage"
|
||||||
|
class="product-image"
|
||||||
|
fit="cover"
|
||||||
|
/>
|
||||||
|
<div class="product-details">
|
||||||
|
<div class="product-name">
|
||||||
|
{{ scope.row.productName }}
|
||||||
|
</div>
|
||||||
|
<div class="product-sku">{{ scope.row.skuName }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="price" label="单价" width="168">
|
||||||
|
<template #default="scope">
|
||||||
|
¥{{ scope.row.price.toFixed(2) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="quantity" label="数量" width="200" />
|
||||||
|
|
||||||
|
<el-table-column prop="totalPrice" label="小计" width="200">
|
||||||
|
<template #default="scope">
|
||||||
|
¥{{ scope.row.totalPrice.toFixed(2) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="dialogVisible = false">关闭</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</MainLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import MainLayout from "@/layouts/MainLayout.vue";
|
||||||
|
import { ref, onMounted } from "vue";
|
||||||
|
import { useUserStore } from "@/stores/UserStore";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
import { getOrderList, getOrderDetail } from "@/apis/order";
|
||||||
|
import type { OrderListItem, OrderDetail } from "@/types/order";
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
// 订单列表相关
|
||||||
|
const orderList = ref<OrderListItem[]>([]);
|
||||||
|
const currentPage = ref(1);
|
||||||
|
const pageSize = ref(10);
|
||||||
|
const total = ref(0);
|
||||||
|
|
||||||
|
// 订单详情相关
|
||||||
|
const dialogVisible = ref(false);
|
||||||
|
const currentOrder = ref<OrderDetail | null>(null);
|
||||||
|
|
||||||
|
// 获取订单列表
|
||||||
|
const loadOrderList = async () => {
|
||||||
|
if (!userStore.userInfo) {
|
||||||
|
ElMessage.warning("请先登录");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await getOrderList({
|
||||||
|
page: currentPage.value,
|
||||||
|
size: pageSize.value,
|
||||||
|
userId: userStore.userInfo.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.code === 200) {
|
||||||
|
orderList.value = res.data.list;
|
||||||
|
total.value = res.data.total;
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.message || "获取订单列表失败");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
ElMessage.error("获取订单列表失败");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理页面大小变化
|
||||||
|
const handleSizeChange = (val: number) => {
|
||||||
|
pageSize.value = val;
|
||||||
|
currentPage.value = 1;
|
||||||
|
loadOrderList();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理页码变化
|
||||||
|
const handleCurrentChange = (val: number) => {
|
||||||
|
currentPage.value = val;
|
||||||
|
loadOrderList();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 查看订单详情
|
||||||
|
const viewOrderDetail = async (order: OrderListItem) => {
|
||||||
|
if (!userStore.userInfo) {
|
||||||
|
ElMessage.warning("请先登录");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await getOrderDetail(order.id, userStore.userInfo.id);
|
||||||
|
if (res.code === 200) {
|
||||||
|
currentOrder.value = res.data;
|
||||||
|
dialogVisible.value = true;
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.message || "获取订单详情失败");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
ElMessage.error("获取订单详情失败");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
const formatDate = (dateString: string) => {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleDateString() + " " + date.toLocaleTimeString();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取订单状态文本
|
||||||
|
const getOrderStatusText = (status: number) => {
|
||||||
|
const statusMap: Record<number, string> = {
|
||||||
|
0: "待付款",
|
||||||
|
1: "待发货",
|
||||||
|
2: "待收货",
|
||||||
|
3: "已完成",
|
||||||
|
4: "已取消",
|
||||||
|
};
|
||||||
|
return statusMap[status] || "未知状态";
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取支付方式文本
|
||||||
|
const getPayTypeText = (payType: number) => {
|
||||||
|
const payTypeMap: Record<number, string> = {
|
||||||
|
0: "未支付",
|
||||||
|
1: "微信",
|
||||||
|
2: "支付宝",
|
||||||
|
};
|
||||||
|
return payTypeMap[payType] || "未知";
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadOrderList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.order-page {
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-header {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-orders {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
margin: 20px 0 10px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 商品信息样式仿照购物车 */
|
||||||
|
.product-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-image {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-details {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-name {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-sku {
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-items {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
383
src/pages/product/DetailPage.vue
Normal file
383
src/pages/product/DetailPage.vue
Normal file
@@ -0,0 +1,383 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, onMounted } from "vue";
|
||||||
|
import { useRoute, useRouter } from "vue-router";
|
||||||
|
import MainLayout from "@/layouts/MainLayout.vue";
|
||||||
|
import { useUserStore } from "@/stores/UserStore.ts";
|
||||||
|
import {
|
||||||
|
ElCard,
|
||||||
|
ElRow,
|
||||||
|
ElCol,
|
||||||
|
ElImage,
|
||||||
|
ElButton,
|
||||||
|
// ElBreadcrumb,
|
||||||
|
// ElBreadcrumbItem,
|
||||||
|
ElCarousel,
|
||||||
|
ElCarouselItem,
|
||||||
|
ElDivider,
|
||||||
|
ElIcon,
|
||||||
|
ElMessage,
|
||||||
|
} from "element-plus";
|
||||||
|
import { ArrowLeft, Plus, Minus } from "@element-plus/icons-vue";
|
||||||
|
import { getProductDetail } from "@/apis/product";
|
||||||
|
import type { ProductDetail, ProductSku } from "@/types/product";
|
||||||
|
import { addCart } from "@/apis/shoppingcart.ts";
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// 商品详情数据
|
||||||
|
const product = ref<ProductDetail | null>(null);
|
||||||
|
const loading = ref(true);
|
||||||
|
|
||||||
|
// 购买相关
|
||||||
|
const quantity = ref(1);
|
||||||
|
const selectedSku = ref<ProductSku | null>(null);
|
||||||
|
|
||||||
|
//获取用户信息
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
// 获取商品详情
|
||||||
|
const fetchProductDetail = async () => {
|
||||||
|
const productId = route.params.id as string;
|
||||||
|
if (!productId) {
|
||||||
|
ElMessage.error("商品ID无效");
|
||||||
|
router.push("/product/product");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await getProductDetail(Number(productId));
|
||||||
|
if (response.code === 200) {
|
||||||
|
product.value = response.data;
|
||||||
|
|
||||||
|
// 默认选择第一个SKU
|
||||||
|
if (response.data.skuList && response.data.skuList.length > 0) {
|
||||||
|
selectedSku.value = response.data.skuList[0];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.message || "获取商品详情失败");
|
||||||
|
router.push("/product");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error("获取商品详情失败");
|
||||||
|
console.error("获取商品详情失败:", error);
|
||||||
|
router.push("/product");
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 增加数量
|
||||||
|
const increaseQuantity = () => {
|
||||||
|
if (
|
||||||
|
product.value &&
|
||||||
|
quantity.value < (selectedSku.value?.stock || product.value.stock || 0)
|
||||||
|
) {
|
||||||
|
quantity.value++;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 减少数量
|
||||||
|
const decreaseQuantity = () => {
|
||||||
|
if (quantity.value > 1) {
|
||||||
|
quantity.value--;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加到购物车
|
||||||
|
const addToCart = async () => {
|
||||||
|
if (!product.value) {
|
||||||
|
ElMessage.warning("商品信息加载中...");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedSku.value) {
|
||||||
|
ElMessage.warning("请选择商品规格");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查用户是否已登录
|
||||||
|
if (!userStore.userInfo) {
|
||||||
|
ElMessage.warning("请先登录");
|
||||||
|
router.push("/user/login");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await addCart({
|
||||||
|
userId: userStore.userInfo.id,
|
||||||
|
productId: product.value.id,
|
||||||
|
skuId: selectedSku.value.id,
|
||||||
|
count: quantity.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.code === 200) {
|
||||||
|
ElMessage.success(`已将 ${product.value.name} 添加到购物车`);
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.message || "添加到购物车失败");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error("添加到购物车失败");
|
||||||
|
console.error("添加到购物车失败:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 立即购买
|
||||||
|
const buyNow = () => {
|
||||||
|
if (!product.value) {
|
||||||
|
ElMessage.warning("商品信息加载中...");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedSku.value) {
|
||||||
|
ElMessage.warning("请选择商品规格");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 这里应该是跳转到订单确认页面
|
||||||
|
// 暂时用提示代替
|
||||||
|
ElMessage.success("立即购买功能待实现");
|
||||||
|
console.log("立即购买:", {
|
||||||
|
productId: product.value.id,
|
||||||
|
skuId: selectedSku.value.id,
|
||||||
|
quantity: quantity.value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 选择SKU
|
||||||
|
const selectSku = (sku: ProductSku) => {
|
||||||
|
selectedSku.value = sku;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 返回商品列表
|
||||||
|
const goBack = () => {
|
||||||
|
router.push("/product");
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchProductDetail();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<MainLayout>
|
||||||
|
<div class="product-detail-page">
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<!-- 返回按钮 -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<el-button @click="goBack" type="primary" link>
|
||||||
|
<el-icon><ArrowLeft /></el-icon>
|
||||||
|
返回商品列表
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-card v-loading="loading" class="product-card">
|
||||||
|
<div v-if="product">
|
||||||
|
<el-row :gutter="30">
|
||||||
|
<!-- 商品图片 -->
|
||||||
|
<el-col :span="24" :md="12">
|
||||||
|
<div class="product-images">
|
||||||
|
<!-- 主图轮播 -->
|
||||||
|
<el-carousel
|
||||||
|
height="400px"
|
||||||
|
indicator-position="outside"
|
||||||
|
arrow="always"
|
||||||
|
>
|
||||||
|
<el-carousel-item v-if="product.mainImage">
|
||||||
|
<el-image
|
||||||
|
:src="product.mainImage"
|
||||||
|
class="w-full h-full object-contain"
|
||||||
|
fit="contain"
|
||||||
|
/>
|
||||||
|
</el-carousel-item>
|
||||||
|
<el-carousel-item
|
||||||
|
v-for="(image, index) in product.subImages?.split(',')"
|
||||||
|
:key="index"
|
||||||
|
v-show="image"
|
||||||
|
>
|
||||||
|
<el-image
|
||||||
|
:src="image"
|
||||||
|
class="w-full h-full object-contain"
|
||||||
|
fit="contain"
|
||||||
|
/>
|
||||||
|
</el-carousel-item>
|
||||||
|
</el-carousel>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<!-- 商品信息 -->
|
||||||
|
<el-col :span="24" :md="12">
|
||||||
|
<div class="product-info">
|
||||||
|
<h1 class="product-title text-2xl font-bold mb-4">
|
||||||
|
{{ product.name }}
|
||||||
|
</h1>
|
||||||
|
<p class="product-subtitle text-gray-600 mb-6">
|
||||||
|
{{ product.subtitle }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="price-section mb-6">
|
||||||
|
<div class="text-3xl text-red-600 font-bold">
|
||||||
|
¥{{ selectedSku?.price || product.price }}
|
||||||
|
</div>
|
||||||
|
<div class="text-gray-500 line-through mt-1">
|
||||||
|
原价: ¥{{ product.price }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-divider />
|
||||||
|
|
||||||
|
<!-- SKU选择 -->
|
||||||
|
<div
|
||||||
|
v-if="product.skuList && product.skuList.length > 0"
|
||||||
|
class="mb-6"
|
||||||
|
>
|
||||||
|
<div class="mb-3">
|
||||||
|
<span class="font-medium">规格:</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap gap-3">
|
||||||
|
<el-button
|
||||||
|
v-for="sku in product.skuList"
|
||||||
|
:key="sku.id"
|
||||||
|
:type="selectedSku?.id === sku.id ? 'primary' : 'info'"
|
||||||
|
plain
|
||||||
|
@click="selectSku(sku)"
|
||||||
|
>
|
||||||
|
{{ sku.skuName }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 数量选择 -->
|
||||||
|
<div class="quantity-section mb-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<span class="font-medium">数量:</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<el-button
|
||||||
|
:disabled="quantity <= 1"
|
||||||
|
:icon="Minus"
|
||||||
|
@click="decreaseQuantity"
|
||||||
|
/>
|
||||||
|
<div class="mx-3 w-12 text-center">{{ quantity }}</div>
|
||||||
|
<el-button
|
||||||
|
:disabled="
|
||||||
|
quantity >= (selectedSku?.stock || product.stock || 0)
|
||||||
|
"
|
||||||
|
:icon="Plus"
|
||||||
|
@click="increaseQuantity"
|
||||||
|
/>
|
||||||
|
<div class="ml-4 text-gray-500">
|
||||||
|
剩余库存: {{ selectedSku?.stock || product.stock }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<div class="action-buttons flex flex-wrap gap-4">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
@click="addToCart"
|
||||||
|
class="flex-1 min-w-[150px]"
|
||||||
|
>
|
||||||
|
加入购物车
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
size="large"
|
||||||
|
@click="buyNow"
|
||||||
|
class="flex-1 min-w-[150px]"
|
||||||
|
>
|
||||||
|
立即购买
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 商品详情 -->
|
||||||
|
<el-divider class="my-8" />
|
||||||
|
|
||||||
|
<div class="product-description">
|
||||||
|
<h2 class="text-xl font-bold mb-4">商品详情</h2>
|
||||||
|
<div class="description-content p-4 bg-gray-50 rounded">
|
||||||
|
<div
|
||||||
|
v-html="product.description || '暂无商品详情'"
|
||||||
|
class="prose max-w-none"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="!loading" class="text-center py-12">
|
||||||
|
<div class="text-gray-500">商品不存在或加载失败</div>
|
||||||
|
<el-button @click="goBack" type="primary" class="mt-4"
|
||||||
|
>返回商品列表</el-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MainLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.product-detail-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-title {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-subtitle {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-section {
|
||||||
|
padding: 15px;
|
||||||
|
background: #fff8f8;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quantity-section {
|
||||||
|
padding: 15px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-carousel__arrow) {
|
||||||
|
background-color: rgba(255, 255, 255, 0.8);
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-carousel__arrow:hover) {
|
||||||
|
background-color: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-carousel__indicator:hover button) {
|
||||||
|
background-color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-carousel__indicator.is-active button) {
|
||||||
|
background-color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-divider--horizontal) {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-content {
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
596
src/pages/product/ProductPage.vue
Normal file
596
src/pages/product/ProductPage.vue
Normal file
@@ -0,0 +1,596 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, onMounted } from "vue";
|
||||||
|
import MainLayout from "@/layouts/MainLayout.vue";
|
||||||
|
import {
|
||||||
|
ElCard,
|
||||||
|
ElRow,
|
||||||
|
ElCol,
|
||||||
|
ElPagination,
|
||||||
|
ElImage,
|
||||||
|
ElEmpty,
|
||||||
|
ElSkeleton,
|
||||||
|
ElBreadcrumb,
|
||||||
|
ElBreadcrumbItem,
|
||||||
|
} from "element-plus";
|
||||||
|
import { Search } from "@element-plus/icons-vue";
|
||||||
|
import {
|
||||||
|
getProductList,
|
||||||
|
getProductCategories,
|
||||||
|
getProductSubCategories,
|
||||||
|
} from "@/apis/product";
|
||||||
|
import type { ProductListItem, ProductCategory } from "@/types/product";
|
||||||
|
|
||||||
|
// 商品列表相关
|
||||||
|
const productList = ref<ProductListItem[]>([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
const currentPage = ref(1);
|
||||||
|
const pageSize = ref(12);
|
||||||
|
const total = ref(0);
|
||||||
|
|
||||||
|
// 分类相关
|
||||||
|
const categories = ref<ProductCategory[]>([]);
|
||||||
|
const activeCategoryId = ref<number | null>(null);
|
||||||
|
const expandedCategories = ref<number[]>([]); // 展开的分类ID列表
|
||||||
|
const breadcrumbCategories = ref<ProductCategory[]>([]); // 面包屑分类路径
|
||||||
|
const subCategoriesMap = ref<{ [key: number]: ProductCategory[] }>({}); // 子分类映射
|
||||||
|
|
||||||
|
// 搜索关键词
|
||||||
|
const searchKeyword = ref("");
|
||||||
|
|
||||||
|
// 获取商品列表
|
||||||
|
const fetchProductList = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const response = await getProductList({
|
||||||
|
page: currentPage.value,
|
||||||
|
size: pageSize.value,
|
||||||
|
categoryId: activeCategoryId.value || undefined,
|
||||||
|
name: searchKeyword.value || undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.code === 200) {
|
||||||
|
productList.value = response.data.list;
|
||||||
|
total.value = response.data.total;
|
||||||
|
} else {
|
||||||
|
productList.value = [];
|
||||||
|
total.value = 0;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("获取商品列表失败:", err);
|
||||||
|
productList.value = [];
|
||||||
|
total.value = 0;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取商品分类
|
||||||
|
const fetchCategories = async () => {
|
||||||
|
try {
|
||||||
|
const response = await getProductCategories();
|
||||||
|
if (response.code === 200) {
|
||||||
|
categories.value = response.data;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("获取商品分类失败:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取子分类
|
||||||
|
const fetchSubCategories = async (parentId: number) => {
|
||||||
|
// 如果已经获取过,直接返回
|
||||||
|
if (subCategoriesMap.value[parentId]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await getProductSubCategories(parentId);
|
||||||
|
if (response.code === 200) {
|
||||||
|
subCategoriesMap.value[parentId] = response.data;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("获取子分类失败:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 切换分类展开/收起状态
|
||||||
|
const toggleCategory = async (category: ProductCategory) => {
|
||||||
|
// 设置为当前激活的分类
|
||||||
|
activeCategoryId.value = category.id;
|
||||||
|
|
||||||
|
// 更新面包屑路径
|
||||||
|
await updateBreadcrumb(category);
|
||||||
|
|
||||||
|
// 检查并加载子分类
|
||||||
|
const hasChildren = await checkAndLoadChildren(category.id);
|
||||||
|
|
||||||
|
// 如果是顶级分类(parentId为0)
|
||||||
|
if (category.parentId === 0) {
|
||||||
|
if (hasChildren) {
|
||||||
|
// 如果有子分类
|
||||||
|
if (expandedCategories.value.includes(category.id)) {
|
||||||
|
// 如果已经展开,则收起
|
||||||
|
expandedCategories.value = expandedCategories.value.filter(
|
||||||
|
(id) => id !== category.id,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// 收起所有顶级分类的展开状态
|
||||||
|
const topLevelCategoryIds = categories.value.map((cat) => cat.id);
|
||||||
|
expandedCategories.value = expandedCategories.value.filter(
|
||||||
|
(id) => !topLevelCategoryIds.includes(id),
|
||||||
|
);
|
||||||
|
// 展开当前分类
|
||||||
|
expandedCategories.value = [...expandedCategories.value, category.id];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果没有子分类,收起所有已展开的分类
|
||||||
|
expandedCategories.value = [];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果是非顶级分类,保持原有逻辑
|
||||||
|
if (hasChildren) {
|
||||||
|
if (expandedCategories.value.includes(category.id)) {
|
||||||
|
// 如果已经展开,则收起
|
||||||
|
expandedCategories.value = expandedCategories.value.filter(
|
||||||
|
(id) => id !== category.id,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// 如果未展开,则展开
|
||||||
|
expandedCategories.value = [...expandedCategories.value, category.id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新加载商品列表
|
||||||
|
currentPage.value = 1;
|
||||||
|
fetchProductList();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 检查并加载子分类
|
||||||
|
const checkAndLoadChildren = async (categoryId: number): Promise<boolean> => {
|
||||||
|
// 如果还没有加载过该分类的子分类,则加载
|
||||||
|
if (!subCategoriesMap.value[categoryId]) {
|
||||||
|
await fetchSubCategories(categoryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回是否有子分类
|
||||||
|
return (
|
||||||
|
subCategoriesMap.value[categoryId] &&
|
||||||
|
subCategoriesMap.value[categoryId].length > 0
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新面包屑路径
|
||||||
|
const updateBreadcrumb = async (category: ProductCategory) => {
|
||||||
|
const path: ProductCategory[] = [category];
|
||||||
|
let currentCategory = category;
|
||||||
|
|
||||||
|
// 向上查找父级分类
|
||||||
|
while (currentCategory.parentId !== 0) {
|
||||||
|
// 查找父级分类
|
||||||
|
const parentCategory = findCategoryById(currentCategory.parentId);
|
||||||
|
if (parentCategory) {
|
||||||
|
path.unshift(parentCategory);
|
||||||
|
currentCategory = parentCategory;
|
||||||
|
} else {
|
||||||
|
// 如果找不到父级分类,则尝试从服务器获取
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
breadcrumbCategories.value = path;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 根据ID查找分类
|
||||||
|
const findCategoryById = (id: number): ProductCategory | null => {
|
||||||
|
// 先在主分类中查找
|
||||||
|
for (const category of categories.value) {
|
||||||
|
if (category.id === id) {
|
||||||
|
return category;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在已加载的子分类中查找
|
||||||
|
for (const subCategories of Object.values(subCategoriesMap.value)) {
|
||||||
|
const found = subCategories.find((cat) => cat.id === id);
|
||||||
|
if (found) {
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取指定分类的子分类
|
||||||
|
const getSubCategories = (parentId: number): ProductCategory[] => {
|
||||||
|
return subCategoriesMap.value[parentId] || [];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理面包屑点击
|
||||||
|
const handleBreadcrumbClick = (category: ProductCategory | null) => {
|
||||||
|
if (category) {
|
||||||
|
toggleCategory(category);
|
||||||
|
} else {
|
||||||
|
// 点击"全部商品"
|
||||||
|
activeCategoryId.value = null;
|
||||||
|
expandedCategories.value = [];
|
||||||
|
breadcrumbCategories.value = [];
|
||||||
|
currentPage.value = 1;
|
||||||
|
fetchProductList();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理分页变化
|
||||||
|
const handlePageChange = (page: number) => {
|
||||||
|
currentPage.value = page;
|
||||||
|
fetchProductList();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理页面大小变化
|
||||||
|
const handleSizeChange = (size: number) => {
|
||||||
|
pageSize.value = size;
|
||||||
|
currentPage.value = 1;
|
||||||
|
fetchProductList();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理搜索
|
||||||
|
const handleSearch = () => {
|
||||||
|
currentPage.value = 1;
|
||||||
|
fetchProductList();
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchProductList();
|
||||||
|
fetchCategories();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<MainLayout>
|
||||||
|
<div class="product-page">
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<!-- 页面标题 -->
|
||||||
|
<div class="page-header mb-8">
|
||||||
|
<h1 class="text-3xl font-bold text-gray-800">商品列表</h1>
|
||||||
|
<p class="text-gray-600 mt-2">发现你喜欢的商品</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 面包屑导航 -->
|
||||||
|
<div class="breadcrumb mb-6">
|
||||||
|
<el-breadcrumb separator="/">
|
||||||
|
<el-breadcrumb-item>
|
||||||
|
<a
|
||||||
|
@click="handleBreadcrumbClick(null)"
|
||||||
|
class="cursor-pointer hover:text-blue-600"
|
||||||
|
>
|
||||||
|
全部商品
|
||||||
|
</a>
|
||||||
|
</el-breadcrumb-item>
|
||||||
|
<el-breadcrumb-item
|
||||||
|
v-for="category in breadcrumbCategories"
|
||||||
|
:key="category.id"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
@click="handleBreadcrumbClick(category)"
|
||||||
|
class="cursor-pointer hover:text-blue-600"
|
||||||
|
>
|
||||||
|
{{ category.name }}
|
||||||
|
</a>
|
||||||
|
</el-breadcrumb-item>
|
||||||
|
</el-breadcrumb>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 搜索栏 -->
|
||||||
|
<div class="search-bar mb-8">
|
||||||
|
<div class="flex">
|
||||||
|
<el-input
|
||||||
|
v-model="searchKeyword"
|
||||||
|
placeholder="搜索商品..."
|
||||||
|
class="flex-1"
|
||||||
|
@keyup.enter="handleSearch"
|
||||||
|
>
|
||||||
|
<template #append>
|
||||||
|
<el-button :icon="Search" @click="handleSearch" />
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 商品分类 -->
|
||||||
|
<div class="category-section mb-8">
|
||||||
|
<el-card class="category-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span class="font-bold">商品分类</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="category-container">
|
||||||
|
<!-- 一级分类 -->
|
||||||
|
<div class="category-level">
|
||||||
|
<div
|
||||||
|
v-for="category in categories"
|
||||||
|
:key="category.id"
|
||||||
|
class="category-item"
|
||||||
|
:class="{ active: activeCategoryId === category.id }"
|
||||||
|
@click="toggleCategory(category)"
|
||||||
|
>
|
||||||
|
<span>{{ category.name }}</span>
|
||||||
|
<i
|
||||||
|
v-if="getSubCategories(category.id).length > 0"
|
||||||
|
:class="[
|
||||||
|
'el-icon-arrow-down',
|
||||||
|
'transition-transform',
|
||||||
|
{
|
||||||
|
'rotate-180': expandedCategories.includes(category.id),
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 二级分类 -->
|
||||||
|
<template v-for="category in categories" :key="category.id">
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
expandedCategories.includes(category.id) &&
|
||||||
|
getSubCategories(category.id).length > 0
|
||||||
|
"
|
||||||
|
class="category-level ml-4"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="subCategory in getSubCategories(category.id)"
|
||||||
|
:key="subCategory.id"
|
||||||
|
class="category-item"
|
||||||
|
:class="{ active: activeCategoryId === subCategory.id }"
|
||||||
|
@click="toggleCategory(subCategory)"
|
||||||
|
>
|
||||||
|
<span>{{ subCategory.name }}</span>
|
||||||
|
<i
|
||||||
|
v-if="getSubCategories(subCategory.id).length > 0"
|
||||||
|
:class="[
|
||||||
|
'el-icon-arrow-down',
|
||||||
|
'transition-transform',
|
||||||
|
{
|
||||||
|
'rotate-180': expandedCategories.includes(
|
||||||
|
subCategory.id,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 三级分类 -->
|
||||||
|
<template
|
||||||
|
v-for="subCategory in getSubCategories(category.id)"
|
||||||
|
:key="subCategory.id"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
expandedCategories.includes(subCategory.id) &&
|
||||||
|
getSubCategories(subCategory.id).length > 0
|
||||||
|
"
|
||||||
|
class="category-level ml-4"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="subSubCategory in getSubCategories(
|
||||||
|
subCategory.id,
|
||||||
|
)"
|
||||||
|
:key="subSubCategory.id"
|
||||||
|
class="category-item"
|
||||||
|
:class="{
|
||||||
|
active: activeCategoryId === subSubCategory.id,
|
||||||
|
}"
|
||||||
|
@click="toggleCategory(subSubCategory)"
|
||||||
|
>
|
||||||
|
<span>{{ subSubCategory.name }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<!-- 左侧占位(如果需要放置其他内容) -->
|
||||||
|
<el-col :span="6" class="hidden md:block">
|
||||||
|
<!-- 可以放置其他侧边栏内容 -->
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<!-- 右侧商品列表 -->
|
||||||
|
<el-col :span="24" class="md:col-span-24">
|
||||||
|
<!-- 商品列表 -->
|
||||||
|
<div class="product-list">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<template v-if="loading">
|
||||||
|
<el-col
|
||||||
|
v-for="n in pageSize"
|
||||||
|
:key="n"
|
||||||
|
:span="24"
|
||||||
|
class="sm:col-span-12 md:col-span-8 lg:col-span-6 mb-6"
|
||||||
|
>
|
||||||
|
<el-card class="product-card">
|
||||||
|
<el-skeleton :rows="3" animated />
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="productList.length > 0">
|
||||||
|
<el-col
|
||||||
|
v-for="product in productList"
|
||||||
|
:key="product.id"
|
||||||
|
:span="24"
|
||||||
|
class="sm:col-span-12 md:col-span-8 lg:col-span-6 mb-6"
|
||||||
|
>
|
||||||
|
<!-- 添加点击事件跳转到详情页 -->
|
||||||
|
<el-card
|
||||||
|
class="product-card h-full cursor-pointer"
|
||||||
|
shadow="hover"
|
||||||
|
@click="$router.push(`/product/detail/${product.id}`)"
|
||||||
|
>
|
||||||
|
<div class="product-image mb-3">
|
||||||
|
<el-image
|
||||||
|
:src="product.mainImage || '/placeholder.jpg'"
|
||||||
|
class="w-full h-48 object-cover rounded"
|
||||||
|
fit="cover"
|
||||||
|
lazy
|
||||||
|
>
|
||||||
|
<template #error>
|
||||||
|
<div class="image-slot">
|
||||||
|
<i
|
||||||
|
class="el-icon-picture-outline text-4xl text-gray-300"
|
||||||
|
></i>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-image>
|
||||||
|
</div>
|
||||||
|
<div class="product-info">
|
||||||
|
<h3
|
||||||
|
class="product-title text-sm font-medium mb-2 line-clamp-2 h-12"
|
||||||
|
:title="product.name"
|
||||||
|
>
|
||||||
|
{{ product.name }}
|
||||||
|
</h3>
|
||||||
|
<div class="product-price text-red-600 font-bold mb-2">
|
||||||
|
¥{{ product.price }}
|
||||||
|
</div>
|
||||||
|
<div class="product-sales text-gray-500 text-xs">
|
||||||
|
销量: {{ product.sales }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-empty description="暂无商品数据" />
|
||||||
|
</el-col>
|
||||||
|
</template>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination-container mt-8 flex justify-center">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="currentPage"
|
||||||
|
v-model:page-size="pageSize"
|
||||||
|
:page-sizes="[12, 24, 36, 48]"
|
||||||
|
:total="total"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@current-change="handlePageChange"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MainLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.product-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-card {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-container {
|
||||||
|
max-height: 500px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-level {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
border-radius: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-item:hover {
|
||||||
|
background-color: #e1eaff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-item.active {
|
||||||
|
background-color: #409eff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-item i {
|
||||||
|
font-size: 12px;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card {
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-slot {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: #f5f7fa;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-clamp-2 {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-breadcrumb__inner a) {
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滚动条样式 */
|
||||||
|
.category-container::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-container::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-container::-webkit-scrollbar-thumb {
|
||||||
|
background: #c1c1c1;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-container::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #a1a1a1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
407
src/pages/shoppingcart/ContentPage.vue
Normal file
407
src/pages/shoppingcart/ContentPage.vue
Normal file
@@ -0,0 +1,407 @@
|
|||||||
|
<template>
|
||||||
|
<MainLayout>
|
||||||
|
<div class="shopping-cart-page">
|
||||||
|
<el-card class="cart-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="cart-header">
|
||||||
|
<span>购物车</span>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
:disabled="!hasSelectedItems"
|
||||||
|
@click="handleClearSelected"
|
||||||
|
>
|
||||||
|
删除选中商品
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-if="cartItems.length === 0" class="empty-cart">
|
||||||
|
<el-empty description="购物车为空" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<!-- 全选栏 -->
|
||||||
|
<div class="select-all">
|
||||||
|
<el-checkbox v-model="selectAll" @change="handleSelectAllChange">
|
||||||
|
全选
|
||||||
|
</el-checkbox>
|
||||||
|
<div class="cart-summary">
|
||||||
|
<span>已选 {{ selectedCount }} 件商品</span>
|
||||||
|
<span class="total-price"
|
||||||
|
>合计:¥{{ totalPrice.toFixed(2) }}</span
|
||||||
|
>
|
||||||
|
<el-button type="primary" :disabled="selectedCount === 0">
|
||||||
|
结算
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 购物车商品列表 -->
|
||||||
|
<div class="cart-items">
|
||||||
|
<el-table :data="cartItems" style="width: 100%" row-key="id">
|
||||||
|
<el-table-column width="50">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-checkbox
|
||||||
|
v-model="scope.row.checked"
|
||||||
|
:true-value="1"
|
||||||
|
:false-value="0"
|
||||||
|
@change="
|
||||||
|
(val: boolean | string | number) =>
|
||||||
|
handleItemCheckChange(scope.row, val)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="productImage" label="商品" width="300">
|
||||||
|
<template #default="scope">
|
||||||
|
<div class="product-info">
|
||||||
|
<el-image
|
||||||
|
:src="scope.row.productImage"
|
||||||
|
class="product-image"
|
||||||
|
fit="cover"
|
||||||
|
/>
|
||||||
|
<div class="product-details">
|
||||||
|
<div class="product-name">
|
||||||
|
{{ scope.row.productName }}
|
||||||
|
</div>
|
||||||
|
<div class="product-sku">{{ scope.row.skuName }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="price" label="单价" width="168">
|
||||||
|
<template #default="scope"> ¥{{ scope.row.price }} </template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="count" label="数量" width="200">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-input-number
|
||||||
|
v-model="scope.row.count"
|
||||||
|
:min="1"
|
||||||
|
:max="999"
|
||||||
|
@change="
|
||||||
|
(val: number | undefined) =>
|
||||||
|
handleCountChange(scope.row, val)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="subtotal" label="小计" width="200">
|
||||||
|
<template #default="scope">
|
||||||
|
¥{{ (scope.row.price * scope.row.count).toFixed(2) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="操作" width="110">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
link
|
||||||
|
@click="handleDeleteItem(scope.row)"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</MainLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import MainLayout from "@/layouts/MainLayout.vue";
|
||||||
|
import { ref, computed, onMounted } from "vue";
|
||||||
|
import { useUserStore } from "@/stores/UserStore";
|
||||||
|
import {
|
||||||
|
getUserCartList,
|
||||||
|
updateCartCount,
|
||||||
|
updateCartChecked,
|
||||||
|
deleteCart,
|
||||||
|
} from "@/apis/shoppingcart";
|
||||||
|
import { ElMessage, ElMessageBox } from "element-plus";
|
||||||
|
|
||||||
|
interface CartItem {
|
||||||
|
id: number;
|
||||||
|
userId: number;
|
||||||
|
productId: number;
|
||||||
|
skuId: number;
|
||||||
|
count: number;
|
||||||
|
checked: number; // 0-未勾选 1-已勾选
|
||||||
|
productName: string;
|
||||||
|
productImage: string;
|
||||||
|
skuName: string;
|
||||||
|
price: number;
|
||||||
|
specJson: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const cartItems = ref<CartItem[]>([]);
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const selectAll = computed({
|
||||||
|
get: () =>
|
||||||
|
cartItems.value.length > 0 &&
|
||||||
|
cartItems.value.every((item) => item.checked === 1),
|
||||||
|
set: (val) => {
|
||||||
|
cartItems.value.forEach((item) => {
|
||||||
|
item.checked = val ? 1 : 0;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedItems = computed(() => {
|
||||||
|
return cartItems.value.filter((item) => item.checked === 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasSelectedItems = computed(() => {
|
||||||
|
return selectedItems.value.length > 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedCount = computed(() => {
|
||||||
|
return selectedItems.value.reduce((total, item) => total + item.count, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalPrice = computed(() => {
|
||||||
|
return selectedItems.value.reduce(
|
||||||
|
(total, item) => total + item.price * item.count,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取购物车列表
|
||||||
|
const loadCartItems = async () => {
|
||||||
|
if (!userStore.userInfo) {
|
||||||
|
ElMessage.warning("请先登录");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await getUserCartList(userStore.userInfo.id);
|
||||||
|
if (res.code === 200) {
|
||||||
|
cartItems.value = res.data.map((item) => ({
|
||||||
|
...item,
|
||||||
|
checked: 0, // 默认不选中
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.message || "获取购物车列表失败");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
ElMessage.error("获取购物车列表失败");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理全选变化
|
||||||
|
const handleSelectAllChange = async (val: boolean) => {
|
||||||
|
if (!userStore.userInfo) return;
|
||||||
|
|
||||||
|
// 更新所有项目的选中状态
|
||||||
|
for (const item of cartItems.value) {
|
||||||
|
try {
|
||||||
|
await updateCartChecked({
|
||||||
|
id: item.id,
|
||||||
|
userId: userStore.userInfo.id,
|
||||||
|
productId: item.productId,
|
||||||
|
skuId: item.skuId,
|
||||||
|
checked: val ? 1 : 0,
|
||||||
|
});
|
||||||
|
item.checked = val ? 1 : 0;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
ElMessage.error("更新选中状态失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ElMessage.success(val ? "已全选" : "已取消全选");
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理单项选中变化
|
||||||
|
const handleItemCheckChange = async (
|
||||||
|
item: CartItem,
|
||||||
|
val: boolean | string | number,
|
||||||
|
) => {
|
||||||
|
if (!userStore.userInfo) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateCartChecked({
|
||||||
|
id: item.id,
|
||||||
|
userId: userStore.userInfo.id,
|
||||||
|
productId: item.productId,
|
||||||
|
skuId: item.skuId,
|
||||||
|
checked: val ? 1 : 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
item.checked = val ? 1 : 0;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
ElMessage.error("更新选中状态失败");
|
||||||
|
// 恢复原状态
|
||||||
|
item.checked = !val ? 1 : 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理数量变化
|
||||||
|
const handleCountChange = async (item: CartItem, val: number | undefined) => {
|
||||||
|
if (!userStore.userInfo || val === undefined) return;
|
||||||
|
|
||||||
|
const oldCount = item.count; // 保存原始数量
|
||||||
|
try {
|
||||||
|
await updateCartCount({
|
||||||
|
id: item.id,
|
||||||
|
userId: userStore.userInfo.id,
|
||||||
|
productId: item.productId,
|
||||||
|
skuId: item.skuId,
|
||||||
|
count: val,
|
||||||
|
});
|
||||||
|
|
||||||
|
item.count = val;
|
||||||
|
ElMessage.success("数量更新成功");
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
ElMessage.error("更新数量失败");
|
||||||
|
// 恢复原数量
|
||||||
|
item.count = oldCount;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理删除单个项目
|
||||||
|
const handleDeleteItem = async (item: CartItem) => {
|
||||||
|
if (!userStore.userInfo) return;
|
||||||
|
|
||||||
|
ElMessageBox.confirm("确定要删除该商品吗?", "提示", {
|
||||||
|
type: "warning",
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
try {
|
||||||
|
const res = await deleteCart(item.id, userStore.userInfo!.id);
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage.success("删除成功");
|
||||||
|
// 从列表中移除
|
||||||
|
cartItems.value = cartItems.value.filter(
|
||||||
|
(cartItem) => cartItem.id !== item.id,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.message || "删除失败");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
ElMessage.error("删除失败");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// 取消删除
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理清空选中项
|
||||||
|
const handleClearSelected = async () => {
|
||||||
|
if (!userStore.userInfo) return;
|
||||||
|
|
||||||
|
ElMessageBox.confirm(
|
||||||
|
`确定要删除选中的 ${selectedCount.value} 件商品吗?`,
|
||||||
|
"提示",
|
||||||
|
{
|
||||||
|
type: "warning",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.then(async () => {
|
||||||
|
try {
|
||||||
|
// 删除所有选中项
|
||||||
|
const deletePromises = selectedItems.value.map((item) =>
|
||||||
|
deleteCart(item.id, userStore.userInfo!.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(deletePromises);
|
||||||
|
|
||||||
|
ElMessage.success("删除成功");
|
||||||
|
// 从列表中移除
|
||||||
|
cartItems.value = cartItems.value.filter((item) => item.checked !== 1);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
ElMessage.error("删除失败");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// 取消删除
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadCartItems();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.shopping-cart-page {
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cart-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-cart {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-all {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 15px 0;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cart-summary {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-price {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #ff4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-image {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-details {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-name {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-sku {
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cart-items {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -30,10 +30,10 @@ function formatPathFromComponentName(name: string): string {
|
|||||||
* @param filePath Path to the component file
|
* @param filePath Path to the component file
|
||||||
* @returns Component name without extension
|
* @returns Component name without extension
|
||||||
*/
|
*/
|
||||||
function getComponentNameFromPath(filePath: string): string {
|
// function getComponentNameFromPath(filePath: string): string {
|
||||||
// Get the file name from the path
|
// // Get the file name from the path
|
||||||
return filePath.split("/").pop() || "";
|
// return filePath.split("/").pop() || "";
|
||||||
}
|
// }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate route path from full file path, preserving directory structure
|
* Generate route path from full file path, preserving directory structure
|
||||||
@@ -71,15 +71,17 @@ function generateRoutesFromPages(): RouteRecordRaw[] {
|
|||||||
const pages = import.meta.glob("@/pages/**/*.vue");
|
const pages = import.meta.glob("@/pages/**/*.vue");
|
||||||
|
|
||||||
for (const path in pages) {
|
for (const path in pages) {
|
||||||
const componentName = getComponentNameFromPath(path);
|
// const componentName = getComponentNameFromPath(path);
|
||||||
const routePath = generateRoutePathFromFilePath(path);
|
const routePath = generateRoutePathFromFilePath(path);
|
||||||
|
|
||||||
// Special case for home page
|
// Special case for home page
|
||||||
const finalPath = routePath.toLowerCase() === "/home" ? "/" : routePath;
|
const finalPath = routePath.toLowerCase().endsWith("/home")
|
||||||
|
? routePath.slice(0, -5)
|
||||||
|
: routePath;
|
||||||
|
|
||||||
routes.push({
|
routes.push({
|
||||||
path: finalPath,
|
path: finalPath,
|
||||||
name: componentName,
|
name: routePath,
|
||||||
component: pages[path],
|
component: pages[path],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -97,6 +99,11 @@ const extraRoutes: RouteRecordRaw[] = [
|
|||||||
name: "UserProfile",
|
name: "UserProfile",
|
||||||
component: () => import("@/pages/user/ProfilePage.vue"),
|
component: () => import("@/pages/user/ProfilePage.vue"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/product/detail/:id",
|
||||||
|
name: "ProductDetail",
|
||||||
|
component: () => import("@/pages/product/DetailPage.vue"),
|
||||||
|
},
|
||||||
// 可以在这里添加其他需要特殊配置的路由
|
// 可以在这里添加其他需要特殊配置的路由
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
64
src/types/order.ts
Normal file
64
src/types/order.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
// 订单列表项
|
||||||
|
export interface OrderListItem {
|
||||||
|
id: number;
|
||||||
|
orderNo: string;
|
||||||
|
totalAmount: number;
|
||||||
|
payAmount: number;
|
||||||
|
status: number;
|
||||||
|
statusText: string;
|
||||||
|
createTime: string;
|
||||||
|
productCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 订单详情
|
||||||
|
export interface OrderDetail {
|
||||||
|
id: number;
|
||||||
|
orderNo: string;
|
||||||
|
userId: number;
|
||||||
|
totalAmount: number;
|
||||||
|
payAmount: number;
|
||||||
|
freightAmount: number;
|
||||||
|
payType: number;
|
||||||
|
status: number;
|
||||||
|
receiverName: string;
|
||||||
|
receiverPhone: string;
|
||||||
|
receiverAddress: string;
|
||||||
|
payTime: string;
|
||||||
|
deliveryTime: string;
|
||||||
|
receiveTime: string;
|
||||||
|
createTime: string;
|
||||||
|
orderItems: OrderItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 订单项
|
||||||
|
export interface OrderItem {
|
||||||
|
id: number;
|
||||||
|
orderId: number;
|
||||||
|
orderNo: string;
|
||||||
|
productId: number;
|
||||||
|
skuId: number;
|
||||||
|
productName: string;
|
||||||
|
skuName: string;
|
||||||
|
productImage: string;
|
||||||
|
price: number;
|
||||||
|
quantity: number;
|
||||||
|
totalPrice: number;
|
||||||
|
createTime: string;
|
||||||
|
updateTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 订单分页请求参数
|
||||||
|
export interface OrderPageParams {
|
||||||
|
page: number;
|
||||||
|
size: number;
|
||||||
|
userId: number;
|
||||||
|
status?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 订单分页响应数据
|
||||||
|
export interface OrderPageResponse {
|
||||||
|
list: OrderListItem[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
70
src/types/product.ts
Normal file
70
src/types/product.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
// 商品列表项
|
||||||
|
export interface ProductListItem {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
mainImage: string;
|
||||||
|
price: number;
|
||||||
|
sales: number;
|
||||||
|
status: number;
|
||||||
|
categoryName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 商品详情
|
||||||
|
export interface ProductDetail {
|
||||||
|
id: number;
|
||||||
|
categoryId: number;
|
||||||
|
categoryName: string;
|
||||||
|
name: string;
|
||||||
|
subtitle: string;
|
||||||
|
mainImage: string;
|
||||||
|
subImages: string;
|
||||||
|
description: string;
|
||||||
|
price: number;
|
||||||
|
stock: number;
|
||||||
|
sales: number;
|
||||||
|
status: number;
|
||||||
|
sort: number;
|
||||||
|
createTime: string;
|
||||||
|
updateTime: string;
|
||||||
|
skuList: ProductSku[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 商品SKU
|
||||||
|
export interface ProductSku {
|
||||||
|
id: number;
|
||||||
|
productId: number;
|
||||||
|
skuName: string;
|
||||||
|
skuCode: string;
|
||||||
|
price: number;
|
||||||
|
stock: number;
|
||||||
|
specJson: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 商品分类
|
||||||
|
export interface ProductCategory {
|
||||||
|
id: number;
|
||||||
|
parentId: number;
|
||||||
|
name: string;
|
||||||
|
sort: number;
|
||||||
|
icon: string;
|
||||||
|
level: number;
|
||||||
|
createTime: string;
|
||||||
|
updateTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 商品分页请求参数
|
||||||
|
export interface ProductPageParams {
|
||||||
|
page: number;
|
||||||
|
size: number;
|
||||||
|
categoryId?: number;
|
||||||
|
name?: string;
|
||||||
|
status?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 商品分页响应数据
|
||||||
|
export interface ProductPageResponse {
|
||||||
|
list: ProductListItem[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
24
src/types/shoppingcart.ts
Normal file
24
src/types/shoppingcart.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// 购物车项目
|
||||||
|
export interface ShoppingCartItem {
|
||||||
|
id: number;
|
||||||
|
userId: number;
|
||||||
|
productId: number;
|
||||||
|
skuId: number;
|
||||||
|
count: number;
|
||||||
|
checked: number; // 0-未勾选 1-已勾选
|
||||||
|
productName: string;
|
||||||
|
productImage: string;
|
||||||
|
skuName: string;
|
||||||
|
price: number;
|
||||||
|
specJson: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 购物车DTO
|
||||||
|
export interface ShoppingCartDTO {
|
||||||
|
id?: number;
|
||||||
|
userId: number;
|
||||||
|
productId: number;
|
||||||
|
skuId: number;
|
||||||
|
count?: number;
|
||||||
|
checked?: number;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user