博客文章
This commit is contained in:
parent
22932ccaf9
commit
4a64f8f492
@ -9,8 +9,10 @@
|
||||
"dependencies": {
|
||||
"axios": "^0.22.0",
|
||||
"element-plus": "^1.1.0-beta.19",
|
||||
"hyperdown": "^2.4.29",
|
||||
"moment": "^2.29.1",
|
||||
"pretty-bytes": "^5.6.0",
|
||||
"prismjs": "^1.25.0",
|
||||
"unplugin-element-plus": "^0.1.0",
|
||||
"vue": "^3.2.16",
|
||||
"vue-axios": "^3.3.7",
|
||||
@ -19,6 +21,7 @@
|
||||
"vuex": "^4.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/prismjs": "^1.16.6",
|
||||
"@vitejs/plugin-vue": "^1.9.2",
|
||||
"less": "^4.1.1",
|
||||
"typescript": "^4.4.3",
|
||||
|
||||
@ -11,20 +11,12 @@ export interface ArticleModel {
|
||||
is_splited: boolean // 是否分词
|
||||
}
|
||||
|
||||
export interface TreeNodeBase {
|
||||
name: string | null // 节点名称(ID)
|
||||
deep: number // 节点深度(根节点为0)
|
||||
}
|
||||
|
||||
export interface TreeNode extends TreeNodeBase {
|
||||
expand: boolean // 是否展开
|
||||
export interface TreeNodeData {
|
||||
id?: string // 文章ID
|
||||
name: string | null // 节点名称(ID)
|
||||
title: string //显示的文字
|
||||
children?: TreeNode[] // 子节点
|
||||
loading?: boolean // 是否显示加载中
|
||||
nodeKey?: number // 树节点唯一标识
|
||||
isDirectory: boolean // 该节点是否为目录
|
||||
// render?: (h: CreateElement, {data}: {data: TreeNode}) => Array<VNode | string>
|
||||
isLeaf: boolean // 该节点是否叶子节点
|
||||
}
|
||||
|
||||
export interface TreeNodeSource {
|
||||
|
||||
@ -6,6 +6,7 @@ import Welcome from './views/Welcome.vue'
|
||||
import SystemUser from './views/system/SystemUser.vue'
|
||||
import SystemRole from './views/system/SystemRole.vue'
|
||||
import SystemConfig from './views/system/SystemConfig.vue'
|
||||
import Article from './views/system/Article.vue'
|
||||
|
||||
import Music from './views/api/Music.vue'
|
||||
import Hitokoto from './views/api/Hitokoto.vue'
|
||||
@ -18,6 +19,7 @@ const routes: Array<RouteRecordRaw> = [
|
||||
{ path: '/system/user', name: 'SystemUser', component: SystemUser },
|
||||
{ path: '/system/role', name: 'SystemRole', component: SystemRole },
|
||||
{ path: '/system/config', name: 'SystemConfig', component: SystemConfig },
|
||||
{ path: '/system/article', name: 'Article', component: Article },
|
||||
|
||||
{ path: '/api/music', name: 'Music', component: Music },
|
||||
{ path: '/api/hitokoto', name: 'Hitokoto', component: Hitokoto },
|
||||
|
||||
@ -64,4 +64,12 @@ html,body,#app,.layout {
|
||||
.main-view {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
.custom-drawer {
|
||||
> .el-drawer__header {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
> .el-drawer__body {
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
@ -1,16 +1,16 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form inline :model="search" @submit.prevent>
|
||||
<el-form-item label="内容:">
|
||||
<el-form-item label="内容">
|
||||
<el-input size="small" v-model="search.content" />
|
||||
</el-form-item>
|
||||
<el-form-item label="类型:">
|
||||
<el-form-item label="类型">
|
||||
<el-select size="small" v-model="search.type" multiple collapse-tags>
|
||||
<el-option v-for="item in typeList" :key="item.value" :value="item.value" :label="item.label" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="创建时间:">
|
||||
<el-date-picker
|
||||
<el-form-item label="创建时间">
|
||||
<el-date-picker size="small"
|
||||
v-model="search.createdAt"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
|
||||
@ -1,26 +1,26 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form inline :model="search" @submit.prevent>
|
||||
<el-form-item label="名称:">
|
||||
<el-form-item label="名称">
|
||||
<el-input size="small" v-model="search.name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="所属歌单:">
|
||||
<el-form-item label="所属歌单">
|
||||
<el-select size="small" v-model="search.lib_id" multiple :max-tag-count="3">
|
||||
<el-option v-for="musicLib in musicLibs" :key="musicLib._id" :value="musicLib._id" :label="musicLib.name" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="文件类型:">
|
||||
<el-form-item label="文件类型">
|
||||
<el-select size="small" v-model="search.ext" multiple :max-tag-count="3">
|
||||
<el-option v-for="ext in exts" :key="ext" :value="ext" :label="ext" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="标题:">
|
||||
<el-form-item label="标题">
|
||||
<el-input size="small" v-model="search.title" />
|
||||
</el-form-item>
|
||||
<el-form-item label="唱片集:">
|
||||
<el-form-item label="唱片集">
|
||||
<el-input size="small" v-model="search.album" />
|
||||
</el-form-item>
|
||||
<el-form-item label="艺术家:">
|
||||
<el-form-item label="艺术家">
|
||||
<el-input size="small" v-model="search.artist" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
291
src/views/system/Article.vue
Normal file
291
src/views/system/Article.vue
Normal file
@ -0,0 +1,291 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="search-panel">
|
||||
<el-form inline :model="search" @submit.prevent>
|
||||
<el-form-item label="标题">
|
||||
<el-input size="small" v-model="search.title" />
|
||||
</el-form-item>
|
||||
<el-form-item label="创建时间">
|
||||
<el-date-picker size="small"
|
||||
v-model="search.createDate"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期" />
|
||||
</el-form-item>
|
||||
<el-form-item label="分类">
|
||||
<el-select size="small" v-model="search.category" filterable clearable>
|
||||
<el-option v-for="item in categories" :key="item" :value="item" :label="item" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="标签">
|
||||
<el-select size="small" v-model="search.tag" filterable clearable>
|
||||
<el-option v-for="item in tags" :key="item" :value="item" :label="item" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="已分词">
|
||||
<el-select size="small" v-model="search.isSplited" >
|
||||
<el-option :value="true" label="是" />
|
||||
<el-option :value="false" label="否" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="btn-container">
|
||||
<el-button type="primary" @click="splitWord" size="small" >分词处理</el-button>
|
||||
<el-button @click="pullArticles" size="small" >拉取文章</el-button>
|
||||
<!-- <Upload :action="($http.defaults.baseURL || '') + '/api/system/deployBlog'"
|
||||
name="blogZip" :show-upload-list="false"
|
||||
:format="allowUploadExt" :headers="uploadHeaders" :max-size="102400"
|
||||
:before-upload="beforeUpload" :on-success="uploadSuccess" @on-error="uploadError"
|
||||
:on-format-error="uploadFormatError" :on-exceeded-size="uploadFileSizeError"
|
||||
style="display: inline-block;">
|
||||
<Button type="primary" icon="ios-cloud-upload-outline">发布博客</Button>
|
||||
</Upload> -->
|
||||
<div class="search-btn">
|
||||
<el-button type="primary" @click="loadDataBase(true)" size="small" icon="el-icon-search">搜索</el-button>
|
||||
<el-button @click="reset" size="small" icon="el-icon-refresh-right">重置</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-row>
|
||||
<el-col :span="4" style="height: 520px;overflow: auto;">
|
||||
<el-tree :props="treeProps" :load="loadTreeData" lazy highlight-current @node-click="articlePreview" />
|
||||
</el-col>
|
||||
<el-col :span="20">
|
||||
<el-table :data="articleData" v-loading="loading" stripe height="520" @selection-change="dataSelect">
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="title" label="标题" />
|
||||
<el-table-column prop="path" label="路径" >
|
||||
<template #default="scope">
|
||||
{{ scope.row.path.join('/') }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="categories" label="分类" width="150" >
|
||||
<template #default="scope">
|
||||
{{ typeof scope.row.categories === 'string' ? scope.row.categories : scope.row.categories.join(',') }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="tags" label="标签" width="180" >
|
||||
<template #default="scope">
|
||||
{{ typeof scope.row.tags === 'string' ? scope.row.tags : scope.row.tags.join(',') }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="content_len" label="正文长度" width="100" />
|
||||
<el-table-column prop="create_date" label="创建时间" width="180" >
|
||||
<template #default="scope">
|
||||
{{ datetimeFormat(scope.row.create_date) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="tags" label="是否已分词" width="120" >
|
||||
<template #default="scope">
|
||||
<i :class="scope.row.is_splited ? 'el-icon-check' : 'el-icon-close'" ></i>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-col>
|
||||
</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">
|
||||
<el-pagination background
|
||||
:page-sizes="$store.state.pageSizeOpts"
|
||||
:layout="$store.state.pageLayout"
|
||||
:total="total"
|
||||
@size-change="pageSizeChange"
|
||||
@current-change="pageChange">
|
||||
</el-pagination>
|
||||
</div>
|
||||
<el-drawer
|
||||
v-model="markdownPreview.show"
|
||||
:title="markdownPreview.title"
|
||||
direction="rtl"
|
||||
custom-class="custom-drawer">
|
||||
<div v-html="markdownPreview.content"></div>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
// import moment from 'moment'
|
||||
import hyperdown from 'hyperdown'
|
||||
import prismjs from 'prismjs'
|
||||
import 'prismjs/themes/prism.css'
|
||||
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 Node from 'element-plus/lib/components/tree/src/model/node'
|
||||
import { Options } from 'vue-class-component'
|
||||
import BaseList from '../../model/baselist'
|
||||
import { Page } from '../../model/common.dto'
|
||||
import { AxiosResponse } from 'axios'
|
||||
|
||||
let selectedData: string[] = []
|
||||
let closeUploadTip: Function | void | null
|
||||
|
||||
@Options({
|
||||
name: 'Article',
|
||||
components: { ElButton, ElForm, ElFormItem, ElInput, ElTable, ElTableColumn, ElPagination, ElSelect, ElOption, ElDatePicker, ElRow, ElCol, ElTree, ElDrawer }
|
||||
})
|
||||
export default class Article extends BaseList<ArticlePage> {
|
||||
search = new ArticlePage()
|
||||
allowUploadExt = ['zip']
|
||||
uploadHeaders = {token: localStorage.getItem('login_token')}
|
||||
articleData: ArticleModel[] = []
|
||||
tags: string[] = [] // 所有标签
|
||||
categories: string[] = [] // 所有分类
|
||||
markdownPreview :{ // markdown内容预览
|
||||
show?: boolean,
|
||||
title?: string | null,
|
||||
content?: string | null
|
||||
} = {
|
||||
show: false,
|
||||
title: null,
|
||||
content: null
|
||||
}
|
||||
|
||||
async loadData() {
|
||||
this.loading = true
|
||||
const { data } = await this.$http.get<ArticlePage, AxiosResponse<any>>('/api/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[]}, AxiosResponse<any>>('/api/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.get<never, AxiosResponse<any>>('/api/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) {
|
||||
// let filenameCut = undefined
|
||||
// if(file.name.length > 15) {
|
||||
// filenameCut = file.name.substr(0, 15) + '...'
|
||||
// }
|
||||
// closeUploadTip = ElMessage.loading({
|
||||
// content: (filenameCut || file.name) + ' 正在上传,请稍候...',
|
||||
// duration: 0
|
||||
// })
|
||||
// return true
|
||||
// }
|
||||
// uploadFormatError() {
|
||||
// this.closeUploadTip()
|
||||
// ElMessage.error(`只能上传 ${this.allowUploadExt.join('、')} 格式的文件`)
|
||||
// }
|
||||
// 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 = {
|
||||
label: 'name',
|
||||
children: 'children',
|
||||
isLeaf: 'isLeaf',
|
||||
}
|
||||
async loadTreeData(node: Node, resolve: Function) {
|
||||
const childItems: TreeNodeSource[] = (await this.$http.get('/api/article/tree', {params:{deep: node.level, parent: node.data.name}})).data
|
||||
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('/api/article/markdown', {params:{id: node.id}})).data
|
||||
this.markdownPreview.show = true
|
||||
const markdownHtml = new hyperdown().makeHtml(mdText)
|
||||
this.markdownPreview.content = markdownHtml.replace(/(?<=<pre><code[^>]*?>)[\s\S]*?(?=<\/code><\/pre>)/gi, v => {
|
||||
v = v.replace(/_&/g, ' ').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&')
|
||||
return prismjs.highlight(v, prismjs.languages.javascript, 'javascript')
|
||||
})
|
||||
this.markdownPreview.title = node.name
|
||||
console.log(this.markdownPreview)
|
||||
}
|
||||
created() {
|
||||
this.loadData()
|
||||
this.$http.get('/api/article/listCategories').then(({data}) => {
|
||||
this.categories = data
|
||||
})
|
||||
this.$http.get('/api/article/listTags').then(({data}) => {
|
||||
this.tags = data
|
||||
})
|
||||
// this.loadTreeData({deep:-1, name: null}, (treeNodes: TreeNode[]) => {
|
||||
// this.articleTree.push(...treeNodes)
|
||||
// })
|
||||
}
|
||||
}
|
||||
|
||||
class ArticlePage extends Page {
|
||||
title?: string
|
||||
createDate?: [Date, Date]
|
||||
category?: string
|
||||
tag?: string
|
||||
isSplited?: boolean
|
||||
reset() {
|
||||
super.reset()
|
||||
this.title = undefined
|
||||
this.createDate = undefined
|
||||
this.category = undefined
|
||||
this.tag = undefined
|
||||
this.isSplited = undefined
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form inline :model="search" @submit.prevent>
|
||||
<el-form-item label="配置项:">
|
||||
<el-form-item label="配置项">
|
||||
<el-input size="small" placeholder="名称/描述" v-model="search.name" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form inline :model="search" @submit.prevent>
|
||||
<el-form-item label="角色名称/描述:">
|
||||
<el-form-item label="角色名称/描述">
|
||||
<el-input size="small" v-model="search.name" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form inline :model="search" @submit.prevent>
|
||||
<el-form-item label="用户名/昵称:">
|
||||
<el-form-item label="用户名/昵称">
|
||||
<el-input size="small" v-model="search.username" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
15
yarn.lock
15
yarn.lock
@ -52,6 +52,11 @@
|
||||
estree-walker "^2.0.1"
|
||||
picomatch "^2.2.2"
|
||||
|
||||
"@types/prismjs@^1.16.6":
|
||||
version "1.16.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/prismjs/-/prismjs-1.16.6.tgz#377054f72f671b36dbe78c517ce2b279d83ecc40"
|
||||
integrity sha512-dTvnamRITNqNkqhlBd235kZl3KfVJQQoT5jkXeiWSBK7i4/TLKBNLV0S1wOt8gy4E2TY722KLtdmv2xc6+Wevg==
|
||||
|
||||
"@vitejs/plugin-vue@^1.9.2":
|
||||
version "1.9.2"
|
||||
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-1.9.2.tgz#7234efb8c3c3d60c7eac350a935074ab1820ae0e"
|
||||
@ -541,6 +546,11 @@ htmlparser2@^6.1.0:
|
||||
domutils "^2.5.2"
|
||||
entities "^2.0.0"
|
||||
|
||||
hyperdown@^2.4.29:
|
||||
version "2.4.29"
|
||||
resolved "https://registry.yarnpkg.com/hyperdown/-/hyperdown-2.4.29.tgz#a45bb662f59856f2d2c5796bb61f120b6ff808b8"
|
||||
integrity sha512-vwpa65JOmo6zBdvmNV3tM5IxNMbTRCXmCz4rajM9NHuiI9aAMw9tGzp8FBO8NT7ZnyWND0HEY6vKCVYl//U8kA==
|
||||
|
||||
iconv-lite@^0.4.4:
|
||||
version "0.4.24"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
|
||||
@ -736,6 +746,11 @@ pretty-bytes@^5.6.0:
|
||||
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
|
||||
integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==
|
||||
|
||||
prismjs@^1.25.0:
|
||||
version "1.25.0"
|
||||
resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.25.0.tgz#6f822df1bdad965734b310b315a23315cf999756"
|
||||
integrity sha512-WCjJHl1KEWbnkQom1+SzftbtXMKQoezOCYs5rECqMN+jP+apI7ftoflyqigqzopSO3hMhTEb0mFClA8lkolgEg==
|
||||
|
||||
promise@^7.0.1:
|
||||
version "7.3.1"
|
||||
resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user