统计分析
This commit is contained in:
parent
4a64f8f492
commit
c751f26122
@ -8,6 +8,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.22.0",
|
"axios": "^0.22.0",
|
||||||
|
"echarts": "^5.2.1",
|
||||||
"element-plus": "^1.1.0-beta.19",
|
"element-plus": "^1.1.0-beta.19",
|
||||||
"hyperdown": "^2.4.29",
|
"hyperdown": "^2.4.29",
|
||||||
"moment": "^2.29.1",
|
"moment": "^2.29.1",
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import SystemUser from './views/system/SystemUser.vue'
|
|||||||
import SystemRole from './views/system/SystemRole.vue'
|
import SystemRole from './views/system/SystemRole.vue'
|
||||||
import SystemConfig from './views/system/SystemConfig.vue'
|
import SystemConfig from './views/system/SystemConfig.vue'
|
||||||
import Article from './views/system/Article.vue'
|
import Article from './views/system/Article.vue'
|
||||||
|
import Statistics from './views/system/Statistics.vue'
|
||||||
|
|
||||||
import Music from './views/api/Music.vue'
|
import Music from './views/api/Music.vue'
|
||||||
import Hitokoto from './views/api/Hitokoto.vue'
|
import Hitokoto from './views/api/Hitokoto.vue'
|
||||||
@ -20,6 +21,7 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
{ path: '/system/role', name: 'SystemRole', component: SystemRole },
|
{ path: '/system/role', name: 'SystemRole', component: SystemRole },
|
||||||
{ path: '/system/config', name: 'SystemConfig', component: SystemConfig },
|
{ path: '/system/config', name: 'SystemConfig', component: SystemConfig },
|
||||||
{ path: '/system/article', name: 'Article', component: Article },
|
{ path: '/system/article', name: 'Article', component: Article },
|
||||||
|
{ path: '/system/statistics', name: 'Statistics', component: Statistics },
|
||||||
|
|
||||||
{ path: '/api/music', name: 'Music', component: Music },
|
{ path: '/api/music', name: 'Music', component: Music },
|
||||||
{ path: '/api/hitokoto', name: 'Hitokoto', component: Hitokoto },
|
{ path: '/api/hitokoto', name: 'Hitokoto', component: Hitokoto },
|
||||||
|
|||||||
8
src/types.ts
Normal file
8
src/types.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { Vue } from 'vue-class-component'
|
||||||
|
|
||||||
|
export type VForm = Vue & {
|
||||||
|
validate: (callback: (valid: boolean) => void) => void
|
||||||
|
resetValidation: () => boolean
|
||||||
|
reset: () => void
|
||||||
|
clearValidate: () => void
|
||||||
|
}
|
||||||
@ -19,16 +19,15 @@
|
|||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Options, Vue } from 'vue-class-component'
|
import { Options, Vue } from 'vue-class-component'
|
||||||
import { ref } from 'vue'
|
|
||||||
import { ElMessage, ElButton, ElForm, ElFormItem, ElInput, ElTooltip } from 'element-plus'
|
import { ElMessage, ElButton, ElForm, ElFormItem, ElInput, ElTooltip } from 'element-plus'
|
||||||
import { AxiosResponse } from 'axios'
|
import { AxiosResponse } from 'axios'
|
||||||
|
import { VForm } from "../types"
|
||||||
|
|
||||||
@Options({
|
@Options({
|
||||||
name: 'Login',
|
name: 'Login',
|
||||||
components: { ElButton, ElForm, ElFormItem, ElInput, ElTooltip }
|
components: { ElButton, ElForm, ElFormItem, ElInput, ElTooltip }
|
||||||
})
|
})
|
||||||
export default class Login extends Vue {
|
export default class Login extends Vue {
|
||||||
private readonly loginForm: any = ref('loginForm')
|
|
||||||
userInfo: UserInfo = {
|
userInfo: UserInfo = {
|
||||||
username: null,
|
username: null,
|
||||||
password: null
|
password: null
|
||||||
@ -45,7 +44,7 @@ export default class Login extends Vue {
|
|||||||
* 登录
|
* 登录
|
||||||
*/
|
*/
|
||||||
async login() {
|
async login() {
|
||||||
this.loginForm.validate(async (valid: boolean) => {
|
(this.$refs.loginForm as VForm).validate(async (valid: boolean) => {
|
||||||
if(!valid) return
|
if(!valid) return
|
||||||
const { data } = await this.$http.post<UserInfo, AxiosResponse<any>>('/api/common/login', this.userInfo)
|
const { data } = await this.$http.post<UserInfo, AxiosResponse<any>>('/api/common/login', this.userInfo)
|
||||||
if(data.token) {
|
if(data.token) {
|
||||||
|
|||||||
@ -67,13 +67,13 @@
|
|||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import HitokotoAdd from './HitokotoAdd.vue'
|
import HitokotoAdd from './HitokotoAdd.vue'
|
||||||
import { Options } from 'vue-class-component'
|
import { Options, Vue } from 'vue-class-component'
|
||||||
import BaseList from '../../model/baselist'
|
import BaseList from '../../model/baselist'
|
||||||
import { Page } from '../../model/common.dto'
|
import { Page } from '../../model/common.dto'
|
||||||
import HitokotoModel from '../../model/api/hitokoto'
|
import HitokotoModel from '../../model/api/hitokoto'
|
||||||
import { AxiosResponse } from 'axios'
|
import { AxiosResponse } from 'axios'
|
||||||
import { ElButton, ElForm, ElFormItem, ElInput, ElTable, ElTableColumn, ElPagination, ElDialog, ElSelect, ElOption, ElDatePicker, ElMessage, ElMessageBox } from 'element-plus'
|
import { ElButton, ElForm, ElFormItem, ElInput, ElTable, ElTableColumn, ElPagination, ElDialog, ElSelect, ElOption, ElDatePicker, ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { ref } from 'vue'
|
import { VForm } from '../../types'
|
||||||
|
|
||||||
let selectedData: string[] = []
|
let selectedData: string[] = []
|
||||||
@Options({
|
@Options({
|
||||||
@ -81,7 +81,6 @@ let selectedData: string[] = []
|
|||||||
components: { ElButton, ElForm, ElFormItem, ElInput, ElTable, ElTableColumn, ElPagination, ElDialog, ElSelect, ElOption, ElDatePicker, HitokotoAdd }
|
components: { ElButton, ElForm, ElFormItem, ElInput, ElTable, ElTableColumn, ElPagination, ElDialog, ElSelect, ElOption, ElDatePicker, HitokotoAdd }
|
||||||
})
|
})
|
||||||
export default class Hitokoto extends BaseList<HitokotoPage> {
|
export default class Hitokoto extends BaseList<HitokotoPage> {
|
||||||
private readonly addForm: any = ref('addForm')
|
|
||||||
search = new HitokotoPage()
|
search = new HitokotoPage()
|
||||||
typeList: {label: string, value: string}[] = []
|
typeList: {label: string, value: string}[] = []
|
||||||
hitokotoData: HitokotoModel[] = []
|
hitokotoData: HitokotoModel[] = []
|
||||||
@ -97,7 +96,7 @@ export default class Hitokoto extends BaseList<HitokotoPage> {
|
|||||||
this.hitokotoData = data.data
|
this.hitokotoData = data.data
|
||||||
}
|
}
|
||||||
async save() {
|
async save() {
|
||||||
this.addForm.$refs.hitokotoForm.validate(async (valid: boolean) => {
|
((this.$refs.addForm as Vue).$refs.hitokotoForm as VForm).validate(async (valid: boolean) => {
|
||||||
if(!valid) return
|
if(!valid) return
|
||||||
this.modalLoading = true
|
this.modalLoading = true
|
||||||
const { data } = await this.$http.post<any, AxiosResponse<any>>('/api/hitokoto/save', this.formData)
|
const { data } = await this.$http.post<any, AxiosResponse<any>>('/api/hitokoto/save', this.formData)
|
||||||
|
|||||||
@ -117,10 +117,9 @@ import BaseList from '../../model/baselist'
|
|||||||
import { Page } from '../../model/common.dto'
|
import { Page } from '../../model/common.dto'
|
||||||
import { AxiosResponse } from 'axios'
|
import { AxiosResponse } from 'axios'
|
||||||
import { ElButton, ElForm, ElFormItem, ElInput, ElTable, ElTableColumn, ElPagination, ElDialog, ElSelect, ElOption, ElRadioGroup, ElRadio, ElMessage, ElMessageBox } from 'element-plus'
|
import { ElButton, ElForm, ElFormItem, ElInput, ElTable, ElTableColumn, ElPagination, ElDialog, ElSelect, ElOption, ElRadioGroup, ElRadio, ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { ref } from 'vue'
|
|
||||||
|
|
||||||
import { MusicModel, MusicLibModel, MusicLyricModel, MusicPlayerItem } from '../../model/api/music'
|
import { MusicModel, MusicLibModel, MusicLyricModel, MusicPlayerItem } from '../../model/api/music'
|
||||||
import prettyBytes from 'pretty-bytes'
|
import prettyBytes from 'pretty-bytes'
|
||||||
|
import { VForm } from '../../types'
|
||||||
|
|
||||||
let selectedIds: string[] = []
|
let selectedIds: string[] = []
|
||||||
@Options({
|
@Options({
|
||||||
@ -128,8 +127,6 @@ let selectedIds: string[] = []
|
|||||||
components: { ElButton, ElForm, ElFormItem, ElInput, ElTable, ElTableColumn, ElPagination, ElDialog, ElSelect, ElOption, ElRadioGroup, ElRadio }
|
components: { ElButton, ElForm, ElFormItem, ElInput, ElTable, ElTableColumn, ElPagination, ElDialog, ElSelect, ElOption, ElRadioGroup, ElRadio }
|
||||||
})
|
})
|
||||||
export default class Music extends BaseList<MusicPage> {
|
export default class Music extends BaseList<MusicPage> {
|
||||||
// @Ref('player') private readonly player!: APlayerComponent
|
|
||||||
private readonly lyricForm: any = ref('lyricForm')
|
|
||||||
search = new MusicPage()
|
search = new MusicPage()
|
||||||
currentRow: MusicModel | null = null
|
currentRow: MusicModel | null = null
|
||||||
exts: string[] = []
|
exts: string[] = []
|
||||||
@ -226,7 +223,7 @@ export default class Music extends BaseList<MusicPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
async saveLyric() {
|
async saveLyric() {
|
||||||
this.lyricForm.validate(async (valid: boolean) => {
|
(this.$refs.lyricForm as VForm).validate(async (valid: boolean) => {
|
||||||
if(!valid) return
|
if(!valid) return
|
||||||
this.modalLoading = true
|
this.modalLoading = true
|
||||||
const { data } = await this.$http.post<MusicLyricModel, AxiosResponse<any>>(`/api/music/lyric/save?musicId=${this.currentRow ? this.currentRow._id : ''}`, this.lyricFormData)
|
const { data } = await this.$http.post<MusicLyricModel, AxiosResponse<any>>(`/api/music/lyric/save?musicId=${this.currentRow ? this.currentRow._id : ''}`, this.lyricFormData)
|
||||||
|
|||||||
@ -34,14 +34,19 @@
|
|||||||
<div class="btn-container">
|
<div class="btn-container">
|
||||||
<el-button type="primary" @click="splitWord" size="small" >分词处理</el-button>
|
<el-button type="primary" @click="splitWord" size="small" >分词处理</el-button>
|
||||||
<el-button @click="pullArticles" size="small" >拉取文章</el-button>
|
<el-button @click="pullArticles" size="small" >拉取文章</el-button>
|
||||||
<!-- <Upload :action="($http.defaults.baseURL || '') + '/api/system/deployBlog'"
|
<el-upload
|
||||||
name="blogZip" :show-upload-list="false"
|
action="/api/system/deployBlog"
|
||||||
:format="allowUploadExt" :headers="uploadHeaders" :max-size="102400"
|
accept=".zip"
|
||||||
:before-upload="beforeUpload" :on-success="uploadSuccess" @on-error="uploadError"
|
name="blogZip"
|
||||||
:on-format-error="uploadFormatError" :on-exceeded-size="uploadFileSizeError"
|
:headers="uploadHeaders"
|
||||||
style="display: inline-block;">
|
:before-upload="beforeUpload"
|
||||||
<Button type="primary" icon="ios-cloud-upload-outline">发布博客</Button>
|
:on-success="uploadSuccess"
|
||||||
</Upload> -->
|
:on-error="uploadError"
|
||||||
|
auto-upload
|
||||||
|
:show-file-list="false"
|
||||||
|
style="display: inline-block;margin-left: 10px;">
|
||||||
|
<el-button size="small" type="primary" icon="el-icon-upload" :loading="isUploading">发布博客</el-button>
|
||||||
|
</el-upload>
|
||||||
<div class="search-btn">
|
<div class="search-btn">
|
||||||
<el-button type="primary" @click="loadDataBase(true)" size="small" icon="el-icon-search">搜索</el-button>
|
<el-button type="primary" @click="loadDataBase(true)" size="small" icon="el-icon-search">搜索</el-button>
|
||||||
<el-button @click="reset" size="small" icon="el-icon-refresh-right">重置</el-button>
|
<el-button @click="reset" size="small" icon="el-icon-refresh-right">重置</el-button>
|
||||||
@ -62,12 +67,12 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="categories" label="分类" width="150" >
|
<el-table-column prop="categories" label="分类" width="150" >
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
{{ typeof scope.row.categories === 'string' ? scope.row.categories : scope.row.categories.join(',') }}
|
{{ typeof scope.row.categories === 'string' ? scope.row.categories : scope.row.categories?.join(',') }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="tags" label="标签" width="180" >
|
<el-table-column prop="tags" label="标签" width="180" >
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
{{ typeof scope.row.tags === 'string' ? scope.row.tags : scope.row.tags.join(',') }}
|
{{ typeof scope.row.tags === 'string' ? scope.row.tags : scope.row.tags?.join(',') }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="content_len" label="正文长度" width="100" />
|
<el-table-column prop="content_len" label="正文长度" width="100" />
|
||||||
@ -84,18 +89,6 @@
|
|||||||
</el-table>
|
</el-table>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
<!-- <Row :gutter="16">
|
|
||||||
<Col span="4" style="height:520px;overflow:auto;">
|
|
||||||
<Tree :data="articleTree" :load-data="loadTreeData" @on-select-change="articlePreview"></Tree>
|
|
||||||
</Col>
|
|
||||||
<Col span="20" >
|
|
||||||
<div class="table-container">
|
|
||||||
<Table border :loading="loading" :columns="articleColumns" :data="articleData" height="520" @on-selection-change="dataSelect"></Table>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
</Row> -->
|
|
||||||
|
|
||||||
|
|
||||||
<div class="page-container">
|
<div class="page-container">
|
||||||
<el-pagination background
|
<el-pagination background
|
||||||
:page-sizes="$store.state.pageSizeOpts"
|
:page-sizes="$store.state.pageSizeOpts"
|
||||||
@ -115,42 +108,38 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
// import moment from 'moment'
|
|
||||||
import hyperdown from 'hyperdown'
|
import hyperdown from 'hyperdown'
|
||||||
import prismjs from 'prismjs'
|
import prismjs from 'prismjs'
|
||||||
import 'prismjs/themes/prism.css'
|
import 'prismjs/themes/prism.css'
|
||||||
import { ArticleModel, TreeNodeData, TreeNodeSource } from '../../model/system/article'
|
import { ArticleModel, TreeNodeData, TreeNodeSource } from '../../model/system/article'
|
||||||
import { ElButton, ElForm, ElFormItem, ElInput, ElTable, ElTableColumn, ElPagination, ElSelect, ElOption, ElDatePicker, ElRow, ElCol, ElTree, ElDrawer, ElMessage, ElMessageBox } from 'element-plus'
|
import { ElButton, ElForm, ElFormItem, ElInput, ElTable, ElTableColumn, ElPagination, ElSelect, ElOption, ElDatePicker, ElRow, ElCol, ElTree, ElDrawer, ElUpload, ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import Node from 'element-plus/lib/components/tree/src/model/node'
|
import Node from 'element-plus/lib/components/tree/src/model/node'
|
||||||
import { Options } from 'vue-class-component'
|
import { Options } from 'vue-class-component'
|
||||||
import BaseList from '../../model/baselist'
|
import BaseList from '../../model/baselist'
|
||||||
import { Page } from '../../model/common.dto'
|
import { Page, MsgResult } from '../../model/common.dto'
|
||||||
import { AxiosResponse } from 'axios'
|
import { AxiosResponse } from 'axios'
|
||||||
|
|
||||||
let selectedData: string[] = []
|
let selectedData: string[] = []
|
||||||
let closeUploadTip: Function | void | null
|
|
||||||
|
|
||||||
@Options({
|
@Options({
|
||||||
name: 'Article',
|
name: 'Article',
|
||||||
components: { ElButton, ElForm, ElFormItem, ElInput, ElTable, ElTableColumn, ElPagination, ElSelect, ElOption, ElDatePicker, ElRow, ElCol, ElTree, ElDrawer }
|
components: { ElButton, ElForm, ElFormItem, ElInput, ElTable, ElTableColumn, ElPagination, ElSelect, ElOption, ElDatePicker, ElRow, ElCol, ElTree, ElDrawer, ElUpload }
|
||||||
})
|
})
|
||||||
export default class Article extends BaseList<ArticlePage> {
|
export default class Article extends BaseList<ArticlePage> {
|
||||||
search = new ArticlePage()
|
search = new ArticlePage()
|
||||||
allowUploadExt = ['zip']
|
|
||||||
uploadHeaders = {token: localStorage.getItem('login_token')}
|
uploadHeaders = {token: localStorage.getItem('login_token')}
|
||||||
articleData: ArticleModel[] = []
|
articleData: ArticleModel[] = []
|
||||||
tags: string[] = [] // 所有标签
|
tags: string[] = [] // 所有标签
|
||||||
categories: string[] = [] // 所有分类
|
categories: string[] = [] // 所有分类
|
||||||
markdownPreview :{ // markdown内容预览
|
markdownPreview :{ // markdown内容预览
|
||||||
show?: boolean,
|
show: boolean,
|
||||||
title?: string | null,
|
title: string | null,
|
||||||
content?: string | null
|
content: string | null
|
||||||
} = {
|
} = {
|
||||||
show: false,
|
show: false,
|
||||||
title: null,
|
title: null,
|
||||||
content: null
|
content: null
|
||||||
}
|
}
|
||||||
|
isUploading: boolean = false
|
||||||
async loadData() {
|
async loadData() {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
const { data } = await this.$http.get<ArticlePage, AxiosResponse<any>>('/api/article/list', {params:this.search})
|
const { data } = await this.$http.get<ArticlePage, AxiosResponse<any>>('/api/article/list', {params:this.search})
|
||||||
@ -187,43 +176,22 @@ export default class Article extends BaseList<ArticlePage> {
|
|||||||
dataSelect(selection: ArticleModel[]) {
|
dataSelect(selection: ArticleModel[]) {
|
||||||
selectedData = selection.map(item => item._id)
|
selectedData = selection.map(item => item._id)
|
||||||
}
|
}
|
||||||
// beforeUpload(file: File) {
|
beforeUpload(file: File) {
|
||||||
// let filenameCut = undefined
|
this.isUploading = true
|
||||||
// if(file.name.length > 15) {
|
return true
|
||||||
// filenameCut = file.name.substr(0, 15) + '...'
|
}
|
||||||
// }
|
uploadSuccess(response: MsgResult) {
|
||||||
// closeUploadTip = ElMessage.loading({
|
if(response.status) {
|
||||||
// content: (filenameCut || file.name) + ' 正在上传,请稍候...',
|
ElMessage.success(response.message)
|
||||||
// duration: 0
|
} else {
|
||||||
// })
|
ElMessage.warning(response.message)
|
||||||
// return true
|
}
|
||||||
// }
|
this.isUploading = false
|
||||||
// uploadFormatError() {
|
}
|
||||||
// this.closeUploadTip()
|
uploadError() {
|
||||||
// ElMessage.error(`只能上传 ${this.allowUploadExt.join('、')} 格式的文件`)
|
this.isUploading = false
|
||||||
// }
|
ElMessage.error('上传失败')
|
||||||
// uploadFileSizeError() {
|
}
|
||||||
// this.closeUploadTip()
|
|
||||||
// ElMessage.error(`只能上传不超过100MB的文件`)
|
|
||||||
// }
|
|
||||||
// uploadSuccess(response: MsgResult) {
|
|
||||||
// this.closeUploadTip()
|
|
||||||
// if(response.status) {
|
|
||||||
// ElMessage.success(response.message)
|
|
||||||
// } else {
|
|
||||||
// ElMessage.warning(response.message)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// uploadError() {
|
|
||||||
// this.closeUploadTip()
|
|
||||||
// ElMessage.error('上传失败')
|
|
||||||
// }
|
|
||||||
// closeUploadTip() {
|
|
||||||
// if(typeof closeUploadTip === 'function') {
|
|
||||||
// closeUploadTip.call(this)
|
|
||||||
// closeUploadTip = null
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
readonly treeProps = {
|
readonly treeProps = {
|
||||||
label: 'name',
|
label: 'name',
|
||||||
children: 'children',
|
children: 'children',
|
||||||
|
|||||||
210
src/views/system/Statistics.vue
Normal file
210
src/views/system/Statistics.vue
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="echarts-container">
|
||||||
|
<div ref="categoriesChart" v-loading="categoriesChartLoading"></div>
|
||||||
|
<div ref="publishDatesChart" v-loading="publishDatesChartLoading"></div>
|
||||||
|
<div class="timeline-chart" ref="timelineWordsChart" v-loading="timelineWordsChartLoading"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import * as echarts from 'echarts'
|
||||||
|
import { Options, Vue } from 'vue-class-component'
|
||||||
|
import { AxiosResponse } from 'axios'
|
||||||
|
|
||||||
|
@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 articleData = (await this.$http.get<{params:{type:string}}, AxiosResponse<any>>('/api/article/statistics', {params:{type:'normal'}})).data
|
||||||
|
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 timelineData = (await this.$http.get<{params:{type:string}}, AxiosResponse<any>>('/api/article/statistics', {params:{type:'timelineWords'}})).data
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
categoriesChartOption = {
|
||||||
|
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: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
publishDatesChartOption = {
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.echarts-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 50% 50%;
|
||||||
|
grid-template-rows: 50% 50%;
|
||||||
|
height: 100%;
|
||||||
|
> .echarts {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.timeline-chart {
|
||||||
|
grid-column-start: 1;
|
||||||
|
grid-column-end: 3;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -59,17 +59,16 @@
|
|||||||
import { Options, Vue } from 'vue-class-component'
|
import { Options, Vue } from 'vue-class-component'
|
||||||
import { AxiosResponse } from 'axios'
|
import { AxiosResponse } from 'axios'
|
||||||
import { ElButton, ElForm, ElFormItem, ElInput, ElTable, ElTableColumn, ElDialog, ElMessage, ElMessageBox } from 'element-plus'
|
import { ElButton, ElForm, ElFormItem, ElInput, ElTable, ElTableColumn, ElDialog, ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { ref } from 'vue'
|
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import SystemConfigAdd from './SystemConfigAdd.vue'
|
import SystemConfigAdd from './SystemConfigAdd.vue'
|
||||||
import { SystemConfigModel } from '../../model/system/system-config'
|
import { SystemConfigModel } from '../../model/system/system-config'
|
||||||
|
import { VForm } from '../../types'
|
||||||
|
|
||||||
@Options({
|
@Options({
|
||||||
name: 'SystemConfig',
|
name: 'SystemConfig',
|
||||||
components: { ElButton, ElForm, ElFormItem, ElInput, ElTable, ElTableColumn, ElDialog, SystemConfigAdd }
|
components: { ElButton, ElForm, ElFormItem, ElInput, ElTable, ElTableColumn, ElDialog, SystemConfigAdd }
|
||||||
})
|
})
|
||||||
export default class SystemConfig extends Vue {
|
export default class SystemConfig extends Vue {
|
||||||
private readonly addForm: any = ref('addForm')
|
|
||||||
modalLoading: boolean = false
|
modalLoading: boolean = false
|
||||||
loading: boolean = false
|
loading: boolean = false
|
||||||
search: {name?:string} = {}
|
search: {name?:string} = {}
|
||||||
@ -111,7 +110,7 @@ export default class SystemConfig extends Vue {
|
|||||||
this.addModal = true
|
this.addModal = true
|
||||||
}
|
}
|
||||||
async save() {
|
async save() {
|
||||||
this.addForm.$refs.configForm.validate(async (valid: boolean) => {
|
((this.$refs.addForm as Vue).$refs.configForm as VForm).validate(async (valid: boolean) => {
|
||||||
if(!valid) return
|
if(!valid) return
|
||||||
this.modalLoading = true
|
this.modalLoading = true
|
||||||
const { data } = await this.$http.post<SystemConfigModel, AxiosResponse<any>>('/api/system/config/save', this.formData)
|
const { data } = await this.$http.post<SystemConfigModel, AxiosResponse<any>>('/api/system/config/save', this.formData)
|
||||||
@ -137,7 +136,7 @@ export default class SystemConfig extends Vue {
|
|||||||
}
|
}
|
||||||
clearValidate() {
|
clearValidate() {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.addForm.$refs.configForm.clearValidate()
|
((this.$refs.addForm as Vue).$refs.configForm as VForm).clearValidate()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -97,14 +97,13 @@ import { Page } from '../../model/common.dto'
|
|||||||
import { SystemRoleModel } from '../../model/system/system-role'
|
import { SystemRoleModel } from '../../model/system/system-role'
|
||||||
import { AxiosResponse } from 'axios'
|
import { AxiosResponse } from 'axios'
|
||||||
import { ElButton, ElForm, ElFormItem, ElInput, ElTable, ElTableColumn, ElTag, ElPagination, ElDialog, ElSelect, ElOption, ElMessage, ElMessageBox } from 'element-plus'
|
import { ElButton, ElForm, ElFormItem, ElInput, ElTable, ElTableColumn, ElTag, ElPagination, ElDialog, ElSelect, ElOption, ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { ref } from 'vue'
|
import { VForm } from '../../types'
|
||||||
|
|
||||||
@Options({
|
@Options({
|
||||||
name: 'SystemRole',
|
name: 'SystemRole',
|
||||||
components: { ElButton, ElForm, ElFormItem, ElInput, ElTable, ElTableColumn, ElTag, ElPagination, ElDialog, ElSelect, ElOption }
|
components: { ElButton, ElForm, ElFormItem, ElInput, ElTable, ElTableColumn, ElTag, ElPagination, ElDialog, ElSelect, ElOption }
|
||||||
})
|
})
|
||||||
export default class SystemRole extends BaseList<SystemRolePage> {
|
export default class SystemRole extends BaseList<SystemRolePage> {
|
||||||
private readonly roleForm: any = ref('roleForm')
|
|
||||||
ruleValidate = {
|
ruleValidate = {
|
||||||
name: [
|
name: [
|
||||||
{ required: true, message: '请输入角色名称', trigger: 'blur' }
|
{ required: true, message: '请输入角色名称', trigger: 'blur' }
|
||||||
@ -192,7 +191,7 @@ export default class SystemRole extends BaseList<SystemRolePage> {
|
|||||||
}).catch(() => {})
|
}).catch(() => {})
|
||||||
}
|
}
|
||||||
async save() {
|
async save() {
|
||||||
this.roleForm.validate(async (valid: boolean) => {
|
(this.$refs.roleForm as VForm).validate(async (valid: boolean) => {
|
||||||
if(!valid) return
|
if(!valid) return
|
||||||
this.modalLoading = true
|
this.modalLoading = true
|
||||||
const { data } = await this.$http.post<SystemRoleModel, AxiosResponse<any>>('/api/system/role/save', this.formData)
|
const { data } = await this.$http.post<SystemRoleModel, AxiosResponse<any>>('/api/system/role/save', this.formData)
|
||||||
@ -204,7 +203,7 @@ export default class SystemRole extends BaseList<SystemRolePage> {
|
|||||||
}
|
}
|
||||||
clearValidate() {
|
clearValidate() {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.roleForm.clearValidate()
|
(this.$refs.roleForm as VForm).clearValidate()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -77,14 +77,13 @@ import { SystemUserModel } from '../../model/system/system-user'
|
|||||||
import { SystemRoleModel } from '../../model/system/system-role'
|
import { SystemRoleModel } from '../../model/system/system-role'
|
||||||
import { AxiosResponse } from 'axios'
|
import { AxiosResponse } from 'axios'
|
||||||
import { ElButton, ElForm, ElFormItem, ElInput, ElTable, ElTableColumn, ElPagination, ElDialog, ElSelect, ElOption, ElMessage, ElMessageBox } from 'element-plus'
|
import { ElButton, ElForm, ElFormItem, ElInput, ElTable, ElTableColumn, ElPagination, ElDialog, ElSelect, ElOption, ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { ref } from 'vue'
|
import { VForm } from '../../types'
|
||||||
|
|
||||||
@Options({
|
@Options({
|
||||||
name: 'SystemUser',
|
name: 'SystemUser',
|
||||||
components: { ElButton, ElForm, ElFormItem, ElInput, ElTable, ElTableColumn, ElPagination, ElDialog, ElSelect, ElOption }
|
components: { ElButton, ElForm, ElFormItem, ElInput, ElTable, ElTableColumn, ElPagination, ElDialog, ElSelect, ElOption }
|
||||||
})
|
})
|
||||||
export default class SystemUser extends BaseList<SystemUserPage> {
|
export default class SystemUser extends BaseList<SystemUserPage> {
|
||||||
private readonly userForm: any = ref('userForm')
|
|
||||||
get ruleValidate() {
|
get ruleValidate() {
|
||||||
return {
|
return {
|
||||||
username: [
|
username: [
|
||||||
@ -148,8 +147,7 @@ export default class SystemUser extends BaseList<SystemUserPage> {
|
|||||||
this.clearValidate()
|
this.clearValidate()
|
||||||
}
|
}
|
||||||
async save() {
|
async save() {
|
||||||
console.log(this.userForm)
|
(this.$refs.userForm as VForm).validate(async (valid: boolean) => {
|
||||||
this.userForm.validate(async (valid: boolean) => {
|
|
||||||
if(!valid) return
|
if(!valid) return
|
||||||
this.modalLoading = true
|
this.modalLoading = true
|
||||||
const { data } = await this.$http.post<SystemUserModel, AxiosResponse<any>>('/api/system/user/save', this.formData)
|
const { data } = await this.$http.post<SystemUserModel, AxiosResponse<any>>('/api/system/user/save', this.formData)
|
||||||
@ -178,7 +176,7 @@ export default class SystemUser extends BaseList<SystemUserPage> {
|
|||||||
}
|
}
|
||||||
clearValidate() {
|
clearValidate() {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.userForm.clearValidate()
|
(this.$refs.userForm as VForm).clearValidate()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,14 +23,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Options, Vue } from 'vue-class-component'
|
import { Options, Vue } from 'vue-class-component'
|
||||||
import { ElForm, ElFormItem, ElInput, ElButtonGroup, ElButton, ElMessage } from 'element-plus'
|
import { ElForm, ElFormItem, ElInput, ElButtonGroup, ElButton, ElMessage } from 'element-plus'
|
||||||
import { ref } from 'vue'
|
|
||||||
|
|
||||||
@Options({
|
@Options({
|
||||||
name: 'SqlReplace',
|
name: 'SqlReplace',
|
||||||
components: { ElForm, ElFormItem, ElInput, ElButtonGroup, ElButton }
|
components: { ElForm, ElFormItem, ElInput, ElButtonGroup, ElButton }
|
||||||
})
|
})
|
||||||
export default class SqlReplace extends Vue {
|
export default class SqlReplace extends Vue {
|
||||||
private readonly inputInstance: any = ref('resultInput')
|
|
||||||
sql: string = ''
|
sql: string = ''
|
||||||
params: string = ''
|
params: string = ''
|
||||||
replaceResult: string = ''
|
replaceResult: string = ''
|
||||||
@ -57,7 +55,7 @@ export default class SqlReplace extends Vue {
|
|||||||
}
|
}
|
||||||
this.replaceResult = sql
|
this.replaceResult = sql
|
||||||
Promise.resolve('已复制到剪贴板').then(message => {
|
Promise.resolve('已复制到剪贴板').then(message => {
|
||||||
this.inputInstance.select()
|
(this.$refs.resultInput as HTMLInputElement).select()
|
||||||
if (document.execCommand('copy')) {
|
if (document.execCommand('copy')) {
|
||||||
ElMessage.success(message)
|
ElMessage.success(message)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -27,6 +27,8 @@ export default defineConfig({
|
|||||||
manualChunks(id) {
|
manualChunks(id) {
|
||||||
if (id.includes('element-plus')) {
|
if (id.includes('element-plus')) {
|
||||||
return 'element-plus'
|
return 'element-plus'
|
||||||
|
} else if(id.includes('echarts')) {
|
||||||
|
return 'echarts'
|
||||||
} else if(id.includes('node_modules')) {
|
} else if(id.includes('node_modules')) {
|
||||||
return 'vendor'
|
return 'vendor'
|
||||||
}
|
}
|
||||||
|
|||||||
20
yarn.lock
20
yarn.lock
@ -342,6 +342,14 @@ domutils@^2.5.2:
|
|||||||
domelementtype "^2.2.0"
|
domelementtype "^2.2.0"
|
||||||
domhandler "^4.2.0"
|
domhandler "^4.2.0"
|
||||||
|
|
||||||
|
echarts@^5.2.1:
|
||||||
|
version "5.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/echarts/-/echarts-5.2.1.tgz#bd58ec011cd82def4a714e4038ef4b73b8417bc3"
|
||||||
|
integrity sha512-OJ79b22eqRfbSV8vYmDKmA+XWfNbr0Uk/OafWcFNIGDWti2Uw9A6eVCiJLmqPa9Sk+EWL+t5v26aak0z3gxiZw==
|
||||||
|
dependencies:
|
||||||
|
tslib "2.3.0"
|
||||||
|
zrender "5.2.1"
|
||||||
|
|
||||||
element-plus@^1.1.0-beta.19:
|
element-plus@^1.1.0-beta.19:
|
||||||
version "1.1.0-beta.19"
|
version "1.1.0-beta.19"
|
||||||
resolved "https://registry.yarnpkg.com/element-plus/-/element-plus-1.1.0-beta.19.tgz#234f3bc3f1d822a7102045e7ee1239b675f27acc"
|
resolved "https://registry.yarnpkg.com/element-plus/-/element-plus-1.1.0-beta.19.tgz#234f3bc3f1d822a7102045e7ee1239b675f27acc"
|
||||||
@ -938,6 +946,11 @@ token-stream@1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/token-stream/-/token-stream-1.0.0.tgz#cc200eab2613f4166d27ff9afc7ca56d49df6eb4"
|
resolved "https://registry.yarnpkg.com/token-stream/-/token-stream-1.0.0.tgz#cc200eab2613f4166d27ff9afc7ca56d49df6eb4"
|
||||||
integrity sha1-zCAOqyYT9BZtJ/+a/HylbUnfbrQ=
|
integrity sha1-zCAOqyYT9BZtJ/+a/HylbUnfbrQ=
|
||||||
|
|
||||||
|
tslib@2.3.0:
|
||||||
|
version "2.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e"
|
||||||
|
integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==
|
||||||
|
|
||||||
tslib@^1.10.0:
|
tslib@^1.10.0:
|
||||||
version "1.14.1"
|
version "1.14.1"
|
||||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
||||||
@ -1182,3 +1195,10 @@ yallist@^4.0.0:
|
|||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
|
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
|
||||||
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
|
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
|
||||||
|
|
||||||
|
zrender@5.2.1:
|
||||||
|
version "5.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/zrender/-/zrender-5.2.1.tgz#5f4bbda915ba6d412b0b19dc2431beaad05417bb"
|
||||||
|
integrity sha512-M3bPGZuyLTNBC6LiNKXJwSCtglMp8XUEqEBG+2MdICDI3d1s500Y4P0CzldQGsqpRVB7fkvf3BKQQRxsEaTlsw==
|
||||||
|
dependencies:
|
||||||
|
tslib "2.3.0"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user