Files
MTKJPAY-FRONT/src/views/ProductDetail.vue
qiube d914301ee3 feat(order): 实现国际化支持并优化创建订单界面
- 将创建订单界面的所有静态文本替换为国际化标签
- 实现在移动端和桌面端的响应式表单布局
- 添加多语言国家名称显示功能
- 集成Vue I18n国际化框架到应用主入口
- 优化订单确认页面的国际化文本显示
- 移除导航菜单中的创建订单项
- 添加针对不同国家的地址字段验证规则
- 实现越南地址层级映射逻辑
- 添加货币代码自动设置国家功能
2025-12-24 17:38:33 +08:00

1315 lines
30 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="product-detail">
<!-- 加载状态 -->
<div v-if="productLoading" class="loading-container">
<el-skeleton :rows="10" animated />
</div>
<!-- 无商品数据提示 -->
<div v-else-if="!product.id" class="empty-container">
<el-empty :description="t('product.productNotExist')">
<p class="empty-tip">{{ t('product.linkExpired') }}</p>
</el-empty>
</div>
<!-- 商品详情内容 -->
<template v-else>
<!-- 商品基本信息区域 -->
<div class="product-main">
<!-- 左侧商品图片 -->
<div class="product-images">
<div class="main-image">
<el-image
:src="currentImage"
:preview-src-list="product.mainImages || []"
fit="contain"
class="main-img"
:lazy="true"
>
<template #error>
<div class="image-slot">
<el-icon><Picture /></el-icon>
<div>{{ t('product.imageLoadFailed') }}</div>
</div>
</template>
</el-image>
</div>
<div class="thumbnail-list" v-if="product.mainImages && product.mainImages.length > 1">
<div
v-for="(img, index) in product.mainImages"
:key="index"
class="thumbnail-item"
:class="{ active: currentImageIndex === index }"
@click="selectImage(index)"
@mouseenter="hoverImage(img)"
@mouseleave="hoverImage(null)"
>
<el-image :src="img" fit="cover" class="thumbnail-img" />
</div>
</div>
</div>
<!-- 右侧商品信息 -->
<div class="product-info">
<h1 class="product-title">{{ product.name }}</h1>
<!-- 货币和语言选择如果有多个货币- 突出显示 -->
<div class="currency-selector-card" v-if="Object.keys(skusByCurrency).length > 1">
<div class="currency-selector-header">
<el-icon class="currency-icon"><Location /></el-icon>
<span class="selector-label">{{ t('product.selectCurrency') }}</span>
</div>
<div class="currency-selector-content">
<el-radio-group v-model="currentCurrency" size="default" class="currency-radio-group">
<el-radio-button
v-for="(skus, currency) in skusByCurrency"
:key="currency"
:label="currency"
class="currency-radio-button"
>
{{ getCurrencyName(currency) }}
</el-radio-button>
</el-radio-group>
</div>
</div>
<!-- SKU选择区域 -->
<div class="sku-selection-section" v-if="currentCurrencySkus.length > 0">
<div class="sku-section-title">{{ t('product.selectSku') }}</div>
<div class="sku-list">
<div
v-for="(sku, index) in currentCurrencySkus"
:key="sku.id || index"
:class="['sku-item', {
active: selectedSku && selectedSku.id === sku.id,
disabled: !sku.stock || sku.stock === 0
}]"
@click="selectSku(sku)"
>
<div class="sku-info">
<div class="sku-name">{{ sku.sku }}</div>
<div class="sku-price-stock">
<span class="sku-price">{{ sku.currency }} {{ formatPrice(sku.price) }}</span>
<span class="sku-stock" :class="{ 'stock-zero': !sku.stock || sku.stock === 0 }">
{{ t('product.stock') }}{{ sku.stock || 0 }}
</span>
</div>
</div>
<div class="sku-check" v-if="selectedSku && selectedSku.id === sku.id">
<el-icon><Check /></el-icon>
</div>
<div class="sku-disabled-mask" v-if="!sku.stock || sku.stock === 0">
{{ t('product.outOfStock') }}
</div>
</div>
</div>
</div>
<div v-else class="no-sku-tip">
<el-alert type="warning" :closable="false">
{{ t('product.noSkuAvailable') }}
</el-alert>
</div>
<!-- 价格区域显示选中SKU的价格 -->
<div class="price-section" v-if="selectedSku">
<div class="price-label">{{ t('product.currentPrice') }}</div>
<div class="price-main">
<span class="currency">{{ selectedSku.currency }}</span>
<span class="price">{{ formatPrice(selectedSku.price) }}</span>
</div>
</div>
<div class="price-section" v-else>
<div class="price-label">{{ t('product.selectSkuForPrice') }}</div>
</div>
<!-- 数量选择 -->
<div class="quantity-section">
<div class="quantity-label">{{ t('product.quantity') }}</div>
<el-input-number
v-model="quantity"
:min="1"
:max="selectedSku ? (selectedSku.stock || 999) : 999"
:disabled="!selectedSku || !selectedSku.stock || selectedSku.stock === 0"
class="quantity-input"
/>
<span class="stock-info">{{ t('product.stock') }}{{ selectedSku ? (selectedSku.stock || 0) : t('product.selectSku') }}</span>
</div>
<!-- 服务保障 -->
<div class="service-section">
<div class="service-item">
<el-icon><Goods /></el-icon>
<span>{{ t('product.sevenDayReturn') }}</span>
</div>
<div class="service-item">
<el-icon><Lock /></el-icon>
<span>{{ t('product.authenticGuarantee') }}</span>
</div>
<div class="service-item">
<el-icon><Clock /></el-icon>
<span>{{ t('product.expeditedShipping') }}</span>
</div>
</div>
<!-- 操作按钮 -->
<div class="action-buttons">
<el-button
type="primary"
size="large"
class="buy-now-btn"
@click="handleBuyNow"
:loading="loading"
:disabled="!selectedSku || !selectedSku.stock || selectedSku.stock === 0"
>
<el-icon><ShoppingCart /></el-icon>
{{ t('product.buyNow') }}
</el-button>
</div>
</div>
</div>
<!-- 商品详情区域 -->
<div class="product-tabs">
<el-tabs v-model="activeTab" class="detail-tabs">
<el-tab-pane :label="t('product.productDetails')" name="detail">
<div class="tab-content">
<div v-if="product.description" v-html="product.description" class="product-description"></div>
<div v-else class="empty-description">
<el-empty :description="t('product.noProductDetails')" />
</div>
</div>
</el-tab-pane>
<el-tab-pane :label="t('product.specifications')" name="params">
<div class="tab-content">
<el-descriptions :column="2" border v-if="selectedSku">
<el-descriptions-item :label="t('product.skuCode')">{{ selectedSku.sku }}</el-descriptions-item>
<el-descriptions-item :label="t('product.price')">{{ selectedSku.currency }} {{ formatPrice(selectedSku.price) }}</el-descriptions-item>
<el-descriptions-item :label="t('product.stock')">{{ selectedSku.stock || 0 }}</el-descriptions-item>
<el-descriptions-item :label="t('product.weight')" v-if="selectedSku.weight">
{{ selectedSku.weight }}g
</el-descriptions-item>
<el-descriptions-item :label="t('product.size')" v-if="selectedSku.size" :span="2">
{{ formatSize(selectedSku.size) }}
</el-descriptions-item>
</el-descriptions>
<el-empty v-else :description="t('product.selectSkuForSpecs')" />
</div>
</el-tab-pane>
</el-tabs>
</div>
<!-- 购买确认对话框 -->
<el-dialog
v-model="showConfirmDialog"
:title="t('product.confirmPurchaseInfo')"
:width="isMobile ? '95%' : '600px'"
:close-on-click-modal="false"
class="confirm-dialog"
>
<div class="confirm-dialog-content">
<!-- 商品信息 -->
<div class="confirm-product-info">
<el-image
:src="currentImage"
fit="cover"
class="confirm-product-image"
/>
<div class="confirm-product-details">
<div class="confirm-product-name">{{ product.name }}</div>
<div class="confirm-sku-info" v-if="selectedSku">
<span class="confirm-sku-label">{{ t('product.sku') }}</span>
<span class="confirm-sku-value">{{ selectedSku.sku }}</span>
</div>
</div>
</div>
<!-- 价格和数量 -->
<el-divider />
<div class="confirm-price-section">
<div class="confirm-price-item">
<span class="confirm-price-label">{{ t('product.unitPrice') }}</span>
<span class="confirm-price-value">
{{ selectedSku ? selectedSku.currency : '' }} {{ formatPrice(selectedSku ? selectedSku.price : 0) }}
</span>
</div>
<div class="confirm-price-item">
<span class="confirm-price-label">{{ t('product.quantity') }}</span>
<span class="confirm-price-value">{{ quantity }}</span>
</div>
<div class="confirm-price-item confirm-total">
<span class="confirm-price-label">{{ t('product.total') }}</span>
<span class="confirm-price-value confirm-total-price">
{{ selectedSku ? selectedSku.currency : '' }} {{ formatPrice(selectedSku ? (selectedSku.price * quantity) : 0) }}
</span>
</div>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="showConfirmDialog = false">{{ t('product.cancel') }}</el-button>
<el-button type="primary" @click="confirmBuy" :loading="loading">
{{ t('product.confirmPurchase') }}
</el-button>
</div>
</template>
</el-dialog>
</template>
</div>
</template>
<script setup>
import { ref, computed, onMounted, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { ElMessage } from 'element-plus'
// 图标已全局注册在 main.js 中,直接使用组件名称即可
import { getProduct, getProductByLinkCode } from '../api/product'
import { formatAmount } from '../utils/helpers'
import { loadTranslationByCurrency } from '../i18n'
const { t } = useI18n()
const route = useRoute()
const router = useRouter()
const loading = ref(false)
const productLoading = ref(true)
const currentImageIndex = ref(0)
// 鼠标悬停的主图(优先展示)
const hoveredImage = ref(null)
const quantity = ref(1)
const activeTab = ref('detail')
const selectedSku = ref(null)
const showConfirmDialog = ref(false)
// 移动端检测
const isMobile = computed(() => {
if (typeof window !== 'undefined') {
return window.innerWidth <= 768
}
return false
})
// 商品数据
const product = ref({
id: null,
name: '',
price: null,
mainImage: '',
mainImages: [],
status: '',
shopId: null,
skus: []
})
// 国家名称映射(使用对应国家的本地语言,固定不变)
const countryNamesByCurrency = {
'USD': 'United States', // 美国 - 英语
'CNY': '中国', // 中国 - 中文
'MYR': 'Malaysia', // 马来西亚 - 马来语(英语也是官方语言)
'PHP': 'Pilipinas', // 菲律宾 - 菲律宾语
'THB': 'ประเทศไทย', // 泰国 - 泰语
'VND': 'Việt Nam', // 越南 - 越南语
'SGD': 'Singapore', // 新加坡 - 英语
'EUR': 'Europe', // 欧洲 - 英语
'GBP': 'United Kingdom' // 英国 - 英语
}
// 按货币分组SKU
const skusByCurrency = computed(() => {
if (!product.value.skus || product.value.skus.length === 0) {
return {}
}
const grouped = {}
product.value.skus.forEach(sku => {
const currency = sku.currency || 'USD'
if (!grouped[currency]) {
grouped[currency] = []
}
grouped[currency].push(sku)
})
return grouped
})
// 当前货币(默认使用第一个货币)
const currentCurrency = ref('')
// 当前货币的SKU列表
const currentCurrencySkus = computed(() => {
if (!currentCurrency.value || !skusByCurrency.value[currentCurrency.value]) {
return []
}
return skusByCurrency.value[currentCurrency.value]
})
// 当前显示的图片
const currentImage = computed(() => {
// 1) 若有悬停主图,优先展示
if (hoveredImage.value) {
return hoveredImage.value
}
// 2) 已选SKU且有SKU图
if (selectedSku.value && selectedSku.value.skuImage) {
return selectedSku.value.skuImage
}
// 3) 主图列表
if (product.value.mainImages && product.value.mainImages.length > 0) {
return product.value.mainImages[currentImageIndex.value] || product.value.mainImages[0]
}
// 4) 兜底单张主图
return product.value.mainImage || ''
})
// 获取国家名称(使用对应国家的本地语言,固定不变)
const getCurrencyName = (currency) => {
return countryNamesByCurrency[currency] || currency
}
// 选择SKU
const selectSku = (sku) => {
if (!sku.stock || sku.stock === 0) {
ElMessage.warning('该SKU已缺货')
return
}
selectedSku.value = sku
// 如果SKU有图片更新当前图片
if (sku.skuImage) {
const imageIndex = product.value.mainImages?.findIndex(img => img === sku.skuImage)
if (imageIndex >= 0) {
currentImageIndex.value = imageIndex
}
}
}
// 格式化价格
const formatPrice = (price) => {
if (!price) return '0.00'
return formatAmount(price)
}
// 格式化尺寸
const formatSize = (sizeJson) => {
if (!sizeJson) return '-'
try {
const size = typeof sizeJson === 'string' ? JSON.parse(sizeJson) : sizeJson
if (size.length && size.width && size.height) {
return `${size.length} × ${size.width} × ${size.height} ${size.unit || 'cm'}`
}
return sizeJson
} catch (e) {
return sizeJson
}
}
// 选择图片
const selectImage = (index) => {
currentImageIndex.value = index
}
// 悬停主图时优先展示该主图
const hoverImage = (img) => {
hoveredImage.value = img
}
// 立即购买
const handleBuyNow = () => {
if (!selectedSku.value) {
ElMessage.warning('请先选择商品SKU')
return
}
if (selectedSku.value.stock && quantity.value > selectedSku.value.stock) {
ElMessage.warning('库存不足')
return
}
showConfirmDialog.value = true
}
// 确认购买
const confirmBuy = () => {
if (!selectedSku.value) {
ElMessage.warning('请先选择商品SKU')
return
}
showConfirmDialog.value = false
// 构建订单数据
const orderData = {
product: {
id: product.value.id,
name: product.value.name,
price: selectedSku.value.price,
currency: selectedSku.value.currency,
sku: selectedSku.value.sku,
skuId: selectedSku.value.id,
quantity: quantity.value,
stock: selectedSku.value.stock || 0,
image: selectedSku.value.skuImage || currentImage.value
}
}
// 跳转到创建订单页面(填写收货信息)
router.push({
path: '/create-order',
query: {
data: encodeURIComponent(JSON.stringify(orderData))
}
})
}
// 判断是否为链接码32位字符串
const isLinkCode = (id) => {
if (!id) return false
const idStr = String(id)
// 链接码是32位字符串UUID去掉横线
return idStr.length === 32 && /^[a-f0-9]+$/i.test(idStr)
}
// 加载商品详情
const loadProductDetail = async (id) => {
productLoading.value = true
try {
let response
// 判断是商品ID还是链接码
if (isLinkCode(id)) {
// 使用链接码获取商品
response = await getProductByLinkCode(id)
} else {
// 使用商品ID获取商品
response = await getProduct(id)
}
if (response.code === '0000' && response.data) {
product.value = response.data
// 初始化货币选择(使用第一个货币)
const currencies = Object.keys(skusByCurrency.value)
if (currencies.length > 0) {
currentCurrency.value = currencies[0]
// 自动选择第一个有库存的SKU
const firstAvailableSku = currentCurrencySkus.value.find(sku => sku.stock && sku.stock > 0)
if (firstAvailableSku) {
selectedSku.value = firstAvailableSku
}
}
} else {
ElMessage.error(response.message || '获取商品详情失败')
// 如果获取失败设置product.id为null以显示空状态
product.value.id = null
}
} catch (error) {
console.error('加载商品详情失败:', error)
ElMessage.error(error.response?.data?.message || '加载商品详情失败,请稍后重试')
} finally {
productLoading.value = false
}
}
// 监听货币变化重置选中的SKU并加载翻译
watch(currentCurrency, async (newCurrency) => {
// 加载对应货币的翻译
if (newCurrency) {
await loadTranslationByCurrency(newCurrency)
}
selectedSku.value = null
const firstAvailableSku = currentCurrencySkus.value.find(sku => sku.stock && sku.stock > 0)
if (firstAvailableSku) {
selectedSku.value = firstAvailableSku
}
}, { immediate: false })
onMounted(() => {
const id = route.params.id
if (id) {
loadProductDetail(id)
} else {
// 如果没有ID显示提示并停止加载
ElMessage.error('商品ID或链接码不能为空')
productLoading.value = false
// 客户访问时不应该跳转到管理页面,只显示错误提示
product.value.id = null
}
})
</script>
<style scoped>
.product-detail {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background-color: #f5f7fa;
min-height: calc(100vh - 60px);
}
.loading-container {
background: white;
padding: 40px;
border-radius: 12px;
margin-bottom: 20px;
}
/* 商品主区域 */
.product-main {
display: flex;
gap: 30px;
background: white;
padding: 40px;
border-radius: 12px;
margin-bottom: 20px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
}
/* 左侧图片区域 */
.product-images {
flex: 0 0 450px;
}
.main-image {
width: 100%;
height: 450px;
border: 1px solid #e8e8e8;
border-radius: 8px;
margin-bottom: 15px;
overflow: hidden;
background: #fafafa;
cursor: pointer;
transition: all 0.3s;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.main-image:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
}
.main-img {
width: 100%;
height: 100%;
}
.image-slot {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
background: #f5f7fa;
color: #909399;
}
.thumbnail-list {
display: flex;
gap: 10px;
justify-content: center;
}
.thumbnail-item {
width: 80px;
height: 80px;
border: 2px solid transparent;
border-radius: 4px;
overflow: hidden;
cursor: pointer;
transition: all 0.3s;
}
.thumbnail-item:hover {
border-color: #409eff;
}
.thumbnail-item.active {
border-color: #409eff;
}
.thumbnail-img {
width: 100%;
height: 100%;
}
/* 右侧商品信息 */
.product-info {
flex: 1;
padding-left: 20px;
}
.product-title {
font-size: 26px;
font-weight: 700;
color: #333;
margin: 0 0 20px 0;
line-height: 1.5;
letter-spacing: 0.5px;
}
/* 货币选择器卡片 - 紧凑但醒目,适合移动端 */
.currency-selector-card {
margin-top: 15px;
margin-bottom: 20px;
padding: 12px 16px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 10px;
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.3);
border: 2px solid rgba(255, 255, 255, 0.3);
position: relative;
overflow: hidden;
}
.currency-selector-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, #ffd700, #ffed4e, #ffd700);
animation: shimmer 2s infinite;
}
@keyframes shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
.currency-selector-header {
display: flex;
align-items: center;
margin-bottom: 10px;
color: white;
}
.currency-icon {
font-size: 18px;
margin-right: 6px;
color: #ffd700;
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.2));
}
.currency-selector-header .selector-label {
font-size: 14px;
font-weight: 600;
color: white;
margin: 0;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
.currency-selector-content {
background: white;
padding: 10px;
border-radius: 8px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
}
.currency-radio-group {
width: 100%;
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.currency-radio-button {
flex: 1;
min-width: 0;
}
.currency-radio-button :deep(.el-radio-button__inner) {
font-size: 13px;
font-weight: 500;
padding: 8px 12px;
border-radius: 6px;
transition: all 0.3s;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* 移动端:紧凑布局,全部展示在一行 */
@media (max-width: 768px) {
.currency-selector-content {
padding: 8px;
}
.currency-radio-group {
display: flex;
flex-wrap: nowrap;
gap: 4px;
width: 100%;
}
.currency-radio-button {
flex: 1;
min-width: 0;
}
.currency-radio-button :deep(.el-radio-button__inner) {
font-size: 11px;
padding: 6px 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.currency-radio-button :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-color: #667eea;
color: white;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.4);
}
/* 旧样式保留(兼容性) */
.currency-selector {
margin-bottom: 25px;
padding: 15px;
background: #f5f7fa;
border-radius: 8px;
}
.selector-label {
font-size: 14px;
color: #606266;
margin-right: 15px;
font-weight: 500;
}
/* SKU选择区域 */
.sku-selection-section {
margin-bottom: 25px;
}
.sku-section-title {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 15px;
}
.sku-list {
display: flex;
flex-direction: column;
gap: 12px;
max-height: 400px;
overflow-y: auto;
}
.sku-item {
position: relative;
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
border: 2px solid #e4e7ed;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
background: #fff;
}
.sku-item:hover:not(.disabled) {
border-color: #409eff;
background: #f0f9ff;
}
.sku-item.active:not(.disabled) {
border-color: #409eff;
background: #ecf5ff;
}
.sku-item.disabled {
cursor: not-allowed;
opacity: 0.6;
background: #f5f7fa;
}
.sku-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.sku-name {
font-size: 14px;
color: #303133;
font-weight: 500;
line-height: 1.5;
}
.sku-price-stock {
display: flex;
align-items: center;
gap: 15px;
}
.sku-price {
font-size: 18px;
color: #f56c6c;
font-weight: 600;
}
.sku-stock {
font-size: 14px;
color: #909399;
}
.sku-stock.stock-zero {
color: #f56c6c;
font-weight: 500;
}
.sku-check {
color: #409eff;
font-size: 20px;
}
.sku-disabled-mask {
position: absolute;
top: 50%;
right: 50px;
transform: translateY(-50%);
background: rgba(0, 0, 0, 0.6);
color: white;
padding: 4px 12px;
border-radius: 4px;
font-size: 12px;
}
.no-sku-tip {
margin-bottom: 25px;
}
/* 价格区域 */
.price-section {
background: linear-gradient(135deg, #fff5f5 0%, #ffeef0 100%);
padding: 25px;
border-radius: 8px;
margin-bottom: 25px;
border: 1px solid #ffe0e0;
}
.price-label {
font-size: 14px;
color: #999;
margin-bottom: 10px;
}
.price-main {
display: flex;
align-items: baseline;
margin-bottom: 10px;
}
.currency {
font-size: 20px;
color: #f56c6c;
font-weight: 700;
margin-right: 4px;
}
.price {
font-size: 42px;
color: #f56c6c;
font-weight: 700;
letter-spacing: -1px;
}
/* 数量选择 */
.quantity-section {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 25px;
padding: 15px 0;
border-top: 1px solid #eee;
border-bottom: 1px solid #eee;
}
.quantity-label {
font-size: 14px;
color: #666;
font-weight: 500;
}
.quantity-input {
width: 120px;
}
.stock-info {
font-size: 14px;
color: #999;
}
/* 服务保障 */
.service-section {
display: flex;
gap: 40px;
margin-bottom: 30px;
padding: 20px;
background: linear-gradient(135deg, #f9f9f9 0%, #f5f5f5 100%);
border-radius: 8px;
border: 1px solid #eee;
}
.service-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: #666;
}
.service-item .el-icon {
color: #409eff;
font-size: 18px;
}
/* 操作按钮 */
.action-buttons {
display: flex;
gap: 15px;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #eee;
}
.buy-now-btn {
flex: 1;
height: 52px;
font-size: 16px;
font-weight: 600;
background: linear-gradient(135deg, #ff9500 0%, #ff6b00 100%);
border: none;
box-shadow: 0 4px 12px rgba(255, 107, 0, 0.3);
transition: all 0.3s;
}
.buy-now-btn:hover:not(:disabled) {
background: linear-gradient(135deg, #ff6b00 0%, #ff9500 100%);
box-shadow: 0 6px 16px rgba(255, 107, 0, 0.4);
transform: translateY(-2px);
}
.pay-now-btn {
flex: 1;
height: 52px;
font-size: 16px;
font-weight: 600;
background: linear-gradient(135deg, #f56c6c 0%, #ff4757 100%);
border: none;
box-shadow: 0 4px 12px rgba(245, 108, 108, 0.3);
transition: all 0.3s;
}
.pay-now-btn:hover:not(:disabled) {
background: linear-gradient(135deg, #ff4757 0%, #f56c6c 100%);
box-shadow: 0 6px 16px rgba(245, 108, 108, 0.4);
transform: translateY(-2px);
}
/* 确认对话框样式 */
.confirm-dialog-content {
padding: 10px 0;
}
.confirm-product-info {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.confirm-product-image {
width: 120px;
height: 120px;
border-radius: 8px;
border: 1px solid #e4e7ed;
flex-shrink: 0;
}
.confirm-product-details {
flex: 1;
display: flex;
flex-direction: column;
gap: 12px;
}
.confirm-product-name {
font-size: 16px;
font-weight: 600;
color: #303133;
line-height: 1.5;
}
/* 移动端:弹窗优化 */
@media (max-width: 768px) {
.confirm-dialog :deep(.el-dialog) {
margin: 5vh auto !important;
max-height: 90vh;
display: flex;
flex-direction: column;
}
.confirm-dialog :deep(.el-dialog__body) {
flex: 1;
overflow-y: auto;
padding: 15px;
}
.confirm-dialog-content {
padding: 0;
}
.confirm-product-info {
flex-direction: column;
gap: 12px;
margin-bottom: 15px;
}
.confirm-product-image {
width: 100%;
height: 200px;
align-self: center;
}
.confirm-product-name {
font-size: 14px;
}
.confirm-price-section {
gap: 10px;
}
.confirm-price-item {
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
.dialog-footer {
display: flex;
flex-direction: column;
gap: 10px;
padding: 10px 0;
}
.dialog-footer .el-button {
width: 100%;
margin: 0;
}
}
.confirm-sku-info {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: #f5f7fa;
border-radius: 4px;
border-left: 3px solid #409eff;
}
.confirm-sku-label {
font-size: 14px;
color: #606266;
font-weight: 500;
}
.confirm-sku-value {
font-size: 14px;
color: #303133;
font-weight: 600;
font-family: 'Courier New', monospace;
background: #fff;
padding: 2px 8px;
border-radius: 4px;
border: 1px solid #dcdfe6;
}
.confirm-price-section {
display: flex;
flex-direction: column;
gap: 12px;
}
.confirm-price-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
}
.confirm-price-label {
font-size: 14px;
color: #606266;
}
.confirm-price-value {
font-size: 14px;
color: #303133;
font-weight: 500;
}
.confirm-total {
padding-top: 12px;
border-top: 1px solid #e4e7ed;
margin-top: 8px;
}
.confirm-total-price {
font-size: 20px;
color: #f56c6c;
font-weight: 700;
}
/* 商品详情标签页 */
.product-tabs {
background: white;
padding: 30px;
border-radius: 12px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
margin-top: 20px;
}
.detail-tabs {
margin-top: 20px;
}
.tab-content {
padding: 20px 0;
min-height: 300px;
}
.product-description {
line-height: 1.8;
color: #666;
}
.empty-description {
text-align: center;
padding: 60px 0;
}
/* 空状态容器 */
.empty-container {
background: white;
padding: 60px 20px;
border-radius: 12px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
margin-top: 20px;
text-align: center;
}
.empty-tip {
margin-top: 20px;
color: #909399;
font-size: 14px;
line-height: 1.6;
}
/* 响应式设计 */
@media (max-width: 768px) {
.product-detail {
padding: 10px;
}
.product-main {
flex-direction: column;
padding: 15px;
gap: 20px;
}
.product-images {
flex: 1;
width: 100%;
}
.main-image {
height: 300px;
}
.product-info {
padding-left: 0;
}
.product-title {
font-size: 20px;
}
.sku-list {
max-height: 300px;
}
.action-buttons {
flex-direction: column;
}
.buy-now-btn,
.pay-now-btn {
width: 100%;
}
.service-section {
flex-direction: column;
gap: 15px;
}
.currency-selector-card {
padding: 10px 12px;
margin-top: 10px;
margin-bottom: 15px;
}
.currency-selector-header {
margin-bottom: 8px;
}
.currency-icon {
font-size: 16px;
margin-right: 5px;
}
.currency-selector-header .selector-label {
font-size: 13px;
}
.currency-selector-content {
padding: 8px;
}
.currency-radio-group {
display: flex;
flex-wrap: nowrap;
gap: 4px;
width: 100%;
}
.currency-radio-button {
flex: 1;
min-width: 0;
}
.currency-radio-button :deep(.el-radio-button__inner) {
font-size: 11px;
padding: 6px 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}
/* 旧样式兼容 */
.currency-selector {
flex-direction: column;
gap: 10px;
}
.selector-label {
margin-right: 0;
margin-bottom: 10px;
}
}
</style>