├── .npmrc ├── .gitignore ├── src ├── main.ts ├── env.d.ts ├── components │ ├── MyFooter.vue │ ├── MyMain.vue │ ├── MyHeader.vue │ └── GroupPanel.vue ├── styles │ ├── index.scss │ └── element │ │ └── index.scss ├── assets │ └── github-mark.svg └── App.vue ├── tsconfig.json ├── public └── favicon.svg ├── README.md ├── package.json ├── index.html ├── vite.config.ts ├── LICENSE ├── components.d.ts └── api └── realtime.ts /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | 7 | # lock 8 | yarn.lock 9 | package-lock.json 10 | pnpm-lock.yaml 11 | 12 | *.log 13 | .vercel 14 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import App from './App.vue'; 3 | 4 | import '~/styles/index.scss'; 5 | 6 | const app = createApp(App); 7 | 8 | app.mount('#app'); 9 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module "*.vue" { 4 | import { DefineComponent } from "vue"; 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types 6 | const component: DefineComponent<{}, {}, any>; 7 | export default component; 8 | } 9 | -------------------------------------------------------------------------------- /src/components/MyFooter.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | // :root { 2 | // --el-color-primary: red; 3 | // } 4 | 5 | body { 6 | margin: 0; 7 | } 8 | 9 | a { 10 | color: var(--el-color-primary); 11 | } 12 | 13 | code { 14 | border-radius: 2px; 15 | padding: 2px 4px; 16 | background-color: var(--el-color-primary-light-9); 17 | color: var(--el-color-primary); 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "target": "esnext", 5 | "useDefineForClassFields": true, 6 | "module": "esnext", 7 | "moduleResolution": "node", 8 | "strict": true, 9 | "jsx": "preserve", 10 | "sourceMap": true, 11 | "resolveJsonModule": true, 12 | "esModuleInterop": true, 13 | "lib": ["esnext", "dom"], 14 | "paths": { 15 | "~/*": ["src/*"] 16 | } 17 | }, 18 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "api/**/*.ts"] 19 | } 20 | -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 南哪充电 2 | 3 | 南京大学 (Nanjing Univerisity) 校内充电站状态监测系统(仅前端部分) 4 | 5 | 访问地址:https://charge.zhuxh.net/ 6 | 7 | ## 功能介绍 8 | 9 | 显示南京大学仙林校区内所有电动车充电站的占用及维护状态,约 3 分钟完整更新一次。 10 | 11 | 包含 “已占用时间” “已空闲时间” 等细节信息。 12 | 13 | > 为减小请求量,自 2024/04/26 起 “已占用时间” 等功能通过对比历史数据实现,可能与实际情况存在误差,仅供参考。 14 | 15 | ## 开发计划 16 | > 不一定真的会做( 17 | 18 | - [x] 添加充电站维护检测 19 | - [ ] 支持鼓楼/浦口/苏州校区(可能需要当地的同学配合,有兴趣的同学可以[联系我](mailto:zhuxinhao00@gmail.com)) 20 | - [ ] 历史趋势分析 21 | - [ ] 空闲预测 22 | - [ ] 显示效果优化 23 | 24 | ## DataDump 25 | 26 | 充电站状态历史数据可以通过 [GoogleDrive](https://drive.google.com/drive/folders/1ubZtjE4W07P0NRi36K2cR8D7opyey2b3?usp=sharing) 免费获取。(更新至 2025/01/27) 27 | 28 | ## 致谢 29 | 30 | [Vercel](https://vercel.com/) & [MongoDB Atlas](https://www.mongodb.com/zh-cn/atlas/database) 31 | 32 | -------------------------------------------------------------------------------- /src/styles/element/index.scss: -------------------------------------------------------------------------------- 1 | $--colors: ( 2 | "primary": ( 3 | "base": green, 4 | ), 5 | "success": ( 6 | "base": #21ba45, 7 | ), 8 | "warning": ( 9 | "base": #f2711c, 10 | ), 11 | "danger": ( 12 | "base": #db2828, 13 | ), 14 | "error": ( 15 | "base": #db2828, 16 | ), 17 | "info": ( 18 | "base": #42b8dd, 19 | ), 20 | ); 21 | 22 | // You should use them in scss, because we calculate it by sass. 23 | // comment next lines to use default color 24 | @forward "element-plus/theme-chalk/src/common/var.scss" with 25 | ( 26 | // do not use same name, it will override. 27 | $colors: $--colors, 28 | $button-padding-horizontal: ("default": 50px) 29 | ); 30 | 31 | // if you want to import all 32 | // @use "element-plus/theme-chalk/src/index.scss" as *; 33 | 34 | // You can comment it to hide debug info. 35 | // @debug $--colors; 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nju-electricity", 3 | "version": "1.0.1", 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "dayjs": "^1.11.13", 12 | "element-plus": "^2.6.1", 13 | "mongodb": "^6.20.0", 14 | "vercel": "^48.1.6", 15 | "vue": "^3.4.21", 16 | "vue-router": "^4.3.0" 17 | }, 18 | "devDependencies": { 19 | "@tsconfig/node20": "^20.1.2", 20 | "@types/node": "^20.11.25", 21 | "@vercel/node": "^3.0.0", 22 | "@vitejs/plugin-vue": "^5.0.4", 23 | "@vue/tsconfig": "^0.5.1", 24 | "npm-run-all2": "^6.1.2", 25 | "sass": "^1.72.0", 26 | "typescript": "~5.4.0", 27 | "unplugin-vue-components": "^0.26.0", 28 | "vite": "^5.1.5", 29 | "vue-tsc": "^2.0.6" 30 | }, 31 | "engines": { 32 | "node": "^20.11.1", 33 | "npm": "^10.2.4" 34 | }, 35 | "license": "MIT" 36 | } 37 | -------------------------------------------------------------------------------- /src/assets/github-mark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 南哪充电 8 | 9 | 10 | 14 | 15 | 24 | 25 | 26 | 27 |
28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 25 | 26 | 34 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { defineConfig } from 'vite'; 3 | import vue from '@vitejs/plugin-vue'; 4 | 5 | import Components from 'unplugin-vue-components/vite'; 6 | import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'; 7 | 8 | // https://vitejs.dev/config/ 9 | export default defineConfig({ 10 | resolve: { 11 | alias: { 12 | '~/': `${path.resolve(__dirname, 'src')}/`, 13 | }, 14 | }, 15 | css: { 16 | preprocessorOptions: { 17 | scss: { 18 | additionalData: `@use "~/styles/element/index.scss" as *;`, 19 | }, 20 | }, 21 | }, 22 | plugins: [ 23 | vue(), 24 | Components({ 25 | resolvers: [ 26 | ElementPlusResolver({ 27 | importStyle: 'sass', 28 | }), 29 | ], 30 | }), 31 | ], 32 | test: { 33 | global: true, 34 | environment: 'jsdom', 35 | include: ['src/**/*.spec.ts'], 36 | exclude: ['node_modules', 'dist', 'src/**/*.stories.spec.ts'], 37 | deps: { 38 | inline: ['element-plus'], 39 | }, 40 | }, 41 | }); 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Null_42 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 | -------------------------------------------------------------------------------- /src/components/MyMain.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 34 | 35 | 50 | -------------------------------------------------------------------------------- /components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // Generated by unplugin-vue-components 5 | // Read more: https://github.com/vuejs/core/pull/3399 6 | export {} 7 | 8 | declare module 'vue' { 9 | export interface GlobalComponents { 10 | ElButton: typeof import('element-plus/es')['ElButton'] 11 | ElCard: typeof import('element-plus/es')['ElCard'] 12 | ElCol: typeof import('element-plus/es')['ElCol'] 13 | ElContainer: typeof import('element-plus/es')['ElContainer'] 14 | ElDialog: typeof import('element-plus/es')['ElDialog'] 15 | ElDivider: typeof import('element-plus/es')['ElDivider'] 16 | ElFooter: typeof import('element-plus/es')['ElFooter'] 17 | ElHeader: typeof import('element-plus/es')['ElHeader'] 18 | ElMain: typeof import('element-plus/es')['ElMain'] 19 | ElMenu: typeof import('element-plus/es')['ElMenu'] 20 | ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] 21 | ElProgress: typeof import('element-plus/es')['ElProgress'] 22 | ElRow: typeof import('element-plus/es')['ElRow'] 23 | ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] 24 | GroupPanel: typeof import('./src/components/GroupPanel.vue')['default'] 25 | MyFooter: typeof import('./src/components/MyFooter.vue')['default'] 26 | MyHeader: typeof import('./src/components/MyHeader.vue')['default'] 27 | MyMain: typeof import('./src/components/MyMain.vue')['default'] 28 | RouterLink: typeof import('vue-router')['RouterLink'] 29 | RouterView: typeof import('vue-router')['RouterView'] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /api/realtime.ts: -------------------------------------------------------------------------------- 1 | import type { VercelRequest, VercelResponse } from '@vercel/node'; 2 | import { MongoClient, Db } from 'mongodb'; 3 | 4 | // Get the MongoDB URI from environment variables 5 | const URI = process.env.MONGO_URI; 6 | 7 | // Cache the database connection to reuse it across function invocations 8 | let cachedDb: Db | null = null; 9 | 10 | async function connectToDatabase() { 11 | // If the database connection is already cached, use it 12 | if (cachedDb) { 13 | return cachedDb; 14 | } 15 | 16 | // If there is no cached connection, create a new one 17 | if (!URI) { 18 | throw new Error('MONGO_URI is not defined in environment variables.'); 19 | } 20 | 21 | const client = new MongoClient(URI); 22 | await client.connect(); 23 | 24 | const db = client.db('electricity'); 25 | 26 | // Cache the connection 27 | cachedDb = db; 28 | return db; 29 | } 30 | 31 | // The main handler is now async 32 | export default async function handler(req: VercelRequest, res: VercelResponse) { 33 | try { 34 | // Get the database connection (uses the cached one if available) 35 | const database = await connectToDatabase(); 36 | 37 | const items = database.collection('real-time'); 38 | const data = await items.find({}).limit(10).toArray(); 39 | 40 | // Send the data as a successful response wrapped in a result object 41 | return res.status(200).json({ result: data }); 42 | 43 | } catch (err: any) { 44 | console.error(err); 45 | // Send a proper error response if something goes wrong 46 | return res.status(500).json({ error: 'Internal Server Error', message: err.message }); 47 | } 48 | // We do NOT close the client here, so it can be reused 49 | } -------------------------------------------------------------------------------- /src/components/MyHeader.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 57 | 58 | 63 | -------------------------------------------------------------------------------- /src/components/GroupPanel.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 81 | 82 | 111 | --------------------------------------------------------------------------------