灌糖包子 90a6153776
升级依赖 & 修复 TS 类型解析 & 适配 element-plus 样式变化
依赖升级:
- 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 类型扩充文件)
2026-03-20 00:33:09 +08:00

254 lines
8.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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