博客文章

This commit is contained in:
灌糖包子 2021-10-04 16:24:50 +08:00
parent 22932ccaf9
commit 4a64f8f492
11 changed files with 335 additions and 24 deletions

View File

@ -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",

View File

@ -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 {

View File

@ -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 },

View File

@ -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;
}
}

View File

@ -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="至"

View File

@ -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>

View 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(/&quot;/g, '"').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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"