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

This commit is contained in:
sexygoat
2026-04-23 14:59:05 +08:00
parent a9467e8a0c
commit a5e76313cb
28 changed files with 1284 additions and 702 deletions

View File

@@ -8,6 +8,7 @@
:disabled="v.disabled || false"
>
</el-option>
<el-option v-if="options.length === 0" label="无数据" value="__NO_DATA__" disabled />
</el-select>
</template>
@@ -39,7 +40,7 @@
// 合并默认配置和自定义配置
const config = reactive({
placeholder: `${t('table.searchBar.searchSelectPlaceholder')}${prop.item.label}`, // 修改默认placeholder文案
placeholder: `${t('table.searchBar.searchSelectPlaceholder')}${prop.item.label}`,
...(prop.item.config || {})
})
@@ -56,14 +57,12 @@
// 计算属性:处理选项数据
const options = computed(() => {
if (prop.item.options) {
// 判断options是数组还是函数
if (Array.isArray(prop.item.options)) {
return prop.item.options
} else {
return prop.item.options()
}
} else {
return []
}
return []
})
</script>

View File

@@ -209,6 +209,7 @@ export interface AssetPackageUsageRecord {
virtual_remain_mb?: number // 剩余虚流量 MB
virtual_ratio?: number // 虚流量比例real/virtual
enable_virtual_data?: boolean // 是否启用虚流量
paid_amount?: number // 实际支付金额(分)
package_price?: number // 套餐价格(分)
activated_at?: string // 激活时间
expires_at?: string // 到期时间

View File

@@ -53,6 +53,7 @@ export interface Order {
actual_paid_amount?: number // 实付金额(分)
paid_at: string | null
commission_status: OrderCommissionStatus
commission_status_name?: string // 佣金状态名称(中文)
commission_config_version: number
device_id: number | null
iot_card_id: number | null
@@ -71,13 +72,15 @@ export interface OrderQueryParams {
page?: number
page_size?: number
payment_status?: PaymentStatus
payment_method?: OrderPaymentMethod
order_type?: OrderType
order_no?: string
identifier?: string // 按资产标识符过滤
identifier?: string // 按资产标识符过滤ICCID 或 VirtualNo
purchase_role?: string // 订单渠道
buyer_phone?: string // 按买家手机号精确过滤
start_time?: string
end_time?: string
is_expired?: boolean // 是否已过期
dateRange?: string[] | any // For date range picker in UI
}

View File

@@ -15,8 +15,12 @@ export interface Refund {
id: number
refund_no: string
order_id: number
order_no: string // 订单号
shop_id: number
shop_name: string // 店铺名称
package_usage_id: number | null
asset_identifier: string // 资产标识符
asset_type: string // 资产类型
requested_refund_amount: number // 申请退款金额(分)
approved_refund_amount: number | null // 实际退款金额(分)
actual_received_amount: number // 实收金额(分)
@@ -41,6 +45,7 @@ export interface RefundQueryParams {
status?: RefundStatus
order_id?: number
shop_id?: number
shop_name?: string // 店铺名称(模糊查询)
dateRange?: string[]
}

View File

@@ -278,7 +278,7 @@
// 响应式表单数据
const formFilters = reactive({ ...initialSearchState })
// 店铺和企业列表
// 店铺和企业列表
const shopList = ref<any[]>([])
const enterpriseList = ref<any[]>([])
@@ -359,7 +359,18 @@
filterable: true,
remote: true,
remoteMethod: handleShopSearch,
placeholder: '请输入店铺名称搜索'
placeholder: '请输入店铺名称搜索',
reserveKeyword: true
}
},
{
label: '状态',
prop: 'status',
type: 'select',
options: STATUS_SELECT_OPTIONS,
config: {
clearable: true,
placeholder: '请选择状态'
}
},
{
@@ -375,17 +386,8 @@
filterable: true,
remote: true,
remoteMethod: handleEnterpriseSearch,
placeholder: '请输入企业名称搜索'
}
},
{
label: '状态',
prop: 'status',
type: 'select',
options: STATUS_SELECT_OPTIONS,
config: {
clearable: true,
placeholder: '请选择状态'
placeholder: '请输入企业名称搜索',
reserveKeyword: true
}
}
])

View File

@@ -194,6 +194,11 @@
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="实名认证策略" width="120">
<template #default="scope">
{{ getRealnamePolicyName(scope.row.realname_policy) }}
</template>
</ElTableColumn>
<ElTableColumn label="实名时间" width="180">
<template #default="scope">
{{ scope.row.real_name_at ? formatDateTime(scope.row.real_name_at) : '-' }}
@@ -229,7 +234,7 @@
<!-- 设备实时信息 -->
<ElDivider content-position="left">设备实时信息</ElDivider>
<div v-if="deviceRealtime" style="margin-top: 16px">
<div v-if="deviceRealtime" v-loading="realtimeLoading" style="margin-top: 16px">
<ElDescriptions :column="4" border>
<!-- 基本状态 -->
<ElDescriptionsItem label="在线状态">
@@ -522,9 +527,12 @@
cardInfo: AssetInfo
deviceRealtime: DeviceRealtimeInfo | null
pollingEnabled: boolean
realtimeLoading?: boolean
}
const props = defineProps<Props>()
const props = withDefaults(defineProps<Props>(), {
realtimeLoading: false
})
const { hasAuth } = useAuth()

View File

@@ -2,66 +2,9 @@
<ElCard shadow="never" class="info-card current-package-card">
<template #header>
<div class="card-header">
<div class="card-header-left">
<span class="header-title">当前生效套餐</span>
<ElTag
v-if="currentPackage"
:type="getPackageStatusTagType(currentPackage.status)"
size="small"
>
{{ currentPackage?.status_name || '-' }}
</ElTag>
</div>
<!-- 中间流量进度 -->
<div v-if="currentPackage && !props.boundDeviceId" class="header-progress">
<div class="header-progress-info">
<span class="progress-title">流量</span>
<span class="progress-text">
<template v-if="isAdminOrPlatform">
总量 {{ formatDataSize(currentPackage.data_limit_mb || currentPackage.package_total_data || 0) }} / 实际可用
{{ formatDataSize(currentPackage.enable_virtual_data ? (currentPackage.virtual_limit_mb || 0) : (currentPackage.data_limit_mb || currentPackage.package_total_data || 0)) }} / 剩余可用
{{ formatDataSize(currentPackage.enable_virtual_data ? (currentPackage.virtual_remain_mb || 0) : ((currentPackage.data_limit_mb || currentPackage.package_total_data || 0) - (currentPackage.data_usage_mb || 0))) }}
</template>
<template v-else>
总量 {{ formatDataSize(currentPackage.data_limit_mb || currentPackage.package_total_data || 0) }} / 剩余可用
{{ formatDataSize(currentPackage.enable_virtual_data ? (currentPackage.virtual_remain_mb || 0) : ((currentPackage.data_limit_mb || currentPackage.package_total_data || 0) - (currentPackage.data_usage_mb || 0))) }}
</template>
</span>
</div>
<ElProgress
:percentage="
currentPackage.enable_virtual_data
? getUsageProgress(
currentPackage.virtual_data_used || 0,
currentPackage.virtual_data_total || 0
)
: getUsageProgress(
currentPackage.real_data_used || 0,
currentPackage.real_data_total || 0
)
"
:color="
currentPackage.enable_virtual_data
? getProgressColorByPercentage(
getUsageProgress(
currentPackage.virtual_data_used || 0,
currentPackage.virtual_data_total || 0
)
)
: getProgressColorByPercentage(
getUsageProgress(
currentPackage.real_data_used || 0,
currentPackage.real_data_total || 0
)
)
"
:stroke-width="10"
/>
</div>
<div v-if="currentPackage" class="card-header-right">
<ElButton type="primary" size="small" @click="handleShowRecharge"> 套餐充值 </ElButton>
<span class="header-title">当前生效套餐</span>
<div class="header-actions">
<slot name="header-actions" />
</div>
</div>
</template>
@@ -86,33 +29,80 @@
<!-- 显示错误信息 -->
<ElAlert v-else-if="errorMsg" :title="errorMsg" type="warning" :closable="false" />
<!-- 显示套餐详情 -->
<ElDescriptions v-else-if="currentPackage" :column="3" border>
<ElDescriptionsItem label="套餐名称">
{{ currentPackage.package_name || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="套餐价格">
{{ formatAmount(currentPackage.package_price) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="套餐类型">
{{ getPackageTypeName(currentPackage.package_type) }}
</ElDescriptionsItem>
<ElDescriptionsItem
v-if="currentPackage.enable_virtual_data && isAdminOrPlatform"
label="虚流量比例"
>
{{ currentPackage.virtual_ratio || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem v-if="currentPackage.duration_days" label="套餐时长" :span="3">
{{ currentPackage.duration_days }}
</ElDescriptionsItem>
<ElDescriptionsItem label="开始时间">
{{ formatDateTime(currentPackage.start_time) || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="到期时间">
{{ formatDateTime(currentPackage.expire_time) || '-' }}
</ElDescriptionsItem>
</ElDescriptions>
<template v-else-if="currentPackage">
<!-- 流量使用情况 - 横向进度条卡片 -->
<div class="flow-progress-card">
<div class="flow-stats-row">
<div class="flow-stat-item">
<span class="flow-stat-label">总量</span>
<span class="flow-stat-value">
{{ formatDataSize(currentPackage.data_limit_mb || currentPackage.package_total_data || 0) }}
</span>
</div>
<div v-if="isAdminOrPlatform && currentPackage.enable_virtual_data" class="flow-stat-item">
<span class="flow-stat-label">实际可用</span>
<span class="flow-stat-value">
{{ formatDataSize(currentPackage.virtual_limit_mb || 0) }}
</span>
</div>
<div class="flow-stat-item">
<span class="flow-stat-label">剩余可用</span>
<span class="flow-stat-value">
{{
formatDataSize(
currentPackage.enable_virtual_data
? currentPackage.virtual_remain_mb || 0
: (currentPackage.data_limit_mb || currentPackage.package_total_data || 0) - (currentPackage.data_usage_mb || 0)
)
}}
</span>
</div>
</div>
<div class="flow-progress-bar">
<ElProgress
:percentage="
currentPackage.enable_virtual_data
? getUsageProgress(currentPackage.virtual_data_used || 0, currentPackage.virtual_data_total || 0)
: getUsageProgress(currentPackage.real_data_used || 0, currentPackage.real_data_total || 0)
"
:color="
currentPackage.enable_virtual_data
? getProgressColorByPercentage(getUsageProgress(currentPackage.virtual_data_used || 0, currentPackage.virtual_data_total || 0))
: getProgressColorByPercentage(getUsageProgress(currentPackage.real_data_used || 0, currentPackage.real_data_total || 0))
"
:stroke-width="14"
/>
</div>
</div>
<!-- 套餐详情表格 -->
<ElDescriptions :column="3" border class="package-info-table">
<ElDescriptionsItem label="套餐名称">
{{ currentPackage.package_name || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="套餐价格">
{{ formatAmount(currentPackage.paid_amount || currentPackage.package_price || 0) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="套餐类型">
{{ getPackageTypeName(currentPackage.package_type) }}
</ElDescriptionsItem>
<ElDescriptionsItem v-if="isAdminOrPlatform && currentPackage.enable_virtual_data" label="虚流量比例">
{{ currentPackage.virtual_ratio || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="开始时间">
{{ formatDateTime(currentPackage.start_time) || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="到期时间">
{{ formatDateTime(currentPackage.expire_time) || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="购买时间">
{{ formatDateTime(currentPackage.created_at) || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem v-if="currentPackage.duration_days" label="套餐时长">
{{ currentPackage.duration_days }}
</ElDescriptionsItem>
</ElDescriptions>
</template>
<!-- 空状态 -->
<ElEmpty v-else-if="!loading" :description="errorMsg || '暂无当前生效套餐'" :image-size="100" />
@@ -122,7 +112,6 @@
<script setup lang="ts">
import {
ElCard,
ElTag,
ElButton,
ElDescriptions,
ElDescriptionsItem,
@@ -136,21 +125,19 @@
import { formatDateTime } from '@/utils/business/format'
import type { PackageInfo } from '../types'
// Use formatters
const {
formatAmount,
formatDataSize,
getPackageStatusTagType,
getUsageProgress,
getProgressColorByPercentage
} = useAssetFormatters()
const userStore = useUserStore()
const { info: userInfo } = storeToRefs(userStore)
const isSuperAdmin = computed(() => userInfo.value.user_type === 1)
const isAdminOrPlatform = computed(() => userInfo.value.user_type === 1 || userInfo.value.user_type === 2)
const isAdminOrPlatform = computed(
() => userInfo.value.user_type === 1 || userInfo.value.user_type === 2
)
// Props
interface Props {
currentPackage: PackageInfo | null
loading?: boolean
@@ -168,18 +155,11 @@
boundDeviceName: ''
})
// Emits
const emit = defineEmits<{
showRecharge: []
navigateToDevice: [deviceNo: string]
}>()
// Methods
const handleShowRecharge = () => {
emit('showRecharge')
}
// 套餐类型名称
const getPackageTypeName = (type?: string) => {
const typeMap: Record<string, string> = {
formal: '正式套餐',
@@ -195,52 +175,49 @@
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
.card-header-left {
display: flex;
gap: 12px;
align-items: center;
.header-title {
font-size: 16px;
font-weight: 500;
}
}
.card-header-right {
display: flex;
gap: 8px;
align-items: center;
.header-title {
font-size: 16px;
font-weight: 500;
}
}
.header-progress {
flex: 1;
min-width: 0;
padding: 0 24px;
.flow-progress-card {
padding: 20px;
background: var(--el-fill-color-light);
border-radius: 8px;
margin-bottom: 20px;
.header-progress-info {
.flow-stats-row {
display: flex;
align-items: baseline;
gap: 12px;
margin-bottom: 6px;
gap: 32px;
margin-bottom: 16px;
.progress-title {
flex-shrink: 0;
font-size: 13px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.flow-stat-item {
display: flex;
align-items: baseline;
gap: 8px;
.progress-text {
font-size: 12px;
color: var(--el-text-color-secondary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
.flow-stat-label {
font-size: 13px;
color: var(--el-text-color-secondary);
}
.flow-stat-value {
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
}
.flow-progress-bar {
padding: 0 4px;
}
}
.package-info-table {
margin-top: 0;
}
}
</style>

View File

@@ -3,9 +3,12 @@
<template #header>
<div class="card-header">
<span>套餐列表</span>
<div class="card-header-right">
<ElButton type="primary" @click="handleShowRecharge">套餐充值</ElButton>
</div>
</div>
</template>
<div class="package-table-wrapper">
<div v-loading="loading" class="package-table-wrapper">
<!-- 绑定设备提示 -->
<ElEmpty v-if="props.boundDeviceId" :image-size="100">
<template #description>
@@ -36,7 +39,7 @@
</ElTableColumn>
<ElTableColumn label="套餐价格" width="120">
<template #default="scope">
{{ formatAmount(scope.row.package_price || 0) }}
{{ formatAmount(scope.row.paid_amount || scope.row.package_price || 0) }}
</template>
</ElTableColumn>
<ElTableColumn prop="status_name" label="套餐状态" width="100">
@@ -104,6 +107,11 @@
{{ formatDateTime(scope.row.expires_at) }}
</template>
</ElTableColumn>
<ElTableColumn prop="created_at" label="创建时间" width="170" show-overflow-tooltip>
<template #default="scope">
{{ formatDateTime(scope.row.created_at) }}
</template>
</ElTableColumn>
</ElTable>
<ElPagination
v-model:current-page="pagination.page"
@@ -152,6 +160,7 @@
pageSize?: number
boundDeviceId?: number
boundDeviceNo?: string
loading?: boolean
}
const props = withDefaults(defineProps<Props>(), {
@@ -159,12 +168,14 @@
page: 1,
pageSize: 10,
boundDeviceId: undefined,
boundDeviceNo: ''
boundDeviceNo: '',
loading: false
})
// Emits
interface Emits {
(e: 'showDailyRecords', payload: { packageRow: PackageInfo }): void
(e: 'showRecharge'): void
(e: 'page-change', page: number): void
(e: 'size-change', size: number): void
(e: 'navigateToDevice', deviceNo: string): void
@@ -212,6 +223,10 @@
emit('showDailyRecords', { packageRow })
}
const handleShowRecharge = () => {
emit('showRecharge')
}
// 页码变化
const handlePageChange = (page: number) => {
emit('page-change', page)
@@ -229,6 +244,13 @@
width: 100%;
overflow: hidden; // 防止内容溢出
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.package-table-wrapper {
.table-scroll-container {
overflow-x: auto;

View File

@@ -1,5 +1,5 @@
<template>
<ElCard shadow="never" class="info-card wallet-info">
<ElCard shadow="never" class="info-card wallet-info" v-loading="walletLoading">
<template #header>
<div class="card-header wallet-header">
<div class="header-left">

View File

@@ -29,6 +29,8 @@ export function useAssetInfo() {
const packageList = ref<AssetPackageUsageRecord[]>([])
const walletInfo = ref<AssetWalletResponse | null>(null)
const walletLoading = ref(false)
const packageLoading = ref(false)
const realtimeLoading = ref(false)
/**
* 获取资产详情
@@ -142,6 +144,7 @@ export function useAssetInfo() {
*/
const loadPackageList = async (identifier: string, page: number = 1, pageSize: number = 10) => {
try {
packageLoading.value = true
const response = await AssetService.getAssetPackages(identifier, {
page,
page_size: pageSize
@@ -161,6 +164,8 @@ export function useAssetInfo() {
} catch (error) {
console.error('获取套餐列表失败:', error)
return { items: [], total: 0, page: 1, pageSize }
} finally {
packageLoading.value = false
}
}
@@ -173,23 +178,17 @@ export function useAssetInfo() {
currentPackageLoading.value = true
currentPackageErrorMsg.value = ''
const [currentPkgResponse, packageListResponse] = await Promise.all([
AssetService.getCurrentPackage(identifier),
AssetService.getAssetPackages(identifier, { page: 1, page_size: 50 })
])
const response = await AssetService.getCurrentPackage(identifier)
const activePackage = packageListResponse.data?.items?.find((p: any) => p.status === 1)
const activePaidAmount = activePackage?.paid_amount || 0
if (currentPkgResponse.code === 0) {
if (currentPkgResponse.data) {
const pkg = currentPkgResponse.data
if (response.code === 0) {
if (response.data) {
const pkg = response.data
currentPackage.value = {
id: pkg.package_usage_id || 0,
package_id: pkg.package_id || 0,
package_name: pkg.package_name || '',
package_price: activePaidAmount,
paid_amount: pkg.paid_amount || 0,
package_total_data: pkg.data_limit_mb || 0,
real_data_used: pkg.data_usage_mb || 0,
real_data_remaining: (pkg.data_limit_mb || 0) - (pkg.data_usage_mb || 0),
@@ -208,7 +207,8 @@ export function useAssetInfo() {
data_limit_mb: pkg.data_limit_mb,
virtual_limit_mb: pkg.virtual_limit_mb,
virtual_remain_mb: pkg.virtual_remain_mb,
data_usage_mb: pkg.data_usage_mb
data_usage_mb: pkg.data_usage_mb,
created_at: pkg.created_at || ''
}
// 同时更新 cardInfo 中的流量显示信息(根据是否启用虚流量选择数据源)
@@ -260,6 +260,7 @@ export function useAssetInfo() {
*/
const loadRealtimeStatus = async (identifier: string, assetType: string) => {
try {
realtimeLoading.value = true
const response = await AssetService.getRealtimeStatus(identifier)
if (response.code === 0 && response.data) {
const data = response.data
@@ -323,6 +324,8 @@ export function useAssetInfo() {
}
} catch (error) {
console.error('获取实时状态失败:', error)
} finally {
realtimeLoading.value = false
}
}
@@ -377,8 +380,10 @@ export function useAssetInfo() {
currentPackageLoading,
currentPackageErrorMsg,
packageList,
packageLoading,
walletInfo,
walletLoading,
realtimeLoading,
// Methods
fetchAssetDetail,

View File

@@ -17,6 +17,7 @@
:card-info="cardInfo"
:device-realtime="deviceRealtime"
v-model:polling-enabled="pollingEnabled"
:realtime-loading="realtimeLoading"
@enable-card="handleEnableCard"
@disable-card="handleDisableCard"
@manual-deactivate="handleManualDeactivate"
@@ -56,7 +57,9 @@
:page-size="packagePagination.pageSize"
:bound-device-id="cardInfo.bound_device_id"
:bound-device-no="cardInfo.bound_device_no"
:loading="packageLoading"
@show-daily-records="handleShowDailyRecords"
@show-recharge="showRechargeDialog"
@page-change="handlePackagePageChange"
@size-change="handlePackageSizeChange"
@navigate-to-device="handleNavigateToDevice"
@@ -172,8 +175,10 @@
currentPackageLoading,
currentPackageErrorMsg,
packageList,
packageLoading,
walletInfo,
walletLoading,
realtimeLoading,
fetchAssetDetail,
loadPackageList,
loadCurrentPackage,

View File

@@ -5,6 +5,7 @@
<ArtSearchBar
v-model:filter="searchForm"
:items="searchFormItems"
label-width="100"
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
@@ -340,7 +341,11 @@
<ElRow :gutter="20">
<ElCol :span="6">
<ElFormItem label="搜索类型" prop="search_type">
<ElSelect v-model="bindCardForm.search_type" placeholder="请选择搜索类型" style="width: 100%">
<ElSelect
v-model="bindCardForm.search_type"
placeholder="请选择搜索类型"
style="width: 100%"
>
<ElOption label="ICCID" value="iccid" />
<ElOption label="运营商名称" value="carrier_name" />
</ElSelect>
@@ -655,6 +660,10 @@
})
const seriesBindingResult = ref<BatchSetDeviceSeriesBindingResponse | null>(null)
// 搜索表单店铺/系列选项
const shopOptions = ref<any[]>([])
const searchSeriesOptions = ref<any[]>([])
// 设备详情弹窗相关
const deviceDetailDialogVisible = ref(false)
const deviceDetailLoading = ref(false)
@@ -734,7 +743,12 @@
status: undefined as DeviceStatus | undefined,
batch_no: '',
device_type: '',
manufacturer: ''
manufacturer: '',
shop_id: undefined as number | undefined,
series_id: undefined as number | undefined,
dateRange: [] as string[],
created_at_start: '',
created_at_end: ''
}
// 搜索表单
@@ -801,6 +815,54 @@
clearable: true,
placeholder: '请输入制造商'
}
},
{
label: '店铺名称',
prop: 'shop_id',
type: 'select',
config: {
clearable: true,
filterable: true,
remote: true,
remoteMethod: (query: string) => searchShops(query),
loading: false,
placeholder: '请选择或搜索店铺名称'
},
options: () =>
shopOptions.value.map((shop) => ({
label: shop.shop_name,
value: shop.id
}))
},
{
label: '套餐系列',
prop: 'series_id',
type: 'select',
config: {
clearable: true,
filterable: true,
remote: true,
remoteMethod: (query: string) => searchSeries(query),
loading: seriesLoading.value,
placeholder: '请选择或搜索套餐系列'
},
options: () =>
searchSeriesOptions.value.map((s) => ({
label: s.series_name,
value: s.id
}))
},
{
label: '起始至结束',
prop: 'dateRange',
type: 'date',
config: {
type: 'datetimerange',
rangeSeparator: '至',
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
valueFormat: 'YYYY-MM-DDTHH:mm:ssZZ'
}
}
]
@@ -814,21 +876,26 @@
// 列配置
const columnOptions = [
{ label: '设备号', prop: 'virtual_no' },
{ label: 'IMEI', prop: 'imei' },
{ label: '设备名称', prop: 'device_name' },
{ label: '设备型号', prop: 'device_model' },
{ label: '设备类型', prop: 'device_type' },
{ label: '店铺名称', prop: 'shop_name' },
{ label: '套餐系列', prop: 'series_name' },
{ label: '设备型号', prop: 'device_model' },
{ label: '设备类型', prop: 'device_type' },
{ label: '状态', prop: 'status' },
{ label: '状态名称', prop: 'status_name' },
{ label: '在线状态', prop: 'online_status' },
{ label: '实名策略', prop: 'realname_policy' },
{ label: '切卡模式', prop: 'switch_mode' },
{ label: '激活时间', prop: 'activated_at' },
{ label: '制造商', prop: 'manufacturer' },
{ label: '软件版本', prop: 'software_version' },
{ label: '最大插槽数', prop: 'max_sim_slots' },
{ label: '已绑定卡数', prop: 'bound_card_count' },
{ label: '状态', prop: 'status' },
{ label: '在线状态', prop: 'online_status' },
{ label: '切卡模式', prop: 'switch_mode' },
{ label: '序列号', prop: 'sn' },
{ label: '批次号', prop: 'batch_no' },
{ label: '最后在线时间', prop: 'last_online_time' },
{ label: '最后同步时间', prop: 'last_gateway_sync_at' },
{ label: '批次号', prop: 'batch_no' },
{ label: '创建时间', prop: 'created_at' },
{ label: '操作', prop: 'operation' }
]
@@ -875,10 +942,7 @@
bindCardForm.iot_card_id = undefined
bindCardForm.slot_position = 1
// 加载未绑定设备的卡列表
await Promise.all([
loadDeviceCards(device.virtual_no),
loadDefaultIotCards()
])
await Promise.all([loadDeviceCards(device.virtual_no), loadDefaultIotCards()])
}
// 加载设备绑定的卡列表
@@ -1055,7 +1119,7 @@
{
prop: 'virtual_no',
label: '设备号',
minWidth: 150,
minWidth: 170,
showOverflowTooltip: true,
formatter: (row: Device) => {
return h(
@@ -1071,21 +1135,18 @@
)
}
},
{
prop: 'imei',
label: 'IMEI',
minWidth: 150,
showOverflowTooltip: true,
formatter: (row: Device) => row.imei || '-'
},
{
prop: 'device_name',
label: '设备名称',
minWidth: 120
},
{
prop: 'device_model',
label: '设备型号',
minWidth: 120
},
{
prop: 'device_type',
label: '设备类型',
width: 120
},
{
prop: 'shop_name',
label: '店铺名称',
@@ -1097,23 +1158,14 @@
minWidth: 120
},
{
prop: 'manufacturer',
label: '制造商',
prop: 'device_model',
label: '设备型号',
minWidth: 120
},
{
prop: 'max_sim_slots',
label: '最大插槽数',
width: 100
},
{
prop: 'bound_card_count',
label: '已绑定卡数',
width: 110,
formatter: (row: Device) => {
const color = row.bound_card_count > 0 ? '#67c23a' : '#909399'
return h('span', { style: { color, fontWeight: 'bold' } }, row.bound_card_count)
}
prop: 'device_type',
label: '设备类型',
width: 120
},
{
prop: 'status',
@@ -1130,6 +1182,12 @@
return h(ElTag, { type: status.type }, () => status.text)
}
},
{
prop: 'status_name',
label: '状态名称',
width: 100,
formatter: (row: Device) => row.status_name || '-'
},
{
prop: 'online_status',
label: '在线状态',
@@ -1144,6 +1202,20 @@
return h(ElTag, { type: status.type }, () => status.text)
}
},
{
prop: 'realname_policy',
label: '实名策略',
width: 150,
formatter: (row: Device) => {
const policy = row.realname_policy || ''
const policyMap: Record<string, string> = {
none: '无需实名',
before_order: '先实名后充值/购买',
after_order: '先充值/购买后实名'
}
return policyMap[policy] || (policy ? policy : '未知')
}
},
{
prop: 'switch_mode',
label: '切卡模式',
@@ -1157,19 +1229,51 @@
}
},
{
prop: 'realname_policy',
label: '实名认证策略',
width: 150,
prop: 'activated_at',
label: '激活时间',
width: 180,
formatter: (row: Device) => (row.activated_at ? formatDateTime(row.activated_at) : '-')
},
{
prop: 'manufacturer',
label: '制造商',
minWidth: 100
},
{
prop: 'software_version',
label: '软件版本',
minWidth: 180,
showOverflowTooltip: true,
formatter: (row: Device) => row.software_version || '-'
},
{
prop: 'max_sim_slots',
label: '最大插槽数',
width: 100
},
{
prop: 'bound_card_count',
label: '已绑定卡数',
width: 110,
formatter: (row: Device) => {
const policy = row.realname_policy || ''
const policyMap: Record<string, string> = {
none: '无需实名',
before_order: '先实名后充值/购买',
after_order: '先充值/购买后实名'
}
return policyMap[policy] || (policy ? policy : '未知')
const color = row.bound_card_count > 0 ? '#67c23a' : '#909399'
return h('span', { style: { color, fontWeight: 'bold' } }, row.bound_card_count)
}
},
{
prop: 'sn',
label: '序列号',
minWidth: 120,
showOverflowTooltip: true,
formatter: (row: Device) => row.sn || '-'
},
{
prop: 'batch_no',
label: '批次号',
minWidth: 180,
showOverflowTooltip: true,
formatter: (row: Device) => row.batch_no || '-'
},
{
prop: 'last_online_time',
label: '最后在线时间',
@@ -1184,12 +1288,6 @@
formatter: (row: Device) =>
row.last_gateway_sync_at ? formatDateTime(row.last_gateway_sync_at) : '-'
},
{
prop: 'batch_no',
label: '批次号',
minWidth: 180,
formatter: (row: Device) => row.batch_no || '-'
},
{
prop: 'created_at',
label: '创建时间',
@@ -1198,9 +1296,50 @@
}
])
// 搜索店铺(用于搜索表单)
const searchShops = async (query: string) => {
try {
const params: any = {
page: 1,
page_size: 20
}
if (query) {
params.shop_name = query
}
const res = await ShopService.getShops(params)
if (res.code === 0) {
shopOptions.value = res.data.items || []
}
} catch (error) {
console.error('搜索店铺失败:', error)
}
}
// 搜索套餐系列(用于搜索表单)
const searchSeries = async (query: string) => {
try {
const params: any = {
page: 1,
page_size: 20,
status: 1
}
if (query) {
params.series_name = query
}
const res = await PackageSeriesService.getPackageSeries(params)
if (res.code === 0) {
searchSeriesOptions.value = res.data.items || []
}
} catch (error) {
console.error('搜索套餐系列失败:', error)
}
}
let isFirstActivation = true
onMounted(() => {
getTableData()
searchShops('')
searchSeries('')
})
// 当页面被 keep-alive 激活时自动刷新数据
@@ -1223,7 +1362,11 @@
status: searchForm.status,
batch_no: searchForm.batch_no || undefined,
device_type: searchForm.device_type || undefined,
manufacturer: searchForm.manufacturer || undefined
manufacturer: searchForm.manufacturer || undefined,
shop_id: searchForm.shop_id || undefined,
series_id: searchForm.series_id || undefined,
created_at_start: searchForm.created_at_start || undefined,
created_at_end: searchForm.created_at_end || undefined
}
const res = await DeviceService.getDevices(params)
if (res.code === 0 && res.data) {
@@ -1246,6 +1389,14 @@
// 搜索
const handleSearch = () => {
// 处理日期范围
if (searchForm.dateRange && Array.isArray(searchForm.dateRange)) {
searchForm.created_at_start = searchForm.dateRange[0] || ''
searchForm.created_at_end = searchForm.dateRange[1] || ''
} else {
searchForm.created_at_start = ''
searchForm.created_at_end = ''
}
pagination.page = 1
getTableData()
}

View File

@@ -229,6 +229,7 @@
const searchForm = reactive({
status: undefined,
identifier: '',
created_at_range: [],
created_at_start: '',
created_at_end: ''
})
@@ -306,11 +307,11 @@
prop: 'created_at_range',
type: 'date',
config: {
type: 'datetimerange',
type: 'daterange',
rangeSeparator: '至',
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
valueFormat: 'YYYY-MM-DD HH:mm:ss'
startPlaceholder: '开始日期',
endPlaceholder: '结束日期',
valueFormat: 'YYYY-MM-DD'
}
}
]
@@ -417,12 +418,23 @@
// 搜索
const handleSearch = () => {
// 处理日期范围
if (searchForm.created_at_range && Array.isArray(searchForm.created_at_range)) {
searchForm.created_at_start = searchForm.created_at_range[0] || ''
searchForm.created_at_end = searchForm.created_at_range[1] || ''
} else {
searchForm.created_at_start = ''
searchForm.created_at_end = ''
}
pagination.page = 1
loadExchangeList()
}
// 重置
const handleReset = () => {
searchForm.created_at_range = []
searchForm.created_at_start = ''
searchForm.created_at_end = ''
pagination.page = 1
loadExchangeList()
}

View File

@@ -562,7 +562,13 @@
import { h } from 'vue'
import { useRouter } from 'vue-router'
import { RoutesAlias } from '@/router/routesAlias'
import { CardService, ShopService, PackageSeriesService, AssetService, CarrierService } from '@/api/modules'
import {
CardService,
ShopService,
PackageSeriesService,
AssetService,
CarrierService
} from '@/api/modules'
import { ElMessage, ElTag, ElIcon, ElMessageBox } from 'element-plus'
import { Loading } from '@element-plus/icons-vue'
import RealnamePolicyDialog from '@/components/device/RealnamePolicyDialog.vue'
@@ -659,6 +665,29 @@
}
}
// 套餐系列搜索相关
const searchSeriesOptions = ref<any[]>([])
// 搜索套餐系列(用于搜索栏)
const handleSearchSeries = async (query: string) => {
try {
const params: any = {
page: 1,
page_size: 20,
status: 1
}
if (query) {
params.series_name = query
}
const res = await PackageSeriesService.getPackageSeries(params)
if (res.code === 0) {
searchSeriesOptions.value = res.data.items || []
}
} catch (error) {
console.error('搜索套餐系列失败:', error)
}
}
// 卡详情弹窗相关
const cardDetailDialogVisible = ref(false)
const cardDetailLoading = ref(false)
@@ -767,6 +796,7 @@
msisdn: string
virtual_no: string
device_virtual_no: string
series_id: undefined | number
is_distributed: undefined | number
is_standalone: undefined | boolean
[key: string]: any
@@ -777,6 +807,7 @@
msisdn: '',
virtual_no: '',
device_virtual_no: '',
series_id: undefined,
is_distributed: undefined,
is_standalone: undefined,
is_replaced: undefined,
@@ -952,6 +983,23 @@
placeholder: '请输入绑定设备虚拟号'
}
},
{
label: '套餐系列',
prop: 'series_id',
type: 'select',
config: {
clearable: true,
filterable: true,
remote: true,
remoteMethod: handleSearchSeries,
placeholder: '请选择或搜索套餐系列'
},
options: () =>
searchSeriesOptions.value.map((s) => ({
label: s.series_name,
value: s.id
}))
},
{
label: '是否已分销',
prop: 'is_distributed',
@@ -974,8 +1022,8 @@
placeholder: '全部'
},
options: () => [
{ label: '是(未绑定设备', value: true },
{ label: '否(已绑定设备', value: false }
{ label: '未绑定设备', value: true },
{ label: '已绑定设备', value: false }
]
},
{
@@ -1016,18 +1064,28 @@
{ label: 'ICCID', prop: 'iccid' },
{ label: 'MSISDN', prop: 'msisdn' },
{ label: '卡虚拟号', prop: 'virtual_no' },
{ label: '绑定设备虚拟号', prop: 'device_virtual_no' },
{ label: '卡业务类型', prop: 'card_category' },
{ label: '运营商', prop: 'carrier_name' },
{ label: '店铺名称', prop: 'shop_name' },
{ label: '套餐系列', prop: 'series_name' },
{ label: '卡业务类型', prop: 'card_category' },
{ label: '状态', prop: 'status' },
{ label: '激活状态', prop: 'activation_status' },
{ label: '激活时间', prop: 'activated_at' },
{ label: '网络状态', prop: 'network_status' },
{ label: '实名状态', prop: 'real_name_status' },
{ label: '实名认证策略', prop: 'realname_policy' },
{ label: '绑定设备虚拟号', prop: 'device_virtual_no' },
{ label: '店铺名称', prop: 'shop_name' },
{ label: '套餐系列', prop: 'series_name' },
{ label: '自然月累计流量(MB)', prop: 'current_month_usage_mb' },
{ label: '本月开始日期', prop: 'current_month_start_date' },
{ label: '上月流量总量(MB)', prop: 'last_month_total_mb' },
{ label: '累计流量(MB)', prop: 'data_usage_mb' },
{ label: '创建时间', prop: 'created_at' }
{ label: '最后流量检查时间', prop: 'last_data_check_at' },
{ label: '最后实名检查时间', prop: 'last_real_name_check_at' },
{ label: '启用轮询', prop: 'enable_polling' },
{ label: '批次号', prop: 'batch_no' },
{ label: '供应商', prop: 'supplier' },
{ label: '创建时间', prop: 'created_at' },
{ label: '更新时间', prop: 'updated_at' }
]
const cardList = ref<StandaloneIotCard[]>([])
@@ -1233,12 +1291,70 @@
return text
}
},
{
prop: 'activated_at',
label: '激活时间',
width: 180,
formatter: (row: StandaloneIotCard) =>
row.activated_at ? formatDateTime(row.activated_at) : '-'
},
{
prop: 'current_month_usage_mb',
label: '自然月累计流量(MB)',
width: 180,
formatter: (row: StandaloneIotCard) => row.current_month_usage_mb?.toLocaleString() || '0'
},
{
prop: 'current_month_start_date',
label: '本月开始日期',
width: 200,
formatter: (row: StandaloneIotCard) =>
row.current_month_start_date ? formatDateTime(row.current_month_start_date) : '-'
},
{
prop: 'last_month_total_mb',
label: '上月流量总量(MB)',
width: 150,
formatter: (row: StandaloneIotCard) => row.last_month_total_mb?.toLocaleString() || '0'
},
{
prop: 'data_usage_mb',
label: '累计流量(MB)',
width: 120,
formatter: (row: StandaloneIotCard) => row.data_usage_mb?.toLocaleString() || '0'
},
{
prop: 'last_data_check_at',
label: '最后流量检查时间',
width: 180,
formatter: (row: StandaloneIotCard) =>
row.last_data_check_at ? formatDateTime(row.last_data_check_at) : '-'
},
{
prop: 'last_real_name_check_at',
label: '最后实名检查时间',
width: 180,
formatter: (row: StandaloneIotCard) =>
row.last_real_name_check_at ? formatDateTime(row.last_real_name_check_at) : '-'
},
{
prop: 'enable_polling',
label: '启用轮询',
width: 100,
formatter: (row: StandaloneIotCard) => (row.enable_polling ? '是' : '否')
},
{
prop: 'batch_no',
label: '批次号',
width: 200,
showOverflowTooltip: true
},
{
prop: 'supplier',
label: '供应商',
width: 150,
formatter: (row: StandaloneIotCard) => row.supplier || '-'
},
{
prop: 'created_at',
label: '创建时间',
@@ -1289,7 +1405,6 @@
}
onMounted(() => {
loadSearchCarrierOptions()
getTableData()
})

View File

@@ -59,7 +59,27 @@
},
{ label: '资产标识符', prop: 'asset_identifier' },
{ label: '来源所有者', prop: 'from_owner_name' },
{
label: '来源类型',
formatter: (_, data) => {
const typeMap: Record<string, string> = {
platform: '平台',
shop: '店铺'
}
return typeMap[data.from_owner_type] || data.from_owner_type || '-'
}
},
{ label: '目标所有者', prop: 'to_owner_name' },
{
label: '目标类型',
formatter: (_, data) => {
const typeMap: Record<string, string> = {
platform: '平台',
shop: '店铺'
}
return typeMap[data.to_owner_type] || data.to_owner_type || '-'
}
},
{ label: '操作人', prop: 'operator_name' },
{ label: '关联卡数量', prop: 'related_card_count' },
{ label: '创建时间', prop: 'created_at', formatter: (value) => formatDateTime(value) },

View File

@@ -28,28 +28,13 @@
:pageSize="pagination.pageSize"
: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="contextMenuRef"
:menu-items="contextMenuItems"
:menu-width="120"
@select="handleContextMenuSelect"
/>
</ElCard>
</div>
</ArtTableFullScreen>
@@ -58,7 +43,7 @@
<script setup lang="ts">
import { h } from 'vue'
import { useRouter } from 'vue-router'
import { CardService } from '@/api/modules'
import { CardService, ShopService } from '@/api/modules'
import { ElMessage, ElTag } from 'element-plus'
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
@@ -67,28 +52,14 @@
import { formatDateTime } from '@/utils/business/format'
import type { AssetAllocationRecord, AllocationTypeEnum, AssetTypeEnum } from '@/types/api/card'
import { RoutesAlias } from '@/router/routesAlias'
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'
defineOptions({ name: 'AssetAllocationRecords' })
const { hasAuth } = useAuth()
const router = useRouter()
// 使用表格右键菜单功能
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
const loading = ref(false)
const tableRef = ref()
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const currentRow = ref<AssetAllocationRecord | null>(null)
// 搜索表单初始值
const initialSearchState = {
@@ -99,6 +70,7 @@
from_shop_id: undefined as number | undefined,
to_shop_id: undefined as number | undefined,
operator_id: undefined as number | undefined,
dateRange: [] as string[],
created_at_start: '',
created_at_end: ''
}
@@ -159,16 +131,51 @@
placeholder: 'ICCID或设备号'
}
},
{
label: '来源店铺',
prop: 'from_shop_id',
type: 'select',
config: {
clearable: true,
filterable: true,
remote: true,
remoteMethod: (query: string) => searchShops(query, 'from'),
placeholder: '请选择或搜索来源店铺'
},
options: () =>
fromShopOptions.value.map((shop) => ({
label: shop.shop_name,
value: shop.id
}))
},
{
label: '目标店铺',
prop: 'to_shop_id',
type: 'select',
config: {
clearable: true,
filterable: true,
remote: true,
remoteMethod: (query: string) => searchShops(query, 'to'),
placeholder: '请选择或搜索目标店铺'
},
options: () =>
toShopOptions.value.map((shop) => ({
label: shop.shop_name,
value: shop.id
}))
},
{
label: '创建时间',
prop: 'created_at_range',
prop: 'dateRange',
type: 'date',
config: {
type: 'datetimerange',
type: 'daterange',
clearable: true,
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
valueFormat: 'YYYY-MM-DDTHH:mm:ssZ'
rangeSeparator: '',
startPlaceholder: '开始日期',
endPlaceholder: '结束日期',
valueFormat: 'YYYY-MM-DD'
}
}
]
@@ -186,6 +193,33 @@
{ label: '创建时间', prop: 'created_at' }
]
// 店铺搜索选项
const fromShopOptions = ref<any[]>([])
const toShopOptions = ref<any[]>([])
// 搜索店铺
const searchShops = async (query: string, type: 'from' | 'to') => {
try {
const params: any = {
page: 1,
page_size: 20
}
if (query) {
params.shop_name = query
}
const res = await ShopService.getShops(params)
if (res.code === 0) {
if (type === 'from') {
fromShopOptions.value = res.data.items || []
} else {
toShopOptions.value = res.data.items || []
}
}
} catch (error) {
console.error('搜索店铺失败:', error)
}
}
const recordList = ref<AssetAllocationRecord[]>([])
// 获取分配类型标签类型
@@ -246,12 +280,38 @@
{
prop: 'from_owner_name',
label: '来源所有者',
width: 150
width: 150,
formatter: (row: AssetAllocationRecord) => row.from_owner_name || '-'
},
{
prop: 'from_owner_type',
label: '来源类型',
width: 100,
formatter: (row: AssetAllocationRecord) => {
const typeMap: Record<string, string> = {
platform: '平台',
shop: '店铺'
}
return typeMap[row.from_owner_type] || row.from_owner_type || '-'
}
},
{
prop: 'to_owner_name',
label: '目标所有者',
width: 150
width: 150,
formatter: (row: AssetAllocationRecord) => row.to_owner_name || '-'
},
{
prop: 'to_owner_type',
label: '目标类型',
width: 100,
formatter: (row: AssetAllocationRecord) => {
const typeMap: Record<string, string> = {
platform: '平台',
shop: '店铺'
}
return typeMap[row.to_owner_type] || row.to_owner_type || '-'
}
},
{
prop: 'operator_name',
@@ -279,6 +339,8 @@
onMounted(() => {
getTableData()
searchShops('', 'from')
searchShops('', 'to')
})
// 获取分配记录列表
@@ -291,13 +353,6 @@
...formFilters
}
// 处理日期范围
if ((params as any).created_at_range && (params as any).created_at_range.length === 2) {
params.created_at_start = (params as any).created_at_range[0]
params.created_at_end = (params as any).created_at_range[1]
delete (params as any).created_at_range
}
// 清理空值
Object.keys(params).forEach((key) => {
if (params[key] === '' || params[key] === undefined) {
@@ -327,6 +382,14 @@
// 搜索
const handleSearch = () => {
// 处理日期范围
if (formFilters.dateRange && Array.isArray(formFilters.dateRange)) {
formFilters.created_at_start = formFilters.dateRange[0] || ''
formFilters.created_at_end = formFilters.dateRange[1] || ''
} else {
formFilters.created_at_start = ''
formFilters.created_at_end = ''
}
pagination.page = 1
getTableData()
}
@@ -362,40 +425,10 @@
ElMessage.warning('您没有查看详情的权限')
}
}
// 右键菜单项配置
const contextMenuItems = computed((): MenuItemType[] => {
const items: MenuItemType[] = []
return items
})
// 处理表格行右键菜单
const handleRowContextMenu = (row: AssetAllocationRecord, column: any, event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
currentRow.value = row
contextMenuRef.value?.show(event)
}
// 处理右键菜单选择
const handleContextMenuSelect = (item: MenuItemType) => {
if (!currentRow.value) return
switch (
item.key
// No cases available
) {
}
}
</script>
<style lang="scss" scoped>
.asset-allocation-records-page {
// Allocation records page styles
}
:deep(.el-table__row.table-row-with-context-menu) {
cursor: pointer;
}
</style>

View File

@@ -82,7 +82,7 @@
<script setup lang="ts">
import { h } from 'vue'
import { useRouter } from 'vue-router'
import { AuthorizationService } from '@/api/modules'
import { AuthorizationService, EnterpriseService } from '@/api/modules'
import { ElMessage, ElTag } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import type { SearchFormItem } from '@/types'
@@ -125,6 +125,28 @@
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const currentRow = ref<AuthorizationItem | null>(null)
// 企业搜索选项
const enterpriseOptions = ref<any[]>([])
// 搜索企业
const searchEnterprises = async (query: string) => {
try {
const params: any = {
page: 1,
page_size: 20
}
if (query) {
params.enterprise_name = query
}
const res = await EnterpriseService.getEnterprises(params)
if (res.code === 0) {
enterpriseOptions.value = res.data.items || []
}
} catch (error) {
console.error('搜索企业失败:', error)
}
}
// 搜索表单初始值
const initialSearchState = {
enterprise_id: undefined as number | undefined,
@@ -165,6 +187,23 @@
placeholder: '请输入ICCID'
}
},
{
label: '企业名称',
prop: 'enterprise_id',
type: 'select',
config: {
clearable: true,
filterable: true,
remote: true,
remoteMethod: searchEnterprises,
placeholder: '请选择或搜索企业'
},
options: () =>
enterpriseOptions.value.map((e) => ({
label: e.enterprise_name,
value: e.id
}))
},
{
label: '授权人类型',
prop: 'authorizer_type',
@@ -309,6 +348,7 @@
onMounted(() => {
getTableData()
searchEnterprises('')
})
// 获取授权记录列表

View File

@@ -44,8 +44,7 @@
const loading = ref(false)
const detailData = ref<AgentRecharge | null>(null)
const rechargeId = computed(() => Number(route.params.id))
const pageTitle = computed(() => `充值订单详情 #${rechargeId.value}`)
const pageTitle = computed(() => `充值订单详情`)
// 格式化货币 - 将分转换为元
const formatCurrency = (amount: number): string => {
@@ -159,7 +158,7 @@
const loadDetailData = async () => {
loading.value = true
try {
const res = await AgentRechargeService.getAgentRechargeById(rechargeId.value)
const res = await AgentRechargeService.getAgentRechargeById(Number(route.params.id))
if (res.code === 0) {
detailData.value = res.data
} else {

View File

@@ -6,6 +6,7 @@
v-model:filter="searchForm"
:items="searchFormItems"
:show-expand="false"
label-width="100"
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
@@ -214,6 +215,7 @@
const initialSearchState: AgentRechargeQueryParams = {
shop_id: undefined,
status: undefined,
dateRange: [],
start_date: '',
end_date: ''
}
@@ -288,11 +290,12 @@
}
},
{
label: '创建时间',
label: '开始至结束',
prop: 'dateRange',
type: 'daterange',
type: 'date',
config: {
clearable: true,
type: 'daterange',
rangeSeparator: '至',
startPlaceholder: '开始日期',
endPlaceholder: '结束日期',
valueFormat: 'YYYY-MM-DD'
@@ -414,7 +417,7 @@
style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;',
onClick: (e: MouseEvent) => {
e.stopPropagation()
handleViewDetail(row)
handleNameClick(row)
}
},
row.recharge_no
@@ -733,6 +736,15 @@
})
}
// 处理名称点击
const handleNameClick = (row: AgentRecharge) => {
if (hasAuth('agent_recharge:detail_page')) {
handleViewDetail(row)
} else {
ElMessage.warning('您没有查看详情的权限')
}
}
// 查看详情
const handleViewDetail = (row: AgentRecharge) => {
router.push({

View File

@@ -1,77 +1,24 @@
<template>
<div class="refund-detail-page">
<ElCard shadow="never" v-loading="loading">
<template #header>
<div class="card-header">
<span>退款详情</span>
<ElButton @click="handleGoBack">返回</ElButton>
</div>
</template>
<ElCard shadow="never">
<!-- 页面头部 -->
<div class="detail-header">
<ElButton @click="handleBack">
<template #icon>
<ElIcon><ArrowLeft /></ElIcon>
</template>
返回
</ElButton>
<h2 class="detail-title">{{ pageTitle }}</h2>
</div>
<ElDescriptions :column="2" border v-if="refund">
<ElDescriptionsItem label="退款单号">{{ refund.refund_no }}</ElDescriptionsItem>
<ElDescriptionsItem label="状态">
<ElTag :type="getStatusType(refund.status)">{{ getStatusText(refund.status) }}</ElTag>
</ElDescriptionsItem>
<!-- 详情内容 -->
<DetailPage v-if="refund" :sections="detailSections" :data="refund" />
<ElDescriptionsItem label="申请退款金额">
{{ formatCurrency(refund.requested_refund_amount) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="实际退款金额">
{{ refund.approved_refund_amount ? formatCurrency(refund.approved_refund_amount) : '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="实收金额">
{{ formatCurrency(refund.actual_received_amount) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="资产重置">
<ElTag :type="refund.asset_reset ? 'success' : 'info'">
{{ refund.asset_reset ? '是' : '否' }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="佣金扣除">
<ElTag :type="refund.commission_deducted ? 'success' : 'info'">
{{ refund.commission_deducted ? '是' : '否' }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="退款原因">
{{ refund.refund_reason || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="拒绝原因">
{{ refund.reject_reason || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="审批时间">
{{ refund.processed_at ? formatDateTime(refund.processed_at) : '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="审批备注">
{{ refund.remark || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="创建时间">
{{ formatDateTime(refund.created_at) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="更新时间">
{{ formatDateTime(refund.updated_at) }}
</ElDescriptionsItem>
</ElDescriptions>
<div class="action-buttons" v-if="refund">
<!-- 待审批状态显示审批操作 -->
<template v-if="refund.status === 1">
<ElButton type="primary" @click="handleShowApprove">审批通过</ElButton>
<ElButton type="danger" @click="handleShowReject">审批拒绝</ElButton>
<ElButton type="warning" @click="handleShowReturn">退回</ElButton>
</template>
<!-- 已拒绝或已退回状态显示重新提交 -->
<template v-if="refund.status === 3 || refund.status === 4">
<ElButton type="primary" @click="handleShowResubmit">重新提交</ElButton>
</template>
<!-- 加载中 -->
<div v-if="loading" class="loading-container">
<ElIcon class="is-loading"><Loading /></ElIcon>
<span>加载中...</span>
</div>
</ElCard>
@@ -247,11 +194,13 @@
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { ref, onMounted, computed, h } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElCard, ElButton, ElIcon, ElMessage, ElTag } 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 { RefundService } from '@/api/modules'
import { ElMessage, ElTag } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import type {
Refund,
RefundStatus,
@@ -261,38 +210,39 @@
ResubmitRefundRequest
} from '@/types/api'
import { formatDateTime } from '@/utils/business/format'
import { RoutesAlias } from '@/router/routesAlias'
defineOptions({ name: 'RefundDetail' })
const router = useRouter()
const route = useRoute()
const router = useRouter()
const loading = ref(false)
const approveLoading = ref(false)
const rejectLoading = ref(false)
const returnLoading = ref(false)
const resubmitLoading = ref(false)
const refund = ref<Refund | null>(null)
const pageTitle = computed(() => `退款详情`)
const approveDialogVisible = ref(false)
const rejectDialogVisible = ref(false)
const returnDialogVisible = ref(false)
const resubmitDialogVisible = ref(false)
const approveFormRef = ref<FormInstance>()
const rejectFormRef = ref<FormInstance>()
const returnFormRef = ref<FormInstance>()
const resubmitFormRef = ref<FormInstance>()
const approveLoading = ref(false)
const rejectLoading = ref(false)
const returnLoading = ref(false)
const resubmitLoading = ref(false)
const approveRules = reactive<FormRules>({})
const approveFormRef = ref()
const rejectFormRef = ref()
const returnFormRef = ref()
const resubmitFormRef = ref()
const rejectRules = reactive<FormRules>({
const approveRules = ref()
const rejectRules = ref({
reject_reason: [{ required: true, message: '请输入拒绝原因', trigger: 'blur' }]
})
const returnRules = reactive<FormRules>({})
const resubmitRules = reactive<FormRules>({})
const returnRules = ref()
const resubmitRules = ref()
const approveForm = reactive<ApproveRefundRequest>({
approved_refund_amount: undefined,
@@ -317,23 +267,20 @@
refund_reason: ''
})
// 格式化货币 - 将分转换为元
const formatCurrency = (amount: number): string => {
return `¥${(amount / 100).toFixed(2)}`
}
// 获取状态标签类型
const getStatusType = (status: RefundStatus): 'warning' | 'success' | 'danger' | 'info' => {
const statusMap: Record<RefundStatus, 'warning' | 'success' | 'danger' | 'info'> = {
1: 'warning', // 待审批
2: 'success', // 已通过
3: 'danger', // 已拒绝
4: 'info' // 已退回
1: 'warning',
2: 'success',
3: 'danger',
4: 'info'
}
return statusMap[status] || 'info'
}
// 获取状态文本
const getStatusText = (status: RefundStatus): string => {
const statusMap: Record<RefundStatus, string> = {
1: '待审批',
@@ -344,14 +291,94 @@
return statusMap[status] || '-'
}
onMounted(() => {
const id = route.params.id as string
if (id) {
fetchRefundDetail(Number(id))
const detailSections = computed((): DetailSection[] => [
{
title: '退款信息',
fields: [
{ label: '退款单号', prop: 'refund_no' },
{ label: '店铺名称', prop: 'shop_name' },
{ label: '订单号', prop: 'order_no' },
{ label: '资产标识符', prop: 'asset_identifier' },
{ label: '资产类型', prop: 'asset_type' },
{
label: '申请退款金额',
formatter: (_, data) => formatCurrency(data.requested_refund_amount)
},
{
label: '实际退款金额',
formatter: (_, data) =>
data.approved_refund_amount ? formatCurrency(data.approved_refund_amount) : '-'
},
{
label: '实收金额',
formatter: (_, data) => formatCurrency(data.actual_received_amount)
},
{
label: '状态',
render: (data) =>
h(ElTag, { type: getStatusType(data.status) }, () => getStatusText(data.status))
},
{
label: '资产重置',
render: (data) =>
h(ElTag, { type: data.asset_reset ? 'success' : 'info' }, () =>
data.asset_reset ? '是' : '否'
)
},
{
label: '佣金扣除',
render: (data) =>
h(ElTag, { type: data.commission_deducted ? 'success' : 'info' }, () =>
data.commission_deducted ? '是' : '否'
)
}
]
},
{
title: '退款原因',
fields: [
{
label: '退款原因',
prop: 'refund_reason',
formatter: (value) => value || '-',
fullWidth: true
},
{
label: '拒绝原因',
prop: 'reject_reason',
formatter: (value) => value || '-',
fullWidth: true
},
{
label: '审批备注',
prop: 'remark',
formatter: (value) => value || '-',
fullWidth: true
}
]
},
{
title: '时间信息',
fields: [
{
label: '创建时间',
prop: 'created_at',
formatter: (value) => formatDateTime(value)
},
{
label: '审批时间',
prop: 'processed_at',
formatter: (value) => (value ? formatDateTime(value) : '-')
},
{
label: '更新时间',
prop: 'updated_at',
formatter: (value) => formatDateTime(value)
}
]
}
})
])
// 获取退款详情
const fetchRefundDetail = async (id: number) => {
loading.value = true
try {
@@ -366,24 +393,40 @@
}
}
// 返回
const handleGoBack = () => {
router.back()
const handleBack = () => {
router.push(RoutesAlias.RefundManagement)
}
// 显示审批通过对话框
const handleShowApprove = () => {
approveDialogVisible.value = true
}
onMounted(() => {
const id = route.params.id as string
if (id) {
fetchRefundDetail(Number(id))
}
})
// 审批通过对话框关闭后的清理
const handleApproveDialogClosed = () => {
approveFormRef.value?.resetFields()
approveForm.approved_refund_amount = undefined
approveForm.remark = ''
}
// 审批通过
const handleRejectDialogClosed = () => {
rejectFormRef.value?.resetFields()
rejectForm.reject_reason = ''
}
const handleReturnDialogClosed = () => {
returnFormRef.value?.resetFields()
returnForm.remark = ''
}
const handleResubmitDialogClosed = () => {
resubmitFormRef.value?.resetFields()
resubmitForm.requested_refund_amount = undefined
resubmitForm.actual_received_amount = undefined
resubmitForm.refund_reason = ''
}
const handleApproveRefund = async () => {
if (!approveFormRef.value || !refund.value) return
@@ -411,18 +454,6 @@
})
}
// 显示审批拒绝对话框
const handleShowReject = () => {
rejectDialogVisible.value = true
}
// 审批拒绝对话框关闭后的清理
const handleRejectDialogClosed = () => {
rejectFormRef.value?.resetFields()
rejectForm.reject_reason = ''
}
// 审批拒绝
const handleRejectRefund = async () => {
if (!rejectFormRef.value || !refund.value) return
@@ -443,18 +474,6 @@
})
}
// 显示退回对话框
const handleShowReturn = () => {
returnDialogVisible.value = true
}
// 退回对话框关闭后的清理
const handleReturnDialogClosed = () => {
returnFormRef.value?.resetFields()
returnForm.remark = ''
}
// 退回
const handleReturnRefund = async () => {
if (!returnFormRef.value || !refund.value) return
@@ -475,25 +494,6 @@
})
}
// 显示重新提交对话框
const handleShowResubmit = () => {
if (!refund.value) return
// 预填充原有数据
resubmitForm.requested_refund_amount = refund.value.requested_refund_amount / 100
resubmitForm.actual_received_amount = refund.value.actual_received_amount / 100
resubmitForm.refund_reason = refund.value.refund_reason
resubmitDialogVisible.value = true
}
// 重新提交对话框关闭后的清理
const handleResubmitDialogClosed = () => {
resubmitFormRef.value?.resetFields()
resubmitForm.requested_refund_amount = undefined
resubmitForm.actual_received_amount = undefined
resubmitForm.refund_reason = ''
}
// 重新提交
const handleResubmitRefund = async () => {
if (!resubmitFormRef.value || !refund.value) return
@@ -529,17 +529,32 @@
.refund-detail-page {
padding: 20px;
.card-header {
.detail-header {
display: flex;
gap: 16px;
align-items: center;
justify-content: space-between;
margin-bottom: 24px;
.detail-title {
margin: 0;
font-size: 20px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
.action-buttons {
.loading-container {
display: flex;
gap: 10px;
justify-content: flex-end;
margin-top: 20px;
flex-direction: column;
gap: 12px;
align-items: center;
justify-content: center;
padding: 60px 0;
color: var(--el-text-color-secondary);
.el-icon {
font-size: 32px;
}
}
}
</style>

View File

@@ -62,7 +62,6 @@
:loading="orderSearchLoading"
placeholder="请输入订单号搜索"
style="width: 100%"
@focus="handleOrderSelectFocus"
@change="handleOrderSelectChange"
>
<ElOption
@@ -84,11 +83,7 @@
/>
</ElFormItem>
<ElFormItem label="实收金额">
<ElInput
:model-value="selectedOrderActualPaid"
disabled
style="width: 100%"
/>
<ElInput :model-value="selectedOrderActualPaid" disabled style="width: 100%" />
</ElFormItem>
<ElFormItem label="退款原因">
<ElInput
@@ -315,7 +310,6 @@
defineOptions({ name: 'RefundList' })
const router = useRouter()
const userStore = useUserStore()
const { hasAuth } = useAuth()
const loading = ref(false)
@@ -364,7 +358,26 @@
})),
config: {
clearable: true,
filterable: true
filterable: true,
remote: true,
remoteMethod: (query: string) => searchShops(query)
}
},
{
label: '订单号',
prop: 'order_id',
type: 'select',
placeholder: '请选择订单号',
options: () =>
orderOptions.value.map((order) => ({
label: `${order.order_no}`,
value: order.id
})),
config: {
clearable: true,
filterable: true,
remote: true,
remoteMethod: (query: string) => searchOrdersForFilter(query)
}
},
{
@@ -381,21 +394,6 @@
config: {
clearable: true
}
},
{
label: '订单',
prop: 'order_id',
type: 'select',
placeholder: '请选择订单',
options: () =>
orderOptions.value.map((order) => ({
label: `订单号: ${order.order_no} (ID: ${order.id})`,
value: order.id
})),
config: {
clearable: true,
filterable: true
}
}
]
@@ -409,8 +407,10 @@
// 列配置
const columnOptions = [
{ label: '退款单号', prop: 'refund_no' },
{ label: '订单ID', prop: 'order_id' },
{ label: '店铺ID', prop: 'shop_id' },
{ label: '店铺名称', prop: 'shop_name' },
{ label: '订单号', prop: 'order_no' },
{ label: '资产标识符', prop: 'asset_identifier' },
{ label: '资产类型', prop: 'asset_type' },
{ label: '申请退款金额', prop: 'requested_refund_amount' },
{ label: '实际退款金额', prop: 'approved_refund_amount' },
{ label: '实收金额', prop: 'actual_received_amount' },
@@ -523,7 +523,7 @@
{
prop: 'refund_no',
label: '退款单号',
minWidth: 240,
minWidth: 210,
formatter: (row: Refund) => {
return h(
'span',
@@ -531,7 +531,7 @@
style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;',
onClick: (e: MouseEvent) => {
e.stopPropagation()
handleViewDetail(row)
handleNameClick(row)
}
},
row.refund_no
@@ -539,14 +539,30 @@
}
},
{
prop: 'order_id',
label: '订单ID',
width: 120
prop: 'shop_name',
label: '店铺名称',
width: 160
},
{
prop: 'shop_id',
label: '店铺ID',
width: 120
prop: 'order_no',
label: '订单号',
width: 220
},
{
prop: 'asset_identifier',
label: '资产标识符',
width: 180
},
{
prop: 'asset_type',
label: '资产类型',
width: 100,
formatter: (row: Refund) =>
row.asset_type === 'device'
? '设备'
: row.asset_type === 'iot_card'
? 'IoT卡'
: row.asset_type || '-'
},
{
prop: 'requested_refund_amount',
@@ -629,39 +645,45 @@
onMounted(() => {
getTableData()
loadShops()
loadOrders()
searchShops('')
searchOrdersForFilter('')
})
// 加载店铺列表
const loadShops = async () => {
// 搜索店铺(用于搜索表单)
const searchShops = async (query: string) => {
try {
const params: any = {
page: 1,
page_size: 9999
page_size: 20
}
if (query) {
params.shop_name = query
}
const res = await ShopService.getShops(params)
if (res.code === 0) {
shopOptions.value = res.data.items || []
}
} catch (error) {
console.error('Load shops failed:', error)
console.error('Search shops failed:', error)
}
}
// 加载订单列表
const loadOrders = async () => {
// 搜索订单(用于搜索表单)
const searchOrdersForFilter = async (query: string) => {
try {
const params: any = {
page: 1,
page_size: 9999
page_size: 20
}
if (query) {
params.order_no = query
}
const res = await OrderService.getOrders(params)
if (res.code === 0) {
orderOptions.value = res.data.items || []
}
} catch (error) {
console.error('Load orders failed:', error)
console.error('Search orders failed:', error)
}
}
@@ -759,14 +781,6 @@
createDialogVisible.value = true
}
// 订单选择框获取焦点时加载默认数据
const handleOrderSelectFocus = () => {
// 如果还没有数据则加载默认的20条
if (orderSearchOptions.value.length === 0) {
searchOrders('')
}
}
// 对话框关闭后的清理
const handleCreateDialogClosed = () => {
createFormRef.value?.resetFields()
@@ -965,6 +979,15 @@
})
}
// 处理名称点击
const handleNameClick = (row: Refund) => {
if (hasAuth('refund:detail')) {
handleViewDetail(row)
} else {
ElMessage.warning('您没有查看详情的权限')
}
}
// 查看详情
const handleViewDetail = (row: Refund) => {
router.push({

View File

@@ -5,7 +5,7 @@
<ArtSearchBar
v-model:filter="searchForm"
:items="searchFormItems"
:show-expand="false"
show-expand
@reset="handleReset"
label-width="100"
@search="handleSearch"
@@ -356,8 +356,11 @@
order_no: '',
buyer_phone: '',
payment_status: undefined,
payment_method: undefined,
order_type: undefined,
purchase_role: undefined,
identifier: '',
is_expired: undefined,
start_time: '',
end_time: ''
}
@@ -378,6 +381,28 @@
// 搜索表单配置
const searchFormItems: SearchFormItem[] = [
{
label: '订单号',
prop: 'order_no',
type: 'input',
placeholder: '请输入订单号',
config: {
maxlength: 50,
clearable: true
},
labelWidth: '100'
},
{
label: '资产标识符',
prop: 'identifier',
type: 'input',
placeholder: 'ICCID 或 VirtualNo',
config: {
maxlength: 30,
clearable: true
},
labelWidth: '100'
},
{
label: '买家手机号',
prop: 'buyer_phone',
@@ -389,6 +414,21 @@
},
labelWidth: '100'
},
{
label: '支付方式',
prop: 'payment_method',
type: 'select',
placeholder: '请选择支付方式',
options: [
{ label: '钱包支付', value: 'wallet' },
{ label: '微信支付', value: 'wechat' },
{ label: '支付宝支付', value: 'alipay' },
{ label: '线下支付', value: 'offline' }
],
config: {
clearable: true
}
},
{
label: '支付状态',
prop: 'payment_status',
@@ -404,6 +444,19 @@
clearable: true
}
},
{
label: '是否过期',
prop: 'is_expired',
type: 'select',
placeholder: '请选择',
options: [
{ label: '已过期', value: true },
{ label: '未过期', value: false }
],
config: {
clearable: true
}
},
{
label: '订单类型',
prop: 'order_type',
@@ -433,14 +486,15 @@
}
},
{
label: '创建时间',
label: '开始至结束',
prop: 'dateRange',
type: 'daterange',
type: 'date',
config: {
clearable: true,
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',
valueFormat: 'YYYY-MM-DD HH:mm:ss'
type: 'daterange',
rangeSeparator: '',
startPlaceholder: '开始日期',
endPlaceholder: '结束日期',
valueFormat: 'YYYY-MM-DD'
}
}
]
@@ -459,15 +513,16 @@
{ label: '买家昵称', prop: 'buyer_nickname' },
{ label: '订单类型', prop: 'order_type' },
{ label: '买家类型', prop: 'buyer_type' },
{ label: '资产标识符', prop: 'asset_identifier' },
{ label: '订单渠道', prop: 'purchase_role' },
{ label: '购买备注', prop: 'purchase_remark' },
{ label: '操作者', prop: 'operator_name' },
{ label: '支付状态', prop: 'payment_status' },
{ label: '佣金状态', prop: 'commission_status_name' },
{ label: '订单金额', prop: 'total_amount' },
{ label: '实付金额', prop: 'actual_paid_amount' },
{ label: '支付方式', prop: 'payment_method' },
{ label: '支付时间', prop: 'paid_at' },
{ label: '创建时间', prop: 'created_at' }
{ label: '支付时间', prop: 'paid_at' }
]
const createFormRef = ref<FormInstance>()
@@ -794,6 +849,12 @@
)
}
},
{
prop: 'asset_identifier',
label: '资产标识符',
width: 180,
formatter: (row: Order) => row.asset_identifier || '-'
},
{
prop: 'purchase_role',
label: '订单渠道',
@@ -838,6 +899,12 @@
)
}
},
{
prop: 'commission_status_name',
label: '佣金状态',
width: 120,
formatter: (row: Order) => row.commission_status_name || '-'
},
{
prop: 'total_amount',
label: '订单金额',
@@ -864,12 +931,6 @@
label: '支付时间',
width: 180,
formatter: (row: Order) => (row.paid_at ? formatDateTime(row.paid_at) : '-')
},
{
prop: 'created_at',
label: '创建时间',
width: 180,
formatter: (row: Order) => formatDateTime(row.created_at)
}
])
@@ -896,8 +957,11 @@
order_no: searchForm.order_no || undefined,
buyer_phone: searchForm.buyer_phone || undefined,
payment_status: searchForm.payment_status,
payment_method: searchForm.payment_method,
order_type: searchForm.order_type,
purchase_role: searchForm.purchase_role,
identifier: searchForm.identifier || undefined,
is_expired: searchForm.is_expired,
start_time: searchForm.start_time || undefined,
end_time: searchForm.end_time || undefined
}

View File

@@ -511,15 +511,46 @@
{ label: '套餐名称', prop: 'package_name' },
{ label: '所属系列', prop: 'series_name' },
{ label: '套餐类型', prop: 'package_type' },
{ label: '套餐周期类型', prop: 'calendar_type' },
{ label: '有效期', prop: 'duration_months' },
{ label: '真流量', prop: 'real_data_mb' },
{ label: '虚流量', prop: 'virtual_data_mb' },
{ label: '虚流量比例', prop: 'virtual_ratio' },
{ label: '有效期', prop: 'duration_months' },
{
label: '启用虚流量',
prop: 'enable_virtual_data',
formatter: (row: any) => (row.enable_virtual_data ? '是' : '否')
},
{
label: '流量重置周期',
prop: 'data_reset_cycle',
formatter: (row: any) => {
const map: Record<string, string> = {
daily: '每日',
weekly: '每周',
monthly: '每月',
none: '不重置'
}
return map[row.data_reset_cycle] || row.data_reset_cycle || '-'
}
},
{
label: '到期时间基准',
prop: 'expiry_base',
formatter: (row: any) => {
const map: Record<string, string> = {
from_activation: '实名激活',
from_purchase: '购买时'
}
return map[row.expiry_base] || row.expiry_base || '-'
}
},
{ label: '成本价', prop: 'cost_price' },
{ label: '建议售价', prop: 'suggested_retail_price' },
{ label: '上架状态', prop: 'shelf_status' },
{ label: '状态', prop: 'status' },
{ label: '创建时间', prop: 'created_at' }
{ label: '创建时间', prop: 'created_at' },
{ label: '更新时间', prop: 'updated_at' }
]
// 表单验证规则
@@ -729,6 +760,44 @@
width: 100,
formatter: (row: PackageResponse) => `${row.duration_months}`
},
{
prop: 'calendar_type',
label: '套餐周期类型',
width: 120,
formatter: (row: PackageResponse) => {
const map: Record<string, string> = {
natural_month: '自然月',
by_day: '按天'
}
return map[row.calendar_type] || row.calendar_type || '-'
}
},
{
prop: 'data_reset_cycle',
label: '流量重置周期',
width: 120,
formatter: (row: PackageResponse) => {
const map: Record<string, string> = {
daily: '每日',
weekly: '每周',
monthly: '每月',
none: '不重置'
}
return map[row.data_reset_cycle] || row.data_reset_cycle || '-'
}
},
{
prop: 'expiry_base',
label: '到期时间基准',
width: 120,
formatter: (row: PackageResponse) => {
const map: Record<string, string> = {
from_activation: '实名激活',
from_purchase: '购买时'
}
return map[row.expiry_base] || row.expiry_base || '-'
}
},
{
prop: 'cost_price',
label: '成本价',
@@ -850,8 +919,6 @@
)
onMounted(() => {
loadSeriesOptions()
loadSearchSeriesOptions()
getTableData()
})

View File

@@ -460,14 +460,46 @@
{ label: '系列编码', prop: 'series_code' },
{ label: '系列名称', prop: 'series_name' },
{ label: '描述', prop: 'description' },
{ label: '一次性佣金', prop: 'enable_one_time_commission' },
{ label: '佣金类型', prop: 'commission_type' },
{ label: '触发阈值', prop: 'commission_threshold' },
{ label: '触发类型', prop: 'trigger_type' },
{ label: '强充状态', prop: 'enable_force_recharge' },
{ label: '强充金额', prop: 'force_amount' },
{ label: '强充计算类型', prop: 'force_calc_type' },
{ label: '时效类型', prop: 'validity_type' },
{
label: '一次性佣金',
prop: 'enable_one_time_commission',
formatter: (row: any) => (row.enable_one_time_commission ? '启用' : '未启用')
},
{
label: '佣金类型',
prop: 'commission_type',
formatter: (row: any) => row.one_time_commission_config?.commission_type || '-'
},
{
label: '触发阈值',
prop: 'threshold',
formatter: (row: any) => row.one_time_commission_config?.threshold ?? '-'
},
{
label: '触发类型',
prop: 'trigger_type',
formatter: (row: any) => row.one_time_commission_config?.trigger_type || '-'
},
{
label: '强充状态',
prop: 'enable_force_recharge',
formatter: (row: any) => (row.one_time_commission_config?.enable_force_recharge ? '启用' : '未启用')
},
{
label: '强充金额',
prop: 'force_amount',
formatter: (row: any) => row.one_time_commission_config?.force_amount ?? '-'
},
{
label: '强充计算类型',
prop: 'force_calc_type',
formatter: (row: any) => row.one_time_commission_config?.force_calc_type || '-'
},
{
label: '时效类型',
prop: 'validity_type',
formatter: (row: any) => row.one_time_commission_config?.validity_type || '-'
},
{ label: '状态', prop: 'status' },
{ label: '创建时间', prop: 'created_at' }
]

View File

@@ -459,10 +459,16 @@
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="启用强制充值">
<ElSwitch v-model="form.enable_force_recharge" />
<ElSwitch
v-model="form.enable_force_recharge"
:disabled="form.force_recharge_locked && isAgentAccount"
/>
</ElFormItem>
</ElCol>
<ElCol :span="12" v-if="form.enable_force_recharge">
<ElCol
:span="12"
v-if="form.enable_force_recharge && !(form.force_recharge_locked && isAgentAccount)"
>
<ElFormItem label="强充金额(元)" prop="force_recharge_amount">
<ElInputNumber
v-model="form.force_recharge_amount"
@@ -625,10 +631,13 @@
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="启用强制充值">
<ElSwitch v-model="form.enable_force_recharge" />
<ElSwitch
v-model="form.enable_force_recharge"
:disabled="form.force_recharge_locked && isAgentAccount"
/>
</ElFormItem>
</ElCol>
<ElCol :span="12" v-if="form.enable_force_recharge">
<ElCol :span="12" v-if="form.enable_force_recharge && !(form.force_recharge_locked && isAgentAccount)">
<ElFormItem label="强充金额(元)" prop="force_recharge_amount">
<ElInputNumber
v-model="form.force_recharge_amount"
@@ -783,6 +792,8 @@
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth'
import { useUserStore } from '@/store/modules/user'
import { storeToRefs } from 'pinia'
import { formatDateTime } from '@/utils/business/format'
import {
CommonStatus,
@@ -797,6 +808,9 @@
const { hasAuth } = useAuth()
const router = useRouter()
const userStore = useUserStore()
const { info: userInfo } = storeToRefs(userStore)
const isAgentAccount = computed(() => userInfo.value.user_type === 3)
const dialogVisible = ref(false)
const loading = ref(false)
@@ -1036,6 +1050,7 @@
}>, // 梯度配置(包含完整字段,从系列继承)
enable_force_recharge: false,
force_recharge_amount: undefined,
force_recharge_locked: false, // 强充是否被套餐系列锁定
series_force_recharge_amount: 0, // 系列强充金额(元)
packages: [] as Array<{ package_id: number; cost_price: number }> // 套餐配置
})
@@ -1207,6 +1222,27 @@
)
}
},
{
prop: 'force_recharge_locked',
label: '强充锁定',
width: 100,
formatter: (row: ShopSeriesGrantResponse) => {
return h(
ElTag,
{ type: row.force_recharge_locked ? 'danger' : 'success', size: 'small' },
() => (row.force_recharge_locked ? '是' : '否')
)
}
},
{
prop: 'force_recharge_amount',
label: '强充金额',
width: 100,
formatter: (row: ShopSeriesGrantResponse) => {
if (!row.force_recharge_enabled) return '-'
return `¥${((row.force_recharge_amount || 0) / 100).toFixed(2)}`
}
},
{
prop: 'package_count',
label: '套餐数量',
@@ -1794,6 +1830,7 @@
form.force_recharge_amount = detail.force_recharge_amount
? detail.force_recharge_amount / 100
: undefined
form.force_recharge_locked = detail.force_recharge_locked || false
// 设置套餐数据
if (detail.packages && detail.packages.length > 0) {
@@ -1842,6 +1879,7 @@
form.commission_tiers = []
form.enable_force_recharge = false
form.force_recharge_amount = undefined
form.force_recharge_locked = false
form.packages = []
selectedPackageIds.value = []
packageOptions.value = [] // 清空套餐选项
@@ -1874,6 +1912,7 @@
form.commission_tiers = []
form.enable_force_recharge = false
form.force_recharge_amount = undefined
form.force_recharge_locked = false
form.packages = []
selectedPackageIds.value = []
}

View File

@@ -14,16 +14,7 @@
</div>
</template>
<el-table
v-loading="loading"
:data="tableData"
border
stripe
:row-class-name="getRowClassName"
@row-contextmenu="handleRowContextMenu"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
>
<el-table v-loading="loading" :data="tableData" border stripe>
<el-table-column prop="task_type" label="任务类型" min-width="200" />
<el-table-column prop="task_type_name" label="任务名称" min-width="150" />
<el-table-column prop="max_concurrency" label="最大并发数" min-width="120" align="center" />
@@ -50,18 +41,13 @@
/>
</template>
</el-table-column>
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)">修改配置</el-button>
<el-button type="danger" link @click="handleReset(row)">重置</el-button>
</template>
</el-table-column>
</el-table>
<!-- 鼠标悬浮提示 -->
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
<!-- 右键菜单 -->
<ArtMenuRight
ref="contextMenuRef"
:menu-items="contextMenuItems"
:menu-width="120"
@select="handleContextMenuSelect"
/>
</el-card>
<!-- 编辑对话框 -->
@@ -94,22 +80,16 @@
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { PollingConcurrencyService } from '@/api/modules'
import type { PollingConcurrency, UpdateConcurrencyParams } from '@/types/api'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
import { useTableContextMenu } from '@/composables/useTableContextMenu'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
const loading = ref(false)
const tableData = ref<PollingConcurrency[]>([])
const dialogVisible = ref(false)
const formRef = ref<FormInstance>()
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const currentClickRow = ref<PollingConcurrency | null>(null)
const currentRow = ref<PollingConcurrency>({
task_type: '',
@@ -131,27 +111,6 @@
]
}
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
const contextMenuItems = computed<MenuItemType[]>(() => [
{
label: '修改配置',
key: 'edit',
permission: 'polling_concurrency_update'
},
{
label: '重置',
key: 'reset',
permission: 'polling_concurrency_reset'
}
])
const getUtilizationColor = (percentage: number) => {
if (percentage >= 90) return '#f56c6c'
if (percentage >= 70) return '#e6a23c'
@@ -240,28 +199,6 @@
formRef.value?.resetFields()
}
// 处理表格行右键菜单
const handleRowContextMenu = (row: PollingConcurrency, column: any, event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
currentClickRow.value = row
contextMenuRef.value?.show(event)
}
// 处理右键菜单选择
const handleContextMenuSelect = (item: MenuItemType) => {
if (!currentClickRow.value) return
switch (item.key) {
case 'edit':
handleEdit(currentClickRow.value)
break
case 'reset':
handleReset(currentClickRow.value)
break
}
}
onMounted(() => {
loadData()
})

View File

@@ -35,28 +35,14 @@
:pageSize="pagination.page_size"
:total="pagination.total"
:marginTop="10"
:row-class-name="getRowClassName"
:actions="getActions"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
@row-contextmenu="handleRowContextMenu"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
>
<template #default>
<el-table-column v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 鼠标悬浮提示 -->
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
<!-- 右键菜单 -->
<ArtMenuRight
ref="contextMenuRef"
:menu-items="contextMenuItems"
:menu-width="120"
@select="handleContextMenuSelect"
/>
</el-card>
<!-- 新增/编辑对话框 -->
@@ -256,9 +242,7 @@
} from '@/types/api'
import type { SearchFormItem } from '@/types'
import { formatDateTime } from '@/utils/business/format'
import { useTableContextMenu } from '@/composables/useTableContextMenu'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
const loading = ref(false)
const tableData = ref<PollingConfig[]>([])
@@ -266,8 +250,6 @@
const dialogVisible = ref(false)
const dialogType = ref<'create' | 'edit'>('create')
const formRef = ref<FormInstance>()
const contextMenuRef = ref()
const currentClickRow = ref<PollingConfig | null>(null)
const carrierList = ref<any[]>([])
// 分页数据
@@ -327,14 +309,6 @@
]
}
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
// 搜索表单配置
const searchFormItems = computed<SearchFormItem[]>(() => [
{
@@ -501,23 +475,28 @@
}))
)
const contextMenuItems = computed<MenuItemType[]>(() => [
{
label: '编辑',
key: 'edit',
permission: 'polling_config_update'
},
{
label: currentClickRow.value?.status === 1 ? '禁用' : '启用',
key: 'toggle',
permission: 'polling_config_update_status'
},
{
const getActions = (row: PollingConfig) => {
const actions: any[] = [
{
label: '编辑',
handler: () => handleEdit(row)
}
]
actions.push({
label: row.status === 1 ? '禁用' : '启用',
type: row.status === 1 ? 'danger' : 'primary',
handler: () => handleToggleStatus(row)
})
actions.push({
label: '删除',
key: 'delete',
permission: 'polling_config_delete'
}
])
type: 'danger',
handler: () => handleDelete(row)
})
return actions
}
const loadCarriers = async (carrierName?: string) => {
try {
@@ -708,29 +687,6 @@
formRef.value?.resetFields()
}
const handleRowContextMenu = (row: PollingConfig, column: any, event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
currentClickRow.value = row
contextMenuRef.value?.show(event)
}
const handleContextMenuSelect = (item: MenuItemType) => {
if (!currentClickRow.value) return
switch (item.key) {
case 'edit':
handleEdit(currentClickRow.value)
break
case 'toggle':
handleToggleStatus(currentClickRow.value)
break
case 'delete':
handleDelete(currentClickRow.value)
break
}
}
onMounted(() => {
loadCarriers()
loadData()

View File

@@ -5,7 +5,7 @@
<ArtSearchBar
v-model:filter="searchForm"
:items="searchFormItems"
:show-expand="false"
show-expand
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
@@ -347,6 +347,28 @@
const availableRoles = ref<ShopRoleResponse[]>([])
const selectedRoleId = ref<number | undefined>(undefined)
// 上级店铺搜索选项
const parentShopOptions = ref<any[]>([])
// 搜索上级店铺
const searchParentShops = async (query: string) => {
try {
const params: any = {
page: 1,
page_size: 20
}
if (query) {
params.shop_name = query
}
const res = await ShopService.getShops(params)
if (res.code === 0) {
parentShopOptions.value = res.data.items || []
}
} catch (error) {
console.error('搜索店铺失败:', error)
}
}
// 定义表单搜索初始值
const initialSearchState = {
shop_name: '',
@@ -408,6 +430,23 @@
placeholder: '请输入店铺编号'
}
},
{
label: '上级店铺',
prop: 'parent_id',
type: 'select',
config: {
clearable: true,
filterable: true,
remote: true,
remoteMethod: (query: string) => searchParentShops(query),
placeholder: '请选择或搜索上级店铺'
},
options: () =>
parentShopOptions.value.map((shop) => ({
label: shop.shop_name,
value: shop.id
}))
},
{
label: '状态',
prop: 'status',
@@ -722,6 +761,7 @@
getShopList()
loadTopLevelShops() // 加载顶级店铺用于级联选择
searchDefaultRoles('') // 加载初始默认角色列表
searchParentShops('') // 加载上级店铺选项
})
// 获取店铺列表