依赖升级:
- element-plus 2.2.28 -> 2.13.5
- vue 3.2.45 -> 3.5.30
- vue-router 4.0.11 -> 4.6.4
- axios 0.22 -> 1.13.6
- echarts 5.2.1 -> 5.6.0
- hls.js 1.3 -> 1.6.15
- typescript 4.5.5 -> 5.9.3
破坏性变更修复:
- axios 1.x: config.headers.token= 改为 config.headers.set()
- element-plus locale 路径: lib/ -> es/
- Article.vue: Node 改为具名导入 { Node }
- main.ts: 路由守卫参数补全显式类型声明
- SystemConfig.vue: 移除废弃的 clearValidate 包装函数
TypeScript 配置修复 (tsconfig.json):
- moduleResolution: node -> bundler (支持 exports 字段, 解决 vue-router .mts 类型文件)
- 新增 skipLibCheck: true (规避第三方库内部类型冲突)
- 新增 vuex paths 映射 (vuex exports 字段缺少 types 条件)
样式适配 (common.less):
- el-dialog 新增 padding: 0, 消除升级后产生的白边
- el-form--inline 内 el-select 补 min-width: 160px, 修复搜索栏下拉框过窄问题
清理:
- 删除 src/vuex.d.ts (已无用的 vuex store 类型扩充文件)
254 lines
8.5 KiB
Vue
254 lines
8.5 KiB
Vue
<template>
|
||
<div>
|
||
<div class="search-panel">
|
||
<el-form inline :model="search">
|
||
<el-form-item label="标题">
|
||
<el-input v-model="search.title" />
|
||
</el-form-item>
|
||
<el-form-item label="创建时间">
|
||
<el-date-picker
|
||
v-model="search.createDate"
|
||
type="daterange"
|
||
range-separator="至"
|
||
start-placeholder="开始日期"
|
||
end-placeholder="结束日期" />
|
||
</el-form-item>
|
||
<el-form-item label="分类">
|
||
<el-select 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 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 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" style="vertical-align: bottom">分词处理</el-button>
|
||
<el-button @click="pullArticles" style="vertical-align: bottom">拉取文章</el-button>
|
||
<el-upload
|
||
action="/api/system/deployBlog"
|
||
accept="application/zip"
|
||
name="blogZip"
|
||
:headers="{token: store.state.loginInfo.token}"
|
||
:before-upload="beforeUpload"
|
||
:on-success="uploadSuccess"
|
||
:on-error="uploadError"
|
||
auto-upload
|
||
:show-file-list="false"
|
||
style="display: inline-block;margin-left: 10px;">
|
||
<el-button type="primary" icon="Upload" :loading="isUploading">发布博客</el-button>
|
||
</el-upload>
|
||
<div class="search-btn">
|
||
<el-button type="primary" @click="loadDataBase(true)" icon="Search">搜索</el-button>
|
||
<el-button @click="reset" icon="RefreshLeft">重置</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">
|
||
<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="标题" />
|
||
<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">
|
||
<div style="width: 18px">
|
||
<Check v-if="scope.row.is_splited" />
|
||
<Close v-else/>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<div class="page-container">
|
||
<el-pagination background
|
||
: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
|
||
v-model="markdownPreview.show"
|
||
:title="markdownPreview.title"
|
||
direction="rtl"
|
||
class="custom-drawer">
|
||
<div v-html="markdownPreview.content"></div>
|
||
</el-drawer>
|
||
</div>
|
||
</template>
|
||
<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 { useBaseList } from '@/model/baselist'
|
||
import { Page, MsgResult } from '@/model/common.dto'
|
||
import http from '@/utils/http'
|
||
|
||
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
|
||
}
|
||
}
|
||
|
||
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>
|