更改了购物车逻辑,增加了地址管理
This commit is contained in:
@@ -247,9 +247,21 @@ const routes = [
|
||||
{
|
||||
path: '/pay',
|
||||
name: 'Pay',
|
||||
component: () => import('../views/Pay.vue'),
|
||||
component: () => import('@/views/Pay.vue'),
|
||||
meta: { title: '确认支付' }
|
||||
},
|
||||
{
|
||||
path: '/cart',
|
||||
name: 'Cart',
|
||||
component: () => import('@/views/Cart.vue'),
|
||||
meta: { title: '购物车' }
|
||||
},
|
||||
{
|
||||
path: '/address',
|
||||
name: 'Address',
|
||||
component: () => import('@/views/Address.vue'),
|
||||
meta: { title: '地址管理', requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/payfailed',
|
||||
name: 'PayFailed',
|
||||
|
||||
594
src/views/Address.vue
Normal file
594
src/views/Address.vue
Normal file
@@ -0,0 +1,594 @@
|
||||
<template>
|
||||
<div class="address-page">
|
||||
<!-- 导航栏 -->
|
||||
<nav class="navbar">
|
||||
<div class="nav-left">
|
||||
<el-button
|
||||
type="text"
|
||||
@click="$router.go(-1)"
|
||||
class="back-btn"
|
||||
>
|
||||
<el-icon><ArrowLeft /></el-icon>
|
||||
返回
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="nav-center">
|
||||
<h1 class="nav-title">收货地址</h1>
|
||||
</div>
|
||||
<div class="nav-right">
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="showAddDialog = true"
|
||||
class="add-btn"
|
||||
>
|
||||
新增地址
|
||||
</el-button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div v-loading="loading" class="address-content">
|
||||
<!-- 地址列表为空 -->
|
||||
<div v-if="addresses.length === 0 && !loading" class="empty-address">
|
||||
<el-icon class="empty-icon"><Location /></el-icon>
|
||||
<p>暂无收货地址</p>
|
||||
<p class="empty-tip">点击右上角添加收货地址</p>
|
||||
</div>
|
||||
|
||||
<!-- 地址列表 -->
|
||||
<div v-else class="address-list">
|
||||
<div
|
||||
v-for="address in addresses"
|
||||
:key="address.id"
|
||||
class="address-item"
|
||||
:class="{ 'default-address': address.isDefault }"
|
||||
>
|
||||
<div class="address-info">
|
||||
<div class="address-header">
|
||||
<span class="recipient-name">{{ address.recipientName }}</span>
|
||||
<span class="recipient-phone">{{ address.recipientPhone }}</span>
|
||||
<el-tag v-if="address.isDefault" type="danger" size="small" class="default-tag">
|
||||
默认
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="address-detail">
|
||||
<el-icon><Location /></el-icon>
|
||||
<span>{{ address.province }} {{ address.city }} {{ address.district }} {{ address.detailAddress }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="address-actions">
|
||||
<el-button
|
||||
type="text"
|
||||
@click="editAddress(address)"
|
||||
class="edit-btn"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
type="text"
|
||||
@click="deleteAddress(address.id)"
|
||||
class="delete-btn"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="!address.isDefault"
|
||||
type="text"
|
||||
@click="setDefaultAddress(address.id)"
|
||||
class="default-btn"
|
||||
>
|
||||
设为默认
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新增/编辑地址对话框 -->
|
||||
<el-dialog
|
||||
v-model="showAddDialog"
|
||||
:title="editingAddress ? '编辑地址' : '新增地址'"
|
||||
width="90%"
|
||||
class="address-dialog"
|
||||
>
|
||||
<el-form
|
||||
ref="addressFormRef"
|
||||
:model="addressForm"
|
||||
:rules="addressRules"
|
||||
label-width="80px"
|
||||
class="address-form"
|
||||
>
|
||||
<el-form-item label="收货人" prop="recipientName">
|
||||
<el-input
|
||||
v-model="addressForm.recipientName"
|
||||
placeholder="请输入收货人姓名"
|
||||
maxlength="20"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="手机号" prop="recipientPhone">
|
||||
<el-input
|
||||
v-model="addressForm.recipientPhone"
|
||||
placeholder="请输入手机号"
|
||||
maxlength="11"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="省市区" prop="region">
|
||||
<el-cascader
|
||||
v-model="addressForm.region"
|
||||
:options="regionOptions"
|
||||
placeholder="请选择省市区"
|
||||
style="width: 100%"
|
||||
@change="handleRegionChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="详细地址" prop="detailAddress">
|
||||
<el-input
|
||||
v-model="addressForm.detailAddress"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入详细地址"
|
||||
maxlength="100"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-checkbox v-model="addressForm.isDefault">
|
||||
设为默认地址
|
||||
</el-checkbox>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="cancelEdit">取消</el-button>
|
||||
<el-button type="primary" @click="saveAddress" :loading="saving">
|
||||
{{ editingAddress ? '保存' : '添加' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
ArrowLeft,
|
||||
Location
|
||||
} from '@element-plus/icons-vue'
|
||||
import api from '@/utils/api'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const addresses = ref([])
|
||||
const showAddDialog = ref(false)
|
||||
const editingAddress = ref(null)
|
||||
const addressFormRef = ref(null)
|
||||
|
||||
// 地址表单数据
|
||||
const addressForm = reactive({
|
||||
recipientName: '',
|
||||
recipientPhone: '',
|
||||
region: [],
|
||||
province: '',
|
||||
city: '',
|
||||
district: '',
|
||||
detailAddress: '',
|
||||
isDefault: false
|
||||
})
|
||||
|
||||
// 省市区选项数据(简化版,实际项目中应该从API获取)
|
||||
const regionOptions = ref([
|
||||
{
|
||||
value: '北京市',
|
||||
label: '北京市',
|
||||
children: [
|
||||
{
|
||||
value: '北京市',
|
||||
label: '北京市',
|
||||
children: [
|
||||
{ value: '东城区', label: '东城区' },
|
||||
{ value: '西城区', label: '西城区' },
|
||||
{ value: '朝阳区', label: '朝阳区' },
|
||||
{ value: '丰台区', label: '丰台区' },
|
||||
{ value: '石景山区', label: '石景山区' },
|
||||
{ value: '海淀区', label: '海淀区' }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
value: '上海市',
|
||||
label: '上海市',
|
||||
children: [
|
||||
{
|
||||
value: '上海市',
|
||||
label: '上海市',
|
||||
children: [
|
||||
{ value: '黄浦区', label: '黄浦区' },
|
||||
{ value: '徐汇区', label: '徐汇区' },
|
||||
{ value: '长宁区', label: '长宁区' },
|
||||
{ value: '静安区', label: '静安区' },
|
||||
{ value: '普陀区', label: '普陀区' },
|
||||
{ value: '虹口区', label: '虹口区' }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
value: '广东省',
|
||||
label: '广东省',
|
||||
children: [
|
||||
{
|
||||
value: '广州市',
|
||||
label: '广州市',
|
||||
children: [
|
||||
{ value: '天河区', label: '天河区' },
|
||||
{ value: '越秀区', label: '越秀区' },
|
||||
{ value: '荔湾区', label: '荔湾区' },
|
||||
{ value: '海珠区', label: '海珠区' }
|
||||
]
|
||||
},
|
||||
{
|
||||
value: '深圳市',
|
||||
label: '深圳市',
|
||||
children: [
|
||||
{ value: '福田区', label: '福田区' },
|
||||
{ value: '罗湖区', label: '罗湖区' },
|
||||
{ value: '南山区', label: '南山区' },
|
||||
{ value: '宝安区', label: '宝安区' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
// 表单验证规则
|
||||
const addressRules = {
|
||||
recipientName: [
|
||||
{ required: true, message: '请输入收货人姓名', trigger: 'blur' },
|
||||
{ min: 2, max: 20, message: '姓名长度在 2 到 20 个字符', trigger: 'blur' }
|
||||
],
|
||||
recipientPhone: [
|
||||
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
|
||||
],
|
||||
region: [
|
||||
{ required: true, message: '请选择省市区', trigger: 'change' }
|
||||
],
|
||||
detailAddress: [
|
||||
{ required: true, message: '请输入详细地址', trigger: 'blur' },
|
||||
{ min: 5, max: 100, message: '详细地址长度在 5 到 100 个字符', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 方法
|
||||
const loadAddresses = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await api.get('/addresses')
|
||||
if (response.data.success) {
|
||||
addresses.value = response.data.data.addresses || []
|
||||
} else {
|
||||
throw new Error(response.data.message || '获取地址列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(error.message || '获取地址列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRegionChange = (value) => {
|
||||
if (value && value.length === 3) {
|
||||
addressForm.province = value[0]
|
||||
addressForm.city = value[1]
|
||||
addressForm.district = value[2]
|
||||
}
|
||||
}
|
||||
|
||||
const editAddress = (address) => {
|
||||
editingAddress.value = address
|
||||
addressForm.recipientName = address.recipientName
|
||||
addressForm.recipientPhone = address.recipientPhone
|
||||
addressForm.region = [address.province, address.city, address.district]
|
||||
addressForm.province = address.province
|
||||
addressForm.city = address.city
|
||||
addressForm.district = address.district
|
||||
addressForm.detailAddress = address.detailAddress
|
||||
addressForm.isDefault = address.isDefault
|
||||
showAddDialog.value = true
|
||||
}
|
||||
|
||||
const saveAddress = async () => {
|
||||
if (!addressFormRef.value) return
|
||||
|
||||
try {
|
||||
await addressFormRef.value.validate()
|
||||
saving.value = true
|
||||
|
||||
const addressData = {
|
||||
recipientName: addressForm.recipientName,
|
||||
recipientPhone: addressForm.recipientPhone,
|
||||
province: addressForm.province,
|
||||
city: addressForm.city,
|
||||
district: addressForm.district,
|
||||
detailAddress: addressForm.detailAddress,
|
||||
isDefault: addressForm.isDefault
|
||||
}
|
||||
|
||||
let response
|
||||
if (editingAddress.value) {
|
||||
// 编辑地址
|
||||
response = await api.put(`/addresses/${editingAddress.value.id}`, addressData)
|
||||
} else {
|
||||
// 新增地址
|
||||
response = await api.post('/addresses', addressData)
|
||||
}
|
||||
|
||||
if (response.data.success) {
|
||||
ElMessage.success(editingAddress.value ? '地址更新成功' : '地址添加成功')
|
||||
showAddDialog.value = false
|
||||
resetForm()
|
||||
await loadAddresses()
|
||||
} else {
|
||||
throw new Error(response.data.message || '保存地址失败')
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.message) {
|
||||
ElMessage.error(error.message)
|
||||
}
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const deleteAddress = async (addressId) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'确定要删除这个地址吗?',
|
||||
'确认删除',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
const response = await api.delete(`/addresses/${addressId}`)
|
||||
if (response.data.success) {
|
||||
ElMessage.success('地址删除成功')
|
||||
await loadAddresses()
|
||||
} else {
|
||||
throw new Error(response.data.message || '删除地址失败')
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error(error.message || '删除地址失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const setDefaultAddress = async (addressId) => {
|
||||
try {
|
||||
const response = await api.put(`/addresses/${addressId}/default`)
|
||||
if (response.data.success) {
|
||||
ElMessage.success('默认地址设置成功')
|
||||
await loadAddresses()
|
||||
} else {
|
||||
throw new Error(response.data.message || '设置默认地址失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(error.message || '设置默认地址失败')
|
||||
}
|
||||
}
|
||||
|
||||
const cancelEdit = () => {
|
||||
showAddDialog.value = false
|
||||
resetForm()
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
editingAddress.value = null
|
||||
addressForm.recipientName = ''
|
||||
addressForm.recipientPhone = ''
|
||||
addressForm.region = []
|
||||
addressForm.province = ''
|
||||
addressForm.city = ''
|
||||
addressForm.district = ''
|
||||
addressForm.detailAddress = ''
|
||||
addressForm.isDefault = false
|
||||
|
||||
if (addressFormRef.value) {
|
||||
addressFormRef.value.resetFields()
|
||||
}
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadAddresses()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.address-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
height: 60px;
|
||||
background: white;
|
||||
border-bottom: 1px solid #eee;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.nav-left, .nav-right {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.nav-right {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.nav-center {
|
||||
flex: 2;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
color: #666;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.address-content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.empty-address {
|
||||
text-align: center;
|
||||
padding: 80px 20px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 64px;
|
||||
color: #ddd;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
font-size: 14px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.address-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.address-item {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.address-item:hover {
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.default-address {
|
||||
border: 2px solid #409eff;
|
||||
}
|
||||
|
||||
.address-info {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.address-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.recipient-name {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.recipient-phone {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.default-tag {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.address-detail {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.address-actions {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.edit-btn {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.default-btn {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
.address-dialog {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.address-form {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.address-item {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.address-actions {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.address-dialog {
|
||||
width: 95% !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -24,19 +24,42 @@
|
||||
<div class="address-header">
|
||||
<el-icon><Location /></el-icon>
|
||||
<span class="address-label">收货地址</span>
|
||||
<el-icon class="edit-icon"><Edit /></el-icon>
|
||||
<el-button
|
||||
type="text"
|
||||
@click="goToAddressManage"
|
||||
class="manage-address-btn"
|
||||
>
|
||||
管理地址
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="address-content">
|
||||
<div v-if="!showAddressEdit" class="address-text" @click="showAddressEdit = true">{{ shippingAddress }}</div>
|
||||
<el-input
|
||||
v-else
|
||||
v-model="shippingAddress"
|
||||
@blur="showAddressEdit = false"
|
||||
@keyup.enter="showAddressEdit = false"
|
||||
placeholder="请输入收货地址"
|
||||
class="address-input"
|
||||
autofocus
|
||||
/>
|
||||
<el-select
|
||||
v-model="selectedAddressId"
|
||||
placeholder="请选择收货地址"
|
||||
class="address-select"
|
||||
@change="handleAddressChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="address in addresses"
|
||||
:key="address.id"
|
||||
:label="`${address.recipientName} ${address.recipientPhone} ${address.province}${address.city}${address.district}${address.detailAddress}`"
|
||||
:value="address.id"
|
||||
>
|
||||
<div class="address-option">
|
||||
<div class="address-info">
|
||||
<span class="recipient-info">{{ address.recipientName }} {{ address.recipientPhone }}</span>
|
||||
<el-tag v-if="address.isDefault" type="danger" size="small" class="default-tag">默认</el-tag>
|
||||
</div>
|
||||
<div class="address-detail">{{ address.province }}{{ address.city }}{{ address.district }}{{ address.detailAddress }}</div>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
<div v-if="addresses.length === 0" class="no-address">
|
||||
<span class="no-address-text">暂无收货地址</span>
|
||||
<el-button type="text" @click="goToAddressManage" class="add-address-btn">
|
||||
立即添加
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -115,7 +138,6 @@
|
||||
class="note-input"
|
||||
autofocus
|
||||
/>
|
||||
<el-icon v-if="!showNoteEdit" class="arrow-icon"><ArrowRight /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -127,7 +149,7 @@
|
||||
<el-button
|
||||
size="large"
|
||||
class="cart-button"
|
||||
@click="addToCart"
|
||||
@click="handleAddToCart"
|
||||
:disabled="!canPurchase"
|
||||
>
|
||||
加入购物车
|
||||
@@ -170,9 +192,10 @@ const categories = ref([])
|
||||
const sizes = ref([])
|
||||
const selectedCategory = ref(null)
|
||||
const selectedSize = ref(null)
|
||||
const shippingAddress = ref('请输入收货地址')
|
||||
const addresses = ref([])
|
||||
const selectedAddressId = ref('')
|
||||
const selectedAddress = ref(null)
|
||||
const orderNote = ref('')
|
||||
const showAddressEdit = ref(false)
|
||||
const showNoteEdit = ref(false)
|
||||
|
||||
// 计算属性
|
||||
@@ -272,15 +295,21 @@ const addToCart = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 立即购买功能
|
||||
const handlePurchase = async () => {
|
||||
if (!canPurchase.value) {
|
||||
ElMessage.error('请选择完整的商品信息')
|
||||
return
|
||||
}
|
||||
|
||||
if (!selectedAddress.value) {
|
||||
ElMessage.error('请选择收货地址')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 先将商品添加到购物车
|
||||
const cartItem = {
|
||||
// 创建单独的购买订单
|
||||
const orderData = {
|
||||
productId: product.value.id,
|
||||
quantity: quantity.value,
|
||||
categoryId: selectedCategory.value.id,
|
||||
@@ -289,11 +318,11 @@ const handlePurchase = async () => {
|
||||
name: product.value.name,
|
||||
image: product.value.image,
|
||||
stock: product.value.stock,
|
||||
shippingAddress: shippingAddress.value,
|
||||
addressId: selectedAddress.value.id,
|
||||
orderNote: orderNote.value
|
||||
}
|
||||
|
||||
const response = await api.post('/cart/add', cartItem)
|
||||
const response = await api.post('/cart/buy-now', orderData)
|
||||
|
||||
if (response.data.success) {
|
||||
const cartId = response.data.data.cartId
|
||||
@@ -306,13 +335,73 @@ const handlePurchase = async () => {
|
||||
}
|
||||
})
|
||||
} else {
|
||||
throw new Error(response.data.message || '添加到购物车失败')
|
||||
throw new Error(response.data.message || '创建订单失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(error.message || '操作失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 添加到购物车功能(新增)
|
||||
const handleAddToCart = async () => {
|
||||
if (!canPurchase.value) {
|
||||
ElMessage.error('请选择完整的商品信息')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const cartItem = {
|
||||
productId: product.value.id,
|
||||
quantity: quantity.value,
|
||||
categoryId: selectedCategory.value.id,
|
||||
sizeId: selectedSize.value.id,
|
||||
points: product.value.points,
|
||||
name: product.value.name,
|
||||
image: product.value.image,
|
||||
stock: product.value.stock
|
||||
}
|
||||
|
||||
const response = await api.post('/cart/add', cartItem)
|
||||
|
||||
if (response.data.success) {
|
||||
ElMessage.success('商品已加入购物车!')
|
||||
// 可以选择返回上一页或跳转到购物车页面
|
||||
router.go(-1)
|
||||
} else {
|
||||
throw new Error(response.data.message || '添加到购物车失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(error.message || '添加到购物车失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户地址列表
|
||||
const getAddressList = async () => {
|
||||
try {
|
||||
const response = await api.get('/address/list')
|
||||
addresses.value = response.data.data.addresses || []
|
||||
// 如果有默认地址,自动选中
|
||||
const defaultAddress = addresses.value.find(addr => addr.isDefault)
|
||||
if (defaultAddress) {
|
||||
selectedAddressId.value = defaultAddress.id
|
||||
selectedAddress.value = defaultAddress
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取地址列表失败:', error)
|
||||
ElMessage.error('获取地址列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 处理地址选择变化
|
||||
const handleAddressChange = (addressId) => {
|
||||
selectedAddress.value = addresses.value.find(addr => addr.id === addressId)
|
||||
}
|
||||
|
||||
// 跳转到地址管理页面
|
||||
const goToAddressManage = () => {
|
||||
router.push('/address')
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
// 从URL参数获取初始数量
|
||||
@@ -324,6 +413,7 @@ onMounted(() => {
|
||||
getProductInfo()
|
||||
getCategories()
|
||||
getSizes()
|
||||
getAddressList()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -382,10 +472,7 @@ onMounted(() => {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.address-text {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
|
||||
.product-section {
|
||||
background: white;
|
||||
@@ -557,12 +644,67 @@ onMounted(() => {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.arrow-icon {
|
||||
color: #ccc;
|
||||
.address-select {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.address-input {
|
||||
margin-top: 8px;
|
||||
.manage-address-btn {
|
||||
color: #409eff;
|
||||
font-size: 14px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.manage-address-btn:hover {
|
||||
color: #66b1ff;
|
||||
}
|
||||
|
||||
.address-option {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.address-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.recipient-info {
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.default-tag {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.address-detail {
|
||||
color: #606266;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.no-address {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.no-address-text {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.add-address-btn {
|
||||
color: #409eff;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.add-address-btn:hover {
|
||||
color: #66b1ff;
|
||||
}
|
||||
|
||||
.note-input {
|
||||
@@ -575,10 +717,7 @@ onMounted(() => {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.address-text {
|
||||
cursor: pointer;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
|
||||
.note-content {
|
||||
cursor: pointer;
|
||||
|
||||
559
src/views/Cart.vue
Normal file
559
src/views/Cart.vue
Normal file
@@ -0,0 +1,559 @@
|
||||
<template>
|
||||
<div class="cart-page">
|
||||
<!-- 导航栏 -->
|
||||
<nav class="navbar">
|
||||
<div class="nav-left">
|
||||
<el-button
|
||||
type="text"
|
||||
@click="$router.go(-1)"
|
||||
class="back-btn"
|
||||
>
|
||||
<el-icon><ArrowLeft /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="nav-center">
|
||||
<h1 class="nav-title">购物车</h1>
|
||||
</div>
|
||||
<div class="nav-right">
|
||||
<el-button
|
||||
type="text"
|
||||
@click="clearCart"
|
||||
class="clear-btn"
|
||||
v-if="cartItems.length > 0"
|
||||
>
|
||||
清空
|
||||
</el-button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div v-loading="loading" class="page-content">
|
||||
<!-- 空购物车状态 -->
|
||||
<div v-if="cartItems.length === 0" class="empty-cart">
|
||||
<div class="empty-icon">
|
||||
<el-icon size="80"><ShoppingCart /></el-icon>
|
||||
</div>
|
||||
<div class="empty-text">购物车是空的</div>
|
||||
<div class="empty-desc">快去挑选心仪的商品吧</div>
|
||||
<el-button type="primary" @click="$router.push('/shop')" class="go-shop-btn">
|
||||
去购物
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 购物车商品列表 -->
|
||||
<div v-else class="cart-content">
|
||||
<div class="cart-header">
|
||||
<el-checkbox
|
||||
v-model="selectAll"
|
||||
@change="handleSelectAll"
|
||||
class="select-all-checkbox"
|
||||
>
|
||||
全选
|
||||
</el-checkbox>
|
||||
<span class="item-count">共{{ cartItems.length }}件商品</span>
|
||||
</div>
|
||||
|
||||
<div class="cart-items">
|
||||
<div
|
||||
v-for="item in cartItems"
|
||||
:key="item.id"
|
||||
class="cart-item"
|
||||
>
|
||||
<div class="item-checkbox">
|
||||
<el-checkbox
|
||||
v-model="item.selected"
|
||||
@change="updateSelection"
|
||||
/>
|
||||
</div>
|
||||
<div class="item-image">
|
||||
<img :src="item.image || '/imgs/productdetail/商品主图.png'" :alt="item.name" />
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<div class="item-name">{{ item.name }}</div>
|
||||
<div class="item-details">
|
||||
<span v-if="item.category" class="item-category">{{ item.category }}</span>
|
||||
<span v-if="item.size" class="item-size">{{ item.size }}</span>
|
||||
</div>
|
||||
<div class="item-price">
|
||||
<el-icon class="coin-icon"><Coin /></el-icon>
|
||||
<span class="price-value">{{ item.points }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-actions">
|
||||
<div class="quantity-controls">
|
||||
<el-button
|
||||
size="small"
|
||||
@click="decreaseQuantity(item)"
|
||||
:disabled="item.quantity <= 1"
|
||||
class="quantity-btn"
|
||||
>
|
||||
-
|
||||
</el-button>
|
||||
<span class="quantity">{{ item.quantity }}</span>
|
||||
<el-button
|
||||
size="small"
|
||||
@click="increaseQuantity(item)"
|
||||
:disabled="item.quantity >= item.stock"
|
||||
class="quantity-btn"
|
||||
>
|
||||
+
|
||||
</el-button>
|
||||
</div>
|
||||
<el-button
|
||||
type="text"
|
||||
@click="removeItem(item)"
|
||||
class="remove-btn"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部结算栏 -->
|
||||
<div v-if="cartItems.length > 0" class="bottom-bar">
|
||||
<div class="total-info">
|
||||
<div class="selected-count">已选{{ selectedCount }}件</div>
|
||||
<div class="total-price">
|
||||
<span class="total-label">合计:</span>
|
||||
<el-icon class="coin-icon"><Coin /></el-icon>
|
||||
<span class="total-value">{{ totalPrice }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="large"
|
||||
@click="checkout"
|
||||
:disabled="selectedCount === 0"
|
||||
class="checkout-btn"
|
||||
>
|
||||
结算({{ selectedCount }})
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
ArrowLeft,
|
||||
ShoppingCart,
|
||||
Coin
|
||||
} from '@element-plus/icons-vue'
|
||||
import api from '@/utils/api'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const cartItems = ref([])
|
||||
const selectAll = ref(false)
|
||||
|
||||
// 计算属性
|
||||
const selectedItems = computed(() => {
|
||||
return cartItems.value.filter(item => item.selected)
|
||||
})
|
||||
|
||||
const selectedCount = computed(() => {
|
||||
return selectedItems.value.length
|
||||
})
|
||||
|
||||
const totalPrice = computed(() => {
|
||||
return selectedItems.value.reduce((total, item) => {
|
||||
return total + (item.points * item.quantity)
|
||||
}, 0)
|
||||
})
|
||||
|
||||
// 方法
|
||||
const loadCartData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await api.get('/cart')
|
||||
if (response.data.success) {
|
||||
cartItems.value = response.data.data.map(item => ({
|
||||
...item,
|
||||
selected: false
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('加载购物车失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSelectAll = () => {
|
||||
cartItems.value.forEach(item => {
|
||||
item.selected = selectAll.value
|
||||
})
|
||||
}
|
||||
|
||||
const updateSelection = () => {
|
||||
const selectedCount = cartItems.value.filter(item => item.selected).length
|
||||
selectAll.value = selectedCount === cartItems.value.length && cartItems.value.length > 0
|
||||
}
|
||||
|
||||
const increaseQuantity = async (item) => {
|
||||
if (item.quantity >= item.stock) {
|
||||
ElMessage.warning('库存不足')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await api.put(`/cart/${item.id}`, {
|
||||
quantity: item.quantity + 1
|
||||
})
|
||||
item.quantity++
|
||||
} catch (error) {
|
||||
ElMessage.error('更新数量失败')
|
||||
}
|
||||
}
|
||||
|
||||
const decreaseQuantity = async (item) => {
|
||||
if (item.quantity <= 1) return
|
||||
|
||||
try {
|
||||
await api.put(`/cart/${item.id}`, {
|
||||
quantity: item.quantity - 1
|
||||
})
|
||||
item.quantity--
|
||||
} catch (error) {
|
||||
ElMessage.error('更新数量失败')
|
||||
}
|
||||
}
|
||||
|
||||
const removeItem = async (item) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'确定要删除这件商品吗?',
|
||||
'确认删除',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
await api.delete(`/cart/${item.id}`)
|
||||
const index = cartItems.value.findIndex(i => i.id === item.id)
|
||||
if (index > -1) {
|
||||
cartItems.value.splice(index, 1)
|
||||
}
|
||||
ElMessage.success('删除成功')
|
||||
updateSelection()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const clearCart = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'确定要清空购物车吗?',
|
||||
'确认清空',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
await api.delete('/cart/clear')
|
||||
cartItems.value = []
|
||||
ElMessage.success('购物车已清空')
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('清空失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const checkout = async () => {
|
||||
if (selectedCount.value === 0) {
|
||||
ElMessage.error('请选择要结算的商品')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const cartData = {
|
||||
items: selectedItems.value.map(item => ({
|
||||
productId: item.productId || item.id,
|
||||
quantity: item.quantity,
|
||||
points: item.points,
|
||||
name: item.name,
|
||||
image: item.image,
|
||||
categoryId: item.categoryId,
|
||||
sizeId: item.sizeId
|
||||
}))
|
||||
}
|
||||
|
||||
const response = await api.post('/cart/checkout', cartData)
|
||||
|
||||
if (response.data.success) {
|
||||
const cartId = response.data.data.cartId
|
||||
|
||||
// 跳转到支付页面
|
||||
router.push({
|
||||
path: '/pay',
|
||||
query: {
|
||||
cartId: cartId
|
||||
}
|
||||
})
|
||||
} else {
|
||||
throw new Error(response.data.message || '创建订单失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(error.message || '结算失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadCartData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cart-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
background: white;
|
||||
padding: 10px 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.back-btn, .clear-btn {
|
||||
color: #666;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
color: #ff4757;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.empty-cart {
|
||||
text-align: center;
|
||||
padding: 80px 20px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
color: #ddd;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 18px;
|
||||
color: #666;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.empty-desc {
|
||||
color: #999;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.go-shop-btn {
|
||||
padding: 12px 30px;
|
||||
}
|
||||
|
||||
.cart-content {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cart-header {
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.select-all-checkbox {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.item-count {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.cart-items {
|
||||
max-height: calc(100vh - 300px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.cart-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.cart-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.item-checkbox {
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.item-image {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin-right: 12px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.item-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.item-info {
|
||||
flex: 1;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.item-details {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.item-category, .item-size {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
background: #f5f5f5;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.item-price {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #ff6b35;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.coin-icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.price-value {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.item-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.quantity-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.quantity-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.quantity {
|
||||
min-width: 30px;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.remove-btn {
|
||||
color: #ff4757;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.bottom-bar {
|
||||
background: white;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-shadow: 0 -2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.total-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.selected-count {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.total-price {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.total-label {
|
||||
color: #333;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.total-value {
|
||||
color: #ff6b35;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.checkout-btn {
|
||||
padding: 12px 24px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -146,8 +146,8 @@ export default {
|
||||
{text:'隐私协议'},
|
||||
]);
|
||||
const functionItems = ref([
|
||||
{ image: "/imgs/mainpage/交易记录.png", text: "购物车", path: "" },
|
||||
{ image: "/imgs/mainpage/订单查询.png", text: "地址", path: "" },
|
||||
{ image: "/imgs/mainpage/交易记录.png", text: "购物车", path: "/cart" },
|
||||
{ image: "/imgs/mainpage/订单查询.png", text: "地址", path: "/address" },
|
||||
{ image: "/imgs/mainpage/客服中心.png", text: "收藏", path: "" }
|
||||
]);
|
||||
|
||||
|
||||
@@ -64,6 +64,36 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 商品列表 -->
|
||||
<div class="items-section" v-if="paymentData.items && paymentData.items.length > 0">
|
||||
<h3 class="section-title">商品清单 ({{ paymentData.items.length }})</h3>
|
||||
<div class="items-list">
|
||||
<div
|
||||
v-for="item in paymentData.items"
|
||||
:key="item.id || item.productId"
|
||||
class="item-card"
|
||||
>
|
||||
<div class="item-image">
|
||||
<img :src="item.image || '/imgs/productdetail/商品主图.png'" :alt="item.name" />
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<div class="item-name">{{ item.name }}</div>
|
||||
<div class="item-details">
|
||||
<span v-if="item.category" class="item-category">{{ item.category }}</span>
|
||||
<span v-if="item.size" class="item-size">{{ item.size }}</span>
|
||||
</div>
|
||||
<div class="item-price">
|
||||
<el-icon><Coin /></el-icon>
|
||||
<span>{{ item.points || item.price }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-quantity">
|
||||
<span class="quantity-label">x{{ item.quantity }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 支付方式选择 -->
|
||||
<div class="payment-method-section">
|
||||
<h3 class="section-title">支付方式</h3>
|
||||
@@ -162,7 +192,8 @@ const paymentData = ref({
|
||||
totalAmount: 0,
|
||||
pointsAmount: 0,
|
||||
beansAmount: 0,
|
||||
cartId: null
|
||||
cartId: null,
|
||||
items: [] // 添加商品列表
|
||||
})
|
||||
|
||||
// 计算属性
|
||||
@@ -201,7 +232,8 @@ const fetchPaymentData = async () => {
|
||||
totalAmount: 0,
|
||||
pointsAmount: 0,
|
||||
beansAmount: 0,
|
||||
cartId: null
|
||||
cartId: null,
|
||||
items: []
|
||||
}
|
||||
timeLeft.value = 900 // 默认15分钟
|
||||
startCountdown()
|
||||
@@ -218,7 +250,8 @@ const fetchPaymentData = async () => {
|
||||
totalAmount: data.totalAmount || 0,
|
||||
pointsAmount: data.pointsAmount || 0,
|
||||
beansAmount: data.beansAmount || 0,
|
||||
cartId: cartId
|
||||
cartId: cartId,
|
||||
items: data.items || [] // 获取商品列表
|
||||
}
|
||||
|
||||
// 设置倒计时时间(从后端获取,单位:秒)
|
||||
@@ -245,8 +278,8 @@ const handleGoBack = async () => {
|
||||
'确认要放弃付款吗?',
|
||||
'提示',
|
||||
{
|
||||
confirmButtonText: '狠心离开',
|
||||
cancelButtonText: '继续付款',
|
||||
confirmButtonText: '确认',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
@@ -447,6 +480,7 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.amount-section,
|
||||
.items-section,
|
||||
.payment-method-section {
|
||||
background: white;
|
||||
padding: 16px;
|
||||
@@ -460,6 +494,85 @@ onUnmounted(() => {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.items-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.item-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 8px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.item-image {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.item-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.item-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.item-details {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.item-category,
|
||||
.item-size {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
background: #f0f0f0;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.item-price {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 14px;
|
||||
color: #ffae00;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.item-quantity {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.quantity-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.amount-display {
|
||||
text-align: center;
|
||||
padding: 20px 0;
|
||||
|
||||
@@ -125,17 +125,31 @@
|
||||
|
||||
<!-- 商品描述 -->
|
||||
<div class="product-description">
|
||||
<h3>商品描述</h3>
|
||||
<h3 class="section-title">
|
||||
商品描述
|
||||
</h3>
|
||||
<div v-if="showDescription" class="section-content">
|
||||
<p>{{ product.description }}</p>
|
||||
</div>
|
||||
<div v-else class="section-placeholder">
|
||||
<span class="placeholder-text" @click="toggleDescription">详情</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 商品详情 -->
|
||||
<div class="product-details">
|
||||
<h3>商品详情</h3>
|
||||
<h3 class="section-title">
|
||||
商品详情
|
||||
</h3>
|
||||
<div v-if="showDetails" class="section-content">
|
||||
<div class="detail-content">
|
||||
<p>{{ product.description || '暂无详细描述' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="section-placeholder">
|
||||
<span class="placeholder-text" @click="toggleDetails">详情</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 购买选项 -->
|
||||
<div class="purchase-options">
|
||||
@@ -261,10 +275,15 @@
|
||||
<div v-else class="cart-items">
|
||||
<div class="cart-header">
|
||||
<span>共 {{ cartTotalItems }} 件商品</span>
|
||||
<div class="cart-actions">
|
||||
<el-button type="text" @click="goToCartPage" class="manage-btn">
|
||||
管理
|
||||
</el-button>
|
||||
<el-button type="text" @click="clearCart" class="clear-btn">
|
||||
清空购物车
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cart-list">
|
||||
<div
|
||||
@@ -378,6 +397,10 @@ const cartLoading = ref(false)
|
||||
const userPoints = ref(0)
|
||||
const cartItems = ref([])
|
||||
const cartCount = ref(0)
|
||||
const showDescription = ref(false)
|
||||
const showDetails = ref(false)
|
||||
const selectedCategory = ref(null)
|
||||
const selectedSize = ref(null)
|
||||
|
||||
// 计算属性
|
||||
const totalPoints = computed(() => {
|
||||
@@ -432,7 +455,7 @@ const getProductDetail = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const addToCart = () => {
|
||||
const addToCart = async () => {
|
||||
if (!product.value) {
|
||||
ElMessage.error('商品信息加载中,请稍后再试')
|
||||
return
|
||||
@@ -443,7 +466,10 @@ const addToCart = () => {
|
||||
return
|
||||
}
|
||||
|
||||
// 跳转到BuyDetails页面进行确认订单
|
||||
try {
|
||||
// 检查是否已选择必要的商品属性
|
||||
if (!selectedCategory.value || !selectedSize.value) {
|
||||
// 如果没有选择属性,跳转到BuyDetails页面进行详细配置
|
||||
router.push({
|
||||
path: '/buydetail',
|
||||
query: {
|
||||
@@ -451,6 +477,40 @@ const addToCart = () => {
|
||||
quantity: quantity.value
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 构建购物车商品数据
|
||||
const cartItem = {
|
||||
productId: product.value.id,
|
||||
quantity: quantity.value,
|
||||
categoryId: selectedCategory.value.id,
|
||||
sizeId: selectedSize.value.id,
|
||||
points: product.value.points,
|
||||
name: product.value.name,
|
||||
image: product.value.images?.[0] || product.value.image,
|
||||
stock: product.value.stock
|
||||
}
|
||||
|
||||
// 添加到购物车
|
||||
const response = await api.post('/cart/add', cartItem)
|
||||
|
||||
if (response.data.success) {
|
||||
ElMessage.success('商品已加入购物车!')
|
||||
|
||||
// 更新本地购物车数据
|
||||
await loadCartFromBackend()
|
||||
|
||||
// 重置选择状态
|
||||
quantity.value = 1
|
||||
selectedCategory.value = null
|
||||
selectedSize.value = null
|
||||
} else {
|
||||
throw new Error(response.data.message || '添加到购物车失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(error.message || '添加到购物车失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 购物车商品管理方法
|
||||
@@ -540,6 +600,12 @@ const loadCartFromBackend = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 跳转到购物车管理页面
|
||||
const goToCartPage = () => {
|
||||
showCart.value = false
|
||||
router.push('/cart')
|
||||
}
|
||||
|
||||
// 购物车结算功能
|
||||
const checkoutCart = async () => {
|
||||
if (cartItems.value.length === 0) {
|
||||
@@ -547,43 +613,40 @@ const checkoutCart = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
if (cartTotalPoints.value > userPoints.value) {
|
||||
ElMessage.error('积分不足,无法结算')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要花费 ${cartTotalPoints.value} 积分购买这些商品吗?`,
|
||||
'确认结算',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
const orderData = {
|
||||
// 创建购物车结算请求
|
||||
const cartData = {
|
||||
items: cartItems.value.map(item => ({
|
||||
productId: item.id,
|
||||
productId: item.id || item.productId,
|
||||
quantity: item.quantity,
|
||||
points: item.points
|
||||
})),
|
||||
totalPoints: cartTotalPoints.value
|
||||
points: item.points,
|
||||
name: item.name,
|
||||
image: item.image,
|
||||
categoryId: item.categoryId,
|
||||
sizeId: item.sizeId
|
||||
}))
|
||||
}
|
||||
|
||||
await api.post('/orders', orderData)
|
||||
const response = await api.post('/cart/checkout', cartData)
|
||||
|
||||
// 清空购物车
|
||||
cartItems.value = []
|
||||
if (response.data.success) {
|
||||
const cartId = response.data.data.cartId
|
||||
|
||||
// 跳转到支付页面
|
||||
router.push({
|
||||
path: '/pay',
|
||||
query: {
|
||||
cartId: cartId
|
||||
}
|
||||
})
|
||||
|
||||
// 关闭购物车弹窗
|
||||
showCart.value = false
|
||||
|
||||
ElMessage.success('结算成功!')
|
||||
router.push('/orders')
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('结算失败,请重试')
|
||||
} else {
|
||||
throw new Error(response.data.message || '创建订单失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(error.message || '结算失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -652,6 +715,14 @@ const getUserPoints = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const toggleDescription = () => {
|
||||
showDescription.value = !showDescription.value
|
||||
}
|
||||
|
||||
const toggleDetails = () => {
|
||||
showDetails.value = !showDetails.value
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
//getProductDetail()
|
||||
@@ -881,19 +952,52 @@ watch(
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.product-description h3,
|
||||
.product-details h3 {
|
||||
.section-title {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.product-description p {
|
||||
.section-content {
|
||||
padding: 12px 0;
|
||||
animation: slideDown 0.3s ease;
|
||||
}
|
||||
|
||||
.section-content p {
|
||||
margin: 0;
|
||||
line-height: 1.6;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.section-placeholder {
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.placeholder-text {
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
font-style: italic;
|
||||
cursor: pointer;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.placeholder-text:hover {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
max-height: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
max-height: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
padding: 8px 0;
|
||||
@@ -1172,6 +1276,21 @@ watch(
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.cart-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.manage-btn {
|
||||
color: #409eff;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.manage-btn:hover {
|
||||
color: #66b1ff;
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
color: #ff4757;
|
||||
font-size: 12px;
|
||||
|
||||
Reference in New Issue
Block a user