Compare commits

..

8 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
b2bbbf8c44 feat(product): 添加商品管理分页功能并支持完整链接查询
- 添加分页组件支持,包括页码、页面大小切换功能
- 实现从完整URL中自动提取链接码的功能
- 更新查询表单提示文本为"请输入链接码或完整链接"
- 添加分页数据响应处理,显示总记录数和当前页码信息
- 增加分页相关的CSS样式适配移动端显示
- 重置查询时同步重置分页参数到默认值
2025-12-25 17:11:48 +08:00
01bda65010 feat(product): 添加商品下架和多条件查询功能
- 实现了商品下架功能,下架后SKU库存变为0且链接失效
- 添加了支持多条件查询的商品列表功能
- 增加了包含商品名称、链接码、状态、发售地区等查询条件
- 在商品管理页面添加了查询表单界面
- 实现了下架按钮的禁用逻辑和状态显示
- 添加了移动端响应式查询表单适配
- 集成了API接口和错误处理机制
2025-12-25 16:26:11 +08:00
15 changed files with 1655 additions and 426 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地址
```javascript
const service = axios.create({
baseURL: 'http://localhost:8082/api',
baseURL: '/api', // 使用相对路径通过Nginx代理
timeout: 10000
})
```

View File

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

@@ -65,3 +65,30 @@ export function uploadProductImage(file) {
// 注意:不设置 Content-Type让浏览器自动设置包含 boundary
})
}
/**
* 下架商品
* 下架后商品所有SKU库存改为0链接失效无法再被访问
*/
export function offShelfProductById(id) {
return request({
url: `/product/${id}/off-shelf`,
method: 'put'
})
}
/**
* 查询商品列表(支持多条件查询)
* @param {Object} query - 查询条件
* @param {string} query.name - 商品名称(模糊查询)
* @param {string} query.linkCode - 商品链接码(精确查询)
* @param {string} query.status - 商品状态ACTIVE-上架INACTIVE-下架)
* @param {string} query.salesRegion - 发售地区货币代码MYR, PHP, THB, VND, SGD, CNY, USD等
*/
export function queryProducts(query) {
return request({
url: '/product/query',
method: 'post',
data: query
})
}

View File

@@ -30,6 +30,8 @@ const zh = {
pleaseEnter: '请输入',
optional: '可选',
required: '必填',
more: '更多',
collapse: '收起',
addressFormat: '地址格式',
phoneCode: '国际区号',
mustMatchId: '需与证件一致,支持当地语言+英文',
@@ -234,6 +236,8 @@ const en = {
pleaseEnter: 'Please enter',
optional: 'Optional',
required: 'Required',
more: 'More',
collapse: 'Collapse',
addressFormat: 'Address Format',
phoneCode: 'Phone Code',
mustMatchId: 'Must match ID, supports local language + English',
@@ -436,6 +440,8 @@ const may = {
pleaseEnter: 'Sila masukkan',
optional: 'Pilihan',
required: 'Diperlukan',
more: 'Lebih Banyak',
collapse: 'Tutup',
addressFormat: 'Format Alamat',
phoneCode: 'Kod Telefon',
mustMatchId: 'Mesti sepadan dengan ID, menyokong bahasa tempatan + Inggeris',
@@ -633,9 +639,14 @@ const fil = {
pleaseEnter: 'Mangyaring ipasok',
optional: 'Opsiyonal',
required: 'Kinakailangan',
more: 'Higit Pa',
collapse: 'Itago',
addressFormat: 'Format ng Address',
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: {
selectCurrency: 'Pumili ng Currency at Wika',
@@ -779,6 +790,8 @@ const th = {
pleaseEnter: 'กรุณากรอก',
optional: 'ไม่บังคับ',
required: 'จำเป็น',
more: 'เพิ่มเติม',
collapse: 'ย่อ',
addressFormat: 'รูปแบบที่อยู่',
phoneCode: 'รหัสโทรศัพท์',
mustMatchId: 'ต้องตรงกับบัตรประชาชน รองรับภาษาท้องถิ่น + อังกฤษ',
@@ -979,6 +992,8 @@ const vie = {
pleaseEnter: 'Vui lòng nhập',
optional: 'Tùy chọn',
required: 'Bắt buộc',
more: 'Thêm',
collapse: 'Thu gọn',
addressFormat: 'Định Dạng Địa Chỉ',
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',
@@ -1176,9 +1191,14 @@ const id = {
pleaseEnter: 'Silakan masukkan',
optional: 'Opsional',
required: 'Diperlukan',
more: 'Lebih Banyak',
collapse: 'Tutup',
addressFormat: 'Format Alamat',
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: {
selectCurrency: 'Pilih Mata Uang dan Bahasa',

View File

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

View File

@@ -44,12 +44,14 @@ const routes = [
{
path: '/create-order',
name: 'CreateOrder',
component: CreateOrder
component: CreateOrder,
meta: { isCustomerPage: true } // 标记为客户页面,不显示导航栏
},
{
path: '/order/confirm',
name: 'OrderConfirm',
component: () => import('../views/OrderConfirm.vue')
component: () => import('../views/OrderConfirm.vue'),
meta: { isCustomerPage: true } // 标记为客户页面,不显示导航栏
},
{
path: '/paypal/success',
@@ -78,6 +80,12 @@ const routes = [
name: 'OrderQuery',
component: OrderQuery
},
{
path: '/manage/order',
name: 'OrderManage',
component: () => import('../views/OrderManage.vue'),
meta: { requiresAuth: true }
},
{
path: '/manage/product',
name: 'ProductManage',
@@ -105,8 +113,6 @@ const router = createRouter({
// 路由守卫
router.beforeEach((to, from, next) => {
console.log('路由导航:', from.path, '->', to.path)
// 检查是否需要认证
if (to.meta.requiresAuth) {
// 检查是否已登录

View File

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

View File

@@ -21,8 +21,9 @@
fit="cover"
class="product-info-image"
/>
<div class="product-info-details">
<div class="product-info-name">{{ productInfo.name }}</div>
</div>
<div class="product-info-details">
<div class="product-info-sku" v-if="productInfo.sku">
<span class="sku-label">SKU</span>
<span class="sku-value">{{ productInfo.sku }}</span>
@@ -37,7 +38,6 @@
</div>
</div>
</div>
</div>
</el-card>
<el-form
@@ -76,50 +76,74 @@
<el-divider>{{ $t('order.shippingAddress') }}</el-divider>
<el-alert v-if="currentCountryConfig" :title="`${$t('order.addressFormat')}${currentCountryConfig.addressFormat}`" type="info" :closable="false" style="margin-bottom: 20px" />
<el-form-item :label="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
v-model="form.shippingName"
:placeholder="t('order.pleaseEnter') + t('order.shippingName') + '' + t('order.mustMatchId') + ''"
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.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
v-model="form.shippingPhone"
:placeholder="currentCountryConfig ? `${t('order.pleaseEnter')}${t('order.shippingPhone')}${t('order.phoneCode')}${currentCountryConfig.phoneCode}` : t('order.pleaseEnter') + t('order.shippingPhone') + '' + t('order.phoneCode') + ''"
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 :label="t('order.shippingCountry')" prop="shippingCountry">
<!-- 越南//PayPal要求必填- 常驻展示必填 -->
<template v-if="form.shippingCountry === 'VN'">
<el-form-item :label="t('order.province')" prop="shippingProvince" required>
<el-input
v-model="currentCountryDisplayName"
disabled
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')"
v-model="form.shippingProvince"
:placeholder="t('order.placeholderProvinceVN')"
clearable
@input="updateAddressLine1"
/>
</el-form-item>
<!-- 详细地址2楼层单元号可选 -->
<el-form-item v-if="showField('shippingAddressLine2')" :label="t('order.addressLine2')" prop="shippingAddressLine2">
<el-form-item :label="t('order.district')" prop="shippingDistrict" required>
<el-input
v-model="form.shippingAddressLine2"
:placeholder="t('order.placeholderAddressLine2')"
v-model="form.shippingDistrict"
:placeholder="t('order.placeholderDistrictVN')"
clearable
@input="updateAddressLine1"
/>
</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'">
@@ -159,39 +183,6 @@
/>
</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 -->
<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 v-if="form.shippingCountry !== 'VN'" :label="getCityLabel()" prop="shippingCity">
<div :class="isMobile ? 'mobile-input-group' : 'desktop-input-group'">
<!-- 马来西亚州属 - 常驻展示 -->
<el-form-item v-if="form.shippingCountry === 'MY'" :label="t('order.stateMalaysia')" prop="shippingStateMalaysia">
<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
v-model="form.shippingFloorUnit"
:placeholder="t('order.placeholderFloorUnit')"
v-model="form.shippingStateMalaysia"
:placeholder="t('order.placeholderStateMalaysia')"
clearable
@input="updateAddressLine1"
/>
</el-form-item>
<!-- 兼容旧字段街道地址如果新字段为空使用旧字段 -->
<el-form-item v-if="!form.shippingAddressLine1" :label="t('order.street')" prop="shippingStreet">
<!-- 详细地址1门牌号街道楼栋- 所有国家都显示必填放在最下面 -->
<el-form-item v-if="showField('shippingAddressLine1')" :label="t('order.addressLine1')" prop="shippingAddressLine1" required>
<el-input
v-model="form.shippingStreet"
v-model="form.shippingAddressLine1"
type="textarea"
:rows="2"
:placeholder="t('order.placeholderStreet')"
:placeholder="t('order.placeholderAddressLine1')"
clearable
/>
</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
v-model="form.remark"
type="textarea"
:rows="3"
:placeholder="t('order.placeholderRemark')"
v-model="form.shippingAddressLine2"
:placeholder="t('order.placeholderAddressLine2')"
clearable
/>
</el-form-item>
</template>
<el-form-item>
<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 { useI18n } from 'vue-i18n'
import { ElMessage } from 'element-plus'
import { ArrowDown, ArrowUp } from '@element-plus/icons-vue'
import { createCustomerOrder } from '../api/order'
import { formatAmount } from '../utils/helpers'
import { getCountryConfig, getCountryByCurrency, validatePostcode, getRequiredFields } from '../utils/countryConfig'
@@ -301,13 +274,13 @@ const formRef = ref()
const loading = ref(false)
const productInfo = ref(null)
const postcodeMatching = ref(false) // 邮编匹配中
const showMoreAddressFields = ref(false) // 控制更多地址字段的显示
const isAutoUpdating = ref(false) // 防止自动更新时的递归调用
const form = reactive({
customerName: '',
customerPhone: '',
customerEmail: '',
shippingName: '',
shippingPhone: '',
shippingCountry: '',
shippingState: '',
shippingCity: '',
@@ -324,9 +297,7 @@ const form = reactive({
shippingProvince: '',
shippingDistrict: '',
shippingWard: '',
shippingStateMalaysia: '',
shippingFloorUnit: '',
remark: ''
shippingStateMalaysia: ''
})
// 当前国家配置
@@ -366,9 +337,10 @@ const currentCountryDisplayName = computed(() => {
// 是否显示特定字段
const showField = (fieldName) => {
if (!currentCountryConfig.value) {
// 默认显示基础字段
return ['shippingName', 'shippingPhone', 'shippingCountry', 'shippingCity',
'shippingState', 'shippingStreet', 'shippingPostcode'].includes(fieldName)
// 默认显示基础字段(包括详细地址)
return ['shippingCountry', 'shippingCity',
'shippingState', 'shippingPostcode',
'shippingAddressLine1', 'shippingAddressLine2'].includes(fieldName)
}
const specialFields = currentCountryConfig.value.specialFields || []
@@ -385,8 +357,8 @@ const showField = (fieldName) => {
}
// 基础字段始终显示
const baseFields = ['shippingName', 'shippingPhone', 'shippingCountry',
'shippingCity', 'shippingState', 'shippingStreet',
const baseFields = ['shippingCountry',
'shippingCity', 'shippingState',
'shippingPostcode', 'shippingAddressLine1', 'shippingAddressLine2']
if (baseFields.includes(fieldName)) {
return true
@@ -408,17 +380,9 @@ const getFieldLabel = (fieldName) => {
return fieldName
}
// 获取城市字段标签(根据国家动态显示)
// 获取城市字段标签(根据国家动态显示,只显示对应语言,不包含中文
const getCityLabel = () => {
if (form.shippingCountry === 'TH') {
return t('order.cityTown') + ' (县/Amphoe)'
} else if (form.shippingCountry === 'PH') {
return t('order.cityTown') + ' (市/City)'
} else if (form.shippingCountry === 'SG') {
return t('order.cityTown') + ' (城市/City)'
} else if (form.shippingCountry === 'MY') {
return t('order.cityTown') + ' (城市/City)'
}
// 直接返回国际化翻译i18n会根据当前语言环境自动返回对应语言的标签
return t('order.cityTown')
}
@@ -440,6 +404,66 @@ const getStatePlaceholder = () => {
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) => {
if (newCountry !== oldCountry) {
@@ -453,14 +477,19 @@ watch(() => form.shippingCountry, (newCountry, oldCountry) => {
form.shippingDistrict = ''
form.shippingWard = ''
form.shippingAdministrativeArea = ''
// 更新电话区号提示
if (currentCountryConfig.value) {
form.shippingPhone = currentCountryConfig.value.phoneCode + ' '
}
// 清空详细地址1因为地址组件已清空
form.shippingAddressLine1 = ''
}
})
// 监听地址组件变化自动更新详细地址1
watch([() => form.shippingCity, () => form.shippingState, () => form.shippingStateMalaysia,
() => form.shippingProvince, () => form.shippingDistrict, () => form.shippingWard],
() => {
updateAddressLine1()
}
)
// 监听邮编变化,自动匹配城市
watch(() => form.shippingPostcode, async (newPostcode) => {
if (!newPostcode || !form.shippingCountry) return
@@ -501,40 +530,19 @@ const getRules = () => {
customerEmail: [
{ 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: [
{ required: true, message: t('order.validationSelectCountry'), trigger: 'change' }
],
shippingCity: [
{ required: true, message: t('order.validationRequired', [t('order.cityTown')]), trigger: 'blur' }
],
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 = [
// 详细地址1始终必填
shippingAddressLine1: [
{ required: true, message: t('order.validationRequired', [t('order.addressLine1')]), trigger: 'blur' }
]
}
if (requiredFields.includes('shippingPostcode')) {
baseRules.shippingPostcode = [
],
// 邮编始终必填PayPal要求
shippingPostcode: [
{ required: true, message: t('order.validationRequired', [t('order.postcode')]), trigger: 'blur' },
{
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])))
} else {
callback()
@@ -542,9 +550,17 @@ const getRules = () => {
},
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')) {
baseRules.shippingBlockNumber = [
{ required: true, message: t('order.validationRequired', [t('order.blockNumber')]), trigger: 'blur' }
@@ -639,12 +655,12 @@ const submitForm = async () => {
customerName: form.customerName,
customerPhone: form.customerPhone,
customerEmail: form.customerEmail || null,
shippingName: form.shippingName,
shippingPhone: form.shippingPhone,
shippingName: form.customerName, // 使用客户姓名作为收货人姓名
shippingPhone: form.customerPhone, // 使用客户电话作为收货人电话
shippingCountry: form.shippingCountry,
shippingState: form.shippingState || null,
shippingCity: shippingCity, // 使用映射后的值,越南使用 shippingDistrict
shippingStreet: form.shippingStreet || form.shippingAddressLine1, // 兼容旧字段
shippingStreet: form.shippingAddressLine1, // 使用详细地址1
shippingPostcode: form.shippingPostcode || null,
// 东南亚地址扩展字段
shippingAddressLine1: form.shippingAddressLine1 || null,
@@ -658,8 +674,6 @@ const submitForm = async () => {
shippingDistrict: form.shippingDistrict || null,
shippingWard: form.shippingWard || null,
shippingStateMalaysia: form.shippingStateMalaysia || null,
shippingFloorUnit: form.shippingFloorUnit || null,
remark: form.remark || null
}
const response = await createCustomerOrder(orderData)
@@ -703,10 +717,6 @@ onMounted(async () => {
const countryCode = getCountryByCurrency(data.product.currency)
if (countryCode) {
form.shippingCountry = countryCode
const config = getCountryConfig(countryCode)
if (config) {
form.shippingPhone = config.phoneCode + ' '
}
}
}
}
@@ -755,22 +765,18 @@ onMounted(async () => {
.product-info-main {
display: flex;
gap: 20px;
align-items: center;
gap: 15px;
margin-bottom: 15px;
}
.product-info-image {
width: 120px;
height: 120px;
border-radius: 8px;
width: 80px;
height: 80px;
border-radius: 6px;
border: 1px solid #e4e7ed;
flex-shrink: 0;
}
.product-info-details {
flex: 1;
display: flex;
flex-direction: column;
gap: 10px;
object-fit: cover;
}
.product-info-name {
@@ -778,6 +784,13 @@ onMounted(async () => {
font-weight: 600;
color: #303133;
line-height: 1.5;
flex: 1;
}
.product-info-details {
display: flex;
flex-direction: column;
gap: 10px;
}
.product-info-sku {
@@ -830,39 +843,74 @@ onMounted(async () => {
font-weight: 700;
}
/* 移动端优化 */
/* 移动端优先设计 - 响应式优化 */
@media (max-width: 768px) {
.create-order {
padding: 10px;
padding: 0;
max-width: 100%;
}
.el-card {
border-radius: 0;
box-shadow: none;
border: none;
margin: 0;
}
.card-header {
font-size: 16px;
padding: 10px 0;
padding: 12px 16px;
font-weight: 600;
}
.el-card__body {
padding: 16px;
}
/* 商品信息卡片优化 */
.product-info-card {
margin-bottom: 15px;
margin-bottom: 16px;
border: 1px solid #e4e7ed;
border-radius: 8px;
}
.product-card-header {
font-size: 14px;
font-weight: 600;
}
.product-info-content {
padding: 12px 0;
}
.product-info-main {
flex-direction: column;
flex-direction: row;
gap: 12px;
align-items: flex-start;
margin-bottom: 12px;
}
.product-info-image {
width: 100%;
height: 200px;
align-self: center;
width: 60px;
height: 60px;
flex-shrink: 0;
border-radius: 6px;
}
.product-info-name {
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 {
@@ -870,33 +918,58 @@ onMounted(async () => {
align-items: flex-start;
gap: 8px;
padding: 10px;
font-size: 13px;
}
.price-label,
.quantity-label {
font-size: 12px;
}
.price-value,
.quantity-value {
font-size: 14px;
}
.total-label {
margin-left: 0;
margin-top: 8px;
font-size: 16px;
font-size: 14px;
font-weight: 600;
}
.total-value {
font-size: 20px;
font-size: 18px;
}
/* 表单优化 */
.el-form {
padding: 0;
}
.el-form-item {
margin-bottom: 18px;
margin-bottom: 16px;
}
.el-form-item__label {
padding-bottom: 5px;
padding-bottom: 6px;
font-size: 14px;
font-weight: 600;
line-height: 1.4;
margin-bottom: 4px;
}
.el-input,
.el-select,
.el-textarea {
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%;
}
/* 按钮优化 */
.el-form-item:last-child {
margin-bottom: 0;
padding-top: 15px;
border-top: 1px solid #e4e7ed;
}
.el-form-item:last-child .el-button {
width: 100%;
margin: 0;
height: 44px;
font-size: 16px;
}
.el-form-item:last-child .el-button + .el-button {
margin-top: 10px;
margin-left: 0;
}
/* 分隔线优化 */
.el-divider {
margin: 20px 0;
margin: 16px 0;
}
.el-divider__text {
font-size: 14px;
padding: 0 15px;
padding: 0 12px;
font-weight: 600;
}
/* 提示信息优化 */
.el-alert {
margin-bottom: 15px;
margin-bottom: 12px;
font-size: 12px;
padding: 10px 12px;
}
/* 输入组优化 */
@@ -951,23 +1007,73 @@ onMounted(async () => {
display: flex;
flex-direction: column;
width: 100%;
gap: 10px;
}
.desktop-input-group {
display: flex;
width: 100%;
gap: 4%;
}
.mobile-postcode-group {
display: flex;
flex-direction: column;
width: 100%;
gap: 8px;
}
.desktop-postcode-group {
display: flex;
align-items: center;
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>

View File

@@ -152,7 +152,7 @@
:style="isMobile ? 'width: 100%' : 'width: 200px'"
>
<el-icon><Money /></el-icon>
{{ $t('confirm.payNow') }}
{{ countdown > 0 ? `${$t('confirm.payNow')} (${countdown}s)` : $t('confirm.payNow') }}
</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>
@@ -168,7 +168,7 @@
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { ElMessage } from 'element-plus'
@@ -185,6 +185,9 @@ const route = useRoute()
const orderLoading = ref(true)
const payLoading = ref(false)
const order = ref(null)
const autoPayTimer = ref(null) // 自动支付定时器
const countdownTimer = ref(null) // 倒计时定时器
const countdown = ref(0) // 倒计时(秒)
// 移动端检测
const isMobile = computed(() => {
@@ -363,6 +366,31 @@ const loadOrder = async () => {
order.value = null
} finally {
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
}
// 清除自动支付定时器和倒计时(如果存在)
if (autoPayTimer.value) {
clearTimeout(autoPayTimer.value)
autoPayTimer.value = null
}
if (countdownTimer.value) {
clearInterval(countdownTimer.value)
countdownTimer.value = null
}
countdown.value = 0
payLoading.value = true
try {
@@ -454,6 +493,18 @@ const handlePay = async () => {
onMounted(() => {
loadOrder()
})
// 组件卸载时清理定时器
onUnmounted(() => {
if (autoPayTimer.value) {
clearTimeout(autoPayTimer.value)
autoPayTimer.value = null
}
if (countdownTimer.value) {
clearInterval(countdownTimer.value)
countdownTimer.value = null
}
})
</script>
<style scoped>
@@ -628,18 +679,31 @@ onMounted(() => {
border-top: 1px solid #ebeef5;
}
/* 移动端优化 */
/* 移动端优先设计 - 响应式优化 */
@media (max-width: 768px) {
.order-confirm {
padding: 10px;
padding: 0;
max-width: 100%;
}
.el-card {
border-radius: 0;
box-shadow: none;
border: none;
margin: 0;
}
.el-card__body {
padding: 16px;
}
.card-header {
flex-direction: column;
align-items: flex-start;
gap: 10px;
gap: 12px;
font-size: 16px;
font-weight: 600;
padding: 12px 16px;
}
.order-info-section {
@@ -647,22 +711,47 @@ onMounted(() => {
}
.order-info-section h3 {
font-size: 14px;
font-size: 15px;
margin-bottom: 12px;
padding-bottom: 8px;
font-weight: 600;
border-bottom: 2px solid #409eff;
}
.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 {
font-size: 14px;
margin-bottom: 10px;
flex-direction: row;
align-items: center;
gap: 6px;
}
.title-text {
font-size: 14px;
font-weight: 600;
}
.warning-icon {
@@ -670,35 +759,53 @@ onMounted(() => {
}
.conversion-main-info {
padding: 12px;
padding: 12px 0;
}
.payment-amount-highlight {
flex-direction: column;
align-items: flex-start;
gap: 6px;
margin-bottom: 8px;
}
.payment-amount-large {
font-size: 20px;
font-size: 22px;
font-weight: 700;
}
.exchange-rate-info {
flex-direction: column;
gap: 6px;
margin-top: 8px;
font-size: 12px;
}
.rate-locked-info {
margin-top: 8px;
font-size: 12px;
display: flex;
align-items: center;
gap: 4px;
}
.action-buttons {
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 {
width: 100%;
margin: 0;
height: 44px;
height: 48px;
font-size: 16px;
font-weight: 600;
border-radius: 8px;
}
.action-buttons .el-button + .el-button {
@@ -710,26 +817,49 @@ onMounted(() => {
font-size: 13px;
}
.el-descriptions--border {
border: 1px solid #e4e7ed;
border-radius: 8px;
overflow: hidden;
}
.el-descriptions-item {
padding: 12px;
}
.el-descriptions-item__label {
font-size: 13px;
font-weight: 600;
width: 100px;
padding-right: 12px;
color: #606266;
}
.el-descriptions-item__content {
font-size: 13px;
color: #303133;
word-break: break-word;
}
.shipping-address-detail {
font-size: 13px;
line-height: 1.6;
padding: 8px 0;
}
.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 {
font-size: 12px;
font-weight: 600;
color: #606266;
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>
</div>
<!-- 购买确认对话框 -->
<el-dialog
v-model="showConfirmDialog"
:title="t('product.confirmPurchaseInfo')"
:width="isMobile ? '95%' : '600px'"
:close-on-click-modal="false"
class="confirm-dialog"
>
<div class="confirm-dialog-content">
<!-- 商品信息 -->
<div class="confirm-product-info">
<el-image
:src="currentImage"
fit="cover"
class="confirm-product-image"
/>
<div class="confirm-product-details">
<div class="confirm-product-name">{{ product.name }}</div>
<div class="confirm-sku-info" v-if="selectedSku">
<span class="confirm-sku-label">{{ t('product.sku') }}</span>
<span class="confirm-sku-value">{{ selectedSku.sku }}</span>
</div>
</div>
</div>
<!-- 价格和数量 -->
<el-divider />
<div class="confirm-price-section">
<div class="confirm-price-item">
<span class="confirm-price-label">{{ t('product.unitPrice') }}</span>
<span class="confirm-price-value">
{{ selectedSku ? selectedSku.currency : '' }} {{ formatPrice(selectedSku ? selectedSku.price : 0) }}
</span>
</div>
<div class="confirm-price-item">
<span class="confirm-price-label">{{ t('product.quantity') }}</span>
<span class="confirm-price-value">{{ quantity }}</span>
</div>
<div class="confirm-price-item confirm-total">
<span class="confirm-price-label">{{ t('product.total') }}</span>
<span class="confirm-price-value confirm-total-price">
{{ selectedSku ? selectedSku.currency : '' }} {{ formatPrice(selectedSku ? (selectedSku.price * quantity) : 0) }}
</span>
</div>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="showConfirmDialog = false">{{ t('product.cancel') }}</el-button>
<el-button type="primary" @click="confirmBuy" :loading="loading">
{{ t('product.confirmPurchase') }}
</el-button>
</div>
</template>
</el-dialog>
</template>
</div>
</template>
@@ -281,7 +225,6 @@ const hoveredImage = ref(null)
const quantity = ref(1)
const activeTab = ref('detail')
const selectedSku = ref(null)
const showConfirmDialog = ref(false)
// 移动端检测
const isMobile = computed(() => {
@@ -413,7 +356,7 @@ const hoverImage = (img) => {
hoveredImage.value = img
}
// 立即购买
// 立即购买(直接跳转,不再显示确认弹窗)
const handleBuyNow = () => {
if (!selectedSku.value) {
ElMessage.warning('请先选择商品SKU')
@@ -425,18 +368,6 @@ const handleBuyNow = () => {
return
}
showConfirmDialog.value = true
}
// 确认购买
const confirmBuy = () => {
if (!selectedSku.value) {
ElMessage.warning('请先选择商品SKU')
return
}
showConfirmDialog.value = false
// 构建订单数据
const orderData = {
product: {
@@ -1209,97 +1140,245 @@ onMounted(() => {
line-height: 1.6;
}
/* 响应式设计 */
/* 移动端优先设计 - 响应式设计 */
@media (max-width: 768px) {
.product-detail {
padding: 10px;
padding: 0;
max-width: 100%;
}
.product-main {
flex-direction: column;
padding: 15px;
gap: 20px;
padding: 12px;
gap: 16px;
margin: 0;
border-radius: 0;
box-shadow: none;
}
.product-images {
flex: 1;
width: 100%;
margin-bottom: 0;
}
.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 {
padding-left: 0;
}
.product-title {
font-size: 20px;
}
.sku-list {
max-height: 300px;
}
.action-buttons {
flex-direction: column;
}
.buy-now-btn,
.pay-now-btn {
width: 100%;
}
.service-section {
flex-direction: column;
gap: 15px;
.product-title {
font-size: 18px;
margin-bottom: 16px;
line-height: 1.4;
word-break: break-word;
}
.currency-selector-card {
padding: 10px 12px;
margin-top: 10px;
margin-bottom: 15px;
padding: 12px;
margin-top: 0;
margin-bottom: 16px;
border-radius: 8px;
}
.currency-selector-header {
margin-bottom: 8px;
margin-bottom: 10px;
}
.currency-icon {
font-size: 16px;
margin-right: 5px;
font-size: 18px;
margin-right: 6px;
}
.currency-selector-header .selector-label {
font-size: 13px;
font-size: 14px;
}
.currency-selector-content {
padding: 8px;
padding: 0;
}
.currency-radio-group {
display: flex;
flex-wrap: nowrap;
gap: 4px;
flex-wrap: wrap;
gap: 6px;
width: 100%;
}
.currency-radio-button {
flex: 1;
min-width: 0;
min-width: calc(50% - 3px);
}
.currency-radio-button :deep(.el-radio-button__inner) {
font-size: 11px;
padding: 6px 4px;
font-size: 12px;
padding: 8px 6px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
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 {
flex-direction: column;

View File

@@ -17,6 +17,69 @@
</div>
</template>
<!-- 查询表单 -->
<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.name"
placeholder="请输入商品名称"
clearable
style="width: 200px"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="链接码">
<el-input
v-model="queryForm.linkCode"
placeholder="请输入链接码或完整链接"
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="ACTIVE" />
<el-option label="下架" value="INACTIVE" />
</el-select>
</el-form-item>
<el-form-item label="发售地区">
<el-select
v-model="queryForm.salesRegion"
placeholder="请选择地区"
clearable
style="width: 150px"
>
<el-option label="马来西亚" value="MYR" />
<el-option label="菲律宾" value="PHP" />
<el-option label="泰国" value="THB" />
<el-option label="越南" value="VND" />
<el-option label="新加坡" value="SGD" />
<el-option label="中国" value="CNY" />
<el-option label="美国" value="USD" />
<el-option label="欧洲" value="EUR" />
<el-option label="英国" value="GBP" />
</el-select>
</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-table :data="productList" v-loading="loading" style="width: 100%">
<!-- 商品封面图 -->
@@ -115,12 +178,31 @@
<el-button type="success" link size="small" @click="copyProductUrl(row.id, row.productUrl)">
复制
</el-button>
<el-button type="danger" link size="small" @click="deleteProduct(row.id)">
删除
<el-button
type="danger"
link
size="small"
@click="offShelfProduct(row.id)"
:disabled="row.status === 'INACTIVE'"
>
{{ row.status === 'INACTIVE' ? '已下架' : '下架' }}
</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>
@@ -130,14 +212,31 @@
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, User } from '@element-plus/icons-vue'
import { getProductList, getProductUrl } from '../api/product'
import { Plus, User, Search, Refresh } from '@element-plus/icons-vue'
import { getProductList, getProductUrl, offShelfProductById, queryProducts } from '../api/product'
import { formatAmount } from '../utils/helpers'
const router = useRouter()
const loading = ref(false)
const productList = ref([])
// 查询表单
const queryForm = ref({
name: '',
linkCode: '',
status: '',
salesRegion: '',
pageNum: 1,
pageSize: 10
})
// 分页信息
const pagination = ref({
pageNum: 1,
pageSize: 10,
total: 0
})
// 跳转到新增商品页面
const goToCreate = () => {
router.push('/manage/product/create')
@@ -191,7 +290,56 @@ const getSalesRegions = (row) => {
return regions
}
// 加载商品列表
// 从完整URL中提取链接码
const extractLinkCodeFromUrl = (input) => {
if (!input || !input.trim()) {
return input
}
const trimmed = input.trim()
// 如果包含 "/product/"说明是完整URL提取链接码
const productIndex = trimmed.indexOf('/product/')
if (productIndex >= 0) {
let linkCode = trimmed.substring(productIndex + '/product/'.length)
// 移除可能的查询参数和锚点
const queryIndex = linkCode.indexOf('?')
if (queryIndex >= 0) {
linkCode = linkCode.substring(0, queryIndex)
}
const hashIndex = linkCode.indexOf('#')
if (hashIndex >= 0) {
linkCode = linkCode.substring(0, hashIndex)
}
// 移除尾部斜杠
linkCode = linkCode.replace(/\/$/, '')
return linkCode.trim()
}
// 如果不包含 "/product/",可能是直接输入的链接码,直接返回
// 但也可能是其他格式的URL尝试提取最后一段作为链接码
if (trimmed.includes('/')) {
const parts = trimmed.split('/')
if (parts.length > 0) {
let lastPart = parts[parts.length - 1]
// 移除查询参数和锚点
const queryIndex = lastPart.indexOf('?')
if (queryIndex >= 0) {
lastPart = lastPart.substring(0, queryIndex)
}
const hashIndex = lastPart.indexOf('#')
if (hashIndex >= 0) {
lastPart = lastPart.substring(0, hashIndex)
}
return lastPart.trim()
}
}
// 直接返回(可能是纯链接码)
return trimmed
}
// 加载商品列表(无查询条件)
const loadProductList = async () => {
loading.value = true
try {
@@ -212,6 +360,91 @@ const loadProductList = async () => {
}
}
// 查询商品列表
const handleQuery = async () => {
loading.value = true
try {
// 构建查询条件(只包含非空字段)
const query = {}
if (queryForm.value.name && queryForm.value.name.trim()) {
query.name = queryForm.value.name.trim()
}
if (queryForm.value.linkCode && queryForm.value.linkCode.trim()) {
// 自动从完整URL中提取链接码
const extractedLinkCode = extractLinkCodeFromUrl(queryForm.value.linkCode.trim())
query.linkCode = extractedLinkCode
}
if (queryForm.value.status) {
query.status = queryForm.value.status
}
if (queryForm.value.salesRegion) {
query.salesRegion = queryForm.value.salesRegion
}
// 添加分页参数
query.pageNum = pagination.value.pageNum
query.pageSize = pagination.value.pageSize
const response = await queryProducts(query)
if (response.code === '0000' && response.data) {
const pageResult = response.data
productList.value = pageResult.records || []
// 更新分页信息
pagination.value.total = pageResult.total || 0
pagination.value.pageNum = pageResult.current || 1
pagination.value.pageSize = pageResult.size || 10
if (productList.value.length === 0) {
ElMessage.info('未找到符合条件的商品')
} else {
ElMessage.success(`查询到 ${pagination.value.total} 条商品,当前第 ${pagination.value.pageNum}`)
}
} else {
ElMessage.error(response.message || '查询商品列表失败')
productList.value = []
pagination.value.total = 0
}
} catch (error) {
console.error('查询商品列表失败:', error)
ElMessage.error('查询商品列表失败')
productList.value = []
} finally {
loading.value = false
}
}
// 重置查询条件
const handleReset = () => {
queryForm.value = {
name: '',
linkCode: '',
status: '',
salesRegion: '',
pageNum: 1,
pageSize: 10
}
pagination.value = {
pageNum: 1,
pageSize: 10,
total: 0
}
loadProductList()
}
// 分页大小改变
const handleSizeChange = (size) => {
pagination.value.pageSize = size
pagination.value.pageNum = 1 // 重置到第一页
handleQuery()
}
// 页码改变
const handlePageChange = (page) => {
pagination.value.pageNum = page
handleQuery()
}
// 编辑商品
const editProduct = (id) => {
// TODO: 实现编辑功能,跳转到编辑页面
@@ -259,24 +492,30 @@ const copyProductUrl = async (id, url) => {
}
}
// 删除商品
const deleteProduct = async (id) => {
// 下架商品
const offShelfProduct = async (id) => {
try {
await ElMessageBox.confirm('确定要删除该商品吗?', '提示', {
await ElMessageBox.confirm(
'确定要下架该商品吗下架后商品所有SKU库存将改为0链接将失效无法再被访问。',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
}
)
// TODO: 实现删除商品API
ElMessage.info('删除功能待实现')
// await request.delete(`/api/product/${id}`)
// ElMessage.success('商品删除成功')
// loadProductList()
const response = await offShelfProductById(id)
if (response.code === '0000') {
ElMessage.success('商品下架成功')
loadProductList()
} else {
ElMessage.error(response.message || '商品下架失败')
}
} catch (error) {
if (error !== 'cancel') {
console.error('删除商品失败:', error)
ElMessage.error('删除商品失败')
console.error('下架商品失败:', error)
ElMessage.error('下架商品失败')
}
}
}
@@ -367,5 +606,53 @@ onMounted(() => {
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;
}
}
/* 查询表单 */
.search-card {
margin-bottom: 20px;
}
.search-form {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.search-form .el-form-item {
margin-bottom: 0;
}
@media (max-width: 768px) {
.search-form {
flex-direction: column;
}
.search-form .el-form-item {
width: 100%;
}
.search-form .el-form-item .el-input,
.search-form .el-form-item .el-select {
width: 100% !important;
}
}
</style>

View File

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