63 KiB
EPay Go 重构 - 阶段四:Vue3 前端
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: 实现管理后台、商户中心、收银台三个前端模块,完成完整的用户界面。
Architecture: 使用 Vue3 + TypeScript + Arco Design,Pinia 状态管理,Vue Router 路由,Axios 请求封装。采用模块化目录结构,按功能划分页面。
Tech Stack: Vue3, TypeScript, Vite, Arco Design, Pinia, Vue Router, Axios
前置条件: 阶段一至三已完成,后端 API 已就绪。
Task 1: 配置项目基础结构
Files:
- Modify:
epay-go/web/vite.config.ts - Modify:
epay-go/web/src/main.ts - Create:
epay-go/web/src/env.d.ts
Step 1: 更新 Vite 配置
// web/vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
},
},
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
},
'/admin': {
target: 'http://localhost:8080',
changeOrigin: true,
},
'/merchant': {
target: 'http://localhost:8080',
changeOrigin: true,
},
},
},
})
Step 2: 更新入口文件
// web/src/main.ts
import { createApp } from 'vue'
import ArcoVue from '@arco-design/web-vue'
import ArcoVueIcon from '@arco-design/web-vue/es/icon'
import '@arco-design/web-vue/dist/arco.css'
import App from './App.vue'
import router from './router'
import { createPinia } from 'pinia'
const app = createApp(App)
app.use(ArcoVue)
app.use(ArcoVueIcon)
app.use(createPinia())
app.use(router)
app.mount('#app')
Step 3: 创建类型声明文件
// web/src/env.d.ts
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
interface ImportMetaEnv {
readonly VITE_API_BASE_URL: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
Step 4: 安装额外依赖
cd d:/project/payment/epay-go/web
npm install pinia vue-router@4 axios
npm install -D @types/node
Step 5: 提交
cd d:/project/payment/epay-go
git add web/
git commit -m "feat(web): configure vite, install dependencies"
Task 2: 创建 API 请求封装
Files:
- Create:
epay-go/web/src/api/request.ts - Create:
epay-go/web/src/api/types.ts
Step 1: 创建请求封装
// web/src/api/request.ts
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import { Message } from '@arco-design/web-vue'
const request: AxiosInstance = axios.create({
baseURL: '',
timeout: 30000,
})
// 请求拦截器
request.interceptors.request.use(
(config) => {
// 从 localStorage 获取 token
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)
// 响应拦截器
request.interceptors.response.use(
(response: AxiosResponse) => {
const { data } = response
if (data.code === 0) {
return data
}
// 业务错误
Message.error(data.msg || '请求失败')
return Promise.reject(new Error(data.msg))
},
(error) => {
if (error.response) {
const { status, data } = error.response
if (status === 401) {
Message.error('登录已过期,请重新登录')
localStorage.removeItem('token')
window.location.href = '/login'
} else if (status === 403) {
Message.error('无权访问')
} else {
Message.error(data?.msg || '服务器错误')
}
} else {
Message.error('网络错误')
}
return Promise.reject(error)
}
)
export default request
Step 2: 创建类型定义
// web/src/api/types.ts
// 通用响应
export interface ApiResponse<T = any> {
code: number
msg: string
data: T
}
// 分页响应
export interface PageData<T> {
list: T[]
total: number
page: number
page_size: number
}
// 商户
export interface Merchant {
id: number
username: string
email: string
phone: string
api_key: string
balance: string
frozen_balance: string
status: number
created_at: string
}
// 订单
export interface Order {
id: number
trade_no: string
out_trade_no: string
merchant_id: number
channel_id: number
pay_type: string
amount: string
real_amount: string
fee: string
name: string
status: number
notify_status: number
paid_at: string | null
created_at: string
}
// 通道
export interface Channel {
id: number
name: string
plugin: string
pay_types: string
config: any
rate: string
daily_limit: string
status: number
sort: number
}
// 结算
export interface Settlement {
id: number
settle_no: string
merchant_id: number
amount: string
fee: string
actual_amount: string
account_type: string
account_no: string
account_name: string
status: number
remark: string
created_at: string
}
// 资金记录
export interface BalanceRecord {
id: number
merchant_id: number
action: number
amount: string
before_balance: string
after_balance: string
type: string
trade_no: string
created_at: string
}
Step 3: 提交
git add web/src/api/
git commit -m "feat(web): add axios request wrapper and types"
Task 3: 创建管理后台 API
Files:
- Create:
epay-go/web/src/api/admin.ts
Step 1: 创建管理后台 API
// web/src/api/admin.ts
import request from './request'
import type { ApiResponse, PageData, Merchant, Order, Channel, Settlement } from './types'
// 登录
export function adminLogin(data: { username: string; password: string }) {
return request.post<any, ApiResponse>('/admin/auth/login', data)
}
// 仪表盘
export function getDashboard() {
return request.get<any, ApiResponse>('/admin/dashboard')
}
// 商户列表
export function getMerchants(params: { page: number; page_size: number; status?: number }) {
return request.get<any, ApiResponse<PageData<Merchant>>>('/admin/merchants', { params })
}
// 商户详情
export function getMerchant(id: number) {
return request.get<any, ApiResponse<Merchant>>(`/admin/merchants/${id}`)
}
// 更新商户状态
export function updateMerchantStatus(id: number, status: number) {
return request.patch<any, ApiResponse>(`/admin/merchants/${id}/status`, { status })
}
// 订单列表
export function getOrders(params: { page: number; page_size: number; merchant_id?: number; status?: number }) {
return request.get<any, ApiResponse<PageData<Order>>>('/admin/orders', { params })
}
// 订单详情
export function getOrder(tradeNo: string) {
return request.get<any, ApiResponse<Order>>(`/admin/orders/${tradeNo}`)
}
// 重发通知
export function renotifyOrder(tradeNo: string) {
return request.post<any, ApiResponse>(`/admin/orders/${tradeNo}/renotify`)
}
// 通道列表
export function getChannels(params: { page: number; page_size: number }) {
return request.get<any, ApiResponse<PageData<Channel>>>('/admin/channels', { params })
}
// 创建通道
export function createChannel(data: Partial<Channel>) {
return request.post<any, ApiResponse>('/admin/channels', data)
}
// 更新通道
export function updateChannel(id: number, data: Partial<Channel>) {
return request.put<any, ApiResponse>(`/admin/channels/${id}`, data)
}
// 删除通道
export function deleteChannel(id: number) {
return request.delete<any, ApiResponse>(`/admin/channels/${id}`)
}
// 结算列表
export function getSettlements(params: { page: number; page_size: number; merchant_id?: number; status?: number }) {
return request.get<any, ApiResponse<PageData<Settlement>>>('/admin/settlements', { params })
}
// 审核通过
export function approveSettlement(id: number) {
return request.patch<any, ApiResponse>(`/admin/settlements/${id}/approve`)
}
// 驳回
export function rejectSettlement(id: number, remark: string) {
return request.patch<any, ApiResponse>(`/admin/settlements/${id}/reject`, { remark })
}
Step 2: 提交
git add web/src/api/admin.ts
git commit -m "feat(web): add admin api"
Task 4: 创建商户端 API
Files:
- Create:
epay-go/web/src/api/merchant.ts
Step 1: 创建商户端 API
// web/src/api/merchant.ts
import request from './request'
import type { ApiResponse, PageData, Merchant, Order, Settlement, BalanceRecord } from './types'
// 注册
export function register(data: { username: string; password: string; email?: string; phone?: string }) {
return request.post<any, ApiResponse>('/merchant/auth/register', data)
}
// 登录
export function login(data: { username: string; password: string }) {
return request.post<any, ApiResponse>('/merchant/auth/login', data)
}
// 仪表盘
export function getDashboard() {
return request.get<any, ApiResponse>('/merchant/dashboard')
}
// 获取个人信息
export function getProfile() {
return request.get<any, ApiResponse<Merchant>>('/merchant/profile')
}
// 更新个人信息
export function updateProfile(data: { email?: string; phone?: string }) {
return request.put<any, ApiResponse>('/merchant/profile', data)
}
// 修改密码
export function updatePassword(data: { old_password: string; new_password: string }) {
return request.put<any, ApiResponse>('/merchant/profile/password', data)
}
// 重置API密钥
export function resetApiKey() {
return request.post<any, ApiResponse<{ api_key: string }>>('/merchant/profile/reset-key')
}
// 订单列表
export function getOrders(params: { page: number; page_size: number; status?: number }) {
return request.get<any, ApiResponse<PageData<Order>>>('/merchant/orders', { params })
}
// 订单详情
export function getOrder(tradeNo: string) {
return request.get<any, ApiResponse<Order>>(`/merchant/orders/${tradeNo}`)
}
// 结算列表
export function getSettlements(params: { page: number; page_size: number; status?: number }) {
return request.get<any, ApiResponse<PageData<Settlement>>>('/merchant/settlements', { params })
}
// 申请结算
export function applySettlement(data: {
amount: string
account_type: string
account_no: string
account_name: string
}) {
return request.post<any, ApiResponse>('/merchant/settlements', data)
}
// 资金记录
export function getRecords(params: { page: number; page_size: number }) {
return request.get<any, ApiResponse<PageData<BalanceRecord>>>('/merchant/records', { params })
}
Step 2: 提交
git add web/src/api/merchant.ts
git commit -m "feat(web): add merchant api"
Task 5: 创建路由配置
Files:
- Create:
epay-go/web/src/router/index.ts
Step 1: 创建路由配置
// web/src/router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
const routes: RouteRecordRaw[] = [
// 首页重定向
{
path: '/',
redirect: '/merchant/login',
},
// 管理后台
{
path: '/admin/login',
name: 'AdminLogin',
component: () => import('@/views/admin/Login.vue'),
meta: { title: '管理员登录' },
},
{
path: '/admin',
component: () => import('@/layouts/AdminLayout.vue'),
meta: { requiresAuth: true, authType: 'admin' },
children: [
{
path: '',
redirect: '/admin/dashboard',
},
{
path: 'dashboard',
name: 'AdminDashboard',
component: () => import('@/views/admin/Dashboard.vue'),
meta: { title: '仪表盘' },
},
{
path: 'merchants',
name: 'AdminMerchants',
component: () => import('@/views/admin/Merchants.vue'),
meta: { title: '商户管理' },
},
{
path: 'orders',
name: 'AdminOrders',
component: () => import('@/views/admin/Orders.vue'),
meta: { title: '订单管理' },
},
{
path: 'channels',
name: 'AdminChannels',
component: () => import('@/views/admin/Channels.vue'),
meta: { title: '通道管理' },
},
{
path: 'settlements',
name: 'AdminSettlements',
component: () => import('@/views/admin/Settlements.vue'),
meta: { title: '结算管理' },
},
],
},
// 商户中心
{
path: '/merchant/login',
name: 'MerchantLogin',
component: () => import('@/views/merchant/Login.vue'),
meta: { title: '商户登录' },
},
{
path: '/merchant/register',
name: 'MerchantRegister',
component: () => import('@/views/merchant/Register.vue'),
meta: { title: '商户注册' },
},
{
path: '/merchant',
component: () => import('@/layouts/MerchantLayout.vue'),
meta: { requiresAuth: true, authType: 'merchant' },
children: [
{
path: '',
redirect: '/merchant/dashboard',
},
{
path: 'dashboard',
name: 'MerchantDashboard',
component: () => import('@/views/merchant/Dashboard.vue'),
meta: { title: '仪表盘' },
},
{
path: 'orders',
name: 'MerchantOrders',
component: () => import('@/views/merchant/Orders.vue'),
meta: { title: '订单管理' },
},
{
path: 'settlements',
name: 'MerchantSettlements',
component: () => import('@/views/merchant/Settlements.vue'),
meta: { title: '结算管理' },
},
{
path: 'records',
name: 'MerchantRecords',
component: () => import('@/views/merchant/Records.vue'),
meta: { title: '资金记录' },
},
{
path: 'profile',
name: 'MerchantProfile',
component: () => import('@/views/merchant/Profile.vue'),
meta: { title: '个人信息' },
},
],
},
// 收银台
{
path: '/cashier/:tradeNo',
name: 'Cashier',
component: () => import('@/views/cashier/Index.vue'),
meta: { title: '收银台' },
},
// 404
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('@/views/NotFound.vue'),
},
]
const router = createRouter({
history: createWebHistory(),
routes,
})
// 路由守卫
router.beforeEach((to, from, next) => {
// 设置页面标题
document.title = (to.meta.title as string) || 'EPay'
if (to.meta.requiresAuth) {
const authType = to.meta.authType as string
const tokenKey = authType === 'admin' ? 'admin_token' : 'merchant_token'
const token = localStorage.getItem(tokenKey)
if (!token) {
const loginPath = authType === 'admin' ? '/admin/login' : '/merchant/login'
next({ path: loginPath, query: { redirect: to.fullPath } })
return
}
}
next()
})
export default router
Step 2: 提交
git add web/src/router/
git commit -m "feat(web): add vue router with admin and merchant routes"
Task 6: 创建布局组件
Files:
- Create:
epay-go/web/src/layouts/AdminLayout.vue - Create:
epay-go/web/src/layouts/MerchantLayout.vue
Step 1: 创建管理后台布局
<!-- web/src/layouts/AdminLayout.vue -->
<template>
<a-layout class="layout">
<a-layout-sider collapsible v-model:collapsed="collapsed" :width="220">
<div class="logo">
<span v-if="!collapsed">EPay 管理后台</span>
<span v-else>EP</span>
</div>
<a-menu
:selected-keys="[currentRoute]"
@menu-item-click="handleMenuClick"
>
<a-menu-item key="dashboard">
<template #icon><icon-dashboard /></template>
仪表盘
</a-menu-item>
<a-menu-item key="merchants">
<template #icon><icon-user-group /></template>
商户管理
</a-menu-item>
<a-menu-item key="orders">
<template #icon><icon-file /></template>
订单管理
</a-menu-item>
<a-menu-item key="channels">
<template #icon><icon-apps /></template>
通道管理
</a-menu-item>
<a-menu-item key="settlements">
<template #icon><icon-swap /></template>
结算管理
</a-menu-item>
</a-menu>
</a-layout-sider>
<a-layout>
<a-layout-header class="header">
<div class="header-right">
<a-dropdown>
<a-button type="text">
<icon-user /> 管理员
</a-button>
<template #content>
<a-doption @click="handleLogout">退出登录</a-doption>
</template>
</a-dropdown>
</div>
</a-layout-header>
<a-layout-content class="content">
<router-view />
</a-layout-content>
</a-layout>
</a-layout>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()
const collapsed = ref(false)
const currentRoute = computed(() => {
const path = route.path.split('/')[2] || 'dashboard'
return path
})
const handleMenuClick = (key: string) => {
router.push(`/admin/${key}`)
}
const handleLogout = () => {
localStorage.removeItem('admin_token')
router.push('/admin/login')
}
</script>
<style scoped>
.layout {
min-height: 100vh;
}
.logo {
height: 64px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 18px;
font-weight: bold;
background: rgba(255, 255, 255, 0.1);
}
.header {
background: #fff;
padding: 0 24px;
display: flex;
justify-content: flex-end;
align-items: center;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
}
.header-right {
display: flex;
align-items: center;
}
.content {
margin: 24px;
padding: 24px;
background: #fff;
min-height: calc(100vh - 64px - 48px);
}
</style>
Step 2: 创建商户中心布局
<!-- web/src/layouts/MerchantLayout.vue -->
<template>
<a-layout class="layout">
<a-layout-sider collapsible v-model:collapsed="collapsed" :width="220">
<div class="logo">
<span v-if="!collapsed">EPay 商户中心</span>
<span v-else>EP</span>
</div>
<a-menu
:selected-keys="[currentRoute]"
@menu-item-click="handleMenuClick"
>
<a-menu-item key="dashboard">
<template #icon><icon-dashboard /></template>
仪表盘
</a-menu-item>
<a-menu-item key="orders">
<template #icon><icon-file /></template>
订单管理
</a-menu-item>
<a-menu-item key="settlements">
<template #icon><icon-swap /></template>
结算管理
</a-menu-item>
<a-menu-item key="records">
<template #icon><icon-history /></template>
资金记录
</a-menu-item>
<a-menu-item key="profile">
<template #icon><icon-settings /></template>
个人信息
</a-menu-item>
</a-menu>
</a-layout-sider>
<a-layout>
<a-layout-header class="header">
<div class="header-right">
<span class="balance">余额: ¥{{ balance }}</span>
<a-dropdown>
<a-button type="text">
<icon-user /> {{ username }}
</a-button>
<template #content>
<a-doption @click="handleLogout">退出登录</a-doption>
</template>
</a-dropdown>
</div>
</a-layout-header>
<a-layout-content class="content">
<router-view />
</a-layout-content>
</a-layout>
</a-layout>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { getProfile } from '@/api/merchant'
const router = useRouter()
const route = useRoute()
const collapsed = ref(false)
const username = ref('')
const balance = ref('0.00')
const currentRoute = computed(() => {
const path = route.path.split('/')[2] || 'dashboard'
return path
})
const handleMenuClick = (key: string) => {
router.push(`/merchant/${key}`)
}
const handleLogout = () => {
localStorage.removeItem('merchant_token')
router.push('/merchant/login')
}
onMounted(async () => {
try {
const res = await getProfile()
username.value = res.data.username
balance.value = res.data.balance
} catch (e) {
// ignore
}
})
</script>
<style scoped>
.layout {
min-height: 100vh;
}
.logo {
height: 64px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 18px;
font-weight: bold;
background: rgba(255, 255, 255, 0.1);
}
.header {
background: #fff;
padding: 0 24px;
display: flex;
justify-content: flex-end;
align-items: center;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
}
.header-right {
display: flex;
align-items: center;
gap: 16px;
}
.balance {
color: #165dff;
font-weight: 500;
}
.content {
margin: 24px;
padding: 24px;
background: #fff;
min-height: calc(100vh - 64px - 48px);
}
</style>
Step 3: 提交
git add web/src/layouts/
git commit -m "feat(web): add admin and merchant layout components"
Task 7: 创建登录页面
Files:
- Create:
epay-go/web/src/views/admin/Login.vue - Create:
epay-go/web/src/views/merchant/Login.vue - Create:
epay-go/web/src/views/merchant/Register.vue
Step 1: 创建管理员登录页
<!-- web/src/views/admin/Login.vue -->
<template>
<div class="login-container">
<div class="login-box">
<h2>EPay 管理后台</h2>
<a-form :model="form" @submit="handleSubmit" layout="vertical">
<a-form-item field="username" label="用户名" :rules="[{ required: true, message: '请输入用户名' }]">
<a-input v-model="form.username" placeholder="请输入用户名" size="large" />
</a-form-item>
<a-form-item field="password" label="密码" :rules="[{ required: true, message: '请输入密码' }]">
<a-input-password v-model="form.password" placeholder="请输入密码" size="large" />
</a-form-item>
<a-form-item>
<a-button type="primary" html-type="submit" long size="large" :loading="loading">
登录
</a-button>
</a-form-item>
</a-form>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { Message } from '@arco-design/web-vue'
import { adminLogin } from '@/api/admin'
const router = useRouter()
const route = useRoute()
const loading = ref(false)
const form = reactive({
username: '',
password: '',
})
const handleSubmit = async () => {
loading.value = true
try {
const res = await adminLogin(form)
localStorage.setItem('admin_token', res.data.token)
localStorage.setItem('token', res.data.token)
Message.success('登录成功')
const redirect = route.query.redirect as string || '/admin/dashboard'
router.push(redirect)
} catch (e) {
// error handled by interceptor
} finally {
loading.value = false
}
}
</script>
<style scoped>
.login-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.login-box {
width: 400px;
padding: 40px;
background: #fff;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
}
.login-box h2 {
text-align: center;
margin-bottom: 32px;
color: #1d2129;
}
</style>
Step 2: 创建商户登录页
<!-- web/src/views/merchant/Login.vue -->
<template>
<div class="login-container">
<div class="login-box">
<h2>EPay 商户中心</h2>
<a-form :model="form" @submit="handleSubmit" layout="vertical">
<a-form-item field="username" label="用户名" :rules="[{ required: true, message: '请输入用户名' }]">
<a-input v-model="form.username" placeholder="请输入用户名" size="large" />
</a-form-item>
<a-form-item field="password" label="密码" :rules="[{ required: true, message: '请输入密码' }]">
<a-input-password v-model="form.password" placeholder="请输入密码" size="large" />
</a-form-item>
<a-form-item>
<a-button type="primary" html-type="submit" long size="large" :loading="loading">
登录
</a-button>
</a-form-item>
<div class="register-link">
还没有账号?<router-link to="/merchant/register">立即注册</router-link>
</div>
</a-form>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { Message } from '@arco-design/web-vue'
import { login } from '@/api/merchant'
const router = useRouter()
const route = useRoute()
const loading = ref(false)
const form = reactive({
username: '',
password: '',
})
const handleSubmit = async () => {
loading.value = true
try {
const res = await login(form)
localStorage.setItem('merchant_token', res.data.token)
localStorage.setItem('token', res.data.token)
Message.success('登录成功')
const redirect = route.query.redirect as string || '/merchant/dashboard'
router.push(redirect)
} catch (e) {
// error handled by interceptor
} finally {
loading.value = false
}
}
</script>
<style scoped>
.login-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
}
.login-box {
width: 400px;
padding: 40px;
background: #fff;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
}
.login-box h2 {
text-align: center;
margin-bottom: 32px;
color: #1d2129;
}
.register-link {
text-align: center;
margin-top: 16px;
color: #86909c;
}
.register-link a {
color: #165dff;
}
</style>
Step 3: 创建商户注册页
<!-- web/src/views/merchant/Register.vue -->
<template>
<div class="login-container">
<div class="login-box">
<h2>商户注册</h2>
<a-form :model="form" @submit="handleSubmit" layout="vertical">
<a-form-item field="username" label="用户名" :rules="[{ required: true, message: '请输入用户名' }, { minLength: 4, message: '用户名至少4个字符' }]">
<a-input v-model="form.username" placeholder="请输入用户名" size="large" />
</a-form-item>
<a-form-item field="password" label="密码" :rules="[{ required: true, message: '请输入密码' }, { minLength: 6, message: '密码至少6个字符' }]">
<a-input-password v-model="form.password" placeholder="请输入密码" size="large" />
</a-form-item>
<a-form-item field="email" label="邮箱">
<a-input v-model="form.email" placeholder="请输入邮箱(选填)" size="large" />
</a-form-item>
<a-form-item field="phone" label="手机号">
<a-input v-model="form.phone" placeholder="请输入手机号(选填)" size="large" />
</a-form-item>
<a-form-item>
<a-button type="primary" html-type="submit" long size="large" :loading="loading">
注册
</a-button>
</a-form-item>
<div class="register-link">
已有账号?<router-link to="/merchant/login">立即登录</router-link>
</div>
</a-form>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { Message } from '@arco-design/web-vue'
import { register } from '@/api/merchant'
const router = useRouter()
const loading = ref(false)
const form = reactive({
username: '',
password: '',
email: '',
phone: '',
})
const handleSubmit = async () => {
loading.value = true
try {
const res = await register(form)
localStorage.setItem('merchant_token', res.data.token)
localStorage.setItem('token', res.data.token)
Message.success('注册成功')
router.push('/merchant/dashboard')
} catch (e) {
// error handled by interceptor
} finally {
loading.value = false
}
}
</script>
<style scoped>
.login-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
}
.login-box {
width: 400px;
padding: 40px;
background: #fff;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
}
.login-box h2 {
text-align: center;
margin-bottom: 32px;
color: #1d2129;
}
.register-link {
text-align: center;
margin-top: 16px;
color: #86909c;
}
.register-link a {
color: #165dff;
}
</style>
Step 4: 提交
git add web/src/views/
git commit -m "feat(web): add login and register pages"
Task 8: 创建管理后台页面
Files:
- Create:
epay-go/web/src/views/admin/Dashboard.vue - Create:
epay-go/web/src/views/admin/Merchants.vue - Create:
epay-go/web/src/views/admin/Orders.vue - Create:
epay-go/web/src/views/admin/Channels.vue - Create:
epay-go/web/src/views/admin/Settlements.vue
Step 1: 创建仪表盘页面
<!-- web/src/views/admin/Dashboard.vue -->
<template>
<div class="dashboard">
<a-row :gutter="24">
<a-col :span="8">
<a-card>
<a-statistic title="今日订单数" :value="stats.today_order_count" />
</a-card>
</a-col>
<a-col :span="8">
<a-card>
<a-statistic title="今日交易额" :value="stats.today_order_amount" :precision="2" prefix="¥" />
</a-card>
</a-col>
<a-col :span="8">
<a-card>
<a-statistic title="商户总数" :value="stats.total_merchants" />
</a-card>
</a-col>
</a-row>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { getDashboard } from '@/api/admin'
const stats = ref({
today_order_count: 0,
today_order_amount: '0.00',
total_merchants: 0,
})
onMounted(async () => {
try {
const res = await getDashboard()
stats.value = res.data
} catch (e) {
// ignore
}
})
</script>
<style scoped>
.dashboard {
padding: 0;
}
</style>
Step 2: 创建商户管理页面
<!-- web/src/views/admin/Merchants.vue -->
<template>
<div>
<a-table :data="merchants" :loading="loading" :pagination="pagination" @page-change="handlePageChange">
<template #columns>
<a-table-column title="ID" data-index="id" :width="80" />
<a-table-column title="用户名" data-index="username" />
<a-table-column title="邮箱" data-index="email" />
<a-table-column title="余额" data-index="balance">
<template #cell="{ record }">¥{{ record.balance }}</template>
</a-table-column>
<a-table-column title="状态" data-index="status" :width="100">
<template #cell="{ record }">
<a-tag :color="record.status === 1 ? 'green' : 'red'">
{{ record.status === 1 ? '正常' : '禁用' }}
</a-tag>
</template>
</a-table-column>
<a-table-column title="注册时间" data-index="created_at" :width="180" />
<a-table-column title="操作" :width="120">
<template #cell="{ record }">
<a-button type="text" size="small" @click="toggleStatus(record)">
{{ record.status === 1 ? '禁用' : '启用' }}
</a-button>
</template>
</a-table-column>
</template>
</a-table>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import { getMerchants, updateMerchantStatus } from '@/api/admin'
import type { Merchant } from '@/api/types'
const loading = ref(false)
const merchants = ref<Merchant[]>([])
const pagination = reactive({
current: 1,
pageSize: 20,
total: 0,
})
const fetchData = async () => {
loading.value = true
try {
const res = await getMerchants({ page: pagination.current, page_size: pagination.pageSize })
merchants.value = res.data.list
pagination.total = res.data.total
} catch (e) {
// ignore
} finally {
loading.value = false
}
}
const handlePageChange = (page: number) => {
pagination.current = page
fetchData()
}
const toggleStatus = async (record: Merchant) => {
const newStatus = record.status === 1 ? 0 : 1
try {
await updateMerchantStatus(record.id, newStatus)
Message.success('操作成功')
fetchData()
} catch (e) {
// ignore
}
}
onMounted(() => {
fetchData()
})
</script>
Step 3: 创建订单管理页面
<!-- web/src/views/admin/Orders.vue -->
<template>
<div>
<a-table :data="orders" :loading="loading" :pagination="pagination" @page-change="handlePageChange">
<template #columns>
<a-table-column title="订单号" data-index="trade_no" :width="200" />
<a-table-column title="商户订单号" data-index="out_trade_no" :width="180" />
<a-table-column title="金额" data-index="amount">
<template #cell="{ record }">¥{{ record.amount }}</template>
</a-table-column>
<a-table-column title="支付类型" data-index="pay_type" :width="100" />
<a-table-column title="状态" data-index="status" :width="100">
<template #cell="{ record }">
<a-tag :color="statusColor(record.status)">{{ statusText(record.status) }}</a-tag>
</template>
</a-table-column>
<a-table-column title="创建时间" data-index="created_at" :width="180" />
<a-table-column title="操作" :width="100">
<template #cell="{ record }">
<a-button type="text" size="small" @click="handleRenotify(record)" v-if="record.status === 1">
重发通知
</a-button>
</template>
</a-table-column>
</template>
</a-table>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import { getOrders, renotifyOrder } from '@/api/admin'
import type { Order } from '@/api/types'
const loading = ref(false)
const orders = ref<Order[]>([])
const pagination = reactive({
current: 1,
pageSize: 20,
total: 0,
})
const statusText = (status: number) => {
const map: Record<number, string> = { 0: '未支付', 1: '已支付', 2: '已退款' }
return map[status] || '未知'
}
const statusColor = (status: number) => {
const map: Record<number, string> = { 0: 'orange', 1: 'green', 2: 'red' }
return map[status] || 'gray'
}
const fetchData = async () => {
loading.value = true
try {
const res = await getOrders({ page: pagination.current, page_size: pagination.pageSize })
orders.value = res.data.list
pagination.total = res.data.total
} catch (e) {
// ignore
} finally {
loading.value = false
}
}
const handlePageChange = (page: number) => {
pagination.current = page
fetchData()
}
const handleRenotify = async (record: Order) => {
try {
await renotifyOrder(record.trade_no)
Message.success('通知已发送')
} catch (e) {
// ignore
}
}
onMounted(() => {
fetchData()
})
</script>
Step 4: 创建通道管理页面
<!-- web/src/views/admin/Channels.vue -->
<template>
<div>
<div style="margin-bottom: 16px;">
<a-button type="primary" @click="showCreateModal = true">新增通道</a-button>
</div>
<a-table :data="channels" :loading="loading" :pagination="pagination" @page-change="handlePageChange">
<template #columns>
<a-table-column title="ID" data-index="id" :width="80" />
<a-table-column title="名称" data-index="name" />
<a-table-column title="插件" data-index="plugin" :width="100" />
<a-table-column title="费率" data-index="rate">
<template #cell="{ record }">{{ (parseFloat(record.rate) * 100).toFixed(2) }}%</template>
</a-table-column>
<a-table-column title="状态" data-index="status" :width="100">
<template #cell="{ record }">
<a-tag :color="record.status === 1 ? 'green' : 'red'">
{{ record.status === 1 ? '启用' : '禁用' }}
</a-tag>
</template>
</a-table-column>
<a-table-column title="操作" :width="150">
<template #cell="{ record }">
<a-button type="text" size="small" @click="handleEdit(record)">编辑</a-button>
<a-popconfirm content="确定删除吗?" @ok="handleDelete(record.id)">
<a-button type="text" size="small" status="danger">删除</a-button>
</a-popconfirm>
</template>
</a-table-column>
</template>
</a-table>
<!-- 新增/编辑弹窗 -->
<a-modal v-model:visible="showCreateModal" :title="editingChannel ? '编辑通道' : '新增通道'" @ok="handleSave">
<a-form :model="channelForm" layout="vertical">
<a-form-item field="name" label="名称">
<a-input v-model="channelForm.name" />
</a-form-item>
<a-form-item field="plugin" label="插件">
<a-select v-model="channelForm.plugin">
<a-option value="alipay">支付宝</a-option>
<a-option value="wechat">微信支付</a-option>
<a-option value="paypal">PayPal</a-option>
</a-select>
</a-form-item>
<a-form-item field="pay_types" label="支付方式">
<a-input v-model="channelForm.pay_types" placeholder="alipay,wxpay" />
</a-form-item>
<a-form-item field="rate" label="费率">
<a-input-number v-model="channelForm.rate" :precision="4" :min="0" :max="1" />
</a-form-item>
<a-form-item field="status" label="状态">
<a-switch v-model="channelForm.status" :checked-value="1" :unchecked-value="0" />
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import { getChannels, createChannel, updateChannel, deleteChannel } from '@/api/admin'
import type { Channel } from '@/api/types'
const loading = ref(false)
const channels = ref<Channel[]>([])
const pagination = reactive({ current: 1, pageSize: 20, total: 0 })
const showCreateModal = ref(false)
const editingChannel = ref<Channel | null>(null)
const channelForm = reactive({
name: '',
plugin: 'alipay',
pay_types: '',
rate: 0.006,
status: 1,
})
const fetchData = async () => {
loading.value = true
try {
const res = await getChannels({ page: pagination.current, page_size: pagination.pageSize })
channels.value = res.data.list
pagination.total = res.data.total
} finally {
loading.value = false
}
}
const handlePageChange = (page: number) => {
pagination.current = page
fetchData()
}
const handleEdit = (record: Channel) => {
editingChannel.value = record
Object.assign(channelForm, record)
showCreateModal.value = true
}
const handleSave = async () => {
try {
if (editingChannel.value) {
await updateChannel(editingChannel.value.id, channelForm)
} else {
await createChannel(channelForm)
}
Message.success('保存成功')
showCreateModal.value = false
editingChannel.value = null
fetchData()
} catch (e) {
// ignore
}
}
const handleDelete = async (id: number) => {
try {
await deleteChannel(id)
Message.success('删除成功')
fetchData()
} catch (e) {
// ignore
}
}
onMounted(() => {
fetchData()
})
</script>
Step 5: 创建结算管理页面
<!-- web/src/views/admin/Settlements.vue -->
<template>
<div>
<a-table :data="settlements" :loading="loading" :pagination="pagination" @page-change="handlePageChange">
<template #columns>
<a-table-column title="结算单号" data-index="settle_no" :width="200" />
<a-table-column title="商户ID" data-index="merchant_id" :width="100" />
<a-table-column title="金额" data-index="amount">
<template #cell="{ record }">¥{{ record.amount }}</template>
</a-table-column>
<a-table-column title="手续费" data-index="fee">
<template #cell="{ record }">¥{{ record.fee }}</template>
</a-table-column>
<a-table-column title="实际到账" data-index="actual_amount">
<template #cell="{ record }">¥{{ record.actual_amount }}</template>
</a-table-column>
<a-table-column title="收款账号" data-index="account_no" />
<a-table-column title="状态" data-index="status" :width="100">
<template #cell="{ record }">
<a-tag :color="statusColor(record.status)">{{ statusText(record.status) }}</a-tag>
</template>
</a-table-column>
<a-table-column title="操作" :width="150">
<template #cell="{ record }">
<template v-if="record.status === 0">
<a-button type="text" size="small" @click="handleApprove(record.id)">通过</a-button>
<a-button type="text" size="small" status="danger" @click="showRejectModal(record)">驳回</a-button>
</template>
</template>
</a-table-column>
</template>
</a-table>
<!-- 驳回弹窗 -->
<a-modal v-model:visible="rejectModalVisible" title="驳回结算" @ok="handleReject">
<a-textarea v-model="rejectRemark" placeholder="请输入驳回原因" />
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import { getSettlements, approveSettlement, rejectSettlement } from '@/api/admin'
import type { Settlement } from '@/api/types'
const loading = ref(false)
const settlements = ref<Settlement[]>([])
const pagination = reactive({ current: 1, pageSize: 20, total: 0 })
const rejectModalVisible = ref(false)
const rejectingId = ref(0)
const rejectRemark = ref('')
const statusText = (status: number) => {
const map: Record<number, string> = { 0: '待审核', 1: '处理中', 2: '已完成', 3: '已驳回' }
return map[status] || '未知'
}
const statusColor = (status: number) => {
const map: Record<number, string> = { 0: 'orange', 1: 'blue', 2: 'green', 3: 'red' }
return map[status] || 'gray'
}
const fetchData = async () => {
loading.value = true
try {
const res = await getSettlements({ page: pagination.current, page_size: pagination.pageSize })
settlements.value = res.data.list
pagination.total = res.data.total
} finally {
loading.value = false
}
}
const handlePageChange = (page: number) => {
pagination.current = page
fetchData()
}
const handleApprove = async (id: number) => {
try {
await approveSettlement(id)
Message.success('审核通过')
fetchData()
} catch (e) {
// ignore
}
}
const showRejectModal = (record: Settlement) => {
rejectingId.value = record.id
rejectRemark.value = ''
rejectModalVisible.value = true
}
const handleReject = async () => {
if (!rejectRemark.value) {
Message.warning('请输入驳回原因')
return
}
try {
await rejectSettlement(rejectingId.value, rejectRemark.value)
Message.success('已驳回')
rejectModalVisible.value = false
fetchData()
} catch (e) {
// ignore
}
}
onMounted(() => {
fetchData()
})
</script>
Step 6: 提交
git add web/src/views/admin/
git commit -m "feat(web): add admin dashboard, merchants, orders, channels, settlements pages"
Task 9: 创建商户端页面
Files:
- Create:
epay-go/web/src/views/merchant/Dashboard.vue - Create:
epay-go/web/src/views/merchant/Orders.vue - Create:
epay-go/web/src/views/merchant/Settlements.vue - Create:
epay-go/web/src/views/merchant/Records.vue - Create:
epay-go/web/src/views/merchant/Profile.vue
Step 1: 创建商户仪表盘
<!-- web/src/views/merchant/Dashboard.vue -->
<template>
<div class="dashboard">
<a-row :gutter="24">
<a-col :span="6">
<a-card>
<a-statistic title="可用余额" :value="stats.balance" :precision="2" prefix="¥" />
</a-card>
</a-col>
<a-col :span="6">
<a-card>
<a-statistic title="冻结余额" :value="stats.frozen_balance" :precision="2" prefix="¥" />
</a-card>
</a-col>
<a-col :span="6">
<a-card>
<a-statistic title="今日订单" :value="stats.today_order_count" />
</a-card>
</a-col>
<a-col :span="6">
<a-card>
<a-statistic title="今日交易额" :value="stats.today_order_amount" :precision="2" prefix="¥" />
</a-card>
</a-col>
</a-row>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { getDashboard } from '@/api/merchant'
const stats = ref({
balance: '0.00',
frozen_balance: '0.00',
today_order_count: 0,
today_order_amount: '0.00',
})
onMounted(async () => {
try {
const res = await getDashboard()
stats.value = res.data
} catch (e) {
// ignore
}
})
</script>
Step 2: 创建订单页面
<!-- web/src/views/merchant/Orders.vue -->
<template>
<div>
<a-table :data="orders" :loading="loading" :pagination="pagination" @page-change="handlePageChange">
<template #columns>
<a-table-column title="订单号" data-index="trade_no" :width="200" />
<a-table-column title="商户订单号" data-index="out_trade_no" :width="180" />
<a-table-column title="商品名称" data-index="name" />
<a-table-column title="金额" data-index="amount">
<template #cell="{ record }">¥{{ record.amount }}</template>
</a-table-column>
<a-table-column title="状态" data-index="status" :width="100">
<template #cell="{ record }">
<a-tag :color="statusColor(record.status)">{{ statusText(record.status) }}</a-tag>
</template>
</a-table-column>
<a-table-column title="创建时间" data-index="created_at" :width="180" />
</template>
</a-table>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { getOrders } from '@/api/merchant'
import type { Order } from '@/api/types'
const loading = ref(false)
const orders = ref<Order[]>([])
const pagination = reactive({ current: 1, pageSize: 20, total: 0 })
const statusText = (status: number) => {
const map: Record<number, string> = { 0: '未支付', 1: '已支付', 2: '已退款' }
return map[status] || '未知'
}
const statusColor = (status: number) => {
const map: Record<number, string> = { 0: 'orange', 1: 'green', 2: 'red' }
return map[status] || 'gray'
}
const fetchData = async () => {
loading.value = true
try {
const res = await getOrders({ page: pagination.current, page_size: pagination.pageSize })
orders.value = res.data.list
pagination.total = res.data.total
} finally {
loading.value = false
}
}
const handlePageChange = (page: number) => {
pagination.current = page
fetchData()
}
onMounted(() => {
fetchData()
})
</script>
Step 3: 创建结算页面
<!-- web/src/views/merchant/Settlements.vue -->
<template>
<div>
<div style="margin-bottom: 16px;">
<a-button type="primary" @click="showApplyModal = true">申请结算</a-button>
</div>
<a-table :data="settlements" :loading="loading" :pagination="pagination" @page-change="handlePageChange">
<template #columns>
<a-table-column title="结算单号" data-index="settle_no" :width="200" />
<a-table-column title="金额" data-index="amount">
<template #cell="{ record }">¥{{ record.amount }}</template>
</a-table-column>
<a-table-column title="手续费" data-index="fee" />
<a-table-column title="实际到账" data-index="actual_amount" />
<a-table-column title="收款账号" data-index="account_no" />
<a-table-column title="状态" data-index="status" :width="100">
<template #cell="{ record }">
<a-tag :color="statusColor(record.status)">{{ statusText(record.status) }}</a-tag>
</template>
</a-table-column>
<a-table-column title="备注" data-index="remark" />
<a-table-column title="申请时间" data-index="created_at" :width="180" />
</template>
</a-table>
<!-- 申请结算弹窗 -->
<a-modal v-model:visible="showApplyModal" title="申请结算" @ok="handleApply">
<a-form :model="applyForm" layout="vertical">
<a-form-item field="amount" label="结算金额">
<a-input-number v-model="applyForm.amount" :precision="2" :min="10" style="width: 100%" />
</a-form-item>
<a-form-item field="account_type" label="收款方式">
<a-radio-group v-model="applyForm.account_type">
<a-radio value="alipay">支付宝</a-radio>
<a-radio value="bank">银行卡</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item field="account_no" label="收款账号">
<a-input v-model="applyForm.account_no" />
</a-form-item>
<a-form-item field="account_name" label="收款人姓名">
<a-input v-model="applyForm.account_name" />
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import { getSettlements, applySettlement } from '@/api/merchant'
import type { Settlement } from '@/api/types'
const loading = ref(false)
const settlements = ref<Settlement[]>([])
const pagination = reactive({ current: 1, pageSize: 20, total: 0 })
const showApplyModal = ref(false)
const applyForm = reactive({
amount: 100,
account_type: 'alipay',
account_no: '',
account_name: '',
})
const statusText = (status: number) => {
const map: Record<number, string> = { 0: '待审核', 1: '处理中', 2: '已完成', 3: '已驳回' }
return map[status] || '未知'
}
const statusColor = (status: number) => {
const map: Record<number, string> = { 0: 'orange', 1: 'blue', 2: 'green', 3: 'red' }
return map[status] || 'gray'
}
const fetchData = async () => {
loading.value = true
try {
const res = await getSettlements({ page: pagination.current, page_size: pagination.pageSize })
settlements.value = res.data.list
pagination.total = res.data.total
} finally {
loading.value = false
}
}
const handlePageChange = (page: number) => {
pagination.current = page
fetchData()
}
const handleApply = async () => {
try {
await applySettlement({
amount: applyForm.amount.toString(),
account_type: applyForm.account_type,
account_no: applyForm.account_no,
account_name: applyForm.account_name,
})
Message.success('申请成功')
showApplyModal.value = false
fetchData()
} catch (e) {
// ignore
}
}
onMounted(() => {
fetchData()
})
</script>
Step 4: 创建资金记录页面
<!-- web/src/views/merchant/Records.vue -->
<template>
<div>
<a-table :data="records" :loading="loading" :pagination="pagination" @page-change="handlePageChange">
<template #columns>
<a-table-column title="时间" data-index="created_at" :width="180" />
<a-table-column title="类型" data-index="action" :width="80">
<template #cell="{ record }">
<a-tag :color="record.action === 1 ? 'green' : 'red'">
{{ record.action === 1 ? '收入' : '支出' }}
</a-tag>
</template>
</a-table-column>
<a-table-column title="金额" data-index="amount">
<template #cell="{ record }">
<span :style="{ color: record.action === 1 ? '#00b42a' : '#f53f3f' }">
{{ record.action === 1 ? '+' : '-' }}¥{{ record.amount }}
</span>
</template>
</a-table-column>
<a-table-column title="变动前余额" data-index="before_balance">
<template #cell="{ record }">¥{{ record.before_balance }}</template>
</a-table-column>
<a-table-column title="变动后余额" data-index="after_balance">
<template #cell="{ record }">¥{{ record.after_balance }}</template>
</a-table-column>
<a-table-column title="说明" data-index="type" />
<a-table-column title="关联单号" data-index="trade_no" />
</template>
</a-table>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { getRecords } from '@/api/merchant'
import type { BalanceRecord } from '@/api/types'
const loading = ref(false)
const records = ref<BalanceRecord[]>([])
const pagination = reactive({ current: 1, pageSize: 20, total: 0 })
const fetchData = async () => {
loading.value = true
try {
const res = await getRecords({ page: pagination.current, page_size: pagination.pageSize })
records.value = res.data.list
pagination.total = res.data.total
} finally {
loading.value = false
}
}
const handlePageChange = (page: number) => {
pagination.current = page
fetchData()
}
onMounted(() => {
fetchData()
})
</script>
Step 5: 创建个人信息页面
<!-- web/src/views/merchant/Profile.vue -->
<template>
<div>
<a-card title="账户信息" style="margin-bottom: 24px;">
<a-descriptions :column="2">
<a-descriptions-item label="商户ID">{{ profile.id }}</a-descriptions-item>
<a-descriptions-item label="用户名">{{ profile.username }}</a-descriptions-item>
<a-descriptions-item label="邮箱">{{ profile.email || '-' }}</a-descriptions-item>
<a-descriptions-item label="手机号">{{ profile.phone || '-' }}</a-descriptions-item>
<a-descriptions-item label="可用余额">¥{{ profile.balance }}</a-descriptions-item>
<a-descriptions-item label="冻结余额">¥{{ profile.frozen_balance }}</a-descriptions-item>
</a-descriptions>
</a-card>
<a-card title="API 密钥" style="margin-bottom: 24px;">
<a-space>
<a-input :model-value="profile.api_key" readonly style="width: 400px;" />
<a-popconfirm content="确定重置API密钥吗?重置后原密钥将立即失效。" @ok="handleResetKey">
<a-button type="primary" status="warning">重置密钥</a-button>
</a-popconfirm>
</a-space>
</a-card>
<a-card title="修改密码">
<a-form :model="passwordForm" @submit="handleUpdatePassword" style="max-width: 400px;">
<a-form-item field="old_password" label="原密码">
<a-input-password v-model="passwordForm.old_password" />
</a-form-item>
<a-form-item field="new_password" label="新密码">
<a-input-password v-model="passwordForm.new_password" />
</a-form-item>
<a-form-item>
<a-button type="primary" html-type="submit">修改密码</a-button>
</a-form-item>
</a-form>
</a-card>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import { getProfile, resetApiKey, updatePassword } from '@/api/merchant'
const profile = ref({
id: 0,
username: '',
email: '',
phone: '',
api_key: '',
balance: '0.00',
frozen_balance: '0.00',
})
const passwordForm = reactive({
old_password: '',
new_password: '',
})
const fetchProfile = async () => {
try {
const res = await getProfile()
profile.value = res.data
} catch (e) {
// ignore
}
}
const handleResetKey = async () => {
try {
const res = await resetApiKey()
profile.value.api_key = res.data.api_key
Message.success('密钥已重置')
} catch (e) {
// ignore
}
}
const handleUpdatePassword = async () => {
try {
await updatePassword(passwordForm)
Message.success('密码已修改')
passwordForm.old_password = ''
passwordForm.new_password = ''
} catch (e) {
// ignore
}
}
onMounted(() => {
fetchProfile()
})
</script>
Step 6: 提交
git add web/src/views/merchant/
git commit -m "feat(web): add merchant dashboard, orders, settlements, records, profile pages"
Task 10: 创建收银台和404页面
Files:
- Create:
epay-go/web/src/views/cashier/Index.vue - Create:
epay-go/web/src/views/NotFound.vue - Modify:
epay-go/web/src/App.vue
Step 1: 创建收银台页面
<!-- web/src/views/cashier/Index.vue -->
<template>
<div class="cashier-container">
<div class="cashier-box">
<div class="header">
<h2>订单支付</h2>
</div>
<div class="content" v-if="order">
<div class="order-info">
<div class="row">
<span class="label">商品名称</span>
<span class="value">{{ order.name }}</span>
</div>
<div class="row">
<span class="label">订单号</span>
<span class="value">{{ order.trade_no }}</span>
</div>
<div class="row amount-row">
<span class="label">支付金额</span>
<span class="amount">¥{{ order.amount }}</span>
</div>
</div>
<div class="qrcode" v-if="qrcodeUrl">
<img :src="qrcodeUrl" alt="支付二维码" />
<p>请使用{{ payTypeName }}扫码支付</p>
</div>
<div class="actions" v-if="payUrl">
<a-button type="primary" size="large" @click="goToPay">前往支付</a-button>
</div>
</div>
<div class="loading" v-else>
<a-spin />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const order = ref<any>(null)
const qrcodeUrl = ref('')
const payUrl = ref('')
const payTypeName = computed(() => {
const map: Record<string, string> = {
alipay: '支付宝',
wxpay: '微信',
qqpay: 'QQ钱包',
}
return map[order.value?.pay_type] || '手机'
})
const goToPay = () => {
if (payUrl.value) {
window.location.href = payUrl.value
}
}
onMounted(async () => {
const tradeNo = route.params.tradeNo as string
// TODO: 获取订单信息和支付参数
// 这里模拟数据
order.value = {
trade_no: tradeNo,
name: '测试商品',
amount: '10.00',
pay_type: 'alipay',
}
})
</script>
<style scoped>
.cashier-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: #f5f7fa;
}
.cashier-box {
width: 400px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #165dff 0%, #722ed1 100%);
color: #fff;
padding: 24px;
text-align: center;
}
.header h2 {
margin: 0;
}
.content {
padding: 24px;
}
.order-info .row {
display: flex;
justify-content: space-between;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
}
.order-info .label {
color: #86909c;
}
.order-info .amount-row {
border-bottom: none;
padding-top: 24px;
}
.order-info .amount {
font-size: 28px;
font-weight: bold;
color: #f53f3f;
}
.qrcode {
text-align: center;
padding: 24px 0;
}
.qrcode img {
width: 200px;
height: 200px;
}
.qrcode p {
margin-top: 12px;
color: #86909c;
}
.actions {
text-align: center;
padding: 24px 0;
}
.loading {
padding: 48px;
text-align: center;
}
</style>
Step 2: 创建404页面
<!-- web/src/views/NotFound.vue -->
<template>
<div class="not-found">
<a-result status="404" title="404" subtitle="页面不存在">
<template #extra>
<a-button type="primary" @click="$router.push('/')">返回首页</a-button>
</template>
</a-result>
</div>
</template>
<style scoped>
.not-found {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
</style>
Step 3: 更新 App.vue
<!-- web/src/App.vue -->
<template>
<router-view />
</template>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body, #app {
height: 100%;
}
</style>
Step 4: 提交
git add web/src/views/ web/src/App.vue
git commit -m "feat(web): add cashier page and 404 page"
Task 11: 最终构建验证
Step 1: 验证前端构建
cd d:/project/payment/epay-go/web
npm run build
Expected: 构建成功,生成 dist/ 目录
Step 2: 提交所有更改
cd d:/project/payment/epay-go
git add .
git commit -m "feat(web): complete vue3 frontend with admin, merchant, cashier"
阶段四完成检查清单
- Vite 配置和依赖安装
- API 请求封装 (
src/api/request.ts) - 类型定义 (
src/api/types.ts) - 管理后台 API (
src/api/admin.ts) - 商户端 API (
src/api/merchant.ts) - 路由配置 (
src/router/index.ts) - 管理后台布局 (
src/layouts/AdminLayout.vue) - 商户端布局 (
src/layouts/MerchantLayout.vue) - 登录/注册页面
- 管理后台5个页面(仪表盘、商户、订单、通道、结算)
- 商户端5个页面(仪表盘、订单、结算、记录、个人信息)
- 收银台页面
- 404页面
- 构建验证通过
页面汇总
| 模块 | 页面 | 路由 |
|---|---|---|
| 管理后台 | 登录 | /admin/login |
| 仪表盘 | /admin/dashboard |
|
| 商户管理 | /admin/merchants |
|
| 订单管理 | /admin/orders |
|
| 通道管理 | /admin/channels |
|
| 结算管理 | /admin/settlements |
|
| 商户中心 | 登录 | /merchant/login |
| 注册 | /merchant/register |
|
| 仪表盘 | /merchant/dashboard |
|
| 订单管理 | /merchant/orders |
|
| 结算管理 | /merchant/settlements |
|
| 资金记录 | /merchant/records |
|
| 个人信息 | /merchant/profile |
|
| 收银台 | 支付页 | /cashier/:tradeNo |
下一阶段: 阶段五将完成 Docker 部署优化、集成测试和文档编写。