feat: homepage

This commit is contained in:
MoYuan-CN
2025-11-17 19:25:36 +08:00
parent d16f70601b
commit ba9ee04387
31 changed files with 2567 additions and 1020 deletions

8
.editorconfig Normal file
View File

@@ -0,0 +1,8 @@
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}]
charset = utf-8
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
end_of_line = lf
max_line_length = 100

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
* text=auto eol=crlf

5
.gitignore vendored
View File

@@ -34,8 +34,3 @@ coverage
# Vitest
__screenshots__/
# package
package-lock.json
pnpm-lock.yaml
yarn.lock

6
.prettierrc Normal file
View File

@@ -0,0 +1,6 @@
{
"tabWidth": 4,
"useTabs": false,
"vueIndentScriptAndStyle": true,
"bracketSameLine": true
}

View File

@@ -1,3 +1,7 @@
{
"recommendations": ["Vue.volar", "bradlc.vscode-tailwindcss"]
"recommendations": [
"Vue.volar",
"dbaeumer.vscode-eslint",
"EditorConfig.EditorConfig"
]
}

View File

@@ -1,4 +1,4 @@
# EasyTierWeb
# NeoUptime Web
This template should help get you started developing with Vue 3 in Vite.
@@ -32,11 +32,17 @@ pnpm install
### Compile and Hot-Reload for Development
```sh
pnpm run dev
pnpm dev
```
### Type-Check, Compile and Minify for Production
```sh
pnpm run build
pnpm build
```
### Lint with [ESLint](https://eslint.org/)
```sh
pnpm lint
```

View File

@@ -1,13 +1,12 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<link rel="stylesheet" href="src/assets/style.css">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>EasyTierMC Uptime</title>
</head>
<body>
<body class="bg-base-200 w-screen h-screen">
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>

View File

@@ -1,9 +1,8 @@
{
"name": "neoeasytierweb",
"name": "neouptime-web",
"version": "0.0.0",
"private": true,
"type": "module",
"packageManager": "pnpm@10.18.0",
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
@@ -12,27 +11,30 @@
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build"
"type-check": "vue-tsc --build",
"lint": "eslint . --fix --cache"
},
"dependencies": {
"echarts": "^6.0.0",
"neoeasytierweb": "link:",
"@iconify/tailwind4": "^1.1.0",
"@tailwindcss/vite": "^4.1.17",
"daisyui": "^5.5.4",
"pinia": "^3.0.4",
"tailwindcss": "^4.1.17",
"vue": "^3.5.22",
"vue-echarts": "^8.0.1",
"vue": "^3.5.24",
"vue-router": "^4.6.3"
},
"devDependencies": {
"@tailwindcss/vite": "^4.1.17",
"@tsconfig/node22": "^22.0.2",
"@types/node": "^22.18.11",
"@iconify-json/octicon": "^1.2.19",
"@tsconfig/node22": "^22.0.3",
"@types/node": "^22.19.1",
"@vitejs/plugin-vue": "^6.0.1",
"@vue/eslint-config-typescript": "^14.6.0",
"@vue/tsconfig": "^0.8.1",
"daisyui": "^5.4.7",
"jiti": "^2.6.1",
"npm-run-all2": "^8.0.4",
"typescript": "~5.9.0",
"vite": "^7.1.11",
"typescript": "~5.9.3",
"vite": "npm:rolldown-vite@^7.2.5",
"vite-plugin-vue-devtools": "^8.0.3",
"vue-tsc": "^3.1.1"
"vue-tsc": "^3.1.3"
}
}

2706
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,70 +1,11 @@
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
const year = new Date().getFullYear()
import { Layout } from "./layout";
</script>
<template>
<div class="layout">
<header class="navbar bg-base-100 shadow-md">
<div class="navbar-start">
<div class="dropdown">
<div tabindex="0" role="button" class="btn btn-ghost lg:hidden">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h8m-8 6h16" />
</svg>
</div>
<ul tabindex="0" class="dropdown-content menu p-2 shadow bg-base-100 rounded-box w-52">
<li><RouterLink to="/">主页</RouterLink></li>
<li><RouterLink to="/monitor">节点监控</RouterLink></li>
<li><RouterLink to="/submit">提交节点</RouterLink></li>
</ul>
</div>
<div class="flex-1">
<h1 class="btn btn-ghost normal-case text-2xl font-bold">EasyTierMC Uptime</h1>
</div>
</div>
<div class="navbar-end hidden lg:flex">
<ul class="menu menu-horizontal px-1 gap-2">
<li><RouterLink to="/" class="btn btn-ghost">主页</RouterLink></li>
<li><RouterLink to="/monitor" class="btn btn-ghost">节点监控</RouterLink></li>
<li><RouterLink to="/submit" class="btn btn-ghost">提交节点</RouterLink></li>
</ul>
</div>
</header>
<main class="flex-1">
<Layout>
<RouterView />
</main>
<footer class="footer footer-center bg-base-200 text-base-content p-4 border-t">
<div>
<small>© {{ year }} EasyTierMC Uptime</small>
</div>
</footer>
</div>
</Layout>
</template>
<style scoped>
.layout {
min-height: 100vh;
display: flex;
flex-direction: column;
}
header {
flex-shrink: 0;
}
main {
flex: 1;
padding: 1.25rem 1.5rem 2rem;
}
footer {
flex-shrink: 0;
}
@media (min-width: 1024px) {
main {
padding: 1.75rem 2rem 2.5rem;
}
}
</style>
<style scoped></style>

View File

@@ -1,89 +0,0 @@
@import "tailwindcss";
@plugin "daisyui";
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

84
src/assets/index.css Normal file
View File

@@ -0,0 +1,84 @@
@import "tailwindcss";
@layer base {
*:not(.selectable) {
@apply select-none;
}
}
@plugin "@iconify/tailwind4";
@plugin "daisyui" {
themes: false;
excludes: "rootscrollgutter";
}
@plugin "daisyui/theme" {
name: "light";
default: true;
prefersdark: false;
color-scheme: "light";
--color-base-100: oklch(0.9782 0.0034 247.86);
--color-base-200: oklch(0.9782 0.0034 247.86);
--color-base-300: oklch(1 0 0);
--color-base-content: oklch(0.135 0.0111 254.04);
--color-primary: oklch(0.7967 0.1103 178.33);
--color-primary-content: oklch(0.0757 0.1103 178.33);
--color-secondary: oklch(0.7967 0.1103 62.49);
--color-secondary-content: oklch(0.0757 0.1103 62.49);
--color-accent: oklch(0.7967 0.1103 337.03);
--color-accent-content: oklch(0.0757 0.1103 337.03);
--color-neutral: oklch(14% 0.005 285.823);
--color-neutral-content: oklch(92% 0.004 286.32);
--color-info: oklch(0.701 0.201 255.48);
--color-info-content: oklch(25% 0.09 281.288);
--color-success: oklch(0.7701 0.1809 145.62);
--color-success-content: oklch(26% 0.051 172.552);
--color-warning: oklch(0.848 0.1394 72.63);
--color-warning-content: oklch(27% 0.077 45.635);
--color-error: oklch(0.7145 0.2234 26.79);
--color-error-content: oklch(25% 0.092 26.042);
--radius-selector: 0.5rem;
--radius-field: 0.25rem;
--radius-box: 0.5rem;
--size-selector: 0.25rem;
--size-field: 0.25rem;
--border: 1px;
--depth: 1;
--noise: 0;
}
@plugin "daisyui/theme" {
name: "dark";
default: false;
prefersdark: true;
color-scheme: "dark";
--color-base-100: oklch(0.2735 0.0179 251.92);
--color-base-200: oklch(0.1763 0.014 258.36);
--color-base-300: oklch(0.1039 0.0194 248.35);
--color-base-content: oklch(0.9852 0 116.07);
--color-primary: oklch(0.7967 0.1103 178.33);
--color-primary-content: oklch(0.0757 0.1103 178.33);
--color-secondary: oklch(0.7967 0.1103 62.49);
--color-secondary-content: oklch(0.0757 0.1103 62.49);
--color-accent: oklch(0.7967 0.1103 337.03);
--color-accent-content: oklch(0.0757 0.1103 337.03);
--color-neutral: oklch(14% 0.005 285.823);
--color-neutral-content: oklch(92% 0.004 286.32);
--color-info: oklch(0.626 0.201 255.48);
--color-info-content: oklch(25% 0.09 281.288);
--color-success: oklch(0.6951 0.1809 145.62);
--color-success-content: oklch(26% 0.051 172.552);
--color-warning: oklch(0.773 0.1394 72.63);
--color-warning-content: oklch(27% 0.077 45.635);
--color-error: oklch(0.6395 0.2234 26.79);
--color-error-content: oklch(25% 0.092 26.042);
--radius-selector: 0.5rem;
--radius-field: 0.25rem;
--radius-box: 0.5rem;
--size-selector: 0.25rem;
--size-field: 0.25rem;
--border: 1px;
--depth: 1;
--noise: 0;
}

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

Before

Width:  |  Height:  |  Size: 276 B

View File

@@ -1,29 +0,0 @@
@import './base.css';
#app {
padding: 2rem;
font-weight: normal;
}
a,
.green {
text-decoration: none;
color: hsla(160, 100%, 37%, 1);
transition: 0.4s;
padding: 3px;
}
@media (hover: hover) {
a:hover {
background-color: hsla(160, 100%, 37%, 0.2);
}
}
@media (min-width: 1024px) {
body {
}
#app {
padding: 0 2rem;
}
}

View File

@@ -1,92 +0,0 @@
<template>
<div>
<v-chart class="chart" :option="option" />
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import VChart from 'vue-echarts';
import china from "../../../public/china.json"
import * as echarts from 'echarts';
echarts.registerMap('china',china as any);
const data = [
{ name: '北京', value: 85 },
{ name: '上海', value: 92 },
{ name: '广东', value: 9 },
{ name: '浙江', value: 65 },
{ name: '江苏', value: 58 },
// 更多数据...
]
const option = ref<echarts.EChartsOption>({
tooltip: {
show: true,
formatter: '{b}: {c}个节点'
},
visualMap: {
type: 'piecewise',
min: 0,
max: 50,
pieces:[
{
max:5,
label:'少',
color:'#ff4d4d'
},
{
min:5,
max:15,
label:'中',
color:'#ffa64d'
},
{
min:15,
max:20,
label:'多',
color:'#ffcc00'
},
{
min:20,
max:30,
label:'非常多',
color:'#99cc33'
},
{
min:30,
label:'数据中心',
color:'#33cc33'
}
],
textStyle:{
color:'#fff'
}
},
series: [{
name: '服务器节点',
type: 'map',
map: 'china',
roam: false,
select: {
disabled: true // 禁用选择状态
},
emphasis: {
label: {
show: true,
},
itemStyle:{
areaColor:'#FFDE59'
}
},
data
}]
});
</script>
<style scoped>
.chart {
height: 600px;
width: 30%;
}
</style>

1
src/layout/Admin.vue Normal file
View File

@@ -0,0 +1 @@
<template></template>

20
src/layout/Default.vue Normal file
View File

@@ -0,0 +1,20 @@
<template>
<div class="layout">
<nav class="navbar bg-base-100 shadow-sm justify-between">
<a class="btn btn-ghost text-xl">EasyTierMC Uptime</a>
<section class="flex gap-2">
<button class="btn btn-link">Home</button>
<button class="btn btn-link">About</button>
<button class="btn btn-link">Docs</button>
</section>
<button class="btn btn-primary mr-4">Submit a Node</button>
</nav>
<div class="content p-4">
<slot />
</div>
<div class="divider"></div>
<footer class="p-4 -mt-2">
<p>&copy; {{ new Date().getFullYear() }} EasyTierMC. All Right Reserved.</p>
</footer>
</div>
</template>

1
src/layout/index.ts Normal file
View File

@@ -0,0 +1 @@
export { default as Layout } from "./index.vue";

26
src/layout/index.vue Normal file
View File

@@ -0,0 +1,26 @@
<script setup lang="ts">
import { ref, watch, type Component } from "vue";
import { useRoute } from "vue-router";
import Default from "./Default.vue";
import Admin from "./Admin.vue";
const route = useRoute();
const LayoutComponent = ref<Component>(Default);
watch(
() => route.fullPath,
(newPath) => {
if (newPath === "/admin") {
LayoutComponent.value = Admin;
} else {
LayoutComponent.value = Default;
}
}
);
</script>
<template>
<LayoutComponent>
<slot />
</LayoutComponent>
</template>

View File

@@ -1,11 +1,14 @@
import './assets/main.css'
import { createApp } from "vue";
import { createPinia } from "pinia";
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import App from "./App.vue";
import router from "./modules/router";
const app = createApp(App)
const app = createApp(App);
app.use(router)
app.use(createPinia());
app.use(router);
app.mount('#app')
app.mount("#app");
import "@/assets/index.css";

View File

@@ -0,0 +1,60 @@
import type { Component } from "vue";
import { createRouter, createWebHistory, type RouteRecordRaw } from "vue-router";
type ComponentImport = () => Promise<{ default: Component }>;
/**
* Step 1
* Lazy improt all files from \@/pages
* @returns ComponentImport
*/
const modules = import.meta.glob("../../pages/**/*.vue");
/**
* Step 2
* Convert file path to router path
* @param file - File path
* @returns Router path
*/
function pathFromFile(file: string): string {
let path = file.replace(/^\..\/..\/pages|\.vue$/g, "");
path = path.replace(/\[\.\.\.(.+?)\]/g, ":$1(.*)*"); // [...slug] → :slug(.*)*
path = path.replace(/\[(.+?)\]/g, ":$1"); // [id] → :id
path = path.replace(/\/index$/, "/"); // /user/index -> /user/
path = path.replace(/^\/index$/, "/"); // /index -> /
return path;
}
/**
* Step 3
* Create router routes from modules
* @returns RouteRecordRaw[]
*/
const routes: RouteRecordRaw[] = Object.keys(modules).map((file) => ({
path: pathFromFile(file),
component: modules[file] as ComponentImport,
}));
/**
* Step 4
* Auto regist 404 route (if /404.vue exists)
*/
const notFound = Object.keys(modules).find((f) => f.includes("/404.vue"));
if (notFound) {
routes.push({
path: "/:pathMatch(.*)*",
component: modules[notFound] as ComponentImport,
});
}
/**
* Step 5
* Create router instance
* @returns Router instance
*/
const router = createRouter({
history: createWebHistory(),
routes,
});
export default router;

96
src/pages/index.vue Normal file
View File

@@ -0,0 +1,96 @@
<script setup lang="ts">
import { ref } from "vue";
const nodes = ref([
{
id: 1,
status: "down",
name: "Beta Node 7",
location: "US",
sponsor: "Blue",
uptime: "99.8%",
},
{
id: 2,
status: "up",
name: "Alpha Node 3",
location: "UK",
sponsor: "Jason",
uptime: "98.5%",
},
{
id: 3,
status: "up",
name: "Gamma Node 2",
location: "JP",
sponsor: "Whatever Foundation",
uptime: "97.2%",
},
]);
</script>
<template>
<section class="pt-4 px-4">
<div>
<h1 class="text-3xl font-bold">Public Node Directory</h1>
<p class="opacity-50 mt-2">
Explore and contribute to our community-maintained list of public nodes. Your
<br />
participation ensures transparency and a robust network.
</p>
</div>
<div class="grid grid-cols-3 grid-rows-1 mt-4 w-full gap-4">
<div class="card shadow-[0_0_2px_0_var(--color-neutral-content)] p-4 bg-base-100">
<div class="stat-title">Total Nodes</div>
<div class="stat-value text-neutral-content">138</div>
<div class="stat-desc text-success">+1.2% this week</div>
</div>
<div class="card shadow-[0_0_2px_0_var(--color-neutral-content)] p-4 bg-base-100">
<div class="stat-title">Active Nodes</div>
<div class="stat-value text-neutral-content">72</div>
<div class="stat-desc text-success">+0.5% this week</div>
</div>
<div class="card shadow-[0_0_2px_0_var(--color-neutral-content)] p-4 bg-base-100">
<div class="stat-title">Total Sponsors</div>
<div class="stat-value text-neutral-content">24</div>
<div class="stat-desc text-success">+3.1% this week</div>
</div>
</div>
<div class="overflow-x-auto rounded-box border border-base-content/5 bg-base-100 mt-4">
<table class="table">
<!-- head -->
<thead class="bg-base-300">
<tr>
<th>STATUS</th>
<th>NODE NAME</th>
<th>LOCATION</th>
<th>SPONSOR</th>
<th>UPTIME</th>
<th>ACTIONS</th>
</tr>
</thead>
<tbody>
<tr v-for="node in nodes" :key="node.id">
<td>
<div class="inline-grid *:[grid-area:1/1] -translate-y-0.25" v-if="node.status === 'up'">
<div class="status status-success animate-ping"></div>
<div class="status status-success"></div>
</div>
<div class="status status-error -translate-y-0.25" v-else></div>
<span class="ml-2">{{ node.status == "up" ? "Online" : "Offline" }}</span>
</td>
<td>{{ node.name }}</td>
<td>{{ node.location }}</td>
<td>{{ node.sponsor }}</td>
<td>{{ node.uptime }}</td>
<td>
<button class="btn aspect-square">
<i class="icon-[octicon--info-24] size-5 -mx-2.5" />
</button>
</td>
</tr>
</tbody>
</table>
</div>
</section>
</template>

View File

@@ -1,25 +0,0 @@
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView,
},
{
path: '/monitor',
name: 'monitor',
component: () => import('../views/MonitorView.vue'),
},
{
path: '/submit',
name: 'submit',
component: () => import('../views/SubmitView.vue'),
},
],
})
export default router

View File

@@ -1,15 +0,0 @@
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>
<style>
@media (min-width: 1024px) {
.about {
min-height: 100vh;
display: flex;
align-items: center;
}
}
</style>

View File

@@ -1,11 +0,0 @@
<template>
<section style="padding:2rem; text-align:center;">
<h2>欢迎来到 EasyTierMC Uptime</h2>
<p>使用上方导航查看节点监控或提交新的节点</p>
<Map />
</section>
</template>
<script setup lang="ts">
import Map from '@/components/dashboard/Map.vue';
</script>

View File

@@ -1,10 +0,0 @@
<template>
<div>
<h2>节点监控</h2>
<p>这里是节点监控页面</p>
</div>
</template>
<script setup lang="ts">
// 可根据需要添加逻辑
</script>

View File

@@ -1,10 +0,0 @@
<template>
<div>
<h2>提交节点</h2>
<p>这里是提交节点页面</p>
</div>
</template>
<script setup lang="ts">
// 可根据需要添加逻辑
</script>

View File

@@ -1,20 +1,16 @@
import { fileURLToPath, URL } from 'node:url'
import { fileURLToPath, URL } from "node:url";
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
import tailwindcss from '@tailwindcss/vite'
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import vueDevTools from "vite-plugin-vue-devtools";
import tailwindcss from "@tailwindcss/vite";
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
tailwindcss()
],
plugins: [vue(), vueDevTools(), tailwindcss()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
})
});