Compare commits
6 Commits
ae0b5f27be
...
f9b103426c
| Author | SHA1 | Date | |
|---|---|---|---|
| f9b103426c | |||
| f440ce2ade | |||
| 0cfe1e6942 | |||
| ed745ee6a5 | |||
| 3e1d77988d | |||
| 57d9c03332 |
130
PRODUCT_DETAIL.md
Normal file
130
PRODUCT_DETAIL.md
Normal file
@@ -0,0 +1,130 @@
|
||||
# 商品详情页说明
|
||||
|
||||
## 页面功能
|
||||
|
||||
### 1. 商品展示区域
|
||||
- **左侧大图**:支持点击放大预览
|
||||
- **缩略图列表**:点击切换主图
|
||||
- **图片预览**:Element Plus图片预览功能
|
||||
|
||||
### 2. 商品信息区域
|
||||
- **商品标题**:大字体,醒目展示
|
||||
- **商品副标题**:补充说明信息
|
||||
- **价格展示**:
|
||||
- 现价(大字体,红色高亮)
|
||||
- 原价(删除线,灰色)
|
||||
- 节省金额提示
|
||||
|
||||
### 3. 规格选择
|
||||
- **多规格支持**:颜色、尺寸等
|
||||
- **可视化选择**:标签式选择,选中高亮
|
||||
- **必选验证**:支付前验证所有规格已选择
|
||||
|
||||
### 4. 数量选择
|
||||
- **数量输入框**:支持增减
|
||||
- **库存显示**:实时显示库存信息
|
||||
- **库存限制**:超过库存时禁用
|
||||
|
||||
### 5. 服务保障
|
||||
- **7天无理由退货**
|
||||
- **正品保证**
|
||||
- **极速发货**
|
||||
|
||||
### 6. 操作按钮
|
||||
- **立即购买**:橙色渐变按钮
|
||||
- **立即支付**:红色渐变按钮(主要支付按钮)
|
||||
- **悬停效果**:按钮有阴影和上浮动画
|
||||
|
||||
### 7. 商品详情标签页
|
||||
- **商品详情**:HTML格式的商品描述
|
||||
- **规格参数**:表格形式展示参数
|
||||
- **用户评价**:评价列表展示
|
||||
|
||||
## 样式特点
|
||||
|
||||
### 参考主流电商设计
|
||||
1. **价格突出**:大字体、红色、渐变背景
|
||||
2. **图片展示**:大图+缩略图,支持预览
|
||||
3. **按钮设计**:渐变背景、阴影效果、悬停动画
|
||||
4. **卡片布局**:圆角、阴影、层次分明
|
||||
5. **响应式设计**:移动端自适应
|
||||
|
||||
### 颜色方案
|
||||
- **主色调**:红色系(#f56c6c, #ff4757)
|
||||
- **辅助色**:橙色系(#ff9500, #ff6b00)
|
||||
- **背景色**:浅灰色(#f5f7fa)
|
||||
- **文字色**:深灰色(#333, #666)
|
||||
|
||||
## 支付流程
|
||||
|
||||
1. 用户选择商品规格和数量
|
||||
2. 点击"立即支付"按钮
|
||||
3. 系统自动生成订单号
|
||||
4. 调用后端API创建支付订单
|
||||
5. 跳转到收银台页面完成支付
|
||||
|
||||
## 数据格式
|
||||
|
||||
### 商品数据结构
|
||||
```javascript
|
||||
{
|
||||
id: '1',
|
||||
name: '商品名称',
|
||||
subtitle: '商品副标题',
|
||||
price: 20.00,
|
||||
originalPrice: 25.00,
|
||||
currency: 'USD',
|
||||
stock: 100,
|
||||
images: ['图片URL数组'],
|
||||
specs: [
|
||||
{
|
||||
name: '规格名称',
|
||||
options: [
|
||||
{ label: '选项标签', value: '选项值' }
|
||||
]
|
||||
}
|
||||
],
|
||||
description: 'HTML格式的商品描述',
|
||||
params: [
|
||||
{ name: '参数名', value: '参数值' }
|
||||
],
|
||||
reviews: [
|
||||
{
|
||||
user: '用户名',
|
||||
rating: 5,
|
||||
date: '日期',
|
||||
content: '评价内容'
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 路由配置
|
||||
|
||||
- **路径**:`/` 或 `/product/:id`
|
||||
- **组件**:`ProductDetail.vue`
|
||||
- **参数**:`id` - 商品ID(可选)
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 访问商品详情页
|
||||
```
|
||||
http://localhost:3000/
|
||||
http://localhost:3000/product/1
|
||||
```
|
||||
|
||||
### 从其他页面跳转
|
||||
```javascript
|
||||
router.push('/product/1')
|
||||
router.push({ path: '/product', params: { id: '1' } })
|
||||
```
|
||||
|
||||
## 扩展建议
|
||||
|
||||
1. **商品列表页**:创建商品列表,点击跳转到详情页
|
||||
2. **购物车功能**:添加购物车按钮和功能
|
||||
3. **商品推荐**:在详情页下方展示相关商品
|
||||
4. **图片轮播**:主图支持自动轮播
|
||||
5. **视频展示**:支持商品视频播放
|
||||
6. **分享功能**:添加商品分享功能
|
||||
|
||||
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
84
src/App.vue
Normal file
84
src/App.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<!-- 客户页面:商品详情页,不显示管理导航 -->
|
||||
<div v-if="isCustomerPage" class="customer-layout">
|
||||
<router-view />
|
||||
</div>
|
||||
|
||||
<!-- 管理页面:显示完整导航 -->
|
||||
<el-container v-else>
|
||||
<el-header>
|
||||
<div class="header-content">
|
||||
<h1>MT Pay 管理系统</h1>
|
||||
<el-menu
|
||||
mode="horizontal"
|
||||
:default-active="activeIndex"
|
||||
router
|
||||
>
|
||||
<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>
|
||||
</el-menu>
|
||||
</div>
|
||||
</el-header>
|
||||
<el-main>
|
||||
<router-view />
|
||||
</el-main>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
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>
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#app {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
.el-header {
|
||||
background-color: #409eff;
|
||||
color: white;
|
||||
line-height: 60px;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-content h1 {
|
||||
font-size: 20px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.el-main {
|
||||
padding: 20px;
|
||||
min-height: calc(100vh - 60px);
|
||||
}
|
||||
|
||||
/* 客户页面布局:全屏显示,无导航栏 */
|
||||
.customer-layout {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
</style>
|
||||
|
||||
33
src/api/order.js
Normal file
33
src/api/order.js
Normal 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'
|
||||
})
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
39
src/components/PageHeader.vue
Normal file
39
src/components/PageHeader.vue
Normal 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>
|
||||
|
||||
24
src/config/index.js
Normal file
24
src/config/index.js
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* 配置文件
|
||||
*/
|
||||
|
||||
export default {
|
||||
// API基础URL
|
||||
apiBaseUrl: import.meta.env.VITE_API_BASE_URL || '/api',
|
||||
|
||||
// PingPong SDK模式
|
||||
pingpongMode: import.meta.env.VITE_PINGPONG_MODE || 'sandbox',
|
||||
|
||||
// PingPong SDK URL
|
||||
pingpongSdkUrl: 'https://pay-cdn.pingpongx.com/production/static/sdk/1.2.0/ppPay.min.js',
|
||||
|
||||
// 请求超时时间(毫秒)
|
||||
requestTimeout: 30000,
|
||||
|
||||
// 分页配置
|
||||
pagination: {
|
||||
pageSize: 10,
|
||||
pageSizes: [10, 20, 50, 100]
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
78
src/router/index.js
Normal file
78
src/router/index.js
Normal file
@@ -0,0 +1,78 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import CreateOrder from '../views/CreateOrder.vue'
|
||||
import Checkout from '../views/Checkout.vue'
|
||||
import PaymentResult from '../views/PaymentResult.vue'
|
||||
import OrderQuery from '../views/OrderQuery.vue'
|
||||
import ProductDetail from '../views/ProductDetail.vue'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
redirect: '/manage/product'
|
||||
},
|
||||
{
|
||||
path: '/product/:id',
|
||||
name: 'ProductDetail',
|
||||
component: ProductDetail,
|
||||
// 支持商品ID(数字)或链接码(32位字符串)
|
||||
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',
|
||||
component: Checkout
|
||||
},
|
||||
{
|
||||
path: '/result',
|
||||
name: 'PaymentResult',
|
||||
component: PaymentResult
|
||||
},
|
||||
{
|
||||
path: '/query',
|
||||
name: 'OrderQuery',
|
||||
component: OrderQuery
|
||||
},
|
||||
{
|
||||
path: '/manage/product',
|
||||
name: 'ProductManage',
|
||||
component: () => import('../views/ProductManage.vue')
|
||||
},
|
||||
{
|
||||
path: '/manage/product/create',
|
||||
name: 'ProductCreate',
|
||||
component: () => import('../views/ProductCreate.vue')
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
// 添加路由守卫,用于调试
|
||||
router.beforeEach((to, from, next) => {
|
||||
console.log('路由导航:', from.path, '->', to.path)
|
||||
next()
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
42
src/store/index.js
Normal file
42
src/store/index.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import { reactive } from 'vue'
|
||||
|
||||
/**
|
||||
* 简单的状态管理
|
||||
* 如果需要更复杂的状态管理,可以使用 Pinia
|
||||
*/
|
||||
|
||||
const state = reactive({
|
||||
// 用户信息
|
||||
user: null,
|
||||
// 当前订单
|
||||
currentOrder: null,
|
||||
// 加载状态
|
||||
loading: false
|
||||
})
|
||||
|
||||
export default {
|
||||
state,
|
||||
|
||||
// 设置用户信息
|
||||
setUser(user) {
|
||||
state.user = user
|
||||
},
|
||||
|
||||
// 设置当前订单
|
||||
setCurrentOrder(order) {
|
||||
state.currentOrder = order
|
||||
},
|
||||
|
||||
// 设置加载状态
|
||||
setLoading(loading) {
|
||||
state.loading = loading
|
||||
},
|
||||
|
||||
// 清除状态
|
||||
clear() {
|
||||
state.user = null
|
||||
state.currentOrder = null
|
||||
state.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
75
src/utils/constants.js
Normal file
75
src/utils/constants.js
Normal file
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* 常量定义
|
||||
*/
|
||||
|
||||
// 订单状态
|
||||
export const ORDER_STATUS = {
|
||||
PENDING: 'PENDING',
|
||||
SUCCESS: 'SUCCESS',
|
||||
FAILED: 'FAILED',
|
||||
REVIEW: 'REVIEW',
|
||||
CANCELLED: 'CANCELLED'
|
||||
}
|
||||
|
||||
// 订单状态文本
|
||||
export const ORDER_STATUS_TEXT = {
|
||||
[ORDER_STATUS.PENDING]: '待支付',
|
||||
[ORDER_STATUS.SUCCESS]: '支付成功',
|
||||
[ORDER_STATUS.FAILED]: '支付失败',
|
||||
[ORDER_STATUS.REVIEW]: '审核中',
|
||||
[ORDER_STATUS.CANCELLED]: '已取消'
|
||||
}
|
||||
|
||||
// 支付类型
|
||||
export const PAYMENT_TYPE = {
|
||||
SALE: 'SALE',
|
||||
AUTH: 'AUTH'
|
||||
}
|
||||
|
||||
// 支付类型文本
|
||||
export const PAYMENT_TYPE_TEXT = {
|
||||
[PAYMENT_TYPE.SALE]: '直接付款',
|
||||
[PAYMENT_TYPE.AUTH]: '预授权'
|
||||
}
|
||||
|
||||
// 币种
|
||||
export const CURRENCY = {
|
||||
USD: 'USD',
|
||||
EUR: 'EUR',
|
||||
GBP: 'GBP',
|
||||
CNY: 'CNY',
|
||||
JPY: 'JPY'
|
||||
}
|
||||
|
||||
// 币种文本
|
||||
export const CURRENCY_TEXT = {
|
||||
[CURRENCY.USD]: '美元',
|
||||
[CURRENCY.EUR]: '欧元',
|
||||
[CURRENCY.GBP]: '英镑',
|
||||
[CURRENCY.CNY]: '人民币',
|
||||
[CURRENCY.JPY]: '日元'
|
||||
}
|
||||
|
||||
// 响应码
|
||||
export const RESPONSE_CODE = {
|
||||
SUCCESS: '0000',
|
||||
FAIL: '9999',
|
||||
PARAM_ERROR: '4000',
|
||||
VALIDATION_ERROR: '4001',
|
||||
ORDER_NOT_FOUND: '1001',
|
||||
ORDER_EXISTS: '1002'
|
||||
}
|
||||
|
||||
// API基础路径
|
||||
export const API_BASE_URL = '/api'
|
||||
|
||||
// PingPong SDK URL
|
||||
export const PINGPONG_SDK_URL = 'https://pay-cdn.pingpongx.com/production/static/sdk/1.2.0/ppPay.min.js'
|
||||
|
||||
// PingPong SDK模式
|
||||
export const PINGPONG_MODE = {
|
||||
SANDBOX: 'sandbox',
|
||||
TEST: 'test',
|
||||
BUILD: 'build'
|
||||
}
|
||||
|
||||
123
src/utils/helpers.js
Normal file
123
src/utils/helpers.js
Normal file
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* 工具函数
|
||||
*/
|
||||
|
||||
/**
|
||||
* 格式化金额
|
||||
*/
|
||||
export function formatAmount(amount, currency = 'USD') {
|
||||
if (!amount) return '0.00'
|
||||
return parseFloat(amount).toFixed(2)
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期时间
|
||||
*/
|
||||
export function formatDateTime(dateTime, format = 'YYYY-MM-DD HH:mm:ss') {
|
||||
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 format
|
||||
.replace('YYYY', year)
|
||||
.replace('MM', month)
|
||||
.replace('DD', day)
|
||||
.replace('HH', hours)
|
||||
.replace('mm', minutes)
|
||||
.replace('ss', seconds)
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成订单号
|
||||
*/
|
||||
export function generateOrderId() {
|
||||
const timestamp = Date.now()
|
||||
const random = Math.floor(Math.random() * 10000)
|
||||
return `MTN${timestamp}${random.toString().padStart(4, '0')}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取订单状态标签类型
|
||||
*/
|
||||
export function getStatusTagType(status) {
|
||||
if (!status) return 'info'
|
||||
const statusUpper = status.toUpperCase()
|
||||
if (statusUpper === 'SUCCESS' || statusUpper === 'SUCCESSFUL') {
|
||||
return 'success'
|
||||
} else if (statusUpper === 'FAILED' || statusUpper === 'FAILURE') {
|
||||
return 'danger'
|
||||
} else if (statusUpper === 'REVIEW') {
|
||||
return 'warning'
|
||||
}
|
||||
return 'info'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取订单状态文本
|
||||
*/
|
||||
export function getStatusText(status) {
|
||||
if (!status) return '未知'
|
||||
const statusMap = {
|
||||
'PENDING': '待支付',
|
||||
'SUCCESS': '支付成功',
|
||||
'SUCCESSFUL': '支付成功',
|
||||
'FAILED': '支付失败',
|
||||
'FAILURE': '支付失败',
|
||||
'REVIEW': '审核中',
|
||||
'CANCELLED': '已取消',
|
||||
'CANCEL': '已取消'
|
||||
}
|
||||
return statusMap[status.toUpperCase()] || status
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证邮箱
|
||||
*/
|
||||
export function validateEmail(email) {
|
||||
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
return re.test(email)
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证手机号(简单验证)
|
||||
*/
|
||||
export function validatePhone(phone) {
|
||||
const re = /^1[3-9]\d{9}$/
|
||||
return re.test(phone)
|
||||
}
|
||||
|
||||
/**
|
||||
* 防抖函数
|
||||
*/
|
||||
export function debounce(func, wait) {
|
||||
let timeout
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout)
|
||||
func(...args)
|
||||
}
|
||||
clearTimeout(timeout)
|
||||
timeout = setTimeout(later, wait)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 节流函数
|
||||
*/
|
||||
export function throttle(func, limit) {
|
||||
let inThrottle
|
||||
return function(...args) {
|
||||
if (!inThrottle) {
|
||||
func.apply(this, args)
|
||||
inThrottle = true
|
||||
setTimeout(() => inThrottle = false, limit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
139
src/views/Checkout.vue
Normal file
139
src/views/Checkout.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<div class="checkout">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>PingPong支付收银台</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div id="ufo-container" class="checkout-container"></div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import config from '../config'
|
||||
|
||||
const route = useRoute()
|
||||
const token = route.query.token
|
||||
|
||||
onMounted(() => {
|
||||
if (!token) {
|
||||
ElMessage.error('缺少支付token,请重新创建订单')
|
||||
return
|
||||
}
|
||||
|
||||
// 动态加载PingPong SDK
|
||||
loadPingPongSDK()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// 清理资源
|
||||
})
|
||||
|
||||
const loadPingPongSDK = () => {
|
||||
// 检查SDK是否已加载
|
||||
if (window.ppPay) {
|
||||
initPingPongPay()
|
||||
return
|
||||
}
|
||||
|
||||
// 加载SDK
|
||||
const script = document.createElement('script')
|
||||
script.src = config.pingpongSdkUrl
|
||||
script.onload = () => {
|
||||
initPingPongPay()
|
||||
}
|
||||
script.onerror = () => {
|
||||
ElMessage.error('加载支付SDK失败,请刷新页面重试')
|
||||
}
|
||||
document.head.appendChild(script)
|
||||
}
|
||||
|
||||
const initPingPongPay = () => {
|
||||
try {
|
||||
const client = new window.ppPay({
|
||||
lang: 'zh',
|
||||
root: '#ufo-container',
|
||||
manul: false,
|
||||
located: true,
|
||||
showPrice: true,
|
||||
bill: true,
|
||||
mode: config.pingpongMode, // 根据环境配置:sandbox/test/build
|
||||
menu: false,
|
||||
base: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
fontSize: '14px',
|
||||
backgroundColor: '#fff',
|
||||
showHeader: true,
|
||||
showHeaderLabel: true,
|
||||
headerLabelFont: '支付',
|
||||
headerColor: '#333333',
|
||||
headerSize: '16px',
|
||||
headerBackgroundColor: '#fff',
|
||||
headerPadding: '20px',
|
||||
btnSize: '100%',
|
||||
btnColor: '#fff',
|
||||
btnFontSize: '14px',
|
||||
btnPaddingX: '20px',
|
||||
btnPaddingY: '10px',
|
||||
btnBackgroundColor: '#1fa0e8',
|
||||
btnBorderRadius: '4px',
|
||||
btnMarginTop: '20px'
|
||||
}
|
||||
})
|
||||
|
||||
const sdkConfig = {
|
||||
token: token
|
||||
}
|
||||
|
||||
client.createPayment(sdkConfig)
|
||||
|
||||
// 调整容器大小
|
||||
adjustContainerSize()
|
||||
window.addEventListener('resize', adjustContainerSize)
|
||||
} catch (error) {
|
||||
console.error('初始化支付失败:', error)
|
||||
ElMessage.error('初始化支付失败,请刷新页面重试')
|
||||
}
|
||||
}
|
||||
|
||||
const adjustContainerSize = () => {
|
||||
const container = document.getElementById('ufo-container')
|
||||
if (!container) return
|
||||
|
||||
const winWidth = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth
|
||||
|
||||
if (winWidth >= 500) {
|
||||
const clientW = Math.floor(winWidth / 3)
|
||||
container.style.width = (clientW >= 500 ? clientW : 500) + 'px'
|
||||
container.style.margin = '0 auto'
|
||||
} else {
|
||||
container.style.width = winWidth + 'px'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.checkout {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.checkout-container {
|
||||
min-height: 600px;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
|
||||
413
src/views/CreateOrder.vue
Normal file
413
src/views/CreateOrder.vue
Normal file
@@ -0,0 +1,413 @@
|
||||
<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-sku" v-if="productInfo.sku">
|
||||
<span class="sku-label">SKU:</span>
|
||||
<span class="sku-value">{{ productInfo.sku }}</span>
|
||||
</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="120px"
|
||||
label-position="left"
|
||||
>
|
||||
<el-divider>客户信息</el-divider>
|
||||
|
||||
<el-form-item label="客户姓名" prop="customerName">
|
||||
<el-input
|
||||
v-model="form.customerName"
|
||||
placeholder="请输入客户姓名"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="客户电话" prop="customerPhone">
|
||||
<el-input
|
||||
v-model="form.customerPhone"
|
||||
placeholder="请输入客户电话"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="客户邮箱" prop="customerEmail">
|
||||
<el-input
|
||||
v-model="form.customerEmail"
|
||||
placeholder="请输入客户邮箱(可选)"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider>收货地址</el-divider>
|
||||
|
||||
<el-form-item label="收货人姓名" prop="shippingName">
|
||||
<el-input
|
||||
v-model="form.shippingName"
|
||||
placeholder="请输入收货人姓名"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="收货人电话" prop="shippingPhone">
|
||||
<el-input
|
||||
v-model="form.shippingPhone"
|
||||
placeholder="请输入收货人电话"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<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-form-item label="收货城市" prop="shippingCity">
|
||||
<el-input
|
||||
v-model="form.shippingCity"
|
||||
placeholder="请输入收货城市"
|
||||
style="width: 48%"
|
||||
clearable
|
||||
/>
|
||||
<el-input
|
||||
v-model="form.shippingState"
|
||||
placeholder="州/省(可选)"
|
||||
style="width: 48%; margin-left: 4%"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="街道地址" prop="shippingStreet">
|
||||
<el-input
|
||||
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-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" size="large" @click="submitForm" :loading="loading" style="width: 200px">
|
||||
提交订单
|
||||
</el-button>
|
||||
<el-button @click="goBack" style="margin-left: 10px">返回</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { createCustomerOrder } from '../api/order'
|
||||
import { formatAmount } from '../utils/helpers'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const formRef = ref()
|
||||
const loading = ref(false)
|
||||
const productInfo = ref(null)
|
||||
|
||||
const form = reactive({
|
||||
customerName: '',
|
||||
customerPhone: '',
|
||||
customerEmail: '',
|
||||
shippingName: '',
|
||||
shippingPhone: '',
|
||||
shippingCountry: '',
|
||||
shippingState: '',
|
||||
shippingCity: '',
|
||||
shippingStreet: '',
|
||||
shippingPostcode: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const rules = {
|
||||
customerName: [
|
||||
{ required: true, message: '请输入客户姓名', trigger: 'blur' }
|
||||
],
|
||||
customerPhone: [
|
||||
{ required: true, message: '请输入客户电话', trigger: 'blur' },
|
||||
{ pattern: /^[0-9+\-\s()]+$/, message: '请输入有效的电话号码', trigger: 'blur' }
|
||||
],
|
||||
customerEmail: [
|
||||
{ type: 'email', message: '请输入有效的邮箱地址', trigger: 'blur' }
|
||||
],
|
||||
shippingName: [
|
||||
{ required: true, message: '请输入收货人姓名', trigger: 'blur' }
|
||||
],
|
||||
shippingPhone: [
|
||||
{ required: true, message: '请输入收货人电话', trigger: 'blur' },
|
||||
{ pattern: /^[0-9+\-\s()]+$/, message: '请输入有效的电话号码', trigger: 'blur' }
|
||||
],
|
||||
shippingCountry: [
|
||||
{ required: true, message: '请选择收货国家', trigger: 'change' }
|
||||
],
|
||||
shippingCity: [
|
||||
{ required: true, message: '请输入收货城市', trigger: 'blur' }
|
||||
],
|
||||
shippingStreet: [
|
||||
{ required: true, message: '请输入街道地址', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
const goBack = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
// 提交订单
|
||||
const submitForm = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (!valid) {
|
||||
ElMessage.error('请填写完整信息')
|
||||
return
|
||||
}
|
||||
|
||||
if (!productInfo.value) {
|
||||
ElMessage.error('商品信息缺失,请重新选择商品')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
// 构建订单请求数据
|
||||
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 createCustomerOrder(orderData)
|
||||
|
||||
if (response.code === '0000' && response.data) {
|
||||
ElMessage.success('订单创建成功')
|
||||
// 跳转到订单确认页面
|
||||
router.push({
|
||||
path: '/order/confirm',
|
||||
query: { orderNo: response.data.orderNo }
|
||||
})
|
||||
} else {
|
||||
ElMessage.error(response.message || '创建订单失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建订单失败:', error)
|
||||
ElMessage.error(error.response?.data?.message || '创建订单失败,请稍后重试')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 格式化价格
|
||||
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
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('解析商品信息失败:', error)
|
||||
ElMessage.error('商品信息解析失败')
|
||||
router.push('/')
|
||||
}
|
||||
} else {
|
||||
ElMessage.error('缺少商品信息')
|
||||
router.push('/')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.create-order {
|
||||
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;
|
||||
}
|
||||
|
||||
/* 商品信息卡片 */
|
||||
.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-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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.total-label {
|
||||
margin-left: auto;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.total-value {
|
||||
font-size: 18px;
|
||||
color: #f56c6c;
|
||||
font-weight: 700;
|
||||
}
|
||||
</style>
|
||||
59
src/views/Home.vue
Normal file
59
src/views/Home.vue
Normal 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
330
src/views/OrderConfirm.vue
Normal 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>
|
||||
|
||||
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>
|
||||
|
||||
1424
src/views/ProductCreate.vue
Normal file
1424
src/views/ProductCreate.vue
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user