新增: 微信配置-代理充值
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m58s

This commit is contained in:
sexygoat
2026-03-17 14:06:38 +08:00
parent f4ccf9ed24
commit e975e6af4b
19 changed files with 2940 additions and 81 deletions

View File

@@ -0,0 +1,61 @@
/**
* 代理充值相关 API
*/
import { BaseService } from '../BaseService'
import type {
AgentRecharge,
AgentRechargeQueryParams,
AgentRechargeListResponse,
CreateAgentRechargeRequest,
ConfirmOfflinePaymentRequest,
BaseResponse
} from '@/types/api'
export class AgentRechargeService extends BaseService {
/**
* 获取代理充值订单列表
* @param params 查询参数
*/
static getAgentRecharges(
params?: AgentRechargeQueryParams
): Promise<BaseResponse<AgentRechargeListResponse>> {
return this.get<BaseResponse<AgentRechargeListResponse>>(
'/api/admin/agent-recharges',
params
)
}
/**
* 获取代理充值订单详情
* @param id 充值订单ID
*/
static getAgentRechargeById(id: number): Promise<BaseResponse<AgentRecharge>> {
return this.getOne<AgentRecharge>(`/api/admin/agent-recharges/${id}`)
}
/**
* 创建代理充值订单
* @param data 创建充值订单请求参数
*/
static createAgentRecharge(
data: CreateAgentRechargeRequest
): Promise<BaseResponse<AgentRecharge>> {
return this.post<BaseResponse<AgentRecharge>>('/api/admin/agent-recharges', data)
}
/**
* 确认线下充值
* @param id 充值订单ID
* @param data 确认线下充值请求参数
*/
static confirmOfflinePayment(
id: number,
data: ConfirmOfflinePaymentRequest
): Promise<BaseResponse<AgentRecharge>> {
return this.post<BaseResponse<AgentRecharge>>(
`/api/admin/agent-recharges/${id}/offline-pay`,
data
)
}
}

View File

@@ -26,6 +26,8 @@ export { PackageManageService } from './packageManage'
export { ShopSeriesGrantService } from './shopSeriesGrant' export { ShopSeriesGrantService } from './shopSeriesGrant'
export { OrderService } from './order' export { OrderService } from './order'
export { AssetService } from './asset' export { AssetService } from './asset'
export { AgentRechargeService } from './agentRecharge'
export { WechatConfigService } from './wechatConfig'
// TODO: 按需添加其他业务模块 // TODO: 按需添加其他业务模块
// export { SettingService } from './setting' // export { SettingService } from './setting'

View File

@@ -9,6 +9,8 @@ import type {
OrderListResponse, OrderListResponse,
CreateOrderRequest, CreateOrderRequest,
CreateOrderResponse, CreateOrderResponse,
PurchaseCheckRequest,
PurchaseCheckResponse,
BaseResponse BaseResponse
} from '@/types/api' } from '@/types/api'
@@ -44,4 +46,17 @@ export class OrderService extends BaseService {
static cancelOrder(id: number): Promise<BaseResponse> { static cancelOrder(id: number): Promise<BaseResponse> {
return this.post<BaseResponse>(`/api/admin/orders/${id}/cancel`, {}) return this.post<BaseResponse>(`/api/admin/orders/${id}/cancel`, {})
} }
/**
* 套餐购买预检
* @param data 预检请求参数
*/
static purchaseCheck(
data: PurchaseCheckRequest
): Promise<BaseResponse<PurchaseCheckResponse>> {
return this.post<BaseResponse<PurchaseCheckResponse>>(
'/api/admin/orders/purchase-check',
data
)
}
} }

View File

@@ -0,0 +1,78 @@
/**
* 微信支付配置管理 API
*/
import { BaseService } from '../BaseService'
import type { BaseResponse } from '@/types/api'
import type {
WechatConfig,
WechatConfigQueryParams,
WechatConfigListResponse,
CreateWechatConfigRequest,
UpdateWechatConfigRequest
} from '@/types/api/wechatConfig'
export class WechatConfigService extends BaseService {
/**
* 获取支付配置列表
*/
static getWechatConfigs(
params?: WechatConfigQueryParams
): Promise<BaseResponse<WechatConfigListResponse>> {
return this.get<BaseResponse<WechatConfigListResponse>>('/api/admin/wechat-configs', params)
}
/**
* 获取支付配置详情
*/
static getWechatConfigById(id: number): Promise<BaseResponse<WechatConfig>> {
return this.get<BaseResponse<WechatConfig>>(`/api/admin/wechat-configs/${id}`)
}
/**
* 创建支付配置
*/
static createWechatConfig(
data: CreateWechatConfigRequest
): Promise<BaseResponse<WechatConfig>> {
return this.post<BaseResponse<WechatConfig>>('/api/admin/wechat-configs', data)
}
/**
* 更新支付配置
*/
static updateWechatConfig(
id: number,
data: UpdateWechatConfigRequest
): Promise<BaseResponse<WechatConfig>> {
return this.put<BaseResponse<WechatConfig>>(`/api/admin/wechat-configs/${id}`, data)
}
/**
* 删除支付配置
*/
static deleteWechatConfig(id: number): Promise<BaseResponse<void>> {
return this.delete<BaseResponse<void>>(`/api/admin/wechat-configs/${id}`)
}
/**
* 激活支付配置
*/
static activateWechatConfig(id: number): Promise<BaseResponse<WechatConfig>> {
return this.post<BaseResponse<WechatConfig>>(`/api/admin/wechat-configs/${id}/activate`)
}
/**
* 停用支付配置
*/
static deactivateWechatConfig(id: number): Promise<BaseResponse<WechatConfig>> {
return this.post<BaseResponse<WechatConfig>>(`/api/admin/wechat-configs/${id}/deactivate`)
}
/**
* 获取当前生效的支付配置
*/
static getActiveWechatConfig(): Promise<BaseResponse<WechatConfig>> {
return this.get<BaseResponse<WechatConfig>>('/api/admin/wechat-configs/active')
}
}

View File

@@ -1146,6 +1146,37 @@ export const asyncRoutes: AppRouteRecord[] = [
] ]
}, },
{
path: '/finance',
name: 'FinanceManagement',
component: RoutesAlias.Home,
meta: {
title: '财务管理',
icon: '&#xe816;'
},
children: [
{
path: 'agent-recharge',
name: 'AgentRecharge',
component: RoutesAlias.AgentRecharge,
meta: {
title: '代理充值',
keepAlive: true
}
},
{
path: 'agent-recharge/detail/:id',
name: 'AgentRechargeDetailRoute',
component: RoutesAlias.AgentRechargeDetail,
meta: {
title: '充值详情',
isHide: true,
keepAlive: false
}
}
]
},
{ {
path: '/commission', path: '/commission',
name: 'CommissionManagement', name: 'CommissionManagement',
@@ -1196,44 +1227,63 @@ export const asyncRoutes: AppRouteRecord[] = [
} }
} }
] ]
} },
// { {
// path: '/settings', path: '/settings',
// name: 'Settings', name: 'Settings',
// component: RoutesAlias.Home, component: RoutesAlias.Home,
// meta: { meta: {
// title: 'menus.settings.title', title: '设置管理',
// icon: '&#xe715;' icon: '&#xe715;'
// }, },
// children: [ children: [
// { {
// path: 'payment-merchant', path: 'wechat-config',
// name: 'PaymentMerchant', name: 'WechatConfig',
// component: RoutesAlias.PaymentMerchant, component: RoutesAlias.WechatConfig,
// meta: { meta: {
// title: 'menus.settings.paymentMerchant', title: '微信支付配置',
// keepAlive: true keepAlive: true
// } }
// }, },
// { {
// path: 'developer-api', path: 'wechat-config/detail/:id',
// name: 'DeveloperApi', name: 'WechatConfigDetailRoute',
// component: RoutesAlias.DeveloperApi, component: RoutesAlias.WechatConfigDetail,
// meta: { meta: {
// title: 'menus.settings.developerApi', title: '支付配置详情',
// keepAlive: true isHide: true,
// } keepAlive: false
// }, }
// { }
// path: 'commission-template', // {
// name: 'CommissionTemplate', // path: 'payment-merchant',
// component: RoutesAlias.CommissionTemplate, // name: 'PaymentMerchant',
// meta: { // component: RoutesAlias.PaymentMerchant,
// title: 'menus.settings.commissionTemplate', // meta: {
// keepAlive: true // title: 'menus.settings.paymentMerchant',
// } // keepAlive: true
// } // }
// ] // },
// } // {
// path: 'developer-api',
// name: 'DeveloperApi',
// component: RoutesAlias.DeveloperApi,
// meta: {
// title: 'menus.settings.developerApi',
// keepAlive: true
// }
// },
// {
// path: 'commission-template',
// name: 'CommissionTemplate',
// component: RoutesAlias.CommissionTemplate,
// meta: {
// title: 'menus.settings.commissionTemplate',
// keepAlive: true
// }
// }
]
}
] ]

View File

@@ -113,6 +113,8 @@ export enum RoutesAlias {
CarrierManagement = '/finance/carrier-management', // 运营商管理 CarrierManagement = '/finance/carrier-management', // 运营商管理
OrderList = '/account/orders', // 订单管理 OrderList = '/account/orders', // 订单管理
OrderDetail = '/account/orders/detail', // 订单详情 OrderDetail = '/account/orders/detail', // 订单详情
AgentRecharge = '/finance/agent-recharge', // 代理充值
AgentRechargeDetail = '/finance/agent-recharge/detail', // 代理充值详情
// 佣金管理 // 佣金管理
WithdrawalApproval = '/finance/commission/withdrawal-approval', // 提现审批 WithdrawalApproval = '/finance/commission/withdrawal-approval', // 提现审批
@@ -123,7 +125,9 @@ export enum RoutesAlias {
// 设置管理 // 设置管理
PaymentMerchant = '/settings/payment-merchant', // 支付商户 PaymentMerchant = '/settings/payment-merchant', // 支付商户
DeveloperApi = '/settings/developer-api', // 开发者API DeveloperApi = '/settings/developer-api', // 开发者API
CommissionTemplate = '/settings/commission-template' // 分佣模板 CommissionTemplate = '/settings/commission-template', // 分佣模板
WechatConfig = '/settings/wechat-config', // 微信支付配置
WechatConfigDetail = '/settings/wechat-config/detail' // 微信支付配置详情
} }
// 主页路由 // 主页路由

View File

@@ -0,0 +1,65 @@
/**
* 代理充值相关类型定义
*/
// 充值状态
export enum AgentRechargeStatus {
PENDING = 1, // 待支付
COMPLETED = 2, // 已完成
CANCELLED = 3 // 已取消
}
// 支付方式
export type AgentRechargePaymentMethod = 'wechat' | 'offline'
// 支付通道
export type AgentRechargePaymentChannel = 'wechat_direct' | 'fuyou' | 'offline'
// 代理充值订单
export interface AgentRecharge {
id: number
recharge_no: string
agent_wallet_id: number
shop_id: number
shop_name: string
amount: number // 充值金额(单位:分)
status: AgentRechargeStatus
payment_method: AgentRechargePaymentMethod
payment_channel: AgentRechargePaymentChannel
payment_config_id: number | null
payment_transaction_id: string
created_at: string
paid_at: string | null
completed_at: string | null
updated_at: string
}
// 查询代理充值订单列表参数
export interface AgentRechargeQueryParams {
page?: number
page_size?: number
shop_id?: number
status?: AgentRechargeStatus
start_date?: string
end_date?: string
}
// 代理充值订单列表响应
export interface AgentRechargeListResponse {
page: number
page_size: number
total: number
list: AgentRecharge[]
}
// 创建代理充值订单请求
export interface CreateAgentRechargeRequest {
amount: number // 充值金额(单位:分),范围 10000 ~ 100000000
payment_method: AgentRechargePaymentMethod
shop_id: number
}
// 确认线下充值请求
export interface ConfirmOfflinePaymentRequest {
operation_password: string
}

View File

@@ -79,3 +79,9 @@ export * from './order'
// 资产管理相关 // 资产管理相关
export * from './asset' export * from './asset'
// 代理充值相关
export * from './agentRecharge'
// 微信支付配置相关
export * from './wechatConfig'

View File

@@ -17,7 +17,7 @@ export type OrderType = 'single_card' | 'device'
export type BuyerType = 'personal' | 'agent' export type BuyerType = 'personal' | 'agent'
// 订单支付方式 // 订单支付方式
export type OrderPaymentMethod = 'wallet' | 'wechat' | 'alipay' export type OrderPaymentMethod = 'wallet' | 'wechat' | 'alipay' | 'offline'
// 订单佣金状态 // 订单佣金状态
export enum OrderCommissionStatus { export enum OrderCommissionStatus {
@@ -84,7 +84,25 @@ export interface CreateOrderRequest {
package_ids: number[] package_ids: number[]
iot_card_id?: number | null iot_card_id?: number | null
device_id?: number | null device_id?: number | null
payment_method: OrderPaymentMethod // 支付方式
} }
// 创建订单响应 (返回订单详情) // 创建订单响应 (返回订单详情)
export type CreateOrderResponse = Order export type CreateOrderResponse = Order
// 套餐购买预检请求
export interface PurchaseCheckRequest {
order_type: OrderType // 订单类型 (single_card:单卡购买, device:设备购买)
package_ids: number[] // 套餐ID列表
resource_id: number // 资源ID (IoT卡ID或设备ID)
}
// 套餐购买预检响应
export interface PurchaseCheckResponse {
need_force_recharge: boolean // 是否需要强充
force_recharge_amount?: number // 强充金额(分)
total_package_amount?: number // 套餐总价(分)
actual_payment?: number // 实际支付金额(分)
wallet_credit?: number // 钱包到账金额(分)
message?: string // 提示信息
}

View File

@@ -0,0 +1,135 @@
/**
* 微信支付配置管理相关类型定义
*/
// 支付渠道类型
export type PaymentProviderType = 'wechat' | 'fuiou'
// 微信支付配置
export interface WechatConfig {
id: number
name: string
description: string
provider_type: PaymentProviderType
is_active: boolean
// 小程序配置
miniapp_app_id: string
miniapp_app_secret: string
// 公众号配置
oa_app_id: string
oa_app_secret: string
oa_token: string
oa_aes_key: string
oa_oauth_redirect_url: string
// 微信支付配置
wx_mch_id: string
wx_api_v2_key: string
wx_api_v3_key: string
wx_notify_url: string
wx_serial_no: string
wx_cert_content: string
wx_key_content: string
// 富友支付配置
fy_api_url: string
fy_ins_cd: string
fy_mchnt_cd: string
fy_term_id: string
fy_notify_url: string
fy_private_key: string
fy_public_key: string
created_at: string
updated_at: string
}
// 查询参数
export interface WechatConfigQueryParams {
page?: number
page_size?: number
provider_type?: PaymentProviderType
is_active?: boolean
}
// 列表响应
export interface WechatConfigListResponse {
items: WechatConfig[]
page: number
page_size: number
total: number
}
// 创建微信支付配置请求
export interface CreateWechatConfigRequest {
name: string
provider_type: PaymentProviderType
description?: string
// 小程序配置
miniapp_app_id?: string
miniapp_app_secret?: string
// 公众号配置
oa_app_id?: string
oa_app_secret?: string
oa_token?: string
oa_aes_key?: string
oa_oauth_redirect_url?: string
// 微信支付配置
wx_mch_id?: string
wx_api_v2_key?: string
wx_api_v3_key?: string
wx_notify_url?: string
wx_serial_no?: string
wx_cert_content?: string
wx_key_content?: string
// 富友支付配置
fy_api_url?: string
fy_ins_cd?: string
fy_mchnt_cd?: string
fy_term_id?: string
fy_notify_url?: string
fy_private_key?: string
fy_public_key?: string
}
// 更新微信支付配置请求
export interface UpdateWechatConfigRequest {
name?: string
description?: string
provider_type?: PaymentProviderType
// 小程序配置
miniapp_app_id?: string
miniapp_app_secret?: string
// 公众号配置
oa_app_id?: string
oa_app_secret?: string
oa_token?: string
oa_aes_key?: string
oa_oauth_redirect_url?: string
// 微信支付配置
wx_mch_id?: string
wx_api_v2_key?: string
wx_api_v3_key?: string
wx_notify_url?: string
wx_serial_no?: string
wx_cert_content?: string
wx_key_content?: string
// 富友支付配置
fy_api_url?: string
fy_ins_cd?: string
fy_mchnt_cd?: string
fy_term_id?: string
fy_notify_url?: string
fy_private_key?: string
fy_public_key?: string
}

View File

@@ -8,6 +8,10 @@
<div class="section-title">提现配置</div> <div class="section-title">提现配置</div>
<WithdrawalSettings /> <WithdrawalSettings />
<!-- 支付配置 -->
<div class="section-title">支付配置</div>
<ActiveWechatConfig />
<!--<el-row :gutter="20">--> <!--<el-row :gutter="20">-->
<!-- <el-col :xl="14" :lg="15" :xs="24">--> <!-- <el-col :xl="14" :lg="15" :xs="24">-->
<!-- <TodaySales />--> <!-- <TodaySales />-->
@@ -54,6 +58,7 @@
import VolumeServiceLevel from './widget/VolumeServiceLevel.vue' import VolumeServiceLevel from './widget/VolumeServiceLevel.vue'
import CommissionSummary from './widget/CommissionSummary.vue' import CommissionSummary from './widget/CommissionSummary.vue'
import WithdrawalSettings from './widget/WithdrawalSettings.vue' import WithdrawalSettings from './widget/WithdrawalSettings.vue'
import ActiveWechatConfig from './widget/ActiveWechatConfig.vue'
defineOptions({ name: 'Analysis' }) defineOptions({ name: 'Analysis' })
</script> </script>

View File

@@ -0,0 +1,238 @@
<template>
<ElCard shadow="never" class="active-wechat-config-widget" v-if="activeConfig">
<template #header>
<div class="card-header">
<div class="header-left">
<span class="header-title">当前生效支付配置</span>
<ElTag type="success" effect="dark" size="small">生效中</ElTag>
</div>
<div class="header-right">
<span class="creator-info">创建于 {{ formatDateTime(activeConfig.created_at) }}</span>
</div>
</div>
</template>
<div class="config-info">
<div class="info-card">
<div
class="info-icon"
style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%)"
>
<i class="el-icon">📝</i>
</div>
<div class="info-content">
<div class="info-label">配置名称</div>
<div class="info-value">{{ activeConfig.name }}</div>
</div>
</div>
<div class="info-card">
<div
class="info-icon"
style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%)"
>
<i class="el-icon">💳</i>
</div>
<div class="info-content">
<div class="info-label">支付渠道类型</div>
<div class="info-value">{{ getProviderTypeText(activeConfig.provider_type) }}</div>
</div>
</div>
<div class="info-card">
<div
class="info-icon"
style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)"
>
<i class="el-icon">🏪</i>
</div>
<div class="info-content">
<div class="info-label">商户号</div>
<div class="info-value">{{ getMerchantId(activeConfig) }}</div>
</div>
</div>
<div class="info-card">
<div
class="info-icon"
style="background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)"
>
<i class="el-icon"></i>
</div>
<div class="info-content">
<div class="info-label">激活状态</div>
<div class="info-value">{{ activeConfig.is_active ? '已激活' : '未激活' }}</div>
</div>
</div>
</div>
</ElCard>
<ElCard shadow="never" v-else class="active-wechat-config-widget empty-state">
<div class="empty-content">
<i class="el-icon-info" style="font-size: 48px; color: var(--el-text-color-placeholder)"></i>
<p>暂无生效的支付配置</p>
</div>
</ElCard>
</template>
<script setup lang="ts">
import { WechatConfigService } from '@/api/modules'
import { ElTag } from 'element-plus'
import type { WechatConfig, PaymentProviderType } from '@/types/api'
import { formatDateTime } from '@/utils/business/format'
defineOptions({ name: 'ActiveWechatConfigWidget' })
// 当前生效的支付配置
const activeConfig = ref<WechatConfig | null>(null)
// 获取支付渠道类型文本
const getProviderTypeText = (type: PaymentProviderType): string => {
const typeMap: Record<PaymentProviderType, string> = {
wechat: '微信直连',
fuiou: '富友支付'
}
return typeMap[type] || type
}
// 获取商户号
const getMerchantId = (config: WechatConfig): string => {
if (config.provider_type === 'wechat') {
return config.wx_mch_id || '-'
}
if (config.provider_type === 'fuiou') {
return config.fy_mchnt_cd || '-'
}
return '-'
}
// 加载当前生效配置
const loadActiveConfig = async () => {
try {
const res = await WechatConfigService.getActiveWechatConfig()
if (res.code === 0 && res.data) {
activeConfig.value = res.data
}
} catch (error) {
console.error('获取当前生效支付配置失败:', error)
}
}
onMounted(() => {
loadActiveConfig()
})
</script>
<style lang="scss" scoped>
.active-wechat-config-widget {
:deep(.el-card__header) {
padding: 18px 20px;
background: var(--el-fill-color-light);
border-bottom: 1px solid var(--el-border-color-lighter);
}
:deep(.el-card__body) {
padding: 20px;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 12px;
.header-left {
display: flex;
gap: 10px;
align-items: center;
.header-title {
font-size: 15px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
.header-right {
.creator-info {
font-size: 13px;
color: var(--el-text-color-secondary);
}
}
}
.config-info {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 16px;
.info-card {
display: flex;
gap: 12px;
align-items: center;
padding: 16px;
background: var(--el-fill-color-blank);
border-radius: 8px;
.info-icon {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
font-size: 20px;
border-radius: 8px;
}
.info-content {
flex: 1;
min-width: 0;
.info-label {
margin-bottom: 4px;
font-size: 13px;
color: var(--el-text-color-secondary);
}
.info-value {
overflow: hidden;
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
&.empty-state {
.empty-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
color: var(--el-text-color-secondary);
p {
margin-top: 12px;
font-size: 14px;
}
}
}
}
@media (max-width: 768px) {
.active-wechat-config-widget {
.card-header {
.header-left,
.header-right {
width: 100%;
}
}
.config-info {
grid-template-columns: 1fr;
gap: 12px;
}
}
}
</style>

View File

@@ -1,6 +1,15 @@
<template> <template>
<ElRow :gutter="20" class="commission-summary-widget"> <!-- 错误状态 -->
<ElCol :xs="24" :sm="12" :md="12" :lg="8" :xl="4"> <ElCard shadow="never" v-if="errorMsg" class="commission-summary-widget error-state">
<div class="error-content">
<i class="el-icon-warning" style="font-size: 48px; color: var(--el-color-warning)"></i>
<p>{{ errorMsg }}</p>
</div>
</ElCard>
<!-- 正常显示 -->
<div v-else class="commission-summary-widget">
<div class="stats-container">
<ElCard shadow="hover" class="stat-card-wrapper"> <ElCard shadow="hover" class="stat-card-wrapper">
<div class="stat-card"> <div class="stat-card">
<div <div
@@ -15,8 +24,6 @@
</div> </div>
</div> </div>
</ElCard> </ElCard>
</ElCol>
<ElCol :xs="24" :sm="12" :md="12" :lg="8" :xl="4">
<ElCard shadow="hover" class="stat-card-wrapper"> <ElCard shadow="hover" class="stat-card-wrapper">
<div class="stat-card"> <div class="stat-card">
<div <div
@@ -31,8 +38,6 @@
</div> </div>
</div> </div>
</ElCard> </ElCard>
</ElCol>
<ElCol :xs="24" :sm="12" :md="12" :lg="8" :xl="4">
<ElCard shadow="hover" class="stat-card-wrapper"> <ElCard shadow="hover" class="stat-card-wrapper">
<div class="stat-card"> <div class="stat-card">
<div <div
@@ -47,8 +52,6 @@
</div> </div>
</div> </div>
</ElCard> </ElCard>
</ElCol>
<ElCol :xs="24" :sm="12" :md="12" :lg="8" :xl="4">
<ElCard shadow="hover" class="stat-card-wrapper"> <ElCard shadow="hover" class="stat-card-wrapper">
<div class="stat-card"> <div class="stat-card">
<div <div
@@ -63,8 +66,6 @@
</div> </div>
</div> </div>
</ElCard> </ElCard>
</ElCol>
<ElCol :xs="24" :sm="12" :md="12" :lg="8" :xl="4">
<ElCard shadow="hover" class="stat-card-wrapper"> <ElCard shadow="hover" class="stat-card-wrapper">
<div class="stat-card"> <div class="stat-card">
<div <div
@@ -79,8 +80,8 @@
</div> </div>
</div> </div>
</ElCard> </ElCard>
</ElCol> </div>
</ElRow> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -90,6 +91,9 @@
defineOptions({ name: 'CommissionSummaryWidget' }) defineOptions({ name: 'CommissionSummaryWidget' })
// 错误信息
const errorMsg = ref<string>('')
// 佣金概览 // 佣金概览
const summary = ref<MyCommissionSummary>({ const summary = ref<MyCommissionSummary>({
total_commission: 0, total_commission: 0,
@@ -105,9 +109,19 @@
const res = await CommissionService.getMyCommissionSummary() const res = await CommissionService.getMyCommissionSummary()
if (res.code === 0) { if (res.code === 0) {
summary.value = res.data summary.value = res.data
errorMsg.value = ''
} else {
// 显示错误信息
errorMsg.value = res.msg || '获取佣金概览失败'
} }
} catch (error) { } catch (error: any) {
console.error('获取佣金概览失败:', error) console.error('获取佣金概览失败:', error)
// 尝试从错误响应中提取 msg
if (error?.response?.data?.msg) {
errorMsg.value = error.response.data.msg
} else {
errorMsg.value = '获取佣金概览失败'
}
} }
} }
@@ -118,26 +132,50 @@
<style lang="scss" scoped> <style lang="scss" scoped>
.commission-summary-widget { .commission-summary-widget {
&.error-state {
.error-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
color: var(--el-text-color-secondary);
p {
margin-top: 12px;
font-size: 14px;
color: var(--el-text-color-primary);
}
}
}
.stats-container {
display: flex;
gap: 16px;
flex-wrap: wrap;
}
.stat-card-wrapper { .stat-card-wrapper {
margin-bottom: 20px; flex: 1;
min-width: 180px;
:deep(.el-card__body) { :deep(.el-card__body) {
padding: 20px; padding: 16px;
} }
} }
.stat-card { .stat-card {
display: flex; display: flex;
gap: 16px; gap: 12px;
align-items: center; align-items: center;
.stat-icon { .stat-icon {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 48px; width: 40px;
height: 48px; height: 40px;
font-size: 24px; font-size: 20px;
color: white; color: white;
border-radius: 8px; border-radius: 8px;
flex-shrink: 0; flex-shrink: 0;
@@ -148,13 +186,14 @@
min-width: 0; min-width: 0;
.stat-label { .stat-label {
margin-bottom: 6px; margin-bottom: 4px;
font-size: 13px; font-size: 12px;
color: var(--el-text-color-secondary); color: var(--el-text-color-secondary);
white-space: nowrap;
} }
.stat-value { .stat-value {
font-size: 20px; font-size: 18px;
font-weight: 600; font-weight: 600;
color: var(--el-text-color-primary); color: var(--el-text-color-primary);
overflow: hidden; overflow: hidden;
@@ -165,10 +204,52 @@
} }
} }
@media (max-width: 768px) { @media (max-width: 1400px) {
.commission-summary-widget {
.stats-container {
gap: 12px;
}
.stat-card-wrapper {
min-width: 160px;
}
.stat-card {
.stat-content {
.stat-value {
font-size: 16px;
}
}
}
}
}
@media (max-width: 1200px) {
.commission-summary-widget { .commission-summary-widget {
.stat-card-wrapper { .stat-card-wrapper {
margin-bottom: 12px; flex: 1 1 calc(33.333% - 12px);
min-width: 200px;
}
}
}
@media (max-width: 768px) {
.commission-summary-widget {
.stats-container {
gap: 12px;
}
.stat-card-wrapper {
flex: 1 1 calc(50% - 8px);
min-width: 150px;
}
}
}
@media (max-width: 480px) {
.commission-summary-widget {
.stat-card-wrapper {
flex: 1 1 100%;
} }
} }
} }

View File

@@ -0,0 +1,235 @@
<template>
<ArtDataViewer
:service="loadDetailData"
:card-title="pageTitle"
@back="handleBack"
@refresh="handleRefresh"
>
<template #default="{ data }">
<ElDescriptions :column="2" border>
<ElDescriptionsItem label="充值单号">
{{ data.recharge_no }}
</ElDescriptionsItem>
<ElDescriptionsItem label="充值记录ID">
{{ data.id }}
</ElDescriptionsItem>
<ElDescriptionsItem label="代理钱包ID">
{{ data.agent_wallet_id }}
</ElDescriptionsItem>
<ElDescriptionsItem label="店铺ID">
{{ data.shop_id }}
</ElDescriptionsItem>
<ElDescriptionsItem label="店铺名称">
{{ data.shop_name }}
</ElDescriptionsItem>
<ElDescriptionsItem label="充值金额">
<span style="color: var(--el-color-success); font-weight: bold">
{{ formatCurrency(data.amount) }}
</span>
</ElDescriptionsItem>
<ElDescriptionsItem label="状态">
<ElTag :type="getStatusType(data.status)">
{{ getStatusText(data.status) }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="支付方式">
{{ getPaymentMethodText(data.payment_method) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="支付通道">
{{ data.payment_channel }}
</ElDescriptionsItem>
<ElDescriptionsItem label="支付配置ID">
{{ data.payment_config_id || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="第三方支付流水号" :span="2">
{{ data.payment_transaction_id || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="创建时间">
{{ formatDateTime(data.created_at) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="支付时间">
{{ data.paid_at ? formatDateTime(data.paid_at) : '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="完成时间">
{{ data.completed_at ? formatDateTime(data.completed_at) : '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="更新时间">
{{ formatDateTime(data.updated_at) }}
</ElDescriptionsItem>
</ElDescriptions>
<!-- 操作按钮 -->
<div style="margin-top: 20px; text-align: right">
<ElButton v-if="data.status === 1 && data.payment_method === 'offline'" type="primary" @click="handleConfirmPay(data)">
确认支付
</ElButton>
<ElButton @click="handleBack">返回列表</ElButton>
</div>
</template>
</ArtDataViewer>
<!-- 确认线下支付对话框 -->
<ElDialog
v-model="confirmPayDialogVisible"
title="确认线下充值"
width="400px"
@closed="handleConfirmPayDialogClosed"
>
<ElForm
ref="confirmPayFormRef"
:model="confirmPayForm"
:rules="confirmPayRules"
label-width="100px"
>
<ElFormItem label="充值单号">
<span>{{ currentRecharge?.recharge_no }}</span>
</ElFormItem>
<ElFormItem label="充值金额">
<span>{{ formatCurrency(currentRecharge?.amount || 0) }}</span>
</ElFormItem>
<ElFormItem label="操作密码" prop="operation_password">
<ElInput
v-model="confirmPayForm.operation_password"
type="password"
placeholder="请输入操作密码"
show-password
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="confirmPayDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleConfirmPaySubmit" :loading="confirmPayLoading">
确认支付
</ElButton>
</div>
</template>
</ElDialog>
</template>
<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router'
import { AgentRechargeService } from '@/api/modules'
import { ElMessage, ElTag } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import type {
AgentRecharge,
AgentRechargeStatus,
AgentRechargePaymentMethod,
ConfirmOfflinePaymentRequest
} from '@/types/api'
import ArtDataViewer from '@/components/core/views/ArtDataViewer.vue'
import { formatDateTime } from '@/utils/business/format'
import { RoutesAlias } from '@/router/routesAlias'
defineOptions({ name: 'AgentRechargeDetail' })
const route = useRoute()
const router = useRouter()
const rechargeId = computed(() => Number(route.params.id))
const pageTitle = computed(() => `充值订单详情 #${rechargeId.value}`)
const confirmPayDialogVisible = ref(false)
const confirmPayLoading = ref(false)
const currentRecharge = ref<AgentRecharge | null>(null)
const confirmPayFormRef = ref<FormInstance>()
const confirmPayRules = reactive<FormRules>({
operation_password: [{ required: true, message: '请输入操作密码', trigger: 'blur' }]
})
const confirmPayForm = reactive<ConfirmOfflinePaymentRequest>({
operation_password: ''
})
// 格式化货币 - 将分转换为元
const formatCurrency = (amount: number): string => {
return `¥${(amount / 100).toFixed(2)}`
}
// 获取状态标签类型
const getStatusType = (status: AgentRechargeStatus): 'warning' | 'success' | 'info' => {
const statusMap: Record<AgentRechargeStatus, 'warning' | 'success' | 'info'> = {
1: 'warning', // 待支付
2: 'success', // 已完成
3: 'info' // 已取消
}
return statusMap[status] || 'info'
}
// 获取状态文本
const getStatusText = (status: AgentRechargeStatus): string => {
const statusMap: Record<AgentRechargeStatus, string> = {
1: '待支付',
2: '已完成',
3: '已取消'
}
return statusMap[status] || '-'
}
// 获取支付方式文本
const getPaymentMethodText = (method: AgentRechargePaymentMethod): string => {
const methodMap: Record<AgentRechargePaymentMethod, string> = {
wechat: '微信在线支付',
offline: '线下转账'
}
return methodMap[method] || method
}
// 加载详情数据
const loadDetailData = async () => {
const res = await AgentRechargeService.getAgentRechargeById(rechargeId.value)
if (res.code === 0) {
return res.data
}
throw new Error(res.msg || '加载失败')
}
// 返回列表
const handleBack = () => {
router.push(RoutesAlias.AgentRecharge)
}
// 刷新数据
const handleRefresh = () => {
// ArtDataViewer 会自动重新加载数据
}
// 显示确认支付对话框
const handleConfirmPay = (data: AgentRecharge) => {
currentRecharge.value = data
confirmPayDialogVisible.value = true
}
// 确认支付对话框关闭后的清理
const handleConfirmPayDialogClosed = () => {
confirmPayFormRef.value?.resetFields()
confirmPayForm.operation_password = ''
currentRecharge.value = null
}
// 确认线下支付
const handleConfirmPaySubmit = async () => {
if (!confirmPayFormRef.value || !currentRecharge.value) return
await confirmPayFormRef.value.validate(async (valid) => {
if (valid) {
confirmPayLoading.value = true
try {
await AgentRechargeService.confirmOfflinePayment(currentRecharge.value!.id, {
operation_password: confirmPayForm.operation_password
})
ElMessage.success('确认支付成功')
confirmPayDialogVisible.value = false
confirmPayFormRef.value.resetFields()
// 刷新页面数据
handleRefresh()
} catch (error) {
console.error(error)
} finally {
confirmPayLoading.value = false
}
}
})
}
</script>

View File

@@ -0,0 +1,642 @@
<template>
<ArtTableFullScreen>
<div class="agent-recharge-page" id="table-full-screen">
<!-- 搜索栏 -->
<ArtSearchBar
v-model:filter="searchForm"
:items="searchFormItems"
:show-expand="false"
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
<ElCard shadow="never" class="art-table-card">
<!-- 表格头部 -->
<ArtTableHeader
:columnList="columnOptions"
v-model:columns="columnChecks"
@refresh="handleRefresh"
>
<template #left>
<ElButton type="primary" @click="showCreateDialog">创建充值订单</ElButton>
</template>
</ArtTableHeader>
<!-- 表格 -->
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="rechargeList"
:currentPage="pagination.page"
:pageSize="pagination.page_size"
:total="pagination.total"
:marginTop="10"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 创建充值订单对话框 -->
<ElDialog
v-model="createDialogVisible"
title="创建充值订单"
width="500px"
@closed="handleCreateDialogClosed"
>
<ElForm ref="createFormRef" :model="createForm" :rules="createRules" label-width="100px">
<ElFormItem label="充值金额" prop="amount">
<ElInputNumber
v-model="createForm.amount"
:min="100"
:max="1000000"
:precision="2"
:step="100"
style="width: 100%"
placeholder="请输入充值金额(元)"
/>
<div style="margin-top: 8px; font-size: 12px; color: var(--el-text-color-secondary)">
充值范围: ¥100 ~ ¥1,000,000
</div>
</ElFormItem>
<ElFormItem label="支付方式" prop="payment_method">
<ElSelect
v-model="createForm.payment_method"
placeholder="请选择支付方式"
style="width: 100%"
>
<ElOption label="微信在线支付" value="wechat" />
<!-- 只有平台用户才显示线下转账选项 -->
<ElOption
v-if="userStore.info.user_type === 1 || userStore.info.user_type === 2"
label="线下转账"
value="offline"
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="目标店铺" prop="shop_id">
<ElTreeSelect
v-model="createForm.shop_id"
:data="shopTreeData"
placeholder="请选择店铺"
filterable
clearable
check-strictly
:render-after-expand="false"
:props="{
label: 'shop_name',
value: 'id',
children: 'children'
}"
style="width: 100%"
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="createDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleCreateRecharge" :loading="createLoading">
确认创建
</ElButton>
</div>
</template>
</ElDialog>
<!-- 确认线下支付对话框 -->
<ElDialog
v-model="confirmPayDialogVisible"
title="确认线下充值"
width="400px"
@closed="handleConfirmPayDialogClosed"
>
<ElForm
ref="confirmPayFormRef"
:model="confirmPayForm"
:rules="confirmPayRules"
label-width="100px"
>
<ElFormItem label="充值单号">
<span>{{ currentRecharge?.recharge_no }}</span>
</ElFormItem>
<ElFormItem label="充值金额">
<span>{{ formatCurrency(currentRecharge?.amount || 0) }}</span>
</ElFormItem>
<ElFormItem label="操作密码" prop="operation_password">
<ElInput
v-model="confirmPayForm.operation_password"
type="password"
placeholder="请输入操作密码"
show-password
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="confirmPayDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleConfirmPay" :loading="confirmPayLoading">
确认支付
</ElButton>
</div>
</template>
</ElDialog>
</ElCard>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { h } from 'vue'
import { useRouter } from 'vue-router'
import { AgentRechargeService, ShopService } from '@/api/modules'
import { ElMessage, ElTag, ElTreeSelect } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import type {
AgentRecharge,
AgentRechargeQueryParams,
AgentRechargeStatus,
AgentRechargePaymentMethod,
CreateAgentRechargeRequest,
ConfirmOfflinePaymentRequest,
ShopResponse
} from '@/types/api'
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useUserStore } from '@/store/modules/user'
import { formatDateTime } from '@/utils/business/format'
import { RoutesAlias } from '@/router/routesAlias'
defineOptions({ name: 'AgentRechargeList' })
const router = useRouter()
const userStore = useUserStore()
const loading = ref(false)
const createLoading = ref(false)
const confirmPayLoading = ref(false)
const tableRef = ref()
const createDialogVisible = ref(false)
const confirmPayDialogVisible = ref(false)
const currentRecharge = ref<AgentRecharge | null>(null)
// 搜索表单初始值
const initialSearchState: AgentRechargeQueryParams = {
shop_id: undefined,
status: undefined,
start_date: '',
end_date: ''
}
// 搜索表单
const searchForm = reactive<AgentRechargeQueryParams>({ ...initialSearchState })
// 店铺选项
const shopOptions = ref<any[]>([])
const shopTreeData = ref<ShopResponse[]>([])
// 搜索表单配置
const searchFormItems: SearchFormItem[] = [
{
label: '店铺',
prop: 'shop_id',
type: 'select',
placeholder: '请选择店铺',
options: () =>
shopOptions.value.map((shop) => ({
label: shop.shop_name,
value: shop.id
})),
config: {
clearable: true,
filterable: true
}
},
{
label: '状态',
prop: 'status',
type: 'select',
placeholder: '请选择状态',
options: [
{ label: '待支付', value: 1 },
{ label: '已完成', value: 2 },
{ label: '已取消', value: 3 }
],
config: {
clearable: true
}
},
{
label: '创建时间',
prop: 'dateRange',
type: 'daterange',
config: {
clearable: true,
startPlaceholder: '开始日期',
endPlaceholder: '结束日期',
valueFormat: 'YYYY-MM-DD'
}
}
]
// 分页
const pagination = reactive({
page: 1,
page_size: 20,
total: 0
})
// 列配置
const columnOptions = [
{ label: 'ID', prop: 'id' },
{ label: '充值单号', prop: 'recharge_no' },
{ label: '店铺名称', prop: 'shop_name' },
{ label: '充值金额', prop: 'amount' },
{ label: '状态', prop: 'status' },
{ label: '支付方式', prop: 'payment_method' },
{ label: '支付通道', prop: 'payment_channel' },
{ label: '创建时间', prop: 'created_at' },
{ label: '支付时间', prop: 'paid_at' },
{ label: '完成时间', prop: 'completed_at' },
{ label: '操作', prop: 'actions' }
]
const createFormRef = ref<FormInstance>()
const confirmPayFormRef = ref<FormInstance>()
const createRules = reactive<FormRules>({
amount: [{ required: true, message: '请输入充值金额', trigger: 'blur' }],
payment_method: [{ required: true, message: '请选择支付方式', trigger: 'change' }],
shop_id: [{ required: true, message: '请选择目标店铺', trigger: 'change' }]
})
const confirmPayRules = reactive<FormRules>({
operation_password: [{ required: true, message: '请输入操作密码', trigger: 'blur' }]
})
const createForm = reactive<{ amount: number; payment_method: string; shop_id: number | null }>({
amount: 100,
payment_method: 'wechat',
shop_id: null
})
const confirmPayForm = reactive<ConfirmOfflinePaymentRequest>({
operation_password: ''
})
const rechargeList = ref<AgentRecharge[]>([])
// 格式化货币 - 将分转换为元
const formatCurrency = (amount: number): string => {
return `¥${(amount / 100).toFixed(2)}`
}
// 获取状态标签类型
const getStatusType = (status: AgentRechargeStatus): 'warning' | 'success' | 'info' => {
const statusMap: Record<AgentRechargeStatus, 'warning' | 'success' | 'info'> = {
1: 'warning', // 待支付
2: 'success', // 已完成
3: 'info' // 已取消
}
return statusMap[status] || 'info'
}
// 获取状态文本
const getStatusText = (status: AgentRechargeStatus): string => {
const statusMap: Record<AgentRechargeStatus, string> = {
1: '待支付',
2: '已完成',
3: '已取消'
}
return statusMap[status] || '-'
}
// 获取支付方式文本
const getPaymentMethodText = (method: AgentRechargePaymentMethod): string => {
const methodMap: Record<AgentRechargePaymentMethod, string> = {
wechat: '微信在线支付',
offline: '线下转账'
}
return methodMap[method] || method
}
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'id',
label: 'ID',
width: 80
},
{
prop: 'recharge_no',
label: '充值单号',
minWidth: 200,
formatter: (row: AgentRecharge) => {
return h(
'span',
{
style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;',
onClick: (e: MouseEvent) => {
e.stopPropagation()
handleViewDetail(row)
}
},
row.recharge_no
)
}
},
{
prop: 'shop_name',
label: '店铺名称',
minWidth: 150
},
{
prop: 'amount',
label: '充值金额',
width: 120,
formatter: (row: AgentRecharge) => formatCurrency(row.amount)
},
{
prop: 'status',
label: '状态',
width: 100,
formatter: (row: AgentRecharge) => {
return h(ElTag, { type: getStatusType(row.status) }, () => getStatusText(row.status))
}
},
{
prop: 'payment_method',
label: '支付方式',
width: 120,
formatter: (row: AgentRecharge) => getPaymentMethodText(row.payment_method)
},
{
prop: 'payment_channel',
label: '支付通道',
width: 120
},
{
prop: 'created_at',
label: '创建时间',
width: 180,
formatter: (row: AgentRecharge) => formatDateTime(row.created_at)
},
{
prop: 'paid_at',
label: '支付时间',
width: 180,
formatter: (row: AgentRecharge) => (row.paid_at ? formatDateTime(row.paid_at) : '-')
},
{
prop: 'completed_at',
label: '完成时间',
width: 180,
formatter: (row: AgentRecharge) => (row.completed_at ? formatDateTime(row.completed_at) : '-')
},
{
prop: 'actions',
label: '操作',
width: 150,
fixed: 'right',
formatter: (row: AgentRecharge) => {
const buttons: any[] = []
// 待支付且线下转账的订单可以确认支付
if (row.status === 1 && row.payment_method === 'offline') {
buttons.push(
h(
ElButton,
{
type: 'primary',
link: true,
size: 'small',
onClick: () => handleShowConfirmPay(row)
},
() => '确认支付'
)
)
}
buttons.push(
h(
ElButton,
{
type: 'primary',
link: true,
size: 'small',
onClick: () => handleViewDetail(row)
},
() => '查看详情'
)
)
return h('div', { style: 'display: flex; gap: 8px;' }, buttons)
}
}
])
onMounted(() => {
getTableData()
loadShops()
})
// 构建树形数据
const buildTreeData = (items: ShopResponse[]) => {
const map = new Map<number, ShopResponse & { children?: ShopResponse[] }>()
const tree: ShopResponse[] = []
// 先将所有项放入 map
items.forEach((item) => {
map.set(item.id, { ...item, children: [] })
})
// 构建树形结构
items.forEach((item) => {
const node = map.get(item.id)!
if (item.parent_id && map.has(item.parent_id)) {
// 有父节点,添加到父节点的 children 中
const parent = map.get(item.parent_id)!
if (!parent.children) parent.children = []
parent.children.push(node)
} else {
// 没有父节点或父节点不存在,作为根节点
tree.push(node)
}
})
return tree
}
// 加载店铺列表
const loadShops = async () => {
try {
const params: any = {
page: 1,
page_size: 9999 // 获取所有数据用于构建树形结构
}
const res = await ShopService.getShops(params)
if (res.code === 0) {
const items = res.data.items || []
// 保留平铺列表用于搜索
shopOptions.value = items
// 构建树形数据用于创建对话框
shopTreeData.value = buildTreeData(items)
}
} catch (error) {
console.error('Load shops failed:', error)
}
}
// 获取充值订单列表
const getTableData = async () => {
loading.value = true
try {
const params: AgentRechargeQueryParams = {
page: pagination.page,
page_size: pagination.page_size,
shop_id: searchForm.shop_id,
status: searchForm.status,
start_date: searchForm.start_date || undefined,
end_date: searchForm.end_date || undefined
}
const res = await AgentRechargeService.getAgentRecharges(params)
if (res.code === 0) {
rechargeList.value = res.data.list || []
pagination.total = res.data.total || 0
}
} catch (error) {
console.error(error)
} finally {
loading.value = false
}
}
// 重置搜索
const handleReset = () => {
Object.assign(searchForm, { ...initialSearchState })
pagination.page = 1
getTableData()
}
// 搜索
const handleSearch = () => {
// 处理日期范围
if (searchForm.dateRange && Array.isArray(searchForm.dateRange)) {
searchForm.start_date = searchForm.dateRange[0]
searchForm.end_date = searchForm.dateRange[1]
} else {
searchForm.start_date = ''
searchForm.end_date = ''
}
pagination.page = 1
getTableData()
}
// 刷新表格
const handleRefresh = () => {
getTableData()
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.page_size = newPageSize
getTableData()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.page = newCurrentPage
getTableData()
}
// 显示创建订单对话框
const showCreateDialog = async () => {
createDialogVisible.value = true
}
// 对话框关闭后的清理
const handleCreateDialogClosed = () => {
createFormRef.value?.resetFields()
createForm.amount = 100
createForm.payment_method = 'wechat'
createForm.shop_id = null
}
// 创建充值订单
const handleCreateRecharge = async () => {
if (!createFormRef.value) return
await createFormRef.value.validate(async (valid) => {
if (valid) {
createLoading.value = true
try {
const data: CreateAgentRechargeRequest = {
amount: createForm.amount * 100, // 元转分
payment_method: createForm.payment_method as AgentRechargePaymentMethod,
shop_id: createForm.shop_id!
}
await AgentRechargeService.createAgentRecharge(data)
ElMessage.success('充值订单创建成功')
createDialogVisible.value = false
createFormRef.value.resetFields()
await getTableData()
} catch (error) {
console.error(error)
} finally {
createLoading.value = false
}
}
})
}
// 显示确认支付对话框
const handleShowConfirmPay = (row: AgentRecharge) => {
currentRecharge.value = row
confirmPayDialogVisible.value = true
}
// 确认支付对话框关闭后的清理
const handleConfirmPayDialogClosed = () => {
confirmPayFormRef.value?.resetFields()
confirmPayForm.operation_password = ''
currentRecharge.value = null
}
// 确认线下支付
const handleConfirmPay = async () => {
if (!confirmPayFormRef.value || !currentRecharge.value) return
await confirmPayFormRef.value.validate(async (valid) => {
if (valid) {
confirmPayLoading.value = true
try {
await AgentRechargeService.confirmOfflinePayment(currentRecharge.value.id, {
operation_password: confirmPayForm.operation_password
})
ElMessage.success('确认支付成功')
confirmPayDialogVisible.value = false
confirmPayFormRef.value.resetFields()
await getTableData()
} catch (error) {
console.error(error)
} finally {
confirmPayLoading.value = false
}
}
})
}
// 查看详情
const handleViewDetail = (row: AgentRecharge) => {
router.push({
path: `${RoutesAlias.AgentRecharge}/detail/${row.id}`
})
}
</script>
<style scoped lang="scss">
.agent-recharge-page {
height: 100%;
}
</style>

View File

@@ -177,7 +177,12 @@
style="width: 100%" style="width: 100%"
> >
<ElOption label="钱包支付" value="wallet" /> <ElOption label="钱包支付" value="wallet" />
<ElOption label="线下支付" value="offline" /> <!-- 只有平台用户(user_type 1:超级管理员, 2:平台用户)才显示线下支付选项 -->
<ElOption
v-if="userStore.info.user_type === 1 || userStore.info.user_type === 2"
label="线下支付"
value="offline"
/>
</ElSelect> </ElSelect>
<div style="margin-top: 8px; font-size: 12px; color: var(--el-text-color-secondary)"> <div style="margin-top: 8px; font-size: 12px; color: var(--el-text-color-secondary)">
<template v-if="createForm.payment_method === 'wallet'"> <template v-if="createForm.payment_method === 'wallet'">
@@ -312,12 +317,15 @@
OrderCommissionStatus, OrderCommissionStatus,
StandaloneIotCard, StandaloneIotCard,
Device, Device,
PackageResponse PackageResponse,
PurchaseCheckRequest,
PurchaseCheckResponse
} from '@/types/api' } from '@/types/api'
import type { SearchFormItem } from '@/types' import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns' import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth' import { useAuth } from '@/composables/useAuth'
import { useTableContextMenu } from '@/composables/useTableContextMenu' import { useTableContextMenu } from '@/composables/useTableContextMenu'
import { useUserStore } from '@/store/modules/user'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue' import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue' import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue' import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
@@ -330,6 +338,7 @@
const { t } = useI18n() const { t } = useI18n()
const router = useRouter() const router = useRouter()
const { hasAuth } = useAuth() const { hasAuth } = useAuth()
const userStore = useUserStore()
// 使用表格右键菜单功能 // 使用表格右键菜单功能
const { const {
@@ -1000,6 +1009,43 @@
if (valid) { if (valid) {
createLoading.value = true createLoading.value = true
try { try {
// 获取资源ID (IoT卡ID或设备ID)
const resourceId =
createForm.order_type === 'single_card'
? createForm.iot_card_id
: createForm.device_id
if (!resourceId) {
ElMessage.error('请选择IoT卡或设备')
return
}
// 调用套餐购买预检接口
const checkData: PurchaseCheckRequest = {
order_type: createForm.order_type,
package_ids: createForm.package_ids,
resource_id: resourceId
}
const checkResponse = await OrderService.purchaseCheck(checkData)
// 检查预检结果
if (checkResponse.code === 0 && checkResponse.data) {
const checkResult = checkResponse.data
// 如果需要强充,显示确认对话框
if (checkResult.need_force_recharge) {
const confirmMessage = `${checkResult.message || '钱包余额不足'}\n\n套餐总价: ¥${(checkResult.total_package_amount! / 100).toFixed(2)}\n需要强充: ¥${(checkResult.force_recharge_amount! / 100).toFixed(2)}\n钱包到账: ¥${(checkResult.wallet_credit! / 100).toFixed(2)}\n实际支付: ¥${(checkResult.actual_payment! / 100).toFixed(2)}\n\n是否继续创建订单?`
await ElMessageBox.confirm(confirmMessage, '购买预检提示', {
confirmButtonText: '继续创建',
cancelButtonText: '取消',
type: 'warning'
})
}
}
// 预检通过或用户确认后,创建订单
const data: CreateOrderRequest = { const data: CreateOrderRequest = {
order_type: createForm.order_type, order_type: createForm.order_type,
package_ids: createForm.package_ids, package_ids: createForm.package_ids,
@@ -1020,7 +1066,11 @@
createDialogVisible.value = false createDialogVisible.value = false
formEl.resetFields() formEl.resetFields()
await getTableData() await getTableData()
} catch (error) { } catch (error: any) {
// 用户取消确认对话框
if (error === 'cancel') {
return
}
console.error(error) console.error(error)
} finally { } finally {
createLoading.value = false createLoading.value = false

View File

@@ -72,10 +72,7 @@
clearable clearable
style="flex: 1" style="flex: 1"
/> />
<CodeGeneratorButton <CodeGeneratorButton code-type="shop" @generated="handleCodeGenerated" />
code-type="shop"
@generated="handleCodeGenerated"
/>
</div> </div>
</ElFormItem> </ElFormItem>
</ElCol> </ElCol>
@@ -241,14 +238,14 @@
<div v-if="selectedRoleId" class="current-role-display"> <div v-if="selectedRoleId" class="current-role-display">
<div class="current-role-label">当前默认角色</div> <div class="current-role-label">当前默认角色</div>
<div class="current-role-value"> <div class="current-role-value">
{{ availableRoles.find((r) => r.role_id === selectedRoleId)?.role_name || '未知角色' }} {{
availableRoles.find((r) => r.role_id === selectedRoleId)?.role_name || '未知角色'
}}
</div> </div>
</div> </div>
<div v-else class="current-role-display no-role"> <div v-else class="current-role-display no-role">
<div class="current-role-label">当前默认角色</div> <div class="current-role-label">当前默认角色</div>
<div class="current-role-value"> <div class="current-role-value"> 暂未设置 </div>
暂未设置
</div>
</div> </div>
<!-- 默认角色选择 --> <!-- 默认角色选择 -->
@@ -463,7 +460,7 @@
] ]
// 显示对话框 // 显示对话框
const showDialog = (type: string, row?: ShopResponse) => { const showDialog = async (type: string, row?: ShopResponse) => {
dialogType.value = type dialogType.value = type
// 先清除验证状态 // 先清除验证状态
@@ -492,6 +489,9 @@
formData.init_phone = '' formData.init_phone = ''
formData.default_role_id = undefined formData.default_role_id = undefined
} else { } else {
// 新增模式下重新获取上级店铺列表
await loadParentShopList()
formData.id = 0 formData.id = 0
formData.shop_name = '' formData.shop_name = ''
formData.shop_code = '' formData.shop_code = ''
@@ -529,7 +529,9 @@
try { try {
await ShopService.deleteShop(row.id) await ShopService.deleteShop(row.id)
ElMessage.success('删除成功') ElMessage.success('删除成功')
getShopList() await getShopList()
// 删除成功后也更新上级店铺列表
await loadParentShopList()
} catch (error) { } catch (error) {
console.error(error) console.error(error)
} }
@@ -544,13 +546,13 @@
{ {
prop: 'shop_name', prop: 'shop_name',
label: '店铺名称', label: '店铺名称',
minWidth: 160, width: 160,
showOverflowTooltip: true showOverflowTooltip: true
}, },
{ {
prop: 'shop_code', prop: 'shop_code',
label: '店铺编号', label: '店铺编号',
width: 140, minWidth: 160,
showOverflowTooltip: true showOverflowTooltip: true
}, },
{ {
@@ -865,7 +867,9 @@
} }
dialogVisible.value = false dialogVisible.value = false
getShopList() await getShopList()
// 提交成功后也更新上级店铺列表
await loadParentShopList()
} catch (error) { } catch (error) {
console.error(error) console.error(error)
} finally { } finally {

View File

@@ -0,0 +1,307 @@
<template>
<div class="wechat-config-detail-page">
<ElCard shadow="never">
<!-- 页面头部 -->
<div class="detail-header">
<ElButton @click="handleBack">
<template #icon>
<ElIcon><ArrowLeft /></ElIcon>
</template>
返回
</ElButton>
<h2 class="detail-title">{{ pageTitle }}</h2>
</div>
<!-- 详情内容 -->
<DetailPage v-if="detailData" :sections="detailSections" :data="detailData" />
<!-- 加载中 -->
<div v-if="loading" class="loading-container">
<ElIcon class="is-loading"><Loading /></ElIcon>
<span>加载中...</span>
</div>
</ElCard>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElCard, ElButton, ElIcon, ElMessage } from 'element-plus'
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
import DetailPage from '@/components/common/DetailPage.vue'
import type { DetailSection } from '@/components/common/DetailPage.vue'
import { WechatConfigService } from '@/api/modules'
import type { WechatConfig, PaymentProviderType } from '@/types/api'
import { formatDateTime } from '@/utils/business/format'
import { RoutesAlias } from '@/router/routesAlias'
defineOptions({ name: 'WechatConfigDetail' })
const route = useRoute()
const router = useRouter()
const loading = ref(false)
const detailData = ref<WechatConfig | null>(null)
const configId = computed(() => Number(route.params.id))
const pageTitle = computed(() => `支付配置详情 #${configId.value}`)
// 获取支付渠道类型文本
const getProviderTypeText = (type: PaymentProviderType): string => {
const typeMap: Record<PaymentProviderType, string> = {
wechat: '微信直连',
fuiou: '富友支付'
}
return typeMap[type] || type
}
// 获取配置状态文本
const getConfigStatus = (value: string | undefined | null): string => {
if (!value) return '未配置'
return value === 'configured' ? '已配置' : value
}
// 基础详情配置
const baseSection: DetailSection = {
title: '基本信息',
fields: [
{ label: '配置ID', prop: 'id' },
{ label: '配置名称', prop: 'name' },
{
label: '支付渠道类型',
formatter: (_, data) => getProviderTypeText(data.provider_type)
},
{
label: '激活状态',
formatter: (_, data) => (data.is_active ? '已激活' : '未激活')
},
{
label: '配置描述',
prop: 'description',
formatter: (value) => value || '-',
fullWidth: true
},
{
label: '创建时间',
prop: 'created_at',
formatter: (value) => formatDateTime(value)
},
{
label: '更新时间',
prop: 'updated_at',
formatter: (value) => formatDateTime(value)
}
]
}
// 微信小程序配置
const miniappSection: DetailSection = {
title: '小程序配置',
fields: [
{
label: '小程序AppID',
prop: 'miniapp_app_id',
formatter: (value) => value || '-'
},
{
label: '小程序AppSecret',
prop: 'miniapp_app_secret',
formatter: (value) => value || '-'
}
]
}
// 微信公众号配置
const oaSection: DetailSection = {
title: '公众号配置',
fields: [
{
label: '公众号AppID',
prop: 'oa_app_id',
formatter: (value) => value || '-'
},
{
label: '公众号AppSecret',
prop: 'oa_app_secret',
formatter: (value) => value || '-'
},
{
label: '公众号Token',
prop: 'oa_token',
formatter: (value) => value || '-'
},
{
label: '公众号AES Key',
prop: 'oa_aes_key',
formatter: (value) => value || '-'
},
{
label: 'OAuth回调地址',
prop: 'oa_oauth_redirect_url',
formatter: (value) => value || '-',
fullWidth: true
}
]
}
// 微信支付配置
const wechatPaySection: DetailSection = {
title: '微信支付配置',
fields: [
{
label: '微信商户号',
prop: 'wx_mch_id',
formatter: (value) => value || '-'
},
{
label: '证书序列号',
prop: 'wx_serial_no',
formatter: (value) => value || '-'
},
{
label: 'APIv2密钥',
prop: 'wx_api_v2_key',
formatter: (value) => value || '-'
},
{
label: 'APIv3密钥',
prop: 'wx_api_v3_key',
formatter: (value) => value || '-'
},
{
label: '支付回调地址',
prop: 'wx_notify_url',
formatter: (value) => value || '-',
fullWidth: true
},
{
label: '支付证书',
prop: 'wx_cert_content',
formatter: (value) => getConfigStatus(value)
},
{
label: '支付密钥',
prop: 'wx_key_content',
formatter: (value) => getConfigStatus(value)
}
]
}
// 富友支付配置
const fuiouSection: DetailSection = {
title: '富友支付配置',
fields: [
{
label: '富友API地址',
prop: 'fy_api_url',
formatter: (value) => value || '-',
fullWidth: true
},
{
label: '富友机构号',
prop: 'fy_ins_cd',
formatter: (value) => value || '-'
},
{
label: '富友商户号',
prop: 'fy_mchnt_cd',
formatter: (value) => value || '-'
},
{
label: '富友终端号',
prop: 'fy_term_id',
formatter: (value) => value || '-'
},
{
label: '支付回调地址',
prop: 'fy_notify_url',
formatter: (value) => value || '-'
},
{
label: '富友私钥',
prop: 'fy_private_key',
formatter: (value) => getConfigStatus(value)
},
{
label: '富友公钥',
prop: 'fy_public_key',
formatter: (value) => getConfigStatus(value)
}
]
}
// 根据支付类型动态构建详情配置
const detailSections = computed<DetailSection[]>(() => {
if (!detailData.value) return [baseSection]
const sections = [baseSection]
if (detailData.value.provider_type === 'wechat') {
sections.push(miniappSection, oaSection, wechatPaySection)
} else if (detailData.value.provider_type === 'fuiou') {
sections.push(fuiouSection)
}
return sections
})
// 返回列表
const handleBack = () => {
router.push(RoutesAlias.WechatConfig)
}
// 获取详情数据
const fetchDetail = async () => {
loading.value = true
try {
const res = await WechatConfigService.getWechatConfigById(configId.value)
if (res.code === 0) {
detailData.value = res.data
}
} catch (error) {
console.error(error)
ElMessage.error('获取支付配置详情失败')
} finally {
loading.value = false
}
}
onMounted(() => {
fetchDetail()
})
</script>
<style scoped lang="scss">
.wechat-config-detail-page {
padding: 20px;
}
.detail-header {
display: flex;
align-items: center;
gap: 16px;
padding-bottom: 16px;
.detail-title {
margin: 0;
font-size: 20px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
gap: 12px;
color: var(--el-text-color-secondary);
.el-icon {
font-size: 32px;
}
}
</style>

View File

@@ -0,0 +1,863 @@
<template>
<ArtTableFullScreen>
<div class="wechat-config-page" id="table-full-screen">
<!-- 搜索栏 -->
<ArtSearchBar
v-model:filter="searchForm"
:items="searchFormItems"
label-width="100"
:show-expand="false"
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
<ElCard shadow="never" class="art-table-card">
<!-- 表格头部 -->
<ArtTableHeader
:columnList="columnOptions"
v-model:columns="columnChecks"
@refresh="handleRefresh"
>
<template #left>
<ElButton type="primary" @click="showCreateDialog">新增支付配置</ElButton>
</template>
</ArtTableHeader>
<!-- 表格 -->
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="configList"
:currentPage="pagination.page"
:pageSize="pagination.page_size"
:total="pagination.total"
:marginTop="10"
:row-class-name="getRowClassName"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
@row-contextmenu="handleRowContextMenu"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 鼠标悬浮提示 -->
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
<!-- 支付配置操作右键菜单 -->
<ArtMenuRight
ref="configOperationMenuRef"
:menu-items="configOperationMenuItems"
:menu-width="140"
@select="handleConfigOperationMenuSelect"
/>
<!-- 创建/编辑对话框 -->
<ElDialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '新增支付配置' : '编辑支付配置'"
width="800px"
@closed="handleDialogClosed"
>
<ElForm ref="formRef" :model="form" :rules="rules" label-width="130px">
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="配置名称" prop="name">
<ElInput v-model="form.name" placeholder="请输入配置名称" />
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="支付渠道类型" prop="provider_type">
<ElSelect
v-model="form.provider_type"
placeholder="请选择支付渠道类型"
style="width: 100%"
:disabled="dialogType === 'edit'"
>
<ElOption label="微信直连" value="wechat" />
<ElOption label="富友支付" value="fuiou" />
</ElSelect>
</ElFormItem>
</ElCol>
</ElRow>
<ElFormItem label="配置描述" prop="description">
<ElInput
v-model="form.description"
type="textarea"
:rows="2"
placeholder="请输入配置描述"
/>
</ElFormItem>
<!-- 微信相关配置 -->
<template v-if="form.provider_type === 'wechat'">
<ElDivider content-position="left">小程序配置</ElDivider>
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="小程序AppID" prop="miniapp_app_id">
<ElInput v-model="form.miniapp_app_id" placeholder="请输入小程序AppID" />
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="小程序AppSecret" prop="miniapp_app_secret">
<ElInput
v-model="form.miniapp_app_secret"
type="password"
show-password
placeholder="请输入小程序AppSecret"
/>
</ElFormItem>
</ElCol>
</ElRow>
<ElDivider content-position="left">公众号配置</ElDivider>
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="公众号AppID" prop="oa_app_id">
<ElInput v-model="form.oa_app_id" placeholder="请输入公众号AppID" />
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="公众号AppSecret" prop="oa_app_secret">
<ElInput
v-model="form.oa_app_secret"
type="password"
show-password
placeholder="请输入公众号AppSecret"
/>
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="公众号Token" prop="oa_token">
<ElInput
v-model="form.oa_token"
type="password"
show-password
placeholder="请输入公众号Token"
/>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="公众号AES Key" prop="oa_aes_key">
<ElInput
v-model="form.oa_aes_key"
type="password"
show-password
placeholder="请输入公众号AES加密Key"
/>
</ElFormItem>
</ElCol>
</ElRow>
<ElFormItem label="OAuth回调地址" prop="oa_oauth_redirect_url">
<ElInput v-model="form.oa_oauth_redirect_url" placeholder="请输入OAuth回调地址" />
</ElFormItem>
<ElDivider content-position="left">微信支付配置</ElDivider>
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="微信商户号" prop="wx_mch_id">
<ElInput v-model="form.wx_mch_id" placeholder="请输入微信商户号" />
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="证书序列号" prop="wx_serial_no">
<ElInput v-model="form.wx_serial_no" placeholder="请输入微信证书序列号" />
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="APIv2密钥" prop="wx_api_v2_key">
<ElInput
v-model="form.wx_api_v2_key"
type="password"
show-password
placeholder="请输入微信APIv2密钥"
/>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="APIv3密钥" prop="wx_api_v3_key">
<ElInput
v-model="form.wx_api_v3_key"
type="password"
show-password
placeholder="请输入微信APIv3密钥"
/>
</ElFormItem>
</ElCol>
</ElRow>
<ElFormItem label="支付回调地址" prop="wx_notify_url">
<ElInput v-model="form.wx_notify_url" placeholder="请输入微信支付回调地址" />
</ElFormItem>
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="支付证书" prop="wx_cert_content">
<ElInput
v-model="form.wx_cert_content"
type="textarea"
:rows="3"
placeholder="请粘贴PEM格式证书内容"
/>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="支付密钥" prop="wx_key_content">
<ElInput
v-model="form.wx_key_content"
type="textarea"
:rows="3"
placeholder="请粘贴PEM格式密钥内容"
/>
</ElFormItem>
</ElCol>
</ElRow>
</template>
<!-- 富友支付配置 -->
<template v-if="form.provider_type === 'fuiou'">
<ElDivider content-position="left">富友支付配置</ElDivider>
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="富友API地址" prop="fy_api_url">
<ElInput v-model="form.fy_api_url" placeholder="请输入富友API地址" />
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="富友机构号" prop="fy_ins_cd">
<ElInput v-model="form.fy_ins_cd" placeholder="请输入富友机构号" />
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="富友商户号" prop="fy_mchnt_cd">
<ElInput v-model="form.fy_mchnt_cd" placeholder="请输入富友商户号" />
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="富友终端号" prop="fy_term_id">
<ElInput v-model="form.fy_term_id" placeholder="请输入富友终端号" />
</ElFormItem>
</ElCol>
</ElRow>
<ElFormItem label="支付回调地址" prop="fy_notify_url">
<ElInput v-model="form.fy_notify_url" placeholder="请输入富友支付回调地址" />
</ElFormItem>
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="富友私钥" prop="fy_private_key">
<ElInput
v-model="form.fy_private_key"
type="textarea"
:rows="3"
placeholder="请粘贴PEM格式私钥内容"
/>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="富友公钥" prop="fy_public_key">
<ElInput
v-model="form.fy_public_key"
type="textarea"
:rows="3"
placeholder="请粘贴PEM格式公钥内容"
/>
</ElFormItem>
</ElCol>
</ElRow>
</template>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit" :loading="submitLoading">
提交
</ElButton>
</div>
</template>
</ElDialog>
</ElCard>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { h } from 'vue'
import { useRouter } from 'vue-router'
import { WechatConfigService } from '@/api/modules'
import { ElMessage, ElTag, ElMessageBox, ElSwitch, ElButton } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import type {
WechatConfig,
WechatConfigQueryParams,
PaymentProviderType,
CreateWechatConfigRequest,
UpdateWechatConfigRequest
} from '@/types/api'
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth'
import { useTableContextMenu } from '@/composables/useTableContextMenu'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import { formatDateTime } from '@/utils/business/format'
import { RoutesAlias } from '@/router/routesAlias'
defineOptions({ name: 'WechatConfigList' })
const { hasAuth } = useAuth()
const router = useRouter()
const loading = ref(false)
const submitLoading = ref(false)
const tableRef = ref()
const dialogVisible = ref(false)
const dialogType = ref<'add' | 'edit'>('add')
const formRef = ref<FormInstance>()
// 右键菜单
const configOperationMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const currentOperatingConfig = ref<WechatConfig | null>(null)
// 搜索表单初始值
const initialSearchState = {
provider_type: undefined as PaymentProviderType | undefined,
is_active: undefined as number | undefined
}
// 搜索表单
const searchForm = reactive({ ...initialSearchState })
// 搜索表单配置
const searchFormItems: SearchFormItem[] = [
{
label: '支付渠道类型',
prop: 'provider_type',
type: 'select',
placeholder: '请选择支付渠道类型',
options: [
{ label: '微信直连', value: 'wechat' },
{ label: '富友支付', value: 'fuiou' }
],
config: {
clearable: true
}
},
{
label: '激活状态',
prop: 'is_active',
type: 'select',
placeholder: '请选择激活状态',
options: [
{ label: '已激活', value: 1 },
{ label: '未激活', value: 0 }
],
config: {
clearable: true
}
}
]
// 分页
const pagination = reactive({
page: 1,
page_size: 20,
total: 0
})
// 列配置
const columnOptions = [
{ label: 'ID', prop: 'id' },
{ label: '配置名称', prop: 'name' },
{ label: '支付渠道类型', prop: 'provider_type' },
{ label: '激活状态', prop: 'is_active' },
{ label: '商户号', prop: 'merchant_id' },
{ label: '创建时间', prop: 'created_at' },
{ label: '更新时间', prop: 'updated_at' }
]
// 动态表单验证规则
const rules = computed<FormRules>(() => {
const baseRules: FormRules = {
name: [{ required: true, message: '请输入配置名称', trigger: 'blur' }],
provider_type: [{ required: true, message: '请选择支付渠道类型', trigger: 'change' }]
}
// 只在创建模式下添加必填验证
if (dialogType.value === 'add') {
if (form.provider_type === 'wechat') {
// 微信直连必填字段
baseRules.wx_mch_id = [{ required: true, message: '请输入微信商户号', trigger: 'blur' }]
baseRules.wx_api_v3_key = [
{ required: true, message: '请输入微信APIv3密钥', trigger: 'blur' }
]
baseRules.wx_cert_content = [
{ required: true, message: '请输入微信支付证书内容', trigger: 'blur' }
]
baseRules.wx_key_content = [
{ required: true, message: '请输入微信支付密钥内容', trigger: 'blur' }
]
baseRules.wx_serial_no = [
{ required: true, message: '请输入微信证书序列号', trigger: 'blur' }
]
baseRules.wx_notify_url = [
{ required: true, message: '请输入微信支付回调地址', trigger: 'blur' }
]
} else if (form.provider_type === 'fuiou') {
// 富友支付必填字段
baseRules.fy_api_url = [{ required: true, message: '请输入富友API地址', trigger: 'blur' }]
baseRules.fy_ins_cd = [{ required: true, message: '请输入富友机构号', trigger: 'blur' }]
baseRules.fy_mchnt_cd = [{ required: true, message: '请输入富友商户号', trigger: 'blur' }]
baseRules.fy_term_id = [{ required: true, message: '请输入富友终端号', trigger: 'blur' }]
baseRules.fy_private_key = [{ required: true, message: '请输入富友私钥', trigger: 'blur' }]
baseRules.fy_public_key = [{ required: true, message: '请输入富友公钥', trigger: 'blur' }]
baseRules.fy_notify_url = [
{ required: true, message: '请输入富友支付回调地址', trigger: 'blur' }
]
}
}
return baseRules
})
const initialFormState = {
name: '',
provider_type: 'wechat' as PaymentProviderType,
description: '',
miniapp_app_id: '',
miniapp_app_secret: '',
oa_app_id: '',
oa_app_secret: '',
oa_token: '',
oa_aes_key: '',
oa_oauth_redirect_url: '',
wx_mch_id: '',
wx_api_v2_key: '',
wx_api_v3_key: '',
wx_notify_url: '',
wx_serial_no: '',
wx_cert_content: '',
wx_key_content: '',
fy_api_url: '',
fy_ins_cd: '',
fy_mchnt_cd: '',
fy_term_id: '',
fy_notify_url: '',
fy_private_key: '',
fy_public_key: ''
}
const form = reactive({ id: 0, ...initialFormState })
const configList = ref<WechatConfig[]>([])
// 获取支付渠道类型文本
const getProviderTypeText = (type: PaymentProviderType): string => {
const typeMap: Record<PaymentProviderType, string> = {
wechat: '微信直连',
fuiou: '富友支付'
}
return typeMap[type] || type
}
// 获取商户号
const getMerchantId = (row: WechatConfig): string => {
if (row.provider_type === 'wechat') {
return row.wx_mch_id || '-'
}
if (row.provider_type === 'fuiou') {
return row.fy_mchnt_cd || '-'
}
return '-'
}
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'id',
label: 'ID',
width: 80
},
{
prop: 'name',
label: '配置名称',
minWidth: 180,
showOverflowTooltip: true,
formatter: (row: WechatConfig) => {
return h(
'span',
{
style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;',
onClick: (e: MouseEvent) => {
e.stopPropagation()
handleNameClick(row)
}
},
row.name
)
}
},
{
prop: 'provider_type',
label: '支付渠道类型',
width: 130,
formatter: (row: WechatConfig) => getProviderTypeText(row.provider_type)
},
{
prop: 'is_active',
label: '激活状态',
width: 100,
formatter: (row: WechatConfig) => {
return h(ElSwitch, {
modelValue: row.is_active,
activeText: '已激活',
inactiveText: '未激活',
inlinePrompt: true,
'onUpdate:modelValue': (val) => handleStatusChange(row, val)
})
}
},
{
prop: 'merchant_id',
label: '商户号',
width: 150,
formatter: (row: WechatConfig) => getMerchantId(row)
},
{
prop: 'created_at',
label: '创建时间',
width: 180,
formatter: (row: WechatConfig) => formatDateTime(row.created_at)
},
{
prop: 'updated_at',
label: '更新时间',
width: 180,
formatter: (row: WechatConfig) => formatDateTime(row.updated_at)
}
])
// 监听支付渠道类型变化,清除表单验证
watch(
() => form.provider_type,
() => {
nextTick(() => {
formRef.value?.clearValidate()
})
}
)
onMounted(() => {
getTableData()
})
// 获取配置列表
const getTableData = async () => {
loading.value = true
try {
const params: WechatConfigQueryParams = {
page: pagination.page,
page_size: pagination.page_size,
provider_type: searchForm.provider_type,
is_active: searchForm.is_active !== undefined ? searchForm.is_active === 1 : undefined
}
const res = await WechatConfigService.getWechatConfigs(params)
if (res.code === 0) {
configList.value = res.data.items || []
pagination.total = res.data.total || 0
}
} catch (error) {
console.error(error)
} finally {
loading.value = false
}
}
// 重置搜索
const handleReset = () => {
Object.assign(searchForm, { ...initialSearchState })
pagination.page = 1
getTableData()
}
// 搜索
const handleSearch = () => {
pagination.page = 1
getTableData()
}
// 刷新表格
const handleRefresh = () => {
getTableData()
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.page_size = newPageSize
getTableData()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.page = newCurrentPage
getTableData()
}
// 显示创建对话框
const showCreateDialog = () => {
dialogType.value = 'add'
Object.assign(form, { id: 0, ...initialFormState })
dialogVisible.value = true
nextTick(() => {
formRef.value?.clearValidate()
})
}
// 显示编辑对话框
const showEditDialog = (row: WechatConfig) => {
dialogType.value = 'edit'
Object.assign(form, {
id: row.id,
name: row.name,
provider_type: row.provider_type,
description: row.description,
miniapp_app_id: row.miniapp_app_id,
miniapp_app_secret: '',
oa_app_id: row.oa_app_id,
oa_app_secret: '',
oa_token: '',
oa_aes_key: '',
oa_oauth_redirect_url: row.oa_oauth_redirect_url,
wx_mch_id: row.wx_mch_id,
wx_api_v2_key: '',
wx_api_v3_key: '',
wx_notify_url: row.wx_notify_url,
wx_serial_no: row.wx_serial_no,
wx_cert_content: '',
wx_key_content: '',
fy_api_url: row.fy_api_url,
fy_ins_cd: row.fy_ins_cd,
fy_mchnt_cd: row.fy_mchnt_cd,
fy_term_id: row.fy_term_id,
fy_notify_url: row.fy_notify_url,
fy_private_key: '',
fy_public_key: ''
})
dialogVisible.value = true
nextTick(() => {
formRef.value?.clearValidate()
})
}
// 对话框关闭后的清理
const handleDialogClosed = () => {
formRef.value?.resetFields()
Object.assign(form, { id: 0, ...initialFormState })
}
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (valid) {
submitLoading.value = true
try {
if (dialogType.value === 'add') {
const data: CreateWechatConfigRequest = {
name: form.name,
provider_type: form.provider_type,
description: form.description || undefined,
miniapp_app_id: form.miniapp_app_id || undefined,
miniapp_app_secret: form.miniapp_app_secret || undefined,
oa_app_id: form.oa_app_id || undefined,
oa_app_secret: form.oa_app_secret || undefined,
oa_token: form.oa_token || undefined,
oa_aes_key: form.oa_aes_key || undefined,
oa_oauth_redirect_url: form.oa_oauth_redirect_url || undefined,
wx_mch_id: form.wx_mch_id || undefined,
wx_api_v2_key: form.wx_api_v2_key || undefined,
wx_api_v3_key: form.wx_api_v3_key || undefined,
wx_notify_url: form.wx_notify_url || undefined,
wx_serial_no: form.wx_serial_no || undefined,
wx_cert_content: form.wx_cert_content || undefined,
wx_key_content: form.wx_key_content || undefined,
fy_api_url: form.fy_api_url || undefined,
fy_ins_cd: form.fy_ins_cd || undefined,
fy_mchnt_cd: form.fy_mchnt_cd || undefined,
fy_term_id: form.fy_term_id || undefined,
fy_notify_url: form.fy_notify_url || undefined,
fy_private_key: form.fy_private_key || undefined,
fy_public_key: form.fy_public_key || undefined
}
await WechatConfigService.createWechatConfig(data)
ElMessage.success('创建成功')
} else {
const data: UpdateWechatConfigRequest = {
name: form.name,
description: form.description || undefined
}
// 只有填写了新值才更新敏感字段
if (form.miniapp_app_secret) data.miniapp_app_secret = form.miniapp_app_secret
if (form.oa_app_secret) data.oa_app_secret = form.oa_app_secret
if (form.oa_token) data.oa_token = form.oa_token
if (form.oa_aes_key) data.oa_aes_key = form.oa_aes_key
if (form.wx_api_v2_key) data.wx_api_v2_key = form.wx_api_v2_key
if (form.wx_api_v3_key) data.wx_api_v3_key = form.wx_api_v3_key
if (form.wx_cert_content) data.wx_cert_content = form.wx_cert_content
if (form.wx_key_content) data.wx_key_content = form.wx_key_content
if (form.fy_private_key) data.fy_private_key = form.fy_private_key
if (form.fy_public_key) data.fy_public_key = form.fy_public_key
// 非敏感字段总是更新
if (form.miniapp_app_id) data.miniapp_app_id = form.miniapp_app_id
if (form.oa_app_id) data.oa_app_id = form.oa_app_id
if (form.oa_oauth_redirect_url) data.oa_oauth_redirect_url = form.oa_oauth_redirect_url
if (form.wx_mch_id) data.wx_mch_id = form.wx_mch_id
if (form.wx_notify_url) data.wx_notify_url = form.wx_notify_url
if (form.wx_serial_no) data.wx_serial_no = form.wx_serial_no
if (form.fy_api_url) data.fy_api_url = form.fy_api_url
if (form.fy_ins_cd) data.fy_ins_cd = form.fy_ins_cd
if (form.fy_mchnt_cd) data.fy_mchnt_cd = form.fy_mchnt_cd
if (form.fy_term_id) data.fy_term_id = form.fy_term_id
if (form.fy_notify_url) data.fy_notify_url = form.fy_notify_url
await WechatConfigService.updateWechatConfig(form.id, data)
ElMessage.success('更新成功')
}
dialogVisible.value = false
formRef.value?.resetFields()
await getTableData()
} catch (error) {
console.error(error)
} finally {
submitLoading.value = false
}
}
})
}
// 激活/停用状态切换
const handleStatusChange = async (row: WechatConfig, newStatus: string | number | boolean) => {
const newBoolStatus = Boolean(newStatus)
const oldStatus = row.is_active
row.is_active = newBoolStatus
try {
if (newBoolStatus) {
await WechatConfigService.activateWechatConfig(row.id)
ElMessage.success('激活成功')
} else {
await WechatConfigService.deactivateWechatConfig(row.id)
ElMessage.success('停用成功')
}
await getTableData()
} catch (error) {
row.is_active = oldStatus
console.error(error)
}
}
// 删除配置
const handleDelete = async (row: WechatConfig) => {
try {
await ElMessageBox.confirm(`确定要删除支付配置"${row.name}"吗?`, '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
await WechatConfigService.deleteWechatConfig(row.id)
ElMessage.success('删除成功')
await getTableData()
} catch (error) {
if (error !== 'cancel') {
console.error(error)
}
}
}
// 查看详情
const handleViewDetail = (row: WechatConfig) => {
router.push({
path: `${RoutesAlias.WechatConfig}/detail/${row.id}`
})
}
// 处理名称点击
const handleNameClick = (row: WechatConfig) => {
handleViewDetail(row)
}
// 支付配置操作菜单项配置
const configOperationMenuItems = computed((): MenuItemType[] => {
const items: MenuItemType[] = []
// 编辑
items.push({
key: 'edit',
label: '编辑'
})
// 删除
items.push({
key: 'delete',
label: '删除'
})
return items
})
// 显示支付配置操作右键菜单
const showConfigOperationMenu = (e: MouseEvent, row: WechatConfig) => {
e.preventDefault()
e.stopPropagation()
currentOperatingConfig.value = row
configOperationMenuRef.value?.show(e)
}
// 处理表格行右键菜单
const handleRowContextMenu = (row: WechatConfig, column: any, event: MouseEvent) => {
showConfigOperationMenu(event, row)
}
// 处理支付配置操作菜单选择
const handleConfigOperationMenuSelect = (item: MenuItemType) => {
if (!currentOperatingConfig.value) return
switch (item.key) {
case 'edit':
showEditDialog(currentOperatingConfig.value)
break
case 'delete':
handleDelete(currentOperatingConfig.value)
break
}
}
// 使用表格右键菜单功能
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
</script>
<style scoped lang="scss">
.wechat-config-page {
height: 100%;
}
:deep(.el-table__row.table-row-with-context-menu) {
cursor: pointer;
}
</style>