feat: 初始化智能物联网设备管理系统前后端基础项目

完成项目整体结构搭建,包含:
1. 前端Vue3+TS+Vite+Antd项目基础配置与核心依赖
2. 后端MidwayJS+TypeORM+JWT权限认证框架
3. 完整的用户、角色、权限、日志数据库表结构
4. 核心业务模块API与基础服务实现
5. 全局样式、路由、状态管理配置
This commit is contained in:
andyesfly committed 2026-05-28 16:23:15 +08:00
commit 3d7eb12103
89 files changed
+16085

No files matched your search

+606
View File
@@ -0,0 +1,606 @@
<template>
<div class="dashboard">
<div class="dashboard-header">
<h2 class="dashboard-title">系统运营概况</h2>
<div class="header-actions">
<span class="update-time">最后更新: {{ lastUpdateTime }}</span>
<a-button type="primary" :loading="loading" @click="handleRefresh">
<template #icon><ReloadOutlined /></template>
刷新数据
</a-button>
</div>
</div>
<a-spin :spinning="loading" tip="加载中...">
<div class="dashboard-content">
<a-row :gutter="[16, 16]" class="stat-cards">
<a-col :xs="24" :sm="12" :md="6" v-for="card in statCards" :key="card.key">
<a-card :hoverable="true" class="stat-card" :class="`stat-card-${card.key}`">
<div class="stat-card-content">
<div class="stat-icon-wrapper" :style="{ background: card.iconBg }">
<component :is="card.icon" class="stat-icon" />
</div>
<div class="stat-info">
<div class="stat-value" :style="{ color: card.color }">
{{ stats?.[card.key] ?? '--' }}
</div>
<div class="stat-title">{{ card.title }}</div>
</div>
</div>
</a-card>
</a-col>
</a-row>
<a-row :gutter="[16, 16]" class="chart-section">
<a-col :xs="24" :lg="14">
<a-card title="设备趋势" class="chart-card">
<div ref="lineChartRef" class="chart-container"></div>
</a-card>
</a-col>
<a-col :xs="24" :lg="10">
<a-card title="设备类型分布" class="chart-card">
<div ref="pieChartRef" class="chart-container"></div>
</a-card>
</a-col>
</a-row>
<a-row :gutter="[16, 16]" class="list-section">
<a-col :xs="24" :lg="12">
<a-card title="最近告警" class="list-card">
<template #extra>
<a-badge :count="stats?.alertToday || 0" :number-style="{ backgroundColor: '#ff4d4f' }" />
</template>
<div class="alert-list">
<div
v-for="alert in stats?.recentAlerts"
:key="alert.id"
class="alert-item"
>
<div class="alert-header">
<a-tag :color="getAlertLevelColor(alert.level)">
{{ getAlertLevelText(alert.level) }}
</a-tag>
<span class="alert-device">{{ alert.device }}</span>
<a-tag :color="alert.status === 'pending' ? 'orange' : 'green'" class="alert-status">
{{ alert.status === 'pending' ? '待处理' : '已解决' }}
</a-tag>
</div>
<div class="alert-body">
<span class="alert-type">{{ alert.type }}</span>
<span class="alert-time">{{ alert.time }}</span>
</div>
</div>
<a-empty v-if="!stats?.recentAlerts?.length" description="暂无告警信息" />
</div>
</a-card>
</a-col>
<a-col :xs="24" :lg="12">
<a-card title="最近活动" class="list-card">
<a-timeline class="activity-timeline">
<a-timeline-item
v-for="activity in stats?.recentActivities"
:key="activity.id"
:color="getActivityColor(activity.module)"
>
<div class="activity-content">
<div class="activity-header">
<span class="activity-user">{{ activity.user }}</span>
<a-tag color="blue" class="activity-module">{{ activity.module }}</a-tag>
</div>
<div class="activity-action">{{ activity.action }}</div>
<div class="activity-meta">
<span class="activity-time">{{ activity.time }}</span>
<span class="activity-ip">IP: {{ activity.ip }}</span>
</div>
</div>
</a-timeline-item>
</a-timeline>
<a-empty v-if="!stats?.recentActivities?.length" description="暂无活动记录" />
</a-card>
</a-col>
</a-row>
</div>
</a-spin>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, nextTick, markRaw } from 'vue';
import {
Card,
Col,
Row,
Statistic,
Timeline,
TimelineItem,
Tag,
Button,
Badge,
Spin,
Empty,
message,
} from 'ant-design-vue';
import {
DeviceOutlined,
CheckCircleOutlined,
AlertOutlined,
TeamOutlined,
ReloadOutlined,
} from '@ant-design/icons-vue';
import * as echarts from 'echarts';
import { dashboardApi, type DashboardStats } from '@/api/dashboard';
const ACard = Card;
const ACol = Col;
const ARow = Row;
const ATimeline = Timeline;
const ATimelineItem = TimelineItem;
const stats = ref<DashboardStats | null>(null);
const loading = ref(false);
const lineChartRef = ref<HTMLDivElement>();
const pieChartRef = ref<HTMLDivElement>();
let lineChartInstance: echarts.ECharts | null = null;
let pieChartInstance: echarts.ECharts | null = null;
let refreshTimer: ReturnType<typeof setInterval> | null = null;
const lastUpdateTime = ref('--');
const statCards = [
{
key: 'deviceTotal',
title: '设备总数',
icon: markRaw(DeviceOutlined),
color: '#1890ff',
iconBg: '#e6f7ff',
},
{
key: 'deviceOnline',
title: '在线设备',
icon: markRaw(CheckCircleOutlined),
color: '#52c41a',
iconBg: '#f6ffed',
},
{
key: 'alertToday',
title: '今日告警',
icon: markRaw(AlertOutlined),
color: '#ff4d4f',
iconBg: '#fff2f0',
},
{
key: 'activeUsers',
title: '活跃用户',
icon: markRaw(TeamOutlined),
color: '#faad14',
iconBg: '#fffbe6',
},
];
onMounted(async () => {
await loadStats();
await nextTick();
initCharts();
startAutoRefresh();
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
stopAutoRefresh();
disposeCharts();
window.removeEventListener('resize', handleResize);
});
async function loadStats() {
loading.value = true;
try {
const res = await dashboardApi.getStats();
if (res.code === 200) {
stats.value = res.data;
lastUpdateTime.value = new Date().toLocaleTimeString('zh-CN');
updateCharts();
}
} catch (error) {
console.error('加载仪表盘数据失败:', error);
message.error('加载数据失败,请稍后重试');
} finally {
loading.value = false;
}
}
function handleRefresh() {
loadStats();
}
function initCharts() {
if (lineChartRef.value) {
lineChartInstance = echarts.init(lineChartRef.value);
}
if (pieChartRef.value) {
pieChartInstance = echarts.init(pieChartRef.value);
}
}
function updateCharts() {
if (!stats.value) return;
if (lineChartInstance && stats.value.deviceTrend) {
const option = {
tooltip: {
trigger: 'axis',
axisPointer: { type: 'cross' },
},
legend: {
data: ['在线设备', '离线设备'],
top: 0,
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true,
},
xAxis: {
type: 'category',
boundaryGap: false,
data: stats.value.deviceTrend.map(d => d.date.slice(5)),
axisLine: { lineStyle: { color: '#d9d9d9' } },
axisLabel: { color: '#666' },
},
yAxis: {
type: 'value',
axisLine: { show: false },
axisTick: { show: false },
splitLine: { lineStyle: { color: '#f0f0f0', type: 'dashed' } },
axisLabel: { color: '#666' },
},
series: [
{
name: '在线设备',
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 8,
data: stats.value.deviceTrend.map(d => d.online),
itemStyle: { color: '#52c41a' },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(82, 196, 26, 0.3)' },
{ offset: 1, color: 'rgba(82, 196, 26, 0.05)' },
]),
},
},
{
name: '离线设备',
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 8,
data: stats.value.deviceTrend.map(d => d.offline),
itemStyle: { color: '#ff4d4f' },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(255, 77, 79, 0.3)' },
{ offset: 1, color: 'rgba(255, 77, 79, 0.05)' },
]),
},
},
],
};
lineChartInstance.setOption(option, true);
}
if (pieChartInstance && stats.value.deviceTypeDistribution) {
const option = {
tooltip: {
trigger: 'item',
formatter: '{b}: {c}台 ({d}%)',
},
legend: {
orient: 'vertical',
left: 'left',
top: 'center',
textStyle: { color: '#666' },
},
series: [
{
type: 'pie',
radius: ['45%', '70%'],
center: ['60%', '50%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 6,
borderColor: '#fff',
borderWidth: 2,
},
label: {
show: true,
formatter: '{b}\n{d}%',
fontSize: 12,
},
emphasis: {
label: {
show: true,
fontSize: 14,
fontWeight: 'bold',
},
},
labelLine: {
length: 15,
length2: 10,
},
data: stats.value.deviceTypeDistribution.map((item, index) => ({
...item,
itemStyle: {
color: ['#1890ff', '#52c41a', '#faad14', '#722ed1', '#13c2c2'][index],
},
})),
},
],
};
pieChartInstance.setOption(option, true);
}
}
function handleResize() {
lineChartInstance?.resize();
pieChartInstance?.resize();
}
function startAutoRefresh() {
refreshTimer = setInterval(loadStats, 30000);
}
function stopAutoRefresh() {
if (refreshTimer) {
clearInterval(refreshTimer);
refreshTimer = null;
}
}
function disposeCharts() {
lineChartInstance?.dispose();
pieChartInstance?.dispose();
lineChartInstance = null;
pieChartInstance = null;
}
function getAlertLevelColor(level: string): string {
const colors: Record<string, string> = {
high: 'red',
medium: 'orange',
low: 'blue',
};
return colors[level] || 'default';
}
function getAlertLevelText(level: string): string {
const texts: Record<string, string> = {
high: '严重',
medium: '警告',
low: '提示',
};
return texts[level] || level;
}
function getActivityColor(module: string): string {
const colors: Record<string, string> = {
认证: 'blue',
设备管理: 'green',
用户管理: 'purple',
监控中心: 'red',
数据分析: 'orange',
系统: 'cyan',
系统设置: 'geekblue',
};
return colors[module] || 'gray';
}
</script>
<style scoped lang="less">
.dashboard {
padding: 24px;
min-height: calc(100vh - 120px);
.dashboard-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
.dashboard-title {
font-size: 20px;
font-weight: 600;
color: #1f1f1f;
margin: 0;
}
.header-actions {
display: flex;
align-items: center;
gap: 16px;
.update-time {
color: #999;
font-size: 13px;
}
}
}
.dashboard-content {
.stat-cards {
margin-bottom: 24px;
.stat-card {
border-radius: 8px;
transition: all 0.3s ease;
&:hover {
transform: translateY(-4px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.stat-card-content {
display: flex;
align-items: center;
gap: 16px;
padding: 8px 0;
.stat-icon-wrapper {
width: 56px;
height: 56px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
.stat-icon {
font-size: 28px;
}
}
.stat-info {
flex: 1;
text-align: left;
.stat-value {
font-size: 28px;
font-weight: 700;
line-height: 1.2;
margin-bottom: 4px;
}
.stat-title {
font-size: 14px;
color: #666;
}
}
}
}
}
.chart-section {
margin-bottom: 24px;
.chart-card {
border-radius: 8px;
.chart-container {
height: 350px;
width: 100%;
}
}
}
.list-section {
.list-card {
border-radius: 8px;
min-height: 400px;
.alert-list {
max-height: 360px;
overflow-y: auto;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background-color: #d9d9d9;
border-radius: 3px;
}
.alert-item {
padding: 12px 0;
border-bottom: 1px solid #f5f5f5;
&:last-child {
border-bottom: none;
}
.alert-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
.alert-device {
flex: 1;
font-weight: 500;
color: #333;
}
.alert-status {
font-size: 12px;
}
}
.alert-body {
display: flex;
justify-content: space-between;
align-items: center;
padding-left: 4px;
.alert-type {
color: #666;
font-size: 13px;
}
.alert-time {
color: #999;
font-size: 12px;
}
}
}
}
.activity-timeline {
max-height: 360px;
overflow-y: auto;
padding-top: 8px;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background-color: #d9d9d9;
border-radius: 3px;
}
.activity-content {
.activity-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
.activity-user {
font-weight: 500;
color: #333;
}
.activity-module {
font-size: 11px;
}
}
.activity-action {
color: #666;
font-size: 13px;
margin-bottom: 4px;
}
.activity-meta {
display: flex;
gap: 12px;
font-size: 12px;
color: #999;
}
}
}
}
}
}
}
</style>