Compare commits
4 Commits
1ffd56efc2
...
2c8789b8cb
| Author | SHA1 | Date | |
|---|---|---|---|
| 2c8789b8cb | |||
| 8a2ba4e2bf | |||
| cf34013c53 | |||
| 96b032d262 |
1
components.d.ts
vendored
1
components.d.ts
vendored
@ -22,6 +22,7 @@ declare module '@vue/runtime-core' {
|
||||
ElForm: typeof import('element-plus/es')['ElForm']
|
||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||
ElHeader: typeof import('element-plus/es')['ElHeader']
|
||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||
ElInput: typeof import('element-plus/es')['ElInput']
|
||||
ElMain: typeof import('element-plus/es')['ElMain']
|
||||
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||
|
||||
11539
package-lock.json
generated
Normal file
11539
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -15,8 +15,6 @@
|
||||
"moment": "^2.29.1",
|
||||
"pretty-bytes": "^5.6.0",
|
||||
"vue": "^3.2.45",
|
||||
"vue-axios": "^3.3.7",
|
||||
"vue-class-component": "8.0.0-rc.1",
|
||||
"vue-router": "^4.0.11",
|
||||
"vuex": "^4.0.2"
|
||||
},
|
||||
|
||||
10
src/App.vue
10
src/App.vue
@ -6,16 +6,10 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Options, Vue } from 'vue-class-component'
|
||||
<script setup lang="ts">
|
||||
import zhCn from 'element-plus/lib/locale/lang/zh-cn'
|
||||
|
||||
@Options({
|
||||
name: 'App'
|
||||
})
|
||||
export default class App extends Vue{
|
||||
locale = zhCn
|
||||
}
|
||||
const locale = zhCn
|
||||
</script>
|
||||
<style lang="less">
|
||||
@import url('./static/common.less');
|
||||
|
||||
51
src/main.ts
51
src/main.ts
@ -3,58 +3,18 @@ import App from './App.vue'
|
||||
import { router, 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, ElLoading } from 'element-plus'
|
||||
import { ElLoading } from 'element-plus'
|
||||
import 'element-plus/theme-chalk/el-message.css'
|
||||
import 'element-plus/theme-chalk/el-message-box.css'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
|
||||
// 添加请求拦截器
|
||||
service.interceptors.request.use(config => {
|
||||
// 在发送请求之前添加token到请求头
|
||||
const token = mountedApp.$store.state.loginInfo.token
|
||||
if (token !== null && config.headers) {
|
||||
config.headers.token = token
|
||||
}
|
||||
return config
|
||||
}, err => {
|
||||
// 请求错误的处理
|
||||
ElMessage.error('请求超时,请稍后再试')
|
||||
return Promise.reject(err)
|
||||
})
|
||||
|
||||
service.interceptors.response.use(res=> {
|
||||
return res.data
|
||||
}, 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) {
|
||||
mountedApp.$router.push('/login')
|
||||
}
|
||||
}
|
||||
return Promise.reject(err)
|
||||
})
|
||||
|
||||
// 全局路由导航前置守卫
|
||||
router.beforeEach(function (to, from, next) {
|
||||
if (to.meta?.title) {
|
||||
mountedApp.$store.commit('addTab', { title: to.meta.title, path: to.path, name: to.name })
|
||||
store.commit('addTab', { title: to.meta.title, path: to.path, name: to.name })
|
||||
}
|
||||
mountedApp.$store.state.activeTab = to.path
|
||||
if(filterExclude.indexOf(to.path) !== -1 || mountedApp.$store.state.loginInfo.token) {
|
||||
store.state.activeTab = to.path
|
||||
if(filterExclude.indexOf(to.path) !== -1 || store.state.loginInfo.token) {
|
||||
next()
|
||||
} else {
|
||||
next('/login')
|
||||
@ -66,8 +26,7 @@ for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
|
||||
const mountedApp = app.use(router)
|
||||
app.use(router)
|
||||
.use(store)
|
||||
.use(VueAxios, service)
|
||||
.directive('loading', ElLoading.directive)
|
||||
.mount('#app')
|
||||
|
||||
@ -1,65 +1,44 @@
|
||||
import { Vue } from 'vue-class-component'
|
||||
import { ref, reactive } from 'vue'
|
||||
import { Page } from './common.dto'
|
||||
import moment from 'moment'
|
||||
|
||||
export default abstract class BaseList<T extends Page> extends Vue {
|
||||
/**
|
||||
* 表格数据加载中
|
||||
*/
|
||||
loading: boolean = false
|
||||
/**
|
||||
* 表单提交中
|
||||
*/
|
||||
modalLoading: boolean = false
|
||||
/**
|
||||
* 数据总数
|
||||
*/
|
||||
total: number = 0
|
||||
abstract search: T
|
||||
/**
|
||||
* 加载数据的实现
|
||||
*/
|
||||
abstract loadData(): Promise<void>
|
||||
/**
|
||||
* 加载数据
|
||||
* @param resetPage 是否重置页码
|
||||
*/
|
||||
loadDataBase(resetPage: boolean = false): void {
|
||||
if(resetPage) {
|
||||
this.search.pageNum = 1
|
||||
export function useBaseList<T extends Page>(searchInit: T) {
|
||||
const loading = ref(false)
|
||||
const modalLoading = ref(false)
|
||||
const total = ref(0)
|
||||
const search = reactive(searchInit) as T
|
||||
|
||||
let _loadData: (() => Promise<void>) | null = null
|
||||
|
||||
function setLoadData(fn: () => Promise<void>) {
|
||||
_loadData = fn
|
||||
}
|
||||
|
||||
function loadDataBase(resetPage: boolean = false): void {
|
||||
if (resetPage) {
|
||||
search.pageNum = 1
|
||||
}
|
||||
this.loadData()
|
||||
}
|
||||
/**
|
||||
* 重置搜索项
|
||||
*/
|
||||
reset(): void {
|
||||
this.search.reset()
|
||||
this.loadData()
|
||||
}
|
||||
/**
|
||||
* 页码变更
|
||||
* @param pageNum 页码
|
||||
*/
|
||||
pageChange(pageNum: number): void {
|
||||
this.search.pageNum = pageNum
|
||||
this.loadData()
|
||||
_loadData?.()
|
||||
}
|
||||
|
||||
/**
|
||||
* 每页数据条数变更
|
||||
* @param pageSize 每页数据条数
|
||||
*/
|
||||
pageSizeChange(pageSize: number): void {
|
||||
this.search.limit = pageSize
|
||||
this.loadData()
|
||||
function reset(): void {
|
||||
search.reset()
|
||||
_loadData?.()
|
||||
}
|
||||
|
||||
/**
|
||||
* 日期时间格式化
|
||||
* @param dateStr 日期时间
|
||||
*/
|
||||
datetimeFormat(dateStr: string): string | null {
|
||||
function pageChange(pageNum: number): void {
|
||||
search.pageNum = pageNum
|
||||
_loadData?.()
|
||||
}
|
||||
|
||||
function pageSizeChange(pageSize: number): void {
|
||||
search.limit = pageSize
|
||||
_loadData?.()
|
||||
}
|
||||
|
||||
function datetimeFormat(dateStr: string): string | null {
|
||||
return dateStr ? moment(dateStr).format('YYYY-MM-DD HH:mm:ss') : null
|
||||
}
|
||||
|
||||
return { loading, modalLoading, total, search, setLoadData, loadDataBase, reset, pageChange, pageSizeChange, datetimeFormat }
|
||||
}
|
||||
@ -1,15 +1,23 @@
|
||||
html,body,#app,.layout {
|
||||
html, body, #app, .layout {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
}
|
||||
.layout-header{
|
||||
|
||||
/* ===== 顶部导航栏 ===== */
|
||||
.layout-header {
|
||||
color: #fff;
|
||||
background-color: #515a6e;
|
||||
background: linear-gradient(90deg, #1d2b45 0%, #2c3e60 100%);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
|
||||
div.main-title {
|
||||
display: inline-block;
|
||||
font-size: 22px;
|
||||
font-size: 20px;
|
||||
font-weight: var(--el-font-weight-primary);
|
||||
letter-spacing: 1px;
|
||||
line-height: 60px;
|
||||
}
|
||||
.nav-btns-right {
|
||||
@ -20,45 +28,165 @@ html,body,#app,.layout {
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 侧边栏 ===== */
|
||||
.el-aside {
|
||||
background: var(--el-fill-color-light);
|
||||
border-right: 1px solid var(--el-border-color-light);
|
||||
|
||||
.el-menu {
|
||||
border-right: none;
|
||||
background: transparent;
|
||||
}
|
||||
.el-menu-item {
|
||||
border-radius: var(--el-border-radius-base);
|
||||
margin: 2px 8px;
|
||||
width: calc(100% - 16px);
|
||||
|
||||
&.is-active {
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
color: var(--el-color-primary);
|
||||
font-weight: var(--el-font-weight-primary);
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--el-fill-color);
|
||||
}
|
||||
}
|
||||
.el-sub-menu__title:hover {
|
||||
background-color: var(--el-fill-color);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 主内容区 ===== */
|
||||
.layout-main {
|
||||
padding: 10px !important;
|
||||
padding: 0 !important;
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
> .layout-tabs{
|
||||
margin: 15px 15px 0 15px;
|
||||
background: var(--el-fill-color);
|
||||
|
||||
> .layout-tabs {
|
||||
margin: 12px 16px 0;
|
||||
}
|
||||
> .layout-content{
|
||||
margin: 0 15px 15px 15px;
|
||||
> .layout-content {
|
||||
margin: 12px 16px;
|
||||
overflow: auto;
|
||||
background: #fff;
|
||||
border: 1px solid #e1e1e1;
|
||||
padding: 10px;
|
||||
background: var(--el-bg-color);
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
padding: 20px;
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
> .layout-copy{
|
||||
> .layout-copy {
|
||||
text-align: center;
|
||||
padding: 10px 0;
|
||||
color: #9ea7b4;
|
||||
padding: 8px 0 12px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
font-size: var(--el-font-size-extra-small);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 表格美化 ===== */
|
||||
.table-container {
|
||||
position: relative;
|
||||
}
|
||||
.btn-container {
|
||||
margin-bottom: 10px;
|
||||
.search-btn {
|
||||
float: right;
|
||||
|
||||
.el-table {
|
||||
border-radius: var(--el-border-radius-base);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
th.el-table__cell {
|
||||
background: var(--el-fill-color-light);
|
||||
color: var(--el-text-color-regular);
|
||||
font-weight: var(--el-font-weight-primary);
|
||||
font-size: var(--el-font-size-small);
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
td.el-table__cell {
|
||||
border-bottom: 1px solid var(--el-fill-color);
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: var(--el-font-size-small);
|
||||
}
|
||||
.el-table__row:hover > td {
|
||||
background: var(--el-color-primary-light-9) !important;
|
||||
}
|
||||
.el-table__row--striped > td {
|
||||
background: var(--el-fill-color-lighter);
|
||||
}
|
||||
.el-button.is-link {
|
||||
font-size: 16px;
|
||||
padding: 4px 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.page-container {
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
|
||||
/* ===== 操作栏 ===== */
|
||||
.btn-container {
|
||||
margin-bottom: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.search-btn {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 分页 ===== */
|
||||
.page-container {
|
||||
padding: 16px 0 4px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* ===== 表单弹窗 ===== */
|
||||
.el-dialog {
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
|
||||
.el-dialog__header {
|
||||
background: var(--el-fill-color-light);
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
padding: 16px 20px;
|
||||
margin: 0;
|
||||
|
||||
.el-dialog__title {
|
||||
font-size: var(--el-font-size-base);
|
||||
font-weight: var(--el-font-weight-primary);
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
.el-dialog__body {
|
||||
padding: 24px 24px 8px;
|
||||
}
|
||||
.el-dialog__footer {
|
||||
border-top: 1px solid var(--el-border-color-lighter);
|
||||
padding: 12px 20px;
|
||||
background: var(--el-fill-color-lighter);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 搜索表单 ===== */
|
||||
.el-form--inline {
|
||||
margin-bottom: 14px;
|
||||
|
||||
.el-form-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
.search-panel {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
/* ===== 其他工具类 ===== */
|
||||
.carsouel-img {
|
||||
position: relative;
|
||||
left: 50%;transform:
|
||||
translateX(-50%);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
height: 500px;
|
||||
width: auto;
|
||||
}
|
||||
@ -74,7 +202,67 @@ html,body,#app,.layout {
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
.el-tabs--card.nav-tabs > .el-tabs__header {
|
||||
margin-bottom: 0;
|
||||
border-bottom: none;
|
||||
.el-tabs--card.nav-tabs {
|
||||
> .el-tabs__header {
|
||||
margin-bottom: 0;
|
||||
border-bottom: none;
|
||||
background: var(--el-bg-color);
|
||||
padding: 0 4px;
|
||||
border-radius: 8px 8px 0 0;
|
||||
|
||||
.el-tabs__nav-wrap {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.el-tabs__nav {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.el-tabs__item {
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
padding: 0 16px;
|
||||
font-size: var(--el-font-size-small);
|
||||
color: var(--el-text-color-secondary);
|
||||
border: none;
|
||||
border-radius: 6px 6px 0 0;
|
||||
margin-right: 2px;
|
||||
transition: color 0.2s, background 0.2s;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
background: var(--el-color-primary-light-9);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
color: var(--el-color-primary);
|
||||
font-weight: var(--el-font-weight-primary);
|
||||
background: var(--el-fill-color);
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: var(--el-color-primary);
|
||||
border-radius: 2px 2px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.is-icon-close {
|
||||
font-size: 11px;
|
||||
margin-left: 4px;
|
||||
border-radius: 50%;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: var(--el-color-danger-light-7);
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,3 @@
|
||||
import { Vue } from 'vue-class-component'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
|
||||
export type VForm = Vue & {
|
||||
validate: (callback: (valid: boolean) => void) => void
|
||||
resetValidation: () => boolean
|
||||
reset: () => void
|
||||
clearValidate: () => void
|
||||
}
|
||||
export type VForm = FormInstance
|
||||
41
src/utils/http.ts
Normal file
41
src/utils/http.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import axios from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import store from '@/store'
|
||||
import { router } from '@/router'
|
||||
|
||||
const http = axios.create({
|
||||
timeout: 10000
|
||||
})
|
||||
|
||||
// 添加请求拦截器
|
||||
http.interceptors.request.use(config => {
|
||||
const token = store.state.loginInfo.token
|
||||
if (token !== null && config.headers) {
|
||||
config.headers.token = token
|
||||
}
|
||||
return config
|
||||
}, err => {
|
||||
ElMessage.error('请求超时,请稍后再试')
|
||||
return Promise.reject(err)
|
||||
})
|
||||
|
||||
http.interceptors.response.use(res => {
|
||||
return res.data
|
||||
}, 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) {
|
||||
router.push('/login')
|
||||
}
|
||||
}
|
||||
return Promise.reject(err)
|
||||
})
|
||||
|
||||
export default http
|
||||
@ -35,9 +35,9 @@
|
||||
</el-aside>
|
||||
<el-main class="layout-main">
|
||||
<div class="layout-tabs">
|
||||
<el-tabs type="card" class="nav-tabs" v-model="$store.state.activeTab" @tab-change="openMenu" @tab-remove="removeTab">
|
||||
<el-tabs type="card" class="nav-tabs" v-model="store.state.activeTab" @tab-change="openMenu" @tab-remove="removeTab">
|
||||
<el-tab-pane label="首页" name="/"></el-tab-pane>
|
||||
<el-tab-pane v-for="tab in $store.state.tabs" :key="tab.name" :label="tab.title" :name="tab.path" closable></el-tab-pane>
|
||||
<el-tab-pane v-for="tab in store.state.tabs" :key="tab.name" :label="tab.title" :name="tab.path" closable></el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
<div class="layout-content">
|
||||
@ -53,72 +53,74 @@
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Options, Vue } from 'vue-class-component'
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import menus from '../config/menu'
|
||||
import http from '@/utils/http'
|
||||
|
||||
@Options({
|
||||
name: 'Home'
|
||||
const store = useStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const version = process.env.VERSION
|
||||
const currentYear = new Date().getFullYear()
|
||||
const defaultActiveMenuKey = ref<string | null>(null)
|
||||
const openMenuNames = ref<string[]>([])
|
||||
|
||||
const realname = computed((): null | string => {
|
||||
return store.state.loginInfo.userInfo
|
||||
? store.state.loginInfo.userInfo.realname : null
|
||||
})
|
||||
export default class Home extends Vue{
|
||||
public version?: string = process.env.VERSION
|
||||
currentYear = new Date().getFullYear()
|
||||
// 菜单项
|
||||
menus = menus
|
||||
defaultActiveMenuKey: string | null = null
|
||||
openMenuNames: string[] = []
|
||||
get realname(): null | string { // 当前用户的显示名称
|
||||
return this.$store.state.loginInfo.userInfo
|
||||
? this.$store.state.loginInfo.userInfo.realname : null
|
||||
const keepViews = computed((): string[] => {
|
||||
return store.state.tabs
|
||||
.filter((item: any) => item.name)
|
||||
.map((item: any) => item.name)
|
||||
})
|
||||
|
||||
// created
|
||||
defaultActiveMenuKey.value = route.path
|
||||
if (defaultActiveMenuKey.value) {
|
||||
let result = /^\/(.*)\//.exec(defaultActiveMenuKey.value)
|
||||
if (result) {
|
||||
openMenuNames.value.push(result[1])
|
||||
}
|
||||
get keepViews(): string[] {
|
||||
return this.$store.state.tabs
|
||||
.filter(item => item.name)
|
||||
.map(item => item.name)
|
||||
}
|
||||
async created(): Promise<void> {
|
||||
this.defaultActiveMenuKey = this.$route.path
|
||||
if(this.defaultActiveMenuKey) {
|
||||
let result = /^\/(.*)\//.exec(this.defaultActiveMenuKey)
|
||||
if(result) {
|
||||
this.openMenuNames.push(result[1])
|
||||
}
|
||||
}
|
||||
if(!this.$store.state.loginInfo.token) {
|
||||
this.$router.push('/login')
|
||||
return
|
||||
}
|
||||
const data = await this.$http.post<{token: string}, any>('/api/v1/common/verifyToken', {token: this.$store.state.loginInfo.token})
|
||||
if(data.status) {
|
||||
// 如果是已过期的token 服务端会签发新的token
|
||||
this.$store.commit('login', {token: data.newToken || this.$store.state.loginInfo.token, userInfo: data.userInfo})
|
||||
}
|
||||
if (!store.state.loginInfo.token) {
|
||||
router.push('/login')
|
||||
} else {
|
||||
http.post<{token: string}, any>('/api/v1/common/verifyToken', {token: store.state.loginInfo.token}).then(data => {
|
||||
if (data.status) {
|
||||
store.commit('login', {token: data.newToken || store.state.loginInfo.token, userInfo: data.userInfo})
|
||||
} else {
|
||||
this.$router.push('/login')
|
||||
router.push('/login')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function dropdownMenuCommand(command: string): void {
|
||||
switch (command) {
|
||||
case 'home':
|
||||
router.push('/')
|
||||
break
|
||||
case 'changePassword':
|
||||
// TODO
|
||||
break
|
||||
case 'logout':
|
||||
store.commit('logout')
|
||||
router.push('/login')
|
||||
break
|
||||
}
|
||||
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)
|
||||
}
|
||||
removeTab(path: string): void {
|
||||
this.$store.commit('removeTab', path)
|
||||
if (this.$store.state.activeTab === path) { // 关闭的是当前打开的页签
|
||||
const { tabs } = this.$store.state
|
||||
this.openMenu(tabs[tabs.length - 1]?.path || '/')
|
||||
}
|
||||
}
|
||||
function openMenu(path: string): void {
|
||||
router.push(path)
|
||||
}
|
||||
function removeTab(path: string): void {
|
||||
store.commit('removeTab', path)
|
||||
if (store.state.activeTab === path) {
|
||||
const { tabs } = store.state
|
||||
openMenu(tabs[tabs.length - 1]?.path || '/')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -1,80 +1,232 @@
|
||||
<template>
|
||||
<div class="login-wrapper">
|
||||
<h2 class="title">博客管理后台</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>
|
||||
<div class="login-page">
|
||||
<div class="login-card">
|
||||
<div class="card-left">
|
||||
<div class="brand">
|
||||
<div class="brand-icon">✦</div>
|
||||
<h1>Blog Admin</h1>
|
||||
<p>简洁 · 高效 · 专注写作</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-right">
|
||||
<h2 class="form-title">欢迎回来</h2>
|
||||
<p class="form-subtitle">请登录你的管理账号</p>
|
||||
<el-form ref="loginForm" :model="userInfo" :rules="ruleValidate" label-position="top">
|
||||
<el-form-item label="用户名" prop="username">
|
||||
<el-input
|
||||
v-model="userInfo.username"
|
||||
placeholder="请输入用户名"
|
||||
prefix-icon="User"
|
||||
size="large"
|
||||
@keyup.enter="login"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="密码" prop="password">
|
||||
<el-input
|
||||
v-model="userInfo.password"
|
||||
type="password"
|
||||
placeholder="请输入密码"
|
||||
prefix-icon="Lock"
|
||||
size="large"
|
||||
show-password
|
||||
@keyup.enter="login"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-button class="login-btn" type="primary" size="large" :loading="loading" @click="login">
|
||||
登 录
|
||||
</el-button>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Options, Vue } from 'vue-class-component'
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { VForm } from "../types"
|
||||
import type { VForm } from '../types'
|
||||
import http from '@/utils/http'
|
||||
|
||||
@Options({
|
||||
name: 'Login'
|
||||
})
|
||||
export default class Login extends Vue {
|
||||
userInfo: UserInfo = {
|
||||
username: null,
|
||||
password: null
|
||||
}
|
||||
ruleValidate = {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' }
|
||||
],
|
||||
}
|
||||
created(): void {
|
||||
this.$store.commit('logout')
|
||||
this.$store.commit('clearTabs')
|
||||
localStorage.clear()
|
||||
}
|
||||
/**
|
||||
* 登录
|
||||
*/
|
||||
async login() {
|
||||
(this.$refs.loginForm as VForm).validate(async (valid: boolean) => {
|
||||
if (!valid) return
|
||||
const data = await this.$http.post<UserInfo, any>('/api/v1/common/login', this.userInfo)
|
||||
if(data.token) {
|
||||
this.$store.commit('login', data)
|
||||
this.$router.push('/')
|
||||
} else {
|
||||
ElMessage.error(data.message)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
type UserInfo = {
|
||||
username: string | null,
|
||||
password: string | null
|
||||
};
|
||||
}
|
||||
|
||||
const store = useStore()
|
||||
const router = useRouter()
|
||||
const loginForm = ref<VForm>()
|
||||
|
||||
const userInfo: UserInfo = reactive({
|
||||
username: null,
|
||||
password: null
|
||||
})
|
||||
const ruleValidate = {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' }
|
||||
],
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
// created
|
||||
store.commit('logout')
|
||||
store.commit('clearTabs')
|
||||
localStorage.clear()
|
||||
|
||||
async function login() {
|
||||
loginForm.value?.validate(async (valid: boolean) => {
|
||||
if (!valid) return
|
||||
loading.value = true
|
||||
const data = await http.post<UserInfo, any>('/api/v1/common/login', userInfo).finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
if (data.token) {
|
||||
store.commit('login', data)
|
||||
router.push('/')
|
||||
} else {
|
||||
ElMessage.error(data.message)
|
||||
}
|
||||
})
|
||||
}
|
||||
</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-page {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 40%, #0f3460 100%);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(80px);
|
||||
opacity: 0.15;
|
||||
}
|
||||
.login-btn {
|
||||
&::before {
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
background: #409eff;
|
||||
top: -100px;
|
||||
left: -100px;
|
||||
}
|
||||
&::after {
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
background: #67c23a;
|
||||
bottom: -100px;
|
||||
right: -100px;
|
||||
}
|
||||
}
|
||||
|
||||
.login-card {
|
||||
display: flex;
|
||||
width: 820px;
|
||||
min-height: 480px;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.5);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.card-left {
|
||||
flex: 1;
|
||||
background: linear-gradient(145deg, #409eff, #0068d4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
top: -80px;
|
||||
right: -80px;
|
||||
}
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
bottom: -60px;
|
||||
left: -60px;
|
||||
}
|
||||
|
||||
.brand {
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
.brand-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
margin: 0 0 12px;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
p {
|
||||
font-size: 14px;
|
||||
opacity: 0.75;
|
||||
letter-spacing: 3px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-right {
|
||||
flex: 1.1;
|
||||
background: #fff;
|
||||
padding: 52px 48px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
.form-title {
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
color: #1d2129;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
.form-subtitle {
|
||||
font-size: 14px;
|
||||
color: #86909c;
|
||||
margin: 0 0 36px;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
font-weight: 600;
|
||||
color: #4e5969;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
height: 46px;
|
||||
font-size: 16px;
|
||||
letter-spacing: 4px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,45 +1,119 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="nav-list">
|
||||
<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}}
|
||||
<div class="welcome-page">
|
||||
<div class="welcome-header">
|
||||
<h2>欢迎回来 👋</h2>
|
||||
<p>选择下方快捷入口,开始管理你的博客</p>
|
||||
</div>
|
||||
<div class="menu-sections">
|
||||
<div class="menu-section" v-for="menu in menus" :key="menu.name">
|
||||
<div class="section-title">
|
||||
<el-icon><component :is="menu.icon" /></el-icon>
|
||||
<span>{{ menu.title }}</span>
|
||||
</div>
|
||||
<div class="section-items">
|
||||
<router-link
|
||||
:to="submenu.path"
|
||||
v-for="submenu in menu.child"
|
||||
:key="submenu.path"
|
||||
class="menu-item"
|
||||
>
|
||||
<span class="item-label">{{ submenu.title }}</span>
|
||||
<el-icon class="item-arrow"><ArrowRight /></el-icon>
|
||||
</router-link>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Options, Vue } from 'vue-class-component'
|
||||
<script setup lang="ts">
|
||||
import menus from '../config/menu'
|
||||
|
||||
@Options({
|
||||
name: 'Welcome'
|
||||
})
|
||||
export default class Welcome extends Vue {
|
||||
menus = menus
|
||||
}
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.nav-list {
|
||||
.nav-title {
|
||||
font-size: 16px;
|
||||
line-height: 66px;
|
||||
text-align: right;
|
||||
padding-right: 20px;
|
||||
.welcome-page {
|
||||
padding: 32px;
|
||||
max-width: 900px;
|
||||
}
|
||||
|
||||
.welcome-header {
|
||||
margin-bottom: 32px;
|
||||
h2 {
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
color: #1d2129;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
.nav-item {
|
||||
display: inline-block;
|
||||
width: 200px;
|
||||
font-size: 16px;
|
||||
margin: 10px;
|
||||
border: 1px solid #ccc;
|
||||
padding: 10px;
|
||||
text-decoration: none;
|
||||
p {
|
||||
font-size: 14px;
|
||||
color: #86909c;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-sections {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.menu-section {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
||||
border: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #4e5969;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
|
||||
.el-icon {
|
||||
font-size: 17px;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.section-items {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 16px;
|
||||
border-radius: 8px;
|
||||
background: #f7f8fa;
|
||||
text-decoration: none;
|
||||
color: #1d2129;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s;
|
||||
border: 1px solid transparent;
|
||||
|
||||
.item-arrow {
|
||||
font-size: 13px;
|
||||
color: #c9cdd4;
|
||||
transition: transform 0.2s, color 0.2s;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--el-color-primary-light-9);
|
||||
border-color: var(--el-color-primary-light-5);
|
||||
color: var(--el-color-primary);
|
||||
|
||||
.item-arrow {
|
||||
color: var(--el-color-primary);
|
||||
transform: translateX(3px);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -47,8 +47,8 @@
|
||||
</div>
|
||||
<div class="page-container">
|
||||
<el-pagination background
|
||||
:page-sizes="$store.state.pageSizeOpts"
|
||||
:layout="$store.state.pageLayout"
|
||||
:page-sizes="store.state.pageSizeOpts"
|
||||
:layout="store.state.pageLayout"
|
||||
:current-page="search.pageNum"
|
||||
:total="total"
|
||||
@size-change="pageSizeChange"
|
||||
@ -66,73 +66,15 @@
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import HitokotoAdd from './HitokotoAdd.vue'
|
||||
import { Options, Vue } from 'vue-class-component'
|
||||
import BaseList from '@/model/baselist'
|
||||
import { useBaseList } from '@/model/baselist'
|
||||
import { Page } from '@/model/common.dto'
|
||||
import HitokotoModel from '@/model/api/hitokoto'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { VForm } from '@/types'
|
||||
|
||||
let selectedData: string[] = []
|
||||
@Options({
|
||||
name: 'Hitokoto',
|
||||
components: { HitokotoAdd }
|
||||
})
|
||||
export default class Hitokoto extends BaseList<HitokotoPage> {
|
||||
search = new HitokotoPage()
|
||||
typeList: {label: string, value: string}[] = []
|
||||
hitokotoData: HitokotoModel[] = []
|
||||
formData: {[propName:string]: string | null} = {}
|
||||
addModal: boolean = false
|
||||
|
||||
async loadData() {
|
||||
this.loading = true
|
||||
const data = await this.$http.get<HitokotoPage, any>('/api/v1/hitokoto/list', {params:this.search})
|
||||
selectedData = []
|
||||
this.loading = false
|
||||
this.total = data.total
|
||||
this.hitokotoData = data.data
|
||||
}
|
||||
async save() {
|
||||
((this.$refs.addForm as Vue).$refs.hitokotoForm as VForm).validate(async (valid: boolean) => {
|
||||
if (!valid) return
|
||||
this.modalLoading = true
|
||||
const data = await this.$http.post<any, any>('/api/v1/hitokoto/save', this.formData)
|
||||
this.modalLoading = false
|
||||
this.addModal = false
|
||||
ElMessage.success(data.message)
|
||||
this.loadData()
|
||||
// 清空表单
|
||||
this.formData = {}
|
||||
})
|
||||
}
|
||||
deleteAll() {
|
||||
if(!selectedData.length) {
|
||||
ElMessage.warning('请选择要删除的数据')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(`是否确认删除选中的${selectedData.length}条数据?`, '确认删除', {type: 'warning'}).then(async () => {
|
||||
const data = await this.$http.delete<any, any>('/api/v1/hitokoto/delete', {params:{_ids: selectedData}})
|
||||
ElMessage.success(data.message)
|
||||
this.loadData()
|
||||
}).catch(() => {})
|
||||
}
|
||||
dataSelect(selection: HitokotoModel[]) {
|
||||
selectedData = selection.map(item => item._id)
|
||||
}
|
||||
created() {
|
||||
this.loadData()
|
||||
this.$http.get<never, any>('/api/v1/common/config/hitokoto_type').then(data => {
|
||||
this.typeList = data
|
||||
})
|
||||
}
|
||||
findTypeText(value: string): string | null {
|
||||
const type = this.typeList.find(item => item.value === value)
|
||||
return type ? type.label : null
|
||||
}
|
||||
}
|
||||
import http from '@/utils/http'
|
||||
|
||||
class HitokotoPage extends Page {
|
||||
content?: string
|
||||
@ -145,4 +87,62 @@ class HitokotoPage extends Page {
|
||||
this.createdAt = undefined
|
||||
}
|
||||
}
|
||||
|
||||
const store = useStore()
|
||||
const { loading, modalLoading, total, search, setLoadData, loadDataBase, reset, pageChange, pageSizeChange, datetimeFormat } = useBaseList(new HitokotoPage())
|
||||
|
||||
const typeList = ref<{label: string, value: string}[]>([])
|
||||
const hitokotoData = ref<HitokotoModel[]>([])
|
||||
const formData = reactive<{[propName: string]: string | null}>({})
|
||||
const addModal = ref(false)
|
||||
const addForm = ref<InstanceType<typeof HitokotoAdd>>()
|
||||
|
||||
let selectedData: string[] = []
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
const data = await http.get<HitokotoPage, any>('/api/v1/hitokoto/list', {params: search})
|
||||
selectedData = []
|
||||
loading.value = false
|
||||
total.value = data.total
|
||||
hitokotoData.value = data.data
|
||||
}
|
||||
setLoadData(loadData)
|
||||
|
||||
async function save() {
|
||||
addForm.value?.hitokotoForm?.validate(async (valid: boolean) => {
|
||||
if (!valid) return
|
||||
modalLoading.value = true
|
||||
const data = await http.post<any, any>('/api/v1/hitokoto/save', formData)
|
||||
modalLoading.value = false
|
||||
addModal.value = false
|
||||
ElMessage.success(data.message)
|
||||
loadData()
|
||||
Object.keys(formData).forEach(key => delete formData[key])
|
||||
})
|
||||
}
|
||||
function deleteAll() {
|
||||
if (!selectedData.length) {
|
||||
ElMessage.warning('请选择要删除的数据')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(`是否确认删除选中的${selectedData.length}条数据?`, '确认删除', {type: 'warning'}).then(async () => {
|
||||
const data = await http.delete<any, any>('/api/v1/hitokoto/delete', {params: {_ids: selectedData}})
|
||||
ElMessage.success(data.message)
|
||||
loadData()
|
||||
}).catch(() => {})
|
||||
}
|
||||
function dataSelect(selection: HitokotoModel[]) {
|
||||
selectedData = selection.map(item => item._id)
|
||||
}
|
||||
function findTypeText(value: string): string | null {
|
||||
const type = typeList.value.find(item => item.value === value)
|
||||
return type ? type.label : null
|
||||
}
|
||||
|
||||
// created
|
||||
loadData()
|
||||
http.get<never, any>('/api/v1/common/config/hitokoto_type').then(data => {
|
||||
typeList.value = data
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -18,26 +18,25 @@
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Options, Vue } from 'vue-class-component'
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import type { VForm } from '@/types'
|
||||
|
||||
@Options({
|
||||
name: 'SystemConfigAdd',
|
||||
props: {
|
||||
typeList: Array,
|
||||
formData: Object
|
||||
}
|
||||
})
|
||||
export default class HitokotoAdd extends Vue {
|
||||
typeList!: {label: string, value: string}[]
|
||||
formData!: {[propName:string]: string | null}
|
||||
ruleValidate = {
|
||||
hitokoto: [
|
||||
{ required: true, message: '请输入内容', trigger: 'blur' }
|
||||
],
|
||||
type: [
|
||||
{ required: true, message: '请选择类型', trigger: 'blur' }
|
||||
],
|
||||
}
|
||||
defineProps<{
|
||||
typeList: {label: string, value: string}[]
|
||||
formData: {[propName: string]: string | null}
|
||||
}>()
|
||||
|
||||
const hitokotoForm = ref<VForm>()
|
||||
|
||||
const ruleValidate = {
|
||||
hitokoto: [
|
||||
{ required: true, message: '请输入内容', trigger: 'blur' }
|
||||
],
|
||||
type: [
|
||||
{ required: true, message: '请选择类型', trigger: 'blur' }
|
||||
],
|
||||
}
|
||||
|
||||
defineExpose({ hitokotoForm })
|
||||
</script>
|
||||
@ -79,8 +79,8 @@
|
||||
</div>
|
||||
<div class="page-container">
|
||||
<el-pagination background
|
||||
:page-sizes="$store.state.pageSizeOpts"
|
||||
:layout="$store.state.pageLayout"
|
||||
:page-sizes="store.state.pageSizeOpts"
|
||||
:layout="store.state.pageLayout"
|
||||
:current-page="search.pageNum"
|
||||
:total="total"
|
||||
@size-change="pageSizeChange"
|
||||
@ -118,7 +118,7 @@
|
||||
action="/api/v2/music/upload"
|
||||
name="file"
|
||||
accept=".mp3,.flac"
|
||||
:headers="{token: $store.state.loginInfo.token}"
|
||||
:headers="{token: store.state.loginInfo.token}"
|
||||
:on-success="uploadSuccess"
|
||||
:on-error="uploadError"
|
||||
:auto-upload="false"
|
||||
@ -148,207 +148,17 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Options } from 'vue-class-component'
|
||||
import BaseList from '@/model/baselist'
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { useBaseList } from '@/model/baselist'
|
||||
import { MsgResult, Page } from '@/model/common.dto'
|
||||
import { ElUpload, ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { UploadInstance, ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { MusicModel, MusicLibModel, MusicLyricModel, MusicPlayerItem } from '@/model/api/music'
|
||||
import APlayer from './aplayer/vue-aplayer.vue'
|
||||
import prettyBytes from 'pretty-bytes'
|
||||
import { VForm } from '@/types'
|
||||
|
||||
let selectedIds: string[] = []
|
||||
@Options({
|
||||
name: 'Music',
|
||||
components: { ElUpload, APlayer }
|
||||
})
|
||||
export default class Music extends BaseList<MusicPage> {
|
||||
search = new MusicPage()
|
||||
currentRow: MusicModel | null = null
|
||||
libIdSelected: string | null = null
|
||||
exts: string[] = []
|
||||
musicLibs: MusicLibModel[] = []
|
||||
musicData: MusicModel[] = []
|
||||
uploadModal: boolean = false
|
||||
modifyLyricModal: boolean = false
|
||||
lyricRuleValidate = {
|
||||
cloud_id: [
|
||||
{ required: true, message: '请输入网易云ID', trigger: 'blur' }
|
||||
],
|
||||
name: [
|
||||
{ required: true, message: '请输入名称', trigger: 'blur' }
|
||||
],
|
||||
lyric: [
|
||||
{ required: true, message: '请输入歌词正文', trigger: 'blur' }
|
||||
],
|
||||
}
|
||||
prettyBytes = prettyBytes
|
||||
lyricFormData: MusicLyricModel = {}
|
||||
// 是否正在播放音乐
|
||||
musicPlaying: boolean = false
|
||||
musicList: MusicPlayerItem[] = []
|
||||
currentMusic?: MusicPlayerItem
|
||||
created() {
|
||||
this.$http.get<never, any>('/api/v1/music/listLibs').then(data => {
|
||||
this.musicLibs = data
|
||||
this.loadData()
|
||||
})
|
||||
this.$http.get<never, any>('/api/v1/music/listExts').then(data => {
|
||||
this.exts = data
|
||||
})
|
||||
}
|
||||
async loadData() {
|
||||
this.loading = true
|
||||
const data = await this.$http.get<MusicPage, any>('/api/v1/music/list', {params: this.search})
|
||||
selectedIds = []
|
||||
this.loading = false
|
||||
this.total = data.total
|
||||
this.musicData = data.data
|
||||
}
|
||||
dataSelect(selection: MusicModel[]) {
|
||||
selectedIds = selection.map(item => item._id)
|
||||
}
|
||||
findMusicLib(value: string): string | null {
|
||||
const musicLib = this.musicLibs.find(item => item._id === value)
|
||||
return musicLib ? musicLib.name : null
|
||||
}
|
||||
// 根据当前搜索条件播放音乐
|
||||
async playMusic() {
|
||||
try {
|
||||
const data = await this.$http.get<any, any>('/api/v1/music/list/all', {params: selectedIds.length ? {ids: selectedIds} : this.search})
|
||||
this.musicList = data.map((item: MusicModel, index: number) => {
|
||||
const musicItem: MusicPlayerItem = {
|
||||
id: index,
|
||||
title: item.title || item.name,
|
||||
artist: item.artist,
|
||||
album: item.album,
|
||||
src: `/api/v2/common/music/load/${item._id}`,
|
||||
pic: `/api/v2/common/music/album/${item._id}`,
|
||||
}
|
||||
if(item.lyric_id) {
|
||||
musicItem.lrc = `${location.origin}/api/v2/common/music/lyric/${item.lyric_id}`
|
||||
}
|
||||
return musicItem
|
||||
})
|
||||
this.currentMusic = this.musicList[0]
|
||||
this.musicPlaying = true
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
ElMessage.error('获取播放列表失败')
|
||||
}
|
||||
}
|
||||
updateLib(row: MusicModel) {
|
||||
this.currentRow = { ...row }
|
||||
row.isEditing = true
|
||||
}
|
||||
download(row: MusicModel) {
|
||||
const link = document.createElement('a')
|
||||
link.setAttribute('href', `/api/v2/common/music/load/${row._id}`)
|
||||
link.setAttribute('download', row.name)
|
||||
link.setAttribute('target', '_blank')
|
||||
link.click()
|
||||
}
|
||||
remove(row: MusicModel) {
|
||||
ElMessageBox.confirm(`是否确认删除 ${row.name} ?`, '确认删除', {type: 'warning'}).then(async () => {
|
||||
const data = await this.$http.delete<{params: {id: string}}, any>('/api/v2/music/delete', {params: {id: row._id}})
|
||||
ElMessage.success(data.message)
|
||||
this.loadData()
|
||||
}).catch(() => {})
|
||||
}
|
||||
async updateLyric(row: MusicModel) {
|
||||
this.currentRow = { ...row }
|
||||
this.modifyLyricModal = true
|
||||
if (row.lyric_id) {
|
||||
const data = (await this.$http.get<any, any>('/api/v1/music/lyric/get', {params: {lyricId: row.lyric_id}}))
|
||||
data.cloud_id = data.cloud_id ? data.cloud_id.toString() : null
|
||||
this.lyricFormData = data
|
||||
} else {
|
||||
this.lyricFormData = {}
|
||||
}
|
||||
}
|
||||
async saveLyric() {
|
||||
(this.$refs.lyricForm as VForm).validate(async (valid: boolean) => {
|
||||
if (!valid) return
|
||||
this.modalLoading = true
|
||||
const data = await this.$http.post<MusicLyricModel, any>(`/api/v1/music/lyric/save?musicId=${this.currentRow ? this.currentRow._id : ''}`, this.lyricFormData)
|
||||
this.modalLoading = false
|
||||
this.modifyLyricModal = false
|
||||
ElMessage.success(data.message)
|
||||
this.loadData()
|
||||
// 清空表单
|
||||
this.lyricFormData = {}
|
||||
})
|
||||
}
|
||||
async saveMusicLib(row: MusicModel) {
|
||||
if (!this.currentRow) return
|
||||
const data = await this.$http.post<{id: string, libId: string}, any>('/api/v2/music/updateLib', {id: this.currentRow._id, libId: this.currentRow.lib_id})
|
||||
ElMessage.success(data.message)
|
||||
row.lib_id = this.currentRow.lib_id
|
||||
row.isEditing = false
|
||||
}
|
||||
openUploadModal() {
|
||||
this.uploadModal = true
|
||||
this.libIdSelected = null
|
||||
}
|
||||
async uploadMusic() {
|
||||
if (!this.libIdSelected) {
|
||||
ElMessage.warning('请选择歌单')
|
||||
return
|
||||
}
|
||||
// 执行上传
|
||||
(this.$refs.musicUpload as typeof ElUpload).submit()
|
||||
}
|
||||
uploadSuccess(response: MsgResult) {
|
||||
if(response.code === 0) {
|
||||
ElMessage.success(response.message)
|
||||
this.loadData()
|
||||
} else {
|
||||
ElMessage.warning(response.message)
|
||||
}
|
||||
}
|
||||
uploadError(error: Error) {
|
||||
ElMessage.error(error.message)
|
||||
}
|
||||
uploadModalClosed() {
|
||||
(this.$refs.musicUpload as typeof ElUpload).clearFiles()
|
||||
}
|
||||
/**
|
||||
* 创建媒体信息
|
||||
*/
|
||||
musicPlay() {
|
||||
if(!('mediaSession' in window.navigator) || !this.currentMusic) return;
|
||||
const player = <any>this.$refs.player
|
||||
const currentId = this.currentMusic.id
|
||||
navigator.mediaSession.metadata = new MediaMetadata({
|
||||
title: this.currentMusic.title,
|
||||
artist: this.currentMusic.artist,
|
||||
album: this.currentMusic.album,
|
||||
artwork: [{src: location.origin + this.currentMusic.pic}]
|
||||
})
|
||||
navigator.mediaSession.setActionHandler('play', () => { // 播放
|
||||
player.play()
|
||||
})
|
||||
navigator.mediaSession.setActionHandler('pause', () => { // 暂停
|
||||
player.pause()
|
||||
})
|
||||
navigator.mediaSession.setActionHandler('previoustrack', () => { // 上一首
|
||||
if (currentId === 0) { // 已经是第一首
|
||||
player.switch(this.musicList.length - 1)
|
||||
} else {
|
||||
player.switch(currentId - 1)
|
||||
}
|
||||
})
|
||||
navigator.mediaSession.setActionHandler('nexttrack', () => { // 下一首
|
||||
if (currentId === this.musicList.length - 1) { // 已经是最后一首
|
||||
player.switch(0)
|
||||
} else {
|
||||
player.switch(currentId + 1)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
import type { VForm } from '@/types'
|
||||
import http from '@/utils/http'
|
||||
|
||||
class MusicPage extends Page {
|
||||
name?: string
|
||||
@ -367,4 +177,190 @@ class MusicPage extends Page {
|
||||
this.lib_id = []
|
||||
}
|
||||
}
|
||||
|
||||
const store = useStore()
|
||||
const { loading, modalLoading, total, search, setLoadData, loadDataBase, reset, pageChange, pageSizeChange } = useBaseList(new MusicPage())
|
||||
|
||||
const currentRow = ref<MusicModel | null>(null)
|
||||
const libIdSelected = ref<string | null>(null)
|
||||
const exts = ref<string[]>([])
|
||||
const musicLibs = ref<MusicLibModel[]>([])
|
||||
const musicData = ref<MusicModel[]>([])
|
||||
const uploadModal = ref(false)
|
||||
const modifyLyricModal = ref(false)
|
||||
const lyricFormData = ref<MusicLyricModel>({})
|
||||
const musicPlaying = ref(false)
|
||||
const musicList = ref<MusicPlayerItem[]>([])
|
||||
const currentMusic = ref<MusicPlayerItem>()
|
||||
const lyricForm = ref<VForm>()
|
||||
const musicUpload = ref<UploadInstance>()
|
||||
const player = ref<any>()
|
||||
|
||||
const lyricRuleValidate = {
|
||||
cloud_id: [
|
||||
{ required: true, message: '请输入网易云ID', trigger: 'blur' }
|
||||
],
|
||||
name: [
|
||||
{ required: true, message: '请输入名称', trigger: 'blur' }
|
||||
],
|
||||
lyric: [
|
||||
{ required: true, message: '请输入歌词正文', trigger: 'blur' }
|
||||
],
|
||||
}
|
||||
|
||||
let selectedIds: string[] = []
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
const data = await http.get<MusicPage, any>('/api/v1/music/list', {params: search})
|
||||
selectedIds = []
|
||||
loading.value = false
|
||||
total.value = data.total
|
||||
musicData.value = data.data
|
||||
}
|
||||
setLoadData(loadData)
|
||||
|
||||
function dataSelect(selection: MusicModel[]) {
|
||||
selectedIds = selection.map(item => item._id)
|
||||
}
|
||||
function findMusicLib(value: string): string | null {
|
||||
const musicLib = musicLibs.value.find(item => item._id === value)
|
||||
return musicLib ? musicLib.name : null
|
||||
}
|
||||
async function playMusic() {
|
||||
try {
|
||||
const data = await http.get<any, any>('/api/v1/music/list/all', {params: selectedIds.length ? {ids: selectedIds} : search})
|
||||
musicList.value = data.map((item: MusicModel, index: number) => {
|
||||
const musicItem: MusicPlayerItem = {
|
||||
id: index,
|
||||
title: item.title || item.name,
|
||||
artist: item.artist,
|
||||
album: item.album,
|
||||
src: `/api/v2/common/music/load/${item._id}`,
|
||||
pic: `/api/v2/common/music/album/${item._id}`,
|
||||
}
|
||||
if (item.lyric_id) {
|
||||
musicItem.lrc = `${location.origin}/api/v2/common/music/lyric/${item.lyric_id}`
|
||||
}
|
||||
return musicItem
|
||||
})
|
||||
currentMusic.value = musicList.value[0]
|
||||
musicPlaying.value = true
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
ElMessage.error('获取播放列表失败')
|
||||
}
|
||||
}
|
||||
function updateLib(row: MusicModel) {
|
||||
currentRow.value = { ...row }
|
||||
row.isEditing = true
|
||||
}
|
||||
function download(row: MusicModel) {
|
||||
const link = document.createElement('a')
|
||||
link.setAttribute('href', `/api/v2/common/music/load/${row._id}`)
|
||||
link.setAttribute('download', row.name)
|
||||
link.setAttribute('target', '_blank')
|
||||
link.click()
|
||||
}
|
||||
function remove(row: MusicModel) {
|
||||
ElMessageBox.confirm(`是否确认删除 ${row.name} ?`, '确认删除', {type: 'warning'}).then(async () => {
|
||||
const data = await http.delete<{params: {id: string}}, any>('/api/v2/music/delete', {params: {id: row._id}})
|
||||
ElMessage.success(data.message)
|
||||
loadData()
|
||||
}).catch(() => {})
|
||||
}
|
||||
async function updateLyric(row: MusicModel) {
|
||||
currentRow.value = { ...row }
|
||||
modifyLyricModal.value = true
|
||||
if (row.lyric_id) {
|
||||
const data = (await http.get<any, any>('/api/v1/music/lyric/get', {params: {lyricId: row.lyric_id}}))
|
||||
data.cloud_id = data.cloud_id ? data.cloud_id.toString() : null
|
||||
lyricFormData.value = data
|
||||
} else {
|
||||
lyricFormData.value = {}
|
||||
}
|
||||
}
|
||||
async function saveLyric() {
|
||||
lyricForm.value?.validate(async (valid: boolean) => {
|
||||
if (!valid) return
|
||||
modalLoading.value = true
|
||||
const data = await http.post<MusicLyricModel, any>(`/api/v1/music/lyric/save?musicId=${currentRow.value ? currentRow.value._id : ''}`, lyricFormData.value)
|
||||
modalLoading.value = false
|
||||
modifyLyricModal.value = false
|
||||
ElMessage.success(data.message)
|
||||
loadData()
|
||||
lyricFormData.value = {}
|
||||
})
|
||||
}
|
||||
async function saveMusicLib(row: MusicModel) {
|
||||
if (!currentRow.value) return
|
||||
const data = await http.post<{id: string, libId: string}, any>('/api/v2/music/updateLib', {id: currentRow.value._id, libId: currentRow.value.lib_id})
|
||||
ElMessage.success(data.message)
|
||||
row.lib_id = currentRow.value.lib_id
|
||||
row.isEditing = false
|
||||
}
|
||||
function openUploadModal() {
|
||||
uploadModal.value = true
|
||||
libIdSelected.value = null
|
||||
}
|
||||
async function uploadMusic() {
|
||||
if (!libIdSelected.value) {
|
||||
ElMessage.warning('请选择歌单')
|
||||
return
|
||||
}
|
||||
musicUpload.value?.submit()
|
||||
}
|
||||
function uploadSuccess(response: MsgResult) {
|
||||
if (response.code === 0) {
|
||||
ElMessage.success(response.message)
|
||||
loadData()
|
||||
} else {
|
||||
ElMessage.warning(response.message)
|
||||
}
|
||||
}
|
||||
function uploadError(error: Error) {
|
||||
ElMessage.error(error.message)
|
||||
}
|
||||
function uploadModalClosed() {
|
||||
musicUpload.value?.clearFiles()
|
||||
}
|
||||
function musicPlay() {
|
||||
if (!('mediaSession' in window.navigator) || !currentMusic.value) return
|
||||
const currentId = currentMusic.value.id
|
||||
navigator.mediaSession.metadata = new MediaMetadata({
|
||||
title: currentMusic.value.title,
|
||||
artist: currentMusic.value.artist,
|
||||
album: currentMusic.value.album,
|
||||
artwork: [{src: location.origin + currentMusic.value.pic}]
|
||||
})
|
||||
navigator.mediaSession.setActionHandler('play', () => {
|
||||
player.value.play()
|
||||
})
|
||||
navigator.mediaSession.setActionHandler('pause', () => {
|
||||
player.value.pause()
|
||||
})
|
||||
navigator.mediaSession.setActionHandler('previoustrack', () => {
|
||||
if (currentId === 0) {
|
||||
player.value.switch(musicList.value.length - 1)
|
||||
} else {
|
||||
player.value.switch(currentId - 1)
|
||||
}
|
||||
})
|
||||
navigator.mediaSession.setActionHandler('nexttrack', () => {
|
||||
if (currentId === musicList.value.length - 1) {
|
||||
player.value.switch(0)
|
||||
} else {
|
||||
player.value.switch(currentId + 1)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// created
|
||||
http.get<never, any>('/api/v1/music/listLibs').then(data => {
|
||||
musicLibs.value = data
|
||||
loadData()
|
||||
})
|
||||
http.get<never, any>('/api/v1/music/listExts').then(data => {
|
||||
exts.value = data
|
||||
})
|
||||
</script>
|
||||
@ -34,7 +34,7 @@
|
||||
action="/api/v2/photoWall/upload"
|
||||
accept="image/jpeg,image/png"
|
||||
name="image"
|
||||
:headers="{token: $store.state.loginInfo.token}"
|
||||
:headers="{token: store.state.loginInfo.token}"
|
||||
:before-upload="beforeUpload"
|
||||
:on-success="uploadSuccess"
|
||||
:on-error="uploadError"
|
||||
@ -67,8 +67,8 @@
|
||||
</div>
|
||||
<div class="page-container">
|
||||
<el-pagination background
|
||||
:page-sizes="$store.state.pageSizeOpts"
|
||||
:layout="$store.state.pageLayout"
|
||||
:page-sizes="store.state.pageSizeOpts"
|
||||
:layout="store.state.pageLayout"
|
||||
:current-page="search.pageNum"
|
||||
:total="total"
|
||||
@size-change="pageSizeChange"
|
||||
@ -77,81 +77,14 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Options } from 'vue-class-component'
|
||||
<script setup lang="ts">
|
||||
import { ref, h } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { MsgResult, Page } from '@/model/common.dto'
|
||||
import BaseList from '@/model/baselist'
|
||||
import { useBaseList } from '@/model/baselist'
|
||||
import PhotoWallModel from '@/model/api/photowall'
|
||||
import { h } from 'vue'
|
||||
|
||||
let selectedData: string[] = []
|
||||
@Options({
|
||||
name: 'PhotoWall'
|
||||
})
|
||||
export default class PhotoWall extends BaseList<PhotoWallPage> {
|
||||
search = new PhotoWallPage()
|
||||
allowUploadExt = ['jpg','jpeg','png']
|
||||
photowallData = []
|
||||
isUploading: boolean = false
|
||||
async loadData() {
|
||||
this.loading = true
|
||||
const data = await this.$http.get<PhotoWallPage, any>('/api/v1/photowall/list', {params:this.search})
|
||||
selectedData = []
|
||||
this.loading = false
|
||||
this.total = data.total
|
||||
this.photowallData = data.data
|
||||
}
|
||||
deleteAll() {
|
||||
if(!selectedData || !selectedData.length) {
|
||||
ElMessage.warning('请选择要删除的数据')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(`是否确认删除选中的${selectedData.length}条数据?`, '确认删除', {type: 'warning'}).then(async () => {
|
||||
await this.$http.delete('/api/v1/photowall/delete', {params:{_ids: selectedData}})
|
||||
ElMessage.success('删除成功')
|
||||
this.loadData()
|
||||
}).catch(() => {})
|
||||
}
|
||||
dataSelect(selection: PhotoWallModel[]) {
|
||||
selectedData = selection.map(item => item._id)
|
||||
}
|
||||
beforeUpload(file: File): boolean {
|
||||
if(file.size > 10 << 20) {
|
||||
ElMessage.warning('文件大小超过10MB')
|
||||
return false
|
||||
}
|
||||
this.isUploading = true
|
||||
return true
|
||||
}
|
||||
uploadSuccess(response: MsgResult) {
|
||||
if(response.code === 0) {
|
||||
ElMessage.success(response.message)
|
||||
this.loadData()
|
||||
} else {
|
||||
ElMessage.warning(response.message)
|
||||
}
|
||||
this.isUploading = false
|
||||
}
|
||||
uploadError(error: Error) {
|
||||
this.isUploading = false
|
||||
ElMessage.error(error.message)
|
||||
}
|
||||
async preview(row: PhotoWallModel) {
|
||||
const previewHeight = Math.floor(row.height * (500 / row.width))
|
||||
const pictureCdn = await this.$http.get('/api/v1/common/config/picture_cdn')
|
||||
ElMessageBox({
|
||||
title: '图片预览',
|
||||
message: h('img', { style: `width:500px;height:${previewHeight}px;`, src: `${pictureCdn}/${row.name}` }, ''),
|
||||
showCancelButton: false,
|
||||
confirmButtonText: '关闭',
|
||||
customStyle: {width: '530px', maxWidth: 'unset'}
|
||||
}).catch(() => {})
|
||||
}
|
||||
created() {
|
||||
this.loadData()
|
||||
}
|
||||
}
|
||||
import http from '@/utils/http'
|
||||
|
||||
class PhotoWallPage extends Page {
|
||||
name?: string
|
||||
@ -168,4 +101,73 @@ class PhotoWallPage extends Page {
|
||||
this.heightMax = 0
|
||||
}
|
||||
}
|
||||
|
||||
const store = useStore()
|
||||
const { loading, total, search, setLoadData, loadDataBase, reset, pageChange, pageSizeChange } = useBaseList(new PhotoWallPage())
|
||||
|
||||
const allowUploadExt = ['jpg', 'jpeg', 'png']
|
||||
const photowallData = ref([])
|
||||
const isUploading = ref(false)
|
||||
|
||||
let selectedData: string[] = []
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
const data = await http.get<PhotoWallPage, any>('/api/v1/photowall/list', {params: search})
|
||||
selectedData = []
|
||||
loading.value = false
|
||||
total.value = data.total
|
||||
photowallData.value = data.data
|
||||
}
|
||||
setLoadData(loadData)
|
||||
|
||||
function deleteAll() {
|
||||
if (!selectedData || !selectedData.length) {
|
||||
ElMessage.warning('请选择要删除的数据')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(`是否确认删除选中的${selectedData.length}条数据?`, '确认删除', {type: 'warning'}).then(async () => {
|
||||
await http.delete('/api/v1/photowall/delete', {params: {_ids: selectedData}})
|
||||
ElMessage.success('删除成功')
|
||||
loadData()
|
||||
}).catch(() => {})
|
||||
}
|
||||
function dataSelect(selection: PhotoWallModel[]) {
|
||||
selectedData = selection.map(item => item._id)
|
||||
}
|
||||
function beforeUpload(file: File): boolean {
|
||||
if (file.size > 10 << 20) {
|
||||
ElMessage.warning('文件大小超过10MB')
|
||||
return false
|
||||
}
|
||||
isUploading.value = true
|
||||
return true
|
||||
}
|
||||
function uploadSuccess(response: MsgResult) {
|
||||
if (response.code === 0) {
|
||||
ElMessage.success(response.message)
|
||||
loadData()
|
||||
} else {
|
||||
ElMessage.warning(response.message)
|
||||
}
|
||||
isUploading.value = false
|
||||
}
|
||||
function uploadError(error: Error) {
|
||||
isUploading.value = false
|
||||
ElMessage.error(error.message)
|
||||
}
|
||||
async function preview(row: PhotoWallModel) {
|
||||
const previewHeight = Math.floor(row.height * (500 / row.width))
|
||||
const pictureCdn = await http.get('/api/v1/common/config/picture_cdn')
|
||||
ElMessageBox({
|
||||
title: '图片预览',
|
||||
message: h('img', { style: `width:500px;height:${previewHeight}px;`, src: `${pictureCdn}/${row.name}` }, ''),
|
||||
showCancelButton: false,
|
||||
confirmButtonText: '关闭',
|
||||
customStyle: {width: '530px', maxWidth: 'unset'}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// created
|
||||
loadData()
|
||||
</script>
|
||||
@ -9,7 +9,7 @@
|
||||
action="/api/source-image/upload"
|
||||
accept="image/jpeg,image/png,image/svg+xml,image/x-icon"
|
||||
name="image"
|
||||
:headers="{token: $store.state.loginInfo.token}"
|
||||
:headers="{token: store.state.loginInfo.token}"
|
||||
:before-upload="beforeUpload"
|
||||
:on-success="uploadSuccess"
|
||||
:on-error="uploadError"
|
||||
@ -50,8 +50,8 @@
|
||||
</div>
|
||||
<div class="page-container">
|
||||
<el-pagination background
|
||||
:page-sizes="$store.state.pageSizeOpts"
|
||||
:layout="$store.state.pageLayout"
|
||||
:page-sizes="store.state.pageSizeOpts"
|
||||
:layout="store.state.pageLayout"
|
||||
:current-page="search.pageNum"
|
||||
:total="total"
|
||||
@size-change="pageSizeChange"
|
||||
@ -74,105 +74,107 @@
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Options } from 'vue-class-component'
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, h } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import prettyBytes from 'pretty-bytes'
|
||||
import { MsgResult, Page } from '@/model/common.dto'
|
||||
import BaseList from '@/model/baselist'
|
||||
import { useBaseList } from '@/model/baselist'
|
||||
import { SourceImageModel } from '@/model/api/source-image'
|
||||
import { h } from 'vue'
|
||||
import http from '@/utils/http'
|
||||
|
||||
const store = useStore()
|
||||
const { loading, total, search, setLoadData, loadDataBase, reset, pageChange, pageSizeChange, datetimeFormat } = useBaseList(new Page())
|
||||
|
||||
const allowUploadExt = ['jpg', 'jpeg', 'png', 'svg', 'ico']
|
||||
const sourceImageData = ref<SourceImageModel[]>([])
|
||||
const curModifyLabels = ref<string[]>([])
|
||||
const labelList = ref<string[]>([])
|
||||
const curId = ref<string | null>(null)
|
||||
const modifyModal = ref(false)
|
||||
const isUploading = ref(false)
|
||||
|
||||
function renderFunc(h: Function, option: any) {
|
||||
return h('span', null, option.label)
|
||||
}
|
||||
const labels = computed(() => {
|
||||
return labelList.value.map(item => {
|
||||
return { key: item, label: item }
|
||||
})
|
||||
})
|
||||
|
||||
let selectedData: string[] = []
|
||||
@Options({
|
||||
name: 'SourceImage',
|
||||
})
|
||||
export default class SourceImage extends BaseList<Page> {
|
||||
prettyBytes = prettyBytes
|
||||
search = new Page()
|
||||
allowUploadExt = ['jpg','jpeg','png','svg','ico']
|
||||
sourceImageData: SourceImageModel[] = []
|
||||
curModifyLabels: string[] = []
|
||||
labelList: string[] = []
|
||||
curId: string | null = null
|
||||
modifyModal: boolean = false
|
||||
isUploading: boolean = false
|
||||
renderFunc(h: Function, option: any) {
|
||||
return h('span', null, option.label)
|
||||
}
|
||||
get labels() {
|
||||
return this.labelList.map(item => {
|
||||
return { key: item, label: item }
|
||||
})
|
||||
}
|
||||
async loadData(): Promise<void> {
|
||||
this.loading = true
|
||||
const data = await this.$http.get<Page, any>('/api/v1/source-image/list', {params:this.search})
|
||||
selectedData = []
|
||||
this.loading = false
|
||||
this.total = data.total
|
||||
this.sourceImageData = data.data
|
||||
}
|
||||
deleteAll(): void {
|
||||
if(!selectedData.length) {
|
||||
ElMessage.warning('请选择要删除的数据')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(`是否确认删除选中的${selectedData.length}条数据?`, '确认删除', {type: 'warning'}).then(async () => {
|
||||
await this.$http.delete('/api/v1/source-image/delete', {params:{_ids: selectedData}})
|
||||
ElMessage.success('删除成功')
|
||||
this.loadData()
|
||||
}).catch(() => {})
|
||||
}
|
||||
dataSelect(selection: SourceImageModel[]): void {
|
||||
selectedData = selection.map(item => item._id)
|
||||
}
|
||||
beforeUpload(file: File): boolean {
|
||||
if(file.size > 10 << 20) {
|
||||
ElMessage.warning('文件大小超过10MB')
|
||||
return false
|
||||
}
|
||||
this.isUploading = true
|
||||
return true
|
||||
}
|
||||
uploadSuccess(response: MsgResult): void {
|
||||
if(response.status) {
|
||||
ElMessage.success(response.message)
|
||||
this.loadData()
|
||||
} else {
|
||||
ElMessage.warning(response.message)
|
||||
}
|
||||
this.isUploading = false
|
||||
}
|
||||
uploadError(error: Error): void {
|
||||
this.isUploading = false
|
||||
ElMessage.error(error.message)
|
||||
}
|
||||
preview(row: SourceImageModel): void {
|
||||
ElMessageBox({
|
||||
title: '图片预览',
|
||||
message: h('img', { style: `width:500px`, src: `/api/v1/common/randomBg?id=${row._id}` }, ''),
|
||||
showCancelButton: false,
|
||||
confirmButtonText: '关闭',
|
||||
customStyle: {width: '530px', maxWidth: 'unset'}
|
||||
}).catch(() => {})
|
||||
}
|
||||
modifyTags(item: SourceImageModel): void {
|
||||
this.curModifyLabels.length = 0
|
||||
if(item.label) {
|
||||
this.curModifyLabels.push(...item.label)
|
||||
}
|
||||
this.curId = item._id
|
||||
this.modifyModal = true
|
||||
}
|
||||
async tarnsferChange(newTargetKeys: string[], direction: 'right' | 'left', moveKeys: string[]) {
|
||||
await this.$http.post('/api/v1/source-image/updateLabel', {id: this.curId, labels: newTargetKeys})
|
||||
}
|
||||
created() {
|
||||
this.$http.get<never, any>('/api/v1/common/config/image_label').then(data => {
|
||||
this.labelList.push(...data)
|
||||
this.loadData()
|
||||
})
|
||||
}
|
||||
|
||||
async function loadData(): Promise<void> {
|
||||
loading.value = true
|
||||
const data = await http.get<Page, any>('/api/v1/source-image/list', {params: search})
|
||||
selectedData = []
|
||||
loading.value = false
|
||||
total.value = data.total
|
||||
sourceImageData.value = data.data
|
||||
}
|
||||
setLoadData(loadData)
|
||||
|
||||
function deleteAll(): void {
|
||||
if (!selectedData.length) {
|
||||
ElMessage.warning('请选择要删除的数据')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(`是否确认删除选中的${selectedData.length}条数据?`, '确认删除', {type: 'warning'}).then(async () => {
|
||||
await http.delete('/api/v1/source-image/delete', {params: {_ids: selectedData}})
|
||||
ElMessage.success('删除成功')
|
||||
loadData()
|
||||
}).catch(() => {})
|
||||
}
|
||||
function dataSelect(selection: SourceImageModel[]): void {
|
||||
selectedData = selection.map(item => item._id)
|
||||
}
|
||||
function beforeUpload(file: File): boolean {
|
||||
if (file.size > 10 << 20) {
|
||||
ElMessage.warning('文件大小超过10MB')
|
||||
return false
|
||||
}
|
||||
isUploading.value = true
|
||||
return true
|
||||
}
|
||||
function uploadSuccess(response: MsgResult): void {
|
||||
if (response.status) {
|
||||
ElMessage.success(response.message)
|
||||
loadData()
|
||||
} else {
|
||||
ElMessage.warning(response.message)
|
||||
}
|
||||
isUploading.value = false
|
||||
}
|
||||
function uploadError(error: Error): void {
|
||||
isUploading.value = false
|
||||
ElMessage.error(error.message)
|
||||
}
|
||||
function preview(row: SourceImageModel): void {
|
||||
ElMessageBox({
|
||||
title: '图片预览',
|
||||
message: h('img', { style: `width:500px`, src: `/api/v1/common/randomBg?id=${row._id}` }, ''),
|
||||
showCancelButton: false,
|
||||
confirmButtonText: '关闭',
|
||||
customStyle: {width: '530px', maxWidth: 'unset'}
|
||||
}).catch(() => {})
|
||||
}
|
||||
function modifyTags(item: SourceImageModel): void {
|
||||
curModifyLabels.value.length = 0
|
||||
if (item.label) {
|
||||
curModifyLabels.value.push(...item.label)
|
||||
}
|
||||
curId.value = item._id
|
||||
modifyModal.value = true
|
||||
}
|
||||
async function tarnsferChange(newTargetKeys: string[], direction: 'right' | 'left', moveKeys: string[]) {
|
||||
await http.post('/api/v1/source-image/updateLabel', {id: curId.value, labels: newTargetKeys})
|
||||
}
|
||||
|
||||
// created
|
||||
http.get<never, any>('/api/v1/common/config/image_label').then(data => {
|
||||
labelList.value.push(...data)
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
@ -38,7 +38,7 @@
|
||||
action="/api/system/deployBlog"
|
||||
accept="application/zip"
|
||||
name="blogZip"
|
||||
:headers="{token: $store.state.loginInfo.token}"
|
||||
:headers="{token: store.state.loginInfo.token}"
|
||||
:before-upload="beforeUpload"
|
||||
:on-success="uploadSuccess"
|
||||
:on-error="uploadError"
|
||||
@ -57,6 +57,7 @@
|
||||
<el-tree :props="treeProps" :load="loadTreeData" lazy highlight-current @node-click="articlePreview" />
|
||||
</el-col>
|
||||
<el-col :span="20">
|
||||
<div class="table-container">
|
||||
<el-table :data="articleData" v-loading="loading" stripe @selection-change="dataSelect">
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="title" label="标题" />
|
||||
@ -92,14 +93,15 @@
|
||||
</el-table>
|
||||
<div class="page-container">
|
||||
<el-pagination background
|
||||
:page-sizes="$store.state.pageSizeOpts"
|
||||
:layout="$store.state.pageLayout"
|
||||
:page-sizes="store.state.pageSizeOpts"
|
||||
:layout="store.state.pageLayout"
|
||||
:current-page="search.pageNum"
|
||||
:total="total"
|
||||
@size-change="pageSizeChange"
|
||||
@current-change="pageChange">
|
||||
</el-pagination>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-drawer
|
||||
@ -111,129 +113,16 @@
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import hyperdown from 'hyperdown'
|
||||
import { ArticleModel, TreeNodeData, TreeNodeSource } from '@/model/system/article'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import Node from 'element-plus/lib/components/tree/src/model/node'
|
||||
import { Options } from 'vue-class-component'
|
||||
import BaseList from '@/model/baselist'
|
||||
import { useBaseList } from '@/model/baselist'
|
||||
import { Page, MsgResult } from '@/model/common.dto'
|
||||
|
||||
let selectedData: string[] = []
|
||||
@Options({
|
||||
name: 'Article'
|
||||
})
|
||||
export default class Article extends BaseList<ArticlePage> {
|
||||
search = new ArticlePage()
|
||||
articleData: ArticleModel[] = []
|
||||
tags: string[] = [] // 所有标签
|
||||
categories: string[] = [] // 所有分类
|
||||
markdownPreview :{ // markdown内容预览
|
||||
show: boolean,
|
||||
title: string | null,
|
||||
content: string | null
|
||||
} = {
|
||||
show: false,
|
||||
title: null,
|
||||
content: null
|
||||
}
|
||||
isUploading: boolean = false
|
||||
async loadData() {
|
||||
this.loading = true
|
||||
const data = await this.$http.get<ArticlePage, any>('/api/v1/article/list', {params:this.search})
|
||||
selectedData = []
|
||||
this.loading = false
|
||||
this.total = data.total
|
||||
this.articleData = data.data
|
||||
}
|
||||
splitWord() {
|
||||
if(!selectedData.length) {
|
||||
ElMessage.warning('请选择要执行分词的文章')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(`是否确认对选中的${selectedData.length}篇文章执行分词处理?`, '操作确认', {type: 'info'}).then(async () => {
|
||||
const data = await this.$http.put<{_ids: string[]}, any>('/api/v1/article/splitWord', {_ids: selectedData})
|
||||
if(data.status) {
|
||||
ElMessage.success(data.message)
|
||||
} else {
|
||||
ElMessage.warning(data.message)
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
pullArticles() {
|
||||
ElMessageBox.confirm('确认拉取全部文章?', '操作确认', {type: 'info'}).then(async () => {
|
||||
const data = await this.$http.put<never, any>('/api/v1/article/pull')
|
||||
if(data.status) {
|
||||
ElMessage.success(data.message)
|
||||
this.loadData()
|
||||
} else {
|
||||
ElMessage.warning(data.message)
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
dataSelect(selection: ArticleModel[]) {
|
||||
selectedData = selection.map(item => item._id)
|
||||
}
|
||||
beforeUpload(file: File): boolean {
|
||||
this.isUploading = true
|
||||
return true
|
||||
}
|
||||
uploadSuccess(response: MsgResult) {
|
||||
if(response.status) {
|
||||
ElMessage.success(response.message)
|
||||
} else {
|
||||
ElMessage.warning(response.message)
|
||||
}
|
||||
this.isUploading = false
|
||||
}
|
||||
uploadError(error: Error) {
|
||||
this.isUploading = false
|
||||
ElMessage.error(error.message)
|
||||
}
|
||||
readonly treeProps = {
|
||||
label: 'name',
|
||||
children: 'children',
|
||||
isLeaf: 'isLeaf',
|
||||
}
|
||||
async loadTreeData(node: Node, resolve: Function) {
|
||||
const childItems: TreeNodeSource[] = await this.$http.get('/api/v1/article/tree', {params:{deep: node.level, parent: node.data.name}})
|
||||
resolve(childItems.map((childItem): TreeNodeData => {
|
||||
const treeNode: TreeNodeData = {
|
||||
name: childItem._id,
|
||||
title: childItem.article_id ? childItem._id : `${childItem._id}(${childItem.cnt})`,
|
||||
id: childItem.article_id || childItem._id,
|
||||
isLeaf: !!childItem.article_id
|
||||
}
|
||||
return treeNode
|
||||
}))
|
||||
}
|
||||
/**
|
||||
* 树节点选中事件
|
||||
* @param selectNodes 当前已选中的节点(适用于带复选框的)
|
||||
* @param curNode 本次选中的节点
|
||||
*/
|
||||
async articlePreview(node: TreeNodeData) {
|
||||
if(!node.isLeaf) return
|
||||
// 加载文章markdown预览
|
||||
const mdText = await this.$http.get<never, any>('/api/v1/article/markdown', {params:{id: node.id}})
|
||||
this.markdownPreview.show = true
|
||||
const markdownHtml = new hyperdown().makeHtml(mdText)
|
||||
this.markdownPreview.content = markdownHtml.replace(/(?<=<pre><code[^>]*?>)[\s\S]*?(?=<\/code><\/pre>)/gi, content => {
|
||||
return content.replace(/_&/g, ' ').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&')
|
||||
})
|
||||
this.markdownPreview.title = node.name
|
||||
}
|
||||
created() {
|
||||
this.loadData()
|
||||
this.$http.get<never, any>('/api/v1/article/listCategories').then(data => {
|
||||
this.categories = data
|
||||
})
|
||||
this.$http.get<never, any>('/api/v1/article/listTags').then(data => {
|
||||
this.tags = data
|
||||
})
|
||||
}
|
||||
}
|
||||
import http from '@/utils/http'
|
||||
|
||||
class ArticlePage extends Page {
|
||||
title?: string
|
||||
@ -250,4 +139,115 @@ class ArticlePage extends Page {
|
||||
this.isSplited = undefined
|
||||
}
|
||||
}
|
||||
|
||||
const store = useStore()
|
||||
const { loading, total, search, setLoadData, loadDataBase, reset, pageChange, pageSizeChange, datetimeFormat } = useBaseList(new ArticlePage())
|
||||
|
||||
const articleData = ref<ArticleModel[]>([])
|
||||
const tags = ref<string[]>([])
|
||||
const categories = ref<string[]>([])
|
||||
const markdownPreview = reactive<{
|
||||
show: boolean
|
||||
title: string | null
|
||||
content: string | null
|
||||
}>({
|
||||
show: false,
|
||||
title: null,
|
||||
content: null
|
||||
})
|
||||
const isUploading = ref(false)
|
||||
|
||||
let selectedData: string[] = []
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
const data = await http.get<ArticlePage, any>('/api/v1/article/list', {params: search})
|
||||
selectedData = []
|
||||
loading.value = false
|
||||
total.value = data.total
|
||||
articleData.value = data.data
|
||||
}
|
||||
setLoadData(loadData)
|
||||
|
||||
function splitWord() {
|
||||
if (!selectedData.length) {
|
||||
ElMessage.warning('请选择要执行分词的文章')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(`是否确认对选中的${selectedData.length}篇文章执行分词处理?`, '操作确认', {type: 'info'}).then(async () => {
|
||||
const data = await http.put<{_ids: string[]}, any>('/api/v1/article/splitWord', {_ids: selectedData})
|
||||
if (data.status) {
|
||||
ElMessage.success(data.message)
|
||||
} else {
|
||||
ElMessage.warning(data.message)
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
function pullArticles() {
|
||||
ElMessageBox.confirm('确认拉取全部文章?', '操作确认', {type: 'info'}).then(async () => {
|
||||
const data = await http.put<never, any>('/api/v1/article/pull')
|
||||
if (data.status) {
|
||||
ElMessage.success(data.message)
|
||||
loadData()
|
||||
} else {
|
||||
ElMessage.warning(data.message)
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
function dataSelect(selection: ArticleModel[]) {
|
||||
selectedData = selection.map(item => item._id)
|
||||
}
|
||||
function beforeUpload(file: File): boolean {
|
||||
isUploading.value = true
|
||||
return true
|
||||
}
|
||||
function uploadSuccess(response: MsgResult) {
|
||||
if (response.status) {
|
||||
ElMessage.success(response.message)
|
||||
} else {
|
||||
ElMessage.warning(response.message)
|
||||
}
|
||||
isUploading.value = false
|
||||
}
|
||||
function uploadError(error: Error) {
|
||||
isUploading.value = false
|
||||
ElMessage.error(error.message)
|
||||
}
|
||||
|
||||
const treeProps = {
|
||||
label: 'name',
|
||||
children: 'children',
|
||||
isLeaf: 'isLeaf',
|
||||
}
|
||||
async function loadTreeData(node: Node, resolve: Function) {
|
||||
const childItems: TreeNodeSource[] = await http.get('/api/v1/article/tree', {params: {deep: node.level, parent: node.data.name}})
|
||||
resolve(childItems.map((childItem): TreeNodeData => {
|
||||
const treeNode: TreeNodeData = {
|
||||
name: childItem._id,
|
||||
title: childItem.article_id ? childItem._id : `${childItem._id}(${childItem.cnt})`,
|
||||
id: childItem.article_id || childItem._id,
|
||||
isLeaf: !!childItem.article_id
|
||||
}
|
||||
return treeNode
|
||||
}))
|
||||
}
|
||||
async function articlePreview(node: TreeNodeData) {
|
||||
if (!node.isLeaf) return
|
||||
const mdText = await http.get<never, any>('/api/v1/article/markdown', {params: {id: node.id}})
|
||||
markdownPreview.show = true
|
||||
const markdownHtml = new hyperdown().makeHtml(mdText)
|
||||
markdownPreview.content = markdownHtml.replace(/(?<=<pre><code[^>]*?>)[\s\S]*?(?=<\/code><\/pre>)/gi, (content: string) => {
|
||||
return content.replace(/_&/g, ' ').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&')
|
||||
})
|
||||
markdownPreview.title = node.name
|
||||
}
|
||||
|
||||
// created
|
||||
loadData()
|
||||
http.get<never, any>('/api/v1/article/listCategories').then(data => {
|
||||
categories.value = data
|
||||
})
|
||||
http.get<never, any>('/api/v1/article/listTags').then(data => {
|
||||
tags.value = data
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -7,188 +7,189 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
import { Options, Vue } from 'vue-class-component'
|
||||
import http from '@/utils/http'
|
||||
|
||||
@Options({
|
||||
name: 'Statistics'
|
||||
})
|
||||
export default class Statistics extends Vue {
|
||||
categoriesChartLoading: boolean = false
|
||||
publishDatesChartLoading: boolean = false
|
||||
timelineWordsChartLoading: boolean = false
|
||||
async mounted() {
|
||||
this.categoriesChartLoading = true
|
||||
this.publishDatesChartLoading = true
|
||||
this.timelineWordsChartLoading = true
|
||||
const categoriesChart = ref<HTMLElement>()
|
||||
const publishDatesChart = ref<HTMLElement>()
|
||||
const timelineWordsChart = ref<HTMLElement>()
|
||||
|
||||
const articleData = await this.$http.get<{params:{type:string}}, any>('/api/v1/article/statistics', {params:{type:'normal'}})
|
||||
this.categoriesChartOption.legend.data = articleData.categories.map((item: any) => item._id)
|
||||
this.categoriesChartOption.series.data = articleData.categories.map((item: any) => {
|
||||
return {name: item._id, value: item.cnt}
|
||||
})
|
||||
this.publishDatesChartOption.xAxis.data = articleData.publishDates.map((item: any) => item._id)
|
||||
this.publishDatesChartOption.series.data = articleData.publishDates.map((item: any) => item.cnt)
|
||||
|
||||
const categoriesChart = echarts.init(this.$refs.categoriesChart as HTMLElement)
|
||||
categoriesChart.setOption(this.categoriesChartOption)
|
||||
const publishDatesChart = echarts.init(this.$refs.publishDatesChart as HTMLElement)
|
||||
publishDatesChart.setOption(this.publishDatesChartOption)
|
||||
this.categoriesChartLoading = false
|
||||
this.publishDatesChartLoading = false
|
||||
const categoriesChartLoading = ref(false)
|
||||
const publishDatesChartLoading = ref(false)
|
||||
const timelineWordsChartLoading = ref(false)
|
||||
|
||||
const timelineData = await this.$http.get<{params:{type:string}}, any>('/api/v1/article/statistics', {params:{type:'timelineWords'}})
|
||||
this.timelineWordsChartOption.timeline.data = timelineData.timelineWords.map((item: any) => item._id)
|
||||
timelineData.timelineWords.forEach((item: any) => {
|
||||
this.timelineWordsChartOption.options.push({
|
||||
title: { text: `${item._id}年发布的文章` },
|
||||
xAxis: { data: item.keys.map((keyItem: any) => keyItem.key) },
|
||||
series: { data: item.keys.map((keyItem: any) => keyItem.total) }
|
||||
})
|
||||
})
|
||||
const timelineWordsChart = echarts.init(this.$refs.timelineWordsChart as HTMLElement)
|
||||
timelineWordsChart.setOption(this.timelineWordsChartOption)
|
||||
this.timelineWordsChartLoading = false
|
||||
const categoriesChartOption: any = {
|
||||
title: {
|
||||
text: '文章分类',
|
||||
x: 'center',
|
||||
top: 10
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: "{a} <br/>{b} : {c} ({d}%)"
|
||||
},
|
||||
legend: {
|
||||
type: 'scroll',
|
||||
orient: 'vertical',
|
||||
right: 10,
|
||||
top: 50,
|
||||
bottom: 20,
|
||||
data: [],
|
||||
},
|
||||
series: {
|
||||
name: '类别',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
center: ['40%', '50%'],
|
||||
itemStyle: {
|
||||
borderRadius: 5,
|
||||
borderColor: '#FFF',
|
||||
borderWidth: 1
|
||||
},
|
||||
data: []
|
||||
}
|
||||
|
||||
categoriesChartOption = {
|
||||
title : {
|
||||
text: '文章分类',
|
||||
x: 'center',
|
||||
top: 10
|
||||
}
|
||||
const publishDatesChartOption: any = {
|
||||
title: {
|
||||
left: 'center',
|
||||
text: '文章发布时间',
|
||||
top: 10
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
animation: false,
|
||||
label: {
|
||||
backgroundColor: '#ccc',
|
||||
borderColor: '#aaa',
|
||||
borderWidth: 1,
|
||||
shadowBlur: 0,
|
||||
shadowOffsetX: 0,
|
||||
shadowOffsetY: 0,
|
||||
color: '#222'
|
||||
}
|
||||
},
|
||||
tooltip : {
|
||||
trigger: 'item',
|
||||
formatter: "{a} <br/>{b} : {c} ({d}%)"
|
||||
},
|
||||
legend: {
|
||||
type: 'scroll',
|
||||
orient: 'vertical',
|
||||
right: 10,
|
||||
top: 50,
|
||||
bottom: 20,
|
||||
data: [],
|
||||
},
|
||||
series: {
|
||||
name: '类别',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
center: ['40%', '50%'],
|
||||
itemStyle: {
|
||||
borderRadius: 5,
|
||||
borderColor: '#FFF',
|
||||
borderWidth: 1
|
||||
},
|
||||
data: []
|
||||
},
|
||||
xAxis: {
|
||||
name: '发布时间',
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: []
|
||||
},
|
||||
yAxis: {
|
||||
name: '文章数量',
|
||||
type: 'value',
|
||||
max: function (value: {max: number}) {
|
||||
return value.max + 10
|
||||
}
|
||||
}
|
||||
publishDatesChartOption = {
|
||||
title: {
|
||||
left: 'center',
|
||||
text: '文章发布时间',
|
||||
top: 10
|
||||
},
|
||||
dataZoom: [{
|
||||
type: 'inside',
|
||||
start: 0,
|
||||
end: 100
|
||||
}, {
|
||||
start: 0,
|
||||
end: 10
|
||||
}],
|
||||
series: {
|
||||
name: '文章数量',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'none',
|
||||
sampling: 'average',
|
||||
itemStyle: {
|
||||
color: 'rgb(255, 70, 131)'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
animation: false,
|
||||
label: {
|
||||
backgroundColor: '#ccc',
|
||||
borderColor: '#aaa',
|
||||
borderWidth: 1,
|
||||
shadowBlur: 0,
|
||||
shadowOffsetX: 0,
|
||||
shadowOffsetY: 0,
|
||||
color: '#222'
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{
|
||||
offset: 0,
|
||||
color: 'rgb(255, 158, 68)'
|
||||
}, {
|
||||
offset: 1,
|
||||
color: 'rgb(255, 70, 131)'
|
||||
}
|
||||
},
|
||||
])
|
||||
},
|
||||
xAxis: {
|
||||
name: '发布时间',
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: []
|
||||
},
|
||||
yAxis: {
|
||||
name: '文章数量',
|
||||
type: 'value',
|
||||
max: function(value: {max: number}) {
|
||||
return value.max + 10
|
||||
}
|
||||
},
|
||||
dataZoom: [{
|
||||
type: 'inside',
|
||||
start: 0,
|
||||
end: 100
|
||||
},{
|
||||
start: 0,
|
||||
end: 10
|
||||
}],
|
||||
series: {
|
||||
name: '文章数量',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'none',
|
||||
sampling: 'average',
|
||||
itemStyle: {
|
||||
color: 'rgb(255, 70, 131)'
|
||||
},
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{
|
||||
offset: 0,
|
||||
color: 'rgb(255, 158, 68)'
|
||||
},{
|
||||
offset: 1,
|
||||
color: 'rgb(255, 70, 131)'
|
||||
}
|
||||
])
|
||||
},
|
||||
data: []
|
||||
}
|
||||
data: []
|
||||
}
|
||||
timelineWordsChartOption: any = {
|
||||
options: [],
|
||||
timeline: {
|
||||
axisType: 'category',
|
||||
autoPlay: false,
|
||||
playInterval: 1000,
|
||||
data: [],
|
||||
},
|
||||
title: {
|
||||
left: 'center',
|
||||
subtext: '数据来自文章分词结果'
|
||||
},
|
||||
calculable : true,
|
||||
grid: {
|
||||
top: 80,
|
||||
bottom: 80
|
||||
},
|
||||
xAxis: {
|
||||
name: '高频词汇',
|
||||
type: 'category',
|
||||
splitLine: {show: false}
|
||||
},
|
||||
yAxis: {
|
||||
name: '词汇出现次数',
|
||||
type: 'value'
|
||||
},
|
||||
tooltip : {
|
||||
trigger: 'axis',
|
||||
axisPointer : { // 坐标轴指示器,坐标轴触发有效
|
||||
type : 'shadow' // 默认为直线,可选为:'line' | 'shadow'
|
||||
}
|
||||
},
|
||||
series: {
|
||||
type: 'bar',
|
||||
itemStyle: {
|
||||
borderRadius: 5
|
||||
}
|
||||
}
|
||||
const timelineWordsChartOption: any = {
|
||||
options: [],
|
||||
timeline: {
|
||||
axisType: 'category',
|
||||
autoPlay: false,
|
||||
playInterval: 1000,
|
||||
data: [],
|
||||
},
|
||||
title: {
|
||||
left: 'center',
|
||||
subtext: '数据来自文章分词结果'
|
||||
},
|
||||
calculable: true,
|
||||
grid: {
|
||||
top: 80,
|
||||
bottom: 80
|
||||
},
|
||||
xAxis: {
|
||||
name: '高频词汇',
|
||||
type: 'category',
|
||||
splitLine: {show: false}
|
||||
},
|
||||
yAxis: {
|
||||
name: '词汇出现次数',
|
||||
type: 'value'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
}
|
||||
},
|
||||
series: {
|
||||
type: 'bar',
|
||||
itemStyle: {
|
||||
borderRadius: 5
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
categoriesChartLoading.value = true
|
||||
publishDatesChartLoading.value = true
|
||||
timelineWordsChartLoading.value = true
|
||||
|
||||
const articleData = await http.get<{params:{type:string}}, any>('/api/v1/article/statistics', {params: {type: 'normal'}})
|
||||
categoriesChartOption.legend.data = articleData.categories.map((item: any) => item._id)
|
||||
categoriesChartOption.series.data = articleData.categories.map((item: any) => {
|
||||
return {name: item._id, value: item.cnt}
|
||||
})
|
||||
publishDatesChartOption.xAxis.data = articleData.publishDates.map((item: any) => item._id)
|
||||
publishDatesChartOption.series.data = articleData.publishDates.map((item: any) => item.cnt)
|
||||
|
||||
const categoriesChartInstance = echarts.init(categoriesChart.value as HTMLElement)
|
||||
categoriesChartInstance.setOption(categoriesChartOption)
|
||||
const publishDatesChartInstance = echarts.init(publishDatesChart.value as HTMLElement)
|
||||
publishDatesChartInstance.setOption(publishDatesChartOption)
|
||||
categoriesChartLoading.value = false
|
||||
publishDatesChartLoading.value = false
|
||||
|
||||
const timelineData = await http.get<{params:{type:string}}, any>('/api/v1/article/statistics', {params: {type: 'timelineWords'}})
|
||||
timelineWordsChartOption.timeline.data = timelineData.timelineWords.map((item: any) => item._id)
|
||||
timelineData.timelineWords.forEach((item: any) => {
|
||||
timelineWordsChartOption.options.push({
|
||||
title: {text: `${item._id}年发布的文章`},
|
||||
xAxis: {data: item.keys.map((keyItem: any) => keyItem.key)},
|
||||
series: {data: item.keys.map((keyItem: any) => keyItem.total)}
|
||||
})
|
||||
})
|
||||
const timelineWordsChartInstance = echarts.init(timelineWordsChart.value as HTMLElement)
|
||||
timelineWordsChartInstance.setOption(timelineWordsChartOption)
|
||||
timelineWordsChartLoading.value = false
|
||||
})
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.echarts-container {
|
||||
|
||||
@ -55,95 +55,93 @@
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Options, Vue } from 'vue-class-component'
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import moment from 'moment'
|
||||
import http from '@/utils/http'
|
||||
import SystemConfigAdd from './SystemConfigAdd.vue'
|
||||
import { SystemConfigModel } from '@/model/system/system-config'
|
||||
import { VForm } from '@/types'
|
||||
|
||||
@Options({
|
||||
name: 'SystemConfig',
|
||||
components: { SystemConfigAdd }
|
||||
const modalLoading = ref(false)
|
||||
const loading = ref(false)
|
||||
const search = ref<{name?: string}>({})
|
||||
const systemConfigData = ref<SystemConfigModel[]>([])
|
||||
const addModal = ref(false)
|
||||
const modalTitle = ref('')
|
||||
const formData = ref<SystemConfigModel>({
|
||||
name: '',
|
||||
value: '',
|
||||
description: '',
|
||||
is_public: false
|
||||
})
|
||||
export default class SystemConfig extends Vue {
|
||||
modalLoading: boolean = false
|
||||
loading: boolean = false
|
||||
search: {name?:string} = {}
|
||||
systemConfigData: SystemConfigModel[] = []
|
||||
addModal: boolean = false
|
||||
modalTitle: string = ''
|
||||
formData: SystemConfigModel = {
|
||||
|
||||
const addForm = ref<InstanceType<typeof SystemConfigAdd>>()
|
||||
|
||||
function reset() {
|
||||
search.value = {}
|
||||
loadData()
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
systemConfigData.value = await http.get('/api/v1/system/config/list', {params: search.value})
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
function add() {
|
||||
formData.value = {
|
||||
name: '',
|
||||
value: '',
|
||||
description: '',
|
||||
is_public: false
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.search = {}
|
||||
this.loadData()
|
||||
}
|
||||
async loadData() {
|
||||
this.loading = true
|
||||
this.systemConfigData = await this.$http.get('/api/v1/system/config/list', {params:this.search})
|
||||
this.loading = false
|
||||
}
|
||||
add() {
|
||||
// 清空表单
|
||||
this.formData = {
|
||||
name: '',
|
||||
value: '',
|
||||
description: '',
|
||||
is_public: false
|
||||
}
|
||||
this.modalTitle = '新增配置项'
|
||||
this.addModal = true
|
||||
}
|
||||
update(row: SystemConfigModel) {
|
||||
const formData = Object.assign({}, row)
|
||||
formData.value = JSON.stringify(formData.value, null, ' ')
|
||||
this.formData = formData
|
||||
this.modalTitle = '修改配置项'
|
||||
this.addModal = true
|
||||
}
|
||||
async save() {
|
||||
((this.$refs.addForm as Vue).$refs.configForm as VForm).validate(async (valid: boolean) => {
|
||||
if (!valid) return
|
||||
this.modalLoading = true
|
||||
const data = await this.$http.post<SystemConfigModel, any>('/api/v1/system/config/save', this.formData)
|
||||
this.modalLoading = false
|
||||
this.addModal = false
|
||||
ElMessage.success(data.message)
|
||||
this.loadData()
|
||||
})
|
||||
}
|
||||
remove(row: SystemConfigModel) {
|
||||
ElMessageBox.confirm(`是否确认删除 ${row.name} 配置项?`, '确认删除', {type: 'warning'}).then(async () => {
|
||||
const data = await this.$http.delete<{params: {id: string}}, any>('/api/v1/system/config/delete', {params: {id: row._id}})
|
||||
if(data.status) {
|
||||
ElMessage.success(data.message)
|
||||
this.loadData()
|
||||
} else {
|
||||
ElMessage.warning(data.message)
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
created() {
|
||||
this.loadData()
|
||||
}
|
||||
clearValidate() {
|
||||
this.$nextTick(() => {
|
||||
((this.$refs.addForm as Vue).$refs.configForm as VForm).clearValidate()
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 日期时间格式化
|
||||
* @param dateStr 日期时间
|
||||
*/
|
||||
datetimeFormat(dateStr: string) {
|
||||
return dateStr ? moment(dateStr).format('YYYY-MM-DD HH:mm:ss') : null
|
||||
}
|
||||
modalTitle.value = '新增配置项'
|
||||
addModal.value = true
|
||||
}
|
||||
|
||||
function update(row: SystemConfigModel) {
|
||||
const data = Object.assign({}, row)
|
||||
data.value = JSON.stringify(data.value, null, ' ')
|
||||
formData.value = data
|
||||
modalTitle.value = '修改配置项'
|
||||
addModal.value = true
|
||||
}
|
||||
|
||||
async function save() {
|
||||
addForm.value?.configForm?.validate(async (valid: boolean) => {
|
||||
if (!valid) return
|
||||
modalLoading.value = true
|
||||
const data = await http.post<SystemConfigModel, any>('/api/v1/system/config/save', formData.value)
|
||||
modalLoading.value = false
|
||||
addModal.value = false
|
||||
ElMessage.success(data.message)
|
||||
loadData()
|
||||
})
|
||||
}
|
||||
|
||||
function remove(row: SystemConfigModel) {
|
||||
ElMessageBox.confirm(`是否确认删除 ${row.name} 配置项?`, '确认删除', {type: 'warning'}).then(async () => {
|
||||
const data = await http.delete<{params: {id: string}}, any>('/api/v1/system/config/delete', {params: {id: row._id}})
|
||||
if(data.status) {
|
||||
ElMessage.success(data.message)
|
||||
loadData()
|
||||
} else {
|
||||
ElMessage.warning(data.message)
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
function clearValidate() {
|
||||
nextTick(() => {
|
||||
addForm.value?.configForm?.clearValidate()
|
||||
})
|
||||
}
|
||||
|
||||
function datetimeFormat(dateStr: string) {
|
||||
return dateStr ? moment(dateStr).format('YYYY-MM-DD HH:mm:ss') : null
|
||||
}
|
||||
|
||||
loadData()
|
||||
</script>
|
||||
@ -19,46 +19,45 @@
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Options, Vue } from 'vue-class-component'
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import http from '@/utils/http'
|
||||
import { SystemConfigModel } from '@/model/system/system-config'
|
||||
import { VForm } from '@/types'
|
||||
|
||||
@Options({
|
||||
name: 'SystemConfigAdd',
|
||||
props: {
|
||||
formData: Object
|
||||
}
|
||||
})
|
||||
export default class SystenConfigAdd extends Vue {
|
||||
formData!: SystemConfigModel
|
||||
get ruleValidate() {
|
||||
return {
|
||||
name: [
|
||||
{ required: true, message: '请输入配置项名称', trigger: 'blur' },
|
||||
{ validator: (rule: object, value: string, callback: Function) => {
|
||||
this.$http.get<any, any>('/api/v1/system/config/exists', {params: {name: value, id: this.formData._id}}).then(data => {
|
||||
if(data.data.exists) {
|
||||
callback(new Error('配置项名称已存在'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
})
|
||||
}, trigger: 'blur'
|
||||
}
|
||||
],
|
||||
value: [
|
||||
{ required: true, message: '请输入配置项值', trigger: 'blur' },
|
||||
{ validator: (rule: object, value: string, callback: Function) => {
|
||||
try {
|
||||
JSON.parse(value)
|
||||
callback()
|
||||
} catch (e) {
|
||||
callback(new Error('值不符合JSON字符串格式'))
|
||||
}
|
||||
}, trigger: 'blur'
|
||||
}
|
||||
],
|
||||
const props = defineProps<{
|
||||
formData: SystemConfigModel
|
||||
}>()
|
||||
|
||||
const configForm = ref<VForm>()
|
||||
|
||||
const ruleValidate = computed(() => ({
|
||||
name: [
|
||||
{ required: true, message: '请输入配置项名称', trigger: 'blur' },
|
||||
{ validator: (rule: object, value: string, callback: Function) => {
|
||||
http.get<any, any>('/api/v1/system/config/exists', {params: {name: value, id: props.formData._id}}).then(data => {
|
||||
if(data.data.exists) {
|
||||
callback(new Error('配置项名称已存在'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
})
|
||||
}, trigger: 'blur'
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
value: [
|
||||
{ required: true, message: '请输入配置项值', trigger: 'blur' },
|
||||
{ validator: (rule: object, value: string, callback: Function) => {
|
||||
try {
|
||||
JSON.parse(value)
|
||||
callback()
|
||||
} catch (e) {
|
||||
callback(new Error('值不符合JSON字符串格式'))
|
||||
}
|
||||
}, trigger: 'blur'
|
||||
}
|
||||
],
|
||||
}))
|
||||
|
||||
defineExpose({ configForm })
|
||||
</script>
|
||||
@ -40,8 +40,8 @@
|
||||
</div>
|
||||
<div class="page-container">
|
||||
<el-pagination background
|
||||
:page-sizes="$store.state.pageSizeOpts"
|
||||
:layout="$store.state.pageLayout"
|
||||
:page-sizes="store.state.pageSizeOpts"
|
||||
:layout="store.state.pageLayout"
|
||||
:current-page="search.pageNum"
|
||||
:total="total"
|
||||
@size-change="pageSizeChange"
|
||||
@ -91,122 +91,17 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Options } from 'vue-class-component'
|
||||
import BaseList from '@/model/baselist'
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, nextTick } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { useBaseList } from '@/model/baselist'
|
||||
import { Page } from '@/model/common.dto'
|
||||
import http from '@/utils/http'
|
||||
import { SystemRoleModel } from '@/model/system/system-role'
|
||||
import { ElButton, ElForm, ElFormItem, ElInput, ElTable, ElTableColumn, ElTag, ElPagination, ElDialog, ElSelect, ElOption, ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { VForm } from '@/types'
|
||||
|
||||
@Options({
|
||||
name: 'SystemRole',
|
||||
components: { ElButton, ElForm, ElFormItem, ElInput, ElTable, ElTableColumn, ElTag, ElPagination, ElDialog, ElSelect, ElOption }
|
||||
})
|
||||
export default class SystemRole extends BaseList<SystemRolePage> {
|
||||
ruleValidate = {
|
||||
name: [
|
||||
{ required: true, message: '请输入角色名称', trigger: 'blur' }
|
||||
],
|
||||
}
|
||||
systemRoleData: SystemRoleModel[] = []
|
||||
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}, any>('/api/v1/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
|
||||
this.clearValidate()
|
||||
}
|
||||
addUri(fieldName: 'include_uri' | 'exclude_uri', uri: string | null) {
|
||||
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
|
||||
this.clearValidate()
|
||||
}
|
||||
remove(row: SystemRoleModel) {
|
||||
ElMessageBox.confirm(`是否确认删除 ${row.name} 角色?`, '确认删除', {type: 'warning'}).then(async () => {
|
||||
const data = await this.$http.delete<{params: {id: string}}, any>('/api/v1/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.$refs.roleForm as VForm).validate(async (valid: boolean) => {
|
||||
if (!valid) return
|
||||
this.modalLoading = true
|
||||
const data = await this.$http.post<SystemRoleModel, any>('/api/v1/system/role/save', this.formData)
|
||||
this.modalLoading = false
|
||||
this.addModal = false
|
||||
ElMessage.success(data.message)
|
||||
this.loadData()
|
||||
})
|
||||
}
|
||||
clearValidate() {
|
||||
this.$nextTick(() => {
|
||||
(this.$refs.roleForm as VForm).clearValidate()
|
||||
})
|
||||
}
|
||||
}
|
||||
const store = useStore()
|
||||
|
||||
class SystemRolePage extends Page {
|
||||
name?: string
|
||||
@ -215,4 +110,118 @@ class SystemRolePage extends Page {
|
||||
this.name = undefined
|
||||
}
|
||||
}
|
||||
|
||||
const { loading, modalLoading, total, search, setLoadData, loadDataBase, reset, pageChange, pageSizeChange, datetimeFormat } = useBaseList(new SystemRolePage())
|
||||
|
||||
const ruleValidate = {
|
||||
name: [
|
||||
{ required: true, message: '请输入角色名称', trigger: 'blur' }
|
||||
],
|
||||
}
|
||||
const systemRoleData = ref<SystemRoleModel[]>([])
|
||||
const addModal = ref(false)
|
||||
const modalTitle = ref<string | null>(null)
|
||||
const uri = reactive<{
|
||||
include: string | null,
|
||||
exclude: string | null
|
||||
}>({
|
||||
include: null,
|
||||
exclude: null
|
||||
})
|
||||
const formData = reactive<SystemRoleModel>({
|
||||
_id: null,
|
||||
name: null,
|
||||
description: null,
|
||||
methods: [],
|
||||
include_uri: [],
|
||||
exclude_uri: []
|
||||
})
|
||||
const roleForm = ref<VForm>()
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
const data = await http.get<{params: SystemRolePage}, any>('/api/v1/system/role/list', {params: search})
|
||||
loading.value = false
|
||||
total.value = data.total
|
||||
systemRoleData.value = data.data
|
||||
}
|
||||
setLoadData(loadData)
|
||||
|
||||
function add() {
|
||||
uri.include = null
|
||||
uri.exclude = null
|
||||
Object.assign(formData, {
|
||||
_id: null,
|
||||
name: null,
|
||||
description: null,
|
||||
methods: [],
|
||||
include_uri: [],
|
||||
exclude_uri: []
|
||||
})
|
||||
modalTitle.value = '新增角色'
|
||||
addModal.value = true
|
||||
clearValidate()
|
||||
}
|
||||
|
||||
function addUri(fieldName: 'include_uri' | 'exclude_uri', uriValue: string | null) {
|
||||
if(!uriValue) return
|
||||
if(formData[fieldName].indexOf(uriValue) === -1) {
|
||||
formData[fieldName].push(uriValue)
|
||||
}
|
||||
}
|
||||
|
||||
function removeUri(fieldName: 'include_uri' | 'exclude_uri', uriValue: string) {
|
||||
let index = formData[fieldName].indexOf(uriValue)
|
||||
if(index !== -1) {
|
||||
formData[fieldName].splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
function update(row: SystemRoleModel) {
|
||||
uri.include = null
|
||||
uri.exclude = null
|
||||
Object.assign(formData, {
|
||||
_id: row._id,
|
||||
name: row.name,
|
||||
description: row.description,
|
||||
methods: row.methods,
|
||||
include_uri: row.include_uri,
|
||||
exclude_uri: row.exclude_uri
|
||||
})
|
||||
modalTitle.value = '修改角色'
|
||||
addModal.value = true
|
||||
clearValidate()
|
||||
}
|
||||
|
||||
function remove(row: SystemRoleModel) {
|
||||
ElMessageBox.confirm(`是否确认删除 ${row.name} 角色?`, '确认删除', {type: 'warning'}).then(async () => {
|
||||
const data = await http.delete<{params: {id: string}}, any>('/api/v1/system/role/delete', {params: {id: row._id}})
|
||||
if(data.status) {
|
||||
ElMessage.success(data.message)
|
||||
loadData()
|
||||
} else {
|
||||
ElMessage.warning(data.message)
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
async function save() {
|
||||
roleForm.value?.validate(async (valid: boolean) => {
|
||||
if (!valid) return
|
||||
modalLoading.value = true
|
||||
const data = await http.post<SystemRoleModel, any>('/api/v1/system/role/save', formData)
|
||||
modalLoading.value = false
|
||||
addModal.value = false
|
||||
ElMessage.success(data.message)
|
||||
loadData()
|
||||
})
|
||||
}
|
||||
|
||||
function clearValidate() {
|
||||
nextTick(() => {
|
||||
roleForm.value?.clearValidate()
|
||||
})
|
||||
}
|
||||
|
||||
loadData()
|
||||
</script>
|
||||
@ -36,8 +36,8 @@
|
||||
</div>
|
||||
<div class="page-container">
|
||||
<el-pagination background
|
||||
:page-sizes="$store.state.pageSizeOpts"
|
||||
:layout="$store.state.pageLayout"
|
||||
:page-sizes="store.state.pageSizeOpts"
|
||||
:layout="store.state.pageLayout"
|
||||
:current-page="search.pageNum"
|
||||
:total="total"
|
||||
@size-change="pageSizeChange"
|
||||
@ -70,115 +70,18 @@
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Options } from 'vue-class-component'
|
||||
import BaseList from '@/model/baselist'
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, nextTick } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { useBaseList } from '@/model/baselist'
|
||||
import { Page } from '@/model/common.dto'
|
||||
import http from '@/utils/http'
|
||||
import { SystemUserModel } from '@/model/system/system-user'
|
||||
import { SystemRoleModel } from '@/model/system/system-role'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { VForm } from '@/types'
|
||||
|
||||
@Options({
|
||||
name: 'SystemUser'
|
||||
})
|
||||
export default class SystemUser extends BaseList<SystemUserPage> {
|
||||
get ruleValidate() {
|
||||
return {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ validator: async (rule: object, value: string, callback: Function) => {
|
||||
const data = await this.$http.get<any, any>('/api/v1/system/user/exists', {params: {username: value, id: this.formData._id}})
|
||||
if(data.data.exists) {
|
||||
callback(new Error('用户名已存在'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}, trigger: 'blur'
|
||||
}
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 8, max: 16, message: '密码长度8~16位', trigger: 'blur' },
|
||||
{ pattern: /^(?![\d]+$)(?![a-zA-Z]+$)(?![-=+_.,]+$)[\da-zA-Z-=+_.,]{8,16}$/, message: '密码由字母、数字、特殊字符中的任意两种组成', trigger: 'blur' }
|
||||
],
|
||||
}
|
||||
}
|
||||
search = new SystemUserPage()
|
||||
systemUserData: SystemUserModel[] = []
|
||||
roles: SystemRoleModel[] = []
|
||||
addModal: boolean = false
|
||||
modalTitle: string | null = null
|
||||
formData: SystemUserModel = {
|
||||
_id: null,
|
||||
username: null,
|
||||
password: null,
|
||||
realname: null,
|
||||
role_ids: []
|
||||
}
|
||||
async loadData() {
|
||||
this.loading = true
|
||||
const data = await this.$http.get<{params: SystemUserPage}, any>('/api/v1/system/user/list', {params:this.search})
|
||||
this.loading = false
|
||||
this.total = data.total
|
||||
this.systemUserData = data.data
|
||||
}
|
||||
add() {
|
||||
// 清空表单
|
||||
this.formData = {
|
||||
_id: null,
|
||||
username: null,
|
||||
password: null,
|
||||
realname: null,
|
||||
role_ids: []
|
||||
}
|
||||
this.modalTitle = '新增用户'
|
||||
this.addModal = true
|
||||
this.clearValidate()
|
||||
}
|
||||
update(row: SystemUserModel) {
|
||||
this.formData._id = row._id
|
||||
this.formData.username = row.username
|
||||
this.formData.realname = row.realname
|
||||
this.formData.role_ids = row.role_ids
|
||||
this.modalTitle = '修改用户'
|
||||
this.addModal = true
|
||||
this.clearValidate()
|
||||
}
|
||||
async save() {
|
||||
(this.$refs.userForm as VForm).validate(async (valid: boolean) => {
|
||||
if (!valid) return
|
||||
this.modalLoading = true
|
||||
const data = await this.$http.post<SystemUserModel, any>('/api/v1/system/user/save', this.formData)
|
||||
this.modalLoading = false
|
||||
this.addModal = false
|
||||
ElMessage.success(data.message)
|
||||
this.loadData()
|
||||
})
|
||||
}
|
||||
remove(row: SystemUserModel) {
|
||||
ElMessageBox.confirm(`是否确认删除 ${row.username} 用户?`, '确认删除', {type: 'warning'}).then(async () => {
|
||||
const data = await this.$http.delete<{params: {id: string}}, any>('/api/v1/system/user/delete', {params: {id: row._id}})
|
||||
if(data.status) {
|
||||
ElMessage.success(data.message)
|
||||
this.loadData()
|
||||
} else {
|
||||
ElMessage.warning(data.message)
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
created() {
|
||||
this.loadData()
|
||||
this.$http.get<never, any>('/api/v1/system/role/listAll').then(data => {
|
||||
this.roles = data
|
||||
})
|
||||
}
|
||||
clearValidate() {
|
||||
this.$nextTick(() => {
|
||||
(this.$refs.userForm as VForm).clearValidate()
|
||||
})
|
||||
}
|
||||
}
|
||||
const store = useStore()
|
||||
|
||||
class SystemUserPage extends Page {
|
||||
username?: string
|
||||
@ -187,4 +90,108 @@ class SystemUserPage extends Page {
|
||||
this.username = undefined
|
||||
}
|
||||
}
|
||||
|
||||
const { loading, modalLoading, total, search, setLoadData, loadDataBase, reset, pageChange, pageSizeChange, datetimeFormat } = useBaseList(new SystemUserPage())
|
||||
|
||||
const systemUserData = ref<SystemUserModel[]>([])
|
||||
const roles = ref<SystemRoleModel[]>([])
|
||||
const addModal = ref(false)
|
||||
const modalTitle = ref<string | null>(null)
|
||||
const formData = reactive<SystemUserModel>({
|
||||
_id: null,
|
||||
username: null,
|
||||
password: null,
|
||||
realname: null,
|
||||
role_ids: []
|
||||
})
|
||||
const userForm = ref<VForm>()
|
||||
|
||||
const ruleValidate = computed(() => ({
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ validator: async (rule: object, value: string, callback: Function) => {
|
||||
const data = await http.get<any, any>('/api/v1/system/user/exists', {params: {username: value, id: formData._id}})
|
||||
if(data.data.exists) {
|
||||
callback(new Error('用户名已存在'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}, trigger: 'blur'
|
||||
}
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 8, max: 16, message: '密码长度8~16位', trigger: 'blur' },
|
||||
{ pattern: /^(?![\d]+$)(?![a-zA-Z]+$)(?![-=+_.,]+$)[\da-zA-Z-=+_.,]{8,16}$/, message: '密码由字母、数字、特殊字符中的任意两种组成', trigger: 'blur' }
|
||||
],
|
||||
}))
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
const data = await http.get<{params: SystemUserPage}, any>('/api/v1/system/user/list', {params: search})
|
||||
loading.value = false
|
||||
total.value = data.total
|
||||
systemUserData.value = data.data
|
||||
}
|
||||
setLoadData(loadData)
|
||||
|
||||
function add() {
|
||||
Object.assign(formData, {
|
||||
_id: null,
|
||||
username: null,
|
||||
password: null,
|
||||
realname: null,
|
||||
role_ids: []
|
||||
})
|
||||
modalTitle.value = '新增用户'
|
||||
addModal.value = true
|
||||
clearValidate()
|
||||
}
|
||||
|
||||
function update(row: SystemUserModel) {
|
||||
Object.assign(formData, {
|
||||
_id: row._id,
|
||||
username: row.username,
|
||||
realname: row.realname,
|
||||
role_ids: row.role_ids
|
||||
})
|
||||
modalTitle.value = '修改用户'
|
||||
addModal.value = true
|
||||
clearValidate()
|
||||
}
|
||||
|
||||
async function save() {
|
||||
userForm.value?.validate(async (valid: boolean) => {
|
||||
if (!valid) return
|
||||
modalLoading.value = true
|
||||
const data = await http.post<SystemUserModel, any>('/api/v1/system/user/save', formData)
|
||||
modalLoading.value = false
|
||||
addModal.value = false
|
||||
ElMessage.success(data.message)
|
||||
loadData()
|
||||
})
|
||||
}
|
||||
|
||||
function remove(row: SystemUserModel) {
|
||||
ElMessageBox.confirm(`是否确认删除 ${row.username} 用户?`, '确认删除', {type: 'warning'}).then(async () => {
|
||||
const data = await http.delete<{params: {id: string}}, any>('/api/v1/system/user/delete', {params: {id: row._id}})
|
||||
if(data.status) {
|
||||
ElMessage.success(data.message)
|
||||
loadData()
|
||||
} else {
|
||||
ElMessage.warning(data.message)
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
function clearValidate() {
|
||||
nextTick(() => {
|
||||
userForm.value?.clearValidate()
|
||||
})
|
||||
}
|
||||
|
||||
loadData()
|
||||
http.get<never, any>('/api/v1/system/role/listAll').then(data => {
|
||||
roles.value = data
|
||||
})
|
||||
</script>
|
||||
Loading…
x
Reference in New Issue
Block a user