├── .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 |
2 |
6 |
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 |
9 |
10 |
11 |
12 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
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 |
20 |
21 |
22 |
23 |
29 |
30 |
31 |
32 |
33 |
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 |
12 |
18 | 南哪充电
19 |
20 |
21 | 状态监控
22 |
23 |
24 |
25 |
26 | 使用说明
27 |
28 |
33 |
34 |
35 |
36 |
41 |
42 | X 表示对应充电口已空闲 X 分钟
43 |
44 | Y 表示对应充电口已充电 Y 分钟
45 |
46 | Z 表示对应充电口已维护 Z 分钟
47 |
48 |
49 |
50 |
53 |
54 |
55 |
56 |
57 |
58 |
63 |
--------------------------------------------------------------------------------
/src/components/GroupPanel.vue:
--------------------------------------------------------------------------------
1 |
42 |
43 |
44 |
45 |
46 |
47 |
54 |
55 | {{ percentage.toFixed(0) }}%
56 | {{ usedOutletCount }} / {{ allOutletCount }} Used
59 |
60 |
61 |
62 |
63 |
64 | #{{station.stationName.split(' ')[1][0]}}
65 |
66 |
67 |
68 |
69 | {{Math.min(outlet.state_min, 999)}}
70 | {{Math.min(outlet.state_min, 999)}}
71 | {{outlet.state_min}}
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
111 |
--------------------------------------------------------------------------------