This commit is contained in:
朱进禄 2021-10-03 10:11:06 +08:00
commit 94d899dfc4
20 changed files with 1932 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
node_modules
.DS_Store
dist
dist-ssr
*.local

13
index.html Normal file
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

18
src/App.vue Normal file
View 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>

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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}} &copy; 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
View 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
View 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
View 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
View 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
}
})

1159
yarn.lock Normal file

File diff suppressed because it is too large Load Diff