Files
jurong_circle_frontdesk/src/views/BuyDetails.vue
2025-09-03 11:00:08 +08:00

768 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="buy-details-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">
</div>
</nav>
<div v-loading="loading" class="page-content">
<!-- 商品信息 -->
<div class="product-section">
<div class="product-info">
<div class="product-image">
<img :src="product?.image || '/imgs/productdetail/商品主图.png'" alt="商品主图" />
</div>
<div class="product-details">
<div class="product-price">
<span class="price-label">实付</span>
<div class="price-container">
<div class="main-price">
<img src='/imgs/profile/rongdou.png' alt="融豆" class="rongdou-icon" />
<span class="rongdou-price">{{ totalPrice }}</span>
</div>
<div class="sub-price">
<el-icon class="points-icon"><Coin /></el-icon>
<span class="points-price">{{ totalPointsPrice }}</span>
</div>
</div>
</div>
<div class="quantity-selector">
<el-button size="small" @click="decreaseQuantity" :disabled="quantity <= 1">-</el-button>
<span class="quantity">{{ quantity }}</span>
<el-button size="small" @click="increaseQuantity">+</el-button>
</div>
</div>
</div>
</div>
<!-- 动态规格选择 -->
<div
v-for="(specOptions, specName) in specGroups"
:key="specName"
class="spec-section"
>
<h3 class="section-title">{{ specName }} ({{ specOptions.length }})</h3>
<div class="spec-grid">
<div
v-for="option in specOptions"
:key="option.id"
class="spec-item"
:class="{
active: selectedSpecs[specName]?.id === option.id,
disabled: availableSpecs[specName] && !availableSpecs[specName][option.id]
}"
@click="selectSpec(specName, option)"
:disabled="availableSpecs[specName] && !availableSpecs[specName][option.id]"
>
<span class="spec-label">{{ option.name }}</span>
</div>
</div>
</div>
<!-- 订单备注 -->
<div class="note-section">
<h3 class="section-title">订单备注</h3>
<div class="note-content" @click="showNoteEdit = true">
<span v-if="!showNoteEdit && !orderNote" class="note-placeholder">无备注</span>
<span v-if="!showNoteEdit && orderNote" class="note-text">{{ orderNote }}</span>
<el-input
v-if="showNoteEdit"
v-model="orderNote"
@blur="showNoteEdit = false"
@keyup.enter="showNoteEdit = false"
placeholder="请输入订单备注"
class="note-input"
autofocus
/>
</div>
</div>
</div>
<!-- 底部操作按钮 -->
<div class="bottom-actions">
<el-button
size="large"
class="cart-button"
@click="handleAddToCart"
:disabled="!canPurchase"
>
加入购物车
</el-button>
<el-button
type="primary"
size="large"
class="buy-button"
@click="handlePurchase"
:disabled="!canPurchase"
>
立即购买
</el-button>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import {
ArrowLeft,
Close,
Edit,
Coin,
ArrowRight
} from '@element-plus/icons-vue'
import api from '@/utils/api'
import { getImageUrl } from '@/config'
const route = useRoute()
const router = useRouter()
// 响应式数据
const loading = ref(false)
const product = ref(null)
const quantity = ref(1)
const specGroups = ref({}) // 动态规格组
const selectedSpecs = ref({}) // 选中的规格值
const orderNote = ref('')
const showNoteEdit = ref(false)
const availableSpecs = ref({}) // 存储每个规格选项的可选状态
const validCombinations = ref([]) // 存储有效的规格组合键
const specIdToOrder = ref({}) // 规格ID到顺序编号的映射
// 计算属性
const totalPrice = computed(() => {
if (!product.value) return 0
return product.value.rongdou_price * quantity.value
})
const totalPointsPrice = computed(() => {
if (!product.value) return 0
return product.value.points_price * quantity.value
})
const canPurchase = computed(() => {
const specNames = Object.keys(specGroups.value)
const allSpecsSelected = specNames.every(specName => selectedSpecs.value[specName])
return allSpecsSelected && quantity.value > 0
})
// 方法
const increaseQuantity = () => {
if (product.value && quantity.value < product.value.stock) {
quantity.value++
}
}
const decreaseQuantity = () => {
if (quantity.value > 1) {
quantity.value--
}
}
// 检查规格组合是否有效
const isValidCombination = (testSelection) => {
const selectedIds = []
const specNames = Object.keys(specGroups.value)
// 按规格名称顺序收集选中的规格ID转换为顺序编号
specNames.forEach(specName => {
if (testSelection[specName]) {
const specId = testSelection[specName].id
const orderNumber = specIdToOrder.value[specId]
selectedIds.push(orderNumber)
} else {
selectedIds.push(null) // 未选择的规格用null占位
}
})
// 如果还没有选择完所有规格,检查部分选择是否与任何有效组合兼容
if (selectedIds.includes(null)) {
return validCombinations.value.some(combinationKey => {
const keyParts = combinationKey.split('-').map(k => parseInt(k))
// 检查当前部分选择是否与这个combination_key兼容
return selectedIds.every((selectedOrder, index) => {
// 如果该位置未选择,则兼容
if (selectedOrder === null) return true
// 如果该位置已选择,检查是否匹配
return selectedOrder === keyParts[index]
})
})
}
// 如果选择了所有规格,检查完整组合是否有效
const combinationKey = selectedIds.join('-')
return validCombinations.value.includes(combinationKey)
}
// 更新可选规格状态
const updateAvailableSpecs = () => {
const specNames = Object.keys(specGroups.value)
const newAvailableSpecs = {}
specNames.forEach(specName => {
newAvailableSpecs[specName] = {}
specGroups.value[specName].forEach(option => {
// 检查如果选择这个选项,是否存在有效的组合
const testSelection = { ...selectedSpecs.value, [specName]: option }
newAvailableSpecs[specName][option.id] = isValidCombination(testSelection)
})
})
availableSpecs.value = newAvailableSpecs
}
// 选择规格
const selectSpec = (specName, option) => {
// 检查该选项是否被禁用
if (availableSpecs.value[specName] && !availableSpecs.value[specName][option.id]) {
ElMessage.warning('该规格组合不可选,请选择其他规格')
return
}
selectedSpecs.value[specName] = option
console.log(`选择${specName}:`, option)
console.log('当前选中的所有规格:', selectedSpecs.value)
// 更新可选规格状态
updateAvailableSpecs()
}
const getProductInfo = async () => {
try {
loading.value = true
const productId = route.query.productId
if (!productId) {
ElMessage.error('商品信息缺失')
router.go(-1)
return
}
const response = await api.get(`/products/${productId}`)
const productData = response.data.data.product
product.value = productData
// 从商品规格中解析颜色分类和尺寸
parseSpecifications(productData.specifications || [])
} catch (error) {
ElMessage.error('获取商品信息失败')
router.go(-1)
} finally {
loading.value = false
}
}
// 解析商品规格信息从spec_details中提取规格
const parseSpecifications = (specifications) => {
console.log('原始规格数据:', specifications)
const tempSpecGroups = {}
const validCombinationKeys = [] // 存储有效的combination_key
const specIdToOrderMap = {} // 规格ID到顺序编号的映射
// 遍历每个规格组合提取combination_key
specifications.forEach(spec => {
if (spec.combination_key) {
validCombinationKeys.push(spec.combination_key)
}
// 遍历每个规格组合中的spec_details
spec.spec_details.forEach(detail => {
const specName = detail.spec_name
const specValue = detail.value
if (!tempSpecGroups[specName]) {
tempSpecGroups[specName] = new Set()
}
// 使用Set避免重复值
tempSpecGroups[specName].add(JSON.stringify({
id: detail.id,
name: specValue,
label: specValue,
description: specValue,
spec_name_id: detail.spec_name_id,
sort_order: detail.sort_order,
}))
})
})
// 转换Set为数组并解析JSON
const finalSpecGroups = {}
let orderCounter = 1
Object.keys(tempSpecGroups).forEach(specName => {
finalSpecGroups[specName] = Array.from(tempSpecGroups[specName]).map(item => JSON.parse(item))
// 按sort_order排序
finalSpecGroups[specName].sort((a, b) => a.sort_order - b.sort_order)
// 为每个规格选项分配顺序编号从1开始
finalSpecGroups[specName].forEach(option => {
specIdToOrderMap[option.id] = orderCounter++
})
})
specGroups.value = finalSpecGroups
// 存储有效的combination_key和ID映射用于验证
validCombinations.value = validCombinationKeys
specIdToOrder.value = specIdToOrderMap
console.log('有效的规格组合键:', validCombinationKeys)
console.log('规格ID到顺序编号映射:', specIdToOrderMap)
// 初始化可选规格状态
updateAvailableSpecs()
// 输出解析后的规格信息
console.log('解析后的规格分组:', finalSpecGroups)
}
// 根据选中的规格组合找到对应的规格规则ID
const getSelectedSpecificationId = () => {
const specNames = Object.keys(specGroups.value)
const selectedIds = []
// 按规格名称顺序收集选中的规格ID转换为顺序编号
specNames.forEach(specName => {
if (selectedSpecs.value[specName]) {
const specId = selectedSpecs.value[specName].id
const orderNumber = specIdToOrder.value[specId]
selectedIds.push(orderNumber)
}
})
// 生成combination_key
const combinationKey = selectedIds.join('-')
// 在specifications数组中找到对应的规格规则
const specification = product.value.specifications?.find(spec =>
spec.combination_key === combinationKey
)
return specification ? specification.id : null
}
// 立即购买功能
const handlePurchase = async () => {
// 检查是否选择了所有必需的规格
const specNames = Object.keys(specGroups.value)
for (const specName of specNames) {
if (!selectedSpecs.value[specName]) {
ElMessage.warning(`请选择${specName}`)
return
}
}
// 获取选中规格对应的规格规则ID
const specificationId = getSelectedSpecificationId()
if (!specificationId) {
ElMessage.error('所选规格组合无效,请重新选择')
return
}
try {
// 先将商品添加到购物车
const cartItem = {
productId: product.value.id,
quantity: quantity.value,
specificationId: specificationId,
points: product.value.points,
name: product.value.name,
image: product.value.image,
stock: product.value.stock
}
const addToCartResponse = await api.post('/cart/add', cartItem)
if (!addToCartResponse.data.success) {
throw new Error(addToCartResponse.data.message || '添加到购物车失败')
}
// 获取刚添加的购物车项ID
const cartItemId = addToCartResponse.data.data?.cart_item_id || addToCartResponse.data.data?.id || addToCartResponse.data.data?.cartItemId || addToCartResponse.data.id
if (!cartItemId) {
console.error('添加购物车API响应:', addToCartResponse.data)
throw new Error('无法获取购物车项ID')
}
// 创建订单
const response = await api.post('/orders/create-from-cart', {
cart_item_ids: [cartItemId]
})
if (response.data.success) {
// 跳转到Pay页面
router.push(`/pay/${response.data.data.preOrderId}`)
} else {
throw new Error(response.data.message || '创建订单失败')
}
} catch (error) {
ElMessage.error(error.message || '操作失败,请重试')
}
}
// 添加到购物车功能(新增)
const handleAddToCart = async () => {
// 检查是否选择了所有必需的规格
const specNames = Object.keys(specGroups.value)
for (const specName of specNames) {
if (!selectedSpecs.value[specName]) {
ElMessage.warning(`请选择${specName}`)
return
}
}
// 获取选中规格对应的规格规则ID
const specificationId = getSelectedSpecificationId()
if (!specificationId) {
ElMessage.error('所选规格组合无效,请重新选择')
return
}
try {
const cartItem = {
productId: product.value.id, // 商品ID
quantity: quantity.value, // 购买数量
specificationId: specificationId, // 规格规则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.push('/cart')
} else {
throw new Error(response.data.message || '添加到购物车失败')
}
} catch (error) {
ElMessage.error(error.message || '添加到购物车失败,请重试')
}
}
// 生命周期
onMounted(() => {
// 从URL参数获取初始数量
const initialQuantity = route.query.quantity
if (initialQuantity && !isNaN(initialQuantity)) {
quantity.value = parseInt(initialQuantity)
}
getProductInfo() // 商品信息中已包含规格信息,无需单独获取颜色分类和尺寸
})
</script>
<style scoped>
.buy-details-page {
min-height: 100vh;
background: #f5f5f5;
display: flex;
flex-direction: column;
}
.navbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: white;
border-bottom: 1px solid #eee;
}
.nav-title {
font-size: 18px;
font-weight: 500;
margin: 0;
}
.back-btn {
color: #333;
padding: 0;
}
.page-content {
flex: 1;
padding: 0;
}
.product-section {
background: white;
padding: 16px;
margin-bottom: 8px;
}
.product-info {
display: flex;
gap: 12px;
}
.product-image {
width: 60px;
height: 60px;
border-radius: 8px;
overflow: hidden;
}
.product-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.product-details {
flex: 1;
display: flex;
justify-content: space-between;
align-items: center;
}
.product-price {
display: flex;
align-items: flex-start;
gap: 8px;
}
.price-label {
font-size: 14px;
color: #666;
margin-top: 2px;
}
.price-container {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
.main-price {
display: flex;
align-items: center;
gap: 4px;
}
.rongdou-icon {
width: 20px;
height: 20px;
object-fit: contain;
}
.rongdou-price {
font-size: 18px;
font-weight: bold;
color: #ff6b35;
}
.sub-price {
display: flex;
align-items: center;
gap: 3px;
margin-left: 0;
}
.points-icon {
width: 14px;
height: 14px;
color: #666;
}
.points-price {
font-size: 14px;
color: #666;
font-weight: normal;
}
.quantity-selector {
display: flex;
align-items: center;
gap: 12px;
}
.quantity {
font-size: 16px;
min-width: 20px;
text-align: center;
}
.spec-section,
.note-section,
.payment-section {
background: white;
padding: 16px;
margin-bottom: 8px;
}
.section-title {
font-size: 16px;
font-weight: 500;
margin: 0 0 12px 0;
}
.spec-grid {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 10px;
}
.spec-item {
display: flex;
align-items: center;
justify-content: center;
padding: 10px 15px;
border: 1px solid #e0e0e0;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
min-width: 60px;
text-align: center;
}
.spec-item:hover {
border-color: #ff6b35;
background-color: #fff5f2;
}
.spec-item.active {
border-color: #ff6b35;
background-color: #ff6b35;
color: white;
}
.spec-item.disabled {
background-color: #f5f5f5;
border-color: #e0e0e0;
color: #ccc;
cursor: not-allowed;
}
.spec-item.disabled:hover {
background-color: #f5f5f5;
border-color: #e0e0e0;
}
.spec-label {
font-size: 14px;
font-weight: 500;
}
.note-content {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 0;
}
.note-placeholder {
color: #999;
}
.note-input {
flex: 1;
margin-right: 8px;
}
.note-text {
color: #333;
flex: 1;
}
.note-content {
cursor: pointer;
}
.payment-options {
display: flex;
flex-direction: column;
gap: 12px;
}
.payment-option {
display: flex;
align-items: center;
padding: 8px 0;
}
.bottom-actions {
padding: 16px;
background: white;
border-top: 1px solid #eee;
display: flex;
gap: 12px;
}
.cart-button {
flex: 1;
height: 48px;
background: white;
border: 1px solid #ffae00;
color: #ffae00;
border-radius: 24px;
font-size: 16px;
font-weight: 500;
}
.cart-button:hover {
background: #fff7e6;
}
.cart-button:disabled {
background: #f5f5f5;
border-color: #ccc;
color: #ccc;
cursor: not-allowed;
}
.buy-button {
flex: 1;
height: 48px;
background: #ffae00;
border: none;
border-radius: 24px;
font-size: 16px;
font-weight: 500;
}
.buy-button:hover {
background: #e69900;
}
.buy-button:disabled {
background: #ccc;
cursor: not-allowed;
}
</style>