├── public ├── favicon.ico ├── apple-touch-icon.png └── logo.svg ├── postcss.config.js ├── src ├── main.js ├── style.css ├── utils │ ├── api.js │ ├── monitor.js │ └── chartConfig.js ├── App.vue └── components │ ├── Header.vue │ ├── Stats.vue │ ├── Footer.vue │ └── Card.vue ├── vite.config.js ├── .env ├── .gitignore ├── tailwind.config.js ├── functions └── api │ ├── README.md │ └── status.js ├── api ├── README.md └── status.js ├── package.json ├── LICENSE ├── index.html └── README.md /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JLinMr/Uptime-Status/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JLinMr/Uptime-Status/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import './style.css' 4 | 5 | const app = createApp(App) 6 | app.mount('#app') 7 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import path from 'path' 4 | 5 | export default defineConfig({ 6 | plugins: [vue()], 7 | resolve: { 8 | alias: { 9 | '@': path.resolve(__dirname, './src') 10 | } 11 | }, 12 | server: { 13 | port: 3100, 14 | open: true 15 | } 16 | }) -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # UptimeRobot API Key 2 | VITE_UPTIMEROBOT_API_KEY = "ur2290572-af4663a4e3f83be26119abbe" 3 | 4 | # UptimeRobot API URL 5 | # 除腾讯云 EdgeOne Pages 、vercel 、cloudflare pages 外 6 | ## 其它部署方式需要自行搭建 API 代理 7 | ## 代理地址 https://api.uptimerobot.com/v2/getMonitors 8 | VITE_UPTIMEROBOT_API_URL = "/api/status" 9 | 10 | # 站点名称 11 | VITE_APP_TITLE = "梦爱吃鱼" 12 | 13 | # 监控面板排序方式 14 | # 支持 friendly_name 和 create_datetime 两种方式 15 | VITE_UPTIMEROBOT_STATUS_SORT = "friendly_name" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | # 环境文件 27 | .env.local 28 | .env.*.local 29 | 30 | # 保留生产环境配置 31 | !.env.production 32 | 33 | package-lock.json 34 | pnpm-lock.yaml 35 | yarn.lock -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{vue,js,ts,jsx,tsx}", 6 | ], 7 | darkMode: 'class', 8 | theme: { 9 | extend: { 10 | animation: { 11 | 'fade': 'fade 0.3s ease-in-out', 12 | }, 13 | keyframes: { 14 | fade: { 15 | '0%': { 16 | opacity: '0', 17 | transform: 'translateY(10px)' 18 | }, 19 | '100%': { 20 | opacity: '1', 21 | transform: 'translateY(0)' 22 | }, 23 | } 24 | } 25 | }, 26 | }, 27 | plugins: [], 28 | } -------------------------------------------------------------------------------- /functions/api/README.md: -------------------------------------------------------------------------------- 1 | # API 目录说明 2 | 3 | 本目录 (`functions/api/`) 包含了 腾讯云 EdgeOne Pages 和 Cloudflare Pages 的 API 代理实现。 4 | 5 | ## 文件说明 6 | 7 | ### status.js 8 | 9 | 这是一个运行在边缘节点上的代理函数,主要用于: 10 | - 转发请求到 UptimeRobot API 11 | - 处理 CORS(跨域)请求 12 | - 处理错误响应 13 | 14 | ## 请求处理 15 | 16 | 1. OPTIONS 请求:返回 CORS 头部 17 | 2. POST 请求:转发到 UptimeRobot API 18 | 3. 错误处理:返回统一的错误响应 19 | 20 | ## 环境变量配置 21 | 22 | 在项目根目录的 `.env` 文件中配置: 23 | 24 | ```bash 25 | # API 代理地址 26 | VITE_UPTIMEROBOT_API_URL = "/api/status" # 使用默认配置 27 | ``` 28 | 29 | ## 部署说明 30 | 31 | - 支持多个平台部署: 32 | - 腾讯云 EdgeOne Pages 33 | - Cloudflare Pages 34 | - 路由 `/api/status` 由平台自动处理 35 | - 不需要额外的路径检查或路由配置 36 | 37 | 请确保在部署前正确配置环境变量,以保证 API 请求能够正常工作。 -------------------------------------------------------------------------------- /api/README.md: -------------------------------------------------------------------------------- 1 | # API 目录说明 2 | 3 | 本目录 (`api/`) 包含了 Vercel Serverless Functions 的 API 代理实现。 4 | 5 | ## 文件说明 6 | 7 | ### status.js 8 | 9 | 这是一个运行在 Vercel 平台上的 Serverless Function,主要用于: 10 | - 转发请求到 UptimeRobot API 11 | - 处理 CORS(跨域)请求 12 | - 处理错误响应 13 | 14 | ## 请求处理 15 | 16 | 1. OPTIONS 请求:返回 CORS 头部 17 | 2. POST 请求:转发到 UptimeRobot API 18 | 3. 错误处理:返回统一的错误响应 19 | 20 | ## 环境变量配置 21 | 22 | 在项目根目录的 `.env` 文件中配置: 23 | 24 | ```bash 25 | # API 代理地址 26 | VITE_UPTIMEROBOT_API_URL = "/api/status" # 使用 Vercel 部署时 27 | ``` 28 | 29 | ## 部署说明 30 | 31 | - 此实现专门用于 Vercel 平台部署 32 | - 路由 `/api/status` 由 Vercel 自动处理 33 | - 不需要额外的路径检查或路由配置 34 | - Vercel 会自动将 `api` 目录下的文件识别为 Serverless Functions 35 | 36 | 请确保在部署前正确配置环境变量,以保证 API 请求能够正常工作。 -------------------------------------------------------------------------------- /api/status.js: -------------------------------------------------------------------------------- 1 | export default async function handler(req, res) { 2 | // 设置 CORS 头 3 | res.setHeader('Access-Control-Allow-Origin', '*') 4 | res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') 5 | res.setHeader('Access-Control-Allow-Headers', 'Content-Type') 6 | 7 | // 处理 OPTIONS 请求 8 | if (req.method === 'OPTIONS') { 9 | return res.status(200).end() 10 | } 11 | 12 | // 只允许 POST 请求 13 | if (req.method !== 'POST') { 14 | return res.status(405).json({ error: '只支持 POST 请求' }) 15 | } 16 | 17 | try { 18 | const response = await fetch('https://api.uptimerobot.com/v2/getMonitors', { 19 | method: 'POST', 20 | headers: { 'Content-Type': 'application/json' }, 21 | body: JSON.stringify(req.body) 22 | }) 23 | 24 | const data = await response.json() 25 | return res.status(200).json(data) 26 | 27 | } catch (error) { 28 | return res.status(500).json({ error: '请求失败' }) 29 | } 30 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "uptime-status", 3 | "version": "1.0.0", 4 | "private": true, 5 | "author": "JLinmr", 6 | "url": "https://www.bsgun.cn", 7 | "email": "ruoms@qq.com", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/JLinmr/Uptime-Status" 11 | }, 12 | "repositoryUrl": "https://github.com/JLinmr/Uptime-Status", 13 | "type": "module", 14 | "scripts": { 15 | "dev": "vite", 16 | "build": "vite build", 17 | "preview": "vite preview", 18 | "server": "node server/index.js" 19 | }, 20 | "dependencies": { 21 | "axios": "^1.6.7", 22 | "chart.js": "^4.4.7", 23 | "date-fns": "^3.6.0", 24 | "vue": "^3.4.15", 25 | "vue-chartjs": "^5.3.2", 26 | "vue-router": "^4.2.5" 27 | }, 28 | "devDependencies": { 29 | "@iconify/vue": "^4.3.0", 30 | "@vitejs/plugin-vue": "^5.0.3", 31 | "autoprefixer": "^10.4.20", 32 | "cors": "^2.8.5", 33 | "express": "^4.21.2", 34 | "postcss": "^8.5.1", 35 | "tailwindcss": "^3.4.17", 36 | "vite": "^6.0.5" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 摸鱼玩家 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /functions/api/status.js: -------------------------------------------------------------------------------- 1 | /** 2 | * API 代理实现 3 | * 这是一个边缘函数,运行在边缘节点上 4 | * 用于代理 UptimeRobot API 请求,避免跨域问题 5 | * 6 | * 支持以下部署平台: 7 | * - 腾讯云 EdgeOne Pages 8 | * - Cloudflare Pages 9 | * 10 | * 环境变量配置说明: 11 | * 在 .env 文件中设置 VITE_UPTIMEROBOT_API_URL: 12 | * - 使用默认配置:设置为 "/api/status" 13 | * - 其他部署方式:设置为你的完整代理地址 14 | */ 15 | 16 | export async function onRequest(context) { 17 | // 设置 CORS 头 18 | const corsHeaders = { 19 | 'Access-Control-Allow-Origin': '*', 20 | 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 21 | 'Access-Control-Allow-Headers': 'Content-Type' 22 | } 23 | 24 | // 处理 OPTIONS 请求 25 | if (context.request.method === 'OPTIONS') { 26 | return new Response(null, { headers: corsHeaders }) 27 | } 28 | 29 | try { 30 | // 从请求中获取数据 31 | const data = await context.request.json() 32 | 33 | // 转发请求到 UptimeRobot API 34 | const response = await fetch('https://api.uptimerobot.com/v2/getMonitors', { 35 | method: 'POST', 36 | headers: { 'Content-Type': 'application/json' }, 37 | body: JSON.stringify(data) 38 | }) 39 | 40 | const newResponse = new Response(response.body, response) 41 | newResponse.headers.set('Access-Control-Allow-Origin', '*') 42 | return newResponse 43 | 44 | } catch (error) { 45 | return new Response('请求失败', { status: 500 }) 46 | } 47 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 |
2 |
3 |
优雅的站点状态监控面板
8 | 9 |
10 |
11 |
12 |
13 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
🎮 在线演示: 31 | 32 | https://status.bsgun.cn 33 | 34 |
35 | 36 | ## 📖 简介 37 | 38 | 站点监测是一个基于 UptimeRobot API 开发的站点状态监控面板,支持多站点状态监控、实时通知、故障统计等功能。界面简洁美观,响应式设计,支持亮暗主题切换。 39 | 40 | ## ✨ 功能预览 41 | 42 |  43 | 44 | ## ✨ 特性 45 | 46 | - 📊 实时监控:支持多种监控方式 47 | - 📱 响应式设计:适配移动端和桌面端 48 | - 🌓 主题切换:支持亮色/暗色主题 49 | - 📈 数据统计:可视化展示可用率和响应时间 50 | - 🔔 故障记录:详细的宕机记录和原因分析 51 | - 🔄 自动刷新:定时自动更新监控数据 52 | - 💫 平滑动画:流畅的用户界面交互体验 53 | 54 | ## ⚙️ 部署配置 55 | 56 | ### 环境要求 57 | 58 | - Node.js >= 16.16.0 59 | - NPM >= 8.15.0 或 PNPM >= 8.0.0 60 | 61 | ### 获取 UptimeRobot API Key 62 | 63 | 1. 注册/登录 [UptimeRobot](https://uptimerobot.com/) 64 | 2. 进入 [Integrations & API](https://dashboard.uptimerobot.com/integrations) 65 | 3. 下拉到最底部在 Main API keys 部分创建 **Read-Only API Key** 66 | 4. 复制生成的 API Key 67 | 68 | ### API 代理说明 69 | 70 | 本项目支持以下三种部署方式,均可实现自动处理跨域请求: 71 | 72 | 1. **腾讯云 EdgeOne Pages** 73 | - 点击上方蓝色 "Deploy" 按钮 74 | - 连接到GitHub,选择项目 75 | - 框架预设选择Vue,点击开始部署 76 | - 使用默认配置 `VITE_UPTIMEROBOT_API_URL = "/api/status"` 77 | 78 | 2. **Vercel** 79 | - 点击上方黑色 "Deploy" 按钮 80 | - 连接到GitHub,选择项目 81 | - 填写项目名称,点击Create 82 | - 使用默认配置 `VITE_UPTIMEROBOT_API_URL = "/api/status"` 83 | 84 | 3. **Cloudflare Pages** 85 | - 点击上方橙色 "Deploy" 按钮 86 | - 找到计算(worker) 部分 87 | - 点击创建,选择Pages,连接到GitHub,选择项目,点击开始创建 88 | - 框架预设选择Vue,点击保持并部署 89 | - 使用默认配置 `VITE_UPTIMEROBOT_API_URL = "/api/status"` 90 | 91 | 4. **其他平台** 92 | - 自行搭建 API 代理 93 | - 在 `.env` 文件中设置 `VITE_UPTIMEROBOT_API_URL` 为你的 API 代理地址 94 | ### 快速开始 95 | 96 | 1. 克隆项目 97 | ```bash 98 | git clone https://github.com/JLinmr/uptime-status.git 99 | cd uptime-status 100 | ``` 101 | 102 | 2. 安装依赖 103 | ```bash 104 | pnpm install 105 | # 或 106 | npm install 107 | ``` 108 | 109 | 3. 配置环境变量 110 | 111 | 在 `.env` 文件中修改以下配置: 112 | ```bash 113 | # UptimeRobot API Key 114 | VITE_UPTIMEROBOT_API_KEY = "ur2290572-af4663a4e3f83be26119abbe" 115 | 116 | # UptimeRobot API URL 117 | # 除腾讯云 EdgeOne Pages 、vercel 、cloudflare pages 外 118 | ## 其它部署方式需要自行搭建 API 代理 119 | ## 代理地址 https://api.uptimerobot.com/v2/getMonitors 120 | VITE_UPTIMEROBOT_API_URL = "/api/status" 121 | 122 | # 站点名称 123 | VITE_APP_TITLE = "梦爱吃鱼" 124 | 125 | # 监控面板排序方式 126 | # 支持 friendly_name 和 create_datetime 两种方式 127 | VITE_UPTIMEROBOT_STATUS_SORT = "friendly_name" 128 | ``` 129 | 130 | 4. 开发调试 131 | ```bash 132 | pnpm dev 133 | # 或 134 | npm run dev 135 | 136 | # 开发环境需要将 VITE_UPTIMEROBOT_API_URL 设置为 "https://api.uptimerobot.com/v2/getMonitors" 137 | ``` 138 | 139 | 5. 构建部署 140 | ```bash 141 | pnpm build 142 | # 或 143 | npm run build 144 | ``` 145 | 构建的文件在 `dist` 目录下,将 `dist` 目录部署到服务器即可。 146 | 147 | ## CDN赞助 148 | 149 | 本项目 CDN 加速及安全防护由 Tencent EdgeOne 赞助:EdgeOne 提供长期有效的免费套餐,包含不限量的流量和请求,覆盖中国大陆节点,且无任何超额收费,感兴趣的朋友可以去 EdgeOne 官网获取 150 | 151 | 最佳亚洲 CDN、Edge 和安全解决方案 - 腾讯 EdgeOne 152 |
153 |
154 |
155 | ## 📝 开源协议
156 |
157 | 本项目基于 [MIT License](LICENSE) 开源,使用时请遵守开源协议。
158 |
159 | ## 🙏 致谢
160 |
161 | - [UptimeRobot](https://uptimerobot.com/) - 提供监控 API 支持
162 | - [Vue.js](https://vuejs.org/) - 前端框架
163 | - [Tailwind CSS](https://tailwindcss.com/) - CSS 框架
164 | - [Chart.js](https://www.chartjs.org/) - 图表库
165 |
--------------------------------------------------------------------------------
/src/utils/monitor.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 监控数据处理工具模块
3 | * @module monitor
4 | */
5 |
6 | /**
7 | * 监控数据处理相关常量
8 | * @constant {Object}
9 | * @property {number} THIRTY_DAYS - 30天的天数
10 | * @property {number} MS_PER_DAY - 每天的毫秒数
11 | * @property {number} DOWNTIME_TYPE - 停机类型标识
12 | * @property {number} HOURS_IN_DAY - 每天的小时数
13 | */
14 | const CONSTANTS = {
15 | THIRTY_DAYS: 30,
16 | MS_PER_DAY: 24 * 60 * 60 * 1000,
17 | DOWNTIME_TYPE: 1,
18 | HOURS_IN_DAY: 24
19 | }
20 |
21 | /**
22 | * 数据验证工具类
23 | * @class
24 | */
25 | class Validator {
26 | /**
27 | * 验证数值是否有效
28 | * @static
29 | * @param {*} value - 要验证的值
30 | * @returns {boolean} 是否为有效数值
31 | */
32 | static isValidNumber(value) {
33 | return value != null && !isNaN(value) && value > 0
34 | }
35 |
36 | /**
37 | * 验证数组是否有效
38 | * @static
39 | * @param {Array} arr - 要验证的数组
40 | * @returns {boolean} 是否为有效数组
41 | */
42 | static isValidArray(arr) {
43 | return Array.isArray(arr) && arr.length > 0
44 | }
45 |
46 | /**
47 | * 验证时间戳是否有效
48 | * @static
49 | * @param {number} timestamp - 要验证的时间戳
50 | * @returns {boolean} 是否为有效时间戳
51 | */
52 | static isValidTimestamp(timestamp) {
53 | return this.isValidNumber(timestamp) && timestamp > 0
54 | }
55 | }
56 |
57 | /**
58 | * 时间处理工具类
59 | * @class
60 | */
61 | class TimeUtils {
62 | /**
63 | * 获取30天前的时间戳
64 | * @static
65 | * @returns {number} UNIX时间戳
66 | */
67 | static getThirtyDaysAgo() {
68 | return Math.floor((Date.now() - CONSTANTS.THIRTY_DAYS * CONSTANTS.MS_PER_DAY) / 1000)
69 | }
70 |
71 | /**
72 | * 生成时间范围字符串
73 | * @static
74 | * @returns {string} 格式化的时间范围字符串
75 | */
76 | static generateTimeRanges() {
77 | return Array.from({ length: CONSTANTS.THIRTY_DAYS }, (_, i) => {
78 | const date = new Date()
79 | date.setDate(date.getDate() - i)
80 | const start = new Date(date).setHours(0, 0, 0, 0)
81 | const end = new Date(date).setHours(23, 59, 59, 999)
82 | return `${Math.floor(start / 1000)}_${Math.floor(end / 1000)}`
83 | }).join('-')
84 | }
85 | }
86 |
87 | /**
88 | * 监控数据处理类
89 | * @class
90 | */
91 | class MonitorDataProcessor {
92 | /**
93 | * 计算平均响应时间
94 | * @static
95 | * @param {Object} monitor - 监控数据对象
96 | * @returns {number|null} 平均响应时间或null
97 | */
98 | static calculateAvgResponseTime(monitor) {
99 | try {
100 | if (Validator.isValidArray(monitor.response_times)) {
101 | // 获取24小时前的时间戳
102 | const twentyFourHoursAgo = Math.floor((Date.now() - 24 * 60 * 60 * 1000) / 1000)
103 |
104 | // 只计算最近24小时的响应时间
105 | const validTimes = monitor.response_times.filter(time =>
106 | time &&
107 | Validator.isValidNumber(time.value) &&
108 | time.datetime >= twentyFourHoursAgo
109 | )
110 |
111 | return validTimes.length > 0
112 | ? Math.round(validTimes.reduce((sum, time) => sum + time.value, 0) / validTimes.length)
113 | : null
114 | }
115 |
116 | // 如果没有详细的响应时间数据,则使用 average_response_time
117 | return Validator.isValidNumber(monitor.average_response_time)
118 | ? Math.round(monitor.average_response_time)
119 | : null
120 | } catch (error) {
121 | console.error('计算平均响应时间出错:', error)
122 | return null
123 | }
124 | }
125 |
126 | static processDowntimeLogs(logs = []) {
127 | const thirtyDaysAgo = TimeUtils.getThirtyDaysAgo()
128 | const recentLogs = logs.filter(log =>
129 | log.type === CONSTANTS.DOWNTIME_TYPE && log.datetime >= thirtyDaysAgo
130 | )
131 |
132 | return {
133 | logs: recentLogs.sort((a, b) => b.datetime - a.datetime),
134 | totalDowntime: recentLogs.reduce((total, log) => total + (log.duration || 0), 0)
135 | }
136 | }
137 |
138 | static processUptimeData(uptimeRanges) {
139 | const dailyUptimes = uptimeRanges?.split('-').map(Number).reverse() || []
140 | const validUptimes = dailyUptimes.filter(Validator.isValidNumber)
141 |
142 | return {
143 | dailyUptimes,
144 | uptime: validUptimes.length > 0
145 | ? validUptimes.reduce((sum, value) => sum + value, 0) / validUptimes.length
146 | : 0
147 | }
148 | }
149 |
150 | static processDailyResponseTimes(monitor) {
151 | try {
152 | // 创建24个小时的数组
153 | const hourlyResponseTimes = Array(CONSTANTS.HOURS_IN_DAY).fill(null)
154 |
155 | if (!Validator.isValidArray(monitor.response_times)) {
156 | return hourlyResponseTimes
157 | }
158 |
159 | // 按小时分组响应时间
160 | const hourlyGroups = monitor.response_times.reduce((groups, time) => {
161 | if (!time || !Validator.isValidNumber(time.value)) return groups
162 |
163 | const date = new Date(time.datetime * 1000)
164 | const hourIndex = Math.floor((Date.now() - date.getTime()) / (60 * 60 * 1000))
165 |
166 | if (hourIndex >= 0 && hourIndex < CONSTANTS.HOURS_IN_DAY) {
167 | if (!groups[hourIndex]) groups[hourIndex] = []
168 | groups[hourIndex].push(time.value)
169 | }
170 |
171 | return groups
172 | }, {})
173 |
174 | // 计算每小时的平均响应时间
175 | Object.entries(hourlyGroups).forEach(([hourIndex, times]) => {
176 | if (times.length > 0) {
177 | hourlyResponseTimes[hourIndex] = Math.round(
178 | times.reduce((sum, value) => sum + value, 0) / times.length
179 | )
180 | }
181 | })
182 |
183 | return hourlyResponseTimes
184 | } catch (error) {
185 | console.error('处理响应时间数据出错:', error)
186 | return Array(CONSTANTS.HOURS_IN_DAY).fill(null)
187 | }
188 | }
189 | }
190 |
191 | /**
192 | * 处理监控数据
193 | * @param {Object} monitor - 原始监控数据
194 | * @returns {Object} 处理后的监控数据
195 | * @throws {Error} 处理失败时抛出错误
196 | */
197 | export const processMonitorData = (monitor) => {
198 | try {
199 | const avgResponseTime = MonitorDataProcessor.calculateAvgResponseTime(monitor)
200 | const dailyResponseTimes = MonitorDataProcessor.processDailyResponseTimes(monitor)
201 | const { logs: downtimeLogs, totalDowntime } = MonitorDataProcessor.processDowntimeLogs(
202 | monitor.logs
203 | )
204 | const { dailyUptimes, uptime } = MonitorDataProcessor.processUptimeData(
205 | monitor.custom_uptime_ranges
206 | )
207 |
208 | return {
209 | ...monitor,
210 | stats: {
211 | avgResponseTime,
212 | dailyResponseTimes,
213 | uptime,
214 | dailyUptimes,
215 | downtimeLogs,
216 | totalDowntime
217 | }
218 | }
219 | } catch (error) {
220 | console.error('处理监控数据失败:', error)
221 | throw new Error('处理监控数据失败: ' + error.message)
222 | }
223 | }
224 |
225 | /** 导出工具函数 */
226 | export const generateTimeRanges = TimeUtils.generateTimeRanges
--------------------------------------------------------------------------------
/src/utils/chartConfig.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 图表配置工具模块
3 | * @module chartConfig
4 | */
5 |
6 | import { format } from 'date-fns'
7 |
8 | /**
9 | * 图表基础配置对象
10 | * @constant {Object}
11 | * @property {Object} styles - 图表样式配置
12 | * @property {Object} points - 数据点样式配置
13 | * @property {Object} colors - 图表颜色配置
14 | */
15 | const CHART_CONFIG = {
16 | styles: {
17 | responsive: true,
18 | maintainAspectRatio: false,
19 | plugins: {
20 | legend: { display: false },
21 | },
22 | interaction: {
23 | intersect: false,
24 | mode: 'nearest'
25 | }
26 | },
27 |
28 | points: {
29 | pointBackgroundColor: '#fff',
30 | pointBorderColor: '#10b981',
31 | pointBorderWidth: 1.5,
32 | pointHoverBorderWidth: 1.5,
33 | pointHoverBackgroundColor: '#fff'
34 | },
35 |
36 | colors: {
37 | success: '#10b981',
38 | warning: '#eab308',
39 | error: '#ef4444',
40 | orange: '#ff7a45',
41 | gray: 'rgb(120, 120, 120, 0.3)',
42 | paused: '#eab308'
43 | }
44 | }
45 |
46 | /**
47 | * 根据可用率值获取对应的图表颜色
48 | * @param {number} value - 可用率值
49 | * @param {boolean} isBeforeCreation - 是否为创建前的数据
50 | * @param {number} status - 监控状态 (2: 在线, 0: 暂停, 9: 离线)
51 | * @returns {string} 颜色代码
52 | */
53 | export const getChartColor = (value, isBeforeCreation, status) => {
54 | if (isBeforeCreation) return CHART_CONFIG.colors.gray
55 | if (status === 0) return CHART_CONFIG.colors.paused // 使用 status 判断暂停状态
56 | if (value === null || isNaN(value)) return CHART_CONFIG.colors.error
57 |
58 | const thresholds = [
59 | { min: 99.9, color: CHART_CONFIG.colors.success }, // ≥99.9% 绿色
60 | { min: 90, color: CHART_CONFIG.colors.warning }, // ≥90% 黄色
61 | { min: 0.1, color: CHART_CONFIG.colors.orange } // >0% 橙色
62 | ]
63 |
64 | // 等于0时显示红色,其他情况按阈值判断
65 | if (value === 0) return CHART_CONFIG.colors.error
66 | return thresholds.find(t => value >= t.min)?.color || CHART_CONFIG.colors.orange
67 | }
68 |
69 | /**
70 | * 获取状态时间线图表配置
71 | * @param {Object} monitor - 监控数据对象
72 | * @param {Object} dateRange - 日期范围对象
73 | * @param {boolean} isMobile - 是否为移动设备
74 | * @returns {Object} 图表配置对象
75 | */
76 | export const getStatusChartConfig = (monitor, dateRange, isMobile) => {
77 | const data = monitor.stats?.dailyUptimes ?? Array(30).fill(null)
78 |
79 | // 添加时间验证逻辑
80 | const createTime = monitor.create_datetime * 1000
81 | const now = Date.now()
82 | const effectiveCreateTime = createTime > now ? now : createTime
83 |
84 | const daysSinceStart = Math.max(0, Math.floor(
85 | (new Date(effectiveCreateTime) - dateRange.startDate) / 86400000
86 | ))
87 |
88 | const pointSize = isMobile ? { radius: 4, hoverRadius: 5 } : { radius: 8, hoverRadius: 10 }
89 |
90 | return {
91 | data: {
92 | datasets: [{
93 | data: data.map((value, i) => ({
94 | x: i,
95 | y: 50,
96 | value: i < daysSinceStart ? null : (value ?? null),
97 | date: dateRange.dates[i],
98 | status: monitor.status,
99 | isBeforeCreation: i < daysSinceStart // 添加创建前标记
100 | })),
101 | backgroundColor: data.map((v, i) =>
102 | getChartColor(v, i < daysSinceStart, monitor.status)
103 | ),
104 | borderColor: 'transparent',
105 | ...pointSize,
106 | pointStyle: 'rectRounded'
107 | }]
108 | },
109 | options: {
110 | ...CHART_CONFIG.styles,
111 | plugins: {
112 | ...CHART_CONFIG.styles.plugins,
113 | tooltip: {
114 | callbacks: {
115 | title: items => format(items[0].raw.date, 'yyyy-MM-dd'),
116 | label: context => {
117 | if (context.raw.isBeforeCreation) {
118 | return '无数据' // 创建前显示"无数据"
119 | }
120 | if (context.raw.status === 0) {
121 | return '已暂停' // 然后再判断暂停状态
122 | }
123 | if (context.raw.value === null) {
124 | return '无数据'
125 | }
126 | return `可用率:${context.raw.value.toFixed(2)}%`
127 | }
128 | }
129 | }
130 | },
131 | scales: {
132 | x: { display: false, min: -0.5, max: 29.5 },
133 | y: { display: false, min: 45, max: 55 }
134 | },
135 | animation: { duration: 200 },
136 | elements: {
137 | point: {
138 | ...pointSize,
139 | borderWidth: 0,
140 | borderRadius: 4
141 | }
142 | }
143 | }
144 | }
145 | }
146 |
147 | /**
148 | * 获取响应时间图表数据
149 | * @param {Object} monitor - 监控数据对象
150 | * @returns {Object} 图表数据配置对象
151 | */
152 | export const getResponseTimeChartData = (monitor) => {
153 | const hourLabels = Array.from({ length: 24 }, (_, i) => {
154 | const date = new Date()
155 | date.setHours(date.getHours() - i)
156 | return format(date, 'HH:mm')
157 | }).reverse()
158 |
159 | // 处理数据
160 | const validData = [...(monitor?.stats?.dailyResponseTimes || [])]
161 | .reverse()
162 | .slice(-24) // 只取最近24小时的数据
163 | .map(time => {
164 | // 严格的数值检查
165 | if (typeof time !== 'number' || isNaN(time) || time < 0 || time > 60000) {
166 | return null
167 | }
168 | return time
169 | })
170 |
171 | return {
172 | labels: hourLabels,
173 | datasets: [{
174 | label: '响应时间',
175 | data: validData,
176 | borderColor: '#10b981',
177 | backgroundColor: 'rgba(16, 185, 129, 0.05)',
178 | borderWidth: 1.5,
179 | fill: true,
180 | tension: 0.4,
181 | pointRadius: 0,
182 | pointHoverRadius: 4,
183 | ...CHART_CONFIG.points
184 | }]
185 | }
186 | }
187 |
188 | /**
189 | * 响应时间图表配置对象
190 | * @constant {Object}
191 | */
192 | export const responseTimeChartOptions = {
193 | ...CHART_CONFIG.styles,
194 | plugins: {
195 | ...CHART_CONFIG.styles.plugins,
196 | tooltip: {
197 | enabled: true,
198 | mode: 'index',
199 | intersect: false,
200 | callbacks: {
201 | title: items => items[0]?.label || '',
202 | label: context => context.raw ? `响应时间:${context.raw} ms` : '无数据'
203 | }
204 | }
205 | },
206 | scales: {
207 | x: {
208 | grid: { display: false, drawBorder: false },
209 | ticks: {
210 | maxRotation: 0,
211 | color: 'rgb(156, 163, 175)',
212 | padding: 8,
213 | maxTicksLimit: 12,
214 | font: { size: 11 }
215 | }
216 | },
217 | y: {
218 | beginAtZero: true,
219 | border: { display: false },
220 | grid: {
221 | color: 'rgba(229, 231, 235, 0.5)',
222 | drawBorder: false,
223 | lineWidth: 1
224 | },
225 | ticks: {
226 | color: 'rgb(156, 163, 175)',
227 | padding: 8,
228 | font: { size: 11 },
229 | callback: value => `${value} ms`,
230 | maxTicksLimit: 8
231 | }
232 | }
233 | },
234 | elements: {
235 | line: {
236 | tension: 0.4,
237 | borderWidth: 1.5,
238 | borderCapStyle: 'round',
239 | borderJoinStyle: 'round',
240 | capBezierPoints: true
241 | }
242 | },
243 | animation: {
244 | duration: 600,
245 | easing: 'easeInOutCubic',
246 | delay: (context) => context.dataIndex * 20
247 | }
248 | }
--------------------------------------------------------------------------------
/src/components/Card.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |