角色管理
This commit is contained in:
parent
94d899dfc4
commit
58892dea40
@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
"dependencies": {
|
||||
"axios": "^0.22.0",
|
||||
"element-plus": "^1.1.0-beta.19",
|
||||
"moment": "^2.29.1",
|
||||
"unplugin-element-plus": "^0.1.0",
|
||||
"vue": "^3.2.16",
|
||||
"vue-axios": "^3.3.7",
|
||||
|
||||
10
src/App.vue
10
src/App.vue
@ -1,16 +1,22 @@
|
||||
<template>
|
||||
<div id="app" >
|
||||
<router-view ></router-view>
|
||||
<el-config-provider :locale="locale" >
|
||||
<router-view ></router-view>
|
||||
</el-config-provider>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Options, Vue } from 'vue-class-component'
|
||||
import { ElConfigProvider } from 'element-plus'
|
||||
import zhCn from 'element-plus/lib/locale/lang/zh-cn'
|
||||
|
||||
@Options({
|
||||
name: 'App'
|
||||
name: 'App',
|
||||
components: { ElConfigProvider }
|
||||
})
|
||||
export default class App extends Vue{
|
||||
locale = zhCn
|
||||
}
|
||||
</script>
|
||||
<style lang="less">
|
||||
|
||||
@ -1,57 +0,0 @@
|
||||
<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>
|
||||
@ -4,29 +4,29 @@ export default [
|
||||
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' }
|
||||
{ title: '系统配置', path: '/system/config' },
|
||||
{ title: '用户管理', path: '/system/user' },
|
||||
{ title: '角色管理', path: '/system/role' },
|
||||
{ title: '博客文章', path: '/system/article' },
|
||||
{ title: '分析统计', path: '/system/statistics' }
|
||||
]
|
||||
},{
|
||||
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' }
|
||||
{ title: '一言', path: '/api/hitokoto' },
|
||||
{ title: '照片墙', path: '/api/photoWall' },
|
||||
{ title: '图片资源库', path: '/api/sourceImage' },
|
||||
{ title: '中国行政区划', path: '/api/chinaProvince' },
|
||||
{ title: '歌曲库', path: '/api/music' }
|
||||
]
|
||||
},{
|
||||
name: 'tool',
|
||||
title: '工具',
|
||||
icon: 'el-icon-s-tools',
|
||||
child: [
|
||||
{ title: 'SQL占位符替换', path: '/tool/sqlReplace', icon: 'md-copy' }
|
||||
{ title: 'SQL占位符替换', path: '/tool/sqlReplace' }
|
||||
]
|
||||
}
|
||||
]
|
||||
2
src/env.d.ts
vendored
2
src/env.d.ts
vendored
@ -1,7 +1,7 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import { DefineComponent, ComponentCustomProperties } from 'vue'
|
||||
import { DefineComponent } from 'vue'
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
|
||||
12
src/main.ts
12
src/main.ts
@ -10,14 +10,15 @@ const service = axios.create({
|
||||
timeout: 10000
|
||||
})
|
||||
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ElMessage, ElLoading } from 'element-plus'
|
||||
|
||||
// 添加请求拦截器
|
||||
service.interceptors.request.use(config => {
|
||||
// 在发送请求之前添加token到请求头
|
||||
// if (localStorage.getItem('login_token')) {
|
||||
// config.headers.token = localStorage.getItem('login_token')
|
||||
// }
|
||||
const token = localStorage.getItem('login_token')
|
||||
if (token !== null && config.headers) {
|
||||
config.headers.token = token
|
||||
}
|
||||
return config
|
||||
}, err => {
|
||||
// 请求错误的处理
|
||||
@ -45,7 +46,7 @@ service.interceptors.response.use(res=> {
|
||||
})
|
||||
|
||||
// 全局路由导航前置守卫
|
||||
router.beforeEach(function (to, from, next: Function) {
|
||||
router.beforeEach(function (to, from, next) {
|
||||
app.$store.commit('setBreadcrumb', routePathes[to.path] || [])
|
||||
if(filterExclude.indexOf(to.path) !== -1 || localStorage.getItem('login_token')) {
|
||||
next()
|
||||
@ -58,4 +59,5 @@ const app = createApp(App)
|
||||
.use(router)
|
||||
.use(store)
|
||||
.use(VueAxios, service)
|
||||
.directive('loading', ElLoading.directive)
|
||||
.mount('#app')
|
||||
|
||||
9
src/model/api/hitokoto.ts
Normal file
9
src/model/api/hitokoto.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export default interface HitokotoModel {
|
||||
_id: string
|
||||
hitokoto: string
|
||||
type: string
|
||||
from: string
|
||||
creator: string
|
||||
created_at: Date
|
||||
number: number
|
||||
}
|
||||
35
src/model/api/music.ts
Normal file
35
src/model/api/music.ts
Normal file
@ -0,0 +1,35 @@
|
||||
export interface MusicModel {
|
||||
_id: string
|
||||
name: string // 文件名
|
||||
ext: string // 扩展名
|
||||
md5?: string // md5哈希值
|
||||
size: number // 文件大小
|
||||
title?: string // 标题
|
||||
album?: string // 唱片集
|
||||
artist?: string // 艺术家
|
||||
lib_id: string // 歌单ID
|
||||
lyric_id: string // 歌词ID
|
||||
}
|
||||
|
||||
export interface MusicLibModel {
|
||||
_id: string
|
||||
name: string // 歌单名称
|
||||
path: string // 歌单文件路径
|
||||
}
|
||||
|
||||
export interface MusicLyricModel {
|
||||
_id?: string
|
||||
cloud_id?: number
|
||||
name?: string
|
||||
lyric?: string
|
||||
}
|
||||
|
||||
export interface MusicPlayerItem {
|
||||
id: number
|
||||
name: string
|
||||
artist: string
|
||||
album: string
|
||||
url: string
|
||||
cover: string
|
||||
lrc: string | undefined
|
||||
}
|
||||
9
src/model/api/photowall.ts
Normal file
9
src/model/api/photowall.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export default interface PhotoWallModel {
|
||||
_id: string
|
||||
name: string // 图片文件名
|
||||
md5: string // 文件md5哈希值
|
||||
thumbnail: string // 缩略图名称
|
||||
width: number // 宽度
|
||||
height: number // 高度
|
||||
index: number // 序号
|
||||
}
|
||||
17
src/model/api/source-image.ts
Normal file
17
src/model/api/source-image.ts
Normal file
@ -0,0 +1,17 @@
|
||||
export interface SourceImageModel {
|
||||
_id: string
|
||||
mime: string // MIME类型
|
||||
hash: string // 图片md5值
|
||||
size: number // 图片大小
|
||||
label: string[] // 图片标签
|
||||
img: ArrayBuffer // 图片二进制数据
|
||||
created_at: Date
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片标签
|
||||
*/
|
||||
export interface ImageLabel {
|
||||
name: string // 标签名称
|
||||
color: string // 标签颜色
|
||||
}
|
||||
65
src/model/baselist.ts
Normal file
65
src/model/baselist.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import { Vue } from 'vue-class-component'
|
||||
import { Page } from './common.dto'
|
||||
import moment from 'moment'
|
||||
|
||||
export default abstract class BaseList<T extends Page> extends Vue {
|
||||
/**
|
||||
* 表格数据加载中
|
||||
*/
|
||||
protected loading: boolean = false
|
||||
/**
|
||||
* 表单提交中
|
||||
*/
|
||||
protected modalLoading: boolean = true
|
||||
/**
|
||||
* 数据总数
|
||||
*/
|
||||
protected total: number = 0
|
||||
protected abstract search: T
|
||||
/**
|
||||
* 加载数据的实现
|
||||
*/
|
||||
abstract loadData(): Promise<void>
|
||||
/**
|
||||
* 加载数据
|
||||
* @param resetPage 是否重置页码
|
||||
*/
|
||||
loadDataBase(resetPage: boolean = false): void {
|
||||
if(resetPage) {
|
||||
this.search.pageNum = 1
|
||||
}
|
||||
this.loadData()
|
||||
}
|
||||
/**
|
||||
* 重置搜索项
|
||||
*/
|
||||
reset() {
|
||||
this.search.reset()
|
||||
this.loadData()
|
||||
}
|
||||
/**
|
||||
* 页码变更
|
||||
* @param pageNum 页码
|
||||
*/
|
||||
pageChange(pageNum: number) {
|
||||
this.search.pageNum = pageNum
|
||||
this.loadData()
|
||||
}
|
||||
|
||||
/**
|
||||
* 每页数据条数变更
|
||||
* @param pageSize 每页数据条数
|
||||
*/
|
||||
pageSizeChange(pageSize: number) {
|
||||
this.search.limit = pageSize
|
||||
this.loadData()
|
||||
}
|
||||
|
||||
/**
|
||||
* 日期时间格式化
|
||||
* @param dateStr 日期时间
|
||||
*/
|
||||
datetimeFormat(dateStr: string) {
|
||||
return moment(dateStr).format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
}
|
||||
21
src/model/common.dto.ts
Normal file
21
src/model/common.dto.ts
Normal file
@ -0,0 +1,21 @@
|
||||
export interface MsgResult {
|
||||
status: boolean
|
||||
message: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询基础类
|
||||
*/
|
||||
export class Page {
|
||||
// 页码
|
||||
pageNum: number = 1
|
||||
// 每页数据条数
|
||||
limit: number = 10
|
||||
/**
|
||||
* 重置分页
|
||||
*/
|
||||
public reset() {
|
||||
this.pageNum = 1
|
||||
this.limit = 10
|
||||
}
|
||||
}
|
||||
34
src/model/system/article.ts
Normal file
34
src/model/system/article.ts
Normal file
@ -0,0 +1,34 @@
|
||||
// import { CreateElement, VNode } from 'vue'
|
||||
|
||||
export interface ArticleModel {
|
||||
_id: string
|
||||
path: string[] // 访问路径
|
||||
categories: string[] //分类
|
||||
tags: string[] // 标签
|
||||
title: string // 标题
|
||||
create_date: Date // 文章发布日期
|
||||
content_len: number // 正文长度
|
||||
is_splited: boolean // 是否分词
|
||||
}
|
||||
|
||||
export interface TreeNodeBase {
|
||||
name: string | null // 节点名称(ID)
|
||||
deep: number // 节点深度(根节点为0)
|
||||
}
|
||||
|
||||
export interface TreeNode extends TreeNodeBase {
|
||||
expand: boolean // 是否展开
|
||||
id?: string // 文章ID
|
||||
title: string //显示的文字
|
||||
children?: TreeNode[] // 子节点
|
||||
loading?: boolean // 是否显示加载中
|
||||
nodeKey?: number // 树节点唯一标识
|
||||
isDirectory: boolean // 该节点是否为目录
|
||||
// render?: (h: CreateElement, {data}: {data: TreeNode}) => Array<VNode | string>
|
||||
}
|
||||
|
||||
export interface TreeNodeSource {
|
||||
_id: string
|
||||
cnt: number
|
||||
article_id: string | null
|
||||
}
|
||||
9
src/model/system/system-config.ts
Normal file
9
src/model/system/system-config.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export interface SystemConfigModel {
|
||||
_id?: string
|
||||
name: string // 配置项名称
|
||||
value: object | string // 配置项内容
|
||||
description: string // 描述
|
||||
is_public: boolean // 是否公开
|
||||
created_at?: Date // 创建时间
|
||||
updated_at?: Date // 更新时间
|
||||
}
|
||||
10
src/model/system/system-role.ts
Normal file
10
src/model/system/system-role.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export interface SystemRoleModel {
|
||||
_id: string | null
|
||||
name: string | null // 角色名称
|
||||
description: string | null // 描述
|
||||
methods: string[] // 允许的请求类型(优先级3)
|
||||
include_uri: string[] // 包含的URI(优先级2)
|
||||
exclude_uri: string[] // 排除的URI(优先级1)
|
||||
created_at?: Date // 创建时间
|
||||
updated_at?: Date // 更新时间
|
||||
}
|
||||
9
src/model/system/system-user.ts
Normal file
9
src/model/system/system-user.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export interface SystemUserModel {
|
||||
_id: string | null
|
||||
username: string | null
|
||||
password: string | null
|
||||
realname: string | null
|
||||
role_ids: string[]
|
||||
created_at?: Date
|
||||
updated_at?: Date
|
||||
}
|
||||
@ -2,11 +2,16 @@ 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'
|
||||
|
||||
import Welcome from './views/Welcome.vue'
|
||||
import SystemUser from './views/system/SystemUser.vue'
|
||||
import SystemRole from './views/system/SystemRole.vue'
|
||||
const routes: Array<RouteRecordRaw> = [
|
||||
{ path: '/login', name: 'Login', component: Login },
|
||||
{ path: '/', name: 'Home', component: Home, children: []}
|
||||
{ path: '/', name: 'Home', component: Home, children: [
|
||||
{ path: '/', name: 'Welcome', component: Welcome },
|
||||
{ path: '/system/user', name: 'SystemUser', component: SystemUser },
|
||||
{ path: '/system/role', name: 'SystemRole', component: SystemRole },
|
||||
]}
|
||||
]
|
||||
|
||||
export const router = createRouter({
|
||||
|
||||
@ -41,16 +41,11 @@ html,body,#app,.layout {
|
||||
color: #9ea7b4;
|
||||
}
|
||||
}
|
||||
.search-title {
|
||||
line-height: 32px;
|
||||
text-align: right;
|
||||
padding-right: 5px;
|
||||
}
|
||||
.table-container {
|
||||
position: relative;
|
||||
}
|
||||
.btn-container {
|
||||
padding: 10px 0;
|
||||
margin-bottom: 10px;
|
||||
button {
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
export type VForm = Vue & {
|
||||
validate: (callback: (valid: boolean) => void) => void
|
||||
resetValidation: () => boolean
|
||||
reset: () => void
|
||||
}
|
||||
@ -51,6 +51,7 @@
|
||||
<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 { AxiosResponse } from 'axios'
|
||||
import menus from '../config/menu'
|
||||
|
||||
@Options({
|
||||
@ -58,12 +59,12 @@ import menus from '../config/menu'
|
||||
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()
|
||||
currentYear = new Date().getFullYear()
|
||||
// 菜单项
|
||||
private menus = menus
|
||||
private activeMenuItem: string | null = null
|
||||
private openMenuNames: string[] = []
|
||||
get realname(): string { // 当前用户的显示名称
|
||||
menus = menus
|
||||
activeMenuItem: string | null = null
|
||||
openMenuNames: string[] = []
|
||||
get realname(): null | string { // 当前用户的显示名称
|
||||
return this.$store.state.loginInfo.userInfo
|
||||
? this.$store.state.loginInfo.userInfo.realname : null
|
||||
}
|
||||
@ -79,7 +80,7 @@ export default class Home extends Vue{
|
||||
this.$router.push('/login')
|
||||
return
|
||||
}
|
||||
const { data } = await this.$http.post('/api/common/verifyToken', {token: localStorage.getItem('login_token')})
|
||||
const { data } = await this.$http.post<{token: null | string}, AxiosResponse<any>>('/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})
|
||||
|
||||
@ -20,7 +20,6 @@
|
||||
<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'
|
||||
|
||||
@ -29,12 +28,12 @@ import { AxiosResponse } from 'axios'
|
||||
components: { ElButton, ElForm, ElFormItem, ElInput, ElTooltip }
|
||||
})
|
||||
export default class Login extends Vue {
|
||||
private readonly loginForm: VForm = ref('loginForm')
|
||||
private userInfo: UserInfo = {
|
||||
private readonly loginForm: any = ref('loginForm')
|
||||
userInfo: UserInfo = {
|
||||
username: null,
|
||||
password: null
|
||||
}
|
||||
private ruleValidate = {
|
||||
ruleValidate = {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' }
|
||||
],
|
||||
|
||||
@ -1,121 +1,31 @@
|
||||
<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>
|
||||
<el-row v-for="menu in menus" :key="menu.name">
|
||||
<el-col :span="3" class="nav-title">{{ menu.title }}</el-col>
|
||||
<el-col :span="21">
|
||||
<router-link :to="submenu.path" v-for="submenu in menu.child" :key="submenu.path" class="nav-item">
|
||||
{{submenu.title}}
|
||||
</router-link>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator'
|
||||
import moment from 'moment'
|
||||
import { Options, Vue } from 'vue-class-component'
|
||||
import { ElRow, ElCol } from 'element-plus'
|
||||
import menus from '../config/menu'
|
||||
|
||||
@Component({})
|
||||
@Options({
|
||||
name: 'Welcome',
|
||||
components: { ElRow, ElCol }
|
||||
})
|
||||
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
|
||||
}
|
||||
menus = menus
|
||||
}
|
||||
</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;
|
||||
@ -130,6 +40,8 @@ export default class Welcome extends Vue {
|
||||
margin: 10px;
|
||||
border: 1px solid #ccc;
|
||||
padding: 10px;
|
||||
text-decoration: none;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
212
src/views/system/SystemRole.vue
Normal file
212
src/views/system/SystemRole.vue
Normal file
@ -0,0 +1,212 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form inline :model="search" @submit.prevent>
|
||||
<el-form-item label="角色名称/描述:">
|
||||
<el-input size="small" v-model="search.name" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="btn-container">
|
||||
<el-button size="small" type="primary" @click="add">添加</el-button>
|
||||
<div class="search-btn">
|
||||
<el-button type="primary" @click="loadDataBase(true)" size="small" icon="el-icon-search">搜索</el-button>
|
||||
<el-button @click="reset" size="small" icon="el-icon-refresh-right">重置</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<el-table :data="systemRoleData" v-loading="loading" stripe height="520">
|
||||
<el-table-column prop="name" label="角色名称" />
|
||||
<el-table-column prop="methods" label="允许的请求类型" >
|
||||
<template #default="scope">
|
||||
<el-tag type="info" size="small" v-for="method in scope.row.methods" :key="method">{{method}}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" >
|
||||
<template #default="scope">
|
||||
{{ datetimeFormat(scope.row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="updated_at" label="更新时间" >
|
||||
<template #default="scope">
|
||||
{{ datetimeFormat(scope.row.updated_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" >
|
||||
<template #default="scope">
|
||||
<el-button size="mini" plain @click="update(scope.row)">修改</el-button>
|
||||
<el-button type="danger" size="mini" plain @click="remove(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="page-container">
|
||||
<el-pagination background
|
||||
:page-sizes="$store.state.pageSizeOpts"
|
||||
:total="total"
|
||||
@size-change="pageSizeChange"
|
||||
@current-change="pageChange">
|
||||
</el-pagination>
|
||||
</div>
|
||||
<el-dialog v-model="addModal" :title="modalTitle" >
|
||||
<el-form ref="roleForm" :model="formData" :rules="ruleValidate" :label-width="120">
|
||||
<el-form-item label="角色名称" prop="name">
|
||||
<el-input v-model="formData.name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="描述">
|
||||
<el-input v-model="formData.description" />
|
||||
</el-form-item>
|
||||
<el-form-item label="允许的请求类型">
|
||||
<el-select v-model="formData.methods" multiple >
|
||||
<el-option value="GET">GET</el-option>
|
||||
<el-option value="POST">POST</el-option>
|
||||
<el-option value="PUT">PUT</el-option>
|
||||
<el-option value="DELETE">DELETE</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="允许的URI">
|
||||
<el-input v-model="uri.include">
|
||||
<template #append>
|
||||
<el-button icon="el-icon-plus" @click="addUri('include_uri', uri.include)"></el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-tag v-for="uri in formData.include_uri" :key="uri" closable @close="removeUri('include_uri', uri)">{{uri}}</el-tag>
|
||||
</el-form-item>
|
||||
<el-form-item label="禁止的URI">
|
||||
<el-input v-model="uri.exclude">
|
||||
<template #append>
|
||||
<el-button icon="el-icon-plus" @click="addUri('exclude_uri', uri.exclude)"></el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-tag v-for="uri in formData.exclude_uri" :key="uri" closable @close="removeUri('exclude_uri', uri)">{{uri}}</el-tag>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="addModal = false">取消</el-button>
|
||||
<el-button type="primary" @click="save" :loading="modalLoading">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Options } from 'vue-class-component'
|
||||
import BaseList from '../../model/baselist'
|
||||
import { Page } from '../../model/common.dto'
|
||||
import { SystemRoleModel } from '../../model/system/system-role'
|
||||
import { AxiosResponse } from 'axios'
|
||||
import { ElButton, ElForm, ElFormItem, ElInput, ElTable, ElTableColumn, ElTag, ElPagination, ElDialog, ElSelect, ElOption, ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ref } from 'vue'
|
||||
|
||||
@Options({
|
||||
name: 'SystemRole',
|
||||
components: { ElButton, ElForm, ElFormItem, ElInput, ElTable, ElTableColumn, ElTag, ElPagination, ElDialog, ElSelect, ElOption }
|
||||
})
|
||||
export default class SystemRole extends BaseList<SystemRolePage> {
|
||||
private readonly roleForm: any = ref('roleForm')
|
||||
ruleValidate = {
|
||||
name: [
|
||||
{ required: true, message: '请输入角色名称', trigger: 'blur' }
|
||||
],
|
||||
}
|
||||
systemRoleData: SystemRoleModel[] = []
|
||||
protected search = new SystemRolePage()
|
||||
addModal: boolean = false
|
||||
modalTitle: string | null = null
|
||||
uri: {
|
||||
include: string | null,
|
||||
exclude: string | null
|
||||
} = {
|
||||
include: null,
|
||||
exclude: null
|
||||
}
|
||||
formData: SystemRoleModel = {
|
||||
_id: null,
|
||||
name: null,
|
||||
description: null,
|
||||
methods: [],
|
||||
include_uri: [],
|
||||
exclude_uri: []
|
||||
}
|
||||
async loadData() {
|
||||
this.loading = true
|
||||
const { data } = await this.$http.get<{params: SystemRolePage}, AxiosResponse<any>>('/api/system/role/list', {params:this.search})
|
||||
this.loading = false
|
||||
this.total = data.total
|
||||
this.systemRoleData = data.data
|
||||
}
|
||||
created() {
|
||||
this.loadData()
|
||||
}
|
||||
add() {
|
||||
// 清空表单
|
||||
this.uri.include = null
|
||||
this.uri.exclude = null
|
||||
this.formData = {
|
||||
_id: null,
|
||||
name: null,
|
||||
description: null,
|
||||
methods: [],
|
||||
include_uri: [],
|
||||
exclude_uri: []
|
||||
}
|
||||
this.modalTitle = '新增角色'
|
||||
this.addModal = true
|
||||
}
|
||||
addUri(fieldName: 'include_uri' | 'exclude_uri', uri: string) {
|
||||
if(!uri) return
|
||||
if(this.formData[fieldName].indexOf(uri) === -1) {
|
||||
this.formData[fieldName].push(uri)
|
||||
}
|
||||
}
|
||||
removeUri(fieldName: 'include_uri' | 'exclude_uri', uri: string) {
|
||||
let index = this.formData[fieldName].indexOf(uri)
|
||||
if(index !== -1) {
|
||||
this.formData[fieldName].splice(index, 1)
|
||||
}
|
||||
}
|
||||
update(row: SystemRoleModel) {
|
||||
this.uri.include = null
|
||||
this.uri.exclude = null
|
||||
this.formData._id = row._id
|
||||
this.formData.name = row.name
|
||||
this.formData.description = row.description
|
||||
this.formData.methods = row.methods
|
||||
this.formData.include_uri = row.include_uri
|
||||
this.formData.exclude_uri = row.exclude_uri
|
||||
this.modalTitle = '修改角色'
|
||||
this.addModal = true
|
||||
}
|
||||
remove(row: SystemRoleModel) {
|
||||
ElMessageBox.confirm(`是否确认删除 ${row.name} 角色?`, '确认删除', {type: 'warning'}).then(async () => {
|
||||
const { data } = await this.$http.delete<{params: {id: string}}, AxiosResponse<any>>('/api/system/role/delete', {params: {id: row._id}})
|
||||
if(data.status) {
|
||||
ElMessage.success(data.message)
|
||||
this.loadData()
|
||||
} else {
|
||||
ElMessage.warning(data.message)
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
async save() {
|
||||
this.roleForm.validate(async (valid: boolean) => {
|
||||
if(!valid) {
|
||||
this.modalLoading = false
|
||||
return
|
||||
}
|
||||
const { data } = await this.$http.post<SystemRoleModel, AxiosResponse<any>>('/api/system/role/save', this.formData)
|
||||
this.addModal = false
|
||||
ElMessage.success(data.message)
|
||||
this.loadData()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class SystemRolePage extends Page {
|
||||
name?: string
|
||||
reset() {
|
||||
super.reset()
|
||||
this.name = undefined
|
||||
}
|
||||
}
|
||||
</script>
|
||||
0
src/views/system/SystemUser.vue
Normal file
0
src/views/system/SystemUser.vue
Normal file
10
src/vuex.d.ts
vendored
Normal file
10
src/vuex.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
import { ComponentCustomProperties } from 'vue'
|
||||
import { Store } from 'vuex'
|
||||
import { StateType } from './store/types'
|
||||
|
||||
declare module '@vue/runtime-core' {
|
||||
// provide typings for `this.$store`
|
||||
interface ComponentCustomProperties {
|
||||
$store: Store<StateType>
|
||||
}
|
||||
}
|
||||
@ -21,6 +21,6 @@ export default defineConfig({
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
assetsDir: 'assets', // 静态资源的存放路径, 相对于outDir
|
||||
cssCodeSplit: true
|
||||
cssCodeSplit: true // 是否拆分css
|
||||
}
|
||||
})
|
||||
|
||||
11
yarn.lock
11
yarn.lock
@ -663,6 +663,11 @@ mime@^1.4.1:
|
||||
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
|
||||
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
|
||||
|
||||
moment@^2.29.1:
|
||||
version "2.29.1"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
|
||||
integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==
|
||||
|
||||
ms@^2.1.1:
|
||||
version "2.1.3"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
||||
@ -1068,9 +1073,9 @@ vscode-uri@^3.0.2:
|
||||
integrity sha512-jkjy6pjU1fxUvI51P+gCsxg1u2n8LSt0W6KrCNQceaziKzff74GoWmjVG46KieVzybO1sttPQmYfrwSHey7GUA==
|
||||
|
||||
vscode-vue-languageservice@^0.27.0:
|
||||
version "0.27.27"
|
||||
resolved "https://registry.yarnpkg.com/vscode-vue-languageservice/-/vscode-vue-languageservice-0.27.27.tgz#abaf6190768d30e317d24220c76e96c0c7d31751"
|
||||
integrity sha512-q5vaaTxQpsqr4O2h2A5zXQtK17ZHKdrcjv6gZaItTBcI4rxw6GIHoblYsNXMo+dhBZEmFmyI18iPn+a7RajFEQ==
|
||||
version "0.27.28"
|
||||
resolved "https://registry.yarnpkg.com/vscode-vue-languageservice/-/vscode-vue-languageservice-0.27.28.tgz#97f537cf8d9be80f835b440de49a6a3d492d5b3d"
|
||||
integrity sha512-dkYufKL/kchAxIxz2mYJDwsQuc7wnjTY+vc+sVz/DkFWfPEK0VaOStuKc85UME6VJ2IJzGEN1LHERGM65u8deQ==
|
||||
dependencies:
|
||||
"@volar/code-gen" "^0.27.24"
|
||||
"@volar/html2pug" "^0.27.13"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user