fix:修改bug: 虚流量关闭的时候还是显示了
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m46s

This commit is contained in:
sexygoat
2026-05-29 10:46:01 +08:00
parent 9ce48f1714
commit 76a526643e
14 changed files with 714 additions and 42 deletions

View File

@@ -16,6 +16,7 @@
import { setThemeTransitionClass } from '@/utils'
import { checkStorageCompatibility } from '@/utils'
import ChunkErrorBoundary from '@/components/core/others/ChunkErrorBoundary.vue'
import { ElMessageBox } from 'element-plus'
const userStore = useUserStore()
const { language } = storeToRefs(userStore)
@@ -25,6 +26,12 @@
en: en
}
const VERSION_CHECK_INTERVAL = 5 * 60 * 1000
let versionCheckTimer: number | undefined
let currentEntrySignature = ''
let upgradePromptVisible = false
let removeVisibilityChangeListener: (() => void) | undefined
onBeforeMount(() => {
setThemeTransitionClass(true)
})
@@ -36,10 +43,19 @@
setThemeTransitionClass(false)
// 系统升级
systemUpgrade()
// 检查前端版本更新
startVersionUpdateCheck()
// 获取用户信息
getUserInfo()
})
onBeforeUnmount(() => {
if (versionCheckTimer) {
window.clearInterval(versionCheckTimer)
}
removeVisibilityChangeListener?.()
})
// 获取用户信息
const getUserInfo = async () => {
if (userStore.isLogin && userStore.accessToken) {
@@ -55,4 +71,68 @@
}
}
}
const getEntrySignature = (html: string) => {
const matches = html.match(/(?:src|href)="[^"]*\/assets\/[^""]+\.(?:js|css)"/g)
return matches?.sort().join('|') || ''
}
const getLoadedEntrySignature = () => {
return Array.from(document.querySelectorAll<HTMLScriptElement | HTMLLinkElement>('script[src],link[href]'))
.map((element) => ('src' in element ? element.src : element.href))
.filter((url) => /\/assets\/.*\.(js|css)(\?|$)/.test(url))
.sort()
.join('|')
}
const checkVersionUpdate = async () => {
if (upgradePromptVisible || document.hidden) return
try {
const response = await fetch(`${window.location.origin}${import.meta.env.BASE_URL}`, {
cache: 'no-store'
})
const html = await response.text()
const latestSignature = getEntrySignature(html)
if (!latestSignature) return
if (!currentEntrySignature) {
currentEntrySignature = latestSignature
return
}
if (currentEntrySignature !== latestSignature) {
upgradePromptVisible = true
await ElMessageBox.alert('系统已发布新版本,请刷新页面后继续使用。', '发现新版本', {
confirmButtonText: '立即刷新',
type: 'warning',
closeOnClickModal: false,
closeOnPressEscape: false
})
window.location.reload()
}
} catch (error) {
console.warn('检查版本更新失败:', error)
} finally {
upgradePromptVisible = false
}
}
const startVersionUpdateCheck = () => {
if (import.meta.env.DEV) return
currentEntrySignature = getLoadedEntrySignature()
checkVersionUpdate()
versionCheckTimer = window.setInterval(checkVersionUpdate, VERSION_CHECK_INTERVAL)
const handleVisibilityChange = () => {
if (!document.hidden) {
checkVersionUpdate()
}
}
document.addEventListener('visibilitychange', handleVisibilityChange)
removeVisibilityChangeListener = () => {
document.removeEventListener('visibilitychange', handleVisibilityChange)
}
}
</script>

View File

@@ -12,7 +12,7 @@
v-model="searchIccid"
placeholder="请输入虚拟号、ICCID、IMEI、SN或MSISDN"
clearable
style="width: 500px"
class="asset-search-input"
@focus="iccidInputFocused = true"
@blur="iccidInputFocused = false"
@keyup.enter="handleSearch"
@@ -27,7 +27,7 @@
</div>
</Transition>
</div>
<ElButton type="primary" @click="handleSearch" :icon="Search" style="margin-left: 16px">
<ElButton class="search-action-button" type="primary" @click="handleSearch" :icon="Search">
查询
</ElButton>
@@ -141,6 +141,7 @@
.card-header {
display: flex;
gap: 12px;
align-items: center;
justify-content: space-between;
}
@@ -159,6 +160,12 @@
.iccid-input-wrapper {
position: relative;
flex: 1 1 360px;
min-width: 240px;
.asset-search-input {
width: 100%;
}
.iccid-magnifier {
position: absolute;
@@ -182,6 +189,7 @@
// 内容区域
.magnifier-content {
max-width: min(720px, calc(100vw - 48px));
padding: 16px 24px;
font-family: 'Courier New', Consolas, monospace;
font-size: 28px;
@@ -189,6 +197,8 @@
line-height: 1.4;
color: var(--el-text-color-primary);
letter-spacing: 3px;
overflow-wrap: anywhere;
white-space: normal;
background-color: var(--el-bg-color, #fff);
border: 2px solid var(--el-color-primary);
border-radius: 8px;
@@ -239,6 +249,56 @@
:deep(.el-card__body) {
padding: 12px 16px;
}
:deep(.el-card__header) .card-header {
align-items: stretch;
flex-direction: column;
.el-button {
width: 100%;
margin-left: 0;
}
}
.iccid-search {
align-items: stretch;
flex-direction: column;
.iccid-input-wrapper {
flex-basis: auto;
min-width: 0;
width: 100%;
.iccid-magnifier {
right: 0;
left: 0;
white-space: normal;
.magnifier-arrow {
left: 24px;
}
.magnifier-content {
display: block;
width: 100%;
padding: 12px 14px;
font-size: 18px;
letter-spacing: 1px;
}
}
}
.search-action-button,
.operation-button-group,
.operation-button-group .el-button {
width: 100%;
margin-left: 0;
}
.operation-button-group {
margin-left: 0;
}
}
}
}
</style>

View File

@@ -25,7 +25,7 @@
</div>
</div>
</template>
<ElDescriptions :column="4" border>
<ElDescriptions :column="descriptionsColumn" border>
<!-- 卡专属字段 -->
<template v-if="cardInfo?.asset_type === 'card'">
<ElDescriptionsItem label="ICCID">{{ cardInfo?.iccid || '-' }}</ElDescriptionsItem>
@@ -269,7 +269,7 @@
<!-- 设备实时信息 -->
<ElDivider content-position="left">设备实时信息</ElDivider>
<div v-if="hasDeviceRealtimeSection" v-loading="realtimeLoading" style="margin-top: 16px">
<ElDescriptions :column="4" border>
<ElDescriptions :column="descriptionsColumn" border>
<!-- 基本状态 -->
<ElDescriptionsItem label="在线状态">
<ElTag :type="getOnlineStatusType(deviceRealtime?.online_status)" size="small">
@@ -461,6 +461,7 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useWindowSize } from '@vueuse/core'
import {
ElCard,
ElDescriptions,
@@ -585,6 +586,7 @@
const { hasAuth } = useAuth()
const userStore = useUserStore()
const { width } = useWindowSize()
const CARD_MONTH_USAGE_PERMISSION = 'asset_info:view_card_month_usage'
@@ -609,6 +611,12 @@
return ![3, 4].includes(userType) && hasAuth(CARD_MONTH_USAGE_PERMISSION)
})
const descriptionsColumn = computed(() => {
if (width.value <= 640) return 1
if (width.value <= 1024) return 2
return 4
})
// Emits
interface Emits {
(e: 'update:pollingEnabled', value: boolean): void
@@ -786,6 +794,7 @@
.card-header {
display: flex;
gap: 12px;
align-items: center;
justify-content: space-between;
@@ -808,6 +817,56 @@
}
}
}
:deep(.el-descriptions__body) {
overflow-x: auto;
}
:deep(.el-descriptions__cell) {
word-break: break-word;
}
:deep(.el-table) {
min-width: 760px;
}
.card-operations,
.device-operations {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: flex-end;
.el-button {
margin-left: 0;
}
}
@media (width <= 768px) {
.card-header {
align-items: flex-start;
flex-direction: column;
.card-header-left,
.card-header-right {
width: 100%;
}
.card-header-right {
display: flex;
justify-content: flex-start;
}
}
.card-operations,
.device-operations {
justify-content: stretch;
.el-button {
flex: 1 1 120px;
}
}
}
}
}
</style>

View File

@@ -79,7 +79,7 @@
/>
</div>
<!-- 虚流量 -->
<div v-if="canViewCurrentPackageVirtualUsage" class="flow-stat-section">
<div v-if="canShowCurrentPackageVirtualUsage" class="flow-stat-section">
<div class="flow-stat-label">虚流量使用</div>
<div class="flow-stat-row">
<span class="stat-item">
@@ -186,7 +186,7 @@
</div>
<!-- 套餐详情表格 -->
<ElDescriptions :column="4" border class="package-info-table">
<ElDescriptions :column="descriptionsColumn" border class="package-info-table">
<ElDescriptionsItem label="订单号">
{{ currentPackage.order_no || '-' }}
</ElDescriptionsItem>
@@ -232,6 +232,7 @@
ElEmpty,
ElAlert
} from 'element-plus'
import { useWindowSize } from '@vueuse/core'
import { useAssetFormatters } from '../composables/useAssetFormatters'
import { useAuth } from '@/composables/useAuth'
import { useUserStore } from '@/store/modules/user'
@@ -250,21 +251,12 @@
} = useAssetFormatters()
const { hasAuth } = useAuth()
const { width } = useWindowSize()
const userStore = useUserStore()
const { info: userInfo } = storeToRefs(userStore)
const isAdminOrPlatform = computed(
() => userInfo.value.user_type === 1 || userInfo.value.user_type === 2
)
const canViewCurrentPackageRealUsage = computed(() =>
hasAuth('asset_info:view_current_package_real_usage')
)
const canViewCurrentPackageVirtualUsage = computed(() =>
hasAuth('asset_info:view_current_package_virtual_usage')
)
const shouldShowTrafficCard = computed(() => {
if (!isAdminOrPlatform.value) return true
return canViewCurrentPackageRealUsage.value || canViewCurrentPackageVirtualUsage.value
})
interface Props {
currentPackage: AssetPackageUsageRecord | null
@@ -283,6 +275,26 @@
boundDeviceName: ''
})
const canViewCurrentPackageRealUsage = computed(() =>
hasAuth('asset_info:view_current_package_real_usage')
)
const canViewCurrentPackageVirtualUsage = computed(() =>
hasAuth('asset_info:view_current_package_virtual_usage')
)
const canShowCurrentPackageVirtualUsage = computed(
() => canViewCurrentPackageVirtualUsage.value && props.currentPackage?.enable_virtual_data !== false
)
const shouldShowTrafficCard = computed(() => {
if (!isAdminOrPlatform.value) return true
return canViewCurrentPackageRealUsage.value || canShowCurrentPackageVirtualUsage.value
})
const descriptionsColumn = computed(() => {
if (width.value <= 640) return 1
if (width.value <= 1024) return 2
return 4
})
const emit = defineEmits<{
showRecharge: []
navigateToDevice: [deviceNo: string]
@@ -307,10 +319,11 @@
<style scoped lang="scss">
.current-package-card {
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
.card-header {
display: flex;
gap: 12px;
align-items: center;
justify-content: space-between;
.header-title {
font-size: 16px;
@@ -324,9 +337,10 @@
border-radius: 8px;
margin-bottom: 20px;
.flow-stats-container {
display: flex;
gap: 40px;
.flow-stats-container {
display: flex;
gap: 40px;
min-width: 0;
.flow-stat-section {
flex: 1;
@@ -341,6 +355,7 @@
.flow-stat-row {
display: flex;
flex-wrap: wrap;
gap: 16px;
margin-bottom: 8px;
@@ -366,6 +381,7 @@
.flow-stats-row {
display: flex;
flex-wrap: wrap;
gap: 32px;
margin-bottom: 16px;
@@ -433,5 +449,54 @@
.package-info-table {
margin-top: 0;
}
:deep(.el-descriptions__body) {
overflow-x: auto;
}
:deep(.el-descriptions__cell) {
word-break: break-word;
}
@media (width <= 992px) {
.flow-progress-card {
.flow-stats-container {
flex-direction: column;
gap: 20px;
}
}
}
@media (width <= 768px) {
.card-header {
align-items: flex-start;
flex-direction: column;
.header-actions {
width: 100%;
}
}
.flow-progress-card {
padding: 14px;
.flow-stats-row,
.flow-stat-row {
flex-direction: column;
gap: 8px;
}
.progress-wrapper {
align-items: stretch;
flex-direction: column;
gap: 6px;
.progress-percentage {
min-width: 0;
text-align: left;
}
}
}
}
}
</style>

View File

@@ -8,7 +8,7 @@
v-model="filterForm.operation_type"
placeholder="操作类型"
clearable
style="width: 180px"
class="operation-type-select"
@change="handleFilterChange"
>
<ElOption
@@ -22,7 +22,7 @@
v-model="filterForm.result_status"
placeholder="执行结果"
clearable
style="width: 120px"
class="result-status-select"
@change="handleFilterChange"
>
<ElOption label="成功" value="success" />
@@ -99,6 +99,7 @@
</ElTableColumn>
</ElTable>
<ElPagination
class="logs-pagination"
v-if="total > 0"
v-model:current-page="pagination.page"
v-model:page-size="pagination.page_size"
@@ -107,7 +108,6 @@
layout="total, prev, pager, next"
@current-change="handlePageChange"
@size-change="handleSizeChange"
style="margin-top: 16px; justify-content: flex-end"
/>
</div>
</ElCard>
@@ -619,11 +619,26 @@
align-items: center;
gap: 8px;
flex-wrap: wrap;
justify-content: flex-end;
min-width: 0;
.operation-type-select {
width: 180px;
}
.result-status-select {
width: 120px;
}
}
}
.logs-table-wrapper {
overflow-x: auto;
.logs-table {
width: max-content;
min-width: 980px;
:deep(.el-table__header-wrapper) {
th {
background-color: var(--el-fill-color-light);
@@ -668,6 +683,56 @@
}
}
}
.logs-pagination {
justify-content: flex-end;
margin-top: 16px;
}
}
@media (width <= 1200px) {
.card-header {
align-items: stretch;
flex-direction: column;
.filter-section {
justify-content: flex-start;
width: 100%;
}
}
}
@media (width <= 768px) {
.card-header {
align-items: stretch;
flex-direction: column;
.filter-section {
align-items: stretch;
flex-direction: column;
:deep(.el-select),
.el-button,
.el-tag {
width: 100% !important;
}
.el-button {
margin-left: 0;
}
}
}
.logs-table-wrapper {
margin-right: -12px;
margin-left: -12px;
padding: 0 12px;
:deep(.el-pagination) {
justify-content: flex-start !important;
overflow-x: auto;
}
}
}
}
</style>

View File

@@ -63,7 +63,7 @@
<ElTableColumn label="流量" min-width="450">
<template #default="scope">
<div class="traffic-progress">
<template v-if="isAdminOrPlatform && shouldShowAdminTrafficBreakdown">
<template v-if="isAdminOrPlatform && shouldShowAdminTrafficBreakdown(scope.row)">
<div v-if="canViewCurrentPackageRealUsage" class="progress-text">
<span class="used"
>已使用真流量: {{ formatDataSize(scope.row.real_used_mb || 0) }}</span
@@ -93,7 +93,7 @@
"
/>
<div
v-if="canViewCurrentPackageVirtualUsage"
v-if="canShowPackageVirtualUsage(scope.row)"
class="progress-text"
:style="{ marginTop: canViewCurrentPackageRealUsage ? '8px' : '0' }"
>
@@ -104,7 +104,7 @@
>停机阈值: {{ formatDataSize(scope.row.virtual_total_mb || 0) }}</span
>
</div>
<div v-if="canViewCurrentPackageVirtualUsage" class="progress-wrapper">
<div v-if="canShowPackageVirtualUsage(scope.row)" class="progress-wrapper">
<div class="progress-track">
<ElProgress
style="margin-top: 4px"
@@ -285,11 +285,6 @@
const isAdminOrPlatform = computed(
() => userInfo.value.user_type === 1 || userInfo.value.user_type === 2
)
const shouldShowAdminTrafficBreakdown = computed(() => {
if (!isAdminOrPlatform.value) return true
return canViewCurrentPackageRealUsage.value || canViewCurrentPackageVirtualUsage.value
})
// 退款申请对话框
const createRefundDialogVisible = ref(false)
const currentRefundOrderNo = ref<string | undefined>(undefined)
@@ -310,6 +305,14 @@
// Props 使用 API 类型
type PackageInfo = AssetPackageUsageRecord
const canShowPackageVirtualUsage = (pkg: PackageInfo) =>
canViewCurrentPackageVirtualUsage.value && pkg.enable_virtual_data !== false
const shouldShowAdminTrafficBreakdown = (pkg: PackageInfo) => {
if (!isAdminOrPlatform.value) return true
return canViewCurrentPackageRealUsage.value || canShowPackageVirtualUsage(pkg)
}
interface Props {
packageList: PackageInfo[]
total?: number
@@ -459,6 +462,7 @@
.traffic-progress {
.progress-text {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
justify-content: space-between;
@@ -517,6 +521,49 @@
margin-top: 16px;
justify-content: flex-end;
}
@media (width <= 768px) {
.card-header {
align-items: stretch;
flex-direction: column;
gap: 12px;
.card-header-right,
.card-header-right .el-button {
width: 100%;
}
}
.package-table-wrapper {
.package-table {
min-width: 900px;
}
.traffic-progress {
.progress-text {
align-items: flex-start;
flex-direction: column;
gap: 4px;
}
.progress-wrapper {
align-items: stretch;
flex-direction: column;
gap: 6px;
.progress-percentage {
min-width: 0;
text-align: left;
}
}
}
.pagination {
justify-content: flex-start;
overflow-x: auto;
}
}
}
}
}
</style>

View File

@@ -8,15 +8,15 @@
{{ walletInfo.status_text || '-' }}
</ElTag>
<template v-if="walletInfo">
<ElDivider direction="vertical" />
<ElDivider class="wallet-divider" direction="vertical" />
<span class="balance-item"
>💰 总余额 <strong>{{ formatAmount(walletInfo.balance) }}</strong></span
>
<ElDivider direction="vertical" />
<ElDivider class="wallet-divider" direction="vertical" />
<span class="balance-item"
> 可用 <strong>{{ formatAmount(walletInfo.available_balance) }}</strong></span
>
<ElDivider direction="vertical" />
<ElDivider class="wallet-divider" direction="vertical" />
<span class="balance-item"
>🔒 冻结 <strong>{{ formatAmount(walletInfo.frozen_balance) }}</strong></span
>
@@ -27,7 +27,7 @@
v-model="filterForm.transaction_type"
placeholder="交易类型"
clearable
style="width: 150px"
class="transaction-type-select"
@change="handleFilterChange"
>
<ElOption label="充值" value="recharge" />
@@ -40,7 +40,7 @@
range-separator=""
start-placeholder="开始时间"
end-placeholder="结束时间"
style="width: 360px"
class="date-range-picker"
/>
<ElButton type="primary" @click="handleQuery">查询</ElButton>
<ElButton @click="handleReset()">重置</ElButton>
@@ -356,6 +356,10 @@
font-weight: 500;
}
.wallet-divider {
flex-shrink: 0;
}
.balance-item {
font-size: 13px;
color: var(--el-text-color-regular);
@@ -372,10 +376,22 @@
flex-wrap: wrap;
gap: 10px;
align-items: center;
justify-content: flex-end;
min-width: 0;
.transaction-type-select {
width: 150px;
}
.date-range-picker {
width: min(360px, 100%);
}
}
}
.wallet-table-wrapper {
overflow-x: auto;
.wallet-empty-state {
display: flex;
align-items: center;
@@ -384,6 +400,9 @@
}
.wallet-table {
width: max-content;
min-width: 900px;
:deep(.el-table__header) {
th {
font-weight: 600;
@@ -396,6 +415,84 @@
display: flex;
justify-content: flex-end;
margin-top: 16px;
overflow-x: auto;
}
}
@media (width <= 1200px) {
.wallet-header {
align-items: flex-start;
flex-direction: column;
.header-left,
.filter-section {
width: 100%;
}
.filter-section {
justify-content: flex-start;
}
}
}
@media (width <= 768px) {
.wallet-header {
align-items: stretch;
flex-direction: column;
.header-left,
.filter-section {
width: 100%;
}
.filter-section {
align-items: stretch;
flex-direction: column;
:deep(.el-select),
:deep(.el-date-editor),
.el-button,
.el-tag {
width: 100% !important;
}
.el-button {
margin-left: 0;
}
}
.header-left {
align-items: flex-start;
flex-direction: column;
gap: 6px;
.wallet-divider {
display: none;
}
.balance-item {
width: 100%;
}
}
}
.wallet-table-wrapper {
.wallet-table {
min-width: 900px;
}
.pagination-wrapper {
justify-content: flex-start;
overflow-x: auto;
}
}
}
@media (width <= 480px) {
.wallet-table-wrapper {
margin-right: -12px;
margin-left: -12px;
padding: 0 12px;
}
}
}

View File

@@ -691,6 +691,8 @@
.single-card-page {
padding: 20px;
max-width: 100%;
overflow-x: hidden;
// 资产查询卡片
.search-card {
@@ -828,11 +830,14 @@
display: flex;
flex-direction: column;
gap: 24px;
min-width: 0;
// 行布局
.row {
display: flex;
gap: 24px;
min-width: 0;
width: 100%;
&.full-width {
.info-card {
@@ -900,13 +905,15 @@
// 信息卡片通用样式
.info-card {
width: 100%;
min-width: 0;
:deep(.el-card__header) {
padding: 20px 24px 16px;
.card-header {
display: flex;
gap: 8px;
.card-header {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
justify-content: space-between;
font-size: 16px;
@@ -1296,6 +1303,8 @@
}
.wallet-table-wrapper {
overflow-x: auto;
.wallet-table {
:deep(.el-table__header) {
th {
@@ -1363,6 +1372,15 @@
}
}
.empty-state-card,
.empty-state {
min-width: 0;
}
:deep(.el-pagination) {
flex-wrap: nowrap;
}
.traffic-info .traffic-overview {
.progress-stats {
flex-direction: column;
@@ -1381,6 +1399,22 @@
}
@media (width <= 480px) {
padding: 10px;
.main-content-layout {
gap: 12px;
}
.info-card {
:deep(.el-card__body) {
padding: 12px;
}
:deep(.el-card__header) {
padding: 12px;
}
}
.traffic-info .traffic-overview {
.progress-percentage {
font-size: 20px;