Compare commits

...

3 Commits

Author SHA1 Message Date
8d8937e2ff merge 2024-12-10 23:46:41 +08:00
2810c44f8a Changes view 2024-12-09 23:44:29 +08:00
e68da9927e Changes view 2024-12-09 23:42:59 +08:00
16 changed files with 438 additions and 273 deletions

View File

@ -13,6 +13,7 @@
"dependencies": { "dependencies": {
"@ant-design/icons-vue": "^7.0.1", "@ant-design/icons-vue": "^7.0.1",
"ant-design-vue": "^4.2.5", "ant-design-vue": "^4.2.5",
"js-cookie": "^3.0.5",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"vue": "^3.4.29", "vue": "^3.4.29",
"vue-router": "^4.3.3" "vue-router": "^4.3.3"

View File

@ -1,35 +1,47 @@
<template> <style scoped></style>
<header v-if="isLogin">
<div class="wrapper">
<nav>
<a-menu v-model:selectedKeys="current" mode="horizontal" :items="items" />
</nav>
</div>
</header>
<main> <template>
<article> <a-layout style="min-height: 100vh">
<a-layout-header :style="{ background: '#fff', width: '100%' }">
<a-menu
v-model:selectedKeys="current"
mode="horizontal"
:items="items"
theme="light"
:style="{ justifyContent: 'center' }"
/>
</a-layout-header>
<a-layout>
<a-layout-content
theme="light"
:style="{ flexDirection: 'column', justifyContent: 'flex-start', alignItems: 'center' }"
>
<RouterView /> <RouterView />
</article> </a-layout-content>
<aside> <a-layout-sider
<CustomCard :style="{ margin: 'auto' }" /> theme="light"
</aside> :collapsible="true"
</main> :collapsed-width="0"
:defaultCollapsed="true"
>
<CustomCard :name="'rsgl'" :style="{ margin: 'auto', padding: '10px' }" />
</a-layout-sider>
</a-layout>
<a-layout-footer> jjjj</a-layout-footer>
</a-layout>
</template> </template>
<script setup lang="js"> <script setup lang="js">
import { RouterView, useRoute } from 'vue-router' import { RouterView } from 'vue-router'
import { h, ref, onMounted, watch } from 'vue' import { h, ref } from 'vue'
import { AppstoreOutlined } from '@ant-design/icons-vue' import { AppstoreOutlined, MailOutlined } from '@ant-design/icons-vue'
import CustomCard from '@/components/CustomCard.vue' import CustomCard from '@/components/CustomCard.vue'
const isLogin = ref(true)
const current = ref(['mail']) const current = ref(['mail'])
const items = ref([ const items = ref([
{ {
key: 'home', key: 'home',
icon: () => h(AppstoreOutlined), icon: () => h(MailOutlined),
label: h('a', { href: '/' }, '首页'), label: h('a', { href: '/' }, '首页'),
title: '首页' title: '首页'
}, },
@ -38,35 +50,18 @@ const items = ref([
icon: () => h(AppstoreOutlined), icon: () => h(AppstoreOutlined),
label: h('a', { href: '/about' }, '关于'), label: h('a', { href: '/about' }, '关于'),
title: '关于' title: '关于'
},
{
key: 'tool',
icon: () => h(AppstoreOutlined),
label: h('a', { href: '/tool' }, '工具'),
title: '工具'
},
{
key: 'login',
icon: () => h(AppstoreOutlined),
label: h('a', { href: '/login' }, '登录'),
title: '登录'
} }
]) ])
const route = useRoute()
const updateLoginStatus = () => {
isLogin.value = !!localStorage.getItem('token')
console.log('isLogin.value:', isLogin.value)
}
onMounted(updateLoginStatus)
watch(route, updateLoginStatus)
</script> </script>
<style scoped>
article {
flex: 5;
border: 2px solid #ccc;
border-radius: 8px;
padding: 16px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
aside {
flex: 2;
display: flex;
border: 2px solid #ccc;
border-radius: 8px;
padding: 16px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
</style>

View File

@ -1,7 +0,0 @@
export class CustomCard {
constructor(avatar = 'src/components/images/avatar.jpg', name = '雨霖铃', content = '人生孤旅,天真以渡') {
this.avatar = avatar
this.name = name
this.content = content
}
}

View File

@ -1,51 +1,66 @@
<script setup lang="js"> <script setup lang="js">
import { ref } from 'vue' defineProps({
import { CustomCard } from '@/components/CustomCard.js' avatar: {
type: String,
const customCard = ref(new CustomCard()) default: 'src/components/images/avatar.jpg'
},
name: {
type: String,
default: '雨霖铃'
},
content: {
type: String,
default: '人生孤旅,天真以渡'
}
})
</script> </script>
<template> <template>
<div class="card"> <a-card hoverable style="width: 100%">
<div class="avatar"> <template #cover>
<img :src="customCard.avatar" alt="avatar" /> <img :src="avatar" alt="avatar" />
</div> </template>
<div class="name"> <a-card-meta :title="name" :description="content">
<h2>{{ customCard.name }}</h2> <template #avatar>
</div> <a-avatar :src="avatar" />
<div class="content"> </template>
<p>{{ customCard.content }}</p> </a-card-meta>
</div> </a-card>
</div>
</template> </template>
<style scoped> <style scoped>
.card { .card {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center;
align-items: center;
text-align: center; text-align: center;
height: 100%; align-items: center;
width: 100%;
box-sizing: border-box; box-sizing: border-box;
}
.avatar { .avatar {
display: flex; width: 50%;
justify-content: center; img {
align-items: center;
width: 100%;
}
.avatar img {
border-radius: 50%; border-radius: 50%;
} }
}
.name {
width: 50%;
}
.name,
.content { .content {
white-space: nowrap; width: 100%;
overflow: hidden; }
text-overflow: ellipsis; }
@media screen and (max-width: 800px) {
.card {
width: 160px;
}
}
@media screen and (min-width: 800px) {
.card {
width: 200px;
}
} }
</style> </style>

View File

@ -0,0 +1,82 @@
<template>
<a-list
class="demo-loadmore-list"
:loading="initLoading"
item-layout="horizontal"
:data-source="list"
:style="{ width: '70%' }"
>
<template #loadMore>
<div
v-if="!initLoading && !loading"
:style="{ textAlign: 'center', marginTop: '12px', height: '32px', lineHeight: '32px' }"
>
<a-button @click="onLoadMore">loading more</a-button>
</div>
</template>
<template #renderItem="{ item }">
<a-list-item>
<a-skeleton avatar :title="false" :loading="!!item.loading" active>
<a-list-item-meta
description="Ant Design, a design language for background applications, is refined by Ant UED Team"
>
<template #title>
<a href="https://www.antdv.com/">{{ item.name.last }}</a>
</template>
</a-list-item-meta>
</a-skeleton>
</a-list-item>
</template>
</a-list>
</template>
<script setup>
import { onMounted, ref, nextTick } from 'vue'
const count = 3
const fakeDataUrl = `https://randomuser.me/api/?results=${count}&inc=name,gender,email,nat,picture&noinfo`
const initLoading = ref(true)
const loading = ref(false)
const data = ref([])
const list = ref([])
onMounted(() => {
fetch(fakeDataUrl)
.then((res) => res.json())
.then((res) => {
initLoading.value = false
data.value = res.results
list.value = res.results
})
})
const onLoadMore = () => {
loading.value = true
list.value = data.value.concat(
[...new Array(count)].map(() => ({
loading: true,
name: {},
picture: {}
}))
)
fetch(fakeDataUrl)
.then((res) => res.json())
.then((res) => {
const newData = data.value.concat(res.results)
loading.value = false
data.value = newData
list.value = newData
nextTick(() => {
// Resetting window's offsetTop so as to display react-virtualized demo underfloor.
// In real scene, you can using public method of react-virtualized:
// https://stackoverflow.com/questions/46700726/how-to-use-public-method-updateposition-of-react-virtualized
window.dispatchEvent(new Event('resize'))
})
})
}
</script>
<style scoped>
.demo-loadmore-list {
min-height: 350px;
}
</style>

View File

@ -0,0 +1,55 @@
<script setup>
import { watch, ref } from 'vue'
import { dateTimeToTimestamp, formatDate, timestampToDateTime } from '@/utils/time.js'
const inputTimestamp = ref('')
const outputDatetime = ref('')
const timestampSelect = ref('ms')
const inputDatetime = ref('')
const outputTimestamp = ref('')
const datetimeSelect = ref('ms')
watch(inputTimestamp, async () => {
outputDatetime.value = formatDate(
timestampToDateTime(timestampSelect.value, Number(inputTimestamp.value))
)
})
function changeTimestamp() {
outputDatetime.value = formatDate(
timestampToDateTime(timestampSelect.value, Number(inputTimestamp.value))
)
}
function changeDateTime() {
outputTimestamp.value = dateTimeToTimestamp(timestampSelect.value, inputDatetime.value)
}
</script>
<template>
<a-space direction="horizontal" justify="center" class="time">
<a-input v-model:value.lazy="inputTimestamp" placeholder="输入时间戳" />
<a-select v-model:value="timestampSelect" style="width: 100px">
<a-select-option value="ms">ms/毫秒</a-select-option>
<a-select-option value="s">s/</a-select-option>
</a-select>
<a-button @click="changeTimestamp">转换</a-button>
<a-input v-model:value="outputDatetime" placeholder="输出结果" />
</a-space>
<a-space direction="horizontal" justify="center" class="time">
<a-input v-model:value.lazy="inputDatetime" placeholder="输入时间日期" />
<a-button @click="changeDateTime">转换</a-button>
<a-select v-model:value="datetimeSelect" style="width: 100px">
<a-select-option value="ms">ms/毫秒</a-select-option>
<a-select-option value="s">s/</a-select-option>
</a-select>
<a-input v-model:value="outputTimestamp" placeholder="输出结果" />
</a-space>
</template>
<style scoped>
.time {
margin: 10px 0;
}
</style>

View File

@ -1,7 +1,13 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue' import HomeView from '@/views/HomeView.vue'
import AboutView from '@/views/AboutView.vue'
import ToolView from '@/views/ToolView.vue'
import LoginView from '@/views/LoginView.vue'
import NewsView from '@/views/NewsView.vue'
const routes = [ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{ {
path: '/', path: '/',
name: 'home', name: 'home',
@ -10,34 +16,24 @@ const routes = [
{ {
path: '/about', path: '/about',
name: 'about', name: 'about',
component: () => import('../views/AboutView.vue') component: AboutView
},
{
path: '/tool',
name: 'tool',
component: ToolView
}, },
{ {
path: '/login', path: '/login',
name: 'login', name: 'login',
component: () => import('../views/LoginView.vue'), component: LoginView
meta: { },
requiresAuth: true {
} path: '/news',
name: 'news',
component: NewsView
} }
] ]
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: routes
})
router.beforeEach((to, from, next) => {
const isLogin = !!localStorage.getItem('token')
const requiresAuth = to.matched.some((record) => !!record.meta['requiresAuth'])
console.log('isLogin', isLogin)
console.log('requiresAuth', requiresAuth)
if (!requiresAuth && !isLogin) {
next('/login')
} else {
next()
}
}) })
export default router export default router

15
src/utils/auth.js Normal file
View File

@ -0,0 +1,15 @@
import Cookies from 'js-cookie'
const TokenKey = 'Admin-Token'
export function getToken() {
return Cookies.get(TokenKey)
}
export function setToken(token) {
Cookies.set(TokenKey, token)
}
export function removeToken() {
Cookies.remove(TokenKey)
}

6
src/utils/errorCode.js Normal file
View File

@ -0,0 +1,6 @@
export default {
401: '认证失败',
403: '没有权限',
404: '资源不存在',
default: '系统未知错误'
}

54
src/utils/time.js Normal file
View File

@ -0,0 +1,54 @@
function timestampToDateTime(pattern, timestamp) {
if (pattern === 's') {
return new Date(timestamp * 1000)
} else if (pattern === 'ms') {
console.log(typeof timestamp)
console.log(timestamp)
return new Date(timestamp)
}
return NaN
}
function dateTimeToTimestamp(pattern, datetime) {
const timestamp = new Date(datetime).getTime()
if (pattern === 's') {
return Math.floor(timestamp / 1000).toString()
} else if (pattern === 'ms') {
return timestamp.toString()
}
return NaN.toString()
}
Date.prototype.Format = function (fmt) {
var o = {
'M+': this.getMonth() + 1, //月份
'd+': this.getDate(), //日
'h+': this.getHours(), //小时
'm+': this.getMinutes(), //分
's+': this.getSeconds(), //秒
'q+': Math.floor((this.getMonth() + 3) / 3), //季度
S: this.getMilliseconds() //毫秒
}
if (/(y+)/.test(fmt))
fmt = fmt.replace(RegExp.$1, (this.getFullYear() + '').substr(4 - RegExp.$1.length))
for (var k in o)
if (new RegExp('(' + k + ')').test(fmt))
fmt = fmt.replace(
RegExp.$1,
RegExp.$1.length === 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length)
)
return fmt
}
function formatDate(date) {
const year = date.getFullYear()
const month = date.getMonth() + 1
const day = date.getDate()
const hour = date.getHours()
const minute = date.getMinutes()
const second = date.getSeconds()
return `${year}-${month}-${day}-${hour}:${minute}:${second}`
}
export { timestampToDateTime, dateTimeToTimestamp, formatDate }

View File

@ -1,7 +1,15 @@
<script setup lang="js">
import { useRouter } from 'vue-router'
import CustomNews from '@/components/CustomNews.vue'
const router = useRouter()
</script>
<template> <template>
<div class="about"> <div class="about">
<h1>This is an about page</h1> <h1>This is an about page</h1>
<a-button @click="() => router.push('/news')">Goto News</a-button>
</div> </div>
<CustomNews />
</template> </template>
<style> <style>

9
src/views/GameView.vue Normal file
View File

@ -0,0 +1,9 @@
<script setup></script>
<template>
<a-card>
<a-card-meta :title="'GameView'" :description="'GameView'"> </a-card-meta>
</a-card>
</template>
<style scoped></style>

View File

@ -3,69 +3,82 @@ const cardStyle = {
width: '200px' width: '200px'
} }
class Module { class moduleCard {
/** title
* 模块 content
* @param {string} title url
* @param {string} url
*/ constructor(title, content, url) {
constructor(title, url) {
this.title = title this.title = title
this.content = content
this.url = url this.url = url
} }
}
class ManagerModule { introduceSelf() {
/** console.log(`title:${this.title}\ncontent:${this.content}\nurl:${this.url}`)
* 模块管理
* @param {string} name
* @param {Module[]} moduleList
*/
constructor(name, moduleList) {
this.name = name
this.moduleList = moduleList
} }
} }
const manageModuleList = [ const modules = [
new ManagerModule('Zero tier', [ {
new Module('Pve', 'https://10.18.80.15:8006/'), title: 'zero tier',
new Module('fnOS', 'http://10.18.80.124:8000/') content: [
]), new moduleCard('gitea', '', 'http://10.120.20.137:3000/'),
new ManagerModule('云服务器', [ new moduleCard('pve', '', 'http://10.120.20.15:8006/')
new Module('zero tier', 'https://zerotier.yulinling.asia/'), ]
new Module('gitea', 'https://gitea.yulinling.asia/') },
]) {
title: '腾讯云服务',
content: [
new moduleCard('zero tier', '', 'https://zerotier.yulinling.asia/'),
new moduleCard('gitea', '', 'https://gitea.yulinling.asia/')
]
}
] ]
</script> </script>
<template> <template>
<div v-for="(item, itemIndex) in manageModuleList" :key="itemIndex"> <div v-for="(item, index) in modules" :key="index">
<h2>{{ item.name }}</h2> <h2>{{ item.title }}</h2>
<ul> <ul>
<div v-for="(module, index) in item.moduleList" :key="index"> <a-card
<a-card :style="cardStyle"> v-for="(item, index) in item.content"
:key="index"
:style="cardStyle"
:body-style="{ padding: 0, overflow: 'hidden' }"
hoverable
>
<a-flex justify="space-between"> <a-flex justify="space-between">
<a-flex vertical align="flex-end" justify="space-between" :style="{ padding: '32px' }"> <a-flex
<a-typography> vertical
<a-typography-title :level="3"> {{ module.title }}</a-typography-title> align="flex-end"
justify="space-between"
:style="{ padding: '32px', margin: 'auto', alignItems: 'center' }"
>
<a-typography :style="{ margin: 'auto' }">
<a-typography-title :level="3"> {{ item.title }}</a-typography-title>
</a-typography> </a-typography>
<a-button type="primary" :href="module.url" target="_blank">Get Start</a-button> <a-button type="primary" :href="item.url" target="_blank">Get Start</a-button>
</a-flex> </a-flex>
</a-flex> </a-flex>
</a-card> </a-card>
</div>
</ul> </ul>
</div> </div>
</template> </template>
<style scoped> <style scoped>
h2 {
margin: 10px;
}
ul { ul {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
list-style: none;
padding: 0;
} }
ul > div { ul > div {
margin: 10px; margin: 0 10px;
} }
</style> </style>

View File

@ -1,110 +1,12 @@
<template>
<div class="login-container">
<a-form
:label-col="labelCol"
:model="formState"
:wrapper-col="wrapperCol"
autocomplete="off"
name="basic"
@finish="onFinish"
@finishFailed="onFinishFailed"
>
<a-form-item
:rules="[{ required: true, message: 'Please input your username!' }]"
label="Username"
name="username"
>
<a-input v-model:value="formState.username">
<template #prefix>
<UserOutlined class="site-form-item-icon" />
</template>
</a-input>
</a-form-item>
<a-form-item
:rules="[{ required: true, message: 'Please input your password!' }]"
label="Password"
name="password"
>
<a-input-password v-model:value="formState.password">
<template #prefix>
<LockOutlined class="site-form-item-icon" />
</template>
</a-input-password>
</a-form-item>
<a-form-item :wrapper-col="{ offset: 8, span: 16 }" name="remember">
<a-checkbox v-model:checked="formState.remember">Remember me</a-checkbox>
</a-form-item>
<a-form-item :wrapper-col="{ offset: 8, span: 16 }">
<a-button html-type="submit" type="primary">Submit</a-button>
</a-form-item>
</a-form>
</div>
</template>
<script setup> <script setup>
import { reactive, ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { UserOutlined, LockOutlined } from '@ant-design/icons-vue'
const formState = reactive({ import XiaoMiLogin from '@/components/XiaoMiLogin.vue'
username: '',
password: '',
remember: true
})
const labelCol = {
style: {
width: '200px'
}
}
const wrapperCol = {
span: 12
}
const isLogin = ref(false)
const router = useRouter()
onMounted(() => {
isLogin.value = !!localStorage.getItem('token')
})
const onFinish = async (values) => {
try {
const response = await fetch('http://localhost:3000/users', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
})
const users = await response.json()
const user = users.find(
(user) => user.username === values.username && user.password === values.password
)
// const user = { username: 'test', password: 'test', token: 'test' }
if (user && user.token) {
localStorage.setItem('token', user.token)
isLogin.value = true
await router.push('/')
} else {
alert('Invalid username or password')
}
} catch (error) {
console.error('Error:', error)
}
}
const onFinishFailed = (errorInfo) => {
console.log('Failed:', errorInfo)
}
</script> </script>
<template>
<XiaoMiLogin style="margin: 100px auto"/>
</template>
<style scoped> <style scoped>
.login-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
</style> </style>

9
src/views/NewsView.vue Normal file
View File

@ -0,0 +1,9 @@
<script setup>
import CustomNews from '@/components/CustomNews.vue'
</script>
<template>
<CustomNews></CustomNews>
</template>
<style scoped></style>

12
src/views/ToolView.vue Normal file
View File

@ -0,0 +1,12 @@
<script setup>
import Timestamp from '@/components/CustomTimestamp.vue'
</script>
<template>
<div>this is tool view page</div>
<Timestamp></Timestamp>
</template>
<style scoped>
</style>