feat(order): 实现国际化支持并优化创建订单界面
- 将创建订单界面的所有静态文本替换为国际化标签 - 实现在移动端和桌面端的响应式表单布局 - 添加多语言国家名称显示功能 - 集成Vue I18n国际化框架到应用主入口 - 优化订单确认页面的国际化文本显示 - 移除导航菜单中的创建订单项 - 添加针对不同国家的地址字段验证规则 - 实现越南地址层级映射逻辑 - 添加货币代码自动设置国家功能
This commit is contained in:
@@ -7,8 +7,8 @@
|
||||
|
||||
<!-- 无商品数据提示 -->
|
||||
<div v-else-if="!product.id" class="empty-container">
|
||||
<el-empty description="商品不存在或链接已过期">
|
||||
<p class="empty-tip">该商品链接可能已失效,请联系商家获取新的商品链接</p>
|
||||
<el-empty :description="t('product.productNotExist')">
|
||||
<p class="empty-tip">{{ t('product.linkExpired') }}</p>
|
||||
</el-empty>
|
||||
</div>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<el-icon><Picture /></el-icon>
|
||||
<div>图片加载失败</div>
|
||||
<div>{{ t('product.imageLoadFailed') }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
@@ -53,23 +53,29 @@
|
||||
<div class="product-info">
|
||||
<h1 class="product-title">{{ product.name }}</h1>
|
||||
|
||||
<!-- 货币选择(如果有多个货币) -->
|
||||
<div class="currency-selector" v-if="Object.keys(skusByCurrency).length > 1">
|
||||
<span class="selector-label">选择货币:</span>
|
||||
<el-radio-group v-model="currentCurrency" size="small">
|
||||
<el-radio-button
|
||||
v-for="(skus, currency) in skusByCurrency"
|
||||
:key="currency"
|
||||
:label="currency"
|
||||
>
|
||||
{{ getCurrencyName(currency) }}
|
||||
</el-radio-button>
|
||||
</el-radio-group>
|
||||
<!-- 货币和语言选择(如果有多个货币)- 突出显示 -->
|
||||
<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">选择商品规格(SKU)</div>
|
||||
<div class="sku-section-title">{{ t('product.selectSku') }}</div>
|
||||
<div class="sku-list">
|
||||
<div
|
||||
v-for="(sku, index) in currentCurrencySkus"
|
||||
@@ -85,7 +91,7 @@
|
||||
<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 }">
|
||||
库存:{{ sku.stock || 0 }}
|
||||
{{ t('product.stock') }}:{{ sku.stock || 0 }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -93,32 +99,32 @@
|
||||
<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">
|
||||
暂无可用SKU
|
||||
{{ t('product.noSkuAvailable') }}
|
||||
</el-alert>
|
||||
</div>
|
||||
|
||||
<!-- 价格区域(显示选中SKU的价格) -->
|
||||
<div class="price-section" v-if="selectedSku">
|
||||
<div class="price-label">现价</div>
|
||||
<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">请选择SKU查看价格</div>
|
||||
<div class="price-label">{{ t('product.selectSkuForPrice') }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 数量选择 -->
|
||||
<div class="quantity-section">
|
||||
<div class="quantity-label">数量:</div>
|
||||
<div class="quantity-label">{{ t('product.quantity') }}:</div>
|
||||
<el-input-number
|
||||
v-model="quantity"
|
||||
:min="1"
|
||||
@@ -126,22 +132,22 @@
|
||||
:disabled="!selectedSku || !selectedSku.stock || selectedSku.stock === 0"
|
||||
class="quantity-input"
|
||||
/>
|
||||
<span class="stock-info">库存:{{ selectedSku ? (selectedSku.stock || 0) : '请选择SKU' }}</span>
|
||||
<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>7天无理由退货</span>
|
||||
<span>{{ t('product.sevenDayReturn') }}</span>
|
||||
</div>
|
||||
<div class="service-item">
|
||||
<el-icon><Lock /></el-icon>
|
||||
<span>正品保证</span>
|
||||
<span>{{ t('product.authenticGuarantee') }}</span>
|
||||
</div>
|
||||
<div class="service-item">
|
||||
<el-icon><Clock /></el-icon>
|
||||
<span>极速发货</span>
|
||||
<span>{{ t('product.expeditedShipping') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -156,7 +162,7 @@
|
||||
:disabled="!selectedSku || !selectedSku.stock || selectedSku.stock === 0"
|
||||
>
|
||||
<el-icon><ShoppingCart /></el-icon>
|
||||
立即购买
|
||||
{{ t('product.buyNow') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -165,28 +171,28 @@
|
||||
<!-- 商品详情区域 -->
|
||||
<div class="product-tabs">
|
||||
<el-tabs v-model="activeTab" class="detail-tabs">
|
||||
<el-tab-pane label="商品详情" name="detail">
|
||||
<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="暂无商品详情" />
|
||||
<el-empty :description="t('product.noProductDetails')" />
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="规格参数" name="params">
|
||||
<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="SKU编码">{{ selectedSku.sku }}</el-descriptions-item>
|
||||
<el-descriptions-item label="价格">{{ selectedSku.currency }} {{ formatPrice(selectedSku.price) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="库存">{{ selectedSku.stock || 0 }}</el-descriptions-item>
|
||||
<el-descriptions-item label="重量" v-if="selectedSku.weight">
|
||||
<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="尺寸" v-if="selectedSku.size" :span="2">
|
||||
<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="请先选择SKU查看规格参数" />
|
||||
<el-empty v-else :description="t('product.selectSkuForSpecs')" />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
@@ -195,9 +201,10 @@
|
||||
<!-- 购买确认对话框 -->
|
||||
<el-dialog
|
||||
v-model="showConfirmDialog"
|
||||
title="确认购买信息"
|
||||
width="600px"
|
||||
:title="t('product.confirmPurchaseInfo')"
|
||||
:width="isMobile ? '95%' : '600px'"
|
||||
:close-on-click-modal="false"
|
||||
class="confirm-dialog"
|
||||
>
|
||||
<div class="confirm-dialog-content">
|
||||
<!-- 商品信息 -->
|
||||
@@ -210,7 +217,7 @@
|
||||
<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">SKU:</span>
|
||||
<span class="confirm-sku-label">{{ t('product.sku') }}:</span>
|
||||
<span class="confirm-sku-value">{{ selectedSku.sku }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -220,17 +227,17 @@
|
||||
<el-divider />
|
||||
<div class="confirm-price-section">
|
||||
<div class="confirm-price-item">
|
||||
<span class="confirm-price-label">单价:</span>
|
||||
<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">数量:</span>
|
||||
<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">总计:</span>
|
||||
<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>
|
||||
@@ -240,9 +247,9 @@
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="showConfirmDialog = false">取消</el-button>
|
||||
<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>
|
||||
@@ -254,10 +261,14 @@
|
||||
<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()
|
||||
@@ -272,6 +283,14 @@ 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,
|
||||
@@ -284,17 +303,17 @@ const product = ref({
|
||||
skus: []
|
||||
})
|
||||
|
||||
// 货币名称映射
|
||||
const currencyNames = {
|
||||
'USD': '美元',
|
||||
'CNY': '人民币',
|
||||
'MYR': '马来西亚林吉特',
|
||||
'PHP': '菲律宾比索',
|
||||
'THB': '泰铢',
|
||||
'VND': '越南盾',
|
||||
'SGD': '新加坡元',
|
||||
'EUR': '欧元',
|
||||
'GBP': '英镑'
|
||||
// 国家名称映射(使用对应国家的本地语言,固定不变)
|
||||
const countryNamesByCurrency = {
|
||||
'USD': 'United States', // 美国 - 英语
|
||||
'CNY': '中国', // 中国 - 中文
|
||||
'MYR': 'Malaysia', // 马来西亚 - 马来语(英语也是官方语言)
|
||||
'PHP': 'Pilipinas', // 菲律宾 - 菲律宾语
|
||||
'THB': 'ประเทศไทย', // 泰国 - 泰语
|
||||
'VND': 'Việt Nam', // 越南 - 越南语
|
||||
'SGD': 'Singapore', // 新加坡 - 英语
|
||||
'EUR': 'Europe', // 欧洲 - 英语
|
||||
'GBP': 'United Kingdom' // 英国 - 英语
|
||||
}
|
||||
|
||||
// 按货币分组SKU
|
||||
@@ -344,9 +363,9 @@ const currentImage = computed(() => {
|
||||
return product.value.mainImage || ''
|
||||
})
|
||||
|
||||
// 获取货币名称
|
||||
// 获取国家名称(使用对应国家的本地语言,固定不变)
|
||||
const getCurrencyName = (currency) => {
|
||||
return currencyNames[currency] || currency
|
||||
return countryNamesByCurrency[currency] || currency
|
||||
}
|
||||
|
||||
// 选择SKU
|
||||
@@ -491,14 +510,18 @@ const loadProductDetail = async (id) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 监听货币变化,重置选中的SKU
|
||||
watch(currentCurrency, () => {
|
||||
// 监听货币变化,重置选中的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
|
||||
@@ -623,7 +646,122 @@ onMounted(() => {
|
||||
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;
|
||||
@@ -905,6 +1043,64 @@ onMounted(() => {
|
||||
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;
|
||||
@@ -1060,6 +1256,51 @@ onMounted(() => {
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user