init
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
<template>
|
||||
<div class="max-w-site mx-auto px-4 py-8 space-y-6">
|
||||
<!-- Title -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center space-x-3">
|
||||
<h1 class="text-2xl font-bold text-gray-900">我的购物车</h1>
|
||||
<span class="text-xs text-gray-400">({{ cartStore.validList.length }} 件商品)</span>
|
||||
</div>
|
||||
<router-link to="/products" class="text-xs text-primary font-semibold hover:underline">
|
||||
< 继续选购
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- Cart Main Content -->
|
||||
<div v-if="cartStore.validList.length > 0" class="space-y-4">
|
||||
<!-- Cart Table -->
|
||||
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden">
|
||||
<!-- Table Header -->
|
||||
<div class="grid grid-cols-12 gap-4 px-6 py-4 bg-gray-50 border-b border-gray-100 text-xs font-semibold text-gray-500">
|
||||
<div class="col-span-1 flex items-center">
|
||||
<el-checkbox v-model="isAllSelected" @change="toggleSelectAll">全选</el-checkbox>
|
||||
</div>
|
||||
<div class="col-span-5">商品信息</div>
|
||||
<div class="col-span-2 text-center">单价</div>
|
||||
<div class="col-span-2 text-center">数量</div>
|
||||
<div class="col-span-1 text-center">小计</div>
|
||||
<div class="col-span-1 text-right">操作</div>
|
||||
</div>
|
||||
|
||||
<!-- Table Rows -->
|
||||
<div class="divide-y divide-gray-100">
|
||||
<div
|
||||
v-for="item in cartStore.validList"
|
||||
:key="item.id"
|
||||
class="grid grid-cols-12 gap-4 px-6 py-5 items-center hover:bg-gray-50/50 transition-colors"
|
||||
>
|
||||
<!-- Checkbox -->
|
||||
<div class="col-span-1">
|
||||
<el-checkbox v-model="item.selected" />
|
||||
</div>
|
||||
|
||||
<!-- Product info -->
|
||||
<div class="col-span-5 flex items-center space-x-4">
|
||||
<router-link :to="`/product/${item.product_id}`" class="w-20 h-20 rounded-xl overflow-hidden bg-gray-50 border border-gray-100 flex-shrink-0">
|
||||
<img :src="item.productInfo?.attrInfo?.image || item.productInfo?.image" class="w-full h-full object-cover" />
|
||||
</router-link>
|
||||
<div class="space-y-1">
|
||||
<router-link :to="`/product/${item.product_id}`" class="text-xs font-bold text-gray-800 hover:text-primary line-clamp-2 leading-relaxed">
|
||||
{{ item.productInfo?.store_name }}
|
||||
</router-link>
|
||||
<p v-if="item.productInfo?.attrInfo?.suk" class="text-[11px] text-gray-400 bg-gray-100 px-2 py-0.5 rounded inline-block">
|
||||
规格:{{ item.productInfo?.attrInfo?.suk }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Unit Price -->
|
||||
<div class="col-span-2 text-center text-xs font-semibold text-gray-700">
|
||||
¥{{ item.truePrice || item.productInfo?.attrInfo?.price || item.productInfo?.price }}
|
||||
</div>
|
||||
|
||||
<!-- Quantity Stepper -->
|
||||
<div class="col-span-2 flex justify-center">
|
||||
<el-input-number
|
||||
v-model="item.cart_num"
|
||||
:min="1"
|
||||
:max="item.productInfo?.attrInfo?.stock || item.productInfo?.stock || 99"
|
||||
size="small"
|
||||
@change="(val) => handleNumChange(item.id, val)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Subtotal -->
|
||||
<div class="col-span-1 text-center text-xs font-bold text-primary">
|
||||
¥{{ ((item.truePrice || item.productInfo?.attrInfo?.price || item.productInfo?.price) * item.cart_num).toFixed(2) }}
|
||||
</div>
|
||||
|
||||
<!-- Delete Action -->
|
||||
<div class="col-span-1 text-right">
|
||||
<button @click="handleDelete(item.id)" class="text-xs text-gray-400 hover:text-primary transition-colors">
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom Summary Bar -->
|
||||
<div class="bg-white rounded-2xl p-6 shadow-sm border border-gray-100 flex items-center justify-between sticky bottom-4 z-30">
|
||||
<div class="flex items-center space-x-6 text-xs text-gray-500">
|
||||
<el-checkbox v-model="isAllSelected" @change="toggleSelectAll">全选</el-checkbox>
|
||||
<button @click="batchDelete" class="hover:text-primary transition-colors">
|
||||
删除选中商品
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-6">
|
||||
<div class="text-right">
|
||||
<span class="text-xs text-gray-500">已选择 <strong class="text-primary font-bold">{{ selectedCount }}</strong> 件商品,合计:</span>
|
||||
<span class="text-2xl font-extrabold text-primary ml-2">¥{{ totalPrice.toFixed(2) }}</span>
|
||||
</div>
|
||||
<button
|
||||
:disabled="selectedCount === 0"
|
||||
@click="handleCheckout"
|
||||
class="px-8 py-3.5 bg-primary hover:bg-primary-hover disabled:bg-gray-300 text-white font-bold text-sm rounded-xl shadow-lg shadow-red-200 transition-all"
|
||||
>
|
||||
去结算 ({{ selectedCount }})
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-else class="bg-white rounded-2xl p-20 text-center shadow-sm border border-gray-100">
|
||||
<div class="text-6xl mb-4">🛒</div>
|
||||
<h3 class="text-lg font-bold text-gray-800">您的购物车还是空的</h3>
|
||||
<p class="text-xs text-gray-400 mt-1 mb-6">快去挑选您心仪的优质好物吧!</p>
|
||||
<router-link to="/products" class="inline-block px-8 py-3 bg-primary text-white text-xs font-bold rounded-xl hover:bg-primary-hover shadow-md shadow-red-200 transition-all">
|
||||
立即去逛逛
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useCartStore } from '@/store/cart'
|
||||
import { ElMessageBox, ElMessage } from 'element-plus'
|
||||
|
||||
const router = useRouter()
|
||||
const cartStore = useCartStore()
|
||||
|
||||
const isAllSelected = computed({
|
||||
get() {
|
||||
return cartStore.validList.length > 0 && cartStore.validList.every(i => i.selected)
|
||||
},
|
||||
set(val) {
|
||||
cartStore.validList.forEach(i => i.selected = val)
|
||||
}
|
||||
})
|
||||
|
||||
const selectedItems = computed(() => cartStore.validList.filter(i => i.selected))
|
||||
const selectedCount = computed(() => selectedItems.value.reduce((acc, cur) => acc + cur.cart_num, 0))
|
||||
const totalPrice = computed(() => {
|
||||
return selectedItems.value.reduce((acc, cur) => {
|
||||
const price = cur.truePrice || cur.productInfo?.attrInfo?.price || cur.productInfo?.price || 0
|
||||
return acc + (price * cur.cart_num)
|
||||
}, 0)
|
||||
})
|
||||
|
||||
const toggleSelectAll = (val) => {
|
||||
cartStore.validList.forEach(i => i.selected = val)
|
||||
}
|
||||
|
||||
const handleNumChange = async (id, num) => {
|
||||
await cartStore.updateCartNum(id, num)
|
||||
}
|
||||
|
||||
const handleDelete = (id) => {
|
||||
ElMessageBox.confirm('确定要从购物车中移除该商品吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
await cartStore.removeCartItems([id])
|
||||
})
|
||||
}
|
||||
|
||||
const batchDelete = () => {
|
||||
const ids = selectedItems.value.map(i => i.id)
|
||||
if (!ids.length) {
|
||||
ElMessage.warning('请选择需要删除的商品')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(`确定要删除选中的 ${ids.length} 件商品吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
await cartStore.removeCartItems(ids)
|
||||
})
|
||||
}
|
||||
|
||||
const handleCheckout = () => {
|
||||
const cartIds = selectedItems.value.map(i => i.id).join(',')
|
||||
router.push(`/checkout?cartId=${cartIds}`)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
cartStore.fetchCartList()
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,253 @@
|
||||
<template>
|
||||
<div class="max-w-site mx-auto px-4 py-8 space-y-6">
|
||||
<h1 class="text-2xl font-bold text-gray-900">填写并核对订单信息</h1>
|
||||
|
||||
<div v-loading="loading" class="space-y-6">
|
||||
<!-- 1. Address Section -->
|
||||
<div class="bg-white rounded-2xl p-6 shadow-sm border border-gray-100 space-y-4">
|
||||
<div class="flex items-center justify-between pb-3 border-b border-gray-100">
|
||||
<h3 class="text-sm font-bold text-gray-900 flex items-center space-x-2">
|
||||
<span>📍 收货地址</span>
|
||||
</h3>
|
||||
<button @click="openAddressModal" class="text-xs text-primary font-semibold hover:underline">
|
||||
+ 新增收货地址
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="addressList.length > 0" class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div
|
||||
v-for="addr in addressList"
|
||||
:key="addr.id"
|
||||
@click="selectedAddressId = addr.id"
|
||||
:class="selectedAddressId === addr.id ? 'border-primary bg-red-50/40 ring-2 ring-red-100' : 'border-gray-200 hover:border-gray-300'"
|
||||
class="p-4 rounded-xl border-2 cursor-pointer transition-all relative space-y-1.5 text-xs"
|
||||
>
|
||||
<div class="flex items-center justify-between font-bold text-gray-800">
|
||||
<span>{{ addr.real_name }} ({{ addr.phone }})</span>
|
||||
<span v-if="addr.is_default" class="bg-red-100 text-primary text-[10px] px-1.5 py-0.5 rounded">默认</span>
|
||||
</div>
|
||||
<p class="text-gray-500 leading-relaxed">
|
||||
{{ addr.province }} {{ addr.city }} {{ addr.district }} {{ addr.detail }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-xs text-gray-400 py-2">
|
||||
暂无可用收货地址,请先点击右上角新增。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. Order Items List -->
|
||||
<div class="bg-white rounded-2xl p-6 shadow-sm border border-gray-100 space-y-4">
|
||||
<h3 class="text-sm font-bold text-gray-900 pb-3 border-b border-gray-100">
|
||||
🛍️ 商品清单
|
||||
</h3>
|
||||
|
||||
<div class="divide-y divide-gray-100">
|
||||
<div
|
||||
v-for="item in cartInfo"
|
||||
:key="item.id"
|
||||
class="py-3 flex items-center justify-between text-xs"
|
||||
>
|
||||
<div class="flex items-center space-x-4">
|
||||
<img :src="item.productInfo?.attrInfo?.image || item.productInfo?.image" class="w-14 h-14 rounded-lg object-cover border border-gray-100" />
|
||||
<div>
|
||||
<h4 class="font-bold text-gray-800">{{ item.productInfo?.store_name }}</h4>
|
||||
<p v-if="item.productInfo?.attrInfo?.suk" class="text-gray-400 mt-0.5">规格:{{ item.productInfo?.attrInfo?.suk }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right space-y-1">
|
||||
<span class="font-bold text-gray-900">¥{{ item.truePrice || item.productInfo?.price }} x {{ item.cart_num }}</span>
|
||||
<p class="text-primary font-bold">小计:¥{{ ((item.truePrice || item.productInfo?.price) * item.cart_num).toFixed(2) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. Payment Method -->
|
||||
<div class="bg-white rounded-2xl p-6 shadow-sm border border-gray-100 space-y-4">
|
||||
<h3 class="text-sm font-bold text-gray-900 pb-3 border-b border-gray-100">
|
||||
💳 支付方式
|
||||
</h3>
|
||||
|
||||
<div class="flex flex-wrap gap-4 text-xs">
|
||||
<button
|
||||
@click="payType = 'weixin'"
|
||||
:class="payType === 'weixin' ? 'border-primary bg-red-50/40 text-primary font-bold' : 'border-gray-200 text-gray-700'"
|
||||
class="px-6 py-3 rounded-xl border-2 flex items-center space-x-2 transition-all"
|
||||
>
|
||||
<span>🟢 微信扫码支付</span>
|
||||
</button>
|
||||
<button
|
||||
@click="payType = 'yue'"
|
||||
:class="payType === 'yue' ? 'border-primary bg-red-50/40 text-primary font-bold' : 'border-gray-200 text-gray-700'"
|
||||
class="px-6 py-3 rounded-xl border-2 flex items-center space-x-2 transition-all"
|
||||
>
|
||||
<span>💰 余额支付</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 4. Summary & Submit -->
|
||||
<div class="bg-white rounded-2xl p-6 shadow-sm border border-gray-100 flex flex-col items-end space-y-4">
|
||||
<div class="space-y-2 text-xs text-gray-600 text-right w-64">
|
||||
<div class="flex justify-between">
|
||||
<span>商品总金额:</span>
|
||||
<span class="font-bold text-gray-900">¥{{ (orderData.total_price || 0).toFixed(2) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>运费:</span>
|
||||
<span>+ ¥{{ (orderData.pay_postage || 0).toFixed(2) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between pt-2 border-t border-gray-100 text-sm font-bold text-gray-900">
|
||||
<span>应付总额:</span>
|
||||
<span class="text-2xl font-extrabold text-primary">¥{{ (orderData.pay_price || orderData.total_price || 0).toFixed(2) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
:disabled="submitting || !selectedAddressId"
|
||||
@click="submitOrder"
|
||||
class="px-10 py-4 bg-primary hover:bg-primary-hover disabled:bg-gray-300 text-white font-bold text-base rounded-xl shadow-xl shadow-red-200 transition-all flex items-center space-x-2"
|
||||
>
|
||||
<span v-if="submitting" class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></span>
|
||||
<span>立即提交订单并支付</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Payment QR Code Modal -->
|
||||
<el-dialog
|
||||
v-model="showPayModal"
|
||||
title="微信收银台"
|
||||
width="380px"
|
||||
:align-center="true"
|
||||
destroy-on-close
|
||||
>
|
||||
<div class="text-center py-4 space-y-4">
|
||||
<div class="text-2xl font-extrabold text-primary">¥{{ (orderData.pay_price || orderData.total_price || 0).toFixed(2) }}</div>
|
||||
<div class="w-48 h-48 mx-auto bg-gray-50 border border-gray-200 rounded-xl p-2 flex items-center justify-center">
|
||||
<img v-if="payQrUrl" :src="payQrUrl" class="w-full h-full object-contain" />
|
||||
<span v-else class="text-xs text-gray-400">正在生成支付二维码...</span>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500">请使用微信扫一扫完成支付</p>
|
||||
<button @click="handlePayFinished" class="w-full py-2.5 bg-primary text-white font-bold text-xs rounded-lg hover:bg-primary-hover">
|
||||
我已完成支付
|
||||
</button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { orderConfirm, getAddressList, orderCreate, checkOrderStatus } from '@/api'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const cartId = route.query.cartId
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
|
||||
const orderKey = ref('')
|
||||
const cartInfo = ref([])
|
||||
const orderData = ref({})
|
||||
const addressList = ref([])
|
||||
const selectedAddressId = ref(0)
|
||||
const payType = ref('weixin')
|
||||
|
||||
const showPayModal = ref(false)
|
||||
const payQrUrl = ref('')
|
||||
let payPollTimer = null
|
||||
|
||||
const fetchOrderConfirm = async () => {
|
||||
if (!cartId) {
|
||||
ElMessage.warning('缺少订单商品信息')
|
||||
router.push('/cart')
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await orderConfirm(cartId)
|
||||
orderKey.value = res.orderKey
|
||||
cartInfo.value = res.cartInfo || []
|
||||
orderData.value = res.priceGroup || {}
|
||||
addressList.value = res.addressInfo ? [res.addressInfo] : []
|
||||
|
||||
// Fetch all addresses
|
||||
const addrRes = await getAddressList({ page: 1, limit: 20 })
|
||||
if (addrRes && addrRes.list) {
|
||||
addressList.value = addrRes.list
|
||||
const def = addrRes.list.find(i => i.is_default) || addrRes.list[0]
|
||||
if (def) selectedAddressId.value = def.id
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const openAddressModal = () => {
|
||||
router.push('/user?tab=address')
|
||||
}
|
||||
|
||||
const submitOrder = async () => {
|
||||
if (!selectedAddressId.value) {
|
||||
ElMessage.warning('请选择收货地址')
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
const res = await orderCreate(orderKey.value, {
|
||||
addressId: selectedAddressId.value,
|
||||
payType: payType.value,
|
||||
from: 'pc'
|
||||
})
|
||||
|
||||
if (payType.value === 'yue') {
|
||||
ElMessage.success('余额支付成功!')
|
||||
router.push('/user?tab=orders')
|
||||
} else {
|
||||
// WeChat QR Code
|
||||
const code = res.result?.jsConfig?.code_url || res.result?.code_url
|
||||
payQrUrl.value = code ? `https://api.qrserver.com/v1/create-qr-code/?size=220x220&data=${encodeURIComponent(code)}` : ''
|
||||
showPayModal.value = true
|
||||
|
||||
// Poll order status
|
||||
const orderId = res.result?.order_id
|
||||
if (orderId) {
|
||||
payPollTimer = setInterval(async () => {
|
||||
try {
|
||||
const check = await checkOrderStatus(orderId, Date.now())
|
||||
if (check && check.status) {
|
||||
clearInterval(payPollTimer)
|
||||
ElMessage.success('支付成功!')
|
||||
showPayModal.value = false
|
||||
router.push('/user?tab=orders')
|
||||
}
|
||||
} catch (err) {
|
||||
// ignore
|
||||
}
|
||||
}, 3000)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// handled in interceptor
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handlePayFinished = () => {
|
||||
clearInterval(payPollTimer)
|
||||
showPayModal.value = false
|
||||
router.push('/user?tab=orders')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchOrderConfirm()
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,523 @@
|
||||
<template>
|
||||
<div class="space-y-4 sm:space-y-8 pb-12 sm:pb-16">
|
||||
<!-- Hero Section -->
|
||||
<section class="max-w-site mx-auto px-3 sm:px-4 pt-2 sm:pt-4">
|
||||
<!-- Desktop 3-Column Grid -->
|
||||
<div class="hidden lg:grid grid-cols-12 gap-4 h-[440px]">
|
||||
<!-- Left: Cascading Categories Sidebar -->
|
||||
<div class="col-span-3 bg-white rounded-2xl shadow-sm border border-gray-100 p-3 h-full flex flex-col justify-between relative group/cat z-20">
|
||||
<div class="space-y-1 overflow-y-auto pr-1">
|
||||
<div
|
||||
v-for="cat in categories.slice(0, 10)"
|
||||
:key="cat.id"
|
||||
@mouseenter="hoverCat = cat"
|
||||
class="flex items-center justify-between px-3 py-2 rounded-xl text-sm text-gray-700 hover:text-primary hover:bg-red-50/60 cursor-pointer transition-colors"
|
||||
>
|
||||
<div class="flex items-center space-x-2.5">
|
||||
<img v-if="cat.pic" :src="cat.pic" class="w-5 h-5 object-contain rounded" />
|
||||
<span class="font-medium text-xs">{{ cat.cate_name }}</span>
|
||||
</div>
|
||||
<svg class="w-3.5 h-3.5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<router-link to="/products" class="text-xs text-center py-2 text-primary font-semibold hover:underline border-t border-gray-100 flex items-center justify-center space-x-1">
|
||||
<span>查看全部分类</span>
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"></path></svg>
|
||||
</router-link>
|
||||
|
||||
<!-- Flyout Subcategory Panel on Hover -->
|
||||
<div
|
||||
v-if="hoverCat && hoverCat.children && hoverCat.children.length"
|
||||
class="absolute left-full top-0 ml-2 w-[480px] h-[440px] bg-white rounded-2xl shadow-2xl border border-gray-100 p-6 z-30 overflow-y-auto hidden group-hover/cat:block"
|
||||
>
|
||||
<h4 class="font-bold text-gray-900 mb-4 pb-2 border-b border-gray-100 text-sm flex items-center justify-between">
|
||||
<span>{{ hoverCat.cate_name }}</span>
|
||||
<router-link :to="`/products?cid=${hoverCat.id}`" class="text-xs text-primary font-normal hover:underline">进入分类列表 ></router-link>
|
||||
</h4>
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<router-link
|
||||
v-for="sub in hoverCat.children"
|
||||
:key="sub.id"
|
||||
:to="`/products?sid=${sub.id}`"
|
||||
class="p-2.5 rounded-xl border border-gray-100 hover:border-red-200 hover:bg-red-50/40 text-center transition-all group flex flex-col items-center"
|
||||
>
|
||||
<img :src="sub.pic || 'https://via.placeholder.com/60x60'" class="w-12 h-12 object-contain mb-1.5 group-hover:scale-110 transition-transform" />
|
||||
<span class="text-xs text-gray-700 font-medium group-hover:text-primary">{{ sub.cate_name }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Middle: Banner Carousel -->
|
||||
<div class="col-span-6 h-full rounded-2xl overflow-hidden shadow-sm">
|
||||
<el-carousel trigger="click" height="440px" class="h-full rounded-2xl">
|
||||
<el-carousel-item v-for="(item, idx) in banners" :key="idx">
|
||||
<div class="w-full h-full relative cursor-pointer" @click="handleBannerClick(item)">
|
||||
<img :src="item.pic || item.img" class="w-full h-full object-cover" />
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/50 via-transparent to-transparent flex items-end p-8">
|
||||
<div>
|
||||
<span class="px-2.5 py-1 bg-primary text-white text-xs font-bold rounded-lg mb-2 inline-block">精选推荐</span>
|
||||
<h3 class="text-2xl font-bold text-white tracking-wide">{{ item.name || '大锤网络 · 数码狂欢' }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
</div>
|
||||
|
||||
<!-- Right: User Quick Widget & Bulletins -->
|
||||
<div class="col-span-3 bg-white rounded-2xl shadow-sm border border-gray-100 p-5 h-full flex flex-col justify-between">
|
||||
<!-- User Profile Brief -->
|
||||
<div class="text-center pb-4 border-b border-gray-100">
|
||||
<img
|
||||
:src="userStore.userInfo?.avatar || 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'"
|
||||
class="w-16 h-16 rounded-full mx-auto object-cover border-2 border-red-100 shadow-sm"
|
||||
/>
|
||||
<p class="text-sm font-bold text-gray-800 mt-2">
|
||||
{{ userStore.token ? (userStore.userInfo?.nickname || userStore.userInfo?.account || '尊贵会员') : 'Hi,欢迎来到 大锤网络' }}
|
||||
</p>
|
||||
<p class="text-xs text-gray-400 mt-0.5">正品大牌 · 全场包邮 · 售后无忧</p>
|
||||
|
||||
<div class="flex items-center justify-center gap-3 mt-3">
|
||||
<template v-if="userStore.token">
|
||||
<router-link to="/user" class="px-4 py-1.5 bg-red-50 text-primary text-xs font-semibold rounded-full hover:bg-red-100 transition-colors">
|
||||
个人中心
|
||||
</router-link>
|
||||
<router-link to="/cart" class="px-4 py-1.5 bg-gray-100 text-gray-700 text-xs font-semibold rounded-full hover:bg-gray-200 transition-colors">
|
||||
购物车 ({{ cartStore.cartCount }})
|
||||
</router-link>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button @click="userStore.openAuthModal('login')" class="px-4 py-1.5 bg-primary text-white text-xs font-bold rounded-full hover:bg-primary-hover transition-colors shadow-sm">
|
||||
登录
|
||||
</button>
|
||||
<button @click="userStore.openAuthModal('register')" class="px-4 py-1.5 bg-gray-100 text-gray-700 text-xs font-bold rounded-full hover:bg-gray-200 transition-colors">
|
||||
注册
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fast Features Icons -->
|
||||
<div class="grid grid-cols-3 gap-2 py-3 border-b border-gray-100 text-center">
|
||||
<router-link to="/user?tab=orders" class="p-2 rounded-xl hover:bg-gray-50 transition-colors flex flex-col items-center">
|
||||
<span class="text-lg">📦</span>
|
||||
<span class="text-[11px] text-gray-600 mt-1">待发货</span>
|
||||
</router-link>
|
||||
<router-link to="/user?tab=collect" class="p-2 rounded-xl hover:bg-gray-50 transition-colors flex flex-col items-center">
|
||||
<span class="text-lg">⭐</span>
|
||||
<span class="text-[11px] text-gray-600 mt-1">收藏夹</span>
|
||||
</router-link>
|
||||
<router-link to="/user?tab=coupons" class="p-2 rounded-xl hover:bg-gray-50 transition-colors flex flex-col items-center">
|
||||
<span class="text-lg">🎟️</span>
|
||||
<span class="text-[11px] text-gray-600 mt-1">优惠券</span>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- News / Bulletin -->
|
||||
<div>
|
||||
<div class="flex items-center justify-between text-xs font-bold text-gray-800 mb-2">
|
||||
<span>商城快讯</span>
|
||||
<router-link to="/news" class="text-gray-400 hover:text-primary font-normal">更多 ></router-link>
|
||||
</div>
|
||||
<ul class="space-y-1.5 text-xs text-gray-600">
|
||||
<li v-for="(item, idx) in newsList.slice(0, 3)" :key="idx" class="truncate">
|
||||
<router-link :to="`/news/${item.id}`" class="hover:text-primary transition-colors flex items-center space-x-1">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-primary flex-shrink-0"></span>
|
||||
<span class="truncate">{{ item.title }}</span>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Full-Width Banner (Visible on mobile/tablet) -->
|
||||
<div class="lg:hidden">
|
||||
<el-carousel trigger="click" height="175px" class="rounded-2xl overflow-hidden shadow-sm">
|
||||
<el-carousel-item v-for="(item, idx) in banners" :key="idx">
|
||||
<div class="w-full h-full relative cursor-pointer" @click="handleBannerClick(item)">
|
||||
<img :src="item.pic || item.img" class="w-full h-full object-cover" />
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent flex items-end p-4">
|
||||
<h3 class="text-sm font-bold text-white line-clamp-1">{{ item.name || '大锤网络 · 爆款数码狂欢' }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
|
||||
<!-- Mobile 8-Grid Quick Navigation -->
|
||||
<div class="grid grid-cols-4 gap-2.5 mt-3 bg-white p-3.5 rounded-2xl shadow-sm border border-gray-100 text-center">
|
||||
<router-link to="/rental" class="flex flex-col items-center space-y-1">
|
||||
<div class="w-10 h-10 rounded-xl bg-indigo-50 flex items-center justify-center text-lg">💻</div>
|
||||
<span class="text-[11px] font-bold text-gray-800">租电脑</span>
|
||||
</router-link>
|
||||
<router-link to="/rental" class="flex flex-col items-center space-y-1">
|
||||
<div class="w-10 h-10 rounded-xl bg-purple-50 flex items-center justify-center text-lg">📱</div>
|
||||
<span class="text-[11px] font-bold text-gray-800">租手机</span>
|
||||
</router-link>
|
||||
<router-link to="/rental" class="flex flex-col items-center space-y-1">
|
||||
<div class="w-10 h-10 rounded-xl bg-blue-50 flex items-center justify-center text-lg">📟</div>
|
||||
<span class="text-[11px] font-bold text-gray-800">租iPad</span>
|
||||
</router-link>
|
||||
<router-link to="/rental" class="flex flex-col items-center space-y-1">
|
||||
<div class="w-10 h-10 rounded-xl bg-orange-50 flex items-center justify-center text-lg">🎧</div>
|
||||
<span class="text-[11px] font-bold text-gray-800">租影音</span>
|
||||
</router-link>
|
||||
<router-link to="/products?type=4" class="flex flex-col items-center space-y-1">
|
||||
<div class="w-10 h-10 rounded-xl bg-red-50 flex items-center justify-center text-lg">⚡</div>
|
||||
<span class="text-[11px] font-medium text-gray-700">秒杀专区</span>
|
||||
</router-link>
|
||||
<router-link to="/products?type=1" class="flex flex-col items-center space-y-1">
|
||||
<div class="w-10 h-10 rounded-xl bg-emerald-50 flex items-center justify-center text-lg">🌟</div>
|
||||
<span class="text-[11px] font-medium text-gray-700">精品推荐</span>
|
||||
</router-link>
|
||||
<router-link to="/products?type=2" class="flex flex-col items-center space-y-1">
|
||||
<div class="w-10 h-10 rounded-xl bg-amber-50 flex items-center justify-center text-lg">🔥</div>
|
||||
<span class="text-[11px] font-medium text-gray-700">热门榜单</span>
|
||||
</router-link>
|
||||
<router-link to="/news" class="flex flex-col items-center space-y-1">
|
||||
<div class="w-10 h-10 rounded-xl bg-slate-50 flex items-center justify-center text-lg">📰</div>
|
||||
<span class="text-[11px] font-medium text-gray-700">商城资讯</span>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Equipment Rental Showcase (免押设备租赁专区) -->
|
||||
<section class="max-w-site mx-auto px-3 sm:px-4">
|
||||
<div class="bg-gradient-to-r from-slate-900 via-indigo-950 to-slate-900 rounded-2xl sm:rounded-3xl p-4 sm:p-7 text-white shadow-xl relative overflow-hidden">
|
||||
<!-- Glow effects -->
|
||||
<div class="absolute -right-10 -top-10 w-72 h-72 bg-indigo-500/20 rounded-full blur-3xl pointer-events-none"></div>
|
||||
|
||||
<div class="flex items-center justify-between pb-4 sm:pb-6 border-b border-indigo-900/60 gap-2">
|
||||
<div class="space-y-0.5 sm:space-y-1">
|
||||
<div class="flex items-center space-x-2 sm:space-x-3">
|
||||
<span class="px-2 py-0.5 bg-gradient-to-r from-red-500 to-orange-500 text-white font-extrabold text-[10px] sm:text-xs rounded-md shadow-sm">
|
||||
全新免押模式
|
||||
</span>
|
||||
<h2 class="text-base sm:text-2xl font-black tracking-tight text-white flex items-center">
|
||||
💻 数码设备租赁专区
|
||||
</h2>
|
||||
</div>
|
||||
<p class="hidden sm:block text-xs text-indigo-200/70">
|
||||
免押金租电脑、旗舰手机、iPad平板 · 随租随还 · 企个两用 · 顺丰速达 · 租完即送
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<router-link
|
||||
to="/rental"
|
||||
class="px-3 sm:px-5 py-1.5 sm:py-2 bg-indigo-600 hover:bg-indigo-500 text-white font-bold text-xs rounded-xl shadow-md shadow-indigo-600/30 transition-all whitespace-nowrap"
|
||||
>
|
||||
<span>进入大厅 ></span>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- Rental Items Grid (2 cols on mobile, 4 on desktop) -->
|
||||
<div class="grid grid-cols-2 lg:grid-cols-4 gap-2.5 sm:gap-4 mt-4 sm:mt-6">
|
||||
<div
|
||||
v-for="item in featuredRentals"
|
||||
:key="item.id"
|
||||
class="bg-white/10 backdrop-blur-md rounded-xl sm:rounded-2xl p-2.5 sm:p-4 border border-white/15 hover:border-indigo-400 hover:bg-white/15 transition-all flex flex-col justify-between group"
|
||||
>
|
||||
<div>
|
||||
<div class="relative aspect-video rounded-lg sm:rounded-xl overflow-hidden bg-slate-800/80 mb-2 sm:mb-3">
|
||||
<img :src="item.image" class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300" />
|
||||
<span class="absolute top-1.5 left-1.5 px-1.5 py-0.5 text-[9px] sm:text-[10px] font-bold bg-indigo-600 text-white rounded shadow-sm">
|
||||
{{ item.tag }}
|
||||
</span>
|
||||
<span class="absolute bottom-1.5 right-1.5 px-1.5 py-0.5 text-[9px] sm:text-[10px] bg-black/60 text-white rounded backdrop-blur-sm">
|
||||
{{ item.condition }}
|
||||
</span>
|
||||
</div>
|
||||
<h3 class="text-xs font-bold text-white line-clamp-1 group-hover:text-indigo-300 transition-colors">
|
||||
{{ item.title }}
|
||||
</h3>
|
||||
<p class="hidden sm:block text-[11px] text-gray-300 line-clamp-1 mt-1">{{ item.desc }}</p>
|
||||
</div>
|
||||
|
||||
<div class="pt-2 sm:pt-3 mt-2 sm:mt-3 border-t border-white/10 flex items-center justify-between">
|
||||
<div>
|
||||
<span class="text-[9px] sm:text-[10px] text-gray-400">低至 </span>
|
||||
<span class="text-sm sm:text-lg font-black text-orange-400">¥{{ item.dailyPrice }}</span>
|
||||
<span class="text-[9px] sm:text-[10px] text-gray-300">/天</span>
|
||||
</div>
|
||||
<router-link
|
||||
to="/rental"
|
||||
class="px-2 sm:px-3 py-1 sm:py-1.5 bg-gradient-to-r from-red-500 to-orange-500 text-white text-[10px] sm:text-[11px] font-bold rounded-lg transition-all"
|
||||
>
|
||||
立即租
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Flash Sale / Hot Section -->
|
||||
<section class="max-w-site mx-auto px-3 sm:px-4">
|
||||
<div class="bg-gradient-to-r from-red-600 to-orange-500 rounded-2xl p-3.5 sm:p-6 text-white shadow-lg">
|
||||
<div class="flex items-center justify-between mb-3 sm:mb-5">
|
||||
<div class="flex items-center space-x-2 sm:space-x-3">
|
||||
<span class="text-base sm:text-2xl font-black tracking-tight flex items-center">
|
||||
⚡ 限时秒杀 / 特惠抢购
|
||||
</span>
|
||||
<span class="hidden sm:inline-block bg-white/20 text-xs px-2.5 py-1 rounded-full backdrop-blur-sm">爆款抄底价</span>
|
||||
</div>
|
||||
<router-link to="/products?type=4" class="text-[11px] sm:text-xs bg-white text-primary font-bold px-3 sm:px-4 py-1 sm:py-1.5 rounded-full hover:bg-red-50 transition-colors shadow-sm">
|
||||
查看更多 >
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- Flash sale product cards -->
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-2.5 sm:gap-4">
|
||||
<ProductCard
|
||||
v-for="item in benefitProducts.slice(0, 5)"
|
||||
:key="item.id"
|
||||
:product="item"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Tabbed Recommendations (精品推荐 / 热门榜单 / 首发新品) -->
|
||||
<section class="max-w-site mx-auto px-3 sm:px-4">
|
||||
<div class="bg-white rounded-2xl p-3.5 sm:p-6 shadow-sm border border-gray-100">
|
||||
<div class="flex items-center justify-between mb-4 sm:mb-6 pb-3 sm:pb-4 border-b border-gray-100">
|
||||
<div class="flex items-center space-x-3 sm:space-x-8">
|
||||
<h3 class="text-base sm:text-xl font-bold text-gray-900">优选精选</h3>
|
||||
<div class="flex items-center space-x-2 sm:space-x-4 text-xs sm:text-sm font-semibold">
|
||||
<button
|
||||
@click="switchRecommendTab(1)"
|
||||
:class="recommendTab === 1 ? 'text-primary border-b-2 border-primary pb-1' : 'text-gray-400 hover:text-gray-700'"
|
||||
>
|
||||
精品推荐
|
||||
</button>
|
||||
<button
|
||||
@click="switchRecommendTab(2)"
|
||||
:class="recommendTab === 2 ? 'text-primary border-b-2 border-primary pb-1' : 'text-gray-400 hover:text-gray-700'"
|
||||
>
|
||||
热门榜单
|
||||
</button>
|
||||
<button
|
||||
@click="switchRecommendTab(3)"
|
||||
:class="recommendTab === 3 ? 'text-primary border-b-2 border-primary pb-1' : 'text-gray-400 hover:text-gray-700'"
|
||||
>
|
||||
首发新品
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<router-link :to="`/products?type=${recommendTab}`" class="text-xs text-primary font-semibold hover:underline">
|
||||
全部 >
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div v-loading="recommendLoading" class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-2.5 sm:gap-4 min-h-[260px]">
|
||||
<ProductCard
|
||||
v-for="item in recommendList"
|
||||
:key="item.id"
|
||||
:product="item"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Category Floors -->
|
||||
<section v-for="floor in categoryFloors" :key="floor.id" class="max-w-site mx-auto px-3 sm:px-4">
|
||||
<div class="bg-white rounded-2xl p-3.5 sm:p-6 shadow-sm border border-gray-100">
|
||||
<div class="flex items-center justify-between mb-3 sm:mb-6">
|
||||
<div class="flex items-center space-x-2 sm:space-x-3">
|
||||
<div class="w-2 sm:w-2.5 h-4 sm:h-6 bg-primary rounded-full"></div>
|
||||
<h3 class="text-sm sm:text-xl font-bold text-gray-900">{{ floor.cate_name }}</h3>
|
||||
</div>
|
||||
<router-link :to="`/products?cid=${floor.id}`" class="text-xs text-gray-400 hover:text-primary font-medium">
|
||||
更多 >
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-2.5 sm:gap-4">
|
||||
<ProductCard
|
||||
v-for="item in (floor.productList || floor.products || []).slice(0, 5)"
|
||||
:key="item.id"
|
||||
:product="item"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { useCartStore } from '@/store/cart'
|
||||
import ProductCard from '@/components/ProductCard.vue'
|
||||
import {
|
||||
getBanner,
|
||||
getCategory,
|
||||
getRecommendList,
|
||||
getCategoryProduct,
|
||||
getNewsList
|
||||
} from '@/api'
|
||||
|
||||
import {
|
||||
mockCategories,
|
||||
mockBanners,
|
||||
mockProducts,
|
||||
mockRecommendProducts
|
||||
} from '@/api/mock'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const cartStore = useCartStore()
|
||||
|
||||
const banners = ref(mockBanners)
|
||||
|
||||
const featuredRentals = ref([
|
||||
{
|
||||
id: 1,
|
||||
title: 'MacBook Pro 14英寸 M3 Max 芯片 / 36G / 1TB 剪辑办公本',
|
||||
desc: '剪辑渲染办公神器 · 顺丰包邮 · 芝麻免押',
|
||||
dailyPrice: 18.8,
|
||||
image: 'https://images.unsplash.com/photo-1517336714731-489689fd1ca8?auto=format&fit=crop&w=600&q=80',
|
||||
tag: '租电脑',
|
||||
condition: '99新'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Apple iPad Pro 13英寸 M4芯片 256G 深空黑 平板电脑',
|
||||
desc: '原封/99新 · 双层OLED屏 · 支持二代Pencil',
|
||||
dailyPrice: 8.5,
|
||||
image: 'https://images.unsplash.com/photo-1544244015-0df4b3ffc6b0?auto=format&fit=crop&w=600&q=80',
|
||||
tag: '租iPad',
|
||||
condition: '全新'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'iPhone 15 Pro Max 256GB 原色钛金属 5G旗舰手机',
|
||||
desc: 'A17 Pro芯片 · 5倍光学变焦 · 随租随还',
|
||||
dailyPrice: 6.8,
|
||||
image: 'https://images.unsplash.com/photo-1695048133142-1a20484d2569?auto=format&fit=crop&w=600&q=80',
|
||||
tag: '租手机',
|
||||
condition: '99新'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: '索尼 WH-1000XM5 无线降噪头戴式耳机 铂金银',
|
||||
desc: '旗舰主动降噪 · 会议办公/差旅必备',
|
||||
dailyPrice: 3.5,
|
||||
image: 'https://images.unsplash.com/photo-1505740420928-5e560c06d30e?auto=format&fit=crop&w=600&q=80',
|
||||
tag: '租影音',
|
||||
condition: '99新'
|
||||
}
|
||||
])
|
||||
|
||||
const categories = ref(mockCategories)
|
||||
const hoverCat = ref(null)
|
||||
const benefitProducts = ref(mockProducts.slice(0, 5))
|
||||
const recommendTab = ref(1)
|
||||
const recommendList = ref(mockProducts.slice(0, 5))
|
||||
const recommendLoading = ref(false)
|
||||
const categoryFloors = ref([
|
||||
{ id: 1, cate_name: '💻 电脑整机 / 高性能工作站', productList: mockProducts.slice(0, 4) },
|
||||
{ id: 2, cate_name: '📱 智能手机 / 旗舰数码', productList: mockProducts.slice(2, 6) }
|
||||
])
|
||||
const newsList = ref([
|
||||
{ id: 1, title: '关于 大锤网络 开启全国免押数码设备租赁业务的通知' },
|
||||
{ id: 2, title: '数码设备长租与买断方案深度解读:怎样租最划算?' }
|
||||
])
|
||||
|
||||
const fetchBanners = async () => {
|
||||
try {
|
||||
const res = await getBanner()
|
||||
if (res && res.list && res.list.length) {
|
||||
banners.value = res.list
|
||||
}
|
||||
} catch (e) {
|
||||
// keep fallback
|
||||
}
|
||||
}
|
||||
|
||||
const fetchCategories = async () => {
|
||||
try {
|
||||
const res = await getCategory()
|
||||
if (Array.isArray(res) && res.length) {
|
||||
categories.value = res
|
||||
}
|
||||
} catch (e) {
|
||||
// keep fallback
|
||||
}
|
||||
}
|
||||
|
||||
const switchRecommendTab = async (type) => {
|
||||
recommendTab.value = type
|
||||
recommendLoading.value = true
|
||||
try {
|
||||
const res = await getRecommendList(type)
|
||||
if (res && res.list && res.list.length) {
|
||||
recommendList.value = res.list
|
||||
} else {
|
||||
recommendList.value = mockRecommendProducts(type)
|
||||
}
|
||||
} catch (e) {
|
||||
recommendList.value = mockRecommendProducts(type)
|
||||
} finally {
|
||||
recommendLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const fetchFloors = async () => {
|
||||
try {
|
||||
const res = await getCategoryProduct()
|
||||
if (res && res.list && res.list.length) {
|
||||
categoryFloors.value = res.list
|
||||
} else if (Array.isArray(res) && res.length) {
|
||||
categoryFloors.value = res
|
||||
}
|
||||
} catch (e) {
|
||||
// keep fallback
|
||||
}
|
||||
}
|
||||
|
||||
const fetchBenefitProducts = async () => {
|
||||
try {
|
||||
const res = await getRecommendList(4) // 4: 促销特惠
|
||||
if (res && res.list && res.list.length) {
|
||||
benefitProducts.value = res.list
|
||||
} else {
|
||||
benefitProducts.value = mockRecommendProducts(4)
|
||||
}
|
||||
} catch (e) {
|
||||
benefitProducts.value = mockRecommendProducts(4)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchNews = async () => {
|
||||
try {
|
||||
const res = await getNewsList({ page: 1, limit: 5 })
|
||||
if (res && res.list && res.list.length) {
|
||||
newsList.value = res.list
|
||||
}
|
||||
} catch (e) {
|
||||
// keep fallback
|
||||
}
|
||||
}
|
||||
|
||||
const handleBannerClick = (item) => {
|
||||
if (item.url) {
|
||||
window.location.href = item.url
|
||||
} else {
|
||||
router.push('/products')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchBanners()
|
||||
fetchCategories()
|
||||
switchRecommendTab(1)
|
||||
fetchBenefitProducts()
|
||||
fetchFloors()
|
||||
fetchNews()
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<div class="max-w-site mx-auto px-4 py-8 space-y-6">
|
||||
<div class="flex items-center space-x-2 text-xs text-gray-500">
|
||||
<router-link to="/" class="hover:text-primary">首页</router-link>
|
||||
<span>/</span>
|
||||
<router-link to="/news" class="hover:text-primary">商城资讯</router-link>
|
||||
<span>/</span>
|
||||
<span class="text-gray-800 font-medium truncate max-w-xs">{{ detail.title }}</span>
|
||||
</div>
|
||||
|
||||
<div v-loading="loading" class="bg-white rounded-2xl p-10 shadow-sm border border-gray-100 max-w-4xl mx-auto space-y-6">
|
||||
<div class="text-center pb-6 border-b border-gray-100 space-y-3">
|
||||
<h1 class="text-2xl font-black text-gray-900 leading-snug">{{ detail.title }}</h1>
|
||||
<div class="text-xs text-gray-400 space-x-4">
|
||||
<span>发布时间:{{ detail.add_time || '近期' }}</span>
|
||||
<span>浏览量:{{ detail.visit || 1 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="detail.content"
|
||||
v-html="detail.content"
|
||||
class="prose prose-red max-w-none text-sm text-gray-700 leading-relaxed rich-content"
|
||||
></div>
|
||||
|
||||
<div class="pt-8 border-t border-gray-100 text-center">
|
||||
<router-link to="/news" class="px-6 py-2 bg-gray-100 text-gray-700 hover:bg-gray-200 text-xs font-semibold rounded-lg transition-colors">
|
||||
< 返回资讯列表
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { getNewsDetail } from '@/api'
|
||||
|
||||
const route = useRoute()
|
||||
const detail = ref({})
|
||||
const loading = ref(false)
|
||||
|
||||
const fetchDetail = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getNewsDetail(route.params.id)
|
||||
detail.value = res || {}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.rich-content :deep(img) {
|
||||
max-width: 100% !important;
|
||||
height: auto !important;
|
||||
border-radius: 12px;
|
||||
margin: 16px auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<div class="max-w-site mx-auto px-4 py-8 space-y-6">
|
||||
<div class="flex items-center space-x-2 text-xs text-gray-500">
|
||||
<router-link to="/" class="hover:text-primary">首页</router-link>
|
||||
<span>/</span>
|
||||
<span class="text-gray-800 font-medium">商城资讯</span>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-2xl p-8 shadow-sm border border-gray-100 space-y-6">
|
||||
<h1 class="text-2xl font-bold text-gray-900 pb-4 border-b border-gray-100">
|
||||
📰 商城快讯 & 官方公告
|
||||
</h1>
|
||||
|
||||
<div v-loading="loading" class="space-y-6">
|
||||
<div
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
@click="router.push(`/news/${item.id}`)"
|
||||
class="flex items-center justify-between p-4 rounded-2xl border border-gray-100 hover:border-red-200 hover:bg-red-50/20 transition-all cursor-pointer group"
|
||||
>
|
||||
<div class="flex items-center space-x-4">
|
||||
<img v-if="item.image_input" :src="item.image_input" class="w-24 h-16 rounded-xl object-cover" />
|
||||
<div class="space-y-1">
|
||||
<h3 class="text-sm font-bold text-gray-800 group-hover:text-primary transition-colors">
|
||||
{{ item.title }}
|
||||
</h3>
|
||||
<p class="text-xs text-gray-400 line-clamp-1">{{ item.synopsis || '查看详细资讯内容...' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-xs text-gray-400 flex-shrink-0">{{ item.add_time || '近期发布' }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="list.length === 0 && !loading" class="text-center py-16 text-gray-400 text-sm">
|
||||
暂无相关资讯
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { getNewsList } from '@/api'
|
||||
|
||||
const router = useRouter()
|
||||
const list = ref([
|
||||
{
|
||||
id: 1,
|
||||
title: '关于 大锤网络 开启全国免押数码设备租赁业务的通知',
|
||||
synopsis: '支持芝麻信用免押金快速起租,包含苹果MacBook、旗舰手机及平板...',
|
||||
add_time: '2026-08-19'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '数码设备长租与买断方案深度解读:怎样租最划算?',
|
||||
synopsis: '全品类数码设备租满 12 个月即可 0 元买断...',
|
||||
add_time: '2026-08-18'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '企业办公设备批量租赁服务升级,尊享专属客户经理与上门维保',
|
||||
synopsis: '为中小微企业量身定制办公电脑解决方案,降低一次性固定资产投入...',
|
||||
add_time: '2026-08-15'
|
||||
}
|
||||
])
|
||||
const loading = ref(false)
|
||||
|
||||
const fetchNews = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getNewsList({ page: 1, limit: 20 })
|
||||
if (res && res.list && res.list.length) {
|
||||
list.value = res.list
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchNews()
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,463 @@
|
||||
<template>
|
||||
<div class="max-w-site mx-auto px-3 sm:px-4 py-3 sm:py-6 space-y-4 sm:space-y-6 pb-24 lg:pb-6">
|
||||
<!-- Breadcrumb -->
|
||||
<div class="text-xs text-gray-500 flex items-center space-x-2">
|
||||
<router-link to="/" class="hover:text-primary">首页</router-link>
|
||||
<span>/</span>
|
||||
<router-link to="/products" class="hover:text-primary">全部商品</router-link>
|
||||
<span>/</span>
|
||||
<span class="text-gray-800 font-medium truncate max-w-[180px] sm:max-w-xs">{{ storeInfo.store_name }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Main Detail Top Card -->
|
||||
<div v-loading="loading" class="bg-white rounded-2xl p-4 sm:p-8 shadow-sm border border-gray-100 grid grid-cols-1 lg:grid-cols-12 gap-5 lg:gap-10">
|
||||
<!-- Left Gallery -->
|
||||
<div class="lg:col-span-5 space-y-3 sm:space-y-4">
|
||||
<!-- Main Image -->
|
||||
<div class="aspect-square rounded-2xl overflow-hidden bg-gray-50 border border-gray-100 relative group">
|
||||
<img
|
||||
:src="currentImage || storeInfo.image"
|
||||
:alt="storeInfo.store_name"
|
||||
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
|
||||
/>
|
||||
</div>
|
||||
<!-- Thumbnails list -->
|
||||
<div v-if="sliderImages.length > 1" class="flex space-x-2.5 sm:space-x-3 overflow-x-auto pb-1">
|
||||
<button
|
||||
v-for="(img, idx) in sliderImages"
|
||||
:key="idx"
|
||||
@mouseenter="currentImage = img"
|
||||
@click="currentImage = img"
|
||||
:class="currentImage === img ? 'border-primary ring-2 ring-red-100' : 'border-gray-200 hover:border-gray-400'"
|
||||
class="w-14 h-14 sm:w-16 sm:h-16 rounded-xl overflow-hidden border-2 flex-shrink-0 bg-gray-50 transition-all"
|
||||
>
|
||||
<img :src="img" class="w-full h-full object-cover" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Product Specs & Actions -->
|
||||
<div class="lg:col-span-7 flex flex-col justify-between space-y-4 sm:space-y-6">
|
||||
<div>
|
||||
<!-- Title & Badges -->
|
||||
<div class="flex items-start justify-between gap-3 sm:gap-4">
|
||||
<h1 class="text-lg sm:text-2xl font-bold text-gray-900 leading-snug">
|
||||
{{ storeInfo.store_name }}
|
||||
</h1>
|
||||
<!-- Collect button -->
|
||||
<button
|
||||
@click="toggleCollect"
|
||||
:class="userCollect ? 'text-primary bg-red-50' : 'text-gray-400 hover:text-gray-600 bg-gray-50'"
|
||||
class="flex items-center space-x-1 px-2.5 sm:px-3 py-1.5 rounded-full text-xs font-semibold flex-shrink-0 transition-colors"
|
||||
>
|
||||
<span>{{ userCollect ? '❤️ 已藏' : '🤍 收藏' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p v-if="storeInfo.store_info" class="text-xs sm:text-sm text-gray-500 mt-1.5 sm:mt-2">
|
||||
{{ storeInfo.store_info }}
|
||||
</p>
|
||||
|
||||
<!-- Mode Switcher: Buy vs Rent -->
|
||||
<div class="flex items-center space-x-2 sm:space-x-3 p-1 bg-gray-100 rounded-xl my-3 sm:my-4 text-xs font-bold">
|
||||
<button
|
||||
@click="tradeMode = 'buy'"
|
||||
:class="tradeMode === 'buy' ? 'bg-white text-gray-900 shadow-sm' : 'text-gray-500 hover:text-gray-800'"
|
||||
class="flex-1 py-1.5 sm:py-2 rounded-lg transition-all flex items-center justify-center space-x-1"
|
||||
>
|
||||
<span>🛒 现货全款买</span>
|
||||
</button>
|
||||
<button
|
||||
@click="tradeMode = 'rent'"
|
||||
:class="tradeMode === 'rent' ? 'bg-gradient-to-r from-indigo-600 to-purple-600 text-white shadow-sm' : 'text-gray-500 hover:text-gray-800'"
|
||||
class="flex-1 py-1.5 sm:py-2 rounded-lg transition-all flex items-center justify-center space-x-1"
|
||||
>
|
||||
<span>💻 信用免押租</span>
|
||||
<span class="bg-yellow-400 text-gray-900 text-[9px] sm:text-[10px] px-1 py-0.2 rounded font-black">免押</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Price Box (Buy Mode) -->
|
||||
<div v-if="tradeMode === 'buy'" class="bg-red-50/60 rounded-xl sm:rounded-2xl p-3.5 sm:p-5 mt-2 sm:mt-4 flex items-baseline justify-between">
|
||||
<div class="flex items-baseline space-x-1.5 sm:space-x-2">
|
||||
<span class="text-xs sm:text-sm font-bold text-primary">¥</span>
|
||||
<span class="text-2xl sm:text-3xl font-extrabold text-primary tracking-tight">
|
||||
{{ selectedSku ? selectedSku.price : storeInfo.price }}
|
||||
</span>
|
||||
<span v-if="storeInfo.ot_price" class="text-xs text-gray-400 line-through ml-2">
|
||||
¥{{ selectedSku ? selectedSku.ot_price || storeInfo.ot_price : storeInfo.ot_price }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-[11px] sm:text-xs text-gray-500 space-x-2 sm:space-x-4">
|
||||
<span>已售 <strong>{{ storeInfo.sales || 0 }}</strong> {{ storeInfo.unit_name || '件' }}</span>
|
||||
<span>库存 <strong>{{ selectedSku ? selectedSku.stock : storeInfo.stock }}</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Price Box (Rent Mode) -->
|
||||
<div v-else class="bg-indigo-50/80 border border-indigo-100 rounded-xl sm:rounded-2xl p-3.5 sm:p-5 mt-2 sm:mt-4 space-y-2.5 sm:space-y-3">
|
||||
<div class="flex items-baseline justify-between">
|
||||
<div>
|
||||
<span class="text-[11px] sm:text-xs text-gray-500">日租金低至 </span>
|
||||
<span class="text-xs sm:text-sm font-bold text-primary">¥</span>
|
||||
<span class="text-2xl sm:text-3xl font-extrabold text-primary tracking-tight">
|
||||
{{ (Math.max(2.8, (selectedSku ? selectedSku.price : storeInfo.price || 1999) * 0.002)).toFixed(1) }}
|
||||
</span>
|
||||
<span class="text-xs text-primary font-bold">/天</span>
|
||||
<span class="text-[11px] sm:text-xs text-gray-500 ml-2">月租约 ¥{{ (Math.max(80, (selectedSku ? selectedSku.price : storeInfo.price || 1999) * 0.05)).toFixed(0) }}</span>
|
||||
</div>
|
||||
<span class="px-2 py-0.5 sm:px-2.5 sm:py-1 bg-indigo-600 text-white text-[10px] sm:text-[11px] font-bold rounded-lg shadow-sm">
|
||||
芝麻分 ≥ 650 免押
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Lease Term options -->
|
||||
<div class="space-y-1.5 pt-2 border-t border-indigo-100 text-xs">
|
||||
<span class="font-bold text-gray-700">租赁周期:</span>
|
||||
<div class="grid grid-cols-4 gap-1.5 sm:gap-2">
|
||||
<button
|
||||
v-for="d in rentDurations"
|
||||
:key="d.days"
|
||||
@click="rentDays = d.days"
|
||||
:class="rentDays === d.days ? 'bg-indigo-600 text-white font-bold' : 'bg-white text-gray-700 border border-gray-200'"
|
||||
class="py-1 px-1.5 sm:py-1.5 sm:px-2 rounded-lg text-center text-[11px] sm:text-xs transition-all"
|
||||
>
|
||||
<div>{{ d.label }}</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SKU Attributes Selection -->
|
||||
<div v-if="productAttr && productAttr.length" class="mt-4 sm:mt-6 space-y-3 sm:space-y-4">
|
||||
<div v-for="(attr, aIdx) in productAttr" :key="aIdx" class="space-y-1.5 sm:space-y-2">
|
||||
<span class="text-xs font-semibold text-gray-700">{{ attr.attr_name }}:</span>
|
||||
<div class="flex flex-wrap gap-1.5 sm:gap-2">
|
||||
<button
|
||||
v-for="(val, vIdx) in attr.attr_values"
|
||||
:key="vIdx"
|
||||
@click="selectAttrValue(attr.attr_name, val)"
|
||||
:class="selectedAttrs[attr.attr_name] === val ? 'bg-primary text-white border-primary shadow-sm' : 'bg-gray-50 text-gray-700 border-gray-200 hover:border-gray-400'"
|
||||
class="px-3 sm:px-4 py-1.5 sm:py-2 text-xs font-medium rounded-xl border transition-all"
|
||||
>
|
||||
{{ val }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quantity Stepper (Buy Mode) -->
|
||||
<div v-if="tradeMode === 'buy'" class="mt-4 sm:mt-6 flex items-center space-x-3 sm:space-x-4">
|
||||
<span class="text-xs font-semibold text-gray-700">数量:</span>
|
||||
<el-input-number
|
||||
v-model="cartNum"
|
||||
:min="1"
|
||||
:max="selectedSku ? selectedSku.stock : storeInfo.stock || 999"
|
||||
size="default"
|
||||
/>
|
||||
<span class="text-xs text-gray-400">
|
||||
(库存 {{ storeInfo.stock || 0 }} {{ storeInfo.unit_name || '件' }})
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Desktop Action Buttons (Buy Mode) -->
|
||||
<div v-if="tradeMode === 'buy'" class="hidden lg:flex pt-6 border-t border-gray-100 items-center space-x-4">
|
||||
<button
|
||||
@click="handleAddToCart"
|
||||
class="flex-1 py-3.5 bg-red-100 hover:bg-red-200 text-primary font-bold text-sm rounded-xl transition-colors flex items-center justify-center space-x-2"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z"></path></svg>
|
||||
<span>加入购物车</span>
|
||||
</button>
|
||||
<button
|
||||
@click="handleBuyNow"
|
||||
class="flex-1 py-3.5 bg-primary hover:bg-primary-hover text-white font-bold text-sm rounded-xl shadow-lg shadow-red-200 transition-all flex items-center justify-center space-x-2"
|
||||
>
|
||||
<span>立即购买</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Desktop Action Buttons (Rent Mode) -->
|
||||
<div v-else class="hidden lg:flex pt-6 border-t border-gray-100 items-center space-x-4">
|
||||
<button
|
||||
@click="handleApplyRental"
|
||||
class="flex-1 py-3.5 bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-700 hover:to-purple-700 text-white font-bold text-sm rounded-xl shadow-lg shadow-indigo-200 transition-all flex items-center justify-center space-x-2"
|
||||
>
|
||||
<span>⚡ 立即申请免押租赁 ({{ rentDays }}天)</span>
|
||||
</button>
|
||||
<router-link
|
||||
to="/rental"
|
||||
class="px-6 py-3.5 bg-gray-100 hover:bg-gray-200 text-gray-700 font-bold text-sm rounded-xl transition-colors"
|
||||
>
|
||||
更多机型
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Fixed Bottom Action Bar -->
|
||||
<div class="lg:hidden fixed bottom-0 left-0 right-0 z-40 bg-white/95 backdrop-blur-md border-t border-gray-200 px-3 py-2 flex items-center space-x-2 shadow-2xl safe-bottom">
|
||||
<router-link to="/cart" class="flex flex-col items-center justify-center text-gray-600 px-2 relative">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z"></path></svg>
|
||||
<span class="text-[10px] mt-0.5">购物车</span>
|
||||
<span v-if="cartStore.cartCount > 0" class="absolute -top-1 -right-0.5 bg-primary text-white text-[9px] px-1 rounded-full font-bold">
|
||||
{{ cartStore.cartCount }}
|
||||
</span>
|
||||
</router-link>
|
||||
|
||||
<template v-if="tradeMode === 'buy'">
|
||||
<button
|
||||
@click="handleAddToCart"
|
||||
class="flex-1 py-2.5 bg-red-100 text-primary font-bold text-xs rounded-xl"
|
||||
>
|
||||
加入购物车
|
||||
</button>
|
||||
<button
|
||||
@click="handleBuyNow"
|
||||
class="flex-1 py-2.5 bg-primary text-white font-bold text-xs rounded-xl shadow-md"
|
||||
>
|
||||
立即购买
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button
|
||||
@click="handleApplyRental"
|
||||
class="flex-1 py-2.5 bg-gradient-to-r from-indigo-600 to-purple-600 text-white font-bold text-xs rounded-xl shadow-md"
|
||||
>
|
||||
⚡ 立即申请免押租赁 ({{ rentDays }}天)
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Product Description & Reviews Tabbed Section -->
|
||||
<div class="bg-white rounded-2xl p-4 sm:p-8 shadow-sm border border-gray-100">
|
||||
<el-tabs v-model="activeTab" class="product-tabs">
|
||||
<!-- Tab 1: Description -->
|
||||
<el-tab-pane label="商品详情介绍" name="desc">
|
||||
<div class="pt-4 max-w-3xl mx-auto">
|
||||
<div
|
||||
v-if="storeInfo.description"
|
||||
v-html="storeInfo.description"
|
||||
class="prose prose-red max-w-none text-sm text-gray-700 leading-relaxed rich-content"
|
||||
></div>
|
||||
<div v-else class="text-center py-12 text-gray-400 text-sm">
|
||||
暂无图文详情
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- Tab 2: Reviews -->
|
||||
<el-tab-pane :label="`用户评价 (${replyCount})`" name="replies">
|
||||
<div class="pt-4 space-y-6">
|
||||
<!-- Review Summary Header -->
|
||||
<div class="bg-gray-50 rounded-2xl p-6 flex items-center justify-between">
|
||||
<div>
|
||||
<span class="text-xs text-gray-500 font-medium">好评率</span>
|
||||
<p class="text-3xl font-black text-primary mt-1">{{ replyRate }}%</p>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 space-x-4">
|
||||
<span>总评价:{{ replyCount }}</span>
|
||||
<span>好评:{{ replyConfig.good_count || 0 }}</span>
|
||||
<span>差评:{{ replyConfig.poor_count || 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Review Items List -->
|
||||
<div v-if="replyList.length" class="space-y-4">
|
||||
<div
|
||||
v-for="item in replyList"
|
||||
:key="item.id"
|
||||
class="p-5 rounded-2xl border border-gray-100 hover:border-gray-200 transition-colors space-y-3"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center space-x-3">
|
||||
<img :src="item.avatar || 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'" class="w-8 h-8 rounded-full object-cover" />
|
||||
<div>
|
||||
<h5 class="text-xs font-bold text-gray-800">{{ item.nickname }}</h5>
|
||||
<p class="text-[10px] text-gray-400">{{ item.add_time }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<el-rate :model-value="item.product_score || 5" disabled text-color="#ff9900" size="small" />
|
||||
</div>
|
||||
<p class="text-xs text-gray-700 leading-relaxed">{{ item.comment }}</p>
|
||||
<!-- Review Pictures -->
|
||||
<div v-if="item.pics && item.pics.length" class="flex gap-2">
|
||||
<img
|
||||
v-for="(p, pIdx) in item.pics"
|
||||
:key="pIdx"
|
||||
:src="p"
|
||||
class="w-16 h-16 rounded-lg object-cover border border-gray-100"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="text-center py-12 text-gray-400 text-sm">
|
||||
暂无用户评价
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { useCartStore } from '@/store/cart'
|
||||
import { getProductDetail, getReplyList, getReplyConfig, addCollect, delCollect } from '@/api'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const cartStore = useCartStore()
|
||||
|
||||
const productId = route.params.id
|
||||
const loading = ref(false)
|
||||
const storeInfo = ref({})
|
||||
const sliderImages = ref([])
|
||||
const currentImage = ref('')
|
||||
const productAttr = ref([])
|
||||
const productValue = ref({})
|
||||
const selectedAttrs = ref({})
|
||||
const cartNum = ref(1)
|
||||
const userCollect = ref(false)
|
||||
const activeTab = ref('desc')
|
||||
|
||||
const tradeMode = ref('buy') // 'buy' | 'rent'
|
||||
const rentDays = ref(30)
|
||||
const rentDurations = [
|
||||
{ label: '7 天试用', days: 7 },
|
||||
{ label: '30 天月租', days: 30 },
|
||||
{ label: '90 天季租', days: 90 },
|
||||
{ label: '365 天年租', days: 365 }
|
||||
]
|
||||
|
||||
const replyList = ref([])
|
||||
const replyCount = ref(0)
|
||||
const replyRate = ref('100')
|
||||
const replyConfig = ref({})
|
||||
|
||||
// Find the corresponding SKU in productValue
|
||||
const selectedSku = computed(() => {
|
||||
if (!productAttr.value || productAttr.value.length === 0) {
|
||||
return null
|
||||
}
|
||||
const key = Object.values(selectedAttrs.value).join(',')
|
||||
return productValue.value[key] || null
|
||||
})
|
||||
|
||||
const selectAttrValue = (attrName, val) => {
|
||||
selectedAttrs.value[attrName] = val
|
||||
if (selectedSku.value && selectedSku.value.image) {
|
||||
currentImage.value = selectedSku.value.image
|
||||
}
|
||||
}
|
||||
|
||||
const fetchDetail = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getProductDetail(productId)
|
||||
storeInfo.value = res.storeInfo || {}
|
||||
sliderImages.value = res.storeInfo?.slider_image || [res.storeInfo?.image]
|
||||
currentImage.value = sliderImages.value[0] || ''
|
||||
productAttr.value = res.productAttr || []
|
||||
productValue.value = res.productValue || {}
|
||||
userCollect.value = !!res.userCollect
|
||||
|
||||
// Init default selection for attributes
|
||||
if (productAttr.value.length > 0) {
|
||||
const initial = {}
|
||||
productAttr.value.forEach(attr => {
|
||||
if (attr.attr_values && attr.attr_values.length) {
|
||||
initial[attr.attr_name] = attr.attr_values[0]
|
||||
}
|
||||
})
|
||||
selectedAttrs.value = initial
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const fetchReplies = async () => {
|
||||
try {
|
||||
const [cfgRes, listRes] = await Promise.all([
|
||||
getReplyConfig(productId),
|
||||
getReplyList(productId, { page: 1, limit: 10, type: 0 })
|
||||
])
|
||||
replyConfig.value = cfgRes || {}
|
||||
replyRate.value = cfgRes?.reply_chance || '100'
|
||||
replyCount.value = cfgRes?.sum_count || 0
|
||||
replyList.value = listRes || []
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
const toggleCollect = async () => {
|
||||
if (!userStore.token) {
|
||||
userStore.openAuthModal('login')
|
||||
return
|
||||
}
|
||||
try {
|
||||
if (userCollect.value) {
|
||||
await delCollect(productId)
|
||||
userCollect.value = false
|
||||
ElMessage.success('已取消收藏')
|
||||
} else {
|
||||
await addCollect(productId)
|
||||
userCollect.value = true
|
||||
ElMessage.success('已加入收藏夹')
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddToCart = async () => {
|
||||
const uniqueId = selectedSku.value?.unique || ''
|
||||
await cartStore.addToCart(productId, cartNum.value, uniqueId, 0)
|
||||
}
|
||||
|
||||
const handleBuyNow = async () => {
|
||||
if (!userStore.token) {
|
||||
userStore.openAuthModal('login')
|
||||
return
|
||||
}
|
||||
const uniqueId = selectedSku.value?.unique || ''
|
||||
const res = await cartStore.addToCart(productId, cartNum.value, uniqueId, 1)
|
||||
if (res && res.cartId) {
|
||||
router.push(`/checkout?cartId=${res.cartId}`)
|
||||
}
|
||||
}
|
||||
|
||||
const handleApplyRental = () => {
|
||||
if (!userStore.token) {
|
||||
userStore.openAuthModal('login')
|
||||
return
|
||||
}
|
||||
ElMessage.success(`🎉 成功提交【${storeInfo.value.store_name}】${rentDays.value}天免押租赁申请!客服将为您优先安排顺丰发货。`)
|
||||
router.push('/user?tab=orders')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchDetail()
|
||||
fetchReplies()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.rich-content :deep(img) {
|
||||
max-width: 100% !important;
|
||||
height: auto !important;
|
||||
border-radius: 12px;
|
||||
margin: 12px 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,243 @@
|
||||
<template>
|
||||
<div class="max-w-site mx-auto px-4 py-6 space-y-6">
|
||||
<!-- Breadcrumbs -->
|
||||
<div class="text-xs text-gray-500 flex items-center space-x-2">
|
||||
<router-link to="/" class="hover:text-primary">首页</router-link>
|
||||
<span>/</span>
|
||||
<span class="text-gray-800 font-medium">全部商品</span>
|
||||
<template v-if="currentCatName">
|
||||
<span>/</span>
|
||||
<span class="text-primary font-semibold">{{ currentCatName }}</span>
|
||||
</template>
|
||||
<template v-if="searchKeyword">
|
||||
<span>/</span>
|
||||
<span>关键词:<strong class="text-primary">"{{ searchKeyword }}"</strong></span>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Category / Filter Box -->
|
||||
<div class="bg-white rounded-2xl p-6 shadow-sm border border-gray-100 space-y-4">
|
||||
<!-- Categories Filter -->
|
||||
<div class="flex items-start space-x-4 pb-4 border-b border-gray-100 text-xs">
|
||||
<span class="text-gray-400 font-semibold w-16 pt-1">全部分类:</span>
|
||||
<div class="flex-1 flex flex-wrap gap-2">
|
||||
<button
|
||||
@click="selectCategory(0)"
|
||||
:class="!selectedCid && !selectedSid ? 'bg-primary text-white font-bold' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'"
|
||||
class="px-3.5 py-1.5 rounded-lg transition-colors"
|
||||
>
|
||||
全部
|
||||
</button>
|
||||
<button
|
||||
v-for="cat in categories"
|
||||
:key="cat.id"
|
||||
@click="selectCategory(cat.id)"
|
||||
:class="selectedCid == cat.id ? 'bg-primary text-white font-bold' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'"
|
||||
class="px-3.5 py-1.5 rounded-lg transition-colors"
|
||||
>
|
||||
{{ cat.cate_name }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sorting Bar -->
|
||||
<div class="flex flex-wrap items-center justify-between gap-4 pt-1">
|
||||
<div class="flex items-center space-x-2 text-xs">
|
||||
<span class="text-gray-400 font-semibold mr-2">排序方式:</span>
|
||||
<button
|
||||
@click="setSort('default')"
|
||||
:class="sortType === 'default' ? 'bg-red-50 text-primary border border-red-200 font-bold' : 'border border-gray-200 text-gray-700 hover:border-gray-400'"
|
||||
class="px-3.5 py-1.5 rounded-lg transition-all"
|
||||
>
|
||||
综合排序
|
||||
</button>
|
||||
<button
|
||||
@click="setSort('sales')"
|
||||
:class="sortType === 'sales' ? 'bg-red-50 text-primary border border-red-200 font-bold' : 'border border-gray-200 text-gray-700 hover:border-gray-400'"
|
||||
class="px-3.5 py-1.5 rounded-lg transition-all"
|
||||
>
|
||||
销量优先
|
||||
</button>
|
||||
<button
|
||||
@click="setSort('news')"
|
||||
:class="sortType === 'news' ? 'bg-red-50 text-primary border border-red-200 font-bold' : 'border border-gray-200 text-gray-700 hover:border-gray-400'"
|
||||
class="px-3.5 py-1.5 rounded-lg transition-all"
|
||||
>
|
||||
新品优先
|
||||
</button>
|
||||
<button
|
||||
@click="togglePriceSort"
|
||||
:class="sortType === 'price' ? 'bg-red-50 text-primary border border-red-200 font-bold' : 'border border-gray-200 text-gray-700 hover:border-gray-400'"
|
||||
class="px-3.5 py-1.5 rounded-lg transition-all flex items-center space-x-1"
|
||||
>
|
||||
<span>价格</span>
|
||||
<span>{{ priceOrder === 'asc' ? '↑' : priceOrder === 'desc' ? '↓' : '↕' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="text-xs text-gray-500">
|
||||
共找到 <strong class="text-primary">{{ total }}</strong> 款商品
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Product Grid -->
|
||||
<div v-loading="loading" class="min-h-[400px]">
|
||||
<div v-if="products.length > 0" class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
|
||||
<ProductCard
|
||||
v-for="item in products"
|
||||
:key="item.id"
|
||||
:product="item"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-else-if="!loading" class="bg-white rounded-2xl p-16 text-center shadow-sm border border-gray-100">
|
||||
<div class="text-6xl mb-4">🔍</div>
|
||||
<h4 class="text-lg font-bold text-gray-800">暂未找到相关商品</h4>
|
||||
<p class="text-xs text-gray-400 mt-1 mb-6">建议您尝试其他分类或缩短搜索关键词</p>
|
||||
<button @click="resetFilters" class="px-6 py-2.5 bg-primary text-white text-xs font-bold rounded-lg hover:bg-primary-hover transition-colors shadow-sm">
|
||||
查看所有商品
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div v-if="total > limit" class="flex justify-center pt-6">
|
||||
<el-pagination
|
||||
v-model:current-page="page"
|
||||
:page-size="limit"
|
||||
:total="total"
|
||||
layout="prev, pager, next, jumper"
|
||||
background
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import ProductCard from '@/components/ProductCard.vue'
|
||||
import { getProducts, getCategory } from '@/api'
|
||||
import { mockCategories, mockProducts } from '@/api/mock'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const categories = ref(mockCategories)
|
||||
const products = ref(mockProducts)
|
||||
const total = ref(mockProducts.length)
|
||||
const page = ref(1)
|
||||
const limit = ref(20)
|
||||
const loading = ref(false)
|
||||
|
||||
const selectedCid = ref(Number(route.query.cid || 0))
|
||||
const selectedSid = ref(Number(route.query.sid || 0))
|
||||
const searchKeyword = ref(route.query.keyword || '')
|
||||
const sortType = ref('default') // 'default' | 'sales' | 'news' | 'price'
|
||||
const priceOrder = ref('')
|
||||
|
||||
const currentCatName = computed(() => {
|
||||
if (selectedCid.value) {
|
||||
const c = categories.value.find(item => item.id == selectedCid.value)
|
||||
return c ? c.cate_name : ''
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
const fetchCategoryList = async () => {
|
||||
try {
|
||||
const res = await getCategory()
|
||||
categories.value = res || []
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
const fetchProductList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: page.value,
|
||||
limit: limit.value,
|
||||
cid: selectedCid.value || undefined,
|
||||
sid: selectedSid.value || undefined,
|
||||
keyword: searchKeyword.value || undefined,
|
||||
type: route.query.type || undefined
|
||||
}
|
||||
|
||||
if (sortType.value === 'sales') {
|
||||
params.salesOrder = 'desc'
|
||||
} else if (sortType.value === 'news') {
|
||||
params.news = 1
|
||||
} else if (sortType.value === 'price') {
|
||||
params.priceOrder = priceOrder.value
|
||||
}
|
||||
|
||||
const res = await getProducts(params)
|
||||
products.value = res.list || []
|
||||
total.value = res.count || res.total || 0
|
||||
} catch (e) {
|
||||
// ignore
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const selectCategory = (cid) => {
|
||||
selectedCid.value = cid
|
||||
selectedSid.value = 0
|
||||
page.value = 1
|
||||
fetchProductList()
|
||||
}
|
||||
|
||||
const setSort = (type) => {
|
||||
sortType.value = type
|
||||
priceOrder.value = ''
|
||||
page.value = 1
|
||||
fetchProductList()
|
||||
}
|
||||
|
||||
const togglePriceSort = () => {
|
||||
sortType.value = 'price'
|
||||
if (priceOrder.value === 'asc') {
|
||||
priceOrder.value = 'desc'
|
||||
} else {
|
||||
priceOrder.value = 'asc'
|
||||
}
|
||||
page.value = 1
|
||||
fetchProductList()
|
||||
}
|
||||
|
||||
const handlePageChange = (p) => {
|
||||
page.value = p
|
||||
fetchProductList()
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
|
||||
const resetFilters = () => {
|
||||
selectedCid.value = 0
|
||||
selectedSid.value = 0
|
||||
searchKeyword.value = ''
|
||||
sortType.value = 'default'
|
||||
priceOrder.value = ''
|
||||
page.value = 1
|
||||
router.push('/products')
|
||||
fetchProductList()
|
||||
}
|
||||
|
||||
watch(() => route.query, (newQuery) => {
|
||||
selectedCid.value = Number(newQuery.cid || 0)
|
||||
selectedSid.value = Number(newQuery.sid || 0)
|
||||
searchKeyword.value = newQuery.keyword || ''
|
||||
page.value = 1
|
||||
fetchProductList()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
fetchCategoryList()
|
||||
fetchProductList()
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,421 @@
|
||||
<template>
|
||||
<div class="space-y-4 sm:space-y-8 pb-16">
|
||||
<!-- Rental Hero Banner -->
|
||||
<section class="bg-gradient-to-r from-slate-900 via-indigo-950 to-slate-900 text-white py-6 sm:py-12 border-b border-indigo-900/40 relative overflow-hidden">
|
||||
<!-- Background Ambient Glow -->
|
||||
<div class="absolute -right-20 -top-20 w-96 h-96 bg-indigo-500/20 rounded-full blur-3xl pointer-events-none"></div>
|
||||
<div class="absolute left-1/4 -bottom-20 w-80 h-80 bg-red-500/15 rounded-full blur-3xl pointer-events-none"></div>
|
||||
|
||||
<div class="max-w-site mx-auto px-3 sm:px-4 relative z-10 flex flex-col md:flex-row items-center justify-between gap-5 sm:gap-8">
|
||||
<div class="space-y-2.5 sm:space-y-4 max-w-xl text-center md:text-left">
|
||||
<div class="inline-flex items-center space-x-2 px-3 py-1 rounded-full bg-indigo-500/20 border border-indigo-400/30 text-indigo-300 text-[11px] sm:text-xs font-semibold backdrop-blur-sm">
|
||||
<span>🛡️ 信用免押 · 随租随还 · 企个两用</span>
|
||||
</div>
|
||||
<h1 class="text-2xl sm:text-4xl font-extrabold tracking-tight text-white leading-tight">
|
||||
💻 全品类数码设备免押租赁
|
||||
</h1>
|
||||
<p class="text-xs sm:text-sm text-indigo-200/80 leading-relaxed">
|
||||
专业提供 <strong>苹果Mac/高性能电脑、旗舰手机、iPad平板、会展测试机、摄影器材</strong> 租赁。日租低至 <span class="text-orange-400 font-bold text-base sm:text-lg">¥2.8/天</span> 起,到期可还可买断!
|
||||
</p>
|
||||
<div class="flex items-center justify-center md:justify-start space-x-3 pt-1">
|
||||
<a href="#rental-grid" class="px-5 sm:px-6 py-2 sm:py-2.5 bg-gradient-to-r from-red-500 to-orange-500 text-white font-bold text-xs rounded-xl shadow-lg shadow-red-500/25 transition-all">
|
||||
挑选设备
|
||||
</a>
|
||||
<button @click="openConsult" class="px-5 sm:px-6 py-2 sm:py-2.5 bg-white/10 text-white font-semibold text-xs rounded-xl border border-white/20 backdrop-blur-sm transition-colors">
|
||||
企业批量租
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 4 Step Guarantee Highlights -->
|
||||
<div class="grid grid-cols-2 gap-2 sm:gap-3 w-full md:w-auto">
|
||||
<div class="bg-white/10 backdrop-blur-md border border-white/15 p-2.5 sm:p-4 rounded-xl sm:rounded-2xl">
|
||||
<span class="text-xl sm:text-2xl">💎</span>
|
||||
<h4 class="font-bold text-xs sm:text-sm mt-0.5 text-white">芝麻免押</h4>
|
||||
<p class="text-[10px] sm:text-[11px] text-gray-300">信用良好 0 押金</p>
|
||||
</div>
|
||||
<div class="bg-white/10 backdrop-blur-md border border-white/15 p-2.5 sm:p-4 rounded-xl sm:rounded-2xl">
|
||||
<span class="text-xl sm:text-2xl">⚡</span>
|
||||
<h4 class="font-bold text-xs sm:text-sm mt-0.5 text-white">顺丰包邮</h4>
|
||||
<p class="text-[10px] sm:text-[11px] text-gray-300">当日顺丰发出</p>
|
||||
</div>
|
||||
<div class="bg-white/10 backdrop-blur-md border border-white/15 p-2.5 sm:p-4 rounded-xl sm:rounded-2xl">
|
||||
<span class="text-xl sm:text-2xl">🔄</span>
|
||||
<h4 class="font-bold text-xs sm:text-sm mt-0.5 text-white">租完即送</h4>
|
||||
<p class="text-[10px] sm:text-[11px] text-gray-300">满期可0元买断</p>
|
||||
</div>
|
||||
<div class="bg-white/10 backdrop-blur-md border border-white/15 p-2.5 sm:p-4 rounded-xl sm:rounded-2xl">
|
||||
<span class="text-xl sm:text-2xl">🛠️</span>
|
||||
<h4 class="font-bold text-xs sm:text-sm mt-0.5 text-white">全程质保</h4>
|
||||
<p class="text-[10px] sm:text-[11px] text-gray-300">故障免费换新</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Rental Category Tabs & Filter -->
|
||||
<div id="rental-grid" class="max-w-site mx-auto px-3 sm:px-4 space-y-4 sm:space-y-6">
|
||||
<div class="bg-white rounded-2xl p-3.5 sm:p-6 shadow-sm border border-gray-100 space-y-3 sm:space-y-4">
|
||||
<!-- Device Category Tabs (Horizontal scroll on mobile) -->
|
||||
<div class="flex items-center space-x-2 sm:space-x-3 pb-3 sm:pb-4 border-b border-gray-100 overflow-x-auto no-scrollbar text-xs font-semibold">
|
||||
<span class="hidden sm:inline-block text-gray-400 flex-shrink-0 w-16">设备类别:</span>
|
||||
<button
|
||||
v-for="cat in rentalCategories"
|
||||
:key="cat.key"
|
||||
@click="activeCat = cat.key"
|
||||
:class="activeCat === cat.key ? 'bg-indigo-600 text-white shadow-sm' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'"
|
||||
class="px-3 sm:px-4 py-1.5 sm:py-2 rounded-xl transition-all flex items-center space-x-1 sm:space-x-1.5 flex-shrink-0 whitespace-nowrap"
|
||||
>
|
||||
<span>{{ cat.icon }}</span>
|
||||
<span>{{ cat.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Lease Term Filter -->
|
||||
<div class="flex items-center space-x-2 sm:space-x-3 text-xs font-medium overflow-x-auto no-scrollbar">
|
||||
<span class="hidden sm:inline-block text-gray-400 flex-shrink-0 w-16">租赁周期:</span>
|
||||
<button
|
||||
v-for="term in leaseTerms"
|
||||
:key="term.val"
|
||||
@click="selectedTerm = term.val"
|
||||
:class="selectedTerm === term.val ? 'text-indigo-600 bg-indigo-50 border-indigo-200 font-bold' : 'text-gray-600 border-gray-200 hover:border-gray-400'"
|
||||
class="px-2.5 sm:px-3.5 py-1 sm:py-1.5 rounded-lg border transition-all flex-shrink-0 whitespace-nowrap text-[11px] sm:text-xs"
|
||||
>
|
||||
{{ term.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rental Products Grid (2 cols on mobile, 4 on desktop) -->
|
||||
<div class="grid grid-cols-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-2.5 sm:gap-6">
|
||||
<div
|
||||
v-for="item in filteredRentalProducts"
|
||||
:key="item.id"
|
||||
class="bg-white rounded-xl sm:rounded-2xl border border-gray-100 overflow-hidden shadow-sm hover:shadow-xl hover:-translate-y-1 transition-all duration-300 flex flex-col justify-between group relative"
|
||||
>
|
||||
<!-- Badge -->
|
||||
<div class="absolute top-2 left-2 sm:top-3 sm:left-3 z-10 flex gap-1">
|
||||
<span class="px-1.5 sm:px-2.5 py-0.5 sm:py-1 text-[9px] sm:text-[10px] font-extrabold bg-gradient-to-r from-indigo-600 to-purple-600 text-white rounded-md sm:rounded-lg shadow-sm">
|
||||
{{ item.tag || '免押租赁' }}
|
||||
</span>
|
||||
<span v-if="item.condition" class="hidden sm:inline-block px-2 py-1 text-[10px] font-bold bg-gray-900/80 text-white rounded-lg backdrop-blur-sm">
|
||||
{{ item.condition }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Product Image -->
|
||||
<div class="aspect-square bg-gray-50 overflow-hidden relative cursor-pointer" @click="goDetail(item.id)">
|
||||
<img :src="item.image" class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500" />
|
||||
<!-- Term hint overlay -->
|
||||
<div class="absolute bottom-1.5 right-1.5 sm:bottom-2 sm:right-2 bg-black/60 text-white text-[9px] sm:text-[10px] px-1.5 sm:px-2 py-0.5 rounded backdrop-blur-sm">
|
||||
{{ item.termDesc }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Product Info -->
|
||||
<div class="p-2.5 sm:p-5 flex-1 flex flex-col justify-between space-y-2.5 sm:space-y-4">
|
||||
<div>
|
||||
<h3 @click="goDetail(item.id)" class="text-xs sm:text-sm font-bold text-gray-900 line-clamp-2 hover:text-primary cursor-pointer leading-snug">
|
||||
{{ item.title }}
|
||||
</h3>
|
||||
<p class="hidden sm:block text-xs text-gray-400 line-clamp-1 mt-1">{{ item.specs }}</p>
|
||||
</div>
|
||||
|
||||
<div class="pt-2 sm:pt-3 border-t border-gray-100 space-y-2 sm:space-y-3">
|
||||
<!-- Price per day / month -->
|
||||
<div class="flex items-baseline justify-between">
|
||||
<div>
|
||||
<span class="text-[9px] sm:text-[11px] text-gray-400">低至 </span>
|
||||
<span class="text-xs font-bold text-primary">¥</span>
|
||||
<span class="text-lg sm:text-2xl font-black text-primary tracking-tight">{{ item.dailyPrice }}</span>
|
||||
<span class="text-[10px] sm:text-xs text-primary font-semibold">/天</span>
|
||||
</div>
|
||||
<div class="hidden sm:block text-[11px] text-gray-400">
|
||||
月租约 ¥{{ item.monthlyPrice }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div class="flex items-center space-x-1.5 sm:space-x-2">
|
||||
<button
|
||||
@click="openRentalDialog(item)"
|
||||
class="flex-1 py-1.5 sm:py-2.5 bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-700 hover:to-purple-700 text-white font-bold text-[10px] sm:text-xs rounded-lg sm:rounded-xl shadow-sm transition-all"
|
||||
>
|
||||
立即申请免押
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rental Application / Booking Modal -->
|
||||
<el-dialog
|
||||
v-model="showRentalDialog"
|
||||
title="申请数码设备租赁 / 预订"
|
||||
width="92%"
|
||||
:align-center="true"
|
||||
destroy-on-close
|
||||
class="max-w-lg rounded-2xl"
|
||||
>
|
||||
<div v-if="currentRentalItem" class="space-y-5 text-xs">
|
||||
<!-- Selected Item Brief -->
|
||||
<div class="flex items-center space-x-4 p-3 bg-gray-50 rounded-xl border border-gray-100">
|
||||
<img :src="currentRentalItem.image" class="w-16 h-16 rounded-lg object-cover bg-white" />
|
||||
<div class="space-y-1">
|
||||
<h4 class="font-bold text-gray-900 text-sm">{{ currentRentalItem.title }}</h4>
|
||||
<p class="text-gray-400 text-[11px]">{{ currentRentalItem.specs }}</p>
|
||||
<p class="text-primary font-bold">日租金:¥{{ currentRentalItem.dailyPrice }}/天 (月租约 ¥{{ currentRentalItem.monthlyPrice }})</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rent Duration Selector -->
|
||||
<div>
|
||||
<label class="block font-bold text-gray-700 mb-2">选择租赁租期:</label>
|
||||
<div class="grid grid-cols-4 gap-2">
|
||||
<button
|
||||
v-for="d in durationOptions"
|
||||
:key="d.days"
|
||||
@click="selectedDays = d.days"
|
||||
:class="selectedDays === d.days ? 'border-primary bg-red-50 text-primary font-bold ring-2 ring-red-100' : 'border-gray-200 text-gray-700 hover:border-gray-400'"
|
||||
class="p-2.5 rounded-xl border text-center transition-all"
|
||||
>
|
||||
<div class="text-xs font-bold">{{ d.label }}</div>
|
||||
<div class="text-[10px] text-gray-400 mt-0.5">{{ d.discount }}</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rent Scheme Options -->
|
||||
<div class="space-y-2">
|
||||
<label class="block font-bold text-gray-700">到期方案:</label>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div
|
||||
@click="rentScheme = 'return'"
|
||||
:class="rentScheme === 'return' ? 'border-primary bg-red-50/50' : 'border-gray-200'"
|
||||
class="p-3 rounded-xl border cursor-pointer transition-all"
|
||||
>
|
||||
<div class="font-bold text-gray-800">📦 到期归还 / 续租</div>
|
||||
<p class="text-[11px] text-gray-500 mt-1">租期满后顺丰寄回,或随时申请续租</p>
|
||||
</div>
|
||||
<div
|
||||
@click="rentScheme = 'buyout'"
|
||||
:class="rentScheme === 'buyout' ? 'border-primary bg-red-50/50' : 'border-gray-200'"
|
||||
class="p-3 rounded-xl border cursor-pointer transition-all"
|
||||
>
|
||||
<div class="font-bold text-gray-800">🎁 租完即送 / 补差买断</div>
|
||||
<p class="text-[11px] text-gray-500 mt-1">租期满后自动归您所有,无需退还</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User Contact Info -->
|
||||
<div class="space-y-3 pt-2">
|
||||
<div>
|
||||
<label class="block font-bold text-gray-700 mb-1">联系人姓名:</label>
|
||||
<input v-model="rentalForm.name" type="text" placeholder="请输入真实姓名" class="w-full px-3 py-2 border border-gray-200 rounded-lg outline-none focus:border-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block font-bold text-gray-700 mb-1">联系电话:</label>
|
||||
<input v-model="rentalForm.phone" type="text" placeholder="请输入手机号码" class="w-full px-3 py-2 border border-gray-200 rounded-lg outline-none focus:border-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block font-bold text-gray-700 mb-1">收货地址:</label>
|
||||
<input v-model="rentalForm.address" type="text" placeholder="请输入设备寄送详细地址" class="w-full px-3 py-2 border border-gray-200 rounded-lg outline-none focus:border-primary" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cost Estimate -->
|
||||
<div class="p-4 bg-indigo-50/70 border border-indigo-100 rounded-xl flex items-center justify-between">
|
||||
<div>
|
||||
<span class="text-gray-500">预估总租金({{ selectedDays }}天):</span>
|
||||
<p class="text-xs text-indigo-700 mt-0.5">芝麻信用分 ≥ 650 享 ¥0 押金</p>
|
||||
</div>
|
||||
<span class="text-2xl font-black text-primary">¥{{ (currentRentalItem.dailyPrice * selectedDays).toFixed(2) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex items-center justify-end space-x-3">
|
||||
<button @click="showRentalDialog = false" class="px-5 py-2 text-xs text-gray-600 hover:text-gray-800">取消</button>
|
||||
<button @click="submitRental" class="px-6 py-2 bg-primary hover:bg-primary-hover text-white text-xs font-bold rounded-lg shadow-sm">
|
||||
提交免押租赁申请
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const activeCat = ref('all')
|
||||
const selectedTerm = ref('all')
|
||||
|
||||
const rentalCategories = [
|
||||
{ key: 'all', name: '全部设备', icon: '⚡' },
|
||||
{ key: 'computer', name: '笔记本 / 台式电脑', icon: '💻' },
|
||||
{ key: 'phone', name: '智能手机', icon: '📱' },
|
||||
{ key: 'pad', name: 'iPad / 平板电脑', icon: '📟' },
|
||||
{ key: 'audio', name: '音响 / 会议影音', icon: '🎧' },
|
||||
{ key: 'enterprise', name: '企业批量工位机', icon: '🏢' }
|
||||
]
|
||||
|
||||
const leaseTerms = [
|
||||
{ label: '全部周期', val: 'all' },
|
||||
{ label: '短期体验 (7~30天)', val: 'short' },
|
||||
{ label: '季度/半年租 (3~6个月)', val: 'mid' },
|
||||
{ label: '长期租赁 (12个月以上)', val: 'long' }
|
||||
]
|
||||
|
||||
// Curated rental products mapped to store products
|
||||
const rentalProducts = ref([
|
||||
{
|
||||
id: 1,
|
||||
cat: 'computer',
|
||||
title: 'Apple MacBook Pro 14英寸 M3 Max 芯片 / 36G / 1TB 剪辑办公本',
|
||||
specs: '设计剪辑/移动办公神器 · 官方质保 · 顺丰包邮',
|
||||
dailyPrice: 18.8,
|
||||
monthlyPrice: 499,
|
||||
image: 'https://images.unsplash.com/photo-1517336714731-489689fd1ca8?auto=format&fit=crop&w=600&q=80',
|
||||
condition: '99新',
|
||||
tag: '电脑办公',
|
||||
termDesc: '企业免押专享'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
cat: 'pad',
|
||||
title: 'Apple iPad Pro 13英寸 M4芯片 256G 深空黑 (双层OLED屏)',
|
||||
specs: '原封/99新 · 生产力神器 · 支持二代Pencil',
|
||||
dailyPrice: 8.5,
|
||||
monthlyPrice: 240,
|
||||
image: 'https://images.unsplash.com/photo-1544244015-0df4b3ffc6b0?auto=format&fit=crop&w=600&q=80',
|
||||
condition: '全新原封',
|
||||
tag: '平板旗舰',
|
||||
termDesc: '支持租完即送'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
cat: 'phone',
|
||||
title: 'iPhone 15 Pro Max 256GB 原色钛金属 5G旗舰手机',
|
||||
specs: 'A17 Pro芯片 · 5倍潜望光学长焦 · 钛金属机身',
|
||||
dailyPrice: 6.8,
|
||||
monthlyPrice: 188,
|
||||
image: 'https://images.unsplash.com/photo-1695048133142-1a20484d2569?auto=format&fit=crop&w=600&q=80',
|
||||
condition: '99新',
|
||||
tag: '手机租借',
|
||||
termDesc: '7天起租 · 随租随还'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
cat: 'computer',
|
||||
title: 'ThinkPad X1 Carbon 商务旗舰超极本 i7/32G/1TB',
|
||||
specs: '仅1.09kg轻薄碳纤维 · 经典小红点 · 超长续航',
|
||||
dailyPrice: 12.5,
|
||||
monthlyPrice: 320,
|
||||
image: 'https://images.unsplash.com/photo-1588872657578-7efd1f1555ed?auto=format&fit=crop&w=600&q=80',
|
||||
condition: '95新',
|
||||
tag: '商务出差',
|
||||
termDesc: '支持月租/年租'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
cat: 'audio',
|
||||
title: '索尼 WH-1000XM5 无线降噪头戴式耳机 铂金银',
|
||||
specs: '旗舰双芯降噪 · 商务会议降噪 · 40小时长续航',
|
||||
dailyPrice: 3.5,
|
||||
monthlyPrice: 90,
|
||||
image: 'https://images.unsplash.com/photo-1505740420928-5e560c06d30e?auto=format&fit=crop&w=600&q=80',
|
||||
condition: '99新',
|
||||
tag: '影音会议',
|
||||
termDesc: '免押试用'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
cat: 'audio',
|
||||
title: 'Apple Watch Ultra 2 智能手表 钛金属表壳/越野表带',
|
||||
specs: '3000尼特超亮屏 · 双频GPS · 户外探险运动',
|
||||
dailyPrice: 4.2,
|
||||
monthlyPrice: 115,
|
||||
image: 'https://images.unsplash.com/photo-1523275335684-37898b6baf30?auto=format&fit=crop&w=600&q=80',
|
||||
condition: '99新',
|
||||
tag: '智能穿戴',
|
||||
termDesc: '短期户外租'
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
cat: 'enterprise',
|
||||
title: 'Dell / 联想企业级批量商务台式电脑 + 27寸4K双屏工位套装',
|
||||
specs: 'i7/32G/1TB SSD + 27寸4K护眼屏 · 上门安装',
|
||||
dailyPrice: 9.8,
|
||||
monthlyPrice: 260,
|
||||
image: 'https://images.unsplash.com/photo-1497215728101-856f4ea42174?auto=format&fit=crop&w=600&q=80',
|
||||
condition: '全新批量',
|
||||
tag: '企业免押',
|
||||
termDesc: '企业租金按月结'
|
||||
}
|
||||
])
|
||||
|
||||
const filteredRentalProducts = computed(() => {
|
||||
if (activeCat.value === 'all') return rentalProducts.value
|
||||
return rentalProducts.value.filter(item => item.cat === activeCat.value)
|
||||
})
|
||||
|
||||
const showRentalDialog = ref(false)
|
||||
const currentRentalItem = ref(null)
|
||||
const selectedDays = ref(30)
|
||||
const rentScheme = ref('return')
|
||||
|
||||
const durationOptions = [
|
||||
{ label: '7 天试用', days: 7, discount: '标准价' },
|
||||
{ label: '30 天月租', days: 30, discount: '立减10%' },
|
||||
{ label: '90 天季租', days: 90, discount: '立减20%' },
|
||||
{ label: '365 天年租', days: 365, discount: '可0元买断' }
|
||||
]
|
||||
|
||||
const rentalForm = ref({
|
||||
name: '',
|
||||
phone: '',
|
||||
address: ''
|
||||
})
|
||||
|
||||
const goDetail = (id) => {
|
||||
router.push(`/product/${id}`)
|
||||
}
|
||||
|
||||
const openRentalModal = (item) => {
|
||||
if (!userStore.token) {
|
||||
userStore.openAuthModal('login')
|
||||
return
|
||||
}
|
||||
currentRentalItem.value = item
|
||||
rentalForm.value = {
|
||||
name: userStore.userInfo?.nickname || '',
|
||||
phone: userStore.userInfo?.phone || '',
|
||||
address: ''
|
||||
}
|
||||
showRentalDialog.value = true
|
||||
}
|
||||
|
||||
const openConsult = () => {
|
||||
ElMessage.success('企业客服专线已接通:400-888-9999,请联系在线客服了解企业批量免押授信方案')
|
||||
}
|
||||
|
||||
const submitRental = () => {
|
||||
if (!rentalForm.value.name || !rentalForm.value.phone || !rentalForm.value.address) {
|
||||
ElMessage.warning('请填写完整的收货人、电话与寄送地址')
|
||||
return
|
||||
}
|
||||
showRentalDialog.value = false
|
||||
ElMessage.success('🎉 租赁预订申请提交成功!专属客服将在 15 分钟内为您完成免押审核与发货。')
|
||||
router.push('/user?tab=orders')
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,478 @@
|
||||
<template>
|
||||
<div class="max-w-site mx-auto px-4 py-8 space-y-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-12 gap-6">
|
||||
<!-- Left Sidebar Nav -->
|
||||
<div class="md:col-span-3 space-y-4">
|
||||
<!-- User Profile Card -->
|
||||
<div class="bg-white rounded-2xl p-6 shadow-sm border border-gray-100 text-center">
|
||||
<img
|
||||
:src="userStore.userInfo?.avatar || 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'"
|
||||
class="w-20 h-20 rounded-full mx-auto object-cover border-4 border-red-50 shadow-sm"
|
||||
/>
|
||||
<h3 class="text-base font-bold text-gray-900 mt-3">
|
||||
{{ userStore.userInfo?.nickname || userStore.userInfo?.account || '尊贵会员' }}
|
||||
</h3>
|
||||
<p class="text-xs text-gray-400 mt-0.5">ID: {{ userStore.userInfo?.uid || '10001' }}</p>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2 mt-4 pt-4 border-t border-gray-100 text-xs">
|
||||
<div>
|
||||
<span class="text-gray-400">账户余额</span>
|
||||
<p class="font-bold text-primary text-sm mt-0.5">¥{{ userStore.userCenter?.now_money || '0.00' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-gray-400">可用积分</span>
|
||||
<p class="font-bold text-gray-800 text-sm mt-0.5">{{ userStore.userCenter?.integral || '0' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Menu Tabs -->
|
||||
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 p-2 space-y-1 text-sm font-medium">
|
||||
<button
|
||||
@click="switchTab('orders')"
|
||||
:class="activeTab === 'orders' ? 'bg-primary text-white font-bold' : 'text-gray-700 hover:bg-gray-50'"
|
||||
class="w-full text-left px-4 py-3 rounded-xl transition-colors flex items-center space-x-2"
|
||||
>
|
||||
<span>📦</span>
|
||||
<span>我的订单</span>
|
||||
</button>
|
||||
<button
|
||||
@click="switchTab('address')"
|
||||
:class="activeTab === 'address' ? 'bg-primary text-white font-bold' : 'text-gray-700 hover:bg-gray-50'"
|
||||
class="w-full text-left px-4 py-3 rounded-xl transition-colors flex items-center space-x-2"
|
||||
>
|
||||
<span>📍</span>
|
||||
<span>收货地址管理</span>
|
||||
</button>
|
||||
<button
|
||||
@click="switchTab('collect')"
|
||||
:class="activeTab === 'collect' ? 'bg-primary text-white font-bold' : 'text-gray-700 hover:bg-gray-50'"
|
||||
class="w-full text-left px-4 py-3 rounded-xl transition-colors flex items-center space-x-2"
|
||||
>
|
||||
<span>⭐</span>
|
||||
<span>商品收藏夹</span>
|
||||
</button>
|
||||
<button
|
||||
@click="switchTab('coupons')"
|
||||
:class="activeTab === 'coupons' ? 'bg-primary text-white font-bold' : 'text-gray-700 hover:bg-gray-50'"
|
||||
class="w-full text-left px-4 py-3 rounded-xl transition-colors flex items-center space-x-2"
|
||||
>
|
||||
<span>🎟️</span>
|
||||
<span>我的优惠券</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Main Content Panel -->
|
||||
<div class="md:col-span-9">
|
||||
<!-- Tab 1: Orders -->
|
||||
<div v-if="activeTab === 'orders'" class="bg-white rounded-2xl p-6 shadow-sm border border-gray-100 space-y-6">
|
||||
<div class="flex items-center justify-between pb-4 border-b border-gray-100">
|
||||
<h2 class="text-base font-bold text-gray-900">订单列表</h2>
|
||||
<!-- Order status filter chips -->
|
||||
<div class="flex space-x-2 text-xs">
|
||||
<button
|
||||
v-for="st in orderStatusList"
|
||||
:key="st.val"
|
||||
@click="filterOrderStatus(st.val)"
|
||||
:class="orderStatus === st.val ? 'bg-red-50 text-primary border border-red-200 font-bold' : 'border border-gray-200 text-gray-600'"
|
||||
class="px-3 py-1.5 rounded-lg transition-colors"
|
||||
>
|
||||
{{ st.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Orders List -->
|
||||
<div v-loading="ordersLoading" class="space-y-4">
|
||||
<div
|
||||
v-for="order in orders"
|
||||
:key="order.id"
|
||||
class="border border-gray-100 rounded-2xl overflow-hidden hover:border-gray-200 transition-colors"
|
||||
>
|
||||
<!-- Order Header -->
|
||||
<div class="bg-gray-50 px-6 py-3 flex items-center justify-between text-xs text-gray-500">
|
||||
<div class="space-x-4">
|
||||
<span>订单编号:<strong class="text-gray-800">{{ order.order_id }}</strong></span>
|
||||
<span>下单时间:{{ order._add_time || order.add_time_y }}</span>
|
||||
</div>
|
||||
<span class="font-bold text-primary">{{ order._status?._title || order.status_name || '进行中' }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Order Body -->
|
||||
<div class="p-6 space-y-4">
|
||||
<div
|
||||
v-for="cart in order.cartInfo"
|
||||
:key="cart.id"
|
||||
class="flex items-center justify-between text-xs"
|
||||
>
|
||||
<div class="flex items-center space-x-4">
|
||||
<img :src="cart.productInfo?.attrInfo?.image || cart.productInfo?.image" class="w-16 h-16 rounded-xl object-cover border border-gray-100" />
|
||||
<div>
|
||||
<h4 class="font-bold text-gray-800 hover:text-primary cursor-pointer">{{ cart.productInfo?.store_name }}</h4>
|
||||
<p v-if="cart.productInfo?.attrInfo?.suk" class="text-gray-400 mt-1">规格:{{ cart.productInfo?.attrInfo?.suk }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="font-bold text-gray-900">¥{{ cart.truePrice || cart.productInfo?.price }} x {{ cart.cart_num }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Order Footer -->
|
||||
<div class="pt-4 border-t border-gray-100 flex items-center justify-between">
|
||||
<span class="text-xs text-gray-500">
|
||||
实付金额:<strong class="text-base font-extrabold text-primary">¥{{ order.pay_price }}</strong>
|
||||
</span>
|
||||
<div class="flex space-x-2">
|
||||
<button
|
||||
v-if="order._status?._type === 0"
|
||||
@click="cancelOrder(order.id)"
|
||||
class="px-4 py-1.5 text-xs text-gray-600 border border-gray-200 rounded-lg hover:border-gray-400"
|
||||
>
|
||||
取消订单
|
||||
</button>
|
||||
<button
|
||||
v-if="order._status?._type === 2"
|
||||
@click="takeOrder(order.order_id)"
|
||||
class="px-4 py-1.5 text-xs bg-primary text-white font-bold rounded-lg hover:bg-primary-hover shadow-sm"
|
||||
>
|
||||
确认收货
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="orders.length === 0 && !ordersLoading" class="text-center py-16 text-gray-400 text-sm">
|
||||
暂无相关订单
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab 2: Address Book -->
|
||||
<div v-else-if="activeTab === 'address'" class="bg-white rounded-2xl p-6 shadow-sm border border-gray-100 space-y-6">
|
||||
<div class="flex items-center justify-between pb-4 border-b border-gray-100">
|
||||
<h2 class="text-base font-bold text-gray-900">收货地址簿</h2>
|
||||
<button @click="openEditAddressModal()" class="px-4 py-2 bg-primary text-white font-bold text-xs rounded-xl hover:bg-primary-hover shadow-sm">
|
||||
+ 新增收货地址
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div
|
||||
v-for="addr in addressList"
|
||||
:key="addr.id"
|
||||
class="p-5 rounded-2xl border border-gray-200 hover:border-red-200 space-y-2 text-xs relative"
|
||||
>
|
||||
<div class="flex items-center justify-between font-bold text-gray-800 text-sm">
|
||||
<span>{{ addr.real_name }} ({{ addr.phone }})</span>
|
||||
<span v-if="addr.is_default" class="bg-red-100 text-primary text-[10px] px-2 py-0.5 rounded">默认地址</span>
|
||||
</div>
|
||||
<p class="text-gray-500 leading-relaxed">{{ addr.province }} {{ addr.city }} {{ addr.district }} {{ addr.detail }}</p>
|
||||
<div class="pt-3 border-t border-gray-100 flex items-center justify-end space-x-3 text-xs">
|
||||
<button v-if="!addr.is_default" @click="setAddrDefault(addr.id)" class="text-gray-500 hover:text-primary">设为默认</button>
|
||||
<button @click="openEditAddressModal(addr)" class="text-primary hover:underline">编辑</button>
|
||||
<button @click="deleteAddress(addr.id)" class="text-gray-400 hover:text-red-600">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab 3: Collect Favorites -->
|
||||
<div v-else-if="activeTab === 'collect'" class="bg-white rounded-2xl p-6 shadow-sm border border-gray-100 space-y-6">
|
||||
<h2 class="text-base font-bold text-gray-900 pb-4 border-b border-gray-100">商品收藏夹</h2>
|
||||
<div v-if="collectList.length" class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4">
|
||||
<div
|
||||
v-for="item in collectList"
|
||||
:key="item.pid"
|
||||
class="border border-gray-100 rounded-xl overflow-hidden hover:shadow-md transition-all p-3 space-y-2"
|
||||
>
|
||||
<router-link :to="`/product/${item.pid}`" class="block aspect-square rounded-lg overflow-hidden bg-gray-50">
|
||||
<img :src="item.image" class="w-full h-full object-cover" />
|
||||
</router-link>
|
||||
<h4 class="text-xs font-bold text-gray-800 line-clamp-1 hover:text-primary cursor-pointer">{{ item.store_name }}</h4>
|
||||
<div class="flex items-center justify-between text-xs">
|
||||
<span class="text-primary font-bold">¥{{ item.price }}</span>
|
||||
<button @click="removeCollect(item.pid)" class="text-gray-400 hover:text-red-500 text-[11px]">取消收藏</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-center py-16 text-gray-400 text-sm">
|
||||
暂无收藏商品
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab 4: Coupons -->
|
||||
<div v-else-if="activeTab === 'coupons'" class="bg-white rounded-2xl p-6 shadow-sm border border-gray-100 space-y-6">
|
||||
<h2 class="text-base font-bold text-gray-900 pb-4 border-b border-gray-100">我的优惠券</h2>
|
||||
<div v-if="couponList.length" class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div
|
||||
v-for="c in couponList"
|
||||
:key="c.id"
|
||||
class="p-5 rounded-2xl border-2 border-red-200 bg-red-50/30 flex items-center justify-between"
|
||||
>
|
||||
<div>
|
||||
<div class="text-2xl font-black text-primary">¥{{ c.coupon_price }}</div>
|
||||
<p class="text-xs text-gray-600 font-medium mt-1">{{ c.coupon_title }}</p>
|
||||
<p class="text-[10px] text-gray-400 mt-0.5">满 ¥{{ c.use_min_price }} 可用</p>
|
||||
</div>
|
||||
<router-link to="/products" class="px-4 py-2 bg-primary text-white text-xs font-bold rounded-xl hover:bg-primary-hover shadow-sm">
|
||||
去使用
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-center py-16 text-gray-400 text-sm">
|
||||
暂无可用的优惠券
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Address Edit Modal -->
|
||||
<el-dialog
|
||||
v-model="showAddressModal"
|
||||
:title="editingAddress.id ? '编辑收货地址' : '新增收货地址'"
|
||||
width="480px"
|
||||
:align-center="true"
|
||||
>
|
||||
<div class="space-y-4 text-xs">
|
||||
<div>
|
||||
<label class="block font-bold text-gray-700 mb-1">收货人姓名</label>
|
||||
<input v-model="editingAddress.real_name" class="w-full px-3 py-2 border rounded-lg" placeholder="请输入姓名" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block font-bold text-gray-700 mb-1">联系手机号</label>
|
||||
<input v-model="editingAddress.phone" class="w-full px-3 py-2 border rounded-lg" placeholder="请输入手机号" />
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<div>
|
||||
<label class="block font-bold text-gray-700 mb-1">省份</label>
|
||||
<input v-model="editingAddress.province" class="w-full px-3 py-2 border rounded-lg" placeholder="省份" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block font-bold text-gray-700 mb-1">城市</label>
|
||||
<input v-model="editingAddress.city" class="w-full px-3 py-2 border rounded-lg" placeholder="城市" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block font-bold text-gray-700 mb-1">区/县</label>
|
||||
<input v-model="editingAddress.district" class="w-full px-3 py-2 border rounded-lg" placeholder="区县" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block font-bold text-gray-700 mb-1">详细地址</label>
|
||||
<input v-model="editingAddress.detail" class="w-full px-3 py-2 border rounded-lg" placeholder="街道、小区、楼层门牌号" />
|
||||
</div>
|
||||
<div class="pt-2">
|
||||
<el-checkbox v-model="editingAddress.is_default">设为默认收货地址</el-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<button @click="saveAddress" class="px-6 py-2 bg-primary text-white font-bold text-xs rounded-lg hover:bg-primary-hover">
|
||||
保存地址
|
||||
</button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import {
|
||||
getOrderList,
|
||||
orderCancel,
|
||||
orderTake,
|
||||
getAddressList,
|
||||
editAddress,
|
||||
delAddress,
|
||||
setDefaultAddress,
|
||||
getCollectList,
|
||||
delCollect,
|
||||
getMyCoupons
|
||||
} from '@/api'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const activeTab = ref(route.query.tab || 'orders')
|
||||
|
||||
const orderStatusList = [
|
||||
{ label: '全部', val: '' },
|
||||
{ label: '待付款', val: '0' },
|
||||
{ label: '待发货', val: '1' },
|
||||
{ label: '待收货', val: '2' },
|
||||
{ label: '已完成', val: '3' }
|
||||
]
|
||||
|
||||
const orderStatus = ref('')
|
||||
const orders = ref([])
|
||||
const ordersLoading = ref(false)
|
||||
|
||||
const addressList = ref([])
|
||||
const showAddressModal = ref(false)
|
||||
const editingAddress = ref({
|
||||
id: 0,
|
||||
real_name: '',
|
||||
phone: '',
|
||||
province: '',
|
||||
city: '',
|
||||
district: '',
|
||||
detail: '',
|
||||
is_default: false
|
||||
})
|
||||
|
||||
const collectList = ref([])
|
||||
const couponList = ref([])
|
||||
|
||||
const switchTab = (tab) => {
|
||||
activeTab.value = tab
|
||||
router.replace({ query: { ...route.query, tab } })
|
||||
loadTabData(tab)
|
||||
}
|
||||
|
||||
const loadTabData = (tab) => {
|
||||
if (tab === 'orders') fetchOrders()
|
||||
else if (tab === 'address') fetchAddresses()
|
||||
else if (tab === 'collect') fetchCollects()
|
||||
else if (tab === 'coupons') fetchCoupons()
|
||||
}
|
||||
|
||||
const fetchOrders = async () => {
|
||||
ordersLoading.value = true
|
||||
try {
|
||||
const res = await getOrderList({ page: 1, limit: 20, type: orderStatus.value })
|
||||
orders.value = (res && res.list) || []
|
||||
} catch (e) {
|
||||
// ignore
|
||||
} finally {
|
||||
ordersLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const filterOrderStatus = (val) => {
|
||||
orderStatus.value = val
|
||||
fetchOrders()
|
||||
}
|
||||
|
||||
const cancelOrder = (id) => {
|
||||
ElMessageBox.confirm('确定要取消该订单吗?', '提示', { type: 'warning' }).then(async () => {
|
||||
await orderCancel(id)
|
||||
ElMessage.success('订单已取消')
|
||||
fetchOrders()
|
||||
})
|
||||
}
|
||||
|
||||
const takeOrder = (uni) => {
|
||||
ElMessageBox.confirm('确认已收到商品?', '提示', { type: 'success' }).then(async () => {
|
||||
await orderTake(uni)
|
||||
ElMessage.success('已确认收货')
|
||||
fetchOrders()
|
||||
})
|
||||
}
|
||||
|
||||
const fetchAddresses = async () => {
|
||||
try {
|
||||
const res = await getAddressList({ page: 1, limit: 50 })
|
||||
addressList.value = (res && res.list) || []
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
const openEditAddressModal = (addr) => {
|
||||
if (addr) {
|
||||
editingAddress.value = { ...addr, is_default: !!addr.is_default }
|
||||
} else {
|
||||
editingAddress.value = {
|
||||
id: 0,
|
||||
real_name: '',
|
||||
phone: '',
|
||||
province: '',
|
||||
city: '',
|
||||
district: '',
|
||||
detail: '',
|
||||
is_default: false
|
||||
}
|
||||
}
|
||||
showAddressModal.value = true
|
||||
}
|
||||
|
||||
const saveAddress = async () => {
|
||||
if (!editingAddress.value.real_name || !editingAddress.value.phone || !editingAddress.value.detail) {
|
||||
ElMessage.warning('请填写完整的收货人、电话与详细地址')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await editAddress({
|
||||
...editingAddress.value,
|
||||
is_default: editingAddress.value.is_default ? 1 : 0
|
||||
})
|
||||
ElMessage.success('地址保存成功')
|
||||
showAddressModal.value = false
|
||||
fetchAddresses()
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
const deleteAddress = (id) => {
|
||||
ElMessageBox.confirm('确定删除该地址吗?', '提示', { type: 'warning' }).then(async () => {
|
||||
await delAddress(id)
|
||||
ElMessage.success('删除成功')
|
||||
fetchAddresses()
|
||||
})
|
||||
}
|
||||
|
||||
const setAddrDefault = async (id) => {
|
||||
try {
|
||||
await setDefaultAddress(id)
|
||||
ElMessage.success('默认地址设置成功')
|
||||
fetchAddresses()
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
const fetchCollects = async () => {
|
||||
try {
|
||||
const res = await getCollectList({ page: 1, limit: 50 })
|
||||
collectList.value = (res && res.list) || []
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
const removeCollect = async (pid) => {
|
||||
try {
|
||||
await delCollect(pid)
|
||||
ElMessage.success('已取消收藏')
|
||||
fetchCollects()
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
const fetchCoupons = async () => {
|
||||
try {
|
||||
const res = await getMyCoupons(0)
|
||||
couponList.value = res || []
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => route.query.tab, (newTab) => {
|
||||
if (newTab) {
|
||||
activeTab.value = newTab
|
||||
loadTabData(newTab)
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
userStore.fetchUserCenter()
|
||||
loadTabData(activeTab.value)
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user