feat: 完善接口19-顶部通知铃铛与站内通知中心
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m5s
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m5s
This commit is contained in:
@@ -88,7 +88,7 @@
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="预计套餐到期时间">
|
||||
<span
|
||||
:class="{ 'expiry-estimate--expiring': cardInfo?.is_expiring }"
|
||||
:class="getExpiryEstimateClass(cardInfo)"
|
||||
:title="getExpiryEstimateTooltip(cardInfo)"
|
||||
>
|
||||
{{ getExpiryEstimateText(cardInfo) }}
|
||||
@@ -182,7 +182,7 @@
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="预计套餐到期时间">
|
||||
<span
|
||||
:class="{ 'expiry-estimate--expiring': cardInfo?.is_expiring }"
|
||||
:class="getExpiryEstimateClass(cardInfo)"
|
||||
:title="getExpiryEstimateTooltip(cardInfo)"
|
||||
>
|
||||
{{ getExpiryEstimateText(cardInfo) }}
|
||||
@@ -598,7 +598,11 @@
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { getExpiryEstimateText, getExpiryEstimateTooltip } from '@/utils/business/expiryEstimate'
|
||||
import {
|
||||
getExpiryEstimateText,
|
||||
getExpiryEstimateTooltip,
|
||||
getExpiryEstimateClass
|
||||
} from '@/utils/business/expiryEstimate'
|
||||
|
||||
// Props
|
||||
interface BindingCard {
|
||||
@@ -987,11 +991,24 @@
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.expiry-estimate--expiring {
|
||||
.expiry-estimate--critical,
|
||||
.expiry-estimate--warning,
|
||||
.expiry-estimate--notice {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.expiry-estimate--critical {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
|
||||
.expiry-estimate--warning {
|
||||
color: #8e44ad;
|
||||
}
|
||||
|
||||
.expiry-estimate--notice {
|
||||
color: #e83e8c;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
|
||||
@@ -995,7 +995,11 @@
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { getExpiryEstimateText, getExpiryEstimateTooltip } from '@/utils/business/expiryEstimate'
|
||||
import {
|
||||
getExpiryEstimateText,
|
||||
getExpiryEstimateTooltip,
|
||||
getExpiryEstimateClass
|
||||
} from '@/utils/business/expiryEstimate'
|
||||
import type { PackageSeriesResponse } from '@/types/api'
|
||||
import type { EnterpriseItem } from '@/types/api/enterprise'
|
||||
import type {
|
||||
@@ -1900,7 +1904,7 @@
|
||||
h(
|
||||
'span',
|
||||
{
|
||||
class: { 'expiry-estimate--expiring': row.is_expiring },
|
||||
class: getExpiryEstimateClass(row),
|
||||
title: getExpiryEstimateTooltip(row)
|
||||
},
|
||||
getExpiryEstimateText(row)
|
||||
@@ -3218,8 +3222,21 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.expiry-estimate--expiring {
|
||||
.expiry-estimate--critical,
|
||||
.expiry-estimate--warning,
|
||||
.expiry-estimate--notice {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.expiry-estimate--critical {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
|
||||
.expiry-estimate--warning {
|
||||
color: #8e44ad;
|
||||
}
|
||||
|
||||
.expiry-estimate--notice {
|
||||
color: #e83e8c;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1045,7 +1045,11 @@
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { getExpiryEstimateText, getExpiryEstimateTooltip } from '@/utils/business/expiryEstimate'
|
||||
import {
|
||||
getExpiryEstimateText,
|
||||
getExpiryEstimateTooltip,
|
||||
getExpiryEstimateClass
|
||||
} from '@/utils/business/expiryEstimate'
|
||||
import { formatRemainingTime, FrontendRateLimitError } from '@/utils/business/apiRateLimit'
|
||||
import type {
|
||||
StandaloneIotCard,
|
||||
@@ -2127,7 +2131,7 @@
|
||||
h(
|
||||
'span',
|
||||
{
|
||||
class: { 'expiry-estimate--expiring': row.is_expiring },
|
||||
class: getExpiryEstimateClass(row),
|
||||
title: getExpiryEstimateTooltip(row)
|
||||
},
|
||||
getExpiryEstimateText(row)
|
||||
@@ -3044,8 +3048,21 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.expiry-estimate--expiring {
|
||||
.expiry-estimate--critical,
|
||||
.expiry-estimate--warning,
|
||||
.expiry-estimate--notice {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.expiry-estimate--critical {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
|
||||
.expiry-estimate--warning {
|
||||
color: #8e44ad;
|
||||
}
|
||||
|
||||
.expiry-estimate--notice {
|
||||
color: #e83e8c;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="console">
|
||||
开发中敬请期待...
|
||||
<ExpiringAssetStats />
|
||||
<!--<CardList></CardList>-->
|
||||
|
||||
<!--<el-row :gutter="20">-->
|
||||
@@ -29,13 +29,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import CardList from './widget/CardList.vue'
|
||||
import ActiveUser from './widget/ActiveUser.vue'
|
||||
import SalesOverview from './widget/SalesOverview.vue'
|
||||
import NewUser from './widget/NewUser.vue'
|
||||
import Dynamic from './widget/Dynamic.vue'
|
||||
import TodoList from './widget/TodoList.vue'
|
||||
import AboutProject from './widget/AboutProject.vue'
|
||||
import ExpiringAssetStats from './widget/ExpiringAssetStats.vue'
|
||||
import { useCommon } from '@/composables/useCommon'
|
||||
|
||||
defineOptions({ name: 'Console' })
|
||||
|
||||
102
src/views/dashboard/console/widget/ExpiringAssetStats.vue
Normal file
102
src/views/dashboard/console/widget/ExpiringAssetStats.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<ElCard shadow="never" class="expiring-asset-stats">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>临期资产</span>
|
||||
<ElButton link type="primary" @click="() => goToList()">查看列表</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item stat-item--card" @click="() => goToList('card')">
|
||||
<div class="stat-label">临期网卡</div>
|
||||
<div class="stat-value">{{ counts.card.toLocaleString() }}</div>
|
||||
</div>
|
||||
<div class="stat-item stat-item--device" @click="() => goToList('device')">
|
||||
<div class="stat-label">临期设备</div>
|
||||
<div class="stat-value">{{ counts.device.toLocaleString() }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</ElCard>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { AssetService } from '@/api/modules'
|
||||
import type { AssetType } from '@/types/api'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
|
||||
defineOptions({ name: 'ExpiringAssetStats' })
|
||||
|
||||
const router = useRouter()
|
||||
const counts = reactive<Record<AssetType, number>>({ card: 0, device: 0 })
|
||||
|
||||
const loadCount = async (assetType: AssetType) => {
|
||||
try {
|
||||
const response = await AssetService.getExpiringAssets({
|
||||
asset_type: assetType,
|
||||
page: 1,
|
||||
size: 1
|
||||
})
|
||||
if (response.code === 0 && response.data) counts[assetType] = response.data.total || 0
|
||||
} catch {
|
||||
counts[assetType] = 0
|
||||
}
|
||||
}
|
||||
|
||||
const goToList = (assetType?: AssetType) => {
|
||||
router.push({
|
||||
path: RoutesAlias.ExpiringAssets,
|
||||
query: assetType ? { asset_type: assetType } : {}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => void Promise.all([loadCount('card'), loadCount('device')]))
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.expiring-asset-stats {
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
padding: 20px;
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
.stat-item--card {
|
||||
background: #fff0f6;
|
||||
}
|
||||
|
||||
.stat-item--device {
|
||||
background: #f4ecff;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
margin-top: 8px;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -20,10 +20,10 @@
|
||||
|
||||
<div class="filters">
|
||||
<ElSelect v-model="filters.category" clearable placeholder="通知分类" @change="reload">
|
||||
<ElOption label="余额预警" value="balance" />
|
||||
<ElOption label="临期提醒" value="expiry" />
|
||||
<ElOption label="审批" value="approval" />
|
||||
<ElOption label="同步/系统" value="system" />
|
||||
<ElOption label="同步" value="sync" />
|
||||
<ElOption label="系统" value="system" />
|
||||
</ElSelect>
|
||||
<ElSelect v-model="filters.type" clearable placeholder="通知类型" @change="reload">
|
||||
<ElOption v-for="type in typeOptions" :key="type" :label="type" :value="type" />
|
||||
@@ -32,11 +32,11 @@
|
||||
<ElOption label="提示" value="info" />
|
||||
<ElOption label="警告" value="warning" />
|
||||
<ElOption label="严重" value="error" />
|
||||
<ElOption label="成功" value="success" />
|
||||
<ElOption label="严重" value="critical" />
|
||||
</ElSelect>
|
||||
<ElSelect v-model="filters.read_status" clearable placeholder="已读状态" @change="reload">
|
||||
<ElOption label="未读" value="unread" />
|
||||
<ElOption label="已读" value="read" />
|
||||
<ElSelect v-model="filters.is_read" clearable placeholder="已读状态" @change="reload">
|
||||
<ElOption label="未读" :value="false" />
|
||||
<ElOption label="已读" :value="true" />
|
||||
</ElSelect>
|
||||
<ElButton @click="loadNotifications">刷新</ElButton>
|
||||
</div>
|
||||
@@ -59,7 +59,8 @@
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="content" label="通知内容" min-width="300" show-overflow-tooltip />
|
||||
<ElTableColumn prop="body" label="通知内容" min-width="300" show-overflow-tooltip />
|
||||
<ElTableColumn prop="read_at" label="已读时间" width="180" />
|
||||
<ElTableColumn prop="created_at" label="时间" width="180" />
|
||||
<ElTableColumn label="操作" width="100" fixed="right">
|
||||
<template #default="scope">
|
||||
@@ -118,11 +119,10 @@
|
||||
category: undefined,
|
||||
type: undefined,
|
||||
severity: undefined,
|
||||
read_status: undefined
|
||||
is_read: undefined
|
||||
})
|
||||
|
||||
const isRead = (item: NotificationItem) =>
|
||||
item.read === true || item.is_read === true || item.read_status === 'read'
|
||||
const isRead = (item: NotificationItem) => item.is_read
|
||||
|
||||
const getCategoryLabel = (category?: string | null) =>
|
||||
({
|
||||
@@ -137,14 +137,14 @@
|
||||
'-'
|
||||
|
||||
const getSeverityLabel = (severity?: string | null) =>
|
||||
({ info: '提示', warning: '警告', error: '严重', success: '成功' })[severity || ''] ||
|
||||
({ info: '提示', warning: '警告', error: '严重', critical: '严重' })[severity || ''] ||
|
||||
severity ||
|
||||
'-'
|
||||
|
||||
const getSeverityType = (severity?: string | null) => {
|
||||
if (severity === 'error') return 'danger'
|
||||
if (severity === 'warning') return 'warning'
|
||||
if (severity === 'success') return 'success'
|
||||
if (severity === 'critical') return 'danger'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
@@ -204,12 +204,12 @@
|
||||
}
|
||||
const targetResponse = await NotificationService.getTarget(item.id)
|
||||
if (targetResponse.code !== 0 || !targetResponse.data) {
|
||||
ElMessage.info(item.content || '该通知暂无可跳转目标')
|
||||
ElMessage.info(item.body || '该通知暂无可跳转目标')
|
||||
await loadNotifications()
|
||||
return
|
||||
}
|
||||
if (!navigateNotificationTarget(router, targetResponse.data)) {
|
||||
ElMessage.info(targetResponse.data.message || item.content || '该通知暂无可跳转目标')
|
||||
ElMessage.info(item.body || '该通知暂无可跳转目标')
|
||||
}
|
||||
await loadNotifications()
|
||||
} catch (error: any) {
|
||||
|
||||
364
src/views/operations/expiring-assets/index.vue
Normal file
364
src/views/operations/expiring-assets/index.vue
Normal file
@@ -0,0 +1,364 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="expiring-assets-page" id="table-full-screen">
|
||||
<ArtSearchBar
|
||||
v-model:filter="searchForm"
|
||||
:items="searchFormItems"
|
||||
:show-expand="true"
|
||||
label-width="85"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<div class="page-intro"> 仅展示可预计且尚未过期的资产,0-3 天资产优先显示。 </div>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="asset_id"
|
||||
:loading="loading"
|
||||
:data="items"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:actions="getActions"
|
||||
:actionsWidth="140"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h, onMounted, reactive, ref, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElTag } from 'element-plus'
|
||||
import { AssetService, PackageManageService, ShopService } from '@/api/modules'
|
||||
import type {
|
||||
AssetType,
|
||||
ExpiringAssetItem,
|
||||
ExpiringAssetQueryParams,
|
||||
PackageResponse,
|
||||
ShopResponse
|
||||
} from '@/types/api'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
|
||||
defineOptions({ name: 'ExpiringAssets' })
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const tableRef = ref()
|
||||
const loading = ref(false)
|
||||
const shopLoading = ref(false)
|
||||
const packageLoading = ref(false)
|
||||
const shopOptions = ref<ShopResponse[]>([])
|
||||
const packageOptions = ref<PackageResponse[]>([])
|
||||
const items = ref<ExpiringAssetItem[]>([])
|
||||
const searchForm = reactive<ExpiringAssetQueryParams>({
|
||||
asset_type: undefined,
|
||||
keyword: '',
|
||||
shop_id: undefined,
|
||||
package_id: undefined,
|
||||
days_min: undefined,
|
||||
days_max: undefined,
|
||||
expires_from: undefined,
|
||||
expires_to: undefined
|
||||
})
|
||||
const pagination = reactive({ currentPage: 1, pageSize: 20, total: 0 })
|
||||
|
||||
const searchFormItems = computed<SearchFormItem[]>(() => [
|
||||
{
|
||||
label: '资产类型',
|
||||
prop: 'asset_type',
|
||||
type: 'select',
|
||||
config: { clearable: true, placeholder: '全部' },
|
||||
options: [
|
||||
{ label: '网卡', value: 'card' },
|
||||
{ label: '设备', value: 'device' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '关键词',
|
||||
prop: 'keyword',
|
||||
type: 'input',
|
||||
config: { clearable: true, placeholder: '请输入资产标识' }
|
||||
},
|
||||
{
|
||||
label: '店铺',
|
||||
prop: 'shop_id',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
filterable: true,
|
||||
remote: true,
|
||||
loading: shopLoading.value,
|
||||
remoteMethod: (query: string) => searchShops(query),
|
||||
placeholder: '请选择或搜索店铺'
|
||||
},
|
||||
options: () => shopOptions.value.map((shop) => ({ label: shop.shop_name, value: shop.id }))
|
||||
},
|
||||
{
|
||||
label: '套餐',
|
||||
prop: 'package_id',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
filterable: true,
|
||||
remote: true,
|
||||
loading: packageLoading.value,
|
||||
remoteMethod: (query: string) => searchPackages(query),
|
||||
placeholder: '请选择或搜索套餐'
|
||||
},
|
||||
options: () =>
|
||||
packageOptions.value.map((pkg) => ({
|
||||
label: `${pkg.package_name} (${pkg.package_code})`,
|
||||
value: pkg.id
|
||||
}))
|
||||
},
|
||||
{
|
||||
label: '最小剩余天数',
|
||||
prop: 'days_min',
|
||||
type: 'input',
|
||||
config: { clearable: true, inputmode: 'numeric', placeholder: '最小天数' }
|
||||
},
|
||||
{
|
||||
label: '最大剩余天数',
|
||||
prop: 'days_max',
|
||||
type: 'input',
|
||||
config: { clearable: true, inputmode: 'numeric', placeholder: '最大天数' }
|
||||
},
|
||||
{
|
||||
label: '预计到期起始',
|
||||
prop: 'expires_from',
|
||||
type: 'date',
|
||||
config: { valueFormat: 'YYYY-MM-DD', clearable: true }
|
||||
},
|
||||
{
|
||||
label: '预计到期结束',
|
||||
prop: 'expires_to',
|
||||
type: 'date',
|
||||
config: { valueFormat: 'YYYY-MM-DD', clearable: true }
|
||||
}
|
||||
])
|
||||
|
||||
const columnOptions = [
|
||||
{ label: '资产类型', prop: 'asset_type' },
|
||||
{ label: '资产标识', prop: 'identifier' },
|
||||
{ label: '店铺', prop: 'shop_name' },
|
||||
{ label: '当前套餐', prop: 'package_name' },
|
||||
{ label: '预计最终到期', prop: 'estimated_final_expires_at' },
|
||||
{ label: '剩余天数', prop: 'days_until_final_expiry' },
|
||||
{ label: '临期等级', prop: 'expiry_level_name' }
|
||||
]
|
||||
|
||||
const getExpiryClass = (days: number) => {
|
||||
if (days <= 3) return 'expiry-critical'
|
||||
if (days <= 7) return 'expiry-warning'
|
||||
if (days <= 15) return 'expiry-notice'
|
||||
return ''
|
||||
}
|
||||
|
||||
const getExpiryTagType = (days: number) => {
|
||||
if (days <= 3) return 'danger'
|
||||
if (days <= 7) return 'warning'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
const searchShops = async (query = '') => {
|
||||
shopLoading.value = true
|
||||
try {
|
||||
const response = await ShopService.getShops({
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
shop_name: query || undefined
|
||||
})
|
||||
if (response.code === 0) shopOptions.value = response.data.items || []
|
||||
} finally {
|
||||
shopLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const searchPackages = async (query = '') => {
|
||||
packageLoading.value = true
|
||||
try {
|
||||
const response = await PackageManageService.getPackages({
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
package_name: query || undefined
|
||||
})
|
||||
if (response.code === 0) packageOptions.value = response.data.items || []
|
||||
} finally {
|
||||
packageLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{
|
||||
prop: 'asset_type',
|
||||
label: '资产类型',
|
||||
width: 100,
|
||||
formatter: (row: ExpiringAssetItem) => (row.asset_type === 'card' ? '网卡' : '设备')
|
||||
},
|
||||
{
|
||||
prop: 'identifier',
|
||||
label: '资产标识',
|
||||
minWidth: 180,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: ExpiringAssetItem) => row.identifier || '-'
|
||||
},
|
||||
{
|
||||
prop: 'shop_name',
|
||||
label: '店铺',
|
||||
minWidth: 140,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: ExpiringAssetItem) => row.shop_name || '-'
|
||||
},
|
||||
{
|
||||
prop: 'package_name',
|
||||
label: '当前套餐',
|
||||
minWidth: 160,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: ExpiringAssetItem) => row.package_name || '-'
|
||||
},
|
||||
{
|
||||
prop: 'estimated_final_expires_at',
|
||||
label: '预计最终到期',
|
||||
width: 180,
|
||||
formatter: (row: ExpiringAssetItem) =>
|
||||
h(
|
||||
'span',
|
||||
{ class: getExpiryClass(row.days_until_final_expiry) },
|
||||
formatDateTime(row.estimated_final_expires_at)
|
||||
)
|
||||
},
|
||||
{
|
||||
prop: 'days_until_final_expiry',
|
||||
label: '剩余天数',
|
||||
width: 100,
|
||||
formatter: (row: ExpiringAssetItem) =>
|
||||
h(
|
||||
ElTag,
|
||||
{ type: getExpiryTagType(row.days_until_final_expiry), size: 'small' },
|
||||
() => `${row.days_until_final_expiry}天`
|
||||
)
|
||||
},
|
||||
{
|
||||
prop: 'expiry_level_name',
|
||||
label: '临期等级',
|
||||
width: 120,
|
||||
formatter: (row: ExpiringAssetItem) => row.expiry_level_name || '-'
|
||||
}
|
||||
])
|
||||
|
||||
const loadAssets = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await AssetService.getExpiringAssets({
|
||||
...searchForm,
|
||||
page: pagination.currentPage,
|
||||
size: pagination.pageSize
|
||||
})
|
||||
if (response.code === 0 && response.data) {
|
||||
items.value = response.data.items || []
|
||||
pagination.total = response.data.total || 0
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.currentPage = 1
|
||||
void loadAssets()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
Object.assign(searchForm, {
|
||||
asset_type: undefined,
|
||||
keyword: '',
|
||||
shop_id: undefined,
|
||||
package_id: undefined,
|
||||
days_min: undefined,
|
||||
days_max: undefined,
|
||||
expires_from: undefined,
|
||||
expires_to: undefined
|
||||
})
|
||||
pagination.currentPage = 1
|
||||
void loadAssets()
|
||||
}
|
||||
|
||||
const handleRefresh = () => void loadAssets()
|
||||
const handleSizeChange = (value: number) => {
|
||||
pagination.pageSize = value
|
||||
void loadAssets()
|
||||
}
|
||||
const handleCurrentChange = (value: number) => {
|
||||
pagination.currentPage = value
|
||||
void loadAssets()
|
||||
}
|
||||
|
||||
const goToAsset = (row: ExpiringAssetItem) => {
|
||||
router.push({
|
||||
path: RoutesAlias.AssetInformation,
|
||||
query: row.asset_type === 'card' ? { iccid: row.identifier } : { virtual_no: row.identifier }
|
||||
})
|
||||
}
|
||||
|
||||
const getActions = (row: ExpiringAssetItem) => [
|
||||
{ label: '查看资产', type: 'primary' as const, handler: () => goToAsset(row) },
|
||||
...(row.can_renew
|
||||
? [{ label: '续费', type: 'primary' as const, handler: () => goToAsset(row) }]
|
||||
: [])
|
||||
]
|
||||
|
||||
onMounted(() => {
|
||||
const assetType = route.query.asset_type
|
||||
if (assetType === 'card' || assetType === 'device')
|
||||
searchForm.asset_type = assetType as AssetType
|
||||
void Promise.all([loadAssets(), searchShops(), searchPackages()])
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.expiring-assets-page {
|
||||
.page-intro {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.expiry-critical,
|
||||
.expiry-warning,
|
||||
.expiry-notice {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.expiry-critical {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.expiry-warning {
|
||||
color: #8e44ad;
|
||||
}
|
||||
|
||||
.expiry-notice {
|
||||
color: #e83e8c;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -141,14 +141,14 @@
|
||||
>
|
||||
<ElOption
|
||||
v-for="salesperson in salespersonOptions"
|
||||
:key="salesperson.account_id"
|
||||
:label="salesperson.account_name"
|
||||
:value="salesperson.account_id"
|
||||
:key="salesperson.id"
|
||||
:label="salesperson.username"
|
||||
:value="salesperson.id"
|
||||
>
|
||||
<div style="display: flex; justify-content: space-between">
|
||||
<span>{{ salesperson.account_name }}</span>
|
||||
<span>{{ salesperson.username }}</span>
|
||||
<span style="font-size: 12px; color: var(--el-text-color-secondary)">
|
||||
{{ salesperson.phone_masked }}
|
||||
{{ salesperson.phone_summary }}
|
||||
</span>
|
||||
</div>
|
||||
</ElOption>
|
||||
@@ -328,10 +328,14 @@
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { generateShopCode } from '@/utils/codeGenerator'
|
||||
import { ShopService, RoleService, AccountService } from '@/api/modules'
|
||||
import { ShopService, RoleService } from '@/api/modules'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import type { PlatformSalesperson, ShopResponse, ShopRoleResponse } from '@/types/api'
|
||||
import { AccountStatus, RoleType, RoleStatus } from '@/types/api'
|
||||
import type {
|
||||
ShopBusinessOwnerCandidate,
|
||||
ShopResponse,
|
||||
ShopRoleResponse
|
||||
} from '@/types/api'
|
||||
import { RoleType, RoleStatus } from '@/types/api'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { CommonStatus, getStatusText, STATUS_SELECT_OPTIONS } from '@/config/constants'
|
||||
import { regionData } from '@/utils/constants/regionData'
|
||||
@@ -349,7 +353,7 @@
|
||||
const defaultRoleLoading = ref(false)
|
||||
const defaultRoleList = ref<any[]>([])
|
||||
const salespersonLoading = ref(false)
|
||||
const salespersonOptions = ref<PlatformSalesperson[]>([])
|
||||
const salespersonOptions = ref<ShopBusinessOwnerCandidate[]>([])
|
||||
const detailDialogVisible = ref(false)
|
||||
const detailShop = ref<ShopResponse | null>(null)
|
||||
|
||||
@@ -418,13 +422,11 @@
|
||||
const searchPlatformSalespeople = async (query: string) => {
|
||||
salespersonLoading.value = true
|
||||
try {
|
||||
const res = await AccountService.getPlatformSalespeople({
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
account_type: 'platform',
|
||||
status: AccountStatus.ENABLED,
|
||||
account_name: query || undefined
|
||||
})
|
||||
const res = await ShopService.getBusinessOwnerCandidates({
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
keyword: query || undefined
|
||||
})
|
||||
if (res.code === 0) {
|
||||
salespersonOptions.value = res.data.items || []
|
||||
}
|
||||
@@ -436,15 +438,25 @@
|
||||
}
|
||||
}
|
||||
|
||||
const formatBusinessOwner = (shop: ShopResponse) => {
|
||||
if (!shop.business_owner_name) return '-'
|
||||
return shop.business_owner_phone_masked
|
||||
? `${shop.business_owner_name} (${shop.business_owner_phone_masked})`
|
||||
: shop.business_owner_name
|
||||
}
|
||||
const formatBusinessOwner = (shop: ShopResponse) => {
|
||||
if (!shop.business_owner_username) return '-'
|
||||
return shop.business_owner_phone_summary
|
||||
? `${shop.business_owner_username} (${shop.business_owner_phone_summary})`
|
||||
: shop.business_owner_username
|
||||
}
|
||||
|
||||
const showDetail = (row: ShopResponse) => {
|
||||
detailShop.value = row
|
||||
const showDetail = async (row: ShopResponse) => {
|
||||
try {
|
||||
const response = await ShopService.getShopDetail(row.id)
|
||||
if (response.code !== 0 || !response.data) {
|
||||
ElMessage.error(response.msg || '获取店铺详情失败')
|
||||
return
|
||||
}
|
||||
detailShop.value = response.data
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.message || '获取店铺详情失败')
|
||||
return
|
||||
}
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
|
||||
@@ -570,8 +582,8 @@
|
||||
},
|
||||
options: () =>
|
||||
salespersonOptions.value.map((salesperson) => ({
|
||||
label: `${salesperson.account_name} (${salesperson.phone_masked})`,
|
||||
value: salesperson.account_id
|
||||
label: `${salesperson.username} (${salesperson.phone_summary})`,
|
||||
value: salesperson.id
|
||||
}))
|
||||
}
|
||||
])
|
||||
@@ -584,7 +596,7 @@
|
||||
{ label: '所在地区', prop: 'region' },
|
||||
{ label: '联系人', prop: 'contact_name' },
|
||||
{ label: '联系电话', prop: 'contact_phone' },
|
||||
{ label: '平台业务员', prop: 'business_owner_name' },
|
||||
{ label: '平台业务员', prop: 'business_owner_username' },
|
||||
...(canModifyShopStatus ? [{ label: '状态', prop: 'status' }] : []),
|
||||
{ label: '创建时间', prop: 'created_at' },
|
||||
{ label: '操作', prop: 'operation' }
|
||||
@@ -717,11 +729,11 @@
|
||||
width: 130
|
||||
},
|
||||
{
|
||||
prop: 'business_owner_name',
|
||||
prop: 'business_owner_username',
|
||||
label: '平台业务员',
|
||||
minWidth: 180,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: ShopResponse) => formatBusinessOwner(row)
|
||||
formatter: (row: ShopResponse) => formatBusinessOwner(row)
|
||||
},
|
||||
...(canModifyShopStatus
|
||||
? [
|
||||
|
||||
Reference in New Issue
Block a user