feat(order): 实现国际化支持并优化创建订单界面
- 将创建订单界面的所有静态文本替换为国际化标签 - 实现在移动端和桌面端的响应式表单布局 - 添加多语言国家名称显示功能 - 集成Vue I18n国际化框架到应用主入口 - 优化订单确认页面的国际化文本显示 - 移除导航菜单中的创建订单项 - 添加针对不同国家的地址字段验证规则 - 实现越南地址层级映射逻辑 - 添加货币代码自动设置国家功能
This commit is contained in:
@@ -14,7 +14,6 @@
|
||||
:default-active="activeIndex"
|
||||
router
|
||||
>
|
||||
<el-menu-item index="/create-order">创建订单</el-menu-item>
|
||||
<el-menu-item index="/query">订单查询</el-menu-item>
|
||||
<el-menu-item index="/manage/product">商品管理</el-menu-item>
|
||||
</el-menu>
|
||||
|
||||
@@ -4,6 +4,7 @@ import router from './router'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
import i18n from './i18n'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
@@ -14,6 +15,7 @@ for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
|
||||
app.use(router)
|
||||
app.use(ElementPlus)
|
||||
app.use(i18n)
|
||||
|
||||
// 添加错误处理
|
||||
app.config.errorHandler = (err, instance, info) => {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>填写订单信息</span>
|
||||
<span>{{ $t('order.fillOrderInfo') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<el-card v-if="productInfo" class="product-info-card" shadow="never">
|
||||
<template #header>
|
||||
<div class="product-card-header">
|
||||
<span>商品信息</span>
|
||||
<span>{{ $t('order.productInfo') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="product-info-content">
|
||||
@@ -28,11 +28,11 @@
|
||||
<span class="sku-value">{{ productInfo.sku }}</span>
|
||||
</div>
|
||||
<div class="product-info-price">
|
||||
<span class="price-label">单价:</span>
|
||||
<span class="price-label">{{ $t('product.unitPrice') }}:</span>
|
||||
<span class="price-value">{{ productInfo.currency }} {{ formatPrice(productInfo.price) }}</span>
|
||||
<span class="quantity-label">数量:</span>
|
||||
<span class="quantity-label">{{ $t('product.quantity') }}:</span>
|
||||
<span class="quantity-value">x{{ productInfo.quantity }}</span>
|
||||
<span class="total-label">小计:</span>
|
||||
<span class="total-label">{{ $t('order.subtotal') }}:</span>
|
||||
<span class="total-value">{{ productInfo.currency }} {{ formatPrice(productInfo.price * productInfo.quantity) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -44,239 +44,238 @@
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-width="120px"
|
||||
label-position="left"
|
||||
:label-width="isMobile ? 'auto' : '120px'"
|
||||
:label-position="isMobile ? 'top' : 'left'"
|
||||
>
|
||||
<el-divider>客户信息</el-divider>
|
||||
<el-divider>{{ $t('order.customerInfo') }}</el-divider>
|
||||
|
||||
<el-form-item label="客户姓名" prop="customerName">
|
||||
<el-form-item :label="t('order.customerName')" prop="customerName">
|
||||
<el-input
|
||||
v-model="form.customerName"
|
||||
placeholder="请输入客户姓名"
|
||||
:placeholder="t('order.pleaseEnter') + t('order.customerName')"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="客户电话" prop="customerPhone">
|
||||
<el-form-item :label="t('order.customerPhone')" prop="customerPhone">
|
||||
<el-input
|
||||
v-model="form.customerPhone"
|
||||
placeholder="请输入客户电话"
|
||||
:placeholder="t('order.pleaseEnter') + t('order.customerPhone')"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="客户邮箱" prop="customerEmail">
|
||||
<el-form-item :label="t('order.customerEmail')" prop="customerEmail">
|
||||
<el-input
|
||||
v-model="form.customerEmail"
|
||||
placeholder="请输入客户邮箱(可选)"
|
||||
:placeholder="t('order.pleaseEnter') + t('order.customerEmail') + '(' + t('order.optional') + ')'"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider>收货地址</el-divider>
|
||||
<el-alert v-if="currentCountryConfig" :title="`地址格式:${currentCountryConfig.addressFormat}`" type="info" :closable="false" style="margin-bottom: 20px" />
|
||||
<el-divider>{{ $t('order.shippingAddress') }}</el-divider>
|
||||
<el-alert v-if="currentCountryConfig" :title="`${$t('order.addressFormat')}:${currentCountryConfig.addressFormat}`" type="info" :closable="false" style="margin-bottom: 20px" />
|
||||
|
||||
<el-form-item label="收货人姓名" prop="shippingName">
|
||||
<el-form-item :label="t('order.shippingName')" prop="shippingName">
|
||||
<el-input
|
||||
v-model="form.shippingName"
|
||||
placeholder="请输入收货人姓名(需与证件一致,支持当地语言+英文)"
|
||||
:placeholder="t('order.pleaseEnter') + t('order.shippingName') + '(' + t('order.mustMatchId') + ')'"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="收货人电话" prop="shippingPhone">
|
||||
<el-form-item :label="t('order.shippingPhone')" prop="shippingPhone">
|
||||
<el-input
|
||||
v-model="form.shippingPhone"
|
||||
:placeholder="currentCountryConfig ? `请输入收货人电话(国际区号:${currentCountryConfig.phoneCode})` : '请输入收货人电话(带国际区号)'"
|
||||
:placeholder="currentCountryConfig ? `${t('order.pleaseEnter')}${t('order.shippingPhone')}(${t('order.phoneCode')}:${currentCountryConfig.phoneCode})` : t('order.pleaseEnter') + t('order.shippingPhone') + '(' + t('order.phoneCode') + ')'"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="收货国家" prop="shippingCountry">
|
||||
<el-select v-model="form.shippingCountry" placeholder="请选择国家" style="width: 100%">
|
||||
<el-option label="新加坡 (SG)" value="SG" />
|
||||
<el-option label="马来西亚 (MY)" value="MY" />
|
||||
<el-option label="菲律宾 (PH)" value="PH" />
|
||||
<el-option label="泰国 (TH)" value="TH" />
|
||||
<el-option label="越南 (VN)" value="VN" />
|
||||
<el-option label="中国 (CN)" value="CN" />
|
||||
<el-option label="美国 (US)" value="US" />
|
||||
<el-option label="英国 (GB)" value="GB" />
|
||||
<el-option label="德国 (DE)" value="DE" />
|
||||
<el-option label="法国 (FR)" value="FR" />
|
||||
</el-select>
|
||||
<!-- 收货国家(只读显示,根据货币自动确定) -->
|
||||
<el-form-item :label="t('order.shippingCountry')" prop="shippingCountry">
|
||||
<el-input
|
||||
v-model="currentCountryDisplayName"
|
||||
disabled
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 详细地址1(门牌号、街道、楼栋)- 所有国家都显示 -->
|
||||
<el-form-item v-if="showField('shippingAddressLine1')" :label="getFieldLabel('shippingAddressLine1')" prop="shippingAddressLine1">
|
||||
<el-form-item v-if="showField('shippingAddressLine1')" :label="t('order.addressLine1')" prop="shippingAddressLine1">
|
||||
<el-input
|
||||
v-model="form.shippingAddressLine1"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="请输入门牌号、街道、楼栋"
|
||||
:placeholder="t('order.placeholderAddressLine1')"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 详细地址2(楼层、单元号,可选) -->
|
||||
<el-form-item v-if="showField('shippingAddressLine2')" :label="getFieldLabel('shippingAddressLine2')" prop="shippingAddressLine2">
|
||||
<el-form-item v-if="showField('shippingAddressLine2')" :label="t('order.addressLine2')" prop="shippingAddressLine2">
|
||||
<el-input
|
||||
v-model="form.shippingAddressLine2"
|
||||
placeholder="请输入楼层、单元号(可选)"
|
||||
:placeholder="t('order.placeholderAddressLine2')"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 新加坡:组屋号和单元号 -->
|
||||
<template v-if="form.shippingCountry === 'SG'">
|
||||
<el-form-item :label="getFieldLabel('shippingBlockNumber')" prop="shippingBlockNumber">
|
||||
<el-form-item :label="t('order.blockNumber')" prop="shippingBlockNumber">
|
||||
<el-input
|
||||
v-model="form.shippingBlockNumber"
|
||||
placeholder="例如:Blk 123"
|
||||
:placeholder="t('order.placeholderBlockNumber')"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="getFieldLabel('shippingUnitNumber')" prop="shippingUnitNumber">
|
||||
<el-form-item :label="t('order.unitNumber')" prop="shippingUnitNumber">
|
||||
<el-input
|
||||
v-model="form.shippingUnitNumber"
|
||||
placeholder="例如:#01-234"
|
||||
:placeholder="t('order.placeholderUnitNumber')"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<!-- 菲律宾:Barangay -->
|
||||
<el-form-item v-if="form.shippingCountry === 'PH'" :label="getFieldLabel('shippingBarangay')" prop="shippingBarangay">
|
||||
<el-form-item v-if="form.shippingCountry === 'PH'" :label="t('order.barangay')" prop="shippingBarangay">
|
||||
<el-input
|
||||
v-model="form.shippingBarangay"
|
||||
placeholder="请输入Barangay(社区编号)"
|
||||
:placeholder="t('order.placeholderBarangay')"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 泰国:泰文地址 -->
|
||||
<el-form-item v-if="form.shippingCountry === 'TH'" :label="getFieldLabel('shippingAddressThai')" prop="shippingAddressThai">
|
||||
<el-form-item v-if="form.shippingCountry === 'TH'" :label="t('order.addressThai')" prop="shippingAddressThai">
|
||||
<el-input
|
||||
v-model="form.shippingAddressThai"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="请输入泰文地址(支持双语)"
|
||||
:placeholder="t('order.placeholderAddressThai')"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 越南:省、市/郡、区/坊 -->
|
||||
<template v-if="form.shippingCountry === 'VN'">
|
||||
<el-form-item :label="getFieldLabel('shippingProvince')" prop="shippingProvince">
|
||||
<el-form-item :label="t('order.province')" prop="shippingProvince">
|
||||
<el-input
|
||||
v-model="form.shippingProvince"
|
||||
placeholder="请输入省 (Tỉnh)"
|
||||
:placeholder="t('order.placeholderProvinceVN')"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="getFieldLabel('shippingDistrict')" prop="shippingDistrict">
|
||||
<el-form-item :label="t('order.district')" prop="shippingDistrict">
|
||||
<el-input
|
||||
v-model="form.shippingDistrict"
|
||||
placeholder="请输入市/郡 (Thành phố/Huyện)"
|
||||
:placeholder="t('order.placeholderDistrictVN')"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="getFieldLabel('shippingWard')" prop="shippingWard">
|
||||
<el-form-item :label="t('order.ward')" prop="shippingWard">
|
||||
<el-input
|
||||
v-model="form.shippingWard"
|
||||
placeholder="请输入区/坊 (Quận/Phường)"
|
||||
:placeholder="t('order.placeholderWardVN')"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<!-- 马来西亚:州属 -->
|
||||
<el-form-item v-if="form.shippingCountry === 'MY'" :label="getFieldLabel('shippingStateMalaysia')" prop="shippingStateMalaysia">
|
||||
<el-form-item v-if="form.shippingCountry === 'MY'" :label="t('order.stateMalaysia')" prop="shippingStateMalaysia">
|
||||
<el-input
|
||||
v-model="form.shippingStateMalaysia"
|
||||
placeholder="例如:Selangor(雪兰莪)"
|
||||
:placeholder="t('order.placeholderStateMalaysia')"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 泰国:行政区域(区/Tambon) -->
|
||||
<el-form-item v-if="form.shippingCountry === 'TH' && showField('shippingAdministrativeArea')" :label="getFieldLabel('shippingAdministrativeArea')" prop="shippingAdministrativeArea">
|
||||
<el-form-item v-if="form.shippingCountry === 'TH' && showField('shippingAdministrativeArea')" :label="t('order.administrativeArea')" prop="shippingAdministrativeArea">
|
||||
<el-input
|
||||
v-model="form.shippingAdministrativeArea"
|
||||
placeholder="请输入区 (Tambon)"
|
||||
:placeholder="t('order.placeholderAdministrativeArea')"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 城市和州/省(通用字段) -->
|
||||
<el-form-item label="城市/城镇" prop="shippingCity">
|
||||
<el-input
|
||||
v-model="form.shippingCity"
|
||||
:placeholder="form.shippingCountry === 'TH' ? '请输入县 (Amphoe)' : '请输入城市/城镇'"
|
||||
style="width: 48%"
|
||||
clearable
|
||||
/>
|
||||
<el-input
|
||||
v-if="form.shippingCountry !== 'VN' && form.shippingCountry !== 'MY'"
|
||||
v-model="form.shippingState"
|
||||
:placeholder="form.shippingCountry === 'TH' ? '府 (Changwat)' : form.shippingCountry === 'PH' ? '省 (Province)' : '州/省(可选)'"
|
||||
style="width: 48%; margin-left: 4%"
|
||||
clearable
|
||||
/>
|
||||
<!-- 城市和州/省(通用字段)- 越南不使用此字段,使用独立的省/市/郡/区/坊 -->
|
||||
<el-form-item v-if="form.shippingCountry !== 'VN'" :label="getCityLabel()" prop="shippingCity">
|
||||
<div :class="isMobile ? 'mobile-input-group' : 'desktop-input-group'">
|
||||
<el-input
|
||||
v-model="form.shippingCity"
|
||||
:placeholder="getCityPlaceholder()"
|
||||
:style="isMobile ? 'width: 100%' : form.shippingCountry === 'MY' ? 'width: 100%' : 'width: 48%'"
|
||||
clearable
|
||||
/>
|
||||
<!-- 州/省字段:泰国显示府,菲律宾显示省,其他显示州/省(可选) -->
|
||||
<el-input
|
||||
v-if="form.shippingCountry !== 'VN' && form.shippingCountry !== 'MY'"
|
||||
v-model="form.shippingState"
|
||||
:placeholder="getStatePlaceholder()"
|
||||
:style="isMobile ? 'width: 100%; margin-top: 10px' : 'width: 48%; margin-left: 4%'"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 邮编 -->
|
||||
<el-form-item label="邮政编码" prop="shippingPostcode">
|
||||
<el-input
|
||||
v-model="form.shippingPostcode"
|
||||
:placeholder="currentCountryConfig ? `请输入邮编(${currentCountryConfig.postcodeLength}位数字)` : '请输入邮编'"
|
||||
clearable
|
||||
style="width: 48%"
|
||||
>
|
||||
<template #append v-if="postcodeMatching">
|
||||
<el-icon class="is-loading"><el-icon-loading /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<span v-if="currentCountryConfig" style="margin-left: 10px; color: #909399; font-size: 12px">
|
||||
{{ currentCountryConfig.name }}邮编为{{ currentCountryConfig.postcodeLength }}位数字
|
||||
</span>
|
||||
<el-form-item :label="t('order.postcode')" prop="shippingPostcode">
|
||||
<div :class="isMobile ? 'mobile-postcode-group' : 'desktop-postcode-group'">
|
||||
<el-input
|
||||
v-model="form.shippingPostcode"
|
||||
:placeholder="currentCountryConfig ? t('order.placeholderPostcode') + `(${currentCountryConfig.postcodeLength}${t('order.postcodeHint').replace('{0}', '')})` : t('order.placeholderPostcode')"
|
||||
clearable
|
||||
:style="isMobile ? 'width: 100%' : 'width: 48%'"
|
||||
>
|
||||
<template #append v-if="postcodeMatching">
|
||||
<el-icon class="is-loading"><el-icon-loading /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<span v-if="currentCountryConfig" :style="isMobile ? 'display: block; margin-top: 8px; color: #909399; font-size: 12px' : 'margin-left: 10px; color: #909399; font-size: 12px'">
|
||||
{{ currentCountryConfig.name }}{{ t('order.postcodeHint').replace('{0}', currentCountryConfig.postcodeLength) }}
|
||||
</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 楼层/单元/代收点(补充信息) -->
|
||||
<el-form-item label="楼层/单元/代收点" prop="shippingFloorUnit">
|
||||
<el-form-item :label="t('order.floorUnit')" prop="shippingFloorUnit">
|
||||
<el-input
|
||||
v-model="form.shippingFloorUnit"
|
||||
placeholder="楼层、单元号或代收点信息(可选)"
|
||||
:placeholder="t('order.placeholderFloorUnit')"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 兼容旧字段:街道地址(如果新字段为空,使用旧字段) -->
|
||||
<el-form-item v-if="!form.shippingAddressLine1" label="街道地址" prop="shippingStreet">
|
||||
<el-form-item v-if="!form.shippingAddressLine1" :label="t('order.street')" prop="shippingStreet">
|
||||
<el-input
|
||||
v-model="form.shippingStreet"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="请输入详细街道地址"
|
||||
:placeholder="t('order.placeholderStreet')"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="订单备注" prop="remark">
|
||||
<el-form-item :label="t('order.remark')" prop="remark">
|
||||
<el-input
|
||||
v-model="form.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入订单备注(可选)"
|
||||
:placeholder="t('order.placeholderRemark')"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" size="large" @click="submitForm" :loading="loading" style="width: 200px">
|
||||
提交订单
|
||||
{{ $t('order.submit') }}
|
||||
</el-button>
|
||||
<el-button @click="goBack" style="margin-left: 10px">返回</el-button>
|
||||
<el-button @click="goBack" style="margin-left: 10px">{{ $t('order.back') }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
@@ -286,10 +285,15 @@
|
||||
<script setup>
|
||||
import { ref, reactive, computed, watch, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { createCustomerOrder } from '../api/order'
|
||||
import { formatAmount } from '../utils/helpers'
|
||||
import { getCountryConfig, getCountryByCurrency, validatePostcode, getRequiredFields } from '../utils/countryConfig'
|
||||
import { loadTranslationByCurrency } from '../i18n'
|
||||
import i18n from '../i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
@@ -330,6 +334,35 @@ const currentCountryConfig = computed(() => {
|
||||
return getCountryConfig(form.shippingCountry)
|
||||
})
|
||||
|
||||
// 国家显示名称(根据国家代码显示对应语言的名称)
|
||||
const currentCountryDisplayName = computed(() => {
|
||||
if (!form.shippingCountry || !currentCountryConfig.value) {
|
||||
return ''
|
||||
}
|
||||
// 根据当前语言显示国家名称
|
||||
const countryNames = {
|
||||
'SG': { zh: '新加坡', en: 'Singapore', may: 'Singapura', fil: 'Singapore', th: 'สิงคโปร์', vie: 'Singapore', id: 'Singapura' },
|
||||
'MY': { zh: '马来西亚', en: 'Malaysia', may: 'Malaysia', fil: 'Malaysia', th: 'มาเลเซีย', vie: 'Malaysia', id: 'Malaysia' },
|
||||
'PH': { zh: '菲律宾', en: 'Philippines', may: 'Filipina', fil: 'Pilipinas', th: 'ฟิลิปปินส์', vie: 'Philippines', id: 'Filipina' },
|
||||
'TH': { zh: '泰国', en: 'Thailand', may: 'Thailand', fil: 'Thailand', th: 'ประเทศไทย', vie: 'Thailand', id: 'Thailand' },
|
||||
'VN': { zh: '越南', en: 'Vietnam', may: 'Vietnam', fil: 'Vietnam', th: 'เวียดนาม', vie: 'Việt Nam', id: 'Vietnam' },
|
||||
'CN': { zh: '中国', en: 'China', may: 'China', fil: 'China', th: 'จีน', vie: 'Trung Quốc', id: 'China' },
|
||||
'US': { zh: '美国', en: 'United States', may: 'Amerika Syarikat', fil: 'Estados Unidos', th: 'สหรัฐอเมริกา', vie: 'Hoa Kỳ', id: 'Amerika Serikat' },
|
||||
'GB': { zh: '英国', en: 'United Kingdom', may: 'United Kingdom', fil: 'United Kingdom', th: 'สหราชอาณาจักร', vie: 'Vương quốc Anh', id: 'Inggris' },
|
||||
'DE': { zh: '德国', en: 'Germany', may: 'Jerman', fil: 'Alemanya', th: 'เยอรมนี', vie: 'Đức', id: 'Jerman' },
|
||||
'FR': { zh: '法国', en: 'France', may: 'Perancis', fil: 'Pransya', th: 'ฝรั่งเศส', vie: 'Pháp', id: 'Prancis' }
|
||||
}
|
||||
const countryNameMap = countryNames[form.shippingCountry]
|
||||
if (!countryNameMap) {
|
||||
return currentCountryConfig.value.nameEn || form.shippingCountry
|
||||
}
|
||||
// 获取当前语言
|
||||
const currentLang = i18n.global.locale.value || 'zh'
|
||||
const langMap = { 'zh': 'zh', 'en': 'en', 'may': 'may', 'fil': 'fil', 'th': 'th', 'vie': 'vie', 'id': 'id' }
|
||||
const lang = langMap[currentLang] || 'en'
|
||||
return countryNameMap[lang] || countryNameMap.en || form.shippingCountry
|
||||
})
|
||||
|
||||
// 是否显示特定字段
|
||||
const showField = (fieldName) => {
|
||||
if (!currentCountryConfig.value) {
|
||||
@@ -375,6 +408,38 @@ const getFieldLabel = (fieldName) => {
|
||||
return fieldName
|
||||
}
|
||||
|
||||
// 获取城市字段标签(根据国家动态显示)
|
||||
const getCityLabel = () => {
|
||||
if (form.shippingCountry === 'TH') {
|
||||
return t('order.cityTown') + ' (县/Amphoe)'
|
||||
} else if (form.shippingCountry === 'PH') {
|
||||
return t('order.cityTown') + ' (市/City)'
|
||||
} else if (form.shippingCountry === 'SG') {
|
||||
return t('order.cityTown') + ' (城市/City)'
|
||||
} else if (form.shippingCountry === 'MY') {
|
||||
return t('order.cityTown') + ' (城市/City)'
|
||||
}
|
||||
return t('order.cityTown')
|
||||
}
|
||||
|
||||
// 获取城市字段占位符
|
||||
const getCityPlaceholder = () => {
|
||||
if (form.shippingCountry === 'TH') {
|
||||
return t('order.placeholderCityTH')
|
||||
}
|
||||
return t('order.placeholderCity')
|
||||
}
|
||||
|
||||
// 获取州/省字段占位符
|
||||
const getStatePlaceholder = () => {
|
||||
if (form.shippingCountry === 'TH') {
|
||||
return t('order.placeholderStateTH')
|
||||
} else if (form.shippingCountry === 'PH') {
|
||||
return t('order.placeholderStatePH')
|
||||
}
|
||||
return t('order.placeholderStateOptional')
|
||||
}
|
||||
|
||||
// 监听国家变化,清空相关字段
|
||||
watch(() => form.shippingCountry, (newCountry, oldCountry) => {
|
||||
if (newCountry !== oldCountry) {
|
||||
@@ -427,30 +492,30 @@ watch(() => form.shippingPostcode, async (newPostcode) => {
|
||||
const getRules = () => {
|
||||
const baseRules = {
|
||||
customerName: [
|
||||
{ required: true, message: '请输入客户姓名', trigger: 'blur' }
|
||||
{ required: true, message: t('order.validationRequired', [t('order.customerName')]), trigger: 'blur' }
|
||||
],
|
||||
customerPhone: [
|
||||
{ required: true, message: '请输入客户电话', trigger: 'blur' },
|
||||
{ pattern: /^[0-9+\-\s()]+$/, message: '请输入有效的电话号码', trigger: 'blur' }
|
||||
{ required: true, message: t('order.validationRequired', [t('order.customerPhone')]), trigger: 'blur' },
|
||||
{ pattern: /^[0-9+\-\s()]+$/, message: t('order.validationInvalidPhone'), trigger: 'blur' }
|
||||
],
|
||||
customerEmail: [
|
||||
{ type: 'email', message: '请输入有效的邮箱地址', trigger: 'blur' }
|
||||
{ type: 'email', message: t('order.validationInvalidEmail'), trigger: 'blur' }
|
||||
],
|
||||
shippingName: [
|
||||
{ required: true, message: '请输入收货人姓名', trigger: 'blur' }
|
||||
{ required: true, message: t('order.validationRequired', [t('order.shippingName')]), trigger: 'blur' }
|
||||
],
|
||||
shippingPhone: [
|
||||
{ required: true, message: '请输入收货人电话', trigger: 'blur' },
|
||||
{ pattern: /^[0-9+\-\s()]+$/, message: '请输入有效的电话号码', trigger: 'blur' }
|
||||
{ required: true, message: t('order.validationRequired', [t('order.shippingPhone')]), trigger: 'blur' },
|
||||
{ pattern: /^[0-9+\-\s()]+$/, message: t('order.validationInvalidPhone'), trigger: 'blur' }
|
||||
],
|
||||
shippingCountry: [
|
||||
{ required: true, message: '请选择收货国家', trigger: 'change' }
|
||||
{ required: true, message: t('order.validationSelectCountry'), trigger: 'change' }
|
||||
],
|
||||
shippingCity: [
|
||||
{ required: true, message: '请输入收货城市', trigger: 'blur' }
|
||||
{ required: true, message: t('order.validationRequired', [t('order.cityTown')]), trigger: 'blur' }
|
||||
],
|
||||
shippingStreet: [
|
||||
{ required: true, message: '请输入街道地址', trigger: 'blur' }
|
||||
{ required: true, message: t('order.validationRequired', [t('order.street')]), trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -460,17 +525,17 @@ const getRules = () => {
|
||||
|
||||
if (requiredFields.includes('shippingAddressLine1')) {
|
||||
baseRules.shippingAddressLine1 = [
|
||||
{ required: true, message: '请输入详细地址1', trigger: 'blur' }
|
||||
{ required: true, message: t('order.validationRequired', [t('order.addressLine1')]), trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
if (requiredFields.includes('shippingPostcode')) {
|
||||
baseRules.shippingPostcode = [
|
||||
{ required: true, message: '请输入邮编', trigger: 'blur' },
|
||||
{ required: true, message: t('order.validationRequired', [t('order.postcode')]), trigger: 'blur' },
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
if (value && !validatePostcode(form.shippingCountry, value)) {
|
||||
callback(new Error(`邮编格式不正确,应为${currentCountryConfig.value.postcodeLength}位数字`))
|
||||
callback(new Error(t('order.validationPostcodeFormat', [currentCountryConfig.value.postcodeLength])))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
@@ -482,43 +547,50 @@ const getRules = () => {
|
||||
|
||||
if (requiredFields.includes('shippingBlockNumber')) {
|
||||
baseRules.shippingBlockNumber = [
|
||||
{ required: true, message: '请输入组屋号', trigger: 'blur' }
|
||||
{ required: true, message: t('order.validationRequired', [t('order.blockNumber')]), trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
if (requiredFields.includes('shippingUnitNumber')) {
|
||||
baseRules.shippingUnitNumber = [
|
||||
{ required: true, message: '请输入单元号', trigger: 'blur' }
|
||||
{ required: true, message: t('order.validationRequired', [t('order.unitNumber')]), trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
if (requiredFields.includes('shippingBarangay')) {
|
||||
baseRules.shippingBarangay = [
|
||||
{ required: true, message: '请输入Barangay(社区编号)', trigger: 'blur' }
|
||||
{ required: true, message: t('order.validationRequired', [t('order.barangay')]), trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
if (requiredFields.includes('shippingStateMalaysia')) {
|
||||
baseRules.shippingStateMalaysia = [
|
||||
{ required: true, message: '请输入州属', trigger: 'blur' }
|
||||
{ required: true, message: t('order.validationRequired', [t('order.stateMalaysia')]), trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
if (requiredFields.includes('shippingProvince')) {
|
||||
baseRules.shippingProvince = [
|
||||
{ required: true, message: '请输入省', trigger: 'blur' }
|
||||
{ required: true, message: t('order.validationRequired', [t('order.province')]), trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
if (requiredFields.includes('shippingDistrict')) {
|
||||
baseRules.shippingDistrict = [
|
||||
{ required: true, message: '请输入市/郡', trigger: 'blur' }
|
||||
{ required: true, message: t('order.validationRequired', [t('order.district')]), trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
if (requiredFields.includes('shippingWard')) {
|
||||
baseRules.shippingWard = [
|
||||
{ required: true, message: '请输入区/坊', trigger: 'blur' }
|
||||
{ required: true, message: t('order.validationRequired', [t('order.ward')]), trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 泰国:泰文地址必填
|
||||
if (requiredFields.includes('shippingAddressThai')) {
|
||||
baseRules.shippingAddressThai = [
|
||||
{ required: true, message: t('order.validationRequired', [t('order.addressThai')]), trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -539,12 +611,12 @@ const submitForm = async () => {
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (!valid) {
|
||||
ElMessage.error('请填写完整信息')
|
||||
ElMessage.error(t('order.validationFillComplete'))
|
||||
return
|
||||
}
|
||||
|
||||
if (!productInfo.value) {
|
||||
ElMessage.error('商品信息缺失,请重新选择商品')
|
||||
ElMessage.error(t('order.validationProductMissing'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -552,6 +624,14 @@ const submitForm = async () => {
|
||||
|
||||
try {
|
||||
// 构建订单请求数据
|
||||
// 对于越南,将 shippingDistrict(市/郡)映射到 shippingCity,因为后端要求 shippingCity 不能为空
|
||||
// 越南的地址层级:省(Tỉnh)→ 市/郡(Thành phố/Huyện)→ 区/坊(Quận/Phường)
|
||||
// shippingDistrict(市/郡)对应其他国家的"城市"概念
|
||||
let shippingCity = form.shippingCity
|
||||
if (form.shippingCountry === 'VN' && (!shippingCity || shippingCity.trim() === '') && form.shippingDistrict) {
|
||||
shippingCity = form.shippingDistrict
|
||||
}
|
||||
|
||||
const orderData = {
|
||||
productId: productInfo.value.id,
|
||||
skuId: productInfo.value.skuId,
|
||||
@@ -563,7 +643,7 @@ const submitForm = async () => {
|
||||
shippingPhone: form.shippingPhone,
|
||||
shippingCountry: form.shippingCountry,
|
||||
shippingState: form.shippingState || null,
|
||||
shippingCity: form.shippingCity,
|
||||
shippingCity: shippingCity, // 使用映射后的值,越南使用 shippingDistrict
|
||||
shippingStreet: form.shippingStreet || form.shippingAddressLine1, // 兼容旧字段
|
||||
shippingPostcode: form.shippingPostcode || null,
|
||||
// 东南亚地址扩展字段
|
||||
@@ -585,18 +665,18 @@ const submitForm = async () => {
|
||||
const response = await createCustomerOrder(orderData)
|
||||
|
||||
if (response.code === '0000' && response.data) {
|
||||
ElMessage.success('订单创建成功')
|
||||
ElMessage.success(t('order.validationOrderCreateSuccess'))
|
||||
// 跳转到订单确认页面
|
||||
router.push({
|
||||
path: '/order/confirm',
|
||||
query: { orderNo: response.data.orderNo }
|
||||
})
|
||||
} else {
|
||||
ElMessage.error(response.message || '创建订单失败')
|
||||
ElMessage.error(response.message || t('order.validationOrderCreateFailed'))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建订单失败:', error)
|
||||
ElMessage.error(error.response?.data?.message || '创建订单失败,请稍后重试')
|
||||
ElMessage.error(error.response?.data?.message || t('order.validationOrderCreateRetry'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -609,14 +689,17 @@ const formatPrice = (price) => {
|
||||
}
|
||||
|
||||
// 从路由参数获取商品信息
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
if (route.query.data) {
|
||||
try {
|
||||
const data = JSON.parse(decodeURIComponent(route.query.data))
|
||||
if (data.product) {
|
||||
productInfo.value = data.product
|
||||
// 根据SKU的货币代码自动设置国家
|
||||
// 根据SKU的货币代码自动设置国家并加载翻译
|
||||
if (data.product.currency) {
|
||||
// 加载对应货币的翻译
|
||||
await loadTranslationByCurrency(data.product.currency)
|
||||
|
||||
const countryCode = getCountryByCurrency(data.product.currency)
|
||||
if (countryCode) {
|
||||
form.shippingCountry = countryCode
|
||||
@@ -629,11 +712,11 @@ onMounted(() => {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('解析商品信息失败:', error)
|
||||
ElMessage.error('商品信息解析失败')
|
||||
ElMessage.error(t('order.validationProductParseFailed'))
|
||||
router.push('/')
|
||||
}
|
||||
} else {
|
||||
ElMessage.error('缺少商品信息')
|
||||
ElMessage.error(t('order.validationProductInfoMissing'))
|
||||
router.push('/')
|
||||
}
|
||||
})
|
||||
@@ -746,4 +829,145 @@ onMounted(() => {
|
||||
color: #f56c6c;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* 移动端优化 */
|
||||
@media (max-width: 768px) {
|
||||
.create-order {
|
||||
padding: 10px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
font-size: 16px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.product-info-card {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.product-card-header {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.product-info-main {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.product-info-image {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.product-info-name {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.product-info-price {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.total-label {
|
||||
margin-left: 0;
|
||||
margin-top: 8px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.total-value {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
/* 表单优化 */
|
||||
.el-form-item {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.el-form-item__label {
|
||||
padding-bottom: 5px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.el-input,
|
||||
.el-select,
|
||||
.el-textarea {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 地址字段优化 */
|
||||
.el-form-item :deep(.el-input-group) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.el-form-item :deep(.el-input-group__append) {
|
||||
margin-left: 0;
|
||||
margin-top: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 按钮优化 */
|
||||
.el-form-item:last-child {
|
||||
margin-bottom: 0;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #e4e7ed;
|
||||
}
|
||||
|
||||
.el-form-item:last-child .el-button {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
height: 44px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.el-form-item:last-child .el-button + .el-button {
|
||||
margin-top: 10px;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
/* 分隔线优化 */
|
||||
.el-divider {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.el-divider__text {
|
||||
font-size: 14px;
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
/* 提示信息优化 */
|
||||
.el-alert {
|
||||
margin-bottom: 15px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 输入组优化 */
|
||||
.mobile-input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.desktop-input-group {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mobile-postcode-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.desktop-postcode-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>订单确认</span>
|
||||
<span>{{ $t('confirm.orderInfo') }}</span>
|
||||
<el-tag :type="getStatusType(order.status)" size="large">
|
||||
{{ getStatusText(order.status) }}
|
||||
</el-tag>
|
||||
@@ -17,10 +17,10 @@
|
||||
|
||||
<!-- 订单信息 -->
|
||||
<div class="order-info-section">
|
||||
<h3>订单信息</h3>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="订单号">{{ order.orderNo }}</el-descriptions-item>
|
||||
<el-descriptions-item label="订单金额">
|
||||
<h3>{{ $t('confirm.orderInfo') }}</h3>
|
||||
<el-descriptions :column="isMobile ? 1 : 2" border>
|
||||
<el-descriptions-item :label="$t('confirm.orderNo')">{{ order.orderNo }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('confirm.orderAmount')">
|
||||
<div class="order-amount-section">
|
||||
<div class="original-amount">
|
||||
<span class="order-amount">{{ order.currency }} {{ formatPrice(order.totalAmount) }}</span>
|
||||
@@ -38,21 +38,21 @@
|
||||
<div class="conversion-alert-content">
|
||||
<div class="conversion-title">
|
||||
<el-icon class="warning-icon"><Warning /></el-icon>
|
||||
<strong class="title-text">您将以{{ getCurrencyName(order.paymentCurrency) }}支付</strong>
|
||||
<strong class="title-text">{{ $t('confirm.willPayIn', [getCurrencyName(order.paymentCurrency)]) }}</strong>
|
||||
</div>
|
||||
<div class="conversion-main-info">
|
||||
<div class="payment-amount-highlight">
|
||||
<span class="label">实际费用:</span>
|
||||
<span class="label">{{ $t('confirm.actualCost') }}:</span>
|
||||
<span class="payment-amount-large">{{ order.paymentCurrency }} {{ formatPrice(order.paymentAmount) }}</span>
|
||||
</div>
|
||||
<div v-if="order.exchangeRate" class="exchange-rate-info">
|
||||
<span class="equivalent-amount">(约 {{ order.currency }} {{ formatPrice(order.totalAmount) }})</span>
|
||||
<span class="rate-value">汇率:{{ formatRate(order.exchangeRate) }}</span>
|
||||
<span class="equivalent-amount">({{ $t('confirm.approximately') }} {{ order.currency }} {{ formatPrice(order.totalAmount) }})</span>
|
||||
<span class="rate-value">{{ $t('confirm.exchangeRate') }}:{{ formatRate(order.exchangeRate) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="order.rateLockedAt" class="rate-locked-info">
|
||||
<el-icon><Clock /></el-icon>
|
||||
<span>汇率锁定时间:{{ formatDateTime(order.rateLockedAt) }}</span>
|
||||
<span>{{ $t('confirm.rateLockedAt') }}:{{ formatDateTime(order.rateLockedAt) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -60,71 +60,71 @@
|
||||
</div>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="商品名称" :span="2">{{ order.productName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="SKU名称" :span="2">{{ order.skuName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="购买数量">{{ order.quantity }}</el-descriptions-item>
|
||||
<el-descriptions-item label="单价">{{ order.currency }} {{ formatPrice(order.unitPrice) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间" :span="2">{{ formatDateTime(order.createTime) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('confirm.productName')" :span="2">{{ order.productName }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('confirm.skuName')" :span="2">{{ order.skuName }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('confirm.quantity')">{{ order.quantity }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('confirm.unitPrice')">{{ order.currency }} {{ formatPrice(order.unitPrice) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('confirm.createTime')" :span="2">{{ formatDateTime(order.createTime) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<!-- 客户信息 -->
|
||||
<div class="order-info-section">
|
||||
<h3>客户信息</h3>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="客户姓名">{{ order.customerName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="客户电话">{{ order.customerPhone }}</el-descriptions-item>
|
||||
<el-descriptions-item label="客户邮箱" :span="2">
|
||||
{{ order.customerEmail || '未填写' }}
|
||||
<h3>{{ $t('order.customerInfo') }}</h3>
|
||||
<el-descriptions :column="isMobile ? 1 : 2" border>
|
||||
<el-descriptions-item :label="$t('order.customerName')">{{ order.customerName }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('order.customerPhone')">{{ order.customerPhone }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('order.customerEmail')" :span="2">
|
||||
{{ order.customerEmail || $t('confirm.notFilled') }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<!-- 收货地址 -->
|
||||
<div class="order-info-section">
|
||||
<h3>收货地址</h3>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="收货人">{{ order.shippingName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="收货电话">{{ order.shippingPhone }}</el-descriptions-item>
|
||||
<el-descriptions-item label="收货地址" :span="2">
|
||||
<h3>{{ $t('order.shippingAddress') }}</h3>
|
||||
<el-descriptions :column="isMobile ? 1 : 2" border>
|
||||
<el-descriptions-item :label="$t('confirm.recipient')">{{ order.shippingName }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('confirm.recipientPhone')">{{ order.shippingPhone }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('confirm.shippingAddress')" :span="2">
|
||||
<div class="shipping-address-detail">
|
||||
<div v-if="order.shippingAddressLine1" class="address-line">
|
||||
<strong>详细地址1:</strong>{{ order.shippingAddressLine1 }}
|
||||
<strong>{{ $t('confirm.addressLine1') }}:</strong>{{ order.shippingAddressLine1 }}
|
||||
</div>
|
||||
<div v-if="order.shippingAddressLine2" class="address-line">
|
||||
<strong>详细地址2:</strong>{{ order.shippingAddressLine2 }}
|
||||
<strong>{{ $t('confirm.addressLine2') }}:</strong>{{ order.shippingAddressLine2 }}
|
||||
</div>
|
||||
<!-- 从JSON字段中读取特殊字段 -->
|
||||
<template v-if="order.shippingSpecialFields">
|
||||
<div v-if="order.shippingSpecialFields.blockNumber" class="address-line">
|
||||
<strong>组屋号:</strong>{{ order.shippingSpecialFields.blockNumber }}
|
||||
<strong>{{ $t('order.blockNumber') }}:</strong>{{ order.shippingSpecialFields.blockNumber }}
|
||||
</div>
|
||||
<div v-if="order.shippingSpecialFields.unitNumber" class="address-line">
|
||||
<strong>单元号:</strong>{{ order.shippingSpecialFields.unitNumber }}
|
||||
<strong>{{ $t('order.unitNumber') }}:</strong>{{ order.shippingSpecialFields.unitNumber }}
|
||||
</div>
|
||||
<div v-if="order.shippingSpecialFields.barangay" class="address-line">
|
||||
<strong>Barangay:</strong>{{ order.shippingSpecialFields.barangay }}
|
||||
<strong>{{ $t('order.barangay') }}:</strong>{{ order.shippingSpecialFields.barangay }}
|
||||
</div>
|
||||
<div v-if="order.shippingSpecialFields.addressThai" class="address-line">
|
||||
<strong>泰文地址:</strong>{{ order.shippingSpecialFields.addressThai }}
|
||||
<strong>{{ $t('order.addressThai') }}:</strong>{{ order.shippingSpecialFields.addressThai }}
|
||||
</div>
|
||||
<div v-if="order.shippingSpecialFields.province" class="address-line">
|
||||
<strong>省:</strong>{{ order.shippingSpecialFields.province }}
|
||||
<strong>{{ $t('order.province') }}:</strong>{{ order.shippingSpecialFields.province }}
|
||||
</div>
|
||||
<div v-if="order.shippingSpecialFields.district" class="address-line">
|
||||
<strong>市/郡:</strong>{{ order.shippingSpecialFields.district }}
|
||||
<strong>{{ $t('order.district') }}:</strong>{{ order.shippingSpecialFields.district }}
|
||||
</div>
|
||||
<div v-if="order.shippingSpecialFields.ward" class="address-line">
|
||||
<strong>区/坊:</strong>{{ order.shippingSpecialFields.ward }}
|
||||
<strong>{{ $t('order.ward') }}:</strong>{{ order.shippingSpecialFields.ward }}
|
||||
</div>
|
||||
<div v-if="order.shippingSpecialFields.stateMalaysia" class="address-line">
|
||||
<strong>州属:</strong>{{ order.shippingSpecialFields.stateMalaysia }}
|
||||
<strong>{{ $t('order.stateMalaysia') }}:</strong>{{ order.shippingSpecialFields.stateMalaysia }}
|
||||
</div>
|
||||
<div v-if="order.shippingSpecialFields.administrativeArea" class="address-line">
|
||||
<strong>行政区域:</strong>{{ order.shippingSpecialFields.administrativeArea }}
|
||||
<strong>{{ $t('order.administrativeArea') }}:</strong>{{ order.shippingSpecialFields.administrativeArea }}
|
||||
</div>
|
||||
<div v-if="order.shippingSpecialFields.floorUnit" class="address-line">
|
||||
<strong>楼层/单元/代收点:</strong>{{ order.shippingSpecialFields.floorUnit }}
|
||||
<strong>{{ $t('order.floorUnit') }}:</strong>{{ order.shippingSpecialFields.floorUnit }}
|
||||
</div>
|
||||
</template>
|
||||
<div class="address-line">
|
||||
@@ -137,7 +137,7 @@
|
||||
|
||||
<!-- 订单备注 -->
|
||||
<div class="order-info-section" v-if="order.remark">
|
||||
<h3>订单备注</h3>
|
||||
<h3>{{ $t('confirm.orderRemark') }}</h3>
|
||||
<p class="order-remark">{{ order.remark }}</p>
|
||||
</div>
|
||||
|
||||
@@ -149,32 +149,36 @@
|
||||
size="large"
|
||||
@click="handlePay"
|
||||
:loading="payLoading"
|
||||
style="width: 200px"
|
||||
:style="isMobile ? 'width: 100%' : 'width: 200px'"
|
||||
>
|
||||
<el-icon><Money /></el-icon>
|
||||
立即支付
|
||||
{{ $t('confirm.payNow') }}
|
||||
</el-button>
|
||||
<el-button @click="goBack" style="margin-left: 10px">返回</el-button>
|
||||
<el-button @click="goBack" :style="isMobile ? 'width: 100%; margin-top: 10px; margin-left: 0' : 'margin-left: 10px'">{{ $t('confirm.back') }}</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<el-card v-else>
|
||||
<el-empty description="订单不存在">
|
||||
<el-button type="primary" @click="goBack">返回</el-button>
|
||||
<el-empty :description="$t('confirm.orderNotFound')">
|
||||
<el-button type="primary" @click="goBack">{{ $t('confirm.back') }}</el-button>
|
||||
</el-empty>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Money, Clock, Warning } from '@element-plus/icons-vue'
|
||||
import { getOrderByOrderNo, calculateCurrencyConversion } from '../api/order'
|
||||
import { createPayPalOrder, getPayPalOrder, capturePayPalOrder } from '../api/paypal'
|
||||
import { formatAmount } from '../utils/helpers'
|
||||
import { loadTranslationByCurrency } from '../i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
@@ -182,6 +186,14 @@ const orderLoading = ref(true)
|
||||
const payLoading = ref(false)
|
||||
const order = ref(null)
|
||||
|
||||
// 移动端检测
|
||||
const isMobile = computed(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
return window.innerWidth <= 768
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
// 获取订单状态类型
|
||||
const getStatusType = (status) => {
|
||||
const statusMap = {
|
||||
@@ -197,11 +209,11 @@ const getStatusType = (status) => {
|
||||
// 获取订单状态文本
|
||||
const getStatusText = (status) => {
|
||||
const statusMap = {
|
||||
'PENDING': '待支付',
|
||||
'PAID': '已支付',
|
||||
'SHIPPED': '已发货',
|
||||
'COMPLETED': '已完成',
|
||||
'CANCELLED': '已取消'
|
||||
'PENDING': t('confirm.statusPending'),
|
||||
'PAID': t('confirm.statusPaid'),
|
||||
'SHIPPED': t('confirm.statusShipped'),
|
||||
'COMPLETED': t('confirm.statusCompleted'),
|
||||
'CANCELLED': t('confirm.statusCancelled')
|
||||
}
|
||||
return statusMap[status] || status
|
||||
}
|
||||
@@ -214,17 +226,18 @@ const formatPrice = (price) => {
|
||||
// 获取货币名称
|
||||
const getCurrencyName = (currencyCode) => {
|
||||
const currencyNames = {
|
||||
'USD': '美元',
|
||||
'EUR': '欧元',
|
||||
'GBP': '英镑',
|
||||
'CNY': '人民币',
|
||||
'MYR': '马来西亚林吉特',
|
||||
'VND': '越南盾',
|
||||
'JPY': '日元',
|
||||
'KRW': '韩元',
|
||||
'THB': '泰铢',
|
||||
'SGD': '新加坡元',
|
||||
'HKD': '港币'
|
||||
'USD': t('confirm.currencyUSD'),
|
||||
'EUR': t('confirm.currencyEUR'),
|
||||
'GBP': t('confirm.currencyGBP'),
|
||||
'CNY': t('confirm.currencyCNY'),
|
||||
'MYR': t('confirm.currencyMYR'),
|
||||
'VND': t('confirm.currencyVND'),
|
||||
'JPY': t('confirm.currencyJPY'),
|
||||
'KRW': t('confirm.currencyKRW'),
|
||||
'THB': t('confirm.currencyTHB'),
|
||||
'SGD': t('confirm.currencySGD'),
|
||||
'HKD': t('confirm.currencyHKD'),
|
||||
'PHP': t('confirm.currencyPHP')
|
||||
}
|
||||
return currencyNames[currencyCode] || currencyCode
|
||||
}
|
||||
@@ -262,9 +275,9 @@ const formatShippingAddress = (order) => {
|
||||
}
|
||||
|
||||
if (order.shippingCountry) parts.push(order.shippingCountry)
|
||||
if (order.shippingPostcode) parts.push(`邮编:${order.shippingPostcode}`)
|
||||
if (order.shippingPostcode) parts.push(`${t('confirm.postcodeLabel')}:${order.shippingPostcode}`)
|
||||
|
||||
return parts.length > 0 ? parts.join(',') : '地址信息不完整'
|
||||
return parts.length > 0 ? parts.join(',') : t('confirm.addressIncomplete')
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
@@ -274,9 +287,10 @@ const goBack = () => {
|
||||
|
||||
// 加载订单详情
|
||||
const loadOrder = async () => {
|
||||
// 先加载订单,获取货币信息后再加载翻译
|
||||
const orderNo = route.query.orderNo
|
||||
if (!orderNo) {
|
||||
ElMessage.error('订单号不能为空')
|
||||
ElMessage.error(t('order.validationRequired', [t('confirm.orderNo')]))
|
||||
router.push('/')
|
||||
return
|
||||
}
|
||||
@@ -287,6 +301,19 @@ const loadOrder = async () => {
|
||||
if (response.code === '0000' && response.data) {
|
||||
order.value = response.data
|
||||
|
||||
// 调试:打印订单信息
|
||||
console.log('订单加载成功:', {
|
||||
orderNo: order.value.orderNo,
|
||||
paymentStatus: order.value.paymentStatus,
|
||||
status: order.value.status,
|
||||
currency: order.value.currency
|
||||
})
|
||||
|
||||
// 根据订单货币加载翻译
|
||||
if (order.value.currency) {
|
||||
await loadTranslationByCurrency(order.value.currency)
|
||||
}
|
||||
|
||||
// 如果订单未支付且需要货币转换,提前计算货币转换信息
|
||||
if (order.value.paymentStatus === 'UNPAID' && order.value.currency) {
|
||||
// 检查是否需要货币转换(PayPal支持的货币列表)
|
||||
@@ -306,17 +333,14 @@ const loadOrder = async () => {
|
||||
|
||||
if (conversionResponse.code === '0000' && conversionResponse.data) {
|
||||
const conversion = conversionResponse.data
|
||||
// 更新订单显示信息
|
||||
order.value = {
|
||||
...order.value,
|
||||
originalCurrency: conversion.originalCurrency,
|
||||
originalAmount: conversion.originalAmount,
|
||||
paymentCurrency: conversion.paymentCurrency,
|
||||
paymentAmount: conversion.paymentAmount,
|
||||
exchangeRate: conversion.exchangeRate,
|
||||
rateLockedAt: conversion.rateLockedAt
|
||||
}
|
||||
console.log('货币转换信息已计算并更新')
|
||||
// 更新订单显示信息(保留原有字段,只更新货币转换相关字段)
|
||||
order.value.originalCurrency = conversion.originalCurrency
|
||||
order.value.originalAmount = conversion.originalAmount
|
||||
order.value.paymentCurrency = conversion.paymentCurrency
|
||||
order.value.paymentAmount = conversion.paymentAmount
|
||||
order.value.exchangeRate = conversion.exchangeRate
|
||||
order.value.rateLockedAt = conversion.rateLockedAt
|
||||
console.log('货币转换信息已计算并更新,支付状态:', order.value.paymentStatus)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('计算货币转换信息失败:', error)
|
||||
@@ -325,12 +349,12 @@ const loadOrder = async () => {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(response.message || '获取订单信息失败')
|
||||
ElMessage.error(response.message || t('order.validationOrderCreateFailed'))
|
||||
order.value = null
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取订单信息失败:', error)
|
||||
ElMessage.error('获取订单信息失败')
|
||||
ElMessage.error(t('order.validationOrderCreateFailed'))
|
||||
order.value = null
|
||||
} finally {
|
||||
orderLoading.value = false
|
||||
@@ -340,12 +364,12 @@ const loadOrder = async () => {
|
||||
// 处理PayPal支付(步骤1-4)
|
||||
const handlePay = async () => {
|
||||
if (!order.value) {
|
||||
ElMessage.error('订单信息不存在')
|
||||
ElMessage.error(t('confirm.orderNotFound'))
|
||||
return
|
||||
}
|
||||
|
||||
if (order.value.paymentStatus !== 'UNPAID') {
|
||||
ElMessage.warning('订单已支付或已取消')
|
||||
ElMessage.warning(t('confirm.statusPaid') + ' ' + t('confirm.statusCancelled'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -366,9 +390,10 @@ const handlePay = async () => {
|
||||
returnUrl: `${window.location.origin}/paypal/success?orderNo=${order.value.orderNo}`,
|
||||
cancelUrl: `${window.location.origin}/paypal/cancel?orderNo=${order.value.orderNo}`,
|
||||
shippingName: order.value.shippingName,
|
||||
shippingAddressLine1: order.value.shippingStreet,
|
||||
shippingAddressLine1: order.value.shippingAddressLine1 || order.value.shippingStreet,
|
||||
shippingAddressLine2: order.value.shippingAddressLine2 || null,
|
||||
shippingCity: order.value.shippingCity,
|
||||
shippingState: order.value.shippingState || null,
|
||||
shippingState: order.value.shippingState || (order.value.shippingSpecialFields?.stateMalaysia) || null,
|
||||
shippingPostalCode: order.value.shippingPostcode || null,
|
||||
shippingCountryCode: order.value.shippingCountry,
|
||||
emailAddress: order.value.customerEmail || null
|
||||
@@ -397,7 +422,7 @@ const handlePay = async () => {
|
||||
// 显示货币转换提示
|
||||
if (currencyConversion.conversionRequired) {
|
||||
ElMessage.info({
|
||||
message: `您将以${getCurrencyName(currencyConversion.paymentCurrency)}支付,实际费用为:${currencyConversion.paymentCurrency} ${formatPrice(currencyConversion.paymentAmount)}`,
|
||||
message: `${t('confirm.willPayIn', [getCurrencyName(currencyConversion.paymentCurrency)])},${t('confirm.actualCost')}:${currencyConversion.paymentCurrency} ${formatPrice(currencyConversion.paymentAmount)}`,
|
||||
duration: 5000
|
||||
})
|
||||
}
|
||||
@@ -407,18 +432,18 @@ const handlePay = async () => {
|
||||
const approvalLink = paypalOrder.links?.find(link => link.rel === 'payer-action')
|
||||
|
||||
if (approvalLink && approvalLink.href) {
|
||||
ElMessage.success('正在跳转到PayPal支付页面...')
|
||||
ElMessage.success(t('order.validationOrderCreateSuccess'))
|
||||
// 步骤4:跳转到PayPal登录页
|
||||
window.location.href = approvalLink.href
|
||||
} else {
|
||||
ElMessage.error('获取PayPal支付链接失败')
|
||||
ElMessage.error(t('order.validationOrderCreateFailed'))
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(response.message || '创建PayPal订单失败')
|
||||
ElMessage.error(response.message || t('order.validationOrderCreateFailed'))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建PayPal订单失败:', error)
|
||||
ElMessage.error(error.response?.data?.message || '创建PayPal订单失败,请稍后重试')
|
||||
ElMessage.error(error.response?.data?.message || t('order.validationOrderCreateRetry'))
|
||||
} finally {
|
||||
payLoading.value = false
|
||||
}
|
||||
@@ -600,5 +625,111 @@ onMounted(() => {
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
/* 移动端优化 */
|
||||
@media (max-width: 768px) {
|
||||
.order-confirm {
|
||||
padding: 10px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.order-info-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.order-info-section h3 {
|
||||
font-size: 14px;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.order-amount {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.conversion-title {
|
||||
font-size: 14px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.warning-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.conversion-main-info {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.payment-amount-highlight {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.payment-amount-large {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.exchange-rate-info {
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
margin-top: 20px;
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
.action-buttons .el-button {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
height: 44px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.action-buttons .el-button + .el-button {
|
||||
margin-top: 10px;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.el-descriptions {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.el-descriptions-item__label {
|
||||
font-size: 13px;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.el-descriptions-item__content {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.shipping-address-detail {
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.address-line {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.address-line strong {
|
||||
font-size: 12px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -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