560 lines
21 KiB
Vue
560 lines
21 KiB
Vue
<template>
|
||
<div class="space-y-6">
|
||
<!-- 加载状态 -->
|
||
<div
|
||
v-if="canViewReports && isLoading"
|
||
class="flex items-center justify-center py-20"
|
||
>
|
||
<div class="flex flex-col items-center space-y-3 text-text-secondary">
|
||
<svg class="h-8 w-8 animate-spin text-accent" viewBox="0 0 24 24" fill="none">
|
||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z" />
|
||
</svg>
|
||
<span class="text-sm">加载中...</span>
|
||
</div>
|
||
</div>
|
||
|
||
<template v-else-if="canViewReports">
|
||
<!-- 页面头部 -->
|
||
<section class="page-header">
|
||
<div>
|
||
<p class="page-kicker">Overview</p>
|
||
<h2 class="page-title">资产运行总览</h2>
|
||
<div class="mt-3 flex flex-wrap gap-2">
|
||
<router-link v-if="canBorrow" to="/borrow/scan" class="btn-primary text-sm">扫码出库</router-link>
|
||
<router-link v-if="canBorrow" to="/return/scan" class="btn-secondary text-sm">扫码入库</router-link>
|
||
<router-link v-if="canViewAllAssets" to="/equipment" class="btn-secondary text-sm">资产管理</router-link>
|
||
<router-link v-if="canApproveDisposals" to="/workflows/disposals?scope=pending" class="btn-secondary text-sm">待我审批</router-link>
|
||
<router-link v-if="canMaintain" to="/maintenance" class="btn-secondary text-sm">维修管理</router-link>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="page-meta-grid">
|
||
<div class="page-meta">
|
||
<p class="page-meta__label">今日借出</p>
|
||
<p class="page-meta__value">{{ formatNumber(stats?.today_borrow || 0) }}</p>
|
||
<p class="page-meta__subvalue">{{ formatCurrencyNoFraction(stats?.today_borrow_value || 0) }}</p>
|
||
</div>
|
||
<div v-if="canApproveDisposals" class="page-meta">
|
||
<p class="page-meta__label">待我审批</p>
|
||
<p class="page-meta__value text-status-warning">{{ pendingDisposals }}</p>
|
||
<p class="page-meta__subvalue">报废申请</p>
|
||
</div>
|
||
<div class="page-meta">
|
||
<p class="page-meta__label">今日归还</p>
|
||
<p class="page-meta__value">{{ formatNumber(stats?.today_return || 0) }}</p>
|
||
<p class="page-meta__subvalue">{{ formatCurrencyNoFraction(stats?.today_return_value || 0) }}</p>
|
||
</div>
|
||
<div class="page-meta">
|
||
<p class="page-meta__label">利用率</p>
|
||
<p class="page-meta__value">{{ formatPercent(stats?.usage_rate || 0) }}</p>
|
||
<p class="page-meta__subvalue">价值 {{ formatPercent(stats?.value_usage_rate || 0) }}</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 统计卡片 -->
|
||
<div class="grid gap-2 sm:gap-3 grid-cols-2 lg:grid-cols-4">
|
||
<div v-for="card in statCards.slice(0, 4)" :key="card.title" class="card">
|
||
<div class="flex items-start justify-between gap-2">
|
||
<div class="min-w-0 flex-1">
|
||
<p class="text-[10px] font-semibold uppercase tracking-wider text-text-tertiary truncate">{{ card.title }}</p>
|
||
<p class="mt-2 text-lg sm:text-2xl font-semibold tracking-tight text-text-primary truncate">{{ card.value }}</p>
|
||
</div>
|
||
<div class="flex h-8 w-8 sm:h-10 sm:w-10 shrink-0 items-center justify-center rounded-lg bg-accent-primary/10">
|
||
<component :is="card.icon" class="h-4 w-4 sm:h-5 sm:w-5 text-accent-primary" />
|
||
</div>
|
||
</div>
|
||
<p class="mt-2 text-[10px] sm:text-xs text-text-secondary truncate">{{ card.subtitle }}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 图表区域 -->
|
||
<div class="grid gap-4 lg:grid-cols-2">
|
||
<!-- 资产状态分布 -->
|
||
<section class="card">
|
||
<div class="list-header">
|
||
<h3 class="section-title">资产状态分布</h3>
|
||
<span class="badge badge-neutral">实时</span>
|
||
</div>
|
||
<div ref="statusChartRef" class="h-64"></div>
|
||
<div class="mt-3 grid grid-cols-3 gap-2">
|
||
<div class="panel text-center">
|
||
<p class="text-xs text-text-tertiary uppercase">可用</p>
|
||
<p class="text-lg font-semibold text-status-success mt-1">{{ formatNumber(stats?.available_equipment || 0) }}</p>
|
||
</div>
|
||
<div class="panel text-center">
|
||
<p class="text-xs text-text-tertiary uppercase">借出</p>
|
||
<p class="text-lg font-semibold text-status-warning mt-1">{{ formatNumber(stats?.borrowed_equipment || 0) }}</p>
|
||
</div>
|
||
<div class="panel text-center">
|
||
<p class="text-xs text-text-tertiary uppercase">维护</p>
|
||
<p class="text-lg font-semibold text-status-error mt-1">{{ formatNumber(stats?.maintenance_equipment || 0) }}</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 库存结构 -->
|
||
<section class="card">
|
||
<div class="list-header">
|
||
<h3 class="section-title">库存结构</h3>
|
||
<span class="badge badge-neutral">TOP 5</span>
|
||
</div>
|
||
<div ref="categoryChartRef" class="h-64"></div>
|
||
<div class="mt-4 space-y-2">
|
||
<div
|
||
v-for="item in topCategories"
|
||
:key="item.equipment_type"
|
||
class="flex items-center justify-between py-2 border-b border-border last:border-0"
|
||
>
|
||
<div>
|
||
<p class="text-sm font-medium text-text-primary">{{ item.equipment_type }}</p>
|
||
<p class="text-xs text-text-tertiary">
|
||
<template v-if="item.is_child_type">
|
||
可用 {{ formatNumber(item.available_quantity) }} · 空闲可挂载 {{ formatNumber(item.mountable_available_quantity) }} · 借出 {{ formatNumber(item.borrowed_quantity) }}
|
||
</template>
|
||
<template v-else>
|
||
可用 {{ formatNumber(item.available_quantity) }} · 借出 {{ formatNumber(item.borrowed_quantity) }}
|
||
</template>
|
||
</p>
|
||
</div>
|
||
<span class="text-lg font-semibold text-text-primary">{{ formatNumber(item.total_quantity) }}</span>
|
||
</div>
|
||
<div v-if="topCategories.length === 0" class="text-sm text-text-secondary py-3 text-center">
|
||
暂无数据
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
|
||
<!-- 最近借用记录 -->
|
||
<div class="grid gap-6 lg:grid-cols-3">
|
||
<section class="card lg:col-span-2">
|
||
<div class="list-header">
|
||
<h3 class="section-title">最近借用记录</h3>
|
||
<router-link v-if="canBorrow" to="/borrow" class="btn-secondary text-sm">查看全部</router-link>
|
||
</div>
|
||
|
||
<!-- 移动端:卡片列表 -->
|
||
<div class="mobile-card-list md:hidden">
|
||
<article v-for="record in recentRecords" :key="record.id" class="mobile-card">
|
||
<div class="mobile-card__row">
|
||
<div class="min-w-0 flex-1">
|
||
<p class="text-sm font-semibold text-text-primary truncate">{{ record.equipment_name }}</p>
|
||
<p class="text-xs text-text-tertiary font-mono">{{ record.equipment_code }}</p>
|
||
</div>
|
||
<span class="badge" :class="getStatusBadgeClass(record.status)">
|
||
{{ record.status }}
|
||
</span>
|
||
</div>
|
||
<div class="mobile-card__meta mt-3">
|
||
<div>
|
||
<p class="mobile-card__label">使用者</p>
|
||
<p class="mobile-card__value">{{ record.person_nickname }}</p>
|
||
</div>
|
||
<div>
|
||
<p class="mobile-card__label">位置</p>
|
||
<p class="mobile-card__value truncate">{{ record.storageLocation?.name || '-' }}</p>
|
||
</div>
|
||
<div class="col-span-2">
|
||
<p class="mobile-card__label">借用时间</p>
|
||
<p class="mobile-card__value">{{ formatDateTime(record.borrow_time) }}</p>
|
||
</div>
|
||
</div>
|
||
</article>
|
||
<div v-if="recentRecords.length === 0" class="empty-state">
|
||
暂无借用记录
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 桌面端:表格 -->
|
||
<div class="hidden md:block">
|
||
<div class="table-container">
|
||
<table class="data-table">
|
||
<thead>
|
||
<tr>
|
||
<th>资产</th>
|
||
<th>使用者</th>
|
||
<th>位置</th>
|
||
<th>借用时间</th>
|
||
<th>状态</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="record in recentRecords" :key="record.id">
|
||
<td>
|
||
<p class="font-medium text-text-primary">{{ record.equipment_name }}</p>
|
||
<p class="text-xs text-text-tertiary font-mono">{{ record.equipment_code }}</p>
|
||
</td>
|
||
<td>
|
||
<p class="text-text-primary">{{ record.person_nickname }}</p>
|
||
</td>
|
||
<td>
|
||
<p class="text-text-secondary">{{ record.storageLocation?.name || '-' }}</p>
|
||
</td>
|
||
<td class="text-text-secondary text-sm">
|
||
{{ formatDateTime(record.borrow_time) }}
|
||
</td>
|
||
<td>
|
||
<span
|
||
class="badge"
|
||
:class="getStatusBadgeClass(record.status)"
|
||
>
|
||
{{ record.status }}
|
||
</span>
|
||
</td>
|
||
</tr>
|
||
<tr v-if="recentRecords.length === 0">
|
||
<td colspan="5" class="text-center text-text-secondary py-8">暂无借用记录</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 运营提醒 -->
|
||
<section class="card">
|
||
<h3 class="section-title mb-3">运营概览</h3>
|
||
<div class="space-y-3">
|
||
<div class="panel">
|
||
<p class="text-xs text-text-tertiary uppercase">总资产价值</p>
|
||
<p class="text-xl font-semibold text-text-primary mt-1">
|
||
{{ formatCurrencyNoFraction(stats?.total_value || 0) }}
|
||
</p>
|
||
</div>
|
||
<div class="panel">
|
||
<p class="text-xs text-text-tertiary uppercase">可用资产</p>
|
||
<p class="text-base font-semibold text-status-success mt-1">
|
||
{{ formatNumber(stats?.available_equipment || 0) }}
|
||
</p>
|
||
<p class="text-xs text-text-secondary mt-0.5">
|
||
价值 {{ formatCurrencyNoFraction(stats?.available_value || 0) }}
|
||
</p>
|
||
</div>
|
||
<div class="panel">
|
||
<p class="text-xs text-text-tertiary uppercase">借出资产</p>
|
||
<p class="text-base font-semibold text-status-warning mt-1">
|
||
{{ formatNumber(stats?.borrowed_equipment || 0) }} 件
|
||
</p>
|
||
<p class="text-xs text-text-secondary mt-0.5">
|
||
价值 {{ formatCurrencyNoFraction(stats?.borrowed_value || 0) }}
|
||
</p>
|
||
</div>
|
||
<div class="panel">
|
||
<p class="text-xs text-text-tertiary uppercase">维护中</p>
|
||
<p class="text-base font-semibold text-status-error mt-1">
|
||
{{ formatNumber(stats?.maintenance_equipment || 0) }} 件
|
||
</p>
|
||
<p class="text-xs text-text-secondary mt-0.5">
|
||
价值 {{ formatCurrencyNoFraction(stats?.maintenance_value || 0) }}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</template>
|
||
|
||
<template v-else>
|
||
<section class="page-header">
|
||
<div>
|
||
<p class="page-kicker">My Assets</p>
|
||
<h2 class="page-title">我的资产</h2>
|
||
<router-link to="/my-assets" class="btn-primary mt-3 text-sm">查看全部</router-link>
|
||
</div>
|
||
<div class="page-meta-grid">
|
||
<div class="page-meta">
|
||
<p class="page-meta__label">当前未归还</p>
|
||
<p class="page-meta__value">{{ myAssets.length }}</p>
|
||
</div>
|
||
<div class="page-meta">
|
||
<p class="page-meta__label">员工绑定</p>
|
||
<p class="page-meta__value text-base">{{ authStore.user?.person_bound ? '已绑定' : '未绑定' }}</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="card">
|
||
<div class="list-header">
|
||
<h3 class="section-title">正在使用</h3>
|
||
<span class="badge badge-neutral">只读</span>
|
||
</div>
|
||
<p v-if="myAssetsLoading" class="empty-state">正在加载...</p>
|
||
<p v-else-if="myAssetsError" class="text-sm text-status-error">{{ myAssetsError }}</p>
|
||
<p v-else-if="myAssets.length === 0" class="empty-state">当前没有未归还资产。</p>
|
||
<div v-else class="grid gap-3 md:grid-cols-2 lg:grid-cols-3">
|
||
<article v-for="record in myAssets.slice(0, 6)" :key="record.borrow_record_id" class="panel">
|
||
<div class="flex items-start justify-between gap-3">
|
||
<div class="min-w-0">
|
||
<p class="truncate text-sm font-medium text-text-primary">{{ record.equipment.equipment_name }}</p>
|
||
<p class="mt-1 font-mono text-xs text-text-tertiary">{{ record.equipment.equipment_code }}</p>
|
||
</div>
|
||
<span class="badge badge-warning">使用中</span>
|
||
</div>
|
||
<p class="mt-3 text-xs text-text-secondary">
|
||
预计归还:{{ record.is_long_term ? '长期使用' : record.expected_return_time ? formatDateTime(record.expected_return_time) : '未设置' }}
|
||
</p>
|
||
</article>
|
||
</div>
|
||
</section>
|
||
</template>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||
import { useAuthStore } from '@/stores/auth'
|
||
import * as echarts from 'echarts'
|
||
import { getDashboardStats } from '@/api/statistics'
|
||
import { getBorrowRecordList } from '@/api/borrow'
|
||
import type { DashboardStats, BorrowRecord } from '@/types'
|
||
import { getMyAssets, type MyAssetRecord } from '@/api/myAssets'
|
||
import { getDisposalList } from '@/api/disposal'
|
||
import { PERMISSIONS } from '@/auth/permissions'
|
||
import { formatCurrencyNoFraction, formatDateTime, formatNumber, formatPercent } from '@/utils/format'
|
||
import {
|
||
BanknotesIcon,
|
||
ChartBarSquareIcon,
|
||
CheckCircleIcon,
|
||
CubeIcon,
|
||
WrenchScrewdriverIcon,
|
||
ArrowPathRoundedSquareIcon
|
||
} from '@heroicons/vue/24/outline'
|
||
|
||
const authStore = useAuthStore()
|
||
const stats = ref<DashboardStats | null>(null)
|
||
const recentRecords = ref<BorrowRecord[]>([])
|
||
const statusChartRef = ref<HTMLElement>()
|
||
const categoryChartRef = ref<HTMLElement>()
|
||
const isLoading = ref(false)
|
||
const pendingDisposals = ref(0)
|
||
const myAssets = ref<MyAssetRecord[]>([])
|
||
const myAssetsLoading = ref(false)
|
||
const myAssetsError = ref('')
|
||
let statusChart: echarts.ECharts | null = null
|
||
let categoryChart: echarts.ECharts | null = null
|
||
|
||
const canViewReports = computed(() => authStore.can(PERMISSIONS.REPORTS_VIEW))
|
||
const canViewAllAssets = computed(() => authStore.can(PERMISSIONS.ASSETS_VIEW_ALL))
|
||
const canBorrow = computed(() => authStore.can(PERMISSIONS.ASSETS_BORROW))
|
||
const canMaintain = computed(() => authStore.can(PERMISSIONS.ASSETS_MAINTENANCE))
|
||
const canApproveDisposals = computed(() => authStore.can(PERMISSIONS.DISPOSALS_APPROVE))
|
||
|
||
const statCards = computed(() => {
|
||
const data = stats.value
|
||
return [
|
||
{
|
||
title: '资产库存总量',
|
||
value: formatNumber(data?.total_equipment || 0),
|
||
icon: CubeIcon,
|
||
color: 'primary' as const,
|
||
subtitle: `总价值 ${formatCurrencyNoFraction(data?.total_value || 0)}`
|
||
},
|
||
{
|
||
title: '可用库存',
|
||
value: formatNumber(data?.available_equipment || 0),
|
||
icon: CheckCircleIcon,
|
||
color: 'success' as const,
|
||
subtitle: `可用价值 ${formatCurrencyNoFraction(data?.available_value || 0)}`
|
||
},
|
||
{
|
||
title: '已借出库存',
|
||
value: formatNumber(data?.borrowed_equipment || 0),
|
||
icon: ArrowPathRoundedSquareIcon,
|
||
color: 'warning' as const,
|
||
subtitle: `借出价值 ${formatCurrencyNoFraction(data?.borrowed_value || 0)}`
|
||
},
|
||
{
|
||
title: '维护中库存',
|
||
value: formatNumber(data?.maintenance_equipment || 0),
|
||
icon: WrenchScrewdriverIcon,
|
||
color: 'error' as const,
|
||
subtitle: `维护价值 ${formatCurrencyNoFraction(data?.maintenance_value || 0)}`
|
||
},
|
||
{
|
||
title: '今日借出',
|
||
value: formatNumber(data?.today_borrow || 0),
|
||
icon: ChartBarSquareIcon,
|
||
color: 'primary' as const,
|
||
subtitle: `数量利用率 ${formatPercent(data?.usage_rate || 0)}`
|
||
},
|
||
{
|
||
title: '今日归还',
|
||
value: formatNumber(data?.today_return || 0),
|
||
icon: BanknotesIcon,
|
||
color: 'success' as const,
|
||
subtitle: `价值利用率 ${formatPercent(data?.value_usage_rate || 0)}`
|
||
}
|
||
]
|
||
})
|
||
|
||
const topCategories = computed(() =>
|
||
[...(stats.value?.inventory_by_type || [])]
|
||
.sort((a, b) => b.total_quantity - a.total_quantity)
|
||
.slice(0, 5)
|
||
)
|
||
|
||
const fetchDashboardData = async () => {
|
||
isLoading.value = true
|
||
try {
|
||
const [statsResponse, recordsResponse] = await Promise.all([
|
||
getDashboardStats(),
|
||
getBorrowRecordList({ page: 1, pageSize: 8 })
|
||
])
|
||
|
||
if (statsResponse.data) {
|
||
stats.value = statsResponse.data
|
||
updateStatusChart()
|
||
updateCategoryChart()
|
||
}
|
||
|
||
if (recordsResponse.data) {
|
||
recentRecords.value = recordsResponse.data.list
|
||
}
|
||
} catch (error) {
|
||
console.error('获取仪表板数据失败:', error)
|
||
} finally {
|
||
isLoading.value = false
|
||
}
|
||
}
|
||
|
||
const fetchRoleSummary = async () => {
|
||
if (canApproveDisposals.value) {
|
||
try {
|
||
const response = await getDisposalList({ page: 1, pageSize: 1, scope: 'pending' })
|
||
pendingDisposals.value = response.data?.total || 0
|
||
} catch (error) {
|
||
console.error('获取待审批数量失败:', error)
|
||
}
|
||
}
|
||
}
|
||
|
||
const fetchMyAssets = async () => {
|
||
myAssetsLoading.value = true
|
||
myAssetsError.value = ''
|
||
try {
|
||
const response = await getMyAssets()
|
||
myAssets.value = response.data?.assets || []
|
||
} catch (error: any) {
|
||
myAssetsError.value = error?.message || '本人资产加载失败'
|
||
} finally {
|
||
myAssetsLoading.value = false
|
||
}
|
||
}
|
||
|
||
const initCharts = () => {
|
||
if (statusChartRef.value && !statusChart) {
|
||
statusChart = echarts.init(statusChartRef.value)
|
||
}
|
||
if (categoryChartRef.value && !categoryChart) {
|
||
categoryChart = echarts.init(categoryChartRef.value)
|
||
}
|
||
updateStatusChart()
|
||
updateCategoryChart()
|
||
}
|
||
|
||
const updateStatusChart = () => {
|
||
if (!statusChart || !stats.value) return
|
||
|
||
statusChart.setOption({
|
||
tooltip: { trigger: 'item' },
|
||
legend: {
|
||
bottom: 0,
|
||
icon: 'circle',
|
||
textStyle: { color: '#6e6e6e' }
|
||
},
|
||
series: [
|
||
{
|
||
type: 'pie',
|
||
radius: ['45%', '70%'],
|
||
center: ['50%', '45%'],
|
||
padAngle: 3,
|
||
label: {
|
||
formatter: '{b}\n{d}%',
|
||
color: '#a1a1a1'
|
||
},
|
||
data: [
|
||
{ value: stats.value.available_equipment || 0, name: '可用', itemStyle: { color: '#10a37f' } },
|
||
{ value: stats.value.borrowed_equipment || 0, name: '已借出', itemStyle: { color: '#f59e0b' } },
|
||
{ value: stats.value.maintenance_equipment || 0, name: '维护中', itemStyle: { color: '#ef4444' } }
|
||
]
|
||
}
|
||
]
|
||
})
|
||
}
|
||
|
||
const updateCategoryChart = () => {
|
||
if (!categoryChart || !stats.value) return
|
||
|
||
const categories = topCategories.value
|
||
categoryChart.setOption({
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
axisPointer: { type: 'shadow' }
|
||
},
|
||
grid: {
|
||
left: 0,
|
||
right: 0,
|
||
top: 10,
|
||
bottom: 10,
|
||
containLabel: true
|
||
},
|
||
xAxis: {
|
||
type: 'value',
|
||
splitLine: { lineStyle: { color: '#2a2a2a' } },
|
||
axisLabel: { color: '#6e6e6e' }
|
||
},
|
||
yAxis: {
|
||
type: 'category',
|
||
axisTick: { show: false },
|
||
axisLine: { lineStyle: { color: '#2a2a2a' } },
|
||
axisLabel: { color: '#a1a1a1' },
|
||
data: categories.map((item) => item.equipment_type)
|
||
},
|
||
series: [
|
||
{
|
||
type: 'bar',
|
||
barWidth: 16,
|
||
data: categories.map((item, index) => ({
|
||
value: item.total_quantity,
|
||
itemStyle: {
|
||
borderRadius: [0, 8, 8, 0],
|
||
color: ['#10a37f', '#0d8c6d', '#19c59f', '#f59e0b', '#3b82f6'][index % 5]
|
||
}
|
||
}))
|
||
}
|
||
]
|
||
})
|
||
}
|
||
|
||
const getStatusBadgeClass = (status: string) => {
|
||
const classes: Record<string, string> = {
|
||
'已借出': 'badge-warning',
|
||
'已归还': 'badge-success',
|
||
'逾期': 'badge-error'
|
||
}
|
||
return classes[status] || 'badge-neutral'
|
||
}
|
||
|
||
const resizeHandler = () => {
|
||
statusChart?.resize()
|
||
categoryChart?.resize()
|
||
}
|
||
|
||
onMounted(async () => {
|
||
if (canViewReports.value) {
|
||
await Promise.all([fetchDashboardData(), fetchRoleSummary()])
|
||
initCharts()
|
||
window.addEventListener('resize', resizeHandler)
|
||
} else {
|
||
await fetchMyAssets()
|
||
}
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
statusChart?.dispose()
|
||
categoryChart?.dispose()
|
||
window.removeEventListener('resize', resizeHandler)
|
||
})
|
||
</script>
|