@@ -84,41 +84,26 @@
< / div >
< / div >
<!-- 颜色分类 -- >
< div class = "category-section" >
< h3 class = "section-title" > 颜色分类 ( { { categories . length } } ) < / h3 >
< div class = "category-grid" >
<!-- 动态规格选择 -- >
< div
v-for = "category in categorie s"
:key = "category.id "
class = "category-item "
: class = "{ active: selectedCategory?.id === category.id }"
@click ="selectCategory(category)"
v-for = "(specOptions, specName) in specGroup s"
:key = "specName "
class = "spec-section "
>
< div class = "category-image" >
< img :src = "category.image" :alt = "category.name" / >
< / div >
< div class = "category-info" >
< div class = "category-name" > { { category . name } } < / div >
< div class = "category-desc" > { { category . description } } < / div >
< / div >
< / div >
< / div >
< / div >
<!-- 尺寸选择 -- >
< div class = "size-section" >
< h3 class = "section-title" > 尺寸 < / h3 >
< div class = "size-grid" >
< h3 class = "section-title" > { { specName } } ( { { specOptions . length } } ) < / h3 >
< div class = "spec-grid" >
< div
v-for = "size in size s"
:key = "size .id"
class = "size -item"
: class = "{ active: selectedSize?.id === size.id }"
@click ="selectSize(size)"
v-for = "option in specOption s"
: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]"
>
< div class = "size -label" > { { size . label } } < / div >
< div class = "size-range" > { { size . range } } < / div >
< span class = "spec -label" > { { option . name } } < / span >
< / div >
< / div >
< / div >
@@ -188,15 +173,16 @@ const router = useRouter()
const loading = ref ( false )
const product = ref ( null )
const quantity = ref ( 1 )
const categorie s = ref ( [ ] )
const size s = ref ( [ ] )
const selectedCategory = ref ( null )
const selectedSize = ref ( null )
const specGroup s = ref ( { } ) // 动态规格组
const selectedSpec s = ref ( { } ) // 选中的规格值
const addresses = ref ( [ ] )
const selectedAddressId = ref ( '' )
const selectedAddress = ref ( null )
const orderNote = ref ( '' )
const showNoteEdit = ref ( false )
const availableSpecs = ref ( { } ) // 存储每个规格选项的可选状态
const validCombinations = ref ( [ ] ) // 存储有效的规格组合键
const specIdToOrder = ref ( { } ) // 规格ID到顺序编号的映射
// 计算属性
const totalPrice = computed ( ( ) => {
@@ -205,7 +191,9 @@ const totalPrice = computed(() => {
} )
const canPurchase = computed ( ( ) => {
return selectedCategory . value && selectedSize . value && quantity . value > 0
const specNames = Object . keys ( specGroups . value )
const allSpecsSelected = specNames . every ( specName => selectedSpecs . value [ specName ] )
return allSpecsSelected && quantity . value > 0
} )
// 方法
@@ -221,12 +209,73 @@ const decreaseQuantity = () => {
}
}
const selectCategory = ( category ) => {
selectedCategory . value = category
// 检查规格组合是否有效
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 selectSize = ( size ) => {
selectedSize . value = size
// 更新可选规格状态
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 ( ) => {
@@ -240,7 +289,11 @@ const getProductInfo = async () => {
}
const response = await api . get ( ` /products/ ${ productId } ` )
product . value = response . data . data . product
const productData = response . data . data . product
product . value = productData
// 从商品规格中解析颜色分类和尺寸
parseSpecifications ( productData . specifications || [ ] )
} catch ( error ) {
ElMessage . error ( '获取商品信息失败' )
router . go ( - 1 )
@@ -249,80 +302,135 @@ const getProductInfo = async () => {
}
}
const getCategories = async ( ) => {
try {
const productId = route . query . productId
const response = await api . get ( ` /products/ ${ productId } /categories ` )
categories . value = response . data . data . categorie s || [ ]
} catch ( error ) {
console . error ( '获取分类信息失败:' , error )
// 解析商品规格信息, 从spec_details中提取规格
const parseSpecifications = ( specifications ) => {
console . log ( '原始规格数据:' , specifications )
const tempSpecGroup s = { }
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 )
}
const getSizes = async ( ) => {
try {
const productId = route . query . productId
const response = await api . get ( ` /products/ ${ productId } /sizes ` )
sizes . value = response . data . data . sizes || [ ]
} catch ( error ) {
console . error ( '获取尺寸信息失败:' , error )
}
}
// 根据选中的规格组合找到对应的规格规则ID
const getSelectedSpecificationId = ( ) => {
const specNames = Object . keys ( specGroups . value )
const selectedIds = [ ]
const addToCart = async ( ) => {
if ( ! canPurchase . value ) {
ElMessag e. error ( '请选择完整的商品信息' )
return
// 按规格名称顺序收集选中的规格ID, 转换为顺序编号
specNames . forEach ( specName => {
if ( selectedSpecs . valu e[ specName ] ) {
const specId = selectedSpecs . value [ specName ] . id
const orderNumber = specIdToOrder . value [ specId ]
selectedIds . push ( orderNumber )
}
} )
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 . images ? . [ 0 ] || product . value . image ,
stock : product . value . stock
}
// 生成combination_key
const combinationKey = selectedIds . join ( '-' )
await api . post ( '/cart/add' , cartItem )
ElMessage . success ( '商品已加入购物车!' )
router . go ( - 1 ) // 返回上一页
} catch ( error ) {
ElMessage . error ( '加入购物车失败,请重试' )
}
// 在specifications数组中找到对应的规格规则
const specification = product . value . specifications ? . find ( spec =>
spec . combination _key === combinationKey
)
return specification ? specification . id : null
}
// 立即购买功能
const handlePurchase = async ( ) => {
if ( ! canPurchase . value ) {
ElMessage . error ( '请选择完整的商品信息' )
// 检查是否选择了所有必需的规格
const specNames = Object . keys ( specGroups . value )
for ( const specName of specNames ) {
if ( ! selectedSpecs . value [ specName ] ) {
ElMessage . warning ( ` 请选择 ${ specName } ` )
return
}
}
if ( ! selectedAddress . value ) {
ElMessage . error ( '请选择收货地址' )
return
}
// 获取选中规格对应的规格规则ID
const specificationId = getSelectedSpecificationId ( )
if ( ! specificationId ) {
ElMessage . error ( '所选规格组合无效,请重新选择' )
return
}
try {
// 创建单独的购买订单
const orderData = {
productId : product . value . id ,
quantity : quantity . value ,
categoryId : selectedCategory . value . id ,
sizeId : selectedSize . value . id ,
points : product . value . points ,
nam e: product . value . nam e,
image : product . value . image ,
stock : product . value . stock ,
addressId : selectedAddress . value . id ,
orderNote : orderNote . value
productId : product . value . id , // 商品ID
quantity : quantity . value , // 购买数量
specificationId : specificationId , // 规格规则ID
points : product . value . points , // 商品积分价格
name : product . value . name , // 商品名称
imag e: product . value . imag e, // 商品图片
stock : product . value . stock , // 商品库存
addressId : selectedAddress . value . id , // 收货地址ID
orderNote : orderNote . value // 订单备注
}
const response = await api . post ( '/cart/buy-now' , orderData )
const response = await api . post ( '/cart/buy-now' , orderData ) //立即购买
if ( response . data . success ) {
const cartId = response . data . data . cartId
@@ -344,21 +452,31 @@ const handlePurchase = async () => {
// 添加到购物车功能(新增)
const handleAddToCart = async ( ) => {
if ( ! canPurchase . value ) {
ElMessage . error ( '请选择完整的商品信息' )
// 检查是否选择了所有必需的规格
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 ,
categoryId : selectedCategory . value . id ,
sizeId : selectedSize . value . id ,
points : product . value . points ,
nam e: product . value . nam e,
image : product . value . image ,
stock : product . value . stock
productId : product . value . id , // 商品ID
quantity : quantity . value , // 购买数量
specificationId : specificationId , // 规格规则ID
points : product . value . points , // 商品积分价格
name : product . value . name , // 商品名称
imag e: product . value . imag e, // 商品图片
stock : product . value . stock // 商品库存
}
const response = await api . post ( '/cart/add' , cartItem )
@@ -378,17 +496,36 @@ const handleAddToCart = async () => {
// 获取用户地址列表
const getAddressList = async ( ) => {
try {
const response = await api . get ( '/address/list ' )
addresses . value = resp onse. data . data . addresses || [ ]
const response = await api . get ( '/addresses ' )
c onsol e. log ( '获取地址列表响应:' , response )
if ( response . data . success ) {
// 根据接口文档转换数据格式, 与Address.vue保持一致
const addressList = response . data . data || [ ]
addresses . value = addressList . map ( addr => ( {
id : addr . id ,
recipientName : addr . receiver _name ,
recipientPhone : addr . receiver _phone ,
province : addr . province _name ,
city : addr . city _name ,
district : addr . district _name ,
detailAddress : addr . detailed _address ,
isDefault : addr . is _default ,
labelName : addr . label _name ,
labelColor : addr . label _color
} ) )
// 如果有默认地址,自动选中
const defaultAddress = addresses . value . find ( addr => addr . isDefault )
if ( defaultAddress ) {
selectedAddressId . value = defaultAddress . id
selectedAddress . value = defaultAddress
}
} else {
throw new Error ( response . data . message || '获取地址列表失败' )
}
} catch ( error ) {
console . error ( '获取地址列表失败:' , error )
ElMessage . error ( '获取地址列表失败' )
ElMessage . error ( error . message || '获取地址列表失败' )
}
}
@@ -410,9 +547,7 @@ onMounted(() => {
quantity . value = parseInt ( initialQuantity )
}
getProductInfo ( )
getCategories ( )
getSizes ( )
getProductInfo ( ) // 商品信息中已包含规格信息,无需单独获取颜色分类和尺寸
getAddressList ( )
} )
< / script >
@@ -538,8 +673,7 @@ onMounted(() => {
text - align : center ;
}
. category - section ,
. size - section ,
. spec - section ,
. note - section ,
. payment - section {
background : white ;
@@ -553,84 +687,52 @@ onMounted(() => {
margin : 0 0 12 px 0 ;
}
. category - grid {
display : grid ;
grid - template - columns : 1 fr 1 fr ;
gap : 12 px ;
}
. category - item {
. spec - grid {
display : flex ;
g ap: 8 px ;
padding : 8 px ;
border : 1 px solid # eee ;
border - radius : 8 px ;
flex - wr ap: wrap ;
gap : 10 px ;
margin - top : 10 px ;
}
. spec - item {
display : flex ;
align - items : center ;
justify - content : center ;
padding : 10 px 15 px ;
border : 1 px solid # e0e0e0 ;
border - radius : 6 px ;
cursor : pointer ;
transition : all 0.2 s ;
}
. category - item . active {
border - color : # ffae00 ;
background : # fff7e6 ;
}
. category - image {
width : 40 px ;
height : 40 px ;
border - radius : 4 px ;
overflow : hidden ;
}
. category - image img {
width : 100 % ;
height : 100 % ;
object - fit : cover ;
}
. category - info {
flex : 1 ;
}
. category - name {
font - size : 14 px ;
font - weight : 500 ;
margin - bottom : 2 px ;
}
. category - desc {
font - size : 12 px ;
color : # 666 ;
}
. size - grid {
display : grid ;
grid - template - columns : repeat ( 3 , 1 fr ) ;
gap : 8 px ;
}
. size - item {
padding : 12 px 8 px ;
border : 1 px solid # eee ;
border - radius : 8 px ;
transition : all 0.3 s ease ;
min - width : 60 px ;
text - align : center ;
cursor : pointer ;
transition : all 0.2 s ;
}
. size - item . acti ve {
border - color : # ffae00 ;
background : # fff7e6 ;
. spec - item : ho ver {
border - color : # ff6b35 ;
background - color : # fff5f2 ;
}
. size - label {
. 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 : 14 px ;
font - weight : 500 ;
margin - bottom : 4 px ;
}
. size - range {
font - size : 12 px ;
color : # 666 ;
}
. note - content {