灌糖包子 4dd48489a5
refactor: 统一接口响应格式处理
- 修改 axios 响应拦截器,统一处理包装后的响应格式:
  - code === 0: 返回 data 中的实际数据
  - code === -1: ElMessage.error 提示 message 并 reject
  - 其他情况: 兼容原始响应格式

- 更新各页面数据取值方式:
  - 分页列表: data.data → data.list
  - SystemUser.vue, SystemRole.vue, Article.vue
  - Music.vue, Hitokoto.vue, PhotoWall.vue, SourceImage.vue
  - SystemConfigAdd.vue: data.data.exists → data.exists

- Home.vue: 简化 verifyToken 响应处理
- SystemConfig.vue: 简化 save 响应处理
- SystemRole.vue/SystemUser.vue: 简化 delete 响应处理

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 11:46:15 +08:00

246 lines
8.6 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 class="page-wrapper">
<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 style="flex: 1; overflow: hidden;">
<el-col :span="4" style="height: 100%; overflow: auto;">
<el-tree :props="treeProps" :load="loadTreeData" lazy highlight-current @node-click="articlePreview" />
</el-col>
<el-col :span="20" style="height: 100%; display: flex; flex-direction: column;">
<div class="table-container" style="margin-bottom: 0;">
<el-table :data="articleData" v-loading="loading" stripe @selection-change="dataSelect" height="100%">
<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="contentLen" label="正文长度" width="100" />
<el-table-column prop="createDate" label="创建时间" width="180" >
<template #default="scope">
{{ datetimeFormat(scope.row.createDate) }}
</template>
</el-table-column>
<el-table-column prop="tags" label="是否已分词" width="120" >
<template #default="scope">
<div style="width: 18px">
<Check v-if="scope.row.isSplited" />
<Close v-else/>
</div>
</template>
</el-table-column>
</el-table>
<div class="page-container" style="margin-top: 12px; padding: 0;">
<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/v2/article/list', {params: search})
selectedData = []
loading.value = false
total.value = data.total
articleData.value = data.list
}
setLoadData(loadData)
function splitWord() {
if (!selectedData.length) {
ElMessage.warning('请选择要执行分词的文章')
return
}
ElMessageBox.confirm(`是否确认对选中的${selectedData.length}篇文章执行分词处理?`, '操作确认', {type: 'info'}).then(async () => {
const successNum = await http.put<{_ids: string[]}, any>('/api/v2/article/splitWord', {_ids: selectedData})
ElMessage.success(`${successNum}篇文章分词处理成功`)
})
}
function pullArticles() {
ElMessageBox.confirm('确认拉取全部文章?', '操作确认', {type: 'info'}).then(async () => {
const { updateCount, createCount } = await http.put<never, any>('/api/v2/article/pull')
ElMessage.success(`拉取文章完成,更新 ${updateCount} 篇,创建 ${createCount}`)
loadData()
})
}
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/v2/article/tree', {params: {deep: node.level, parent: node.data.name}})
resolve(childItems.map((childItem): TreeNodeData => {
const treeNode: TreeNodeData = {
name: childItem._id,
title: childItem.articleId ? childItem._id : `${childItem._id}(${childItem.cnt})`,
id: childItem.articleId || childItem._id,
isLeaf: !!childItem.articleId
}
return treeNode
}))
}
async function articlePreview(node: TreeNodeData) {
if (!node.isLeaf) return
const mdText = await http.get<never, any>('/api/v2/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/v2/article/listCategories').then(data => {
categories.value = data
})
http.get<never, any>('/api/v2/article/listTags').then(data => {
tags.value = data
})
</script>