Compare commits

..

6 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
857f46ad17 chore(config): 更新生产环境API配置和代理设置
- 将生产环境API基础URL从绝对路径改为相对路径 /api
- 修改开发环境代理配置,后端目标端口从 8082 改为 18082
- 更新相关注释和错误提示信息
- 移除生产环境配置中的服务器IP地址硬编码
- 添加
2025-12-26 14:11:46 +08:00
e251153d14 chore(config): 更新生产环境配置和构建设置
- 修复 .env.production 文件中的注释编码问题
- 移除路由守卫中的调试日志输出
- 调整错误处理机制,生产环境静默处理错误
- 移除应用挂载时的调试日志和异常处理包装
- 添加构建配置和基础路径设置
2025-12-26 10:54:17 +08:00
3662ee072b feat(config): 添加生产环境配置文件
- 配置生产环境服务器地址 175.178.252.59
- 设置后端 API 地址为 http://175.178.252.59:8082/api
- 配置 PingPong 模式为 sandbox
- 添加生产环境相关环境变量
2025-12-26 10:27:09 +08:00
3bdb2ff5f3 feat(order): 添加订单管理功能
- 创建新的订单管理页面 OrderManage.vue,支持分页和多条件查询
- 添加订单查询API接口,支持复杂的订单筛选条件
- 在路由中添加订单管理页面路由配置
- 更新导航菜单,将订单查询改为订单管理并指向新页面
- 实现订单列表的表格展示,包含订单号、商品信息、金额、客户信息等
- 添加订单状态、支付状态、PayPal状态的标签显示和筛选功能
- 实现查询表单,支持订单号、PayPal订单ID、客户信息等多维度搜索
- 添加分页组件,支持每页显示数量调整和页码切换
- 实现订单详情查看功能和重置查询条件功能
2025-12-25 18:12:23 +08:00
13 changed files with 1322 additions and 407 deletions

6
.env.production Normal file
View File

@@ -0,0 +1,6 @@
# 鐢熶骇鐜閰嶇疆
# 浣跨敤鐩稿璺緞锛岄€氳繃Nginx浠悊鍒板悗绔?
# API鍩虹URL锛堢浉瀵硅矾寰勶紝閫氳繃Nginx浠悊锛?# 閲嶈锛氬繀椤讳娇鐢ㄧ浉瀵硅矾寰?/api锛屼笉瑕佷娇鐢ㄥ畬鏁碪RL
VITE_API_BASE_URL=/api
# PingPong妯″紡锛坰andbox/production锛?VITE_PINGPONG_MODE=sandbox

View File

@@ -73,7 +73,7 @@ MTKJPAY-FRONT/
`src/api/request.js` 中配置后端API地址 `src/api/request.js` 中配置后端API地址
```javascript ```javascript
const service = axios.create({ const service = axios.create({
baseURL: 'http://localhost:8082/api', baseURL: '/api', // 使用相对路径通过Nginx代理
timeout: 10000 timeout: 10000
}) })
``` ```

View File

@@ -11,11 +11,11 @@
<h1>MT Pay 管理系统</h1> <h1>MT Pay 管理系统</h1>
<div class="nav-menu"> <div class="nav-menu">
<router-link <router-link
to="/query" to="/manage/order"
class="nav-item" class="nav-item"
:class="{ active: activeIndex === '/query' || activeIndex.startsWith('/query') }" :class="{ active: activeIndex === '/manage/order' || activeIndex.startsWith('/manage/order') }"
> >
订单查询 订单管理
</router-link> </router-link>
<router-link <router-link
to="/manage/product" to="/manage/product"

View File

@@ -42,3 +42,14 @@ export function calculateCurrencyConversion(data) {
}) })
} }
/**
* 查询订单列表(支持分页和多条件查询)
*/
export function queryOrders(query) {
return request({
url: '/order/query',
method: 'post',
data: query
})
}

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

@@ -17,18 +17,14 @@ app.use(router)
app.use(ElementPlus) app.use(ElementPlus)
app.use(i18n) app.use(i18n)
// 添加错误处理 // 添加错误处理(生产环境静默处理)
app.config.errorHandler = (err, instance, info) => { app.config.errorHandler = (err, instance, info) => {
console.error('Vue错误:', err) // 生产环境可以记录到错误追踪服务
console.error('错误信息:', info) if (import.meta.env.DEV) {
console.error('组件实例:', instance) console.error('Vue错误:', err, info)
}
} }
// 挂载应用 // 挂载应用
try { app.mount('#app')
app.mount('#app')
console.log('Vue应用已成功挂载')
} catch (error) {
console.error('应用挂载失败:', error)
}

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',
@@ -78,6 +80,12 @@ const routes = [
name: 'OrderQuery', name: 'OrderQuery',
component: OrderQuery component: OrderQuery
}, },
{
path: '/manage/order',
name: 'OrderManage',
component: () => import('../views/OrderManage.vue'),
meta: { requiresAuth: true }
},
{ {
path: '/manage/product', path: '/manage/product',
name: 'ProductManage', name: 'ProductManage',
@@ -105,8 +113,6 @@ const router = createRouter({
// 路由守卫 // 路由守卫
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
console.log('路由导航:', from.path, '->', to.path)
// 检查是否需要认证 // 检查是否需要认证
if (to.meta.requiresAuth) { if (to.meta.requiresAuth) {
// 检查是否已登录 // 检查是否已登录

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,20 +21,20 @@
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-sku" v-if="productInfo.sku"> <div class="product-info-details">
<span class="sku-label">SKU</span> <div class="product-info-sku" v-if="productInfo.sku">
<span class="sku-value">{{ productInfo.sku }}</span> <span class="sku-label">SKU</span>
</div> <span class="sku-value">{{ productInfo.sku }}</span>
<div class="product-info-price"> </div>
<span class="price-label">{{ $t('product.unitPrice') }}</span> <div class="product-info-price">
<span class="price-value">{{ productInfo.currency }} {{ formatPrice(productInfo.price) }}</span> <span class="price-label">{{ $t('product.unitPrice') }}</span>
<span class="quantity-label">{{ $t('product.quantity') }}</span> <span class="price-value">{{ productInfo.currency }} {{ formatPrice(productInfo.price) }}</span>
<span class="quantity-value">x{{ productInfo.quantity }}</span> <span class="quantity-label">{{ $t('product.quantity') }}</span>
<span class="total-label">{{ $t('order.subtotal') }}</span> <span class="quantity-value">x{{ productInfo.quantity }}</span>
<span class="total-value">{{ productInfo.currency }} {{ formatPrice(productInfo.price * productInfo.quantity) }}</span> <span class="total-label">{{ $t('order.subtotal') }}</span>
</div> <span class="total-value">{{ productInfo.currency }} {{ formatPrice(productInfo.price * productInfo.quantity) }}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -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-input <el-form-item :label="t('order.postcode')" prop="shippingPostcode" required>
v-model="form.shippingName" <div :class="isMobile ? 'mobile-postcode-group' : 'desktop-postcode-group'">
:placeholder="t('order.pleaseEnter') + t('order.shippingName') + '' + t('order.mustMatchId') + ''" <el-input
clearable 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>
<el-form-item :label="t('order.shippingPhone')" prop="shippingPhone"> <!-- 城市PayPal要求必填- 常驻展示必填越南不使用此字段使用独立的省//// -->
<el-input <el-form-item v-if="form.shippingCountry !== 'VN'" :label="getCityLabel()" prop="shippingCity" required>
v-model="form.shippingPhone" <div :class="isMobile ? 'mobile-input-group' : 'desktop-input-group'">
:placeholder="currentCountryConfig ? `${t('order.pleaseEnter')}${t('order.shippingPhone')}${t('order.phoneCode')}${currentCountryConfig.phoneCode}` : t('order.pleaseEnter') + t('order.shippingPhone') + '' + t('order.phoneCode') + ''" <el-input
clearable v-model="form.shippingCity"
/> :placeholder="getCityPlaceholder()"
:style="isMobile ? 'width: 100%' : form.shippingCountry === 'MY' ? 'width: 100%' : 'width: 48%'"
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-input <el-form-item :label="t('order.province')" prop="shippingProvince" required>
v-model="currentCountryDisplayName" <el-input
disabled v-model="form.shippingProvince"
style="width: 100%" :placeholder="t('order.placeholderProvinceVN')"
/> clearable
</el-form-item> @input="updateAddressLine1"
/>
<!-- 详细地址1门牌号街道楼栋- 所有国家都显示 --> </el-form-item>
<el-form-item v-if="showField('shippingAddressLine1')" :label="t('order.addressLine1')" prop="shippingAddressLine1"> <el-form-item :label="t('order.district')" prop="shippingDistrict" required>
<el-input <el-input
v-model="form.shippingAddressLine1" v-model="form.shippingDistrict"
type="textarea" :placeholder="t('order.placeholderDistrictVN')"
:rows="2" clearable
:placeholder="t('order.placeholderAddressLine1')" @input="updateAddressLine1"
clearable />
/> </el-form-item>
</el-form-item> <el-form-item :label="t('order.ward')" prop="shippingWard" required>
<el-input
<!-- 详细地址2楼层单元号可选 --> v-model="form.shippingWard"
<el-form-item v-if="showField('shippingAddressLine2')" :label="t('order.addressLine2')" prop="shippingAddressLine2"> :placeholder="t('order.placeholderWardVN')"
<el-input clearable
v-model="form.shippingAddressLine2" @input="updateAddressLine1"
:placeholder="t('order.placeholderAddressLine2')" />
clearable </el-form-item>
/> </template>
</el-form-item>
<!-- 新加坡组屋号和单元号 --> <!-- 新加坡组屋号和单元号 -->
<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,75 +193,56 @@
/> />
</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
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="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 <el-input
v-model="form.shippingFloorUnit" v-model="form.shippingStateMalaysia"
:placeholder="t('order.placeholderFloorUnit')" :placeholder="t('order.placeholderStateMalaysia')"
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-input <el-form-item>
v-model="form.remark" <el-button
type="textarea" type="text"
:rows="3" @click="showMoreAddressFields = !showMoreAddressFields"
:placeholder="t('order.placeholderRemark')" style="padding: 0; color: #409eff;"
clearable >
/> <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> </el-form-item>
<!-- 更多地址信息 - 只包含详细地址2 -->
<template v-if="showMoreAddressFields">
<!-- 详细地址2楼层单元号可选- 放入更多按钮中 -->
<el-form-item v-if="showField('shippingAddressLine2')" :label="t('order.addressLine2')" prop="shippingAddressLine2">
<el-input
v-model="form.shippingAddressLine2"
:placeholder="t('order.placeholderAddressLine2')"
clearable
/>
</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">
{{ $t('order.submit') }} {{ $t('order.submit') }}
@@ -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: ''
}) })
// 当前国家配置 // 当前国家配置
@@ -363,12 +334,13 @@ const currentCountryDisplayName = computed(() => {
return countryNameMap[lang] || countryNameMap.en || form.shippingCountry return countryNameMap[lang] || countryNameMap.en || form.shippingCountry
}) })
// 是否显示特定字段 // 是否显示特定字段
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,21 +530,30 @@ 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: [
{ required: true, message: t('order.validationRequired', [t('order.addressLine1')]), trigger: 'blur' }
], ],
shippingStreet: [ // 邮编始终必填PayPal要求
{ required: true, message: t('order.validationRequired', [t('order.street')]), trigger: 'blur' } shippingPostcode: [
{ required: true, message: t('order.validationRequired', [t('order.postcode')]), trigger: 'blur' },
{
validator: (rule, value, callback) => {
if (value && currentCountryConfig.value && !validatePostcode(form.shippingCountry, value)) {
callback(new Error(t('order.validationPostcodeFormat', [currentCountryConfig.value.postcodeLength])))
} else {
callback()
}
},
trigger: 'blur'
}
],
// 城市始终必填PayPal要求越南除外
shippingCity: [
{ required: true, message: t('order.validationRequired', [t('order.city')]), trigger: 'blur' }
] ]
} }
@@ -523,28 +561,6 @@ const getRules = () => {
if (currentCountryConfig.value) { if (currentCountryConfig.value) {
const requiredFields = getRequiredFields(form.shippingCountry) const requiredFields = getRequiredFields(form.shippingCountry)
if (requiredFields.includes('shippingAddressLine1')) {
baseRules.shippingAddressLine1 = [
{ required: true, message: t('order.validationRequired', [t('order.addressLine1')]), trigger: 'blur' }
]
}
if (requiredFields.includes('shippingPostcode')) {
baseRules.shippingPostcode = [
{ required: true, message: t('order.validationRequired', [t('order.postcode')]), trigger: 'blur' },
{
validator: (rule, value, callback) => {
if (value && !validatePostcode(form.shippingCountry, value)) {
callback(new Error(t('order.validationPostcodeFormat', [currentCountryConfig.value.postcodeLength])))
} else {
callback()
}
},
trigger: 'blur'
}
]
}
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;
} }
} }

545
src/views/OrderManage.vue Normal file
View File

@@ -0,0 +1,545 @@
<template>
<div class="order-manage">
<!-- 查询表单 -->
<el-card class="search-card" style="margin-bottom: 20px">
<el-form :model="queryForm" :inline="true" class="search-form">
<el-form-item label="订单号">
<el-input
v-model="queryForm.orderNo"
placeholder="请输入订单号"
clearable
style="width: 200px"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="PayPal订单ID">
<el-input
v-model="queryForm.paypalOrderId"
placeholder="请输入PayPal订单ID"
clearable
style="width: 200px"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="订单状态">
<el-select
v-model="queryForm.status"
placeholder="请选择状态"
clearable
style="width: 150px"
>
<el-option label="待支付" value="PENDING" />
<el-option label="已支付" value="PAID" />
<el-option label="已发货" value="SHIPPED" />
<el-option label="已完成" value="COMPLETED" />
<el-option label="已取消" value="CANCELLED" />
</el-select>
</el-form-item>
<el-form-item label="支付状态">
<el-select
v-model="queryForm.paymentStatus"
placeholder="请选择支付状态"
clearable
style="width: 150px"
>
<el-option label="未支付" value="UNPAID" />
<el-option label="已支付" value="PAID" />
<el-option label="支付失败" value="FAILED" />
</el-select>
</el-form-item>
<el-form-item label="PayPal状态">
<el-select
v-model="queryForm.paypalStatus"
placeholder="请选择PayPal状态"
clearable
style="width: 150px"
>
<el-option label="已创建" value="CREATED" />
<el-option label="已批准" value="APPROVED" />
<el-option label="已完成" value="COMPLETED" />
<el-option label="已取消" value="VOIDED" />
</el-select>
</el-form-item>
<el-form-item label="客户姓名">
<el-input
v-model="queryForm.customerName"
placeholder="请输入客户姓名"
clearable
style="width: 150px"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="客户电话">
<el-input
v-model="queryForm.customerPhone"
placeholder="请输入客户电话"
clearable
style="width: 150px"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="商品名称">
<el-input
v-model="queryForm.productName"
placeholder="请输入商品名称"
clearable
style="width: 150px"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">
<el-icon><Search /></el-icon>
查询
</el-button>
<el-button @click="handleReset">
<el-icon><Refresh /></el-icon>
重置
</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 订单列表 -->
<el-card>
<el-table :data="orderList" v-loading="loading" style="width: 100%">
<!-- 订单号 -->
<el-table-column label="订单号" prop="orderNo" width="180" show-overflow-tooltip />
<!-- 商品信息 -->
<el-table-column label="商品信息" min-width="200">
<template #default="{ row }">
<div>
<div class="product-name">{{ row.productName }}</div>
<div class="sku-name">SKU: {{ row.skuName }}</div>
<div class="quantity">数量: {{ row.quantity }}</div>
</div>
</template>
</el-table-column>
<!-- 订单金额 -->
<el-table-column label="订单金额" width="150" align="center">
<template #default="{ row }">
<div v-if="row.paymentCurrency && row.paymentCurrency !== row.originalCurrency">
<div class="original-amount">{{ row.originalAmount }} {{ row.originalCurrency }}</div>
<div class="payment-amount">{{ row.paymentAmount }} {{ row.paymentCurrency }}</div>
</div>
<div v-else>
{{ row.totalAmount }} {{ row.originalCurrency || row.currency }}
</div>
</template>
</el-table-column>
<!-- 客户信息 -->
<el-table-column label="客户信息" min-width="180">
<template #default="{ row }">
<div>
<div>{{ row.customerName }}</div>
<div class="customer-phone">{{ row.customerPhone }}</div>
<div class="customer-email" v-if="row.customerEmail">{{ row.customerEmail }}</div>
</div>
</template>
</el-table-column>
<!-- 收货地址 -->
<el-table-column label="收货地址" min-width="200" show-overflow-tooltip>
<template #default="{ row }">
<div>
{{ row.shippingCountry }} {{ row.shippingCity }}
</div>
</template>
</el-table-column>
<!-- 订单状态 -->
<el-table-column label="订单状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="getStatusTagType(row.status)" size="small">
{{ getStatusText(row.status) }}
</el-tag>
</template>
</el-table-column>
<!-- 支付状态 -->
<el-table-column label="支付状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="getPaymentStatusTagType(row.paymentStatus)" size="small">
{{ getPaymentStatusText(row.paymentStatus) }}
</el-tag>
</template>
</el-table-column>
<!-- PayPal信息 -->
<el-table-column label="PayPal信息" min-width="200">
<template #default="{ row }">
<div v-if="row.paypalOrderId">
<div class="paypal-order-id">订单ID: {{ row.paypalOrderId }}</div>
<el-tag v-if="row.paypalStatus" :type="getPaypalStatusTagType(row.paypalStatus)" size="small" style="margin-top: 4px">
{{ row.paypalStatus }}
</el-tag>
<div v-if="row.payerEmail" class="payer-email">{{ row.payerEmail }}</div>
</div>
<span v-else class="no-paypal">未关联PayPal订单</span>
</template>
</el-table-column>
<!-- 创建时间 -->
<el-table-column label="创建时间" width="180" align="center">
<template #default="{ row }">
{{ formatDateTime(row.createTime) }}
</template>
</el-table-column>
<!-- 操作 -->
<el-table-column label="操作" width="120" align="center" fixed="right">
<template #default="{ row }">
<el-button type="primary" link size="small" @click="viewOrderDetail(row.id)">
查看详情
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页组件 -->
<div class="pagination-container" v-if="pagination.total > 0">
<el-pagination
v-model:current-page="pagination.pageNum"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { Search, Refresh } from '@element-plus/icons-vue'
import { queryOrders } from '../api/order'
const router = useRouter()
const loading = ref(false)
const orderList = ref([])
// 查询表单
const queryForm = reactive({
orderNo: '',
paypalOrderId: '',
merchantOrderNo: '',
status: '',
paymentStatus: '',
paypalStatus: '',
paypalPaymentStatus: '',
customerName: '',
customerPhone: '',
customerEmail: '',
productName: '',
payerEmail: '',
payerName: '',
startTime: '',
endTime: ''
})
// 分页信息
const pagination = ref({
pageNum: 1,
pageSize: 10,
total: 0
})
// 查询订单列表
const handleQuery = async () => {
loading.value = true
try {
const query = {}
if (queryForm.orderNo && queryForm.orderNo.trim()) {
query.orderNo = queryForm.orderNo.trim()
}
if (queryForm.paypalOrderId && queryForm.paypalOrderId.trim()) {
query.paypalOrderId = queryForm.paypalOrderId.trim()
}
if (queryForm.merchantOrderNo && queryForm.merchantOrderNo.trim()) {
query.merchantOrderNo = queryForm.merchantOrderNo.trim()
}
if (queryForm.status) {
query.status = queryForm.status
}
if (queryForm.paymentStatus) {
query.paymentStatus = queryForm.paymentStatus
}
if (queryForm.paypalStatus) {
query.paypalStatus = queryForm.paypalStatus
}
if (queryForm.paypalPaymentStatus) {
query.paypalPaymentStatus = queryForm.paypalPaymentStatus
}
if (queryForm.customerName && queryForm.customerName.trim()) {
query.customerName = queryForm.customerName.trim()
}
if (queryForm.customerPhone && queryForm.customerPhone.trim()) {
query.customerPhone = queryForm.customerPhone.trim()
}
if (queryForm.customerEmail && queryForm.customerEmail.trim()) {
query.customerEmail = queryForm.customerEmail.trim()
}
if (queryForm.productName && queryForm.productName.trim()) {
query.productName = queryForm.productName.trim()
}
if (queryForm.payerEmail && queryForm.payerEmail.trim()) {
query.payerEmail = queryForm.payerEmail.trim()
}
if (queryForm.payerName && queryForm.payerName.trim()) {
query.payerName = queryForm.payerName.trim()
}
if (queryForm.startTime && queryForm.startTime.trim()) {
query.startTime = queryForm.startTime.trim()
}
if (queryForm.endTime && queryForm.endTime.trim()) {
query.endTime = queryForm.endTime.trim()
}
// 添加分页参数
query.pageNum = pagination.value.pageNum
query.pageSize = pagination.value.pageSize
const response = await queryOrders(query)
if (response.code === '0000' && response.data) {
const pageResult = response.data
orderList.value = pageResult.records || []
// 更新分页信息
pagination.value.total = pageResult.total || 0
pagination.value.pageNum = pageResult.current || 1
pagination.value.pageSize = pageResult.size || 10
if (orderList.value.length === 0) {
ElMessage.info('未找到符合条件的订单')
} else {
ElMessage.success(`查询到 ${pagination.value.total} 条订单,当前第 ${pagination.value.pageNum}`)
}
} else {
ElMessage.error(response.message || '查询订单列表失败')
orderList.value = []
pagination.value.total = 0
}
} catch (error) {
console.error('查询订单列表失败:', error)
ElMessage.error('查询订单列表失败')
orderList.value = []
pagination.value.total = 0
} finally {
loading.value = false
}
}
// 重置查询条件
const handleReset = () => {
Object.assign(queryForm, {
orderNo: '',
paypalOrderId: '',
merchantOrderNo: '',
status: '',
paymentStatus: '',
paypalStatus: '',
paypalPaymentStatus: '',
customerName: '',
customerPhone: '',
customerEmail: '',
productName: '',
payerEmail: '',
payerName: '',
startTime: '',
endTime: ''
})
pagination.value = {
pageNum: 1,
pageSize: 10,
total: 0
}
handleQuery()
}
// 分页大小改变
const handleSizeChange = (size) => {
pagination.value.pageSize = size
pagination.value.pageNum = 1 // 重置到第一页
handleQuery()
}
// 页码改变
const handlePageChange = (page) => {
pagination.value.pageNum = page
handleQuery()
}
// 查看订单详情
const viewOrderDetail = (orderId) => {
router.push(`/order/detail/${orderId}`)
}
// 获取订单状态文本
const getStatusText = (status) => {
const statusMap = {
'PENDING': '待支付',
'PAID': '已支付',
'SHIPPED': '已发货',
'COMPLETED': '已完成',
'CANCELLED': '已取消'
}
return statusMap[status] || status
}
// 获取订单状态标签类型
const getStatusTagType = (status) => {
const typeMap = {
'PENDING': 'warning',
'PAID': 'success',
'SHIPPED': 'info',
'COMPLETED': 'success',
'CANCELLED': 'danger'
}
return typeMap[status] || ''
}
// 获取支付状态文本
const getPaymentStatusText = (status) => {
const statusMap = {
'UNPAID': '未支付',
'PAID': '已支付',
'FAILED': '支付失败'
}
return statusMap[status] || status
}
// 获取支付状态标签类型
const getPaymentStatusTagType = (status) => {
const typeMap = {
'UNPAID': 'warning',
'PAID': 'success',
'FAILED': 'danger'
}
return typeMap[status] || ''
}
// 获取PayPal状态标签类型
const getPaypalStatusTagType = (status) => {
const typeMap = {
'CREATED': 'info',
'APPROVED': 'success',
'COMPLETED': 'success',
'VOIDED': 'danger'
}
return typeMap[status] || ''
}
// 格式化日期时间
const formatDateTime = (dateTime) => {
if (!dateTime) return ''
const date = new Date(dateTime)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
// 组件挂载时加载订单列表
onMounted(() => {
handleQuery()
})
</script>
<style scoped>
.order-manage {
padding: 20px;
}
.search-card {
margin-bottom: 20px;
}
.search-form {
margin-bottom: 0;
}
.product-name {
font-weight: bold;
margin-bottom: 4px;
}
.sku-name {
color: #909399;
font-size: 12px;
margin-bottom: 2px;
}
.quantity {
color: #909399;
font-size: 12px;
}
.original-amount {
color: #909399;
font-size: 12px;
text-decoration: line-through;
}
.payment-amount {
color: #f56c6c;
font-weight: bold;
}
.customer-phone {
color: #909399;
font-size: 12px;
margin-top: 2px;
}
.customer-email {
color: #909399;
font-size: 12px;
margin-top: 2px;
}
.paypal-order-id {
font-size: 12px;
color: #606266;
margin-bottom: 4px;
}
.payer-email {
font-size: 12px;
color: #909399;
margin-top: 4px;
}
.no-paypal {
color: #c0c4cc;
font-size: 12px;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
padding: 20px 0;
}
@media (max-width: 768px) {
.pagination-container {
justify-content: center;
}
.pagination-container :deep(.el-pagination) {
flex-wrap: wrap;
}
}
</style>

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

@@ -12,11 +12,22 @@ export default defineConfig({
'@': path.resolve(__dirname, 'src') '@': path.resolve(__dirname, 'src')
} }
}, },
// 构建配置
build: {
outDir: 'dist',
assetsDir: 'assets',
// 确保资源路径正确
assetsInlineLimit: 4096,
// 生成source map生产环境可以关闭
sourcemap: false
},
// 基础路径(如果部署在子目录下需要配置)
base: '/',
server: { server: {
port: 3000, port: 3000,
proxy: { proxy: {
'/api': { '/api': {
target: 'http://127.0.0.1:8082', // 使用 127.0.0.1 而不是 localhost避免 IPv6 问题 target: 'http://127.0.0.1:8082',
changeOrigin: true, changeOrigin: true,
secure: false, secure: false,
ws: true, ws: true,