|
|
|
|
@ -0,0 +1,576 @@
|
|
|
|
|
<template>
|
|
|
|
|
<div class="alarm-statistics-container">
|
|
|
|
|
<!-- Header -->
|
|
|
|
|
<div class="header-section">
|
|
|
|
|
<h2 class="page-title">报警统计</h2>
|
|
|
|
|
<el-radio-group v-model="timeRange" @change="handleTimeRangeChange">
|
|
|
|
|
<el-radio-button label="today">今日</el-radio-button>
|
|
|
|
|
<el-radio-button label="week">本周</el-radio-button>
|
|
|
|
|
<el-radio-button label="month">本月</el-radio-button>
|
|
|
|
|
</el-radio-group>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Top Stats Cards -->
|
|
|
|
|
<div class="stats-cards">
|
|
|
|
|
<el-card shadow="never" class="stat-card" v-loading="countLoading">
|
|
|
|
|
<div class="stat-content">
|
|
|
|
|
<div class="stat-icon bg-red-100 text-red-500">
|
|
|
|
|
<Icon icon="ep:bell-filled" :size="32" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="stat-info">
|
|
|
|
|
<div class="stat-value">{{ countData.totalCount }}</div>
|
|
|
|
|
<div class="stat-label">报警总数</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</el-card>
|
|
|
|
|
|
|
|
|
|
<el-card shadow="never" class="stat-card" v-loading="countLoading">
|
|
|
|
|
<div class="stat-content">
|
|
|
|
|
<div class="stat-icon bg-blue-100 text-blue-500">
|
|
|
|
|
<Icon icon="ep:info-filled" :size="32" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="stat-info">
|
|
|
|
|
<div class="stat-value">{{ countData.normalCount }}</div>
|
|
|
|
|
<div class="stat-label">一般报警</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</el-card>
|
|
|
|
|
|
|
|
|
|
<el-card shadow="never" class="stat-card" v-loading="countLoading">
|
|
|
|
|
<div class="stat-content">
|
|
|
|
|
<div class="stat-icon bg-yellow-100 text-yellow-500">
|
|
|
|
|
<Icon icon="ep:opportunity" :size="32" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="stat-info">
|
|
|
|
|
<div class="stat-value">{{ countData.tipCount }}</div>
|
|
|
|
|
<div class="stat-label">提示报警</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</el-card>
|
|
|
|
|
|
|
|
|
|
<el-card shadow="never" class="stat-card" v-loading="countLoading">
|
|
|
|
|
<div class="stat-content">
|
|
|
|
|
<div class="stat-icon bg-orange-100 text-orange-500">
|
|
|
|
|
<Icon icon="ep:warning-filled" :size="32" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="stat-info">
|
|
|
|
|
<div class="stat-value">{{ countData.seriousCount }}</div>
|
|
|
|
|
<div class="stat-label">严重报警</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</el-card>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Charts Section -->
|
|
|
|
|
<div class="charts-section">
|
|
|
|
|
<el-card shadow="never" class="chart-card flex-2" v-loading="trendLoading">
|
|
|
|
|
<template #header>
|
|
|
|
|
<div class="card-title">报警趋势</div>
|
|
|
|
|
</template>
|
|
|
|
|
<div ref="trendChartRef" class="chart-container"></div>
|
|
|
|
|
</el-card>
|
|
|
|
|
|
|
|
|
|
<el-card shadow="never" class="chart-card flex-1" v-loading="countLoading">
|
|
|
|
|
<template #header>
|
|
|
|
|
<div class="card-title">报警级别分布</div>
|
|
|
|
|
</template>
|
|
|
|
|
<div ref="pieChartRef" class="chart-container"></div>
|
|
|
|
|
</el-card>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Alarm Records Table -->
|
|
|
|
|
<el-card shadow="never" class="table-card">
|
|
|
|
|
<template #header>
|
|
|
|
|
<div class="table-header">
|
|
|
|
|
<div class="card-title">报警记录</div>
|
|
|
|
|
<div class="table-filters">
|
|
|
|
|
<el-select
|
|
|
|
|
v-model="queryParams.alarmLevel"
|
|
|
|
|
placeholder="全部级别"
|
|
|
|
|
clearable
|
|
|
|
|
@change="handleQuery"
|
|
|
|
|
class="filter-select"
|
|
|
|
|
>
|
|
|
|
|
<el-option
|
|
|
|
|
v-for="dict in alarmLevelOptions"
|
|
|
|
|
:key="dict.value"
|
|
|
|
|
:label="dict.label"
|
|
|
|
|
:value="dict.value"
|
|
|
|
|
/>
|
|
|
|
|
</el-select>
|
|
|
|
|
<!-- 状态字段后端未返回,前端仅占位展示下拉框 -->
|
|
|
|
|
<el-select v-model="mockStatus" placeholder="全部状态" clearable class="filter-select">
|
|
|
|
|
<el-option label="活动" value="active" />
|
|
|
|
|
<el-option label="已处理" value="handled" />
|
|
|
|
|
</el-select>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<el-table v-loading="tableLoading" :data="tableData" stripe style="width: 100%">
|
|
|
|
|
<el-table-column prop="deviceName" label="设备名称" min-width="120" show-overflow-tooltip />
|
|
|
|
|
<el-table-column prop="customerName" label="客户" min-width="150" show-overflow-tooltip />
|
|
|
|
|
<el-table-column prop="rule" label="报警代码" min-width="100" />
|
|
|
|
|
<el-table-column prop="ruleName" label="报警信息" min-width="150" show-overflow-tooltip />
|
|
|
|
|
<el-table-column prop="alarmLevel" label="级别" min-width="100">
|
|
|
|
|
<template #default="scope">
|
|
|
|
|
<dict-tag :type="DICT_TYPE.IOT_ALARM_REGISTRATION" :value="scope.row.alarmLevel" />
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column prop="createTime" label="报警时间" min-width="160" />
|
|
|
|
|
<el-table-column label="状态" min-width="100">
|
|
|
|
|
<template #default>
|
|
|
|
|
<!-- 模拟状态展示,由于接口没有此字段,统一显示为待处理或占位 -->
|
|
|
|
|
<el-tag type="danger" effect="light" round>活动</el-tag>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column label="操作" min-width="100" fixed="right">
|
|
|
|
|
<template #default="scope">
|
|
|
|
|
<el-button link type="primary" @click="handleProcess(scope.row)">处理</el-button>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
</el-table>
|
|
|
|
|
|
|
|
|
|
<!-- 分页 -->
|
|
|
|
|
<div class="pagination-container">
|
|
|
|
|
<Pagination
|
|
|
|
|
:total="total"
|
|
|
|
|
v-model:page="queryParams.pageNo"
|
|
|
|
|
v-model:limit="queryParams.pageSize"
|
|
|
|
|
@pagination="getList"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</el-card>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { ref, onMounted, onUnmounted, reactive, nextTick } from 'vue'
|
|
|
|
|
import * as echarts from 'echarts'
|
|
|
|
|
import dayjs from 'dayjs'
|
|
|
|
|
import { DeviceWarningRecordApi, DeviceWarningRecordRespVO } from '@/api/iot/deviceWarningRecord'
|
|
|
|
|
import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
|
|
|
|
|
|
|
|
|
|
defineOptions({ name: 'AlarmStatistics' })
|
|
|
|
|
|
|
|
|
|
const timeRange = ref('today')
|
|
|
|
|
const mockStatus = ref('')
|
|
|
|
|
|
|
|
|
|
const countLoading = ref(false)
|
|
|
|
|
const countData = reactive({
|
|
|
|
|
totalCount: 0,
|
|
|
|
|
normalCount: 0,
|
|
|
|
|
tipCount: 0,
|
|
|
|
|
seriousCount: 0
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const trendLoading = ref(false)
|
|
|
|
|
const trendChartRef = ref<HTMLElement | null>(null)
|
|
|
|
|
let trendChartInstance: echarts.ECharts | null = null
|
|
|
|
|
|
|
|
|
|
const pieChartRef = ref<HTMLElement | null>(null)
|
|
|
|
|
let pieChartInstance: echarts.ECharts | null = null
|
|
|
|
|
|
|
|
|
|
const tableLoading = ref(false)
|
|
|
|
|
const tableData = ref<DeviceWarningRecordRespVO[]>([])
|
|
|
|
|
const total = ref(0)
|
|
|
|
|
|
|
|
|
|
const queryParams = reactive({
|
|
|
|
|
pageNo: 1,
|
|
|
|
|
pageSize: 10,
|
|
|
|
|
alarmLevel: undefined,
|
|
|
|
|
startTime: undefined as string | undefined,
|
|
|
|
|
endTime: undefined as string | undefined
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const alarmLevelOptions = getStrDictOptions(DICT_TYPE.IOT_ALARM_REGISTRATION)
|
|
|
|
|
|
|
|
|
|
// 获取当前时间范围
|
|
|
|
|
const getTimeRangeValues = () => {
|
|
|
|
|
const now = dayjs()
|
|
|
|
|
let startTime = ''
|
|
|
|
|
let endTime = ''
|
|
|
|
|
|
|
|
|
|
if (timeRange.value === 'today') {
|
|
|
|
|
startTime = now.startOf('day').format('YYYY-MM-DD HH:mm:ss')
|
|
|
|
|
endTime = now.endOf('day').format('YYYY-MM-DD HH:mm:ss')
|
|
|
|
|
} else if (timeRange.value === 'week') {
|
|
|
|
|
startTime = now.startOf('week').format('YYYY-MM-DD HH:mm:ss')
|
|
|
|
|
endTime = now.endOf('week').format('YYYY-MM-DD HH:mm:ss')
|
|
|
|
|
} else if (timeRange.value === 'month') {
|
|
|
|
|
startTime = now.startOf('month').format('YYYY-MM-DD HH:mm:ss')
|
|
|
|
|
endTime = now.endOf('month').format('YYYY-MM-DD HH:mm:ss')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { startTime, endTime }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleTimeRangeChange = () => {
|
|
|
|
|
const { startTime, endTime } = getTimeRangeValues()
|
|
|
|
|
queryParams.startTime = startTime
|
|
|
|
|
queryParams.endTime = endTime
|
|
|
|
|
queryParams.pageNo = 1
|
|
|
|
|
|
|
|
|
|
loadAllData()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleQuery = () => {
|
|
|
|
|
queryParams.pageNo = 1
|
|
|
|
|
getList()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const loadAllData = () => {
|
|
|
|
|
getCountData()
|
|
|
|
|
getTrendData()
|
|
|
|
|
getList()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const getCountData = async () => {
|
|
|
|
|
try {
|
|
|
|
|
countLoading.value = true
|
|
|
|
|
const { startTime, endTime } = getTimeRangeValues()
|
|
|
|
|
const res = await DeviceWarningRecordApi.getWarningCount({ startTime, endTime })
|
|
|
|
|
if (res) {
|
|
|
|
|
countData.totalCount = res.totalCount || 0
|
|
|
|
|
countData.normalCount = res.normalCount || 0
|
|
|
|
|
countData.tipCount = res.tipCount || 0
|
|
|
|
|
countData.seriousCount = res.seriousCount || 0
|
|
|
|
|
updatePieChart()
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('获取统计数据失败', error)
|
|
|
|
|
} finally {
|
|
|
|
|
countLoading.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const getTrendData = async () => {
|
|
|
|
|
try {
|
|
|
|
|
trendLoading.value = true
|
|
|
|
|
const { startTime, endTime } = getTimeRangeValues()
|
|
|
|
|
const res = await DeviceWarningRecordApi.getWarningTrend({ startTime, endTime })
|
|
|
|
|
if (res) {
|
|
|
|
|
updateTrendChart(res.timePoints || [], res.counts || [])
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('获取趋势数据失败', error)
|
|
|
|
|
} finally {
|
|
|
|
|
trendLoading.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const getList = async () => {
|
|
|
|
|
try {
|
|
|
|
|
tableLoading.value = true
|
|
|
|
|
const res = await DeviceWarningRecordApi.getWarningRecordList(queryParams)
|
|
|
|
|
tableData.value = res?.list || []
|
|
|
|
|
total.value = res?.total || 0
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('获取列表数据失败', error)
|
|
|
|
|
} finally {
|
|
|
|
|
tableLoading.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const initCharts = () => {
|
|
|
|
|
if (trendChartRef.value) {
|
|
|
|
|
trendChartInstance = echarts.init(trendChartRef.value)
|
|
|
|
|
}
|
|
|
|
|
if (pieChartRef.value) {
|
|
|
|
|
pieChartInstance = echarts.init(pieChartRef.value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const updateTrendChart = (xData: string[], yData: number[]) => {
|
|
|
|
|
if (!trendChartInstance) return
|
|
|
|
|
|
|
|
|
|
const option = {
|
|
|
|
|
tooltip: {
|
|
|
|
|
trigger: 'axis'
|
|
|
|
|
},
|
|
|
|
|
grid: {
|
|
|
|
|
left: '3%',
|
|
|
|
|
right: '4%',
|
|
|
|
|
bottom: '3%',
|
|
|
|
|
containLabel: true
|
|
|
|
|
},
|
|
|
|
|
xAxis: {
|
|
|
|
|
type: 'category',
|
|
|
|
|
boundaryGap: false,
|
|
|
|
|
data: xData,
|
|
|
|
|
axisLine: {
|
|
|
|
|
lineStyle: {
|
|
|
|
|
color: '#E5E7EB'
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
axisLabel: {
|
|
|
|
|
color: '#6B7280'
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
yAxis: {
|
|
|
|
|
type: 'value',
|
|
|
|
|
splitLine: {
|
|
|
|
|
lineStyle: {
|
|
|
|
|
type: 'dashed',
|
|
|
|
|
color: '#E5E7EB'
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
axisLabel: {
|
|
|
|
|
color: '#6B7280'
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
series: [
|
|
|
|
|
{
|
|
|
|
|
name: '报警数',
|
|
|
|
|
type: 'line',
|
|
|
|
|
smooth: true,
|
|
|
|
|
data: yData,
|
|
|
|
|
itemStyle: {
|
|
|
|
|
color: '#F56C6C'
|
|
|
|
|
},
|
|
|
|
|
areaStyle: {
|
|
|
|
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
|
|
|
{ offset: 0, color: 'rgba(245, 108, 108, 0.3)' },
|
|
|
|
|
{ offset: 1, color: 'rgba(245, 108, 108, 0.05)' }
|
|
|
|
|
])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
trendChartInstance.setOption(option)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const updatePieChart = () => {
|
|
|
|
|
if (!pieChartInstance) return
|
|
|
|
|
|
|
|
|
|
const option = {
|
|
|
|
|
tooltip: {
|
|
|
|
|
trigger: 'item'
|
|
|
|
|
},
|
|
|
|
|
legend: {
|
|
|
|
|
bottom: '5%',
|
|
|
|
|
left: 'center'
|
|
|
|
|
},
|
|
|
|
|
color: ['#F56C6C', '#E6A23C', '#409EFF'],
|
|
|
|
|
series: [
|
|
|
|
|
{
|
|
|
|
|
name: '报警级别分布',
|
|
|
|
|
type: 'pie',
|
|
|
|
|
radius: ['40%', '70%'],
|
|
|
|
|
avoidLabelOverlap: false,
|
|
|
|
|
itemStyle: {
|
|
|
|
|
borderRadius: 10,
|
|
|
|
|
borderColor: '#fff',
|
|
|
|
|
borderWidth: 2
|
|
|
|
|
},
|
|
|
|
|
label: {
|
|
|
|
|
show: false,
|
|
|
|
|
position: 'center'
|
|
|
|
|
},
|
|
|
|
|
emphasis: {
|
|
|
|
|
label: {
|
|
|
|
|
show: true,
|
|
|
|
|
fontSize: 20,
|
|
|
|
|
fontWeight: 'bold'
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
labelLine: {
|
|
|
|
|
show: false
|
|
|
|
|
},
|
|
|
|
|
data: [
|
|
|
|
|
{ value: countData.seriousCount, name: '严重' },
|
|
|
|
|
{ value: countData.tipCount, name: '提示' },
|
|
|
|
|
{ value: countData.normalCount, name: '一般' }
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
pieChartInstance.setOption(option)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleProcess = (row: any) => {
|
|
|
|
|
// TODO: 实现处理逻辑
|
|
|
|
|
console.log('处理', row)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleResize = () => {
|
|
|
|
|
trendChartInstance?.resize()
|
|
|
|
|
pieChartInstance?.resize()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
const { startTime, endTime } = getTimeRangeValues()
|
|
|
|
|
queryParams.startTime = startTime
|
|
|
|
|
queryParams.endTime = endTime
|
|
|
|
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
initCharts()
|
|
|
|
|
loadAllData()
|
|
|
|
|
window.addEventListener('resize', handleResize)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
window.removeEventListener('resize', handleResize)
|
|
|
|
|
trendChartInstance?.dispose()
|
|
|
|
|
pieChartInstance?.dispose()
|
|
|
|
|
})
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
|
|
.alarm-statistics-container {
|
|
|
|
|
padding: 16px;
|
|
|
|
|
background-color: var(--el-bg-color-page);
|
|
|
|
|
min-height: calc(100vh - 84px);
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
|
|
|
|
.header-section {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
background-color: var(--el-bg-color);
|
|
|
|
|
padding: 16px 20px;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
|
|
|
|
.page-title {
|
|
|
|
|
margin: 0;
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: var(--el-text-color-primary);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stats-cards {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(4, 1fr);
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
|
|
|
|
.stat-card {
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
|
|
|
|
:deep(.el-card__body) {
|
|
|
|
|
padding: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-content {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
|
|
|
|
.stat-icon {
|
|
|
|
|
width: 56px;
|
|
|
|
|
height: 56px;
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-info {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
|
|
|
|
.stat-value {
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
line-height: 1;
|
|
|
|
|
color: var(--el-text-color-primary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-label {
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
color: var(--el-text-color-secondary);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.charts-section {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
|
|
|
|
.chart-card {
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
|
|
|
|
&.flex-2 {
|
|
|
|
|
flex: 2;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&.flex-1 {
|
|
|
|
|
flex: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.card-title {
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chart-container {
|
|
|
|
|
height: 300px;
|
|
|
|
|
width: 100%;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.table-card {
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
|
|
|
|
.table-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
|
|
|
|
.card-title {
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.table-filters {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
|
|
|
|
.filter-select {
|
|
|
|
|
width: 120px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.pagination-container {
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 颜色工具类 */
|
|
|
|
|
.bg-red-100 {
|
|
|
|
|
background-color: #fee2e2;
|
|
|
|
|
}
|
|
|
|
|
.text-red-500 {
|
|
|
|
|
color: #ef4444;
|
|
|
|
|
}
|
|
|
|
|
.bg-blue-100 {
|
|
|
|
|
background-color: #dbeafe;
|
|
|
|
|
}
|
|
|
|
|
.text-blue-500 {
|
|
|
|
|
color: #3b82f6;
|
|
|
|
|
}
|
|
|
|
|
.bg-yellow-100 {
|
|
|
|
|
background-color: #fef9c3;
|
|
|
|
|
}
|
|
|
|
|
.text-yellow-500 {
|
|
|
|
|
color: #eab308;
|
|
|
|
|
}
|
|
|
|
|
.bg-orange-100 {
|
|
|
|
|
background-color: #ffedd5;
|
|
|
|
|
}
|
|
|
|
|
.text-orange-500 {
|
|
|
|
|
color: #f97316;
|
|
|
|
|
}
|
|
|
|
|
</style>
|