|
@@ -1,172 +1,1586 @@
|
|
|
<template>
|
|
|
- <div class="app-container home">
|
|
|
- <el-row :gutter="20">
|
|
|
- <!-- 左侧内容 -->
|
|
|
- <el-col :sm="24" :lg="12" style="padding-left: 20px">
|
|
|
- <h2>IBMS后台管理框架</h2>
|
|
|
- <p>
|
|
|
- <b>当前版本:</b> <span>v{{ version }}</span>
|
|
|
- </p>
|
|
|
- </el-col>
|
|
|
+ <div class="dashboard-container">
|
|
|
+ <!-- 顶部欢迎区域 -->
|
|
|
+ <div class="welcome-section">
|
|
|
+ <div class="welcome-content">
|
|
|
+ <h1 class="welcome-title">
|
|
|
+ <span class="gradient-text">IBMS智能建筑管理系统</span>
|
|
|
+ </h1>
|
|
|
+ <p class="welcome-subtitle">集成化、智能化、可视化的建筑管理解决方案</p>
|
|
|
+ <div class="version-info">
|
|
|
+ <el-tag type="primary" effect="dark" @click="showVersionInfo">
|
|
|
+ <i class="el-icon-price-tag"></i> v{{ version }}
|
|
|
+ </el-tag>
|
|
|
+ <el-tag type="success" effect="plain" @click="showSystemStatus">
|
|
|
+ <i class="el-icon-circle-check"></i> 运行正常
|
|
|
+ </el-tag>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="welcome-illustration">
|
|
|
+ <!-- SVG 图形保持不变 -->
|
|
|
+ <svg viewBox="0 0 400 300" xmlns="http://www.w3.org/2000/svg">
|
|
|
+ <defs>
|
|
|
+ <linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
|
+ <stop offset="0%" style="stop-color:#667eea;stop-opacity:0.8" />
|
|
|
+ <stop offset="100%" style="stop-color:#764ba2;stop-opacity:0.8" />
|
|
|
+ </linearGradient>
|
|
|
+ </defs>
|
|
|
+ <!-- 建筑轮廓 -->
|
|
|
+ <rect x="50" y="100" width="80" height="150" fill="rgba(255,255,255,0.2)" stroke="white" stroke-width="2" rx="5"/>
|
|
|
+ <rect x="150" y="50" width="100" height="200" fill="rgba(255,255,255,0.2)" stroke="white" stroke-width="2" rx="5"/>
|
|
|
+ <rect x="270" y="120" width="80" height="130" fill="rgba(255,255,255,0.2)" stroke="white" stroke-width="2" rx="5"/>
|
|
|
+
|
|
|
+ <!-- 窗户 -->
|
|
|
+ <rect x="60" y="110" width="20" height="20" fill="rgba(255,255,255,0.5)" />
|
|
|
+ <rect x="90" y="110" width="20" height="20" fill="rgba(255,255,255,0.5)" />
|
|
|
+ <rect x="60" y="140" width="20" height="20" fill="rgba(255,255,255,0.5)" />
|
|
|
+ <rect x="90" y="140" width="20" height="20" fill="rgba(255,255,255,0.5)" />
|
|
|
+
|
|
|
+ <!-- 连接线 -->
|
|
|
+ <path d="M 90 250 Q 200 280 310 250" stroke="white" stroke-width="2" fill="none" stroke-dasharray="5,5" opacity="0.5"/>
|
|
|
+ <circle cx="200" cy="265" r="5" fill="white" opacity="0.8"/>
|
|
|
|
|
|
- <!-- 右侧技术选型 -->
|
|
|
- <el-col :sm="24" :lg="12" style="padding-left: 50px">
|
|
|
- <el-row>
|
|
|
- <el-col :span="12">
|
|
|
- <h2>技术选型</h2>
|
|
|
- </el-col>
|
|
|
- </el-row>
|
|
|
- <el-row>
|
|
|
- <el-col :span="6">
|
|
|
- <h4>后端技术</h4>
|
|
|
- <ul>
|
|
|
- <li v-for="(item, index) in backendTech" :key="index">{{ item }}</li>
|
|
|
- </ul>
|
|
|
- </el-col>
|
|
|
- <el-col :span="6">
|
|
|
- <h4>前端技术</h4>
|
|
|
- <ul>
|
|
|
- <li v-for="(item, index) in frontendTech" :key="index">{{ item }}</li>
|
|
|
- </ul>
|
|
|
- </el-col>
|
|
|
- </el-row>
|
|
|
+ <!-- 图标 -->
|
|
|
+ <text x="200" y="40" text-anchor="middle" fill="white" font-size="24" font-weight="bold">IBMS</text>
|
|
|
+ </svg>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 快速统计卡片 -->
|
|
|
+ <el-row :gutter="20" class="stat-cards">
|
|
|
+ <el-col :xs="24" :sm="12" :md="6" v-for="(stat, index) in statistics" :key="index">
|
|
|
+ <div class="stat-card" :style="{ background: stat.color }" @click="handleStatClick(stat)">
|
|
|
+ <div class="stat-icon">
|
|
|
+ <component :is="stat.iconComponent" />
|
|
|
+ </div>
|
|
|
+ <div class="stat-content">
|
|
|
+ <div class="stat-value">
|
|
|
+ <span class="count-up">{{ stat.value }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="stat-label">{{ stat.label }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="stat-trend" :class="stat.trend > 0 ? 'up' : 'down'">
|
|
|
+ <el-icon>
|
|
|
+ <ArrowUp v-if="stat.trend > 0" />
|
|
|
+ <ArrowDown v-else />
|
|
|
+ </el-icon>
|
|
|
+ {{ Math.abs(stat.trend) }}%
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
|
|
|
- <el-divider />
|
|
|
+ <!-- 主要内容区域 -->
|
|
|
+ <el-row :gutter="20" class="main-content">
|
|
|
+ <!-- 系统功能模块 -->
|
|
|
+ <el-col :xs="24" :lg="16">
|
|
|
+ <el-card class="feature-card">
|
|
|
+ <template #header>
|
|
|
+ <div class="card-header">
|
|
|
+ <h3>
|
|
|
+ <el-icon><Menu /></el-icon>
|
|
|
+ 系统功能模块
|
|
|
+ </h3>
|
|
|
+ <el-button type="text" @click="viewAllModules">
|
|
|
+ 查看全部
|
|
|
+ <el-icon><ArrowRight /></el-icon>
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <el-row :gutter="15">
|
|
|
+ <el-col :xs="12" :sm="8" :md="6" v-for="(module, index) in systemModules" :key="index">
|
|
|
+ <div class="module-item" @click="handleModuleClick(module)">
|
|
|
+ <div class="module-icon" :style="{ background: module.color }">
|
|
|
+ <component :is="module.iconComponent" />
|
|
|
+ </div>
|
|
|
+ <div class="module-name">{{ module.name }}</div>
|
|
|
+ <div class="module-desc">{{ module.desc }}</div>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <!-- 实时监控数据 -->
|
|
|
+ <!-- 实时监控数据 -->
|
|
|
+ <el-card class="monitor-card">
|
|
|
+ <template #header>
|
|
|
+ <div class="card-header">
|
|
|
+ <h3>
|
|
|
+ <el-icon><DataLine /></el-icon>
|
|
|
+ 实时监控数据
|
|
|
+ </h3>
|
|
|
+ <el-button-group>
|
|
|
+ <el-button size="small" :type="chartType === 'line' ? 'primary' : ''" @click="switchChart('line')">
|
|
|
+ 折线图
|
|
|
+ </el-button>
|
|
|
+ <el-button size="small" :type="chartType === 'bar' ? 'primary' : ''" @click="switchChart('bar')">
|
|
|
+ 柱状图
|
|
|
+ </el-button>
|
|
|
+ </el-button-group>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <div class="chart-container" @click="viewDetailChart">
|
|
|
+ <!-- 模拟图表 -->
|
|
|
+ <div class="mock-chart">
|
|
|
+ <div class="chart-bars" v-if="chartType === 'bar'">
|
|
|
+ <div
|
|
|
+ v-for="(bar, index) in mockChartData"
|
|
|
+ :key="index"
|
|
|
+ class="bar"
|
|
|
+ :style="{ height: bar + '%', animationDelay: index * 0.1 + 's' }"
|
|
|
+ @click.stop="showDataDetail(index, bar)"
|
|
|
+ >
|
|
|
+ <span class="bar-value">{{ bar }}%</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="chart-lines" v-else>
|
|
|
+ <!-- 简单的折线图模拟 -->
|
|
|
+ <svg width="100%" height="100%" :viewBox="`0 0 ${chartWidth} ${chartHeight}`" preserveAspectRatio="none">
|
|
|
+ <!-- 网格线 -->
|
|
|
+ <g class="grid">
|
|
|
+ <!-- 水平网格线 -->
|
|
|
+ <line v-for="i in 5" :key="'h-' + i"
|
|
|
+ :x1="0"
|
|
|
+ :y1="(chartHeight / 5) * i"
|
|
|
+ :x2="chartWidth"
|
|
|
+ :y2="(chartHeight / 5) * i"
|
|
|
+ stroke="#e0e0e0"
|
|
|
+ stroke-width="1" />
|
|
|
+ <!-- 垂直网格线 -->
|
|
|
+ <line v-for="(label, index) in chartLabels" :key="'v-' + index"
|
|
|
+ :x1="getXPosition(index)"
|
|
|
+ :y1="0"
|
|
|
+ :x2="getXPosition(index)"
|
|
|
+ :y2="chartHeight"
|
|
|
+ stroke="#e0e0e0"
|
|
|
+ stroke-width="1" />
|
|
|
+ </g>
|
|
|
|
|
|
- <!-- 其他内容 -->
|
|
|
- <el-row :gutter="20">
|
|
|
- <el-col :span="24">
|
|
|
- <h2>更新日志</h2>
|
|
|
- <ol class="update-log">
|
|
|
- <li v-for="(log, index) in updateLogs" :key="index">{{ log }}</li>
|
|
|
- </ol>
|
|
|
+ <!-- 折线 -->
|
|
|
+ <polyline
|
|
|
+ :points="linePoints"
|
|
|
+ fill="none"
|
|
|
+ stroke="#409EFF"
|
|
|
+ stroke-width="3"
|
|
|
+ stroke-linejoin="round"
|
|
|
+ stroke-linecap="round"
|
|
|
+ />
|
|
|
+
|
|
|
+ <!-- 数据点 -->
|
|
|
+ <g>
|
|
|
+ <circle
|
|
|
+ v-for="(point, index) in linePointsArray"
|
|
|
+ :key="index"
|
|
|
+ :cx="point.x"
|
|
|
+ :cy="point.y"
|
|
|
+ r="5"
|
|
|
+ fill="#409EFF"
|
|
|
+ stroke="white"
|
|
|
+ stroke-width="2"
|
|
|
+ @click.stop="showDataDetail(index, mockChartData[index])"
|
|
|
+ style="cursor: pointer;"
|
|
|
+ class="data-point"
|
|
|
+ >
|
|
|
+ <title>{{ chartLabels[index] }}: {{ mockChartData[index] }}%</title>
|
|
|
+ </circle>
|
|
|
+ </g>
|
|
|
+
|
|
|
+ <!-- 数值标签 -->
|
|
|
+ <g>
|
|
|
+ <text
|
|
|
+ v-for="(point, index) in linePointsArray"
|
|
|
+ :key="'text-' + index"
|
|
|
+ :x="point.x"
|
|
|
+ :y="point.y - 10"
|
|
|
+ text-anchor="middle"
|
|
|
+ font-size="12"
|
|
|
+ fill="#606266"
|
|
|
+ class="value-label"
|
|
|
+ >
|
|
|
+ {{ mockChartData[index] }}%
|
|
|
+ </text>
|
|
|
+ </g>
|
|
|
+ </svg>
|
|
|
+ </div>
|
|
|
+ <div class="chart-labels">
|
|
|
+ <span v-for="(label, index) in chartLabels" :key="index" @click="showLabelDetail(label)">{{ label }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+
|
|
|
+ <!-- 右侧信息栏 -->
|
|
|
+ <el-col :xs="24" :lg="8">
|
|
|
+ <!-- 技术栈 -->
|
|
|
+ <el-card class="tech-card">
|
|
|
+ <template #header>
|
|
|
+ <div class="card-header">
|
|
|
+ <h3>
|
|
|
+ <el-icon><Cpu /></el-icon>
|
|
|
+ 技术架构
|
|
|
+ </h3>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <el-tabs v-model="activeTech" class="tech-tabs">
|
|
|
+ <el-tab-pane label="后端技术" name="backend">
|
|
|
+ <div class="tech-list">
|
|
|
+ <div class="tech-item" v-for="(tech, index) in backendTech" :key="index" @click="showTechDetail(tech)">
|
|
|
+ <el-tag :type="tech.type" effect="plain">{{ tech.name }}</el-tag>
|
|
|
+ <span class="tech-version">{{ tech.version }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-tab-pane>
|
|
|
+ <el-tab-pane label="前端技术" name="frontend">
|
|
|
+ <div class="tech-list">
|
|
|
+ <div class="tech-item" v-for="(tech, index) in frontendTech" :key="index" @click="showTechDetail(tech)">
|
|
|
+ <el-tag :type="tech.type" effect="plain">{{ tech.name }}</el-tag>
|
|
|
+ <span class="tech-version">{{ tech.version }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-tab-pane>
|
|
|
+ </el-tabs>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <!-- 系统公告 -->
|
|
|
+ <el-card class="notice-card">
|
|
|
+ <template #header>
|
|
|
+ <div class="card-header">
|
|
|
+ <h3>
|
|
|
+ <el-icon><Bell /></el-icon>
|
|
|
+ 系统公告
|
|
|
+<!-- <el-badge v-if="unreadCount > 0" :value="unreadCount" :max="99" class="notice-badge" />-->
|
|
|
+ </h3>
|
|
|
+ <el-link type="primary" @click="viewAllNotices">更多</el-link>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <el-skeleton :loading="noticeLoading" animated :count="3">
|
|
|
+ <template #template>
|
|
|
+ <el-skeleton-item variant="h3" style="width: 50%; margin-bottom: 10px;" />
|
|
|
+ <el-skeleton-item variant="text" style="margin-bottom: 5px;" />
|
|
|
+ <el-skeleton-item variant="text" style="width: 80%; margin-bottom: 20px;" />
|
|
|
+ </template>
|
|
|
+ <template #default>
|
|
|
+ <el-timeline v-if="notices.length > 0">
|
|
|
+ <el-timeline-item
|
|
|
+ v-for="(notice, index) in notices"
|
|
|
+ :key="notice.notice_id"
|
|
|
+ :timestamp="parseTime(notice.create_time)"
|
|
|
+ :type="getNoticeTimelineType(notice.notice_type)"
|
|
|
+ placement="top">
|
|
|
+ <div class="notice-content"><!-- @click="viewNoticeDetail(notice)-->
|
|
|
+ <h4>
|
|
|
+ {{ notice.notice_title }}
|
|
|
+<!-- <el-tag v-if="!notice.is_read" type="danger" size="small" effect="plain">未读</el-tag>-->
|
|
|
+ </h4>
|
|
|
+ <p>{{ getNoticePreview(notice.notice_content) }}</p>
|
|
|
+ <div class="notice-meta">
|
|
|
+ <span class="notice-type">
|
|
|
+ <dict-tag :options="sys_notice_type" :value="notice.notice_type" />
|
|
|
+ </span>
|
|
|
+ <span class="notice-author">{{ notice.create_by }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-timeline-item>
|
|
|
+ </el-timeline>
|
|
|
+ <el-empty v-else description="暂无公告" />
|
|
|
+ </template>
|
|
|
+ </el-skeleton>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <!-- 快捷操作 -->
|
|
|
+ <el-card class="quick-action-card">
|
|
|
+ <template #header>
|
|
|
+ <div class="card-header">
|
|
|
+ <h3>
|
|
|
+ <el-icon><Lightning /></el-icon>
|
|
|
+ 快捷操作
|
|
|
+ </h3>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <div class="quick-actions">
|
|
|
+ <el-button
|
|
|
+ v-for="(action, index) in quickActions"
|
|
|
+ :key="index"
|
|
|
+ :type="action.type"
|
|
|
+ @click="handleQuickAction(action)">
|
|
|
+ <el-icon>
|
|
|
+ <component :is="action.iconComponent" />
|
|
|
+ </el-icon>
|
|
|
+ {{ action.name }}
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
-<script setup name="Index">
|
|
|
-import { ref } from 'vue';
|
|
|
+<script setup name="Dashboard">
|
|
|
+import {
|
|
|
+ Monitor,
|
|
|
+ Connection,
|
|
|
+ Lightning,
|
|
|
+ Warning,
|
|
|
+ Menu,
|
|
|
+ ArrowRight,
|
|
|
+ DataLine,
|
|
|
+ Cpu,
|
|
|
+ Bell,
|
|
|
+ ArrowUp,
|
|
|
+ ArrowDown,
|
|
|
+ VideoCamera,
|
|
|
+ Search,
|
|
|
+ Document,
|
|
|
+ Setting,
|
|
|
+ Download,
|
|
|
+ PartlyCloudy
|
|
|
+} from '@element-plus/icons-vue';
|
|
|
+// 导入必要的API和工具
|
|
|
+import { ref, reactive, onMounted, onUnmounted, shallowRef, computed } from 'vue';
|
|
|
+import { useRouter } from 'vue-router';
|
|
|
+import { ElMessage, ElMessageBox, ElNotification } from 'element-plus';
|
|
|
+import { getNotice, show as markNoticeAsRead } from "@/api/system/notice";
|
|
|
+import {listNoticeRead} from "@/api/system/menu";
|
|
|
+import { connectToWebSocket } from "@/layout/components/websocket";
|
|
|
|
|
|
+// 获取字典数据
|
|
|
+const { proxy } = getCurrentInstance();
|
|
|
+const { sys_notice_status, sys_notice_type } = proxy.useDict("sys_notice_status", "sys_notice_type");
|
|
|
+
|
|
|
+const router = useRouter();
|
|
|
const version = ref('3.8.9');
|
|
|
+const activeTech = ref('backend');
|
|
|
+const chartType = ref('bar');
|
|
|
|
|
|
-// 后端技术列表
|
|
|
-const backendTech = [
|
|
|
- 'SpringBoot',
|
|
|
- 'Spring Security',
|
|
|
- 'JWT',
|
|
|
- 'MyBatis',
|
|
|
- 'Druid',
|
|
|
- 'Fastjson',
|
|
|
- '...'
|
|
|
-];
|
|
|
-
|
|
|
-// 前端技术列表
|
|
|
-const frontendTech = [
|
|
|
- 'Vue',
|
|
|
- 'Vuex',
|
|
|
- 'Element-ui',
|
|
|
- 'Axios',
|
|
|
- 'Sass',
|
|
|
- 'Quill',
|
|
|
- '...'
|
|
|
-];
|
|
|
-
|
|
|
-// 更新日志
|
|
|
-const updateLogs = [
|
|
|
- '修复了一些已知问题',
|
|
|
- '优化了性能',
|
|
|
- '新增了用户管理模块',
|
|
|
- '...'
|
|
|
-];
|
|
|
-</script>
|
|
|
+// 系统公告相关的响应式数据
|
|
|
+const notices = ref([]);
|
|
|
+const noticeLoading = ref(false);
|
|
|
+const unreadCount = ref(0);
|
|
|
+const noticeDialogVisible = ref(false);
|
|
|
+const currentNotice = ref({});
|
|
|
|
|
|
-<style scoped lang="scss">
|
|
|
-.home {
|
|
|
- background: #f0f4f8;
|
|
|
- color: #333;
|
|
|
- font-family: 'Roboto', sans-serif;
|
|
|
- min-height: auto;
|
|
|
- padding: 20px;
|
|
|
+// WebSocket连接
|
|
|
+let wsConnection = null;
|
|
|
+
|
|
|
+// 查询参数
|
|
|
+const noticeQueryParams = ref({
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 5, // Dashboard只显示最新5条
|
|
|
+ orderByColumn: 'create_time',
|
|
|
+ isAsc: 'desc'
|
|
|
+});
|
|
|
|
|
|
- h2 {
|
|
|
- font-size: 26px;
|
|
|
- font-weight: bold;
|
|
|
- margin-bottom: 15px;
|
|
|
+// 获取公告列表
|
|
|
+const getNoticeList = async () => {
|
|
|
+ noticeLoading.value = true;
|
|
|
+ try {
|
|
|
+ const response = await listNoticeRead(noticeQueryParams.value);
|
|
|
+ if (response.code === 200) {
|
|
|
+ // 获取前5条公告
|
|
|
+ notices.value = response.data.slice(0, 3);
|
|
|
+ // 计算未读数量
|
|
|
+ unreadCount.value = response.data.filter(item => !item.is_read).length;
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取公告列表失败:', error);
|
|
|
+ ElMessage.error('获取公告列表失败');
|
|
|
+ } finally {
|
|
|
+ noticeLoading.value = false;
|
|
|
}
|
|
|
+};
|
|
|
+
|
|
|
+// 获取公告预览内容(去除HTML标签,限制长度)
|
|
|
+const getNoticePreview = (content) => {
|
|
|
+ if (!content) return '';
|
|
|
+ // 去除HTML标签
|
|
|
+ const text = content.replace(/<[^>]*>/g, '');
|
|
|
+ // 限制长度
|
|
|
+ return text.length > 50 ? text.substring(0, 50) + '...' : text;
|
|
|
+};
|
|
|
|
|
|
- p {
|
|
|
- font-size: 16px;
|
|
|
- margin-top: 10px;
|
|
|
+// 根据公告类型返回时间线类型
|
|
|
+const getNoticeTimelineType = (noticeType) => {
|
|
|
+ const typeMap = {
|
|
|
+ '1': 'primary', // 通知
|
|
|
+ '2': 'success', // 公告
|
|
|
+ '3': 'warning', // 提醒
|
|
|
+ '4': 'danger' // 紧急
|
|
|
+ };
|
|
|
+ return typeMap[noticeType] || 'info';
|
|
|
+};
|
|
|
|
|
|
- b {
|
|
|
- color: #409eff;
|
|
|
- font-weight: bold;
|
|
|
+// 查看公告详情
|
|
|
+/*const viewNoticeDetail = async (notice) => {
|
|
|
+ try {
|
|
|
+ const response = await getNotice(notice.notice_id);
|
|
|
+ if (response.code === 200) {
|
|
|
+ currentNotice.value = response.data;
|
|
|
+
|
|
|
+ ElMessageBox.alert(
|
|
|
+ `<div style="text-align: left;">
|
|
|
+ <p><strong>公告标题:</strong>${response.data.noticeTitle}</p>
|
|
|
+ <p><strong>公告类型:</strong>${getNoticeTypeLabel(response.data.noticeType)}</p>
|
|
|
+ <p><strong>发布人:</strong>${response.data.createBy}</p>
|
|
|
+ <p><strong>发布时间:</strong>${response.data.createTime}</p>
|
|
|
+ <p><strong>公告内容:</strong></p>
|
|
|
+ <div style="margin-top: 10px; padding: 10px; background: #f5f7fa; border-radius: 4px;">
|
|
|
+ ${response.data.noticeContent}
|
|
|
+ </div>
|
|
|
+ </div>`,
|
|
|
+ '公告详情',
|
|
|
+ {
|
|
|
+ dangerouslyUseHTMLString: true,
|
|
|
+ confirmButtonText: '已读',
|
|
|
+ customClass: 'notice-detail-dialog',
|
|
|
+ beforeClose: async (action, instance, done) => {
|
|
|
+ if (action === 'confirm' && !notice.is_read) {
|
|
|
+ await markAsRead(notice.notice_id);
|
|
|
+ }
|
|
|
+ done();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ );
|
|
|
}
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取公告详情失败:', error);
|
|
|
+ ElMessage.error('获取公告详情失败');
|
|
|
}
|
|
|
+};*/
|
|
|
|
|
|
- .el-row {
|
|
|
- margin-bottom: 20px;
|
|
|
+// 标记为已读
|
|
|
+const markAsRead = async (noticeId) => {
|
|
|
+ try {
|
|
|
+ const response = await markNoticeAsRead({ noticeId });
|
|
|
+ if (response.code === 200) {
|
|
|
+ // 刷新公告列表
|
|
|
+ await getNoticeList();
|
|
|
+ ElMessage.success('已标记为已读');
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('标记已读失败:', error);
|
|
|
}
|
|
|
+};
|
|
|
|
|
|
- .el-col {
|
|
|
- background: #ffffff;
|
|
|
- border-radius: 10px;
|
|
|
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
|
|
- padding: 20px;
|
|
|
- transition: transform 0.3s ease;
|
|
|
+// 获取公告类型标签
|
|
|
+const getNoticeTypeLabel = (type) => {
|
|
|
+ const dict = sys_notice_type.value.find(item => item.value === type);
|
|
|
+ return dict ? dict.label : type;
|
|
|
+};
|
|
|
+
|
|
|
+// 查看所有公告
|
|
|
+const viewAllNotices = () => {
|
|
|
+ router.push('/system/notice');
|
|
|
+};
|
|
|
+
|
|
|
+// WebSocket消息处理
|
|
|
+const handleWebSocketMessage = (message) => {
|
|
|
+ if (message && message !== "del" && message !== "update") {
|
|
|
+ ElNotification({
|
|
|
+ title: '新公告',
|
|
|
+ message: '您有新的公告,请注意查收',
|
|
|
+ type: 'warning',
|
|
|
+ duration: 5000,
|
|
|
+ position: 'top-right',
|
|
|
+ onClick: () => {
|
|
|
+ getNoticeList();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ // 刷新公告列表
|
|
|
+ getNoticeList();
|
|
|
+};
|
|
|
+
|
|
|
+// 其他原有的方法保持不变...
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ // 初始化时显示欢迎信息
|
|
|
+ ElMessage.success('欢迎使用IBMS智能建筑管理系统!');
|
|
|
+
|
|
|
+ // 获取公告列表
|
|
|
+ getNoticeList();
|
|
|
+
|
|
|
+ // 连接WebSocket
|
|
|
+ wsConnection = connectToWebSocket(handleWebSocketMessage);
|
|
|
+
|
|
|
+ // 其他原有的初始化代码...
|
|
|
+});
|
|
|
+
|
|
|
+onUnmounted(() => {
|
|
|
+ // 清理WebSocket连接
|
|
|
+ if (wsConnection) {
|
|
|
+ // 根据实际的WebSocket实现进行清理
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+// 图表尺寸
|
|
|
+const chartWidth = ref(600);
|
|
|
+const chartHeight = ref(250);
|
|
|
+
|
|
|
+// 获取X轴位置
|
|
|
+const getXPosition = (index) => {
|
|
|
+ const padding = 40;
|
|
|
+ const step = (chartWidth.value - 2 * padding) / (chartLabels.length - 1);
|
|
|
+ return padding + index * step;
|
|
|
+};
|
|
|
+
|
|
|
+// 计算折线图点位
|
|
|
+const linePoints = computed(() => {
|
|
|
+ const padding = 40;
|
|
|
+ const xStep = (chartWidth.value - 2 * padding) / (mockChartData.value.length - 1);
|
|
|
+ const yRange = chartHeight.value - 2 * padding;
|
|
|
+
|
|
|
+ return mockChartData.value.map((value, index) => {
|
|
|
+ const x = padding + index * xStep;
|
|
|
+ const y = chartHeight.value - padding - (value / 100) * yRange;
|
|
|
+ return `${x},${y}`;
|
|
|
+ }).join(' ');
|
|
|
+});
|
|
|
+
|
|
|
+const linePointsArray = computed(() => {
|
|
|
+ const padding = 40;
|
|
|
+ const xStep = (chartWidth.value - 2 * padding) / (mockChartData.value.length - 1);
|
|
|
+ const yRange = chartHeight.value - 2 * padding;
|
|
|
+
|
|
|
+ return mockChartData.value.map((value, index) => ({
|
|
|
+ x: padding + index * xStep,
|
|
|
+ y: chartHeight.value - padding - (value / 100) * yRange
|
|
|
+ }));
|
|
|
+});
|
|
|
+
|
|
|
+// 模拟图表数据
|
|
|
+const mockChartData = ref([65, 78, 90, 70, 85, 60, 75]);
|
|
|
+const chartLabels = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
|
|
|
+
|
|
|
+// 统计数据
|
|
|
+const statistics = reactive([
|
|
|
+ {
|
|
|
+ label: '设备总数',
|
|
|
+ value: '1,234',
|
|
|
+ iconComponent: shallowRef(Monitor),
|
|
|
+ color: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
|
|
+ trend: 12.5,
|
|
|
+ path: '/device/list'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: '在线设备',
|
|
|
+ value: '1,180',
|
|
|
+ iconComponent: shallowRef(Connection),
|
|
|
+ color: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
|
|
|
+ trend: 8.3,
|
|
|
+ path: '/device/online'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: '今日能耗',
|
|
|
+ value: '2,845',
|
|
|
+ iconComponent: shallowRef(Lightning),
|
|
|
+ color: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)',
|
|
|
+ trend: -5.2,
|
|
|
+ path: '/energy/today'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: '告警数量',
|
|
|
+ value: '12',
|
|
|
+ iconComponent: shallowRef(Warning),
|
|
|
+ color: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)',
|
|
|
+ trend: -15.8,
|
|
|
+ path: '/alarm/list'
|
|
|
+ }
|
|
|
+]);
|
|
|
+
|
|
|
+// 系统模块
|
|
|
+const systemModules = reactive([
|
|
|
+ {
|
|
|
+ name: '能源管理',
|
|
|
+ desc: '能耗监控分析',
|
|
|
+ iconComponent: shallowRef(Lightning),
|
|
|
+ color: '#409EFF',
|
|
|
+ path: '/energy'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '设备监控',
|
|
|
+ desc: '设备状态管理',
|
|
|
+ iconComponent: shallowRef(Monitor),
|
|
|
+ color: '#67C23A',
|
|
|
+ path: '/device'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '环境监测',
|
|
|
+ desc: '环境参数监控',
|
|
|
+ iconComponent: shallowRef(PartlyCloudy),
|
|
|
+ color: '#E6A23C',
|
|
|
+ path: '/environment'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '安防系统',
|
|
|
+ desc: '安全防护管理',
|
|
|
+ iconComponent: shallowRef(VideoCamera),
|
|
|
+ color: '#F56C6C',
|
|
|
+ path: '/security'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '照明控制',
|
|
|
+ desc: '智能照明管理',
|
|
|
+ iconComponent: shallowRef(Lightning),
|
|
|
+ color: '#909399',
|
|
|
+ path: '/lighting'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '报警管理',
|
|
|
+ desc: '告警处理中心',
|
|
|
+ iconComponent: shallowRef(Bell),
|
|
|
+ color: '#E6A23C',
|
|
|
+ path: '/alarm'
|
|
|
+ }
|
|
|
+]);
|
|
|
+
|
|
|
+// 技术栈数据
|
|
|
+const backendTech = reactive([
|
|
|
+ { name: 'Spring Boot', version: '2.7.x', type: 'primary', desc: '基础框架' },
|
|
|
+ { name: 'Spring Security', version: '5.7.x', type: 'success', desc: '安全框架' },
|
|
|
+ { name: 'JWT', version: '0.11.x', type: 'info', desc: '认证方案' },
|
|
|
+ { name: 'MyBatis Plus', version: '3.5.x', type: 'warning', desc: 'ORM框架' },
|
|
|
+ { name: 'Redis', version: '6.2.x', type: 'danger', desc: '缓存中间件' },
|
|
|
+ { name: 'MySQL', version: '8.0.x', type: 'primary', desc: '关系型数据库' },
|
|
|
+ { name: 'RabbitMQ', version: '3.9.x', type: 'success', desc: '消息队列' }
|
|
|
+]);
|
|
|
+
|
|
|
+const frontendTech = reactive([
|
|
|
+ { name: 'Vue 3', version: '3.3.x', type: 'primary', desc: '前端框架' },
|
|
|
+ { name: 'Vite', version: '4.4.x', type: 'success', desc: '构建工具' },
|
|
|
+ { name: 'Element Plus', version: '2.3.x', type: 'info', desc: 'UI组件库' },
|
|
|
+ { name: 'Pinia', version: '2.1.x', type: 'warning', desc: '状态管理' },
|
|
|
+ { name: 'Axios', version: '1.4.x', type: 'danger', desc: 'HTTP客户端' },
|
|
|
+ { name: 'ECharts', version: '5.4.x', type: 'primary', desc: '图表库' },
|
|
|
+ { name: 'TypeScript', version: '5.1.x', type: 'success', desc: '类型系统' }
|
|
|
+]);
|
|
|
+
|
|
|
+
|
|
|
+// 快捷操作
|
|
|
+const quickActions = reactive([
|
|
|
+ {
|
|
|
+ name: '设备巡检',
|
|
|
+ iconComponent: shallowRef(Search),
|
|
|
+ type: 'primary',
|
|
|
+ action: 'inspection'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '生成报表',
|
|
|
+ iconComponent: shallowRef(Document),
|
|
|
+ type: 'success',
|
|
|
+ action: 'report'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '系统设置',
|
|
|
+ iconComponent: shallowRef(Setting),
|
|
|
+ type: 'info',
|
|
|
+ action: 'settings'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '数据备份',
|
|
|
+ iconComponent: shallowRef(Download),
|
|
|
+ type: 'warning',
|
|
|
+ action: 'backup'
|
|
|
+ }
|
|
|
+]);
|
|
|
+
|
|
|
+// 显示版本信息
|
|
|
+const showVersionInfo = () => {
|
|
|
+ ElMessageBox.alert(
|
|
|
+ `<div style="text-align: left;">
|
|
|
+ <p><strong>当前版本:</strong>v${version.value}</p>
|
|
|
+ <p><strong>发布日期:</strong>2024-01-15</p>
|
|
|
+ <p><strong>更新内容:</strong></p>
|
|
|
+ <ul style="margin-left: 20px;">
|
|
|
+ <li>优化系统性能</li>
|
|
|
+ <li>新增能源分析报表</li>
|
|
|
+ <li>修复已知问题</li>
|
|
|
+ </ul>
|
|
|
+ </div>`,
|
|
|
+ '版本信息',
|
|
|
+ {
|
|
|
+ dangerouslyUseHTMLString: true,
|
|
|
+ confirmButtonText: '确定'
|
|
|
+ }
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+// 显示系统状态
|
|
|
+const showSystemStatus = () => {
|
|
|
+ ElMessageBox.alert(
|
|
|
+ `<div style="text-align: left;">
|
|
|
+ <p><strong>系统状态:</strong><span style="color: #67C23A;">运行正常</span></p>
|
|
|
+ <p><strong>运行时长:</strong>15天 8小时 32分钟</p>
|
|
|
+ <p><strong>CPU使用率:</strong>35%</p>
|
|
|
+ <p><strong>内存使用率:</strong>62%</p>
|
|
|
+ <p><strong>磁盘使用率:</strong>48%</p>
|
|
|
+ </div>`,
|
|
|
+ '系统状态',
|
|
|
+ {
|
|
|
+ dangerouslyUseHTMLString: true,
|
|
|
+ confirmButtonText: '确定'
|
|
|
+ }
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+// 处理统计卡片点击
|
|
|
+const handleStatClick = (stat) => {
|
|
|
+ ElMessage.info(`正在查看${stat.label}详情...`);
|
|
|
+ // 实际项目中这里应该跳转到对应页面
|
|
|
+ // router.push(stat.path);
|
|
|
+};
|
|
|
+
|
|
|
+// 查看所有模块
|
|
|
+const viewAllModules = () => {
|
|
|
+ ElMessage.success('正在加载所有系统模块...');
|
|
|
+ // router.push('/modules');
|
|
|
+};
|
|
|
+
|
|
|
+// 处理模块点击
|
|
|
+const handleModuleClick = (module) => {
|
|
|
+ ElMessage.success(`正在进入${module.name}模块`);
|
|
|
+ // 实际项目中这里应该跳转到对应页面
|
|
|
+ // router.push(module.path);
|
|
|
+};
|
|
|
+
|
|
|
+// 切换图表类型
|
|
|
+const switchChart = (type) => {
|
|
|
+ chartType.value = type;
|
|
|
+ ElMessage.success(`已切换为${type === 'line' ? '折线图' : '柱状图'}`);
|
|
|
+ // 这里可以重新加载图表数据
|
|
|
+ refreshChartData();
|
|
|
+};
|
|
|
+
|
|
|
+// 刷新图表数据
|
|
|
+const refreshChartData = () => {
|
|
|
+ // 模拟数据刷新
|
|
|
+ mockChartData.value = mockChartData.value.map(() =>
|
|
|
+ Math.floor(Math.random() * 40) + 60
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+// 查看详细图表
|
|
|
+const viewDetailChart = () => {
|
|
|
+ ElMessage.info('正在打开详细数据分析页面...');
|
|
|
+ // router.push('/analysis/chart');
|
|
|
+};
|
|
|
+
|
|
|
+// 显示数据详情
|
|
|
+const showDataDetail = (index, value) => {
|
|
|
+ ElMessageBox.alert(
|
|
|
+ `<div style="text-align: left;">
|
|
|
+ <p><strong>日期:</strong>${chartLabels[index]}</p>
|
|
|
+ <p><strong>数值:</strong>${value}%</p>
|
|
|
+ <p><strong>环比:</strong>${index > 0 ? ((value - mockChartData.value[index - 1]) > 0 ? '+' : '') + (value - mockChartData.value[index - 1]).toFixed(1) + '%' : '无'}</p>
|
|
|
+ <p><strong>状态:</strong>${value > 80 ? '<span style="color: #F56C6C;">偏高</span>' : value > 60 ? '<span style="color: #E6A23C;">正常</span>' : '<span style="color: #67C23A;">偏低</span>'}</p>
|
|
|
+ </div>`,
|
|
|
+ '数据详情',
|
|
|
+ {
|
|
|
+ dangerouslyUseHTMLString: true,
|
|
|
+ confirmButtonText: '确定'
|
|
|
+ }
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+// 显示标签详情
|
|
|
+const showLabelDetail = (label) => {
|
|
|
+ ElMessage.info(`查看${label}的详细数据`);
|
|
|
+};
|
|
|
+
|
|
|
+// 显示技术详情
|
|
|
+const showTechDetail = (tech) => {
|
|
|
+ ElMessageBox.alert(
|
|
|
+ `<div style="text-align: left;">
|
|
|
+ <p><strong>技术名称:</strong>${tech.name}</p>
|
|
|
+ <p><strong>当前版本:</strong>${tech.version}</p>
|
|
|
+ <p><strong>功能描述:</strong>${tech.desc}</p>
|
|
|
+ <p><strong>官方网站:</strong><a href="#" style="color: #409EFF;">查看官网</a></p>
|
|
|
+ </div>`,
|
|
|
+ '技术详情',
|
|
|
+ {
|
|
|
+ dangerouslyUseHTMLString: true,
|
|
|
+ confirmButtonText: '确定'
|
|
|
+ }
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+// 处理快捷操作
|
|
|
+const handleQuickAction = (action) => {
|
|
|
+ switch (action.action) {
|
|
|
+ case 'inspection':
|
|
|
+ ElMessageBox.confirm('是否立即开始设备巡检?', '设备巡检', {
|
|
|
+ confirmButtonText: '开始巡检',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'info'
|
|
|
+ }).then(() => {
|
|
|
+ ElMessage.success('设备巡检已开始,预计需要5分钟');
|
|
|
+ // 这里可以调用巡检API
|
|
|
+ }).catch(() => {
|
|
|
+ ElMessage.info('已取消设备巡检');
|
|
|
+ });
|
|
|
+ break;
|
|
|
+
|
|
|
+ case 'report':
|
|
|
+ ElMessageBox.prompt('请选择报表类型', '生成报表', {
|
|
|
+ confirmButtonText: '生成',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ inputPattern: /^(日报|周报|月报)$/,
|
|
|
+ inputPlaceholder: '请输入:日报、周报或月报',
|
|
|
+ inputErrorMessage: '请输入正确的报表类型'
|
|
|
+ }).then(({ value }) => {
|
|
|
+ ElMessage.success(`正在生成${value},请稍候...`);
|
|
|
+ // 模拟生成报表
|
|
|
+ setTimeout(() => {
|
|
|
+ ElMessage.success(`${value}生成成功,已保存到系统`);
|
|
|
+ }, 2000);
|
|
|
+ }).catch(() => {
|
|
|
+ ElMessage.info('已取消生成报表');
|
|
|
+ });
|
|
|
+ break;
|
|
|
+
|
|
|
+ case 'settings':
|
|
|
+ ElMessage.info('正在打开系统设置...');
|
|
|
+ // router.push('/settings');
|
|
|
+ break;
|
|
|
+
|
|
|
+ case 'backup':
|
|
|
+ ElMessageBox.confirm('确定要备份系统数据吗?', '数据备份', {
|
|
|
+ confirmButtonText: '确定备份',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ }).then(() => {
|
|
|
+ ElMessage.success('数据备份已开始,请勿关闭页面');
|
|
|
+ // 模拟备份进度
|
|
|
+ let progress = 0;
|
|
|
+ const timer = setInterval(() => {
|
|
|
+ progress += 20;
|
|
|
+ if (progress >= 100) {
|
|
|
+ clearInterval(timer);
|
|
|
+ ElMessage.success('数据备份完成!');
|
|
|
+ } else {
|
|
|
+ ElMessage.info(`备份进度:${progress}%`);
|
|
|
+ }
|
|
|
+ }, 1000);
|
|
|
+ }).catch(() => {
|
|
|
+ ElMessage.info('已取消数据备份');
|
|
|
+ });
|
|
|
+ break;
|
|
|
+ }
|
|
|
+};
|
|
|
|
|
|
- &:hover {
|
|
|
- transform: translateY(-5px);
|
|
|
- box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15);
|
|
|
+// 数字动画效果
|
|
|
+const animateValue = (obj, start, end, duration) => {
|
|
|
+ let startTimestamp = null;
|
|
|
+ const step = (timestamp) => {
|
|
|
+ if (!startTimestamp) startTimestamp = timestamp;
|
|
|
+ const progress = Math.min((timestamp - startTimestamp) / duration, 1);
|
|
|
+ obj.value = Math.floor(progress * (end - start) + start);
|
|
|
+ if (progress < 1) {
|
|
|
+ window.requestAnimationFrame(step);
|
|
|
+ }
|
|
|
+ };
|
|
|
+ window.requestAnimationFrame(step);
|
|
|
+};
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ // 初始化时显示欢迎信息
|
|
|
+ /*ElMessage.success('欢迎使用IBMS智能建筑管理系统!');*/
|
|
|
+
|
|
|
+ // 模拟数据更新
|
|
|
+ setInterval(() => {
|
|
|
+ // 随机更新一些数据
|
|
|
+ const randomIndex = Math.floor(Math.random() * mockChartData.value.length);
|
|
|
+ mockChartData.value[randomIndex] = Math.floor(Math.random() * 40) + 60;
|
|
|
+ }, 5000);
|
|
|
+});
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.dashboard-container {
|
|
|
+ padding: 20px;
|
|
|
+ background: #f5f7fa;
|
|
|
+ min-height: calc(100vh - 84px);
|
|
|
+
|
|
|
+ // 欢迎区域
|
|
|
+ .welcome-section {
|
|
|
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
+ border-radius: 12px;
|
|
|
+ padding: 40px;
|
|
|
+ margin-bottom: 30px;
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ color: white;
|
|
|
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
|
|
+ position: relative;
|
|
|
+ overflow: hidden;
|
|
|
+
|
|
|
+ &::before {
|
|
|
+ content: '';
|
|
|
+ position: absolute;
|
|
|
+ top: -50%;
|
|
|
+ right: -50%;
|
|
|
+ width: 200%;
|
|
|
+ height: 200%;
|
|
|
+ background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
|
|
|
+ animation: pulse 4s ease-in-out infinite;
|
|
|
+ }
|
|
|
+
|
|
|
+ .welcome-content {
|
|
|
+ flex: 1;
|
|
|
+ position: relative;
|
|
|
+ z-index: 1;
|
|
|
+
|
|
|
+ .welcome-title {
|
|
|
+ font-size: 36px;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ font-weight: 600;
|
|
|
+
|
|
|
+ .gradient-text {
|
|
|
+ background: linear-gradient(to right, #fff, #f0f0f0);
|
|
|
+ -webkit-background-clip: text;
|
|
|
+ -webkit-text-fill-color: transparent;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .welcome-subtitle {
|
|
|
+ font-size: 18px;
|
|
|
+ opacity: 0.9;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .version-info {
|
|
|
+ .el-tag {
|
|
|
+ margin-right: 10px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .welcome-illustration {
|
|
|
+ width: 400px;
|
|
|
+ height: 300px;
|
|
|
+ position: relative;
|
|
|
+ z-index: 1;
|
|
|
+
|
|
|
+ svg {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ filter: drop-shadow(0 4px 8px rgba(0,0,0,0.1));
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- ul {
|
|
|
- list-style-type: none;
|
|
|
- padding: 0;
|
|
|
+ // 统计卡片
|
|
|
+ .stat-cards {
|
|
|
+ margin-bottom: 30px;
|
|
|
|
|
|
- li {
|
|
|
- font-size: 14px;
|
|
|
- line-height: 24px;
|
|
|
+ .stat-card {
|
|
|
+ border-radius: 12px;
|
|
|
+ padding: 25px;
|
|
|
+ color: white;
|
|
|
position: relative;
|
|
|
- padding-left: 20px;
|
|
|
+ overflow: hidden;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+ cursor: pointer;
|
|
|
+ height: 140px;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ transform: translateY(-5px);
|
|
|
+ box-shadow: 0 15px 30px rgba(0, 0, 0, 0.15);
|
|
|
+
|
|
|
+ .stat-icon {
|
|
|
+ transform: scale(1.1) rotate(5deg);
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
&::before {
|
|
|
- content: '•';
|
|
|
+ content: '';
|
|
|
+ position: absolute;
|
|
|
+ top: -50%;
|
|
|
+ right: -50%;
|
|
|
+ width: 200%;
|
|
|
+ height: 200%;
|
|
|
+ background: radial-gradient(circle, rgba(255,255,255,0.2) 0%, transparent 70%);
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-icon {
|
|
|
position: absolute;
|
|
|
- left: 0;
|
|
|
- color: #409eff;
|
|
|
- font-size: 16px;
|
|
|
+ right: 20px;
|
|
|
+ top: 20px;
|
|
|
+ font-size: 48px;
|
|
|
+ opacity: 0.3;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-content {
|
|
|
+ position: relative;
|
|
|
+ z-index: 1;
|
|
|
+
|
|
|
+ .stat-value {
|
|
|
+ font-size: 32px;
|
|
|
+ font-weight: 600;
|
|
|
+ margin-bottom: 5px;
|
|
|
+
|
|
|
+ .count-up {
|
|
|
+ display: inline-block;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-label {
|
|
|
+ font-size: 16px;
|
|
|
+ opacity: 0.9;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-trend {
|
|
|
+ position: absolute;
|
|
|
+ bottom: 20px;
|
|
|
+ right: 20px;
|
|
|
+ font-size: 14px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 5px;
|
|
|
+ background: rgba(255,255,255,0.2);
|
|
|
+ padding: 4px 8px;
|
|
|
+ border-radius: 4px;
|
|
|
+
|
|
|
+ &.up {
|
|
|
+ color: #67C23A;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.down {
|
|
|
+ color: #F56C6C;
|
|
|
+ }
|
|
|
+
|
|
|
+ .el-icon {
|
|
|
+ font-size: 16px;
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- .update-log {
|
|
|
- ol {
|
|
|
- padding-left: 20px;
|
|
|
+ // 主要内容区域
|
|
|
+ .main-content {
|
|
|
+ // 通用卡片样式
|
|
|
+ .el-card {
|
|
|
+ border-radius: 12px;
|
|
|
+ border: none;
|
|
|
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
|
|
|
+ margin-bottom: 20px;
|
|
|
+ transition: all 0.3s ease;
|
|
|
|
|
|
- li {
|
|
|
- font-size: 14px;
|
|
|
- line-height: 24px;
|
|
|
+ &:hover {
|
|
|
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
|
|
+ }
|
|
|
+
|
|
|
+ .card-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+
|
|
|
+ h3 {
|
|
|
+ margin: 0;
|
|
|
+ font-size: 18px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+
|
|
|
+ .el-icon {
|
|
|
+ color: #409EFF;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 功能模块卡片
|
|
|
+ .feature-card {
|
|
|
+ .module-item {
|
|
|
+ text-align: center;
|
|
|
+ padding: 20px 10px;
|
|
|
+ border-radius: 8px;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+ margin-bottom: 15px;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ background: #f5f7fa;
|
|
|
+ transform: translateY(-3px);
|
|
|
+
|
|
|
+ .module-icon {
|
|
|
+ transform: scale(1.1);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .module-icon {
|
|
|
+ width: 60px;
|
|
|
+ height: 60px;
|
|
|
+ margin: 0 auto 12px;
|
|
|
+ border-radius: 12px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+ color: white;
|
|
|
+ font-size: 28px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .module-name {
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+ margin-bottom: 5px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .module-desc {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #909399;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 监控图表卡片
|
|
|
+ .monitor-card {
|
|
|
+ .chart-container {
|
|
|
+ height: 300px;
|
|
|
+ padding: 20px;
|
|
|
+
|
|
|
+ .mock-chart {
|
|
|
+ height: 100%;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ justify-content: flex-end;
|
|
|
+
|
|
|
+ .chart-bars {
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-end;
|
|
|
+ justify-content: space-around;
|
|
|
+ gap: 10px;
|
|
|
+ margin-bottom: 10px;
|
|
|
+
|
|
|
+ .bar {
|
|
|
+ flex: 1;
|
|
|
+ background: linear-gradient(to top, #409EFF, #66b1ff);
|
|
|
+ border-radius: 4px 4px 0 0;
|
|
|
+ position: relative;
|
|
|
+ min-height: 20px;
|
|
|
+ animation: growBar 1s ease-out forwards;
|
|
|
+ transform-origin: bottom;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ opacity: 0.8;
|
|
|
+ }
|
|
|
+
|
|
|
+ .bar-value {
|
|
|
+ position: absolute;
|
|
|
+ top: -25px;
|
|
|
+ left: 50%;
|
|
|
+ transform: translateX(-50%);
|
|
|
+ font-size: 12px;
|
|
|
+ color: #606266;
|
|
|
+ white-space: nowrap;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .chart-labels {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-around;
|
|
|
+ color: #909399;
|
|
|
+ font-size: 12px;
|
|
|
+ padding-top: 10px;
|
|
|
+ border-top: 1px solid #EBEEF5;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 技术栈卡片
|
|
|
+ .tech-card {
|
|
|
+ .tech-tabs {
|
|
|
+ .tech-list {
|
|
|
+ .tech-item {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ padding: 8px 0;
|
|
|
+ border-bottom: 1px solid #f0f0f0;
|
|
|
+
|
|
|
+ &:last-child {
|
|
|
+ border-bottom: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ .el-tag {
|
|
|
+ min-width: 120px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .tech-version {
|
|
|
+ color: #909399;
|
|
|
+ font-size: 13px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 公告卡片
|
|
|
+ .notice-card {
|
|
|
+ :deep(.el-timeline) {
|
|
|
+ padding-left: 0;
|
|
|
+
|
|
|
+ .el-timeline-item__wrapper {
|
|
|
+ padding-left: 28px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .el-timeline-item__timestamp {
|
|
|
+ color: #909399;
|
|
|
+ font-size: 13px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .notice-content {
|
|
|
+ h4 {
|
|
|
+ margin: 0 0 5px 0;
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+ }
|
|
|
+
|
|
|
+ p {
|
|
|
+ margin: 0;
|
|
|
+ font-size: 13px;
|
|
|
+ color: #606266;
|
|
|
+ line-height: 1.5;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 快捷操作卡片
|
|
|
+ .quick-action-card {
|
|
|
+ .quick-actions {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(2, 1fr);
|
|
|
+ gap: 10px;
|
|
|
+
|
|
|
+ .el-button {
|
|
|
+ width: 100%;
|
|
|
+ height: 40px;
|
|
|
+ border-radius: 8px;
|
|
|
+ font-size: 14px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 响应式设计
|
|
|
+ @media (max-width: 768px) {
|
|
|
+ padding: 10px;
|
|
|
+
|
|
|
+ .welcome-section {
|
|
|
+ flex-direction: column;
|
|
|
+ text-align: center;
|
|
|
+ padding: 30px 20px;
|
|
|
+
|
|
|
+ .welcome-content {
|
|
|
+ margin-bottom: 20px;
|
|
|
+
|
|
|
+ .welcome-title {
|
|
|
+ font-size: 28px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .welcome-subtitle {
|
|
|
+ font-size: 16px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .welcome-illustration {
|
|
|
+ width: 100%;
|
|
|
+ max-width: 300px;
|
|
|
+ height: 200px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-cards {
|
|
|
+ .stat-card {
|
|
|
+ margin-bottom: 15px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .main-content {
|
|
|
+ .feature-card {
|
|
|
+ .module-item {
|
|
|
+ padding: 15px 5px;
|
|
|
+
|
|
|
+ .module-icon {
|
|
|
+ width: 50px;
|
|
|
+ height: 50px;
|
|
|
+ font-size: 24px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .module-name {
|
|
|
+ font-size: 14px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .module-desc {
|
|
|
+ font-size: 12px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .quick-action-card {
|
|
|
+ .quick-actions {
|
|
|
+ grid-template-columns: 1fr;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 动画效果
|
|
|
+@keyframes fadeInUp {
|
|
|
+ from {
|
|
|
+ opacity: 0;
|
|
|
+ transform: translateY(20px);
|
|
|
+ }
|
|
|
+ to {
|
|
|
+ opacity: 1;
|
|
|
+ transform: translateY(0);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes pulse {
|
|
|
+ 0%, 100% {
|
|
|
+ transform: scale(1);
|
|
|
+ }
|
|
|
+ 50% {
|
|
|
+ transform: scale(1.1);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes growBar {
|
|
|
+ from {
|
|
|
+ transform: scaleY(0);
|
|
|
+ }
|
|
|
+ to {
|
|
|
+ transform: scaleY(1);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.stat-card,
|
|
|
+.el-card {
|
|
|
+ animation: fadeInUp 0.6s ease-out;
|
|
|
+}
|
|
|
+
|
|
|
+// 延迟动画
|
|
|
+.stat-cards .el-col:nth-child(1) .stat-card { animation-delay: 0.1s; }
|
|
|
+.stat-cards .el-col:nth-child(2) .stat-card { animation-delay: 0.2s; }
|
|
|
+.stat-cards .el-col:nth-child(3) .stat-card { animation-delay: 0.3s; }
|
|
|
+.stat-cards .el-col:nth-child(4) .stat-card { animation-delay: 0.4s; }
|
|
|
+.notice-card {
|
|
|
+ .card-header {
|
|
|
+ .notice-badge {
|
|
|
+ margin-left: 10px;
|
|
|
+
|
|
|
+ :deep(.el-badge__content) {
|
|
|
+ height: 18px;
|
|
|
+ line-height: 18px;
|
|
|
+ padding: 0 6px;
|
|
|
+ font-size: 12px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.el-timeline) {
|
|
|
+ .notice-content {
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.3s;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ background: #f5f7fa;
|
|
|
+ padding: 8px;
|
|
|
+ border-radius: 4px;
|
|
|
+ margin: -8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ h4 {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+
|
|
|
+ .el-tag {
|
|
|
+ margin-left: auto;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .notice-meta {
|
|
|
+ margin-top: 8px;
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #909399;
|
|
|
+
|
|
|
+ .notice-type {
|
|
|
+ :deep(.el-tag) {
|
|
|
+ height: 20px;
|
|
|
+ line-height: 18px;
|
|
|
+ padding: 0 8px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.el-empty) {
|
|
|
+ padding: 20px 0;
|
|
|
+
|
|
|
+ .el-empty__description {
|
|
|
+ margin-top: 10px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 公告详情弹窗样式
|
|
|
+:global(.notice-detail-dialog) {
|
|
|
+ .el-message-box__content {
|
|
|
+ max-height: 60vh;
|
|
|
+ overflow-y: auto;
|
|
|
+ }
|
|
|
+}
|
|
|
+// 监控图表卡片
|
|
|
+.monitor-card {
|
|
|
+ .chart-container {
|
|
|
+ height: 350px;
|
|
|
+ padding: 20px;
|
|
|
+ cursor: pointer;
|
|
|
+
|
|
|
+ .mock-chart {
|
|
|
+ height: 100%;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+
|
|
|
+ .chart-bars {
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-end;
|
|
|
+ justify-content: space-around;
|
|
|
+ gap: 10px;
|
|
|
+ margin-bottom: 10px;
|
|
|
+
|
|
|
+ .bar {
|
|
|
+ flex: 1;
|
|
|
+ background: linear-gradient(to top, #409EFF, #66b1ff);
|
|
|
+ border-radius: 4px 4px 0 0;
|
|
|
+ position: relative;
|
|
|
+ min-height: 20px;
|
|
|
+ animation: growBar 1s ease-out forwards;
|
|
|
+ transform-origin: bottom;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ opacity: 0.8;
|
|
|
+ transform: translateY(-2px);
|
|
|
+ }
|
|
|
+
|
|
|
+ .bar-value {
|
|
|
+ position: absolute;
|
|
|
+ top: -25px;
|
|
|
+ left: 50%;
|
|
|
+ transform: translateX(-50%);
|
|
|
+ font-size: 12px;
|
|
|
+ color: #606266;
|
|
|
+ white-space: nowrap;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .chart-lines {
|
|
|
+ flex: 1;
|
|
|
position: relative;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ background: #fafafa;
|
|
|
+ border-radius: 4px;
|
|
|
+ overflow: hidden;
|
|
|
+
|
|
|
+ svg {
|
|
|
+ display: block;
|
|
|
+
|
|
|
+ .grid {
|
|
|
+ opacity: 0.5;
|
|
|
+ }
|
|
|
|
|
|
- &::before {
|
|
|
- content: counter(list-item) '.';
|
|
|
- position: absolute;
|
|
|
- left: -20px;
|
|
|
- color: #409eff;
|
|
|
- font-weight: bold;
|
|
|
+ .data-point {
|
|
|
+ transition: all 0.3s ease;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ r: 7;
|
|
|
+ fill: #66b1ff;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .value-label {
|
|
|
+ opacity: 0;
|
|
|
+ transition: opacity 0.3s ease;
|
|
|
+ }
|
|
|
+
|
|
|
+ &:hover .value-label {
|
|
|
+ opacity: 1;
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ .chart-labels {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-around;
|
|
|
+ color: #909399;
|
|
|
+ font-size: 12px;
|
|
|
+ padding-top: 10px;
|
|
|
+ border-top: 1px solid #EBEEF5;
|
|
|
+
|
|
|
+ span {
|
|
|
+ cursor: pointer;
|
|
|
+ transition: color 0.3s ease;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ color: #409EFF;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 添加折线动画
|
|
|
+@keyframes drawLine {
|
|
|
+ to {
|
|
|
+ stroke-dashoffset: 0;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.chart-lines {
|
|
|
+ polyline {
|
|
|
+ stroke-dasharray: 1000;
|
|
|
+ stroke-dashoffset: 1000;
|
|
|
+ animation: drawLine 2s ease-out forwards;
|
|
|
+ }
|
|
|
+
|
|
|
+ .data-point {
|
|
|
+ opacity: 0;
|
|
|
+ animation: fadeIn 0.5s ease-out forwards;
|
|
|
+
|
|
|
+ @for $i from 1 through 7 {
|
|
|
+ &:nth-child(#{$i}) {
|
|
|
+ animation-delay: #{$i * 0.1}s;
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
+}
|
|
|
|
|
|
- .el-divider {
|
|
|
- background-color: #ddd;
|
|
|
- margin: 30px 0;
|
|
|
+@keyframes fadeIn {
|
|
|
+ to {
|
|
|
+ opacity: 1;
|
|
|
}
|
|
|
}
|
|
|
</style>
|