Compare commits
3 Commits
1e7c921ae2
...
8d8937e2ff
| Author | SHA1 | Date | |
|---|---|---|---|
| 8d8937e2ff | |||
| 2810c44f8a | |||
| e68da9927e |
@ -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"
|
||||||
|
|||||||
99
src/App.vue
99
src/App.vue
@ -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">
|
||||||
<RouterView />
|
<a-layout-header :style="{ background: '#fff', width: '100%' }">
|
||||||
</article>
|
<a-menu
|
||||||
<aside>
|
v-model:selectedKeys="current"
|
||||||
<CustomCard :style="{ margin: 'auto' }" />
|
mode="horizontal"
|
||||||
</aside>
|
:items="items"
|
||||||
</main>
|
theme="light"
|
||||||
|
:style="{ justifyContent: 'center' }"
|
||||||
|
/>
|
||||||
|
</a-layout-header>
|
||||||
|
<a-layout>
|
||||||
|
<a-layout-content
|
||||||
|
theme="light"
|
||||||
|
:style="{ flexDirection: 'column', justifyContent: 'flex-start', alignItems: 'center' }"
|
||||||
|
>
|
||||||
|
<RouterView />
|
||||||
|
</a-layout-content>
|
||||||
|
<a-layout-sider
|
||||||
|
theme="light"
|
||||||
|
:collapsible="true"
|
||||||
|
: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: '登录'
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
</script>
|
||||||
const route = useRoute()
|
|
||||||
|
|
||||||
const updateLoginStatus = () => {
|
|
||||||
isLogin.value = !!localStorage.getItem('token')
|
|
||||||
console.log('isLogin.value:', isLogin.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(updateLoginStatus)
|
|
||||||
watch(route, updateLoginStatus)
|
|
||||||
</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>
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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%;
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 50%;
|
||||||
|
img {
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar img {
|
@media screen and (max-width: 800px) {
|
||||||
border-radius: 50%;
|
.card {
|
||||||
|
width: 160px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.name,
|
@media screen and (min-width: 800px) {
|
||||||
.content {
|
.card {
|
||||||
white-space: nowrap;
|
width: 200px;
|
||||||
overflow: hidden;
|
}
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
82
src/components/CustomNews.vue
Normal file
82
src/components/CustomNews.vue
Normal 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>
|
||||||
55
src/components/CustomTimestamp.vue
Normal file
55
src/components/CustomTimestamp.vue
Normal 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>
|
||||||
@ -1,43 +1,39 @@
|
|||||||
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'
|
||||||
const routes = [
|
import ToolView from '@/views/ToolView.vue'
|
||||||
{
|
import LoginView from '@/views/LoginView.vue'
|
||||||
path: '/',
|
import NewsView from '@/views/NewsView.vue'
|
||||||
name: 'home',
|
|
||||||
component: HomeView
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/about',
|
|
||||||
name: 'about',
|
|
||||||
component: () => import('../views/AboutView.vue')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/login',
|
|
||||||
name: 'login',
|
|
||||||
component: () => import('../views/LoginView.vue'),
|
|
||||||
meta: {
|
|
||||||
requiresAuth: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
routes: routes
|
routes: [
|
||||||
})
|
{
|
||||||
|
path: '/',
|
||||||
router.beforeEach((to, from, next) => {
|
name: 'home',
|
||||||
const isLogin = !!localStorage.getItem('token')
|
component: HomeView
|
||||||
const requiresAuth = to.matched.some((record) => !!record.meta['requiresAuth'])
|
},
|
||||||
|
{
|
||||||
console.log('isLogin', isLogin)
|
path: '/about',
|
||||||
console.log('requiresAuth', requiresAuth)
|
name: 'about',
|
||||||
if (!requiresAuth && !isLogin) {
|
component: AboutView
|
||||||
next('/login')
|
},
|
||||||
} else {
|
{
|
||||||
next()
|
path: '/tool',
|
||||||
}
|
name: 'tool',
|
||||||
|
component: ToolView
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
name: 'login',
|
||||||
|
component: LoginView
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/news',
|
||||||
|
name: 'news',
|
||||||
|
component: NewsView
|
||||||
|
}
|
||||||
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|||||||
15
src/utils/auth.js
Normal file
15
src/utils/auth.js
Normal 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
6
src/utils/errorCode.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
401: '认证失败',
|
||||||
|
403: '没有权限',
|
||||||
|
404: '资源不存在',
|
||||||
|
default: '系统未知错误'
|
||||||
|
}
|
||||||
54
src/utils/time.js
Normal file
54
src/utils/time.js
Normal 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 }
|
||||||
@ -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
9
src/views/GameView.vue
Normal 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>
|
||||||
@ -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"
|
||||||
<a-flex justify="space-between">
|
:key="index"
|
||||||
<a-flex vertical align="flex-end" justify="space-between" :style="{ padding: '32px' }">
|
:style="cardStyle"
|
||||||
<a-typography>
|
:body-style="{ padding: 0, overflow: 'hidden' }"
|
||||||
<a-typography-title :level="3"> “{{ module.title }}”</a-typography-title>
|
hoverable
|
||||||
</a-typography>
|
>
|
||||||
<a-button type="primary" :href="module.url" target="_blank">Get Start</a-button>
|
<a-flex justify="space-between">
|
||||||
</a-flex>
|
<a-flex
|
||||||
|
vertical
|
||||||
|
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-button type="primary" :href="item.url" target="_blank">Get Start</a-button>
|
||||||
</a-flex>
|
</a-flex>
|
||||||
</a-card>
|
</a-flex>
|
||||||
</div>
|
</a-card>
|
||||||
</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>
|
||||||
|
|||||||
@ -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;
|
</style>
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
height: 100vh;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
9
src/views/NewsView.vue
Normal file
9
src/views/NewsView.vue
Normal 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
12
src/views/ToolView.vue
Normal 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>
|
||||||
Loading…
Reference in New Issue
Block a user