feat(payment): 新增创建支付订单功能
- 添加创建订单页面,支持完整的支付信息填写 - 实现商品信息展示与自动填充功能 - 集成风控信息、收货地址和账单地址表单 - 支持自动生成商户订单号 - 实现表单验证和提交逻辑 - 添加订单创建成功后的跳转逻辑 - 集成Element Plus组件库优化界面交互 - 添加路由配置支持商品ID或链接码访问 - 实现价格格式化和数据显示优化 - 添加基础的错误处理和用户提示机制
This commit is contained in:
14
index.html
Normal file
14
index.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="icon" href="/favicon.ico">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>MT Pay - PingPong支付</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
1580
package-lock.json
generated
Normal file
1580
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
38
src/components/Loading.vue
Normal file
38
src/components/Loading.vue
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="loading" class="loading-overlay">
|
||||||
|
<el-loading
|
||||||
|
:text="text"
|
||||||
|
:spinner="spinner"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
type: String,
|
||||||
|
default: '加载中...'
|
||||||
|
},
|
||||||
|
spinner: {
|
||||||
|
type: String,
|
||||||
|
default: 'el-icon-loading'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.loading-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 9999;
|
||||||
|
background-color: rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
32
src/main.js
Normal file
32
src/main.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
import ElementPlus from 'element-plus'
|
||||||
|
import 'element-plus/dist/index.css'
|
||||||
|
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
// 注册所有图标
|
||||||
|
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||||
|
app.component(key, component)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.use(router)
|
||||||
|
app.use(ElementPlus)
|
||||||
|
|
||||||
|
// 添加错误处理
|
||||||
|
app.config.errorHandler = (err, instance, info) => {
|
||||||
|
console.error('Vue错误:', err)
|
||||||
|
console.error('错误信息:', info)
|
||||||
|
console.error('组件实例:', instance)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 挂载应用
|
||||||
|
try {
|
||||||
|
app.mount('#app')
|
||||||
|
console.log('Vue应用已成功挂载')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('应用挂载失败:', error)
|
||||||
|
}
|
||||||
|
|
||||||
@@ -14,7 +14,9 @@ const routes = [
|
|||||||
{
|
{
|
||||||
path: '/product/:id',
|
path: '/product/:id',
|
||||||
name: 'ProductDetail',
|
name: 'ProductDetail',
|
||||||
component: ProductDetail
|
component: ProductDetail,
|
||||||
|
// 支持商品ID(数字)或链接码(32位字符串)
|
||||||
|
props: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/create-order',
|
path: '/create-order',
|
||||||
|
|||||||
645
src/views/CreateOrder.vue
Normal file
645
src/views/CreateOrder.vue
Normal file
@@ -0,0 +1,645 @@
|
|||||||
|
<template>
|
||||||
|
<div class="create-order">
|
||||||
|
<el-card>
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>创建支付订单</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 商品信息展示卡片 -->
|
||||||
|
<el-card v-if="productInfo" class="product-info-card" shadow="never">
|
||||||
|
<template #header>
|
||||||
|
<div class="product-card-header">
|
||||||
|
<span>商品信息</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="product-info-content">
|
||||||
|
<div class="product-info-main">
|
||||||
|
<el-image
|
||||||
|
:src="productInfo.image"
|
||||||
|
fit="cover"
|
||||||
|
class="product-info-image"
|
||||||
|
/>
|
||||||
|
<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-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>
|
||||||
|
<span class="quantity-label">数量:</span>
|
||||||
|
<span class="quantity-value">x{{ productInfo.quantity }}</span>
|
||||||
|
<span class="total-label">小计:</span>
|
||||||
|
<span class="total-value">{{ productInfo.currency }} {{ formatPrice(productInfo.price * productInfo.quantity) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-form
|
||||||
|
ref="formRef"
|
||||||
|
:model="form"
|
||||||
|
:rules="rules"
|
||||||
|
label-width="150px"
|
||||||
|
label-position="left"
|
||||||
|
>
|
||||||
|
<el-form-item label="商户订单号" prop="merchantTransactionId">
|
||||||
|
<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(可选)"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="结果重定向URL" prop="shopperResultUrl">
|
||||||
|
<el-input
|
||||||
|
v-model="form.shopperResultUrl"
|
||||||
|
placeholder="支付完成后跳转的URL"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="取消重定向URL" prop="shopperCancelUrl">
|
||||||
|
<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%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-divider>收货地址</el-divider>
|
||||||
|
|
||||||
|
<el-form-item label="收货人姓名" prop="riskInfo.shipping.firstName">
|
||||||
|
<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%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="街道地址" prop="riskInfo.shipping.street">
|
||||||
|
<el-input
|
||||||
|
v-model="form.riskInfo.shipping.street"
|
||||||
|
placeholder="请输入街道地址"
|
||||||
|
/>
|
||||||
|
</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-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-divider>账单地址</el-divider>
|
||||||
|
|
||||||
|
<el-form-item label="账单人姓名" prop="riskInfo.billing.firstName">
|
||||||
|
<el-input
|
||||||
|
v-model="form.riskInfo.billing.firstName"
|
||||||
|
placeholder="名"
|
||||||
|
style="width: 48%"
|
||||||
|
/>
|
||||||
|
<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"
|
||||||
|
placeholder="州/省(可选)"
|
||||||
|
style="width: 48%; margin-left: 4%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="账单邮编" prop="riskInfo.billing.postcode">
|
||||||
|
<el-input
|
||||||
|
v-model="form.riskInfo.billing.postcode"
|
||||||
|
placeholder="请输入邮编"
|
||||||
|
style="width: 48%"
|
||||||
|
/>
|
||||||
|
<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>
|
||||||
|
<el-button @click="resetForm">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted, computed } from 'vue'
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { createPaymentOrder } from '../api/payment'
|
||||||
|
import { generateOrderId, formatAmount } from '../utils/helpers'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
const formRef = ref()
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
merchantTransactionId: [
|
||||||
|
{ required: true, message: '请输入商户订单号', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
amount: [
|
||||||
|
{ required: true, message: '请输入交易金额', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
currency: [
|
||||||
|
{ required: true, message: '请选择交易币种', trigger: 'change' }
|
||||||
|
],
|
||||||
|
paymentType: [
|
||||||
|
{ required: true, message: '请选择交易类型', trigger: 'change' }
|
||||||
|
],
|
||||||
|
shopperResultUrl: [
|
||||||
|
{ required: true, message: '请输入结果重定向URL', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
shopperCancelUrl: [
|
||||||
|
{ required: true, message: '请输入取消重定向URL', trigger: 'blur' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const generateOrderIdLocal = () => {
|
||||||
|
form.merchantTransactionId = generateOrderId()
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitForm = async () => {
|
||||||
|
if (!formRef.value) return
|
||||||
|
|
||||||
|
await formRef.value.validate(async (valid) => {
|
||||||
|
if (!valid) {
|
||||||
|
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 response = await createPaymentOrder(requestData)
|
||||||
|
|
||||||
|
if (response.code === '0000') {
|
||||||
|
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 {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
if (!formRef.value) return
|
||||||
|
formRef.value.resetFields()
|
||||||
|
generateOrderIdLocal()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化价格
|
||||||
|
const formatPrice = (price) => {
|
||||||
|
return formatAmount(price)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从路由参数获取商品信息
|
||||||
|
onMounted(() => {
|
||||||
|
if (route.query.data) {
|
||||||
|
try {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 初始化时生成订单号
|
||||||
|
generateOrderIdLocal()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.create-order {
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 商品信息卡片 */
|
||||||
|
.product-info-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card-header {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info-content {
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info-main {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info-image {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info-details {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info-name {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info-subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #909399;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info-sku {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #f5f7fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
border-left: 3px solid #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sku-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sku-value {
|
||||||
|
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 {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
padding: 12px;
|
||||||
|
background: #f9fafb;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-label,
|
||||||
|
.quantity-label,
|
||||||
|
.total-label {
|
||||||
|
font-size: 14px;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-value {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #f56c6c;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
152
src/views/OrderQuery.vue
Normal file
152
src/views/OrderQuery.vue
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
<template>
|
||||||
|
<div class="order-query">
|
||||||
|
<el-card>
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>订单查询</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-form :inline="true" :model="queryForm" class="query-form">
|
||||||
|
<el-form-item label="商户订单号">
|
||||||
|
<el-input
|
||||||
|
v-model="queryForm.merchantTransactionId"
|
||||||
|
placeholder="请输入商户订单号"
|
||||||
|
clearable
|
||||||
|
style="width: 300px"
|
||||||
|
@keyup.enter="handleQuery"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleQuery" :loading="loading">
|
||||||
|
查询
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<el-divider />
|
||||||
|
|
||||||
|
<div v-if="orderData" class="order-detail">
|
||||||
|
<el-descriptions title="订单详情" :column="2" border>
|
||||||
|
<el-descriptions-item label="商户订单号">
|
||||||
|
{{ orderData.merchantTransactionId }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="PingPong交易流水号">
|
||||||
|
{{ orderData.transactionId || '暂无' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="订单状态">
|
||||||
|
<el-tag :type="getStatusTagType(orderData.status)">
|
||||||
|
{{ getStatusText(orderData.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="交易金额">
|
||||||
|
{{ orderData.amount }} {{ orderData.currency }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="交易类型">
|
||||||
|
{{ orderData.paymentType === 'SALE' ? '直接付款' : '预授权' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="创建时间">
|
||||||
|
{{ orderData.createTime }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
|
||||||
|
<div class="action-buttons" style="margin-top: 20px">
|
||||||
|
<el-button type="primary" @click="goToPay" v-if="orderData.status === 'PENDING'">
|
||||||
|
继续支付
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="goToCreate">创建新订单</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-empty v-else-if="!loading" description="请输入订单号进行查询" />
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { getOrderStatus } from '../api/payment'
|
||||||
|
import { getStatusText, getStatusTagType } from '../utils/helpers'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const loading = ref(false)
|
||||||
|
const orderData = ref(null)
|
||||||
|
|
||||||
|
const queryForm = reactive({
|
||||||
|
merchantTransactionId: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleQuery = async () => {
|
||||||
|
if (!queryForm.merchantTransactionId) {
|
||||||
|
ElMessage.warning('请输入商户订单号')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
orderData.value = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await getOrderStatus(queryForm.merchantTransactionId)
|
||||||
|
if (response.code === '0000' && response.data) {
|
||||||
|
orderData.value = response.data
|
||||||
|
ElMessage.success('查询成功')
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.message || '订单不存在')
|
||||||
|
orderData.value = null
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('查询订单失败:', error)
|
||||||
|
ElMessage.error('查询失败,请稍后重试')
|
||||||
|
orderData.value = null
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const goToPay = () => {
|
||||||
|
if (orderData.value && orderData.value.token) {
|
||||||
|
router.push({
|
||||||
|
path: '/checkout',
|
||||||
|
query: { token: orderData.value.token }
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
ElMessage.warning('该订单无法继续支付,请创建新订单')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToCreate = () => {
|
||||||
|
router.push('/')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.order-query {
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-form {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-detail {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons .el-button {
|
||||||
|
margin: 0 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
Reference in New Issue
Block a user