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