Compare commits

...

2 Commits

Author SHA1 Message Date
23e535562d feat(order): 优化创建订单页面的地址字段和必填项
- 调整新加坡、马来西亚、菲律宾、泰国、越南等国家的必填字段配置
- 将邮编和城市设为PayPal要求的必填项,越南使用省/市/郡/区/坊代替城市
- 重新排列表单字段顺序,优化移动端显示效果
- 添加更多地址信息按钮,将详细地址2放入折叠区域
- 实现地址组件自动更新功能,将省/州、市/郡、区/坊等信息拼接到详细地址1
- 更新国际化文本,添加更多和收起按钮的翻译
- 为创建订单和订单确认页面添加路由元信息,标记为不显示导航栏的客户页面
2025-12-26 18:13:11 +08:00
6d725b51cb fix(config): 修正开发服务器代理配置
- 将代理目标端口从 18082 修正为 8082
- 更新错误提示信息中的端口号以保持一致
2025-12-26 16:21:32 +08:00
7 changed files with 734 additions and 392 deletions

View File

@@ -30,6 +30,8 @@ const zh = {
pleaseEnter: '请输入', pleaseEnter: '请输入',
optional: '可选', optional: '可选',
required: '必填', required: '必填',
more: '更多',
collapse: '收起',
addressFormat: '地址格式', addressFormat: '地址格式',
phoneCode: '国际区号', phoneCode: '国际区号',
mustMatchId: '需与证件一致,支持当地语言+英文', mustMatchId: '需与证件一致,支持当地语言+英文',
@@ -234,6 +236,8 @@ const en = {
pleaseEnter: 'Please enter', pleaseEnter: 'Please enter',
optional: 'Optional', optional: 'Optional',
required: 'Required', required: 'Required',
more: 'More',
collapse: 'Collapse',
addressFormat: 'Address Format', addressFormat: 'Address Format',
phoneCode: 'Phone Code', phoneCode: 'Phone Code',
mustMatchId: 'Must match ID, supports local language + English', mustMatchId: 'Must match ID, supports local language + English',
@@ -436,6 +440,8 @@ const may = {
pleaseEnter: 'Sila masukkan', pleaseEnter: 'Sila masukkan',
optional: 'Pilihan', optional: 'Pilihan',
required: 'Diperlukan', required: 'Diperlukan',
more: 'Lebih Banyak',
collapse: 'Tutup',
addressFormat: 'Format Alamat', addressFormat: 'Format Alamat',
phoneCode: 'Kod Telefon', phoneCode: 'Kod Telefon',
mustMatchId: 'Mesti sepadan dengan ID, menyokong bahasa tempatan + Inggeris', mustMatchId: 'Mesti sepadan dengan ID, menyokong bahasa tempatan + Inggeris',
@@ -633,9 +639,14 @@ const fil = {
pleaseEnter: 'Mangyaring ipasok', pleaseEnter: 'Mangyaring ipasok',
optional: 'Opsiyonal', optional: 'Opsiyonal',
required: 'Kinakailangan', required: 'Kinakailangan',
more: 'Higit Pa',
collapse: 'Itago',
addressFormat: 'Format ng Address', addressFormat: 'Format ng Address',
phoneCode: 'Phone Code', phoneCode: 'Phone Code',
mustMatchId: 'Dapat tumugma sa ID, sumusuporta sa lokal na wika + Ingles' mustMatchId: 'Dapat tumugma sa ID, sumusuporta sa lokal na wika + Ingles',
// 占位符文本
placeholderAddressLine1: 'Mangyaring ipasok ang numero ng bahay, kalye, gusali',
placeholderAddressLine2: 'Mangyaring ipasok ang sahig, numero ng unit (opsyonal)'
}, },
product: { product: {
selectCurrency: 'Pumili ng Currency at Wika', selectCurrency: 'Pumili ng Currency at Wika',
@@ -779,6 +790,8 @@ const th = {
pleaseEnter: 'กรุณากรอก', pleaseEnter: 'กรุณากรอก',
optional: 'ไม่บังคับ', optional: 'ไม่บังคับ',
required: 'จำเป็น', required: 'จำเป็น',
more: 'เพิ่มเติม',
collapse: 'ย่อ',
addressFormat: 'รูปแบบที่อยู่', addressFormat: 'รูปแบบที่อยู่',
phoneCode: 'รหัสโทรศัพท์', phoneCode: 'รหัสโทรศัพท์',
mustMatchId: 'ต้องตรงกับบัตรประชาชน รองรับภาษาท้องถิ่น + อังกฤษ', mustMatchId: 'ต้องตรงกับบัตรประชาชน รองรับภาษาท้องถิ่น + อังกฤษ',
@@ -979,6 +992,8 @@ const vie = {
pleaseEnter: 'Vui lòng nhập', pleaseEnter: 'Vui lòng nhập',
optional: 'Tùy chọn', optional: 'Tùy chọn',
required: 'Bắt buộc', required: 'Bắt buộc',
more: 'Thêm',
collapse: 'Thu gọn',
addressFormat: 'Định Dạng Địa Chỉ', addressFormat: 'Định Dạng Địa Chỉ',
phoneCode: 'Mã Điện Thoại', phoneCode: 'Mã Điện Thoại',
mustMatchId: 'Phải khớp với ID, hỗ trợ ngôn ngữ địa phương + tiếng Anh', mustMatchId: 'Phải khớp với ID, hỗ trợ ngôn ngữ địa phương + tiếng Anh',
@@ -1176,9 +1191,14 @@ const id = {
pleaseEnter: 'Silakan masukkan', pleaseEnter: 'Silakan masukkan',
optional: 'Opsional', optional: 'Opsional',
required: 'Diperlukan', required: 'Diperlukan',
more: 'Lebih Banyak',
collapse: 'Tutup',
addressFormat: 'Format Alamat', addressFormat: 'Format Alamat',
phoneCode: 'Kode Telepon', phoneCode: 'Kode Telepon',
mustMatchId: 'Harus sesuai dengan ID, mendukung bahasa lokal + Inggris' mustMatchId: 'Harus sesuai dengan ID, mendukung bahasa lokal + Inggris',
// 占位符文本
placeholderAddressLine1: 'Silakan masukkan nomor rumah, jalan, gedung',
placeholderAddressLine2: 'Silakan masukkan lantai, nomor unit (opsional)'
}, },
product: { product: {
selectCurrency: 'Pilih Mata Uang dan Bahasa', selectCurrency: 'Pilih Mata Uang dan Bahasa',

View File

@@ -44,12 +44,14 @@ const routes = [
{ {
path: '/create-order', path: '/create-order',
name: 'CreateOrder', name: 'CreateOrder',
component: CreateOrder component: CreateOrder,
meta: { isCustomerPage: true } // 标记为客户页面,不显示导航栏
}, },
{ {
path: '/order/confirm', path: '/order/confirm',
name: 'OrderConfirm', name: 'OrderConfirm',
component: () => import('../views/OrderConfirm.vue') component: () => import('../views/OrderConfirm.vue'),
meta: { isCustomerPage: true } // 标记为客户页面,不显示导航栏
}, },
{ {
path: '/paypal/success', path: '/paypal/success',

View File

@@ -25,8 +25,9 @@ export const countryConfigs = {
phoneCode: '+65', phoneCode: '+65',
postcodeLength: 6, postcodeLength: 6,
postcodePattern: /^\d{6}$/, postcodePattern: /^\d{6}$/,
requiredFields: ['shippingName', 'shippingPhone', 'shippingCountry', 'shippingCity', // 新加坡城市和邮编是PayPal要求必填的
'shippingAddressLine1', 'shippingBlockNumber', 'shippingUnitNumber', 'shippingPostcode'], requiredFields: ['shippingName', 'shippingPhone', 'shippingCountry',
'shippingCity', 'shippingAddressLine1', 'shippingBlockNumber', 'shippingUnitNumber', 'shippingPostcode'],
specialFields: ['shippingBlockNumber', 'shippingUnitNumber'], specialFields: ['shippingBlockNumber', 'shippingUnitNumber'],
fieldLabels: { fieldLabels: {
shippingBlockNumber: '组屋号 (Block Number)', shippingBlockNumber: '组屋号 (Block Number)',
@@ -45,8 +46,9 @@ export const countryConfigs = {
phoneCode: '+60', phoneCode: '+60',
postcodeLength: 5, postcodeLength: 5,
postcodePattern: /^\d{5}$/, postcodePattern: /^\d{5}$/,
requiredFields: ['shippingName', 'shippingPhone', 'shippingCountry', 'shippingCity', // 马来西亚城市和邮编是PayPal要求必填的州属放入更多按钮中改为非必填
'shippingStateMalaysia', 'shippingAddressLine1', 'shippingPostcode'], requiredFields: ['shippingName', 'shippingPhone', 'shippingCountry',
'shippingCity', 'shippingAddressLine1', 'shippingPostcode'],
specialFields: ['shippingStateMalaysia'], specialFields: ['shippingStateMalaysia'],
fieldLabels: { fieldLabels: {
shippingStateMalaysia: '州属 (State)', shippingStateMalaysia: '州属 (State)',
@@ -64,8 +66,9 @@ export const countryConfigs = {
phoneCode: '+63', phoneCode: '+63',
postcodeLength: 4, postcodeLength: 4,
postcodePattern: /^\d{4}$/, postcodePattern: /^\d{4}$/,
requiredFields: ['shippingName', 'shippingPhone', 'shippingCountry', 'shippingCity', // 菲律宾城市和邮编是PayPal要求必填的省放入更多按钮中改为非必填
'shippingState', 'shippingBarangay', 'shippingAddressLine1', 'shippingPostcode'], requiredFields: ['shippingName', 'shippingPhone', 'shippingCountry',
'shippingCity', 'shippingBarangay', 'shippingAddressLine1', 'shippingPostcode'],
specialFields: ['shippingBarangay'], specialFields: ['shippingBarangay'],
fieldLabels: { fieldLabels: {
shippingBarangay: 'Barangay社区编号', shippingBarangay: 'Barangay社区编号',
@@ -84,8 +87,9 @@ export const countryConfigs = {
phoneCode: '+66', phoneCode: '+66',
postcodeLength: 5, postcodeLength: 5,
postcodePattern: /^\d{5}$/, postcodePattern: /^\d{5}$/,
requiredFields: ['shippingName', 'shippingPhone', 'shippingCountry', 'shippingCity', // 泰国城市和邮编是PayPal要求必填的府放入更多按钮中改为非必填
'shippingState', 'shippingAddressLine1', 'shippingPostcode', 'shippingAddressThai'], requiredFields: ['shippingName', 'shippingPhone', 'shippingCountry',
'shippingCity', 'shippingAddressLine1', 'shippingAddressThai', 'shippingPostcode'],
specialFields: ['shippingAddressThai', 'shippingAdministrativeArea'], specialFields: ['shippingAddressThai', 'shippingAdministrativeArea'],
fieldLabels: { fieldLabels: {
shippingAddressThai: '泰文地址 (Thai Address)', shippingAddressThai: '泰文地址 (Thai Address)',
@@ -105,8 +109,9 @@ export const countryConfigs = {
phoneCode: '+84', phoneCode: '+84',
postcodeLength: 5, postcodeLength: 5,
postcodePattern: /^\d{5}$/, postcodePattern: /^\d{5}$/,
requiredFields: ['shippingName', 'shippingPhone', 'shippingCountry', 'shippingProvince', // 越南:使用省/市/郡/区/坊代替城市邮编是PayPal要求必填的
'shippingDistrict', 'shippingWard', 'shippingAddressLine1', 'shippingPostcode'], requiredFields: ['shippingName', 'shippingPhone', 'shippingCountry',
'shippingProvince', 'shippingDistrict', 'shippingWard', 'shippingAddressLine1', 'shippingPostcode'],
specialFields: ['shippingProvince', 'shippingDistrict', 'shippingWard'], specialFields: ['shippingProvince', 'shippingDistrict', 'shippingWard'],
fieldLabels: { fieldLabels: {
shippingProvince: '省 (Tỉnh)', shippingProvince: '省 (Tỉnh)',

View File

@@ -21,8 +21,9 @@
fit="cover" fit="cover"
class="product-info-image" class="product-info-image"
/> />
<div class="product-info-details">
<div class="product-info-name">{{ productInfo.name }}</div> <div class="product-info-name">{{ productInfo.name }}</div>
</div>
<div class="product-info-details">
<div class="product-info-sku" v-if="productInfo.sku"> <div class="product-info-sku" v-if="productInfo.sku">
<span class="sku-label">SKU</span> <span class="sku-label">SKU</span>
<span class="sku-value">{{ productInfo.sku }}</span> <span class="sku-value">{{ productInfo.sku }}</span>
@@ -37,7 +38,6 @@
</div> </div>
</div> </div>
</div> </div>
</div>
</el-card> </el-card>
<el-form <el-form
@@ -76,50 +76,74 @@
<el-divider>{{ $t('order.shippingAddress') }}</el-divider> <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-alert v-if="currentCountryConfig" :title="`${$t('order.addressFormat')}${currentCountryConfig.addressFormat}`" type="info" :closable="false" style="margin-bottom: 20px" />
<el-form-item :label="t('order.shippingName')" prop="shippingName"> <!-- 邮编PayPal要求必填- 常驻展示必填 -->
<el-form-item :label="t('order.postcode')" prop="shippingPostcode" required>
<div :class="isMobile ? 'mobile-postcode-group' : 'desktop-postcode-group'">
<el-input <el-input
v-model="form.shippingName" v-model="form.shippingPostcode"
:placeholder="t('order.pleaseEnter') + t('order.shippingName') + '' + t('order.mustMatchId') + ''" :placeholder="currentCountryConfig ? t('order.placeholderPostcode') + `${currentCountryConfig.postcodeLength}${t('order.postcodeHint').replace('{0}', '')}` : t('order.placeholderPostcode')"
clearable 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>
<el-form-item :label="t('order.shippingPhone')" prop="shippingPhone"> <!-- 城市PayPal要求必填- 常驻展示必填越南不使用此字段使用独立的省//// -->
<el-form-item v-if="form.shippingCountry !== 'VN'" :label="getCityLabel()" prop="shippingCity" required>
<div :class="isMobile ? 'mobile-input-group' : 'desktop-input-group'">
<el-input <el-input
v-model="form.shippingPhone" v-model="form.shippingCity"
:placeholder="currentCountryConfig ? `${t('order.pleaseEnter')}${t('order.shippingPhone')}${t('order.phoneCode')}${currentCountryConfig.phoneCode}` : t('order.pleaseEnter') + t('order.shippingPhone') + '' + t('order.phoneCode') + ''" :placeholder="getCityPlaceholder()"
:style="isMobile ? 'width: 100%' : form.shippingCountry === 'MY' ? 'width: 100%' : 'width: 48%'"
clearable clearable
@input="updateAddressLine1"
/> />
<!-- /省字段泰国显示府菲律宾显示省其他显示州/可选 -->
<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
@input="updateAddressLine1"
/>
</div>
</el-form-item> </el-form-item>
<!-- 收货国家只读显示根据货币自动确定 --> <!-- 越南//PayPal要求必填- 常驻展示必填 -->
<el-form-item :label="t('order.shippingCountry')" prop="shippingCountry"> <template v-if="form.shippingCountry === 'VN'">
<el-form-item :label="t('order.province')" prop="shippingProvince" required>
<el-input <el-input
v-model="currentCountryDisplayName" v-model="form.shippingProvince"
disabled :placeholder="t('order.placeholderProvinceVN')"
style="width: 100%"
/>
</el-form-item>
<!-- 详细地址1门牌号街道楼栋- 所有国家都显示 -->
<el-form-item v-if="showField('shippingAddressLine1')" :label="t('order.addressLine1')" prop="shippingAddressLine1">
<el-input
v-model="form.shippingAddressLine1"
type="textarea"
:rows="2"
:placeholder="t('order.placeholderAddressLine1')"
clearable clearable
@input="updateAddressLine1"
/> />
</el-form-item> </el-form-item>
<el-form-item :label="t('order.district')" prop="shippingDistrict" required>
<!-- 详细地址2楼层单元号可选 -->
<el-form-item v-if="showField('shippingAddressLine2')" :label="t('order.addressLine2')" prop="shippingAddressLine2">
<el-input <el-input
v-model="form.shippingAddressLine2" v-model="form.shippingDistrict"
:placeholder="t('order.placeholderAddressLine2')" :placeholder="t('order.placeholderDistrictVN')"
clearable clearable
@input="updateAddressLine1"
/> />
</el-form-item> </el-form-item>
<el-form-item :label="t('order.ward')" prop="shippingWard" required>
<el-input
v-model="form.shippingWard"
:placeholder="t('order.placeholderWardVN')"
clearable
@input="updateAddressLine1"
/>
</el-form-item>
</template>
<!-- 新加坡组屋号和单元号 --> <!-- 新加坡组屋号和单元号 -->
<template v-if="form.shippingCountry === 'SG'"> <template v-if="form.shippingCountry === 'SG'">
@@ -159,39 +183,6 @@
/> />
</el-form-item> </el-form-item>
<!-- 越南// -->
<template v-if="form.shippingCountry === 'VN'">
<el-form-item :label="t('order.province')" prop="shippingProvince">
<el-input
v-model="form.shippingProvince"
:placeholder="t('order.placeholderProvinceVN')"
clearable
/>
</el-form-item>
<el-form-item :label="t('order.district')" prop="shippingDistrict">
<el-input
v-model="form.shippingDistrict"
:placeholder="t('order.placeholderDistrictVN')"
clearable
/>
</el-form-item>
<el-form-item :label="t('order.ward')" prop="shippingWard">
<el-input
v-model="form.shippingWard"
:placeholder="t('order.placeholderWardVN')"
clearable
/>
</el-form-item>
</template>
<!-- 马来西亚州属 -->
<el-form-item v-if="form.shippingCountry === 'MY'" :label="t('order.stateMalaysia')" prop="shippingStateMalaysia">
<el-input
v-model="form.shippingStateMalaysia"
:placeholder="t('order.placeholderStateMalaysia')"
clearable
/>
</el-form-item>
<!-- 泰国行政区域/Tambon --> <!-- 泰国行政区域/Tambon -->
<el-form-item v-if="form.shippingCountry === 'TH' && showField('shippingAdministrativeArea')" :label="t('order.administrativeArea')" prop="shippingAdministrativeArea"> <el-form-item v-if="form.shippingCountry === 'TH' && showField('shippingAdministrativeArea')" :label="t('order.administrativeArea')" prop="shippingAdministrativeArea">
@@ -202,74 +193,55 @@
/> />
</el-form-item> </el-form-item>
<!-- 城市和州/通用字段- 越南不使用此字段使用独立的省//// --> <!-- 马来西亚州属 - 常驻展示 -->
<el-form-item v-if="form.shippingCountry !== 'VN'" :label="getCityLabel()" prop="shippingCity"> <el-form-item v-if="form.shippingCountry === 'MY'" :label="t('order.stateMalaysia')" prop="shippingStateMalaysia">
<div :class="isMobile ? 'mobile-input-group' : 'desktop-input-group'">
<el-input <el-input
v-model="form.shippingCity" v-model="form.shippingStateMalaysia"
:placeholder="getCityPlaceholder()" :placeholder="t('order.placeholderStateMalaysia')"
: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="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="t('order.floorUnit')" prop="shippingFloorUnit">
<el-input
v-model="form.shippingFloorUnit"
:placeholder="t('order.placeholderFloorUnit')"
clearable clearable
@input="updateAddressLine1"
/> />
</el-form-item> </el-form-item>
<!-- 兼容旧字段街道地址如果新字段为空使用旧字段 --> <!-- 详细地址1门牌号街道楼栋- 所有国家都显示必填放在最下面 -->
<el-form-item v-if="!form.shippingAddressLine1" :label="t('order.street')" prop="shippingStreet"> <el-form-item v-if="showField('shippingAddressLine1')" :label="t('order.addressLine1')" prop="shippingAddressLine1" required>
<el-input <el-input
v-model="form.shippingStreet" v-model="form.shippingAddressLine1"
type="textarea" type="textarea"
:rows="2" :rows="2"
:placeholder="t('order.placeholderStreet')" :placeholder="t('order.placeholderAddressLine1')"
clearable clearable
/> />
</el-form-item> </el-form-item>
<el-form-item :label="t('order.remark')" prop="remark"> <!-- 更多地址信息按钮 -->
<el-form-item>
<el-button
type="text"
@click="showMoreAddressFields = !showMoreAddressFields"
style="padding: 0; color: #409eff;"
>
<el-icon style="margin-right: 4px;">
<ArrowDown v-if="!showMoreAddressFields" />
<ArrowUp v-else />
</el-icon>
{{ showMoreAddressFields ? t('order.collapse') : t('order.more') }}
</el-button>
</el-form-item>
<!-- 更多地址信息 - 只包含详细地址2 -->
<template v-if="showMoreAddressFields">
<!-- 详细地址2楼层单元号可选- 放入更多按钮中 -->
<el-form-item v-if="showField('shippingAddressLine2')" :label="t('order.addressLine2')" prop="shippingAddressLine2">
<el-input <el-input
v-model="form.remark" v-model="form.shippingAddressLine2"
type="textarea" :placeholder="t('order.placeholderAddressLine2')"
:rows="3"
:placeholder="t('order.placeholderRemark')"
clearable clearable
/> />
</el-form-item> </el-form-item>
</template>
<el-form-item> <el-form-item>
<el-button type="primary" size="large" @click="submitForm" :loading="loading" style="width: 200px"> <el-button type="primary" size="large" @click="submitForm" :loading="loading" style="width: 200px">
@@ -287,6 +259,7 @@ import { ref, reactive, computed, watch, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { ArrowDown, ArrowUp } from '@element-plus/icons-vue'
import { createCustomerOrder } from '../api/order' import { createCustomerOrder } from '../api/order'
import { formatAmount } from '../utils/helpers' import { formatAmount } from '../utils/helpers'
import { getCountryConfig, getCountryByCurrency, validatePostcode, getRequiredFields } from '../utils/countryConfig' import { getCountryConfig, getCountryByCurrency, validatePostcode, getRequiredFields } from '../utils/countryConfig'
@@ -301,13 +274,13 @@ const formRef = ref()
const loading = ref(false) const loading = ref(false)
const productInfo = ref(null) const productInfo = ref(null)
const postcodeMatching = ref(false) // 邮编匹配中 const postcodeMatching = ref(false) // 邮编匹配中
const showMoreAddressFields = ref(false) // 控制更多地址字段的显示
const isAutoUpdating = ref(false) // 防止自动更新时的递归调用
const form = reactive({ const form = reactive({
customerName: '', customerName: '',
customerPhone: '', customerPhone: '',
customerEmail: '', customerEmail: '',
shippingName: '',
shippingPhone: '',
shippingCountry: '', shippingCountry: '',
shippingState: '', shippingState: '',
shippingCity: '', shippingCity: '',
@@ -324,9 +297,7 @@ const form = reactive({
shippingProvince: '', shippingProvince: '',
shippingDistrict: '', shippingDistrict: '',
shippingWard: '', shippingWard: '',
shippingStateMalaysia: '', shippingStateMalaysia: ''
shippingFloorUnit: '',
remark: ''
}) })
// 当前国家配置 // 当前国家配置
@@ -366,9 +337,10 @@ const currentCountryDisplayName = computed(() => {
// 是否显示特定字段 // 是否显示特定字段
const showField = (fieldName) => { const showField = (fieldName) => {
if (!currentCountryConfig.value) { if (!currentCountryConfig.value) {
// 默认显示基础字段 // 默认显示基础字段(包括详细地址)
return ['shippingName', 'shippingPhone', 'shippingCountry', 'shippingCity', return ['shippingCountry', 'shippingCity',
'shippingState', 'shippingStreet', 'shippingPostcode'].includes(fieldName) 'shippingState', 'shippingPostcode',
'shippingAddressLine1', 'shippingAddressLine2'].includes(fieldName)
} }
const specialFields = currentCountryConfig.value.specialFields || [] const specialFields = currentCountryConfig.value.specialFields || []
@@ -385,8 +357,8 @@ const showField = (fieldName) => {
} }
// 基础字段始终显示 // 基础字段始终显示
const baseFields = ['shippingName', 'shippingPhone', 'shippingCountry', const baseFields = ['shippingCountry',
'shippingCity', 'shippingState', 'shippingStreet', 'shippingCity', 'shippingState',
'shippingPostcode', 'shippingAddressLine1', 'shippingAddressLine2'] 'shippingPostcode', 'shippingAddressLine1', 'shippingAddressLine2']
if (baseFields.includes(fieldName)) { if (baseFields.includes(fieldName)) {
return true return true
@@ -408,17 +380,9 @@ const getFieldLabel = (fieldName) => {
return fieldName return fieldName
} }
// 获取城市字段标签(根据国家动态显示) // 获取城市字段标签(根据国家动态显示,只显示对应语言,不包含中文
const getCityLabel = () => { const getCityLabel = () => {
if (form.shippingCountry === 'TH') { // 直接返回国际化翻译i18n会根据当前语言环境自动返回对应语言的标签
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') return t('order.cityTown')
} }
@@ -440,6 +404,66 @@ const getStatePlaceholder = () => {
return t('order.placeholderStateOptional') return t('order.placeholderStateOptional')
} }
// 自动更新详细地址1将省/州、市/郡、区/坊等信息拼接到详细地址1
// 使用一个标志来跟踪是否应该自动更新(避免用户手动编辑时被覆盖)
const updateAddressLine1 = () => {
if (isAutoUpdating.value) return // 防止递归更新
isAutoUpdating.value = true
try {
// 构建地址组件数组(省/州、市/郡、区/坊等)
const addressParts = []
// 根据国家添加不同的地址组件
if (form.shippingCountry === 'VN') {
// 越南:省、市/郡、区/坊
if (form.shippingProvince) addressParts.push(form.shippingProvince)
if (form.shippingDistrict) addressParts.push(form.shippingDistrict)
if (form.shippingWard) addressParts.push(form.shippingWard)
} else {
// 其他国家:城市、州/省
if (form.shippingCity) addressParts.push(form.shippingCity)
if (form.shippingState) addressParts.push(form.shippingState)
if (form.shippingCountry === 'MY' && form.shippingStateMalaysia) {
addressParts.push(form.shippingStateMalaysia)
}
}
// 如果有地址组件拼接到详细地址1
if (addressParts.length > 0) {
const addressSuffix = addressParts.join(', ')
const currentAddress = form.shippingAddressLine1 || ''
// 检查当前地址是否已包含这些组件(避免重复拼接)
const hasAllParts = addressParts.every(part => currentAddress.includes(part))
if (!hasAllParts) {
// 移除旧的地址组件(如果存在),保留用户手动输入的门牌号、街道、楼栋等信息
let cleanAddress = currentAddress
addressParts.forEach(part => {
// 移除该组件及其前后的逗号和空格
const escapedPart = part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
// 移除 ", 组件" 或 "组件, " 或单独的 "组件"
cleanAddress = cleanAddress.replace(new RegExp(`,\\s*${escapedPart}(?=\\s|,|$)`, 'g'), '')
cleanAddress = cleanAddress.replace(new RegExp(`${escapedPart}\\s*,`, 'g'), '')
cleanAddress = cleanAddress.replace(new RegExp(`^${escapedPart}\\s*$`, 'g'), '')
})
cleanAddress = cleanAddress.trim().replace(/^,\s*|,\s*$/g, '').replace(/,\s*,/g, ',')
// 追加新的地址组件
if (cleanAddress) {
form.shippingAddressLine1 = `${cleanAddress}, ${addressSuffix}`
} else {
form.shippingAddressLine1 = addressSuffix
}
}
}
} finally {
isAutoUpdating.value = false
}
}
// 监听国家变化,清空相关字段 // 监听国家变化,清空相关字段
watch(() => form.shippingCountry, (newCountry, oldCountry) => { watch(() => form.shippingCountry, (newCountry, oldCountry) => {
if (newCountry !== oldCountry) { if (newCountry !== oldCountry) {
@@ -453,14 +477,19 @@ watch(() => form.shippingCountry, (newCountry, oldCountry) => {
form.shippingDistrict = '' form.shippingDistrict = ''
form.shippingWard = '' form.shippingWard = ''
form.shippingAdministrativeArea = '' form.shippingAdministrativeArea = ''
// 清空详细地址1因为地址组件已清空
// 更新电话区号提示 form.shippingAddressLine1 = ''
if (currentCountryConfig.value) {
form.shippingPhone = currentCountryConfig.value.phoneCode + ' '
}
} }
}) })
// 监听地址组件变化自动更新详细地址1
watch([() => form.shippingCity, () => form.shippingState, () => form.shippingStateMalaysia,
() => form.shippingProvince, () => form.shippingDistrict, () => form.shippingWard],
() => {
updateAddressLine1()
}
)
// 监听邮编变化,自动匹配城市 // 监听邮编变化,自动匹配城市
watch(() => form.shippingPostcode, async (newPostcode) => { watch(() => form.shippingPostcode, async (newPostcode) => {
if (!newPostcode || !form.shippingCountry) return if (!newPostcode || !form.shippingCountry) return
@@ -501,40 +530,19 @@ const getRules = () => {
customerEmail: [ customerEmail: [
{ type: 'email', message: t('order.validationInvalidEmail'), trigger: 'blur' } { type: 'email', message: t('order.validationInvalidEmail'), trigger: 'blur' }
], ],
shippingName: [
{ required: true, message: t('order.validationRequired', [t('order.shippingName')]), trigger: 'blur' }
],
shippingPhone: [
{ required: true, message: t('order.validationRequired', [t('order.shippingPhone')]), trigger: 'blur' },
{ pattern: /^[0-9+\-\s()]+$/, message: t('order.validationInvalidPhone'), trigger: 'blur' }
],
shippingCountry: [ shippingCountry: [
{ required: true, message: t('order.validationSelectCountry'), trigger: 'change' } { required: true, message: t('order.validationSelectCountry'), trigger: 'change' }
], ],
shippingCity: [ // 详细地址1始终必填
{ required: true, message: t('order.validationRequired', [t('order.cityTown')]), trigger: 'blur' } shippingAddressLine1: [
],
shippingStreet: [
{ required: true, message: t('order.validationRequired', [t('order.street')]), trigger: 'blur' }
]
}
// 根据国家配置添加必填字段验证
if (currentCountryConfig.value) {
const requiredFields = getRequiredFields(form.shippingCountry)
if (requiredFields.includes('shippingAddressLine1')) {
baseRules.shippingAddressLine1 = [
{ required: true, message: t('order.validationRequired', [t('order.addressLine1')]), trigger: 'blur' } { required: true, message: t('order.validationRequired', [t('order.addressLine1')]), trigger: 'blur' }
] ],
} // 邮编始终必填PayPal要求
shippingPostcode: [
if (requiredFields.includes('shippingPostcode')) {
baseRules.shippingPostcode = [
{ required: true, message: t('order.validationRequired', [t('order.postcode')]), trigger: 'blur' }, { required: true, message: t('order.validationRequired', [t('order.postcode')]), trigger: 'blur' },
{ {
validator: (rule, value, callback) => { validator: (rule, value, callback) => {
if (value && !validatePostcode(form.shippingCountry, value)) { if (value && currentCountryConfig.value && !validatePostcode(form.shippingCountry, value)) {
callback(new Error(t('order.validationPostcodeFormat', [currentCountryConfig.value.postcodeLength]))) callback(new Error(t('order.validationPostcodeFormat', [currentCountryConfig.value.postcodeLength])))
} else { } else {
callback() callback()
@@ -542,9 +550,17 @@ const getRules = () => {
}, },
trigger: 'blur' trigger: 'blur'
} }
],
// 城市始终必填PayPal要求越南除外
shippingCity: [
{ required: true, message: t('order.validationRequired', [t('order.city')]), trigger: 'blur' }
] ]
} }
// 根据国家配置添加必填字段验证
if (currentCountryConfig.value) {
const requiredFields = getRequiredFields(form.shippingCountry)
if (requiredFields.includes('shippingBlockNumber')) { if (requiredFields.includes('shippingBlockNumber')) {
baseRules.shippingBlockNumber = [ baseRules.shippingBlockNumber = [
{ required: true, message: t('order.validationRequired', [t('order.blockNumber')]), trigger: 'blur' } { required: true, message: t('order.validationRequired', [t('order.blockNumber')]), trigger: 'blur' }
@@ -639,12 +655,12 @@ const submitForm = async () => {
customerName: form.customerName, customerName: form.customerName,
customerPhone: form.customerPhone, customerPhone: form.customerPhone,
customerEmail: form.customerEmail || null, customerEmail: form.customerEmail || null,
shippingName: form.shippingName, shippingName: form.customerName, // 使用客户姓名作为收货人姓名
shippingPhone: form.shippingPhone, shippingPhone: form.customerPhone, // 使用客户电话作为收货人电话
shippingCountry: form.shippingCountry, shippingCountry: form.shippingCountry,
shippingState: form.shippingState || null, shippingState: form.shippingState || null,
shippingCity: shippingCity, // 使用映射后的值,越南使用 shippingDistrict shippingCity: shippingCity, // 使用映射后的值,越南使用 shippingDistrict
shippingStreet: form.shippingStreet || form.shippingAddressLine1, // 兼容旧字段 shippingStreet: form.shippingAddressLine1, // 使用详细地址1
shippingPostcode: form.shippingPostcode || null, shippingPostcode: form.shippingPostcode || null,
// 东南亚地址扩展字段 // 东南亚地址扩展字段
shippingAddressLine1: form.shippingAddressLine1 || null, shippingAddressLine1: form.shippingAddressLine1 || null,
@@ -658,8 +674,6 @@ const submitForm = async () => {
shippingDistrict: form.shippingDistrict || null, shippingDistrict: form.shippingDistrict || null,
shippingWard: form.shippingWard || null, shippingWard: form.shippingWard || null,
shippingStateMalaysia: form.shippingStateMalaysia || null, shippingStateMalaysia: form.shippingStateMalaysia || null,
shippingFloorUnit: form.shippingFloorUnit || null,
remark: form.remark || null
} }
const response = await createCustomerOrder(orderData) const response = await createCustomerOrder(orderData)
@@ -703,10 +717,6 @@ onMounted(async () => {
const countryCode = getCountryByCurrency(data.product.currency) const countryCode = getCountryByCurrency(data.product.currency)
if (countryCode) { if (countryCode) {
form.shippingCountry = countryCode form.shippingCountry = countryCode
const config = getCountryConfig(countryCode)
if (config) {
form.shippingPhone = config.phoneCode + ' '
}
} }
} }
} }
@@ -755,22 +765,18 @@ onMounted(async () => {
.product-info-main { .product-info-main {
display: flex; display: flex;
gap: 20px; align-items: center;
gap: 15px;
margin-bottom: 15px;
} }
.product-info-image { .product-info-image {
width: 120px; width: 80px;
height: 120px; height: 80px;
border-radius: 8px; border-radius: 6px;
border: 1px solid #e4e7ed; border: 1px solid #e4e7ed;
flex-shrink: 0; flex-shrink: 0;
} object-fit: cover;
.product-info-details {
flex: 1;
display: flex;
flex-direction: column;
gap: 10px;
} }
.product-info-name { .product-info-name {
@@ -778,6 +784,13 @@ onMounted(async () => {
font-weight: 600; font-weight: 600;
color: #303133; color: #303133;
line-height: 1.5; line-height: 1.5;
flex: 1;
}
.product-info-details {
display: flex;
flex-direction: column;
gap: 10px;
} }
.product-info-sku { .product-info-sku {
@@ -830,39 +843,74 @@ onMounted(async () => {
font-weight: 700; font-weight: 700;
} }
/* 移动端优化 */ /* 移动端优先设计 - 响应式优化 */
@media (max-width: 768px) { @media (max-width: 768px) {
.create-order { .create-order {
padding: 10px; padding: 0;
max-width: 100%; max-width: 100%;
} }
.el-card {
border-radius: 0;
box-shadow: none;
border: none;
margin: 0;
}
.card-header { .card-header {
font-size: 16px; font-size: 16px;
padding: 10px 0; padding: 12px 16px;
font-weight: 600;
} }
.el-card__body {
padding: 16px;
}
/* 商品信息卡片优化 */
.product-info-card { .product-info-card {
margin-bottom: 15px; margin-bottom: 16px;
border: 1px solid #e4e7ed;
border-radius: 8px;
} }
.product-card-header { .product-card-header {
font-size: 14px; font-size: 14px;
font-weight: 600;
}
.product-info-content {
padding: 12px 0;
} }
.product-info-main { .product-info-main {
flex-direction: column; flex-direction: row;
gap: 12px; gap: 12px;
align-items: flex-start;
margin-bottom: 12px;
} }
.product-info-image { .product-info-image {
width: 100%; width: 60px;
height: 200px; height: 60px;
align-self: center; flex-shrink: 0;
border-radius: 6px;
} }
.product-info-name { .product-info-name {
font-size: 14px; font-size: 14px;
line-height: 1.4;
word-break: break-word;
flex: 1;
}
.product-info-details {
gap: 8px;
}
.product-info-sku {
padding: 8px 10px;
font-size: 12px;
} }
.product-info-price { .product-info-price {
@@ -870,33 +918,58 @@ onMounted(async () => {
align-items: flex-start; align-items: flex-start;
gap: 8px; gap: 8px;
padding: 10px; padding: 10px;
font-size: 13px;
}
.price-label,
.quantity-label {
font-size: 12px;
}
.price-value,
.quantity-value {
font-size: 14px;
} }
.total-label { .total-label {
margin-left: 0; margin-left: 0;
margin-top: 8px; margin-top: 8px;
font-size: 16px; font-size: 14px;
font-weight: 600;
} }
.total-value { .total-value {
font-size: 20px; font-size: 18px;
} }
/* 表单优化 */ /* 表单优化 */
.el-form {
padding: 0;
}
.el-form-item { .el-form-item {
margin-bottom: 18px; margin-bottom: 16px;
} }
.el-form-item__label { .el-form-item__label {
padding-bottom: 5px; padding-bottom: 6px;
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
line-height: 1.4;
margin-bottom: 4px;
} }
.el-input, .el-input,
.el-select, .el-select,
.el-textarea { .el-textarea {
width: 100%; width: 100%;
font-size: 14px;
}
.el-input__inner,
.el-textarea__inner {
font-size: 14px;
padding: 10px 12px;
} }
/* 地址字段优化 */ /* 地址字段优化 */
@@ -911,39 +984,22 @@ onMounted(async () => {
width: 100%; 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 { .el-divider {
margin: 20px 0; margin: 16px 0;
} }
.el-divider__text { .el-divider__text {
font-size: 14px; font-size: 14px;
padding: 0 15px; padding: 0 12px;
font-weight: 600;
} }
/* 提示信息优化 */ /* 提示信息优化 */
.el-alert { .el-alert {
margin-bottom: 15px; margin-bottom: 12px;
font-size: 12px; font-size: 12px;
padding: 10px 12px;
} }
/* 输入组优化 */ /* 输入组优化 */
@@ -951,23 +1007,73 @@ onMounted(async () => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
gap: 10px;
} }
.desktop-input-group { .desktop-input-group {
display: flex; display: flex;
width: 100%; width: 100%;
gap: 4%;
} }
.mobile-postcode-group { .mobile-postcode-group {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
gap: 8px;
} }
.desktop-postcode-group { .desktop-postcode-group {
display: flex; display: flex;
align-items: center; align-items: center;
width: 100%; width: 100%;
gap: 10px;
}
/* 按钮优化 */
.el-form-item:last-child {
margin-bottom: 0;
padding-top: 16px;
border-top: 1px solid #e4e7ed;
position: sticky;
bottom: 0;
background: white;
z-index: 10;
}
.el-form-item:last-child .el-button {
width: 100%;
margin: 0;
height: 48px;
font-size: 16px;
font-weight: 600;
border-radius: 8px;
}
.el-form-item:last-child .el-button + .el-button {
margin-top: 10px;
margin-left: 0;
}
/* 更多按钮优化 */
.el-button--text {
font-size: 14px;
padding: 8px 0;
}
/* 国家选择器优化 */
.el-select {
width: 100%;
}
/* 文本域优化 */
.el-textarea {
width: 100%;
}
.el-textarea__inner {
min-height: 80px;
line-height: 1.5;
} }
} }
</style> </style>

View File

@@ -152,7 +152,7 @@
:style="isMobile ? 'width: 100%' : 'width: 200px'" :style="isMobile ? 'width: 100%' : 'width: 200px'"
> >
<el-icon><Money /></el-icon> <el-icon><Money /></el-icon>
{{ $t('confirm.payNow') }} {{ countdown > 0 ? `${$t('confirm.payNow')} (${countdown}s)` : $t('confirm.payNow') }}
</el-button> </el-button>
<el-button @click="goBack" :style="isMobile ? 'width: 100%; margin-top: 10px; margin-left: 0' : 'margin-left: 10px'">{{ $t('confirm.back') }}</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> </div>
@@ -168,7 +168,7 @@
</template> </template>
<script setup> <script setup>
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
@@ -185,6 +185,9 @@ const route = useRoute()
const orderLoading = ref(true) const orderLoading = ref(true)
const payLoading = ref(false) const payLoading = ref(false)
const order = ref(null) const order = ref(null)
const autoPayTimer = ref(null) // 自动支付定时器
const countdownTimer = ref(null) // 倒计时定时器
const countdown = ref(0) // 倒计时(秒)
// 移动端检测 // 移动端检测
const isMobile = computed(() => { const isMobile = computed(() => {
@@ -363,6 +366,31 @@ const loadOrder = async () => {
order.value = null order.value = null
} finally { } finally {
orderLoading.value = false orderLoading.value = false
// 如果订单状态是未支付3秒后自动触发支付
if (order.value && order.value.paymentStatus === 'UNPAID') {
countdown.value = 3
// 倒计时显示
countdownTimer.value = setInterval(() => {
countdown.value--
if (countdown.value <= 0) {
if (countdownTimer.value) {
clearInterval(countdownTimer.value)
countdownTimer.value = null
}
}
}, 1000)
// 3秒后自动触发支付
autoPayTimer.value = setTimeout(() => {
if (countdownTimer.value) {
clearInterval(countdownTimer.value)
countdownTimer.value = null
}
countdown.value = 0
handlePay()
}, 3000)
}
} }
} }
@@ -378,6 +406,17 @@ const handlePay = async () => {
return return
} }
// 清除自动支付定时器和倒计时(如果存在)
if (autoPayTimer.value) {
clearTimeout(autoPayTimer.value)
autoPayTimer.value = null
}
if (countdownTimer.value) {
clearInterval(countdownTimer.value)
countdownTimer.value = null
}
countdown.value = 0
payLoading.value = true payLoading.value = true
try { try {
@@ -454,6 +493,18 @@ const handlePay = async () => {
onMounted(() => { onMounted(() => {
loadOrder() loadOrder()
}) })
// 组件卸载时清理定时器
onUnmounted(() => {
if (autoPayTimer.value) {
clearTimeout(autoPayTimer.value)
autoPayTimer.value = null
}
if (countdownTimer.value) {
clearInterval(countdownTimer.value)
countdownTimer.value = null
}
})
</script> </script>
<style scoped> <style scoped>
@@ -628,18 +679,31 @@ onMounted(() => {
border-top: 1px solid #ebeef5; border-top: 1px solid #ebeef5;
} }
/* 移动端优化 */ /* 移动端优先设计 - 响应式优化 */
@media (max-width: 768px) { @media (max-width: 768px) {
.order-confirm { .order-confirm {
padding: 10px; padding: 0;
max-width: 100%; max-width: 100%;
} }
.el-card {
border-radius: 0;
box-shadow: none;
border: none;
margin: 0;
}
.el-card__body {
padding: 16px;
}
.card-header { .card-header {
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
gap: 10px; gap: 12px;
font-size: 16px; font-size: 16px;
font-weight: 600;
padding: 12px 16px;
} }
.order-info-section { .order-info-section {
@@ -647,22 +711,47 @@ onMounted(() => {
} }
.order-info-section h3 { .order-info-section h3 {
font-size: 14px; font-size: 15px;
margin-bottom: 12px; margin-bottom: 12px;
padding-bottom: 8px; padding-bottom: 8px;
font-weight: 600;
border-bottom: 2px solid #409eff;
} }
.order-amount { .order-amount {
font-size: 18px; font-size: 20px;
font-weight: 700;
}
.order-amount-section {
display: flex;
flex-direction: column;
gap: 12px;
}
.currency-conversion-info {
margin-top: 12px;
}
.conversion-alert-box {
border-radius: 8px;
}
.conversion-alert-content {
padding: 0;
} }
.conversion-title { .conversion-title {
font-size: 14px; font-size: 14px;
margin-bottom: 10px; margin-bottom: 10px;
flex-direction: row;
align-items: center;
gap: 6px;
} }
.title-text { .title-text {
font-size: 14px; font-size: 14px;
font-weight: 600;
} }
.warning-icon { .warning-icon {
@@ -670,35 +759,53 @@ onMounted(() => {
} }
.conversion-main-info { .conversion-main-info {
padding: 12px; padding: 12px 0;
} }
.payment-amount-highlight { .payment-amount-highlight {
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
gap: 6px; gap: 6px;
margin-bottom: 8px;
} }
.payment-amount-large { .payment-amount-large {
font-size: 20px; font-size: 22px;
font-weight: 700;
} }
.exchange-rate-info { .exchange-rate-info {
flex-direction: column; flex-direction: column;
gap: 6px; gap: 6px;
margin-top: 8px; margin-top: 8px;
font-size: 12px;
}
.rate-locked-info {
margin-top: 8px;
font-size: 12px;
display: flex;
align-items: center;
gap: 4px;
} }
.action-buttons { .action-buttons {
margin-top: 20px; margin-top: 20px;
padding-top: 15px; padding-top: 16px;
border-top: 1px solid #e4e7ed;
position: sticky;
bottom: 0;
background: white;
z-index: 10;
} }
.action-buttons .el-button { .action-buttons .el-button {
width: 100%; width: 100%;
margin: 0; margin: 0;
height: 44px; height: 48px;
font-size: 16px; font-size: 16px;
font-weight: 600;
border-radius: 8px;
} }
.action-buttons .el-button + .el-button { .action-buttons .el-button + .el-button {
@@ -710,26 +817,49 @@ onMounted(() => {
font-size: 13px; font-size: 13px;
} }
.el-descriptions--border {
border: 1px solid #e4e7ed;
border-radius: 8px;
overflow: hidden;
}
.el-descriptions-item {
padding: 12px;
}
.el-descriptions-item__label { .el-descriptions-item__label {
font-size: 13px; font-size: 13px;
font-weight: 600;
width: 100px; width: 100px;
padding-right: 12px;
color: #606266;
} }
.el-descriptions-item__content { .el-descriptions-item__content {
font-size: 13px; font-size: 13px;
color: #303133;
word-break: break-word;
} }
.shipping-address-detail { .shipping-address-detail {
font-size: 13px; font-size: 13px;
line-height: 1.6; line-height: 1.6;
padding: 8px 0;
} }
.address-line { .address-line {
margin-bottom: 6px; margin-bottom: 8px;
padding: 6px 0;
border-bottom: 1px solid #f0f0f0;
}
.address-line:last-child {
border-bottom: none;
} }
.address-line strong { .address-line strong {
font-size: 12px; font-weight: 600;
color: #606266;
margin-right: 6px; margin-right: 6px;
} }
} }

View File

@@ -198,62 +198,6 @@
</el-tabs> </el-tabs>
</div> </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> </template>
</div> </div>
</template> </template>
@@ -281,7 +225,6 @@ const hoveredImage = ref(null)
const quantity = ref(1) const quantity = ref(1)
const activeTab = ref('detail') const activeTab = ref('detail')
const selectedSku = ref(null) const selectedSku = ref(null)
const showConfirmDialog = ref(false)
// 移动端检测 // 移动端检测
const isMobile = computed(() => { const isMobile = computed(() => {
@@ -413,7 +356,7 @@ const hoverImage = (img) => {
hoveredImage.value = img hoveredImage.value = img
} }
// 立即购买 // 立即购买(直接跳转,不再显示确认弹窗)
const handleBuyNow = () => { const handleBuyNow = () => {
if (!selectedSku.value) { if (!selectedSku.value) {
ElMessage.warning('请先选择商品SKU') ElMessage.warning('请先选择商品SKU')
@@ -425,18 +368,6 @@ const handleBuyNow = () => {
return return
} }
showConfirmDialog.value = true
}
// 确认购买
const confirmBuy = () => {
if (!selectedSku.value) {
ElMessage.warning('请先选择商品SKU')
return
}
showConfirmDialog.value = false
// 构建订单数据 // 构建订单数据
const orderData = { const orderData = {
product: { product: {
@@ -1209,97 +1140,245 @@ onMounted(() => {
line-height: 1.6; line-height: 1.6;
} }
/* 响应式设计 */ /* 移动端优先设计 - 响应式设计 */
@media (max-width: 768px) { @media (max-width: 768px) {
.product-detail { .product-detail {
padding: 10px; padding: 0;
max-width: 100%;
} }
.product-main { .product-main {
flex-direction: column; flex-direction: column;
padding: 15px; padding: 12px;
gap: 20px; gap: 16px;
margin: 0;
border-radius: 0;
box-shadow: none;
} }
.product-images { .product-images {
flex: 1; flex: 1;
width: 100%; width: 100%;
margin-bottom: 0;
} }
.main-image { .main-image {
height: 300px; height: auto;
min-height: 250px;
max-height: 400px;
border-radius: 8px;
}
.thumbnail-list {
margin-top: 12px;
gap: 8px;
overflow-x: auto;
padding-bottom: 4px;
-webkit-overflow-scrolling: touch;
}
.thumbnail-item {
width: 60px;
height: 60px;
flex-shrink: 0;
} }
.product-info { .product-info {
padding-left: 0; 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%; width: 100%;
} }
.service-section { .product-title {
flex-direction: column; font-size: 18px;
gap: 15px; margin-bottom: 16px;
line-height: 1.4;
word-break: break-word;
} }
.currency-selector-card { .currency-selector-card {
padding: 10px 12px; padding: 12px;
margin-top: 10px; margin-top: 0;
margin-bottom: 15px; margin-bottom: 16px;
border-radius: 8px;
} }
.currency-selector-header { .currency-selector-header {
margin-bottom: 8px; margin-bottom: 10px;
} }
.currency-icon { .currency-icon {
font-size: 16px; font-size: 18px;
margin-right: 5px; margin-right: 6px;
} }
.currency-selector-header .selector-label { .currency-selector-header .selector-label {
font-size: 13px; font-size: 14px;
} }
.currency-selector-content { .currency-selector-content {
padding: 8px; padding: 0;
} }
.currency-radio-group { .currency-radio-group {
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: wrap;
gap: 4px; gap: 6px;
width: 100%; width: 100%;
} }
.currency-radio-button { .currency-radio-button {
flex: 1; flex: 1;
min-width: 0; min-width: calc(50% - 3px);
} }
.currency-radio-button :deep(.el-radio-button__inner) { .currency-radio-button :deep(.el-radio-button__inner) {
font-size: 11px; font-size: 12px;
padding: 6px 4px; padding: 8px 6px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
width: 100%; width: 100%;
} }
.sku-selection-section {
margin-bottom: 16px;
}
.sku-section-title {
font-size: 14px;
margin-bottom: 10px;
}
.sku-list {
max-height: none;
gap: 8px;
}
.sku-item {
padding: 12px;
border-radius: 8px;
}
.sku-name {
font-size: 14px;
margin-bottom: 6px;
}
.sku-price-stock {
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
.sku-price {
font-size: 16px;
}
.sku-stock {
font-size: 12px;
}
.price-section {
margin-bottom: 16px;
padding: 12px;
border-radius: 8px;
}
.price-label {
font-size: 13px;
margin-bottom: 6px;
}
.price-main {
flex-direction: row;
align-items: baseline;
}
.currency {
font-size: 16px;
margin-right: 4px;
}
.price {
font-size: 24px;
}
.quantity-section {
margin-bottom: 16px;
padding: 12px;
background: #f5f7fa;
border-radius: 8px;
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.quantity-label {
font-size: 14px;
font-weight: 600;
}
.quantity-input {
width: 100%;
}
.quantity-input :deep(.el-input-number) {
width: 100%;
}
.stock-info {
font-size: 12px;
color: #909399;
}
.service-section {
flex-direction: column;
gap: 10px;
margin-bottom: 16px;
padding: 12px;
background: #f9fafb;
border-radius: 8px;
}
.service-item {
font-size: 13px;
gap: 8px;
}
.action-buttons {
flex-direction: column;
gap: 10px;
margin-top: 20px;
position: sticky;
bottom: 0;
background: white;
padding: 12px 0;
z-index: 10;
}
.buy-now-btn,
.pay-now-btn {
width: 100%;
height: 48px;
font-size: 16px;
font-weight: 600;
border-radius: 8px;
}
.product-details-section {
padding: 12px;
}
.product-details-section h3 {
font-size: 16px;
margin-bottom: 12px;
}
.product-details-content {
font-size: 14px;
line-height: 1.6;
}
/* 旧样式兼容 */ /* 旧样式兼容 */
.currency-selector { .currency-selector {
flex-direction: column; flex-direction: column;

View File

@@ -27,7 +27,7 @@ export default defineConfig({
port: 3000, port: 3000,
proxy: { proxy: {
'/api': { '/api': {
target: 'http://127.0.0.1:18082', target: 'http://127.0.0.1:8082',
changeOrigin: true, changeOrigin: true,
secure: false, secure: false,
ws: true, ws: true,
@@ -35,7 +35,7 @@ export default defineConfig({
configure: (proxy, options) => { configure: (proxy, options) => {
proxy.on('error', (err, req, res) => { proxy.on('error', (err, req, res) => {
console.error('代理错误:', err.message) console.error('代理错误:', err.message)
console.error('请确保后端服务已启动在 http://127.0.0.1:18082') console.error('请确保后端服务已启动在 http://127.0.0.1:8082')
}) })
} }
} }