initial
This commit is contained in:
commit
94d899dfc4
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
13
index.html
Normal file
13
index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
26
package.json
Normal file
26
package.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "vite-demo",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit --skipLibCheck && vite build",
|
||||
"serve": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.22.0",
|
||||
"element-plus": "^1.1.0-beta.19",
|
||||
"unplugin-element-plus": "^0.1.0",
|
||||
"vue": "^3.2.16",
|
||||
"vue-axios": "^3.3.7",
|
||||
"vue-class-component": "8.0.0-rc.1",
|
||||
"vue-router": "^4.0.11",
|
||||
"vuex": "^4.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^1.9.2",
|
||||
"less": "^4.1.1",
|
||||
"typescript": "^4.4.3",
|
||||
"vite": "^2.6.0",
|
||||
"vue-tsc": "^0.3.0"
|
||||
}
|
||||
}
|
||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
18
src/App.vue
Normal file
18
src/App.vue
Normal file
@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<div id="app" >
|
||||
<router-view ></router-view>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Options, Vue } from 'vue-class-component'
|
||||
|
||||
@Options({
|
||||
name: 'App'
|
||||
})
|
||||
export default class App extends Vue{
|
||||
}
|
||||
</script>
|
||||
<style lang="less">
|
||||
@import url('./static/common.less');
|
||||
</style>
|
||||
57
src/components/HelloWorld.vue
Normal file
57
src/components/HelloWorld.vue
Normal file
@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<h1>{{ msg }}</h1>
|
||||
|
||||
<p>
|
||||
Recommended IDE setup:
|
||||
<a href="https://code.visualstudio.com/" target="_blank">VSCode</a>
|
||||
+
|
||||
<a href="https://github.com/johnsoncodehk/volar" target="_blank">Volar</a>
|
||||
</p>
|
||||
|
||||
<p>See <code>README.md</code> for more information.</p>
|
||||
|
||||
<p>
|
||||
<a href="https://vitejs.dev/guide/features.html" target="_blank">
|
||||
Vite Docs
|
||||
</a>
|
||||
|
|
||||
<a href="https://v3.vuejs.org/" target="_blank">Vue 3 Docs</a>
|
||||
</p>
|
||||
|
||||
<button type="button" @click="count++">count is: {{ count }}</button>
|
||||
<p>
|
||||
Edit
|
||||
<code>components/HelloWorld.vue</code> to test hot module replacement.
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Options, Vue } from 'vue-class-component';
|
||||
|
||||
@Options({
|
||||
props: {
|
||||
msg: String
|
||||
}
|
||||
})
|
||||
export default class HelloWorld extends Vue {
|
||||
msg!: string
|
||||
count: number = 0
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
@text-color: #42b983;
|
||||
a {
|
||||
color: @text-color;
|
||||
}
|
||||
label {
|
||||
margin: 0 0.5em;
|
||||
font-weight: bold;
|
||||
}
|
||||
code {
|
||||
background-color: #eee;
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
color: #304455;
|
||||
}
|
||||
</style>
|
||||
32
src/config/menu.ts
Normal file
32
src/config/menu.ts
Normal file
@ -0,0 +1,32 @@
|
||||
export default [
|
||||
{
|
||||
name: 'system',
|
||||
title: '系统管理',
|
||||
icon: 'el-icon-s-operation',
|
||||
child: [
|
||||
{ title: '系统配置', path: '/system/config', icon: 'md-settings' },
|
||||
{ title: '用户管理', path: '/system/user', icon: 'md-contact' },
|
||||
{ title: '角色管理', path: '/system/role', icon: 'md-contacts' },
|
||||
{ title: '博客文章', path: '/system/article', icon: 'md-paper' },
|
||||
{ title: '分析统计', path: '/system/statistics', icon: 'md-pie' }
|
||||
]
|
||||
},{
|
||||
name: 'api',
|
||||
title: 'API数据',
|
||||
icon: 'el-icon-s-data',
|
||||
child: [
|
||||
{ title: '一言', path: '/api/hitokoto', icon: 'md-chatbubbles' },
|
||||
{ title: '照片墙', path: '/api/photoWall', icon: 'md-images' },
|
||||
{ title: '图片资源库', path: '/api/sourceImage', icon: 'md-image' },
|
||||
{ title: '中国行政区划', path: '/api/chinaProvince', icon: 'md-map' },
|
||||
{ title: '歌曲库', path: '/api/music', icon: 'md-musical-note' }
|
||||
]
|
||||
},{
|
||||
name: 'tool',
|
||||
title: '工具',
|
||||
icon: 'el-icon-s-tools',
|
||||
child: [
|
||||
{ title: 'SQL占位符替换', path: '/tool/sqlReplace', icon: 'md-copy' }
|
||||
]
|
||||
}
|
||||
]
|
||||
8
src/env.d.ts
vendored
Normal file
8
src/env.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import { DefineComponent, ComponentCustomProperties } from 'vue'
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
61
src/main.ts
Normal file
61
src/main.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import { router, routePathes, filterExclude } from './router'
|
||||
import store from './store'
|
||||
|
||||
import VueAxios from 'vue-axios'
|
||||
import axios from 'axios'
|
||||
// 配置默认axios参数
|
||||
const service = axios.create({
|
||||
timeout: 10000
|
||||
})
|
||||
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
// 添加请求拦截器
|
||||
service.interceptors.request.use(config => {
|
||||
// 在发送请求之前添加token到请求头
|
||||
// if (localStorage.getItem('login_token')) {
|
||||
// config.headers.token = localStorage.getItem('login_token')
|
||||
// }
|
||||
return config
|
||||
}, err => {
|
||||
// 请求错误的处理
|
||||
ElMessage.error('请求超时,请稍后再试')
|
||||
return Promise.reject(err)
|
||||
})
|
||||
|
||||
service.interceptors.response.use(res=> {
|
||||
return res
|
||||
}, err => {
|
||||
if (err.response.status >= 500) {
|
||||
ElMessage.error('服务器内部错误')
|
||||
} else if (err.response.status >= 400) {
|
||||
if(typeof err.response.data.message === 'string') {
|
||||
ElMessage.warning(err.response.data.message)
|
||||
} else if (Array.isArray(err.response.data.message)) {
|
||||
let message = err.response.data.message.join('<br/>')
|
||||
ElMessage.warning(message)
|
||||
}
|
||||
if (err.response.status === 403) {
|
||||
app.$router.push('/login')
|
||||
}
|
||||
}
|
||||
return Promise.reject(err)
|
||||
})
|
||||
|
||||
// 全局路由导航前置守卫
|
||||
router.beforeEach(function (to, from, next: Function) {
|
||||
app.$store.commit('setBreadcrumb', routePathes[to.path] || [])
|
||||
if(filterExclude.indexOf(to.path) !== -1 || localStorage.getItem('login_token')) {
|
||||
next()
|
||||
} else {
|
||||
next('/login')
|
||||
}
|
||||
})
|
||||
|
||||
const app = createApp(App)
|
||||
.use(router)
|
||||
.use(store)
|
||||
.use(VueAxios, service)
|
||||
.mount('#app')
|
||||
27
src/router.ts
Normal file
27
src/router.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
|
||||
|
||||
import Login from './views/Login.vue'
|
||||
import Home from './views/Home.vue'
|
||||
// import Welcome from '@/views/Welcome.vue'
|
||||
|
||||
const routes: Array<RouteRecordRaw> = [
|
||||
{ path: '/login', name: 'Login', component: Login },
|
||||
{ path: '/', name: 'Home', component: Home, children: []}
|
||||
]
|
||||
|
||||
export const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
import menus from './config/menu'
|
||||
export const routePathes : {[propName: string]: string[]} = {
|
||||
'/': ['首页'],
|
||||
}
|
||||
for(let menu of menus) {
|
||||
for(let submenu of menu.child) {
|
||||
routePathes[submenu.path] = ['首页', menu.title, submenu.title]
|
||||
}
|
||||
}
|
||||
|
||||
export const filterExclude = ['/login']
|
||||
78
src/static/common.less
Normal file
78
src/static/common.less
Normal file
@ -0,0 +1,78 @@
|
||||
@panel-shadow-color: #ddd;
|
||||
html,body,#app,.layout {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
}
|
||||
.layout-header{
|
||||
color: #fff;
|
||||
background-color: #515a6e;
|
||||
div.main-title {
|
||||
display: inline-block;
|
||||
font-size: 22px;
|
||||
line-height: 60px;
|
||||
}
|
||||
.nav-btns-right {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 9px;
|
||||
}
|
||||
}
|
||||
.layout-main {
|
||||
padding: 10px !important;
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
> .layout-breadcrumb{
|
||||
padding: 10px 15px 0;
|
||||
}
|
||||
> .layout-content{
|
||||
margin: 15px;
|
||||
overflow: auto;
|
||||
background: #fff;
|
||||
box-shadow: -2px -2px 5px 0px @panel-shadow-color;
|
||||
padding: 10px;
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
}
|
||||
> .layout-copy{
|
||||
text-align: center;
|
||||
padding: 10px 0;
|
||||
color: #9ea7b4;
|
||||
}
|
||||
}
|
||||
.search-title {
|
||||
line-height: 32px;
|
||||
text-align: right;
|
||||
padding-right: 5px;
|
||||
}
|
||||
.table-container {
|
||||
position: relative;
|
||||
}
|
||||
.btn-container {
|
||||
padding: 10px 0;
|
||||
button {
|
||||
margin-right: 3px;
|
||||
}
|
||||
.search-btn {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
.page-container {
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
.carsouel-img {
|
||||
position: relative;
|
||||
left: 50%;transform:
|
||||
translateX(-50%);
|
||||
height: 500px;
|
||||
width: auto;
|
||||
}
|
||||
.main-view {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
.main-view .search-row:not(:last-child) {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
49
src/store/index.ts
Normal file
49
src/store/index.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { createStore } from 'vuex'
|
||||
import { StateType, UserInfo } from './types'
|
||||
|
||||
class Store {
|
||||
constructor(breadcrumb: string[]) {
|
||||
this.state.breadcrumb = breadcrumb
|
||||
}
|
||||
state: StateType = {
|
||||
loginInfo: {
|
||||
userInfo: null,
|
||||
token: null
|
||||
},
|
||||
breadcrumb: [],
|
||||
pageSizeOpts: [10, 20, 50, 100]
|
||||
}
|
||||
mutations = {
|
||||
/**
|
||||
* 登录
|
||||
* @param {Object} state
|
||||
* @param {UserInfo} data 登录数据
|
||||
*/
|
||||
login(state: StateType, data: {token: string, userInfo: UserInfo}): void {
|
||||
localStorage.setItem('login_token', data.token)
|
||||
state.loginInfo.token = data.token
|
||||
state.loginInfo.userInfo = data.userInfo
|
||||
},
|
||||
/**
|
||||
* 注销
|
||||
* @param {Object} state
|
||||
*/
|
||||
logout(state: StateType): void {
|
||||
localStorage.removeItem('login_token')
|
||||
state.loginInfo.token = null
|
||||
state.loginInfo.userInfo = null
|
||||
},
|
||||
/**
|
||||
* 设置面包屑导航
|
||||
* @param {Object} state
|
||||
* @param {Array} breadcrumbArr 面包屑导航的内容
|
||||
*/
|
||||
setBreadcrumb(state: StateType, breadcrumbArr: string[]): void {
|
||||
localStorage.setItem('breadcrumb', JSON.stringify(breadcrumbArr))
|
||||
state.breadcrumb = breadcrumbArr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const breadcrumbStr = localStorage.getItem('breadcrumb')
|
||||
export default createStore(new Store(breadcrumbStr ? JSON.parse(breadcrumbStr) : []))
|
||||
15
src/store/types.ts
Normal file
15
src/store/types.ts
Normal file
@ -0,0 +1,15 @@
|
||||
export interface UserInfo {
|
||||
_id: string
|
||||
username: string // 用户名
|
||||
realname: string // 昵称
|
||||
role_ids: string[] // 角色ID
|
||||
}
|
||||
|
||||
export interface StateType {
|
||||
loginInfo: { // 登录信息
|
||||
userInfo: null | UserInfo
|
||||
token: null | string
|
||||
}
|
||||
breadcrumb: string[] // 面包屑导航文字
|
||||
pageSizeOpts: number[] // 分页大小可选列表
|
||||
}
|
||||
5
src/types.ts
Normal file
5
src/types.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export type VForm = Vue & {
|
||||
validate: (callback: (valid: boolean) => void) => void
|
||||
resetValidation: () => boolean
|
||||
reset: () => void
|
||||
}
|
||||
108
src/views/Home.vue
Normal file
108
src/views/Home.vue
Normal file
@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<el-container class="layout">
|
||||
<el-header class="layout-header">
|
||||
<div class="main-title">博客API管理后台</div>
|
||||
<div class="nav-btns-right">
|
||||
<el-dropdown @command="dropdownMenuCommand">
|
||||
<el-button type="text">
|
||||
<i class="el-icon-user-solid"></i>
|
||||
{{ realname }}
|
||||
<i class="el-icon-arrow-down el-icon--right"></i>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="home">返回首页</el-dropdown-item>
|
||||
<el-dropdown-item command="changePassword">修改密码</el-dropdown-item>
|
||||
<el-dropdown-item command="logout" divided>退出</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</el-header>
|
||||
<el-container>
|
||||
<el-aside width="200px">
|
||||
<el-menu :default-active="activeMenuItem" :default-openeds="openMenuNames" style="height: 100%;">
|
||||
<el-sub-menu v-for="menu in menus" :key="menu.name" :index="menu.name">
|
||||
<template #title>
|
||||
<i :class="menu.icon"></i>
|
||||
<span>{{menu.title}}</span>
|
||||
</template>
|
||||
<el-menu-item v-for="(subItem,subIndex) in menu.child" :key="subIndex" :index="subItem.path" @click="openMenu(subItem.path)">
|
||||
{{subItem.title}}
|
||||
</el-menu-item>
|
||||
</el-sub-menu>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
<el-main class="layout-main">
|
||||
<div class="layout-breadcrumb">
|
||||
<el-breadcrumb separator="/">
|
||||
<el-breadcrumb-item v-for="(item,index) in $store.state.breadcrumb" :key="index">{{item}}</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
<div class="layout-content">
|
||||
<router-view class="main-view"></router-view>
|
||||
</div>
|
||||
<div class="layout-copy">2016-{{currentYear}} © colorfulsweet</div>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Options, Vue } from 'vue-class-component'
|
||||
import { ElContainer, ElHeader, ElMain, ElAside, ElDropdown, ElDropdownMenu, ElDropdownItem, ElButton, ElMenu, ElSubMenu, ElMenuItem, ElBreadcrumb, ElBreadcrumbItem } from 'element-plus'
|
||||
import menus from '../config/menu'
|
||||
|
||||
@Options({
|
||||
name: 'Home',
|
||||
components: { ElContainer, ElHeader, ElMain, ElAside, ElDropdown, ElDropdownMenu, ElDropdownItem, ElButton, ElMenu, ElSubMenu, ElMenuItem, ElBreadcrumb, ElBreadcrumbItem }
|
||||
})
|
||||
export default class Home extends Vue{
|
||||
private currentYear = new Date().getFullYear()
|
||||
// 菜单项
|
||||
private menus = menus
|
||||
private activeMenuItem: string | null = null
|
||||
private openMenuNames: string[] = []
|
||||
get realname(): string { // 当前用户的显示名称
|
||||
return this.$store.state.loginInfo.userInfo
|
||||
? this.$store.state.loginInfo.userInfo.realname : null
|
||||
}
|
||||
async created(): Promise<void> {
|
||||
this.activeMenuItem = this.$route.path
|
||||
if(this.activeMenuItem) {
|
||||
let result = /^\/(.*)\//.exec(this.activeMenuItem)
|
||||
if(result) {
|
||||
this.openMenuNames.push(result[1])
|
||||
}
|
||||
}
|
||||
if(!localStorage.getItem('login_token')) {
|
||||
this.$router.push('/login')
|
||||
return
|
||||
}
|
||||
const { data } = await this.$http.post('/api/common/verifyToken', {token: localStorage.getItem('login_token')})
|
||||
if(data.status) {
|
||||
// 如果是已过期的token 服务端会签发新的token
|
||||
this.$store.commit('login', {token: data.newToken || localStorage.getItem('login_token'), userInfo: data.userInfo})
|
||||
} else {
|
||||
this.$router.push('/login')
|
||||
}
|
||||
}
|
||||
dropdownMenuCommand(command: string): void {
|
||||
switch(command) {
|
||||
case 'home': // 返回首页
|
||||
this.$router.push('/')
|
||||
break
|
||||
case 'changePassword': // 修改密码
|
||||
// TODO
|
||||
break
|
||||
case 'logout': //注销
|
||||
this.$store.commit('logout')
|
||||
this.$router.push('/login')
|
||||
break
|
||||
}
|
||||
}
|
||||
openMenu(path: string): void {
|
||||
this.$router.push(path)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
94
src/views/Login.vue
Normal file
94
src/views/Login.vue
Normal file
@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<div class="login-wrapper">
|
||||
<h2 class="title">博客API管理后台</h2>
|
||||
<el-form ref="loginForm" :model="userInfo" :rules="ruleValidate" :label-width="80">
|
||||
<el-form-item label="用户名" prop="username">
|
||||
<el-input v-model="userInfo.username" @on-enter="login"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="密码" prop="password">
|
||||
<el-input v-model="userInfo.password" type="password" @on-enter="login" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="login-btn">
|
||||
<el-button type="primary" @click="login">登录</el-button>
|
||||
<el-tooltip content="无需账号密码,只有查询权限" placement="bottom">
|
||||
<el-button @click="guestLogin">访客模式</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Options, Vue } from 'vue-class-component'
|
||||
import { ref } from 'vue'
|
||||
import { VForm } from "../types"
|
||||
import { ElMessage, ElButton, ElForm, ElFormItem, ElInput, ElTooltip } from 'element-plus'
|
||||
import { AxiosResponse } from 'axios'
|
||||
|
||||
@Options({
|
||||
name: 'Login',
|
||||
components: { ElButton, ElForm, ElFormItem, ElInput, ElTooltip }
|
||||
})
|
||||
export default class Login extends Vue {
|
||||
private readonly loginForm: VForm = ref('loginForm')
|
||||
private userInfo: UserInfo = {
|
||||
username: null,
|
||||
password: null
|
||||
}
|
||||
private ruleValidate = {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' }
|
||||
],
|
||||
}
|
||||
/**
|
||||
* 登录
|
||||
*/
|
||||
async login() {
|
||||
this.loginForm.validate(async (valid: boolean) => {
|
||||
if(!valid) return
|
||||
const { data } = await this.$http.post<UserInfo, AxiosResponse<any>>('/api/common/login', this.userInfo)
|
||||
if(data.token) {
|
||||
this.$store.commit('login', data)
|
||||
this.$router.push('/')
|
||||
} else {
|
||||
ElMessage.error(data.message)
|
||||
}
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 访客模式
|
||||
*/
|
||||
async guestLogin() {
|
||||
const { data } = await this.$http.post<never, AxiosResponse<any>>('/api/common/guestLogin')
|
||||
if (data.status && data.data.token) {
|
||||
this.$store.commit('login', data.data)
|
||||
this.$router.push('/')
|
||||
} else {
|
||||
ElMessage.error(data.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
type UserInfo = {
|
||||
username: string | null,
|
||||
password: string | null
|
||||
};
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.login-wrapper {
|
||||
width: 400px;
|
||||
vertical-align: middle;
|
||||
position: absolute;
|
||||
top: 40%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
.title {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.login-btn {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
135
src/views/Welcome.vue
Normal file
135
src/views/Welcome.vue
Normal file
@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="clock-circle">
|
||||
<div class="clock-face">
|
||||
<div class="clock-hour" :style="{transform: `rotate(${clock.hourRotate}deg)`}" ></div>
|
||||
<div class="clock-minute" :style="{transform: `rotate(${clock.minuteRotate}deg)`}"></div>
|
||||
<div class="clock-second" :style="{transform: `rotate(${clock.secondRotate}deg)`}"></div>
|
||||
</div>
|
||||
</div>
|
||||
<h2 class="time-text">{{timeText}}</h2>
|
||||
<div class="nav-list">
|
||||
<Row v-for="menu in menus" :key="menu.name">
|
||||
<Col :span="3" class="nav-title">{{ menu.title }}</Col>
|
||||
<Col :span="21">
|
||||
<router-link :to="submenu.path" v-for="submenu in menu.child" :key="submenu.path" class="nav-item">
|
||||
<Icon :type="submenu.icon" /> {{submenu.title}}
|
||||
</router-link>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator'
|
||||
import moment from 'moment'
|
||||
import menus from '../config/menu'
|
||||
|
||||
@Component({})
|
||||
export default class Welcome extends Vue {
|
||||
private menus = menus
|
||||
private timeText!: string
|
||||
private clock = {
|
||||
hourRotate: 0,
|
||||
minuteRotate: 0,
|
||||
secondRotate: 0
|
||||
}
|
||||
private clockTimer!: number | null
|
||||
|
||||
created(): void {
|
||||
const timedUpdate = (): void => {
|
||||
this.updateClock()
|
||||
this.clockTimer = setTimeout(timedUpdate, 1000)
|
||||
}
|
||||
timedUpdate()
|
||||
}
|
||||
beforeDestroy(): void {
|
||||
if(this.clockTimer) {
|
||||
clearTimeout(this.clockTimer)
|
||||
this.clockTimer = null
|
||||
}
|
||||
}
|
||||
updateClock(): void {
|
||||
const now = moment()
|
||||
this.timeText = now.format('YYYY年M月D日 HH:mm:ss')
|
||||
this.clock.secondRotate = now.seconds() * 6
|
||||
this.clock.minuteRotate = now.minutes() * 6 + this.clock.secondRotate / 60
|
||||
this.clock.hourRotate = ((now.hours() % 12) / 12) * 360 + 90 + this.clock.minuteRotate / 12
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.clock-circle {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
border: 8px solid #000;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 1px 8px rgba(34,34,34,0.3),inset 0 1px 8px rgba(34,34,34,0.3);
|
||||
}
|
||||
|
||||
.clock-face {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin: -6px 0 0 -6px;
|
||||
background: #000;
|
||||
border-radius: 6px;
|
||||
content: "";
|
||||
display: block
|
||||
}
|
||||
}
|
||||
|
||||
.clock-hour,.clock-minute,.clock-second {
|
||||
width: 0;
|
||||
height: 0;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
background: #000;
|
||||
}
|
||||
.clock-hour {
|
||||
margin: -4px 0 -4px -25%;
|
||||
padding: 4px 0 4px 25%;
|
||||
transform-origin: 100% 50%;
|
||||
border-radius: 4px 0 0 4px
|
||||
}
|
||||
|
||||
.clock-minute {
|
||||
margin: -40% -3px 0;
|
||||
padding: 40% 3px 0;
|
||||
transform-origin: 50% 100%;
|
||||
border-radius: 3px 3px 0 0
|
||||
}
|
||||
|
||||
.clock-second {
|
||||
margin: -40% -1px 0 0;
|
||||
padding: 40% 1px 0;
|
||||
transform-origin: 50% 100%
|
||||
}
|
||||
.time-text {
|
||||
text-align: center;
|
||||
}
|
||||
.nav-list {
|
||||
.nav-title {
|
||||
font-size: 16px;
|
||||
line-height: 66px;
|
||||
text-align: right;
|
||||
padding-right: 20px;
|
||||
}
|
||||
.nav-item {
|
||||
display: inline-block;
|
||||
width: 200px;
|
||||
font-size: 16px;
|
||||
margin: 10px;
|
||||
border: 1px solid #ccc;
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
16
tsconfig.json
Normal file
16
tsconfig.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"sourceMap": true,
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"experimentalDecorators": true,
|
||||
"lib": ["esnext", "dom"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||
}
|
||||
26
vite.config.ts
Normal file
26
vite.config.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import ElementPlus from 'unplugin-element-plus/vite'
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
ElementPlus(),
|
||||
],
|
||||
base: './',
|
||||
server: {
|
||||
port: 8080,
|
||||
proxy: {
|
||||
'^/api': {
|
||||
// target: 'http://localhost:3301'
|
||||
target: 'https://www.colorfulsweet.site',
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
assetsDir: 'assets', // 静态资源的存放路径, 相对于outDir
|
||||
cssCodeSplit: true
|
||||
}
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user