feat(order): 实现客户订单创建与确认功能

- 新增客户订单创建页面,简化表单字段并优化用户体验
- 实现订单确认页面,展示订单详情、客户信息和收货地址
- 添加订单状态显示和支付跳转功能
- 创建订单相关API接口封装
- 优化路由配置,支持商品链接码访问
- 添加页面标题组件和首页跳转逻辑
This commit is contained in:
2025-12-22 15:21:51 +08:00
parent f440ce2ade
commit f9b103426c
7 changed files with 642 additions and 376 deletions

View File

@@ -1,14 +1,19 @@
<template>
<el-container>
<!-- 客户页面商品详情页不显示管理导航 -->
<div v-if="isCustomerPage" class="customer-layout">
<router-view />
</div>
<!-- 管理页面显示完整导航 -->
<el-container v-else>
<el-header>
<div class="header-content">
<h1>MT Pay</h1>
<h1>MT Pay 管理系统</h1>
<el-menu
mode="horizontal"
:default-active="activeIndex"
router
>
<el-menu-item index="/">商品详情</el-menu-item>
<el-menu-item index="/create-order">创建订单</el-menu-item>
<el-menu-item index="/query">订单查询</el-menu-item>
<el-menu-item index="/manage/product">商品管理</el-menu-item>
@@ -26,6 +31,13 @@ import { computed } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
// 判断是否为客户页面(商品详情页)
const isCustomerPage = computed(() => {
return route.meta?.isCustomerPage === true ||
route.path.startsWith('/product/')
})
const activeIndex = computed(() => route.path)
</script>
@@ -62,5 +74,11 @@ const activeIndex = computed(() => route.path)
padding: 20px;
min-height: calc(100vh - 60px);
}
/* 客户页面布局:全屏显示,无导航栏 */
.customer-layout {
min-height: 100vh;
background-color: #f5f7fa;
}
</style>

33
src/api/order.js Normal file
View File

@@ -0,0 +1,33 @@
import request from './request'
/**
* 创建客户订单
*/
export function createCustomerOrder(data) {
return request({
url: '/order',
method: 'post',
data
})
}
/**
* 根据订单号获取订单详情
*/
export function getOrderByOrderNo(orderNo) {
return request({
url: `/order/${orderNo}`,
method: 'get'
})
}
/**
* 根据ID获取订单详情
*/
export function getOrderById(id) {
return request({
url: `/order/id/${id}`,
method: 'get'
})
}

View File

@@ -0,0 +1,39 @@
<template>
<div class="page-header">
<h2>{{ title }}</h2>
<p v-if="description" class="description">{{ description }}</p>
</div>
</template>
<script setup>
defineProps({
title: {
type: String,
required: true
},
description: {
type: String,
default: ''
}
})
</script>
<style scoped>
.page-header {
margin-bottom: 20px;
}
.page-header h2 {
margin: 0;
font-size: 20px;
font-weight: 500;
color: #303133;
}
.description {
margin: 8px 0 0 0;
font-size: 14px;
color: #909399;
}
</style>

View File

@@ -16,13 +16,26 @@ const routes = [
name: 'ProductDetail',
component: ProductDetail,
// 支持商品ID数字或链接码32位字符串
props: true
props: true,
meta: { isCustomerPage: true } // 标记为客户页面
},
{
path: '/product/link/:linkCode',
name: 'ProductDetailByLinkCode',
component: ProductDetail,
props: true,
meta: { isCustomerPage: true } // 标记为客户页面
},
{
path: '/create-order',
name: 'CreateOrder',
component: CreateOrder
},
{
path: '/order/confirm',
name: 'OrderConfirm',
component: () => import('../views/OrderConfirm.vue')
},
{
path: '/checkout',
name: 'Checkout',
@@ -55,5 +68,11 @@ const router = createRouter({
routes
})
// 添加路由守卫,用于调试
router.beforeEach((to, from, next) => {
console.log('路由导航:', from.path, '->', to.path)
next()
})
export default router

View File

@@ -3,11 +3,11 @@
<el-card>
<template #header>
<div class="card-header">
<span>创建支付订单</span>
<span>填写订单信息</span>
</div>
</template>
<!-- 商品信息展示卡片 -->
<!-- 商品信息展示 -->
<el-card v-if="productInfo" class="product-info-card" shadow="never">
<template #header>
<div class="product-card-header">
@@ -23,24 +23,10 @@
/>
<div class="product-info-details">
<div class="product-info-name">{{ productInfo.name }}</div>
<div class="product-info-subtitle">{{ productInfo.subtitle }}</div>
<!-- SKU信息 -->
<div class="product-info-sku" v-if="productInfo.sku">
<span class="sku-label">商品编码</span>
<span class="sku-label">SKU</span>
<span class="sku-value">{{ productInfo.sku }}</span>
</div>
<!-- 规格信息 -->
<div class="product-info-specs" v-if="productInfo.specs && productInfo.specs.length > 0">
<div
v-for="(spec, index) in productInfo.specs"
:key="index"
class="product-spec-item"
>
<span class="spec-label">{{ spec.name }}</span>
<span class="spec-value">{{ spec.label }}</span>
</div>
</div>
<!-- 价格和数量 -->
<div class="product-info-price">
<span class="price-label">单价</span>
<span class="price-value">{{ productInfo.currency }} {{ formatPrice(productInfo.price) }}</span>
@@ -58,252 +44,116 @@
ref="formRef"
:model="form"
:rules="rules"
label-width="150px"
label-width="120px"
label-position="left"
>
<el-form-item label="商户订单号" prop="merchantTransactionId">
<el-divider>客户信息</el-divider>
<el-form-item label="客户姓名" prop="customerName">
<el-input
v-model="form.merchantTransactionId"
placeholder="请输入商户订单号(全局唯一)"
clearable
/>
<el-button
type="primary"
link
@click="generateOrderIdLocal"
style="margin-left: 10px"
>
自动生成
</el-button>
</el-form-item>
<el-form-item label="交易金额" prop="amount">
<el-input-number
v-model="form.amount"
:precision="2"
:min="0.01"
:max="999999.99"
placeholder="请输入交易金额"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="交易币种" prop="currency">
<el-select v-model="form.currency" placeholder="请选择币种" style="width: 100%">
<el-option label="USD - 美元" value="USD" />
<el-option label="EUR - 欧元" value="EUR" />
<el-option label="GBP - 英镑" value="GBP" />
<el-option label="CNY - 人民币" value="CNY" />
<el-option label="JPY - 日元" value="JPY" />
</el-select>
</el-form-item>
<el-form-item label="交易类型" prop="paymentType">
<el-radio-group v-model="form.paymentType">
<el-radio label="SALE">直接付款</el-radio>
<el-radio label="AUTH">预授权</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="商户用户ID" prop="merchantUserId">
<el-input
v-model="form.merchantUserId"
placeholder="请输入商户用户ID可选"
v-model="form.customerName"
placeholder="请输入客户姓名"
clearable
/>
</el-form-item>
<el-form-item label="结果重定向URL" prop="shopperResultUrl">
<el-form-item label="客户电话" prop="customerPhone">
<el-input
v-model="form.shopperResultUrl"
placeholder="支付完成后跳转的URL"
v-model="form.customerPhone"
placeholder="请输入客户电话"
clearable
/>
</el-form-item>
<el-form-item label="取消重定向URL" prop="shopperCancelUrl">
<el-form-item label="客户邮箱" prop="customerEmail">
<el-input
v-model="form.shopperCancelUrl"
placeholder="取消支付后跳转的URL"
/>
</el-form-item>
<el-divider>风控信息</el-divider>
<el-form-item label="客户姓名" prop="riskInfo.customer.firstName">
<el-input
v-model="form.riskInfo.customer.firstName"
placeholder="名"
style="width: 48%"
/>
<el-input
v-model="form.riskInfo.customer.lastName"
placeholder="姓"
style="width: 48%; margin-left: 4%"
/>
</el-form-item>
<el-form-item label="客户邮箱" prop="riskInfo.customer.email">
<el-input
v-model="form.riskInfo.customer.email"
placeholder="请输入邮箱"
/>
</el-form-item>
<el-form-item label="客户电话" prop="riskInfo.customer.phone">
<el-input
v-model="form.riskInfo.customer.phone"
placeholder="请输入电话"
/>
</el-form-item>
<el-divider>商品信息</el-divider>
<el-form-item label="商品名称" prop="riskInfo.goods.0.name">
<el-input
v-model="form.riskInfo.goods[0].name"
placeholder="请输入商品名称"
/>
</el-form-item>
<el-form-item label="商品描述" prop="riskInfo.goods.0.description">
<el-input
v-model="form.riskInfo.goods[0].description"
type="textarea"
:rows="2"
placeholder="请输入商品描述"
/>
</el-form-item>
<el-form-item label="商品单价" prop="riskInfo.goods.0.averageUnitPrice">
<el-input-number
v-model="form.riskInfo.goods[0].averageUnitPrice"
:precision="2"
:min="0.01"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="商品数量" prop="riskInfo.goods.0.number">
<el-input-number
v-model="form.riskInfo.goods[0].number"
:min="1"
style="width: 100%"
v-model="form.customerEmail"
placeholder="请输入客户邮箱(可选)"
clearable
/>
</el-form-item>
<el-divider>收货地址</el-divider>
<el-form-item label="收货人姓名" prop="riskInfo.shipping.firstName">
<el-form-item label="收货人姓名" prop="shippingName">
<el-input
v-model="form.riskInfo.shipping.firstName"
placeholder="名"
style="width: 48%"
/>
<el-input
v-model="form.riskInfo.shipping.lastName"
placeholder="姓"
style="width: 48%; margin-left: 4%"
v-model="form.shippingName"
placeholder="请输入收货人姓名"
clearable
/>
</el-form-item>
<el-form-item label="街道地址" prop="riskInfo.shipping.street">
<el-form-item label="收货人电话" prop="shippingPhone">
<el-input
v-model="form.riskInfo.shipping.street"
placeholder="请输入街道地址"
v-model="form.shippingPhone"
placeholder="请输入收货人电话"
clearable
/>
</el-form-item>
<el-form-item label="城市" prop="riskInfo.shipping.city">
<el-input
v-model="form.riskInfo.shipping.city"
placeholder="请输入城市"
style="width: 48%"
/>
<el-input
v-model="form.riskInfo.shipping.state"
placeholder="州/省(可选)"
style="width: 48%; margin-left: 4%"
/>
</el-form-item>
<el-form-item label="邮编" prop="riskInfo.shipping.postcode">
<el-input
v-model="form.riskInfo.shipping.postcode"
placeholder="请输入邮编"
style="width: 48%"
/>
<el-select
v-model="form.riskInfo.shipping.country"
placeholder="国家"
style="width: 48%; margin-left: 4%"
>
<el-option label="US - 美国" value="US" />
<el-option label="CN - 中国" value="CN" />
<el-option label="GB - 英国" value="GB" />
<el-option label="DE - 德国" value="DE" />
<el-option label="FR - 法国" value="FR" />
<el-form-item label="收货国家" prop="shippingCountry">
<el-select v-model="form.shippingCountry" placeholder="请选择国家" style="width: 100%">
<el-option label="中国 (CN)" value="CN" />
<el-option label="美国 (US)" value="US" />
<el-option label="马来西亚 (MY)" value="MY" />
<el-option label="菲律宾 (PH)" value="PH" />
<el-option label="泰国 (TH)" value="TH" />
<el-option label="越南 (VN)" value="VN" />
<el-option label="新加坡 (SG)" value="SG" />
<el-option label="英国 (GB)" value="GB" />
<el-option label="德国 (DE)" value="DE" />
<el-option label="法国 (FR)" value="FR" />
</el-select>
</el-form-item>
<el-divider>账单地址</el-divider>
<el-form-item label="账单人姓名" prop="riskInfo.billing.firstName">
<el-form-item label="收货城市" prop="shippingCity">
<el-input
v-model="form.riskInfo.billing.firstName"
placeholder=""
v-model="form.shippingCity"
placeholder="请输入收货城市"
style="width: 48%"
clearable
/>
<el-input
v-model="form.riskInfo.billing.lastName"
placeholder="姓"
style="width: 48%; margin-left: 4%"
/>
</el-form-item>
<el-form-item label="账单街道地址" prop="riskInfo.billing.street">
<el-input
v-model="form.riskInfo.billing.street"
placeholder="请输入街道地址"
/>
</el-form-item>
<el-form-item label="账单城市" prop="riskInfo.billing.city">
<el-input
v-model="form.riskInfo.billing.city"
placeholder="请输入城市"
style="width: 48%"
/>
<el-input
v-model="form.riskInfo.billing.state"
v-model="form.shippingState"
placeholder="州/省(可选)"
style="width: 48%; margin-left: 4%"
clearable
/>
</el-form-item>
<el-form-item label="账单邮编" prop="riskInfo.billing.postcode">
<el-form-item label="街道地址" prop="shippingStreet">
<el-input
v-model="form.riskInfo.billing.postcode"
placeholder="请输入邮编"
style="width: 48%"
v-model="form.shippingStreet"
type="textarea"
:rows="2"
placeholder="请输入详细街道地址"
clearable
/>
</el-form-item>
<el-form-item label="邮编" prop="shippingPostcode">
<el-input
v-model="form.shippingPostcode"
placeholder="请输入邮编(可选)"
clearable
/>
</el-form-item>
<el-form-item label="订单备注" prop="remark">
<el-input
v-model="form.remark"
type="textarea"
:rows="3"
placeholder="请输入订单备注(可选)"
clearable
/>
<el-select
v-model="form.riskInfo.billing.country"
placeholder="国家"
style="width: 48%; margin-left: 4%"
>
<el-option label="US - 美国" value="US" />
<el-option label="CN - 中国" value="CN" />
<el-option label="GB - 英国" value="GB" />
<el-option label="DE - 德国" value="DE" />
<el-option label="FR - 法国" value="FR" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm" :loading="loading">
创建订单并支付
<el-button type="primary" size="large" @click="submitForm" :loading="loading" style="width: 200px">
提交订单
</el-button>
<el-button @click="resetForm">重置</el-button>
<el-button @click="goBack" style="margin-left: 10px">返回</el-button>
</el-form-item>
</el-form>
</el-card>
@@ -311,11 +161,11 @@
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import { ref, reactive, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import { createPaymentOrder } from '../api/payment'
import { generateOrderId, formatAmount } from '../utils/helpers'
import { createCustomerOrder } from '../api/order'
import { formatAmount } from '../utils/helpers'
const router = useRouter()
const route = useRoute()
@@ -324,80 +174,54 @@ const loading = ref(false)
const productInfo = ref(null)
const form = reactive({
merchantTransactionId: '',
amount: '',
currency: 'USD',
paymentType: 'SALE',
merchantUserId: '',
shopperResultUrl: `${window.location.origin}/result`,
shopperCancelUrl: `${window.location.origin}/result`,
riskInfo: {
customer: {
firstName: '',
lastName: '',
email: '',
phone: '',
registerTime: new Date().toISOString().replace(/[-:]/g, '').split('.')[0],
registerIp: '',
registerTerminal: 'PC',
registerRange: '1',
orderTime: new Date().toISOString().replace(/[-:]/g, '').split('.')[0],
orderIp: '',
orderCountry: 'US'
},
goods: [{
name: '',
description: '',
averageUnitPrice: '',
number: '1',
virtualProduct: 'N'
}],
shipping: {
firstName: '',
lastName: '',
street: '',
city: '',
state: '',
postcode: '',
country: 'US'
},
billing: {
firstName: '',
lastName: '',
street: '',
city: '',
state: '',
postcode: '',
country: 'US'
}
}
customerName: '',
customerPhone: '',
customerEmail: '',
shippingName: '',
shippingPhone: '',
shippingCountry: '',
shippingState: '',
shippingCity: '',
shippingStreet: '',
shippingPostcode: '',
remark: ''
})
const rules = {
merchantTransactionId: [
{ required: true, message: '请输入商户订单号', trigger: 'blur' }
customerName: [
{ required: true, message: '请输入客户姓名', trigger: 'blur' }
],
amount: [
{ required: true, message: '请输入交易金额', trigger: 'blur' }
customerPhone: [
{ required: true, message: '请输入客户电话', trigger: 'blur' },
{ pattern: /^[0-9+\-\s()]+$/, message: '请输入有效的电话号码', trigger: 'blur' }
],
currency: [
{ required: true, message: '请选择交易币种', trigger: 'change' }
customerEmail: [
{ type: 'email', message: '请输入有效的邮箱地址', trigger: 'blur' }
],
paymentType: [
{ required: true, message: '请选择交易类型', trigger: 'change' }
shippingName: [
{ required: true, message: '请输入收货人姓名', trigger: 'blur' }
],
shopperResultUrl: [
{ required: true, message: '请输入结果重定向URL', trigger: 'blur' }
shippingPhone: [
{ required: true, message: '请输入收货人电话', trigger: 'blur' },
{ pattern: /^[0-9+\-\s()]+$/, message: '请输入有效的电话号码', trigger: 'blur' }
],
shopperCancelUrl: [
{ required: true, message: '请输入取消重定向URL', trigger: 'blur' }
shippingCountry: [
{ required: true, message: '请选择收货国家', trigger: 'change' }
],
shippingCity: [
{ required: true, message: '请输入收货城市', trigger: 'blur' }
],
shippingStreet: [
{ required: true, message: '请输入街道地址', trigger: 'blur' }
]
}
const generateOrderIdLocal = () => {
form.merchantTransactionId = generateOrderId()
// 返回上一页
const goBack = () => {
router.back()
}
// 提交订单
const submitForm = async () => {
if (!formRef.value) return
@@ -407,34 +231,40 @@ const submitForm = async () => {
return
}
if (!productInfo.value) {
ElMessage.error('商品信息缺失,请重新选择商品')
return
}
loading.value = true
try {
// 格式化数据
const requestData = {
...form,
amount: form.amount.toFixed(2),
signType: 'MD5',
language: 'zh',
threeDSecure: 'N',
riskInfo: {
...form.riskInfo,
goods: form.riskInfo.goods.map(g => ({
...g,
averageUnitPrice: g.averageUnitPrice.toFixed(2),
number: g.number.toString()
}))
}
// 构建订单请求数据
const orderData = {
productId: productInfo.value.id,
skuId: productInfo.value.skuId,
quantity: productInfo.value.quantity,
customerName: form.customerName,
customerPhone: form.customerPhone,
customerEmail: form.customerEmail || null,
shippingName: form.shippingName,
shippingPhone: form.shippingPhone,
shippingCountry: form.shippingCountry,
shippingState: form.shippingState || null,
shippingCity: form.shippingCity,
shippingStreet: form.shippingStreet,
shippingPostcode: form.shippingPostcode || null,
remark: form.remark || null
}
const response = await createPaymentOrder(requestData)
const response = await createCustomerOrder(orderData)
if (response.code === '0000') {
if (response.code === '0000' && response.data) {
ElMessage.success('订单创建成功')
// 跳转到收银台页面
// 跳转到订单确认页面
router.push({
path: '/checkout',
query: { token: response.data.token }
path: '/order/confirm',
query: { orderNo: response.data.orderNo }
})
} else {
ElMessage.error(response.message || '创建订单失败')
@@ -448,12 +278,6 @@ const submitForm = async () => {
})
}
const resetForm = () => {
if (!formRef.value) return
formRef.value.resetFields()
generateOrderIdLocal()
}
// 格式化价格
const formatPrice = (price) => {
return formatAmount(price)
@@ -466,33 +290,24 @@ onMounted(() => {
const data = JSON.parse(decodeURIComponent(route.query.data))
if (data.product) {
productInfo.value = data.product
// 自动填充表单
form.amount = (data.product.price * data.product.quantity).toFixed(2)
form.currency = data.product.currency || 'USD'
form.riskInfo.goods[0].name = data.product.name
form.riskInfo.goods[0].description = `${data.product.subtitle} ${data.product.specs.map(s => `${s.name}:${s.label}`).join(';')}`
form.riskInfo.goods[0].averageUnitPrice = data.product.price.toFixed(2)
form.riskInfo.goods[0].number = data.product.quantity.toString()
// 如果有SKU添加到商品描述中
if (data.product.sku) {
form.riskInfo.goods[0].sku = data.product.sku
}
}
} catch (error) {
console.error('解析商品信息失败:', error)
ElMessage.error('商品信息解析失败')
router.push('/')
}
} else {
ElMessage.error('缺少商品信息')
router.push('/')
}
})
// 初始化时生成订单号
generateOrderIdLocal()
</script>
<style scoped>
.create-order {
max-width: 1000px;
margin: 0 auto;
padding: 20px;
}
.card-header {
@@ -546,12 +361,6 @@ generateOrderIdLocal()
line-height: 1.5;
}
.product-info-subtitle {
font-size: 14px;
color: #909399;
line-height: 1.5;
}
.product-info-sku {
display: flex;
align-items: center;
@@ -572,34 +381,6 @@ generateOrderIdLocal()
font-size: 14px;
color: #303133;
font-weight: 600;
font-family: 'Courier New', monospace;
background: #fff;
padding: 2px 8px;
border-radius: 4px;
border: 1px solid #dcdfe6;
}
.product-info-specs {
display: flex;
flex-wrap: wrap;
gap: 15px;
}
.product-spec-item {
display: flex;
align-items: center;
gap: 5px;
}
.spec-label {
font-size: 14px;
color: #909399;
}
.spec-value {
font-size: 14px;
color: #303133;
font-weight: 500;
}
.product-info-price {
@@ -619,18 +400,6 @@ generateOrderIdLocal()
color: #606266;
}
.price-value {
font-size: 14px;
color: #303133;
font-weight: 500;
}
.quantity-value {
font-size: 14px;
color: #303133;
font-weight: 500;
}
.total-label {
margin-left: auto;
font-weight: 600;
@@ -642,4 +411,3 @@ generateOrderIdLocal()
font-weight: 700;
}
</style>

59
src/views/Home.vue Normal file
View File

@@ -0,0 +1,59 @@
<template>
<div class="home">
<el-card>
<template #header>
<div class="card-header">
<h2>欢迎使用 MT Pay 支付系统</h2>
</div>
</template>
<div class="welcome-content">
<p>正在跳转到商品管理页面...</p>
<el-button type="primary" @click="goToProductManage">
立即前往商品管理
</el-button>
</div>
</el-card>
</div>
</template>
<script setup>
import { onMounted } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const goToProductManage = () => {
router.push('/manage/product')
}
onMounted(() => {
// 自动跳转到商品管理页面
setTimeout(() => {
router.push('/manage/product')
}, 1000)
})
</script>
<style scoped>
.home {
max-width: 800px;
margin: 50px auto;
padding: 20px;
}
.card-header {
text-align: center;
}
.welcome-content {
text-align: center;
padding: 40px 20px;
}
.welcome-content p {
font-size: 16px;
color: #666;
margin-bottom: 20px;
}
</style>

330
src/views/OrderConfirm.vue Normal file
View File

@@ -0,0 +1,330 @@
<template>
<div class="order-confirm">
<el-card v-if="orderLoading">
<el-skeleton :rows="10" animated />
</el-card>
<template v-else-if="order">
<el-card>
<template #header>
<div class="card-header">
<span>订单确认</span>
<el-tag :type="getStatusType(order.status)" size="large">
{{ getStatusText(order.status) }}
</el-tag>
</div>
</template>
<!-- 订单信息 -->
<div class="order-info-section">
<h3>订单信息</h3>
<el-descriptions :column="2" border>
<el-descriptions-item label="订单号">{{ order.orderNo }}</el-descriptions-item>
<el-descriptions-item label="订单金额">
<span class="order-amount">{{ order.currency }} {{ formatPrice(order.totalAmount) }}</span>
</el-descriptions-item>
<el-descriptions-item label="商品名称" :span="2">{{ order.productName }}</el-descriptions-item>
<el-descriptions-item label="SKU名称" :span="2">{{ order.skuName }}</el-descriptions-item>
<el-descriptions-item label="购买数量">{{ order.quantity }}</el-descriptions-item>
<el-descriptions-item label="单价">{{ order.currency }} {{ formatPrice(order.unitPrice) }}</el-descriptions-item>
<el-descriptions-item label="创建时间" :span="2">{{ formatDateTime(order.createTime) }}</el-descriptions-item>
</el-descriptions>
</div>
<!-- 客户信息 -->
<div class="order-info-section">
<h3>客户信息</h3>
<el-descriptions :column="2" border>
<el-descriptions-item label="客户姓名">{{ order.customerName }}</el-descriptions-item>
<el-descriptions-item label="客户电话">{{ order.customerPhone }}</el-descriptions-item>
<el-descriptions-item label="客户邮箱" :span="2">
{{ order.customerEmail || '未填写' }}
</el-descriptions-item>
</el-descriptions>
</div>
<!-- 收货地址 -->
<div class="order-info-section">
<h3>收货地址</h3>
<el-descriptions :column="2" border>
<el-descriptions-item label="收货人">{{ order.shippingName }}</el-descriptions-item>
<el-descriptions-item label="收货电话">{{ order.shippingPhone }}</el-descriptions-item>
<el-descriptions-item label="收货地址" :span="2">
{{ formatShippingAddress(order) }}
</el-descriptions-item>
</el-descriptions>
</div>
<!-- 订单备注 -->
<div class="order-info-section" v-if="order.remark">
<h3>订单备注</h3>
<p class="order-remark">{{ order.remark }}</p>
</div>
<!-- 操作按钮 -->
<div class="action-buttons">
<el-button
v-if="order.paymentStatus === 'UNPAID'"
type="primary"
size="large"
@click="handlePay"
:loading="payLoading"
style="width: 200px"
>
<el-icon><Money /></el-icon>
立即支付
</el-button>
<el-button @click="goBack" style="margin-left: 10px">返回</el-button>
</div>
</el-card>
</template>
<el-card v-else>
<el-empty description="订单不存在">
<el-button type="primary" @click="goBack">返回</el-button>
</el-empty>
</el-card>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import { Money } from '@element-plus/icons-vue'
import { getOrderByOrderNo } from '../api/order'
import { createPaymentOrder } from '../api/payment'
import { formatAmount } from '../utils/helpers'
import { generateOrderId } from '../utils/helpers'
const router = useRouter()
const route = useRoute()
const orderLoading = ref(true)
const payLoading = ref(false)
const order = ref(null)
// 获取订单状态类型
const getStatusType = (status) => {
const statusMap = {
'PENDING': 'warning',
'PAID': 'success',
'SHIPPED': 'info',
'COMPLETED': 'success',
'CANCELLED': 'info'
}
return statusMap[status] || 'info'
}
// 获取订单状态文本
const getStatusText = (status) => {
const statusMap = {
'PENDING': '待支付',
'PAID': '已支付',
'SHIPPED': '已发货',
'COMPLETED': '已完成',
'CANCELLED': '已取消'
}
return statusMap[status] || status
}
// 格式化价格
const formatPrice = (price) => {
return formatAmount(price)
}
// 格式化日期时间
const formatDateTime = (dateTime) => {
if (!dateTime) return '-'
const date = new Date(dateTime)
return date.toLocaleString('zh-CN')
}
// 格式化收货地址
const formatShippingAddress = (order) => {
const parts = []
if (order.shippingCountry) parts.push(order.shippingCountry)
if (order.shippingState) parts.push(order.shippingState)
if (order.shippingCity) parts.push(order.shippingCity)
if (order.shippingStreet) parts.push(order.shippingStreet)
if (order.shippingPostcode) parts.push(`邮编:${order.shippingPostcode}`)
return parts.join(' ')
}
// 返回上一页
const goBack = () => {
router.back()
}
// 加载订单详情
const loadOrder = async () => {
const orderNo = route.query.orderNo
if (!orderNo) {
ElMessage.error('订单号不能为空')
router.push('/')
return
}
orderLoading.value = true
try {
const response = await getOrderByOrderNo(orderNo)
if (response.code === '0000' && response.data) {
order.value = response.data
} else {
ElMessage.error(response.message || '获取订单信息失败')
order.value = null
}
} catch (error) {
console.error('获取订单信息失败:', error)
ElMessage.error('获取订单信息失败')
order.value = null
} finally {
orderLoading.value = false
}
}
// 处理支付
const handlePay = async () => {
if (!order.value) {
ElMessage.error('订单信息不存在')
return
}
if (order.value.paymentStatus !== 'UNPAID') {
ElMessage.warning('订单已支付或已取消')
return
}
payLoading.value = true
try {
// 构建支付请求数据
const paymentData = {
merchantTransactionId: generateOrderId(),
amount: order.value.totalAmount.toFixed(2),
currency: order.value.currency,
paymentType: 'SALE',
merchantUserId: '',
shopperResultUrl: `${window.location.origin}/result`,
shopperCancelUrl: `${window.location.origin}/result`,
signType: 'MD5',
language: 'zh',
threeDSecure: 'N',
riskInfo: {
customer: {
firstName: order.value.customerName.split(' ')[0] || order.value.customerName,
lastName: order.value.customerName.split(' ').slice(1).join(' ') || '',
email: order.value.customerEmail || '',
phone: order.value.customerPhone,
registerTime: new Date().toISOString().replace(/[-:]/g, '').split('.')[0],
registerIp: '',
registerTerminal: 'PC',
registerRange: '1',
orderTime: new Date(order.value.createTime).toISOString().replace(/[-:]/g, '').split('.')[0],
orderIp: '',
orderCountry: order.value.shippingCountry || 'US'
},
goods: [{
name: order.value.productName,
description: order.value.skuName,
sku: order.value.skuName,
averageUnitPrice: order.value.unitPrice.toFixed(2),
number: order.value.quantity.toString(),
virtualProduct: 'N'
}],
shipping: {
firstName: order.value.shippingName.split(' ')[0] || order.value.shippingName,
lastName: order.value.shippingName.split(' ').slice(1).join(' ') || '',
street: order.value.shippingStreet,
city: order.value.shippingCity,
state: order.value.shippingState || '',
postcode: order.value.shippingPostcode || '',
country: order.value.shippingCountry
},
billing: {
firstName: order.value.shippingName.split(' ')[0] || order.value.shippingName,
lastName: order.value.shippingName.split(' ').slice(1).join(' ') || '',
street: order.value.shippingStreet,
city: order.value.shippingCity,
state: order.value.shippingState || '',
postcode: order.value.shippingPostcode || '',
country: order.value.shippingCountry
}
},
notificationUrl: `${window.location.origin}/api/callback/pingpong`
}
const response = await createPaymentOrder(paymentData)
if (response.code === '0000' && response.data && response.data.token) {
ElMessage.success('正在跳转到支付页面...')
// 跳转到收银台页面
router.push({
path: '/checkout',
query: { token: response.data.token }
})
} else {
ElMessage.error(response.message || '创建支付订单失败')
}
} catch (error) {
console.error('创建支付订单失败:', error)
ElMessage.error(error.response?.data?.message || '创建支付订单失败,请稍后重试')
} finally {
payLoading.value = false
}
}
onMounted(() => {
loadOrder()
})
</script>
<style scoped>
.order-confirm {
max-width: 1000px;
margin: 0 auto;
padding: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 18px;
font-weight: bold;
}
.order-info-section {
margin-bottom: 30px;
}
.order-info-section h3 {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 2px solid #409eff;
}
.order-amount {
font-size: 20px;
font-weight: 700;
color: #f56c6c;
}
.order-remark {
padding: 15px;
background: #f5f7fa;
border-radius: 6px;
color: #606266;
line-height: 1.6;
}
.action-buttons {
margin-top: 30px;
text-align: center;
padding-top: 20px;
border-top: 1px solid #e4e7ed;
}
</style>