修改资产详情
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m22s

This commit is contained in:
sexygoat
2026-03-20 14:18:47 +08:00
parent 62f828f314
commit a805c27902
3 changed files with 262 additions and 92 deletions

View File

@@ -405,4 +405,30 @@ export class CardService extends BaseService {
static deactivateIotCard(id: number): Promise<BaseResponse> {
return this.patch<BaseResponse>(`/api/admin/iot-cards/${id}/deactivate`, {})
}
/**
* 获取套餐流量详单(每日流量记录)
* @param packageUsageId 套餐使用记录ID
*/
static getPackageDailyRecords(packageUsageId: number): Promise<BaseResponse<{
package_name: string
package_usage_id: number
total_usage_mb: number
records: Array<{
date: string
daily_usage_mb: number
cumulative_usage_mb: number
}>
}>> {
return this.get<BaseResponse<{
package_name: string
package_usage_id: number
total_usage_mb: number
records: Array<{
date: string
daily_usage_mb: number
cumulative_usage_mb: number
}>
}>>(`/api/admin/package-usage/${packageUsageId}/daily-records`)
}
}

View File

@@ -1,15 +1,9 @@
<template>
<div class="single-card-page">
<!-- 占位符用于在卡片固定时保持布局 -->
<div v-if="isSearchCardFixed" ref="searchCardPlaceholder" class="search-card-placeholder"></div>
<!-- ICCID查询区域 -->
<!-- 资产查询区域 -->
<ElCard
ref="searchCardRef"
shadow="never"
class="search-card"
:class="{ 'is-fixed': isSearchCardFixed }"
:style="fixedCardStyle"
>
<template #header>
<div class="card-header">
@@ -488,12 +482,19 @@
class="package-table"
border
>
<ElTableColumn
prop="package_name"
label="套餐名称"
min-width="150"
show-overflow-tooltip
/>
<ElTableColumn label="套餐名称" min-width="150">
<template #default="scope">
<ElButton
type="primary"
link
class="package-name-link"
@click="handleShowDailyRecords(scope.row)"
v-permission="'package-usage:daily-records:view'"
>
{{ scope.row.package_name }}
</ElButton>
</template>
</ElTableColumn>
<ElTableColumn prop="package_type" label="套餐类型" width="100">
<template #default="scope">
<ElTag
@@ -876,6 +877,93 @@
</ElButton>
</template>
</ElDialog>
<!-- 流量详单对话框 -->
<ElDialog
v-model="dailyRecordsDialogVisible"
title="套餐流量详单"
width="900px"
destroy-on-close
>
<div v-if="dailyRecordsData">
<!-- 套餐信息摘要 -->
<ElDescriptions :column="2" border style="margin-bottom: 20px">
<ElDescriptionsItem label="套餐名称">
{{ dailyRecordsData.package_name }}
</ElDescriptionsItem>
<ElDescriptionsItem label="套餐使用记录ID">
{{ dailyRecordsData.package_usage_id }}
</ElDescriptionsItem>
<ElDescriptionsItem label="总使用流量" :span="2">
{{ formatDataSize(dailyRecordsData.total_usage_mb) }}
</ElDescriptionsItem>
</ElDescriptions>
<!-- 筛选器 -->
<div style="margin-bottom: 16px; display: flex; align-items: center; gap: 12px">
<span style="font-weight: 500">筛选方式</span>
<ElRadioGroup v-model="dateFilterType" @change="handleDateFilterChange">
<ElRadio value="all">全部</ElRadio>
<ElRadio value="day">按日</ElRadio>
<ElRadio value="month">按月</ElRadio>
</ElRadioGroup>
<ElDatePicker
v-if="dateFilterType === 'day'"
v-model="selectedDate"
type="date"
placeholder="选择日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
clearable
@change="filterRecordsByDate"
style="width: 200px"
/>
<ElDatePicker
v-if="dateFilterType === 'month'"
v-model="selectedMonth"
type="month"
placeholder="选择月份"
format="YYYY-MM"
value-format="YYYY-MM"
clearable
@change="filterRecordsByMonth"
style="width: 200px"
/>
</div>
<!-- 每日流量记录表格 -->
<ElTable
:data="filteredRecords"
border
stripe
max-height="400"
v-loading="dailyRecordsLoading"
>
<ElTableColumn prop="date" label="日期" width="120" align="center" />
<ElTableColumn label="当日使用流量" width="150" align="center">
<template #default="{ row }">
{{ formatDataSize(row.daily_usage_mb) }}
</template>
</ElTableColumn>
<ElTableColumn label="累计流量" width="150" align="center">
<template #default="{ row }">
{{ formatDataSize(row.cumulative_usage_mb) }}
</template>
</ElTableColumn>
<ElTableColumn label="使用进度" min-width="200">
<template #default="{ row }">
<ElProgress
:percentage="getUsageProgress(row.cumulative_usage_mb, dailyRecordsData.total_usage_mb)"
:color="getProgressColorByPercentage(getUsageProgress(row.cumulative_usage_mb, dailyRecordsData.total_usage_mb))"
/>
</template>
</ElTableColumn>
</ElTable>
</div>
<div v-else-if="!dailyRecordsLoading" style="text-align: center; padding: 40px 0">
<ElEmpty description="暂无流量详单数据" />
</div>
</ElDialog>
</div>
</template>
@@ -988,12 +1076,14 @@
]
})
const isSearchCardFixed = ref(false)
const searchCardRef = ref<any>(null)
const searchCardPlaceholder = ref<HTMLElement | null>(null)
const cardOriginalTop = ref(0)
const cardLeft = ref(0)
const cardWidth = ref(0)
// 流量详单相关
const dailyRecordsDialogVisible = ref(false)
const dailyRecordsLoading = ref(false)
const dailyRecordsData = ref<any>(null)
const dateFilterType = ref<'all' | 'day' | 'month'>('all')
const selectedDate = ref<string>('')
const selectedMonth = ref<string>('')
const filteredRecords = ref<any[]>([])
// ICCID搜索相关
const searchIccid = ref('')
@@ -1401,57 +1491,6 @@
// 页面初始化 - 检查URL参数自动加载
onMounted(() => {
autoLoadFromQuery()
// 初始化位置信息
nextTick(() => {
updateCardPosition()
})
window.addEventListener('scroll', handleScroll, true)
window.addEventListener('resize', updateCardPosition)
})
onUnmounted(() => {
window.removeEventListener('scroll', handleScroll, true)
window.removeEventListener('resize', updateCardPosition)
})
// 更新卡片位置信息
const updateCardPosition = () => {
if (searchCardRef.value && searchCardRef.value.$el) {
const rect = searchCardRef.value.$el.getBoundingClientRect()
cardOriginalTop.value = rect.top + window.scrollY
cardLeft.value = rect.left + window.scrollX
cardWidth.value = rect.width
}
}
// 处理滚动事件
const handleScroll = () => {
if (!searchCardRef.value) return
const scrollTop = window.scrollY || window.pageYOffset || document.documentElement.scrollTop
// 判断是否应该固定:当滚动超过卡片原始位置时
const shouldBeFixed = scrollTop > cardOriginalTop.value - 20
// 只在状态变化时更新
if (shouldBeFixed !== isSearchCardFixed.value) {
if (shouldBeFixed && !isSearchCardFixed.value) {
// 在变成固定之前,更新位置信息
updateCardPosition()
}
isSearchCardFixed.value = shouldBeFixed
}
}
// 计算固定时的样式
const fixedCardStyle = computed(() => {
if (isSearchCardFixed.value && cardWidth.value > 0) {
return {
left: `${cardLeft.value}px`,
width: `${cardWidth.value}px`
}
}
return {}
})
// 监听路由查询参数变化
@@ -2211,31 +2250,98 @@
manualDeactivateDeviceLoading.value = false
}
}
// 显示流量详单
const handleShowDailyRecords = async (packageRow: any) => {
dailyRecordsDialogVisible.value = true
dailyRecordsLoading.value = true
dailyRecordsData.value = null
dateFilterType.value = 'all'
selectedDate.value = ''
selectedMonth.value = ''
try {
const res = await CardService.getPackageDailyRecords(packageRow.package_usage_id)
if (res.code === 0 && res.data) {
dailyRecordsData.value = res.data
filteredRecords.value = res.data.records || []
}
} catch (error) {
console.error('加载流量详单失败:', error)
ElMessage.error('加载流量详单失败')
} finally {
dailyRecordsLoading.value = false
}
}
// 处理筛选方式变化
const handleDateFilterChange = () => {
selectedDate.value = ''
selectedMonth.value = ''
filteredRecords.value = dailyRecordsData.value?.records || []
}
// 按日期筛选
const filterRecordsByDate = () => {
if (!dailyRecordsData.value?.records) return
if (!selectedDate.value) {
filteredRecords.value = dailyRecordsData.value.records
return
}
filteredRecords.value = dailyRecordsData.value.records.filter((record: any) => {
return record.date === selectedDate.value
})
}
// 按月份筛选
const filterRecordsByMonth = () => {
if (!dailyRecordsData.value?.records) return
if (!selectedMonth.value) {
filteredRecords.value = dailyRecordsData.value.records
return
}
filteredRecords.value = dailyRecordsData.value.records.filter((record: any) => {
return record.date.startsWith(selectedMonth.value)
})
}
// 计算使用进度百分比
const getUsageProgress = (cumulative: number, total: number): number => {
if (!total || total <= 0) return 0
const percentage = (cumulative / total) * 100
return Math.min(Math.round(percentage * 100) / 100, 100)
}
// 根据百分比获取进度条颜色
const getProgressColorByPercentage = (percentage: number): string => {
if (percentage < 50) return '#67c23a'
if (percentage < 80) return '#e6a23c'
return '#f56c6c'
}
</script>
<style lang="scss" scoped>
// 套餐名称链接样式
.package-name-link {
text-decoration: underline;
&:hover {
text-decoration: underline;
}
}
.single-card-page {
padding: 20px;
// 占位符
.search-card-placeholder {
height: 140px; // 大约等于搜索卡片的高度
margin-bottom: 20px;
}
// ICCID搜索卡片
// 资产查询卡片
.search-card {
margin-bottom: 20px;
overflow: visible;
background: var(--el-bg-color, #fff);
transition: box-shadow 0.3s ease;
&.is-fixed {
position: fixed;
top: 20px;
z-index: 100;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
}
:deep(.el-card__header) {
padding: 16px 20px;

View File

@@ -375,11 +375,14 @@
// 列配置
const columnOptions = [
{ label: 'ID', prop: 'id' },
{ label: '配置名称', prop: 'name' },
{ label: '配置描述', prop: 'description' },
{ label: '支付渠道类型', prop: 'provider_type' },
{ label: '激活状态', prop: 'is_active' },
{ label: '商户号', prop: 'merchant_id' },
{ label: '小程序AppID', prop: 'miniapp_app_id' },
{ label: '公众号AppID', prop: 'oa_app_id' },
{ label: '支付回调地址', prop: 'notify_url' },
{ label: '创建时间', prop: 'created_at' },
{ label: '更新时间', prop: 'updated_at' }
]
@@ -479,17 +482,23 @@
return '-'
}
// 获取支付回调地址
const getNotifyUrl = (row: WechatConfig): string => {
if (row.provider_type === 'wechat') {
return row.wx_notify_url || '-'
}
if (row.provider_type === 'fuiou') {
return row.fy_notify_url || '-'
}
return '-'
}
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'id',
label: 'ID',
width: 80
},
{
prop: 'name',
label: '配置名称',
minWidth: 180,
minWidth: 160,
showOverflowTooltip: true,
formatter: (row: WechatConfig) => {
return h(
@@ -505,6 +514,13 @@
)
}
},
{
prop: 'description',
label: '配置描述',
minWidth: 180,
showOverflowTooltip: true,
formatter: (row: WechatConfig) => row.description || '-'
},
{
prop: 'provider_type',
label: '支付渠道类型',
@@ -529,8 +545,30 @@
prop: 'merchant_id',
label: '商户号',
width: 150,
showOverflowTooltip: true,
formatter: (row: WechatConfig) => getMerchantId(row)
},
{
prop: 'miniapp_app_id',
label: '小程序AppID',
width: 160,
showOverflowTooltip: true,
formatter: (row: WechatConfig) => row.miniapp_app_id || '-'
},
{
prop: 'oa_app_id',
label: '公众号AppID',
width: 160,
showOverflowTooltip: true,
formatter: (row: WechatConfig) => row.oa_app_id || '-'
},
{
prop: 'notify_url',
label: '支付回调地址',
minWidth: 200,
showOverflowTooltip: true,
formatter: (row: WechatConfig) => getNotifyUrl(row)
},
{
prop: 'created_at',
label: '创建时间',