fix: bug
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 2m27s

This commit is contained in:
sexygoat
2026-04-11 16:51:54 +08:00
parent 33e15314ac
commit 4f02d6faf0
25 changed files with 387 additions and 317 deletions

View File

@@ -1,6 +1,6 @@
<template>
<div class="detail-container">
<ElCard v-for="(section, index) in sections" :key="index" class="detail-section">
<ElCard v-for="(section, index) in sections" :key="index" class="detail-section" shadow="never">
<template #header>
<div class="section-title">{{ section.title }}</div>
</template>

View File

@@ -400,7 +400,7 @@
},
"settings": {
"title": "设置管理",
"wechatPayConfiguration": "微信支付配置",
"wechatPayConfiguration": "微信配置",
"detailsOfPaymentConfiguration": "支付配置详情",
"withdrawalSettings": "提现配置"
}

View File

@@ -578,7 +578,7 @@ export const asyncRoutes: AppRouteRecord[] = [
icon: '&#xe715;'
},
children: [
// 微信支付配置
// 微信配置
{
path: 'wechat-config',
name: 'WechatConfig',

View File

@@ -77,7 +77,7 @@ export enum RoutesAlias {
// 设置管理
WithdrawalSettings = '/settings/withdrawal-settings', // 提现配置
WechatConfig = '/settings/wechat-config', // 微信支付配置
WechatConfig = '/settings/wechat-config', // 微信配置
WechatConfigDetail = '/settings/wechat-config/detail', // 微信支付配置详情
// 轮询管理

View File

@@ -5,8 +5,10 @@
// 充值状态
export enum AgentRechargeStatus {
PENDING = 1, // 待支付
COMPLETED = 2, // 已完成
CANCELLED = 3 // 已取消
PAID = 2, // 已支付
COMPLETED = 3, // 已完成
CLOSED = 4, // 已关闭
REFUNDED = 5 // 已退款
}
// 支付方式
@@ -24,6 +26,7 @@ export interface AgentRecharge {
shop_name: string
amount: number // 充值金额(单位:分)
status: AgentRechargeStatus
status_name: string // 状态名称
payment_method: AgentRechargePaymentMethod
payment_channel: AgentRechargePaymentChannel
payment_config_id: number | null

View File

@@ -20,7 +20,7 @@ export interface PaymentMerchantSetting {
publicKey: string
notifyUrl?: string
}
// 微信支付配置
// 微信配置
wechat?: {
enabled: boolean
appId: string

View File

@@ -5,7 +5,7 @@
// 支付渠道类型
export type PaymentProviderType = 'wechat' | 'wechat_v2' | 'fuiou'
// 微信支付配置
// 微信配置
export interface WechatConfig {
id: number
name: string
@@ -24,7 +24,7 @@ export interface WechatConfig {
oa_aes_key: string
oa_oauth_redirect_url: string
// 微信支付配置
// 微信配置
wx_mch_id: string
wx_api_v2_key: string
wx_api_v3_key: string
@@ -79,7 +79,7 @@ export interface CreateWechatConfigRequest {
oa_aes_key?: string
oa_oauth_redirect_url?: string
// 微信支付配置
// 微信配置
wx_mch_id?: string
wx_api_v2_key?: string
wx_api_v3_key?: string
@@ -115,7 +115,7 @@ export interface UpdateWechatConfigRequest {
oa_aes_key?: string
oa_oauth_redirect_url?: string
// 微信支付配置
// 微信配置
wx_mch_id?: string
wx_api_v2_key?: string
wx_api_v3_key?: string

View File

@@ -131,18 +131,34 @@
</div>
<div class="panel-body">
<ElEmpty v-if="filteredAvailableRoles.length === 0" description="暂无角色" />
<ElRadioGroup v-else v-model="roleToAdd" class="role-list">
<div v-for="role in filteredAvailableRoles" :key="role.ID" class="role-item">
<ElRadio :label="role.ID" :disabled="selectedRoles.includes(role.ID)">
<span class="role-info">
<span>{{ role.role_name }}</span>
<ElTag :type="role.role_type === 1 ? 'primary' : 'success'" size="small">
{{ role.role_type === 1 ? '平台角色' : '客户角色' }}
</ElTag>
</span>
</ElRadio>
</div>
</ElRadioGroup>
<template v-else-if="isPlatformUser">
<ElCheckboxGroup v-model="rolesToAdd" class="role-list">
<div v-for="role in filteredAvailableRoles" :key="role.ID" class="role-item">
<ElCheckbox :label="role.ID" :disabled="selectedRoles.includes(role.ID)">
<span class="role-info">
<span>{{ role.role_name }}</span>
<ElTag :type="role.role_type === 1 ? 'primary' : 'success'" size="small">
{{ role.role_type === 1 ? '平台角色' : '客户角色' }}
</ElTag>
</span>
</ElCheckbox>
</div>
</ElCheckboxGroup>
</template>
<template v-else>
<ElRadioGroup v-model="roleToAdd" class="role-list">
<div v-for="role in filteredAvailableRoles" :key="role.ID" class="role-item">
<ElRadio :label="role.ID" :disabled="selectedRoles.includes(role.ID)">
<span class="role-info">
<span>{{ role.role_name }}</span>
<ElTag :type="role.role_type === 1 ? 'primary' : 'success'" size="small">
{{ role.role_type === 1 ? '平台角色' : '客户角色' }}
</ElTag>
</span>
</ElRadio>
</div>
</ElRadioGroup>
</template>
</div>
</div>
@@ -152,7 +168,7 @@
type="primary"
:icon="'ArrowRight'"
@click="addRoles"
:disabled="!roleToAdd"
:disabled="isPlatformUser ? rolesToAdd.length === 0 : !roleToAdd"
>
分配
</ElButton>
@@ -205,7 +221,16 @@
<script setup lang="ts">
import { h } from 'vue'
import { useRoute } from 'vue-router'
import { FormInstance, ElSwitch, ElCheckbox, ElCheckboxGroup, ElTag, ElEmpty } from 'element-plus'
import {
FormInstance,
ElSwitch,
ElCheckbox,
ElCheckboxGroup,
ElTag,
ElEmpty,
ElRadioGroup,
ElRadio
} from 'element-plus'
import { ElMessageBox, ElMessage } from 'element-plus'
import type { FormRules } from 'element-plus'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
@@ -232,10 +257,14 @@
const currentAccountType = ref<number>(0)
const selectedRoles = ref<number[]>([])
const allRoles = ref<PlatformRole[]>([])
const roleToAdd = ref<number | undefined>(undefined)
const rolesToAdd = ref<number[]>([]) // 多选时使用
const roleToAdd = ref<number | undefined>(undefined) // 单选时使用
const leftRoleFilter = ref('')
const rightRoleFilter = ref('')
// 是否为平台用户(平台用户可多选,其他单选)
const isPlatformUser = computed(() => currentAccountType.value === 2)
// 定义表单搜索初始值
const initialSearchState = {
name: '',
@@ -648,6 +677,7 @@
currentAccountName.value = row.username
currentAccountType.value = row.user_type
selectedRoles.value = []
rolesToAdd.value = []
roleToAdd.value = undefined
leftRoleFilter.value = ''
rightRoleFilter.value = ''
@@ -671,17 +701,26 @@
}
}
// 添加角色(单选)
// 添加角色
const addRoles = async () => {
if (!roleToAdd.value) return
try {
// 新角色会替换之前的角色
const newRoles = [roleToAdd.value]
let newRoles: number[]
// 平台用户可以分配多个角色,其他用户只能分配一个角色
if (isPlatformUser.value) {
if (rolesToAdd.value.length === 0) return
// 平台用户可以分配多个角色
newRoles = [...new Set([...selectedRoles.value, ...rolesToAdd.value])]
} else {
// 其他用户只能分配一个角色(单选)
if (!roleToAdd.value) return
newRoles = [...selectedRoles.value, roleToAdd.value]
}
await AccountService.assignRolesToAccount(currentAccountId.value, newRoles)
selectedRoles.value = newRoles
rolesToAdd.value = []
roleToAdd.value = undefined
ElMessage.success('角色分配成功')

View File

@@ -15,9 +15,17 @@
reserve-keyword
:remote-method="handlePackageSearch"
:loading="packageSearchLoading"
:disabled="!props.seriesId"
clearable
style="width: 100%"
>
<template v-if="!props.seriesId">
<ElOption :value="null" disabled>
<span style="color: var(--el-text-color-warning)"
>该设备未关联套餐系列无法购买套餐</span
>
</ElOption>
</template>
<ElOption
v-for="pkg in packageOptions"
:key="pkg.id"
@@ -143,32 +151,54 @@
// 监听对话框打开,加载套餐列表
watch(visible, async (newVal) => {
if (newVal) {
if (!props.seriesId) {
packageOptions.value = []
return
}
await loadPackages()
}
})
// 监听 seriesId 变化,重新加载套餐列表
watch(
() => props.seriesId,
async (newSeriesId) => {
if (visible.value && newSeriesId) {
await loadPackages()
}
}
)
let loadPackagesPromise: Promise<void> | null = null
const loadPackages = async () => {
if (!props.seriesId) return
// 防止重复调用
if (loadPackagesPromise) return
try {
packageSearchLoading.value = true
const response = await PackageManageService.getPackages({
loadPackagesPromise = PackageManageService.getPackages({
series_id: props.seriesId,
page: 1,
page_size: 20,
status: 1, // 只获取启用的套餐
shelf_status: 1 // 只获取已上架的套餐
})
if (response.code === 0 && response.data) {
packageOptions.value = response.data.items || []
}
.then((response) => {
if (response.code === 0 && response.data) {
packageOptions.value = response.data.items || []
}
})
.finally(() => {
packageSearchLoading.value = false
loadPackagesPromise = null
})
await loadPackagesPromise
} catch (error: any) {
console.error('加载套餐列表失败:', error)
console.log(error?.message || '加载套餐列表失败')
} finally {
packageSearchLoading.value = false
}
}

View File

@@ -1045,14 +1045,12 @@
{
prop: 'max_sim_slots',
label: '最大插槽数',
width: 100,
align: 'center'
width: 100
},
{
prop: 'bound_card_count',
label: '已绑定卡数',
width: 110,
align: 'center',
formatter: (row: Device) => {
const color = row.bound_card_count > 0 ? '#67c23a' : '#909399'
return h('span', { style: { color, fontWeight: 'bold' } }, row.bound_card_count)
@@ -1091,6 +1089,7 @@
prop: 'software_version',
label: '固件版本',
width: 120,
showOverflowTooltip: true,
formatter: (row: Device) => row.software_version || '-'
},
{

View File

@@ -1015,13 +1015,15 @@
{
prop: 'virtual_no',
label: '卡虚拟号',
width: 160,
width: 180,
showOverflowTooltip: true,
formatter: (row: StandaloneIotCard) => row.virtual_no || '-'
},
{
prop: 'device_virtual_no',
label: '绑定设备虚拟号',
width: 160,
width: 180,
showOverflowTooltip: true,
formatter: (row: StandaloneIotCard) => row.device_virtual_no || '-'
},
{
@@ -1033,7 +1035,8 @@
{
prop: 'carrier_name',
label: '运营商',
width: 150
width: 150,
showOverflowTooltip: true
},
{
prop: 'shop_name',

View File

@@ -384,7 +384,8 @@
{
prop: 'carrier_name',
label: '运营商',
width: 120
width: 120,
showOverflowTooltip: true
},
{
prop: 'card_category',

View File

@@ -515,7 +515,8 @@
{
prop: 'shop_code',
label: '店铺编码',
minWidth: 150
minWidth: 150,
showOverflowTooltip: true
},
{
prop: 'shop_name',

View File

@@ -118,18 +118,34 @@
</div>
<div class="panel-body">
<ElEmpty v-if="filteredAvailableRoles.length === 0" description="暂无角色" />
<ElCheckboxGroup v-else v-model="rolesToAdd" class="role-list">
<div v-for="role in filteredAvailableRoles" :key="role.ID" class="role-item">
<ElCheckbox :label="role.ID" :disabled="selectedRoles.includes(role.ID)">
<span class="role-info">
<span>{{ role.role_name }}</span>
<ElTag :type="role.role_type === 1 ? 'primary' : 'success'" size="small">
{{ role.role_type === 1 ? '平台角色' : '客户角色' }}
</ElTag>
</span>
</ElCheckbox>
</div>
</ElCheckboxGroup>
<template v-else-if="isPlatformUser">
<ElCheckboxGroup v-model="rolesToAdd" class="role-list">
<div v-for="role in filteredAvailableRoles" :key="role.ID" class="role-item">
<ElCheckbox :label="role.ID" :disabled="selectedRoles.includes(role.ID)">
<span class="role-info">
<span>{{ role.role_name }}</span>
<ElTag :type="role.role_type === 1 ? 'primary' : 'success'" size="small">
{{ role.role_type === 1 ? '平台角色' : '客户角色' }}
</ElTag>
</span>
</ElCheckbox>
</div>
</ElCheckboxGroup>
</template>
<template v-else>
<ElRadioGroup v-model="roleToAdd" class="role-list">
<div v-for="role in filteredAvailableRoles" :key="role.ID" class="role-item">
<ElRadio :label="role.ID" :disabled="selectedRoles.includes(role.ID)">
<span class="role-info">
<span>{{ role.role_name }}</span>
<ElTag :type="role.role_type === 1 ? 'primary' : 'success'" size="small">
{{ role.role_type === 1 ? '平台角色' : '客户角色' }}
</ElTag>
</span>
</ElRadio>
</div>
</ElRadioGroup>
</template>
</div>
</div>
@@ -139,7 +155,7 @@
type="primary"
:icon="'ArrowRight'"
@click="addRoles"
:disabled="rolesToAdd.length === 0"
:disabled="isPlatformUser ? rolesToAdd.length === 0 : !roleToAdd"
>
添加
</ElButton>
@@ -204,6 +220,8 @@
ElInput,
ElCheckbox,
ElCheckboxGroup,
ElRadioGroup,
ElRadio,
ElEmpty
} from 'element-plus'
import { ArrowLeft } from '@element-plus/icons-vue'
@@ -258,9 +276,13 @@
const selectedRoles = ref<number[]>([])
const allRoles = ref<PlatformRole[]>([])
const rolesToAdd = ref<number[]>([])
const roleToAdd = ref<number | undefined>(undefined) // 单选时使用
const leftRoleFilter = ref('')
const rightRoleFilter = ref('')
// 是否为平台用户(平台用户可多选,其他单选)
const isPlatformUser = computed(() => currentAccountType.value === 2)
// 搜索表单初始值
const initialSearchState = {
username: '',
@@ -619,6 +641,7 @@
currentAccountType.value = row.user_type
selectedRoles.value = []
rolesToAdd.value = []
roleToAdd.value = undefined
leftRoleFilter.value = ''
rightRoleFilter.value = ''
@@ -643,28 +666,25 @@
// 批量添加角色
const addRoles = async () => {
if (rolesToAdd.value.length === 0) return
try {
let newRoles: number[]
// 代理账号只能分配个角色,会覆盖之前的角色
if (currentAccountType.value === 3) {
if (rolesToAdd.value.length > 1) {
ElMessage.warning('代理账号只能分配个角色')
return
}
// 只保留新选择的一个角色
newRoles = rolesToAdd.value
} else {
// 其他账号类型可以分配多个角色
// 平台用户可以分配个角色,其他用户只能分配一个角色
if (isPlatformUser.value) {
if (rolesToAdd.value.length === 0) return
// 平台用户可以分配个角色
newRoles = [...new Set([...selectedRoles.value, ...rolesToAdd.value])]
} else {
// 其他用户只能分配一个角色(单选)
if (!roleToAdd.value) return
newRoles = [...selectedRoles.value, roleToAdd.value]
}
await AccountService.assignRolesToAccount(currentAccountId.value, newRoles)
selectedRoles.value = newRoles
rolesToAdd.value = []
roleToAdd.value = undefined
ElMessage.success('角色分配成功')
// 刷新列表以更新角色显示

View File

@@ -1,16 +1,16 @@
<template>
<ElCard shadow="never" class="commission-stats-widget" v-if="stats">
<ElCard shadow="never" class="commission-stats-widget">
<template #header>
<div class="card-header">
<div class="header-left">
<span class="header-title">佣金资金统计</span>
</div>
<div class="header-right">
<div class="header-right" v-if="stats">
<span class="date-range">{{ startTime }} ~ {{ endTime }}</span>
</div>
</div>
</template>
<div class="stats-info">
<div v-if="stats" class="stats-info">
<div class="stats-card">
<div
class="stats-icon"
@@ -60,7 +60,7 @@
</div>
</div>
</div>
<div class="stats-breakdown">
<div class="stats-breakdown" v-if="stats">
<div class="breakdown-item">
<span class="breakdown-label">成本价差占比</span>
<span class="breakdown-value">{{ formatPercent(stats.cost_diff_percent) }}</span>
@@ -70,9 +70,7 @@
<span class="breakdown-value">{{ formatPercent(stats.one_time_percent) }}</span>
</div>
</div>
</ElCard>
<ElCard shadow="never" v-else class="commission-stats-widget empty-state">
<div class="empty-content">
<div v-if="!stats" class="empty-content">
<i class="el-icon-info" style="font-size: 48px; color: var(--el-text-color-placeholder)"></i>
<p>暂无佣金统计数据</p>
</div>

View File

@@ -1,5 +1,5 @@
<template>
<ElCard shadow="never" class="commission-summary-widget" v-if="!errorMsg">
<ElCard shadow="never" class="commission-summary-widget">
<template #header>
<div class="card-header">
<div class="header-left">
@@ -7,77 +7,77 @@
</div>
</div>
</template>
<div class="stats-info">
<div v-if="!errorMsg" class="stats-info">
<div class="stats-card">
<div
class="stats-icon"
class="stat-icon"
style="background: linear-gradient(135deg, #ff6b6b 0%, #ff8e53 100%)"
>
<i class="iconfont-sys">&#xe722;</i>
</div>
<div class="stats-content">
<div class="stats-label">预充值余额</div>
<div class="stats-value">{{ formatMoney(summary.main_balance) }}</div>
<div class="stat-content">
<div class="stat-label">预充值余额</div>
<div class="stat-value">{{ formatMoney(summary.main_balance) }}</div>
</div>
</div>
<div class="stats-card">
<div
class="stats-icon"
class="stat-icon"
style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%)"
>
<i class="iconfont-sys">&#xe71d;</i>
</div>
<div class="stats-content">
<div class="stats-label">总佣金</div>
<div class="stats-value">{{ formatMoney(summary.total_commission) }}</div>
<div class="stat-content">
<div class="stat-label">总佣金</div>
<div class="stat-value">{{ formatMoney(summary.total_commission) }}</div>
</div>
</div>
<div class="stats-card">
<div
class="stats-icon"
class="stat-icon"
style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%)"
>
<i class="iconfont-sys">&#xe71e;</i>
</div>
<div class="stats-content">
<div class="stats-label">可提现佣金</div>
<div class="stats-value">{{ formatMoney(summary.available_commission) }}</div>
<div class="stat-content">
<div class="stat-label">可提现佣金</div>
<div class="stat-value">{{ formatMoney(summary.available_commission) }}</div>
</div>
</div>
<div class="stats-card">
<div
class="stats-icon"
class="stat-icon"
style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)"
>
<i class="iconfont-sys">&#xe720;</i>
</div>
<div class="stats-content">
<div class="stats-label">冻结佣金</div>
<div class="stats-value">{{ formatMoney(summary.frozen_commission) }}</div>
<div class="stat-content">
<div class="stat-label">冻结佣金</div>
<div class="stat-value">{{ formatMoney(summary.frozen_commission) }}</div>
</div>
</div>
<div class="stats-card">
<div
class="stats-icon"
class="stat-icon"
style="background: linear-gradient(135deg, #fa709a 0%, #fee140 100%)"
>
<i class="iconfont-sys">&#xe71f;</i>
</div>
<div class="stats-content">
<div class="stats-label">提现中佣金</div>
<div class="stats-value">{{ formatMoney(summary.withdrawing_commission) }}</div>
<div class="stat-content">
<div class="stat-label">提现中佣金</div>
<div class="stat-value">{{ formatMoney(summary.withdrawing_commission) }}</div>
</div>
</div>
<div class="stats-card">
<div
class="stats-icon"
class="stat-icon"
style="background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)"
>
<i class="iconfont-sys">&#xe721;</i>
</div>
<div class="stats-content">
<div class="stats-label">已提现佣金</div>
<div class="stats-value">{{ formatMoney(summary.withdrawn_commission) }}</div>
<div class="stat-content">
<div class="stat-label">已提现佣金</div>
<div class="stat-value">{{ formatMoney(summary.withdrawn_commission) }}</div>
</div>
</div>
</div>
@@ -104,9 +104,7 @@
</div>
</div>
</div>
</ElCard>
<ElCard shadow="never" v-else class="commission-summary-widget error-state">
<div class="error-content">
<div v-if="errorMsg" class="error-content">
<i class="el-icon-warning" style="font-size: 48px; color: var(--el-color-warning)"></i>
<p>{{ errorMsg }}</p>
</div>
@@ -250,7 +248,7 @@
background: var(--el-fill-color-blank);
border-radius: 8px;
.stats-icon {
.stat-icon {
display: flex;
flex-shrink: 0;
align-items: center;
@@ -262,17 +260,17 @@
border-radius: 8px;
}
.stats-content {
.stat-content {
flex: 1;
min-width: 0;
.stats-label {
.stat-label {
margin-bottom: 4px;
font-size: 13px;
color: var(--el-text-color-secondary);
}
.stats-value {
.stat-value {
overflow: hidden;
font-size: 16px;
font-weight: 600;
@@ -320,20 +318,18 @@
}
}
&.error-state {
.error-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
color: var(--el-text-color-secondary);
.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);
}
p {
margin-top: 12px;
font-size: 14px;
color: var(--el-text-color-primary);
}
}
}

View File

@@ -1,18 +1,18 @@
<template>
<ElCard shadow="never" class="daily-commission-stats-widget" v-if="statsList.length > 0">
<ElCard shadow="never" class="daily-commission-stats-widget">
<template #header>
<div class="card-header">
<div class="header-left">
<span class="header-title">每日佣金统计</span>
</div>
<div class="header-right">
<div class="header-right" v-if="statsList.length > 0">
<span class="date-range"
>{{ formatDateRange(startDate) }} ~ {{ formatDateRange(endDate) }}</span
>
</div>
</div>
</template>
<div class="stats-info">
<div v-if="statsList.length > 0" class="stats-info">
<div class="stats-card">
<div
class="stats-icon"
@@ -38,7 +38,7 @@
</div>
</div>
</div>
<div class="stats-chart">
<div class="stats-chart" v-if="statsList.length > 0">
<div class="chart-bar" v-for="item in statsList" :key="item.date">
<div class="bar-label">{{ formatDateShort(item.date) }}</div>
<div class="bar-value" :style="{ height: getBarHeight(item.total_amount) + 'px' }">
@@ -48,9 +48,7 @@
</div>
</div>
</div>
</ElCard>
<ElCard shadow="never" v-else class="daily-commission-stats-widget empty-state">
<div class="empty-content">
<div v-if="statsList.length === 0" class="empty-content">
<i class="el-icon-info" style="font-size: 48px; color: var(--el-text-color-placeholder)"></i>
<p>暂无佣金统计数据</p>
</div>

View File

@@ -53,21 +53,28 @@
}
// 获取状态标签类型
const getStatusType = (status: AgentRechargeStatus): 'warning' | 'success' | 'info' => {
const statusMap: Record<AgentRechargeStatus, 'warning' | 'success' | 'info'> = {
const getStatusType = (
status: AgentRechargeStatus
): 'warning' | 'success' | 'info' | 'danger' => {
const statusMap: Record<AgentRechargeStatus, 'warning' | 'success' | 'info' | 'danger'> = {
1: 'warning', // 待支付
2: 'success', // 已完成
3: 'info' // 已取消
2: 'success', // 已支付
3: 'success', // 已完成
4: 'info', // 已关闭
5: 'danger' // 已退款
}
return statusMap[status] || 'info'
}
// 获取状态文本
const getStatusText = (status: AgentRechargeStatus): string => {
// 获取状态文本(优先使用 status_name否则使用本地映射
const getStatusText = (status: AgentRechargeStatus, statusName?: string): string => {
if (statusName) return statusName
const statusMap: Record<AgentRechargeStatus, string> = {
1: '待支付',
2: '已完成',
3: '已取消'
2: '已支付',
3: '已完成',
4: '已关闭',
5: '已退款'
}
return statusMap[status] || '-'
}
@@ -97,7 +104,9 @@
{
label: '状态',
render: (data) =>
h(ElTag, { type: getStatusType(data.status) }, () => getStatusText(data.status))
h(ElTag, { type: getStatusType(data.status) }, () =>
getStatusText(data.status, data.status_name)
)
}
]
},

View File

@@ -327,21 +327,28 @@
}
// 获取状态标签类型
const getStatusType = (status: AgentRechargeStatus): 'warning' | 'success' | 'info' => {
const statusMap: Record<AgentRechargeStatus, 'warning' | 'success' | 'info'> = {
const getStatusType = (
status: AgentRechargeStatus
): 'warning' | 'success' | 'info' | 'danger' => {
const statusMap: Record<AgentRechargeStatus, 'warning' | 'success' | 'info' | 'danger'> = {
1: 'warning', // 待支付
2: 'success', // 已完成
3: 'info' // 已取消
2: 'success', // 已支付
3: 'success', // 已完成
4: 'info', // 已关闭
5: 'danger' // 已退款
}
return statusMap[status] || 'info'
}
// 获取状态文本
const getStatusText = (status: AgentRechargeStatus): string => {
// 获取状态文本(优先使用 status_name否则使用本地映射
const getStatusText = (status: AgentRechargeStatus, statusName?: string): string => {
if (statusName) return statusName
const statusMap: Record<AgentRechargeStatus, string> = {
1: '待支付',
2: '已完成',
3: '已取消'
2: '已支付',
3: '已完成',
4: '已关闭',
5: '已退款'
}
return statusMap[status] || '-'
}
@@ -391,7 +398,9 @@
label: '状态',
width: 100,
formatter: (row: AgentRecharge) => {
return h(ElTag, { type: getStatusType(row.status) }, () => getStatusText(row.status))
return h(ElTag, { type: getStatusType(row.status) }, () =>
getStatusText(row.status, row.status_name)
)
}
},
{

View File

@@ -15,38 +15,40 @@
<!-- 详情内容 -->
<DetailPage v-if="detailData" :sections="detailSections" :data="detailData" />
<!-- 订单列表 -->
<div
v-if="detailData && detailData.items && detailData.items.length > 0"
class="order-items-section"
>
<h3 class="section-title">订单项列表</h3>
<ElTable :data="detailData.items" border style="margin-top: 10px">
<ElTableColumn prop="package_name" label="套餐名称" min-width="150" />
<ElTableColumn prop="package_type" label="套餐类型" width="120">
<template #default="{ row }">
{{
row.package_type === 'formal'
? '正式套餐'
: row.package_type === 'addon'
? '加油包'
: '-'
}}
</template>
</ElTableColumn>
<ElTableColumn prop="quantity" label="数量" width="100" />
<ElTableColumn prop="unit_price" label="单价" width="120">
<template #default="{ row }">
{{ formatCurrency(row.unit_price) }}
</template>
</ElTableColumn>
<ElTableColumn prop="amount" label="金额" width="120">
<template #default="{ row }">
{{ formatCurrency(row.amount) }}
</template>
</ElTableColumn>
</ElTable>
</div>
<!-- 订单列表 -->
<ElCard style="margin-top: 20px" shadow="never">
<div
v-if="detailData && detailData.items && detailData.items.length > 0"
class="order-items-section"
>
<h3 class="section-title">订单列表</h3>
<ElTable :data="detailData.items" border>
<ElTableColumn prop="package_name" label="套餐名称" />
<ElTableColumn prop="package_type" label="套餐类型">
<template #default="{ row }">
{{
row.package_type === 'formal'
? '正式套餐'
: row.package_type === 'addon'
? '加油包'
: '-'
}}
</template>
</ElTableColumn>
<ElTableColumn prop="quantity" label="数量" />
<ElTableColumn prop="unit_price" label="单价">
<template #default="{ row }">
{{ formatCurrency(row.unit_price) }}
</template>
</ElTableColumn>
<ElTableColumn prop="amount" label="金额">
<template #default="{ row }">
{{ formatCurrency(row.amount) }}
</template>
</ElTableColumn>
</ElTable>
</div>
</ElCard>
<!-- 加载中 -->
<div v-if="loading" class="loading-container">
@@ -286,10 +288,6 @@
}
.order-items-section {
padding-top: 20px;
margin-top: 30px;
border-top: 1px solid var(--el-border-color-lighter);
.section-title {
margin: 0 0 16px;
font-size: 16px;

View File

@@ -7,6 +7,7 @@
:items="searchFormItems"
:show-expand="false"
@reset="handleReset"
label-width="100"
@search="handleSearch"
></ArtSearchBar>
@@ -77,6 +78,7 @@
:loading="cardSearchLoading"
style="width: 100%"
clearable
@change="handleIotCardChange"
>
<ElOption
v-for="card in iotCardOptions"
@@ -104,6 +106,7 @@
:loading="deviceSearchLoading"
style="width: 100%"
clearable
@change="handleDeviceChange"
>
<ElOption
v-for="device in deviceOptions"
@@ -125,15 +128,9 @@
v-model="createForm.package_id"
:placeholder="'请选择套餐'"
filterable
remote
reserve-keyword
:remote-method="handlePackageSearch"
:loading="packageSearchLoading"
:disabled="
(!createForm.iot_card_id && !createForm.device_id) ||
(createForm.order_type === 'single_card' && !createForm.iot_card_id) ||
(createForm.order_type === 'device' && !createForm.device_id)
"
:disabled="!createForm.iot_card_id && !createForm.device_id"
clearable
style="width: 100%"
>
@@ -151,6 +148,16 @@
</div>
</ElOption>
</ElSelect>
<template v-if="noSeriesId">
<div style="color: var(--el-text-color-warning); font-size: 12px; margin-top: 4px">
该设备未关联套餐系列无法购买套餐
</div>
</template>
<template v-else-if="packagesEmptyWithSeriesId">
<div style="color: var(--el-text-color-info); font-size: 12px; margin-top: 4px">
该系列暂无可用套餐
</div>
</template>
</ElFormItem>
<ElFormItem label="支付方式" prop="payment_method">
<ElSelect
@@ -379,7 +386,8 @@
config: {
maxlength: 20,
clearable: true
}
},
labelWidth: '100'
},
{
label: '支付状态',
@@ -567,7 +575,9 @@
order_type: 'single_card' as OrderType,
package_id: undefined as number | undefined, // 前端使用单选
iot_card_id: null as number | null,
iot_card_series_id: null as number | null, // IoT卡的套餐系列ID
device_id: null as number | null,
device_series_id: null as number | null, // 设备的套餐系列ID
payment_method: 'wallet' as OrderPaymentMethod, // 默认使用钱包支付
payment_voucher_key: undefined as string | undefined // 线下支付凭证
})
@@ -577,6 +587,7 @@
// 套餐搜索相关
const packageOptions = ref<PackageResponse[]>([])
const packageSearchLoading = ref(false)
const isLoadingPackagesFromSelection = ref(false)
// IoT卡搜索相关
const iotCardOptions = ref<StandaloneIotCard[]>([])
@@ -586,46 +597,60 @@
const deviceOptions = ref<Device[]>([])
const deviceSearchLoading = ref(false)
// 搜索套餐根据套餐名称可选按series_id筛选
const searchPackages = async (query: string, seriesId?: number) => {
packageSearchLoading.value = true
try {
const res = await PackageManageService.getPackages({
package_name: query || undefined,
series_id: seriesId,
page: 1,
page_size: 20,
status: 1, // 只获取启用的套餐
shelf_status: 1 // 只获取已上架的套餐
})
if (res.code === 0) {
packageOptions.value = res.data.items || []
}
} catch (error) {
console.error('Search packages failed:', error)
// 套餐列表为空但有seriesId的提示
const packagesEmptyWithSeriesId = ref(false)
// 检查是否选择了卡/设备但没有关联套餐系列
const noSeriesId = computed(() => {
if (createForm.order_type === 'single_card' && createForm.iot_card_id) {
return !createForm.iot_card_series_id
}
if (createForm.order_type === 'device' && createForm.device_id) {
return !createForm.device_series_id
}
return false
})
// IoT卡选择变化时根据series_id加载套餐列表
const handleIotCardChange = (cardId: number | null) => {
if (!cardId) {
createForm.iot_card_series_id = null
createForm.package_id = undefined
packageOptions.value = []
packagesEmptyWithSeriesId.value = false
return
}
const selectedCard = iotCardOptions.value.find((card) => card.id === cardId)
createForm.iot_card_series_id = selectedCard?.series_id || null
createForm.package_id = undefined
packageOptions.value = []
packagesEmptyWithSeriesId.value = false
if (selectedCard?.series_id) {
loadDefaultPackages(selectedCard.series_id)
} else {
packageOptions.value = []
} finally {
packageSearchLoading.value = false
}
}
// 套餐远程搜索方法自动使用当前选中的IOT卡/设备的series_id
const handlePackageSearch = (query: string) => {
let seriesId: number | undefined
// 如果是单卡订单并且已选择IOT卡使用该卡的series_id筛选
if (createForm.order_type === 'single_card' && createForm.iot_card_id) {
const selectedCard = iotCardOptions.value.find((card) => card.id === createForm.iot_card_id)
if (selectedCard && selectedCard.series_id) {
seriesId = selectedCard.series_id
}
} else if (createForm.order_type === 'device' && createForm.device_id) {
// 如果是设备订单并且已选择设备,使用设备的series_id筛选
const selectedDevice = deviceOptions.value.find((dev) => dev.id === createForm.device_id)
if (selectedDevice && selectedDevice.series_id) {
seriesId = selectedDevice.series_id
}
// 设备选择变化时,根据series_id加载套餐列表
const handleDeviceChange = (deviceId: number | null) => {
if (!deviceId) {
createForm.device_series_id = null
createForm.package_id = undefined
packageOptions.value = []
packagesEmptyWithSeriesId.value = false
return
}
const selectedDevice = deviceOptions.value.find((dev) => dev.id === deviceId)
createForm.device_series_id = selectedDevice?.series_id || null
createForm.package_id = undefined
packageOptions.value = []
packagesEmptyWithSeriesId.value = false
if (selectedDevice?.series_id) {
loadDefaultPackages(selectedDevice.series_id)
} else {
packageOptions.value = []
}
searchPackages(query, seriesId)
}
// 搜索IoT卡根据ICCID
@@ -848,39 +873,12 @@
}
])
// 当选择IOT卡时根据series_id筛选套餐
// 当订单类型切换时,清空已选择的套餐
watch(
() => createForm.iot_card_id,
(newCardId) => {
if (newCardId && createForm.order_type === 'single_card') {
// 找到选中的IOT卡
const selectedCard = iotCardOptions.value.find((card) => card.id === newCardId)
if (selectedCard && selectedCard.series_id) {
// 根据series_id重新加载套餐列表
loadDefaultPackages(selectedCard.series_id)
} else {
// 如果没有series_id加载所有套餐
loadDefaultPackages()
}
}
}
)
// 当选择设备时根据series_id筛选套餐
watch(
() => createForm.device_id,
(newDeviceId) => {
if (newDeviceId && createForm.order_type === 'device') {
// 找到选中的设备
const selectedDevice = deviceOptions.value.find((dev) => dev.id === newDeviceId)
if (selectedDevice && selectedDevice.series_id) {
// 根据series_id重新加载套餐列表
loadDefaultPackages(selectedDevice.series_id)
} else {
// 如果没有series_id加载所有套餐
loadDefaultPackages()
}
}
() => createForm.order_type,
() => {
createForm.package_id = undefined
packageOptions.value = []
}
)
@@ -955,13 +953,14 @@
// 显示创建订单对话框
const showCreateDialog = async () => {
createDialogVisible.value = true
// 加载IoT卡和设备列表套餐列表在选择IoT卡/设备后才加载
await Promise.all([loadDefaultIotCards(), loadDefaultDevices()])
// IoT卡和设备列表在用户搜索时才加载,不在此处预加载
}
// 加载默认套餐列表可选按series_id筛选
const loadDefaultPackages = async (seriesId?: number) => {
packageSearchLoading.value = true
isLoadingPackagesFromSelection.value = true
packagesEmptyWithSeriesId.value = false
try {
const res = await PackageManageService.getPackages({
series_id: seriesId,
@@ -972,50 +971,17 @@
})
if (res.code === 0) {
packageOptions.value = res.data.items || []
// seriesId存在但返回空列表时显示提示
if (seriesId && (!res.data.items || res.data.items.length === 0)) {
packagesEmptyWithSeriesId.value = true
}
}
} catch (error) {
console.error('Load default packages failed:', error)
packageOptions.value = []
} finally {
packageSearchLoading.value = false
}
}
// 加载默认IoT卡列表
const loadDefaultIotCards = async () => {
cardSearchLoading.value = true
try {
const res = await CardService.getStandaloneIotCards({
page: 1,
page_size: 20
})
if (res.code === 0) {
iotCardOptions.value = res.data.items || []
}
} catch (error) {
console.error('Load default IoT cards failed:', error)
iotCardOptions.value = []
} finally {
cardSearchLoading.value = false
}
}
// 加载默认设备列表
const loadDefaultDevices = async () => {
deviceSearchLoading.value = true
try {
const res = await DeviceService.getDevices({
page: 1,
page_size: 20
})
if (res.code === 0) {
deviceOptions.value = res.data.items || []
}
} catch (error) {
console.error('Load default devices failed:', error)
deviceOptions.value = []
} finally {
deviceSearchLoading.value = false
isLoadingPackagesFromSelection.value = false
}
}

View File

@@ -167,11 +167,8 @@
</ElCol>
</ElRow>
<!-- 套餐天数流量重置周期为每日calendar_type为by_day时显示 -->
<ElRow
:gutter="20"
v-if="form.data_reset_cycle === 'daily' || form.calendar_type === 'by_day'"
>
<!-- 套餐天数calendar_type为by_day时显示 -->
<ElRow :gutter="20" v-if="form.calendar_type === 'by_day'">
<ElCol :span="12">
<ElFormItem label="套餐天数" prop="duration_days">
<ElInputNumber
@@ -837,18 +834,15 @@
watch(
() => form.data_reset_cycle,
(cycle) => {
// 不是每日且calendar_type不是按天时,清空套餐天数
if (cycle !== 'daily' && form.calendar_type !== 'by_day') {
form.duration_days = undefined
}
// 流量重置周期变化时不需要清空套餐天数,套餐天数只受calendar_type控制
}
)
// 监听套餐周期类型变化,不是按天且流量重置周期不是每日时清空套餐天数
// 监听套餐周期类型变化,不是按天时清空套餐天数
watch(
() => form.calendar_type,
(type) => {
if (type !== 'by_day' && form.data_reset_cycle !== 'daily') {
if (type !== 'by_day') {
form.duration_days = undefined
}
}
@@ -1135,7 +1129,8 @@
if (form.data_reset_cycle) {
data.data_reset_cycle = form.data_reset_cycle
}
if (form.duration_days !== undefined) {
// 只有 calendar_type 为 by_day 时才传 duration_days
if (form.calendar_type === 'by_day' && form.duration_days !== undefined) {
data.duration_days = form.duration_days
}
if (form.enable_virtual_data !== undefined) {

View File

@@ -146,9 +146,9 @@
]
}
// 微信支付配置
// 微信配置
const wechatPaySection: DetailSection = {
title: '微信支付配置',
title: '微信配置',
fields: [
{
label: '微信商户号',

View File

@@ -163,7 +163,7 @@
</ElFormItem>
<ElDivider content-position="left">
<span class="divider-title">微信支付配置</span>
<span class="divider-title">微信配置</span>
</ElDivider>
<ElRow :gutter="20">
<ElCol :span="12">
@@ -294,7 +294,7 @@
</ElFormItem>
<ElDivider content-position="left">
<span class="divider-title">微信支付配置</span>
<span class="divider-title">微信配置</span>
</ElDivider>
<ElRow :gutter="20">
<ElCol :span="12">

View File

@@ -150,6 +150,7 @@
:icon="'ArrowRight'"
@click="addPermissions"
:disabled="getLeftCheckedKeys().length === 0"
:loading="addingPermissionLoading"
>
添加
</ElButton>
@@ -245,6 +246,7 @@
const permissionDialogVisible = ref(false)
const loading = ref(false)
const submitLoading = ref(false)
const addingPermissionLoading = ref(false)
const tableRef = ref()
const leftTreeRef = ref()
const rightTreeRef = ref()
@@ -799,6 +801,7 @@
const checkedKeys = getLeftCheckedKeys()
if (checkedKeys.length === 0) return
addingPermissionLoading.value = true
try {
// 提交到服务器时包含勾选和半选节点
const keysToSubmit = getLeftCheckedKeysWithHalf()
@@ -826,6 +829,8 @@
ElMessage.success('权限添加成功')
} catch (error) {
console.error('添加权限失败:', error)
} finally {
addingPermissionLoading.value = false
}
}