536 lines
18 KiB
Vue
536 lines
18 KiB
Vue
<template>
|
||
<div>
|
||
<div class="gva-search-box">
|
||
<el-form ref="elSearchFormRef" :inline="true" :model="searchInfo" label-width="0" class="demo-form-inline"
|
||
@keyup.enter="handleSubmitSearch">
|
||
<el-form-item prop="createdAt" style="width:260px">
|
||
<el-tooltip content="搜索范围是创建开始日期(包含)至创建结束日期(包含)" placement="top-start">
|
||
<el-icon>
|
||
<QuestionFilled />
|
||
</el-icon>
|
||
</el-tooltip>
|
||
<el-date-picker v-model="searchInfo.dateRange" type="daterange" value-format="YYYY-MM-DD" :clearable="false"
|
||
:editable="false" />
|
||
</el-form-item>
|
||
<el-form-item style="width:200px">
|
||
<el-tooltip content="从标题、副标题、摘要中搜索" placement="top-start">
|
||
<el-input v-model="searchInfo.keyword" class="keyword" placeholder="请输入关键词" clearable style="width:100%" />
|
||
</el-tooltip>
|
||
</el-form-item>
|
||
<!-- <el-form-item label="所属栏目" prop="channelId" style="width:300px">
|
||
<el-cascader v-model="searchChannelId" :options="channelOptions" style="width:100%"
|
||
:props="{ label: 'title', value: 'ID', disabled: 'disabled', emitPath: false }" :show-all-levels="true"
|
||
clearable placeholder="请选择" filterable />
|
||
</el-form-item>
|
||
<el-form-item label="文章分类" prop="categoryId" style="width:300px">
|
||
<el-cascader v-model="searchCategoryId" :options="categoryOptions" style="width:100%"
|
||
:props="{ label: 'title', value: 'ID', disabled: 'disabled', emitPath: false }" :show-all-levels="true"
|
||
clearable placeholder="请选择" filterable />
|
||
</el-form-item> -->
|
||
<el-form-item label="" prop="status" style="width:200px">
|
||
<el-select v-model="searchInfo.status" placeholder="请选择状态" clearable style="width:300px">
|
||
<el-option v-for="item in statusOptions" :key="item.key" :label="item.label" :value="item.key" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" icon="search" @click="handleSubmitSearch">查询</el-button>
|
||
<el-button icon="refresh" @click="handleResetSearch">重置</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
<div class="gva-table-box">
|
||
<div class="gva-btn-list">
|
||
<el-button type="primary" icon="plus" @click="handleAdd('0')">新增文章</el-button>
|
||
<el-button v-auth="btnAuth.delete" icon="delete" style="margin-left: 10px;"
|
||
:disabled="!multipleSelection.length" @click="handleMultiDelete">删除</el-button>
|
||
</div>
|
||
<!-- 由于此处菜单跟左侧列表一一对应所以不需要分页 pageSize默认999 -->
|
||
<el-table ref="multipleTable" :data="tableData" row-key="ID" @selection-change="handleSelectionChange">
|
||
<el-table-column fixed type="selection" width="40" align="center" />
|
||
<el-table-column align="left" label="ID" min-width="60" prop="ID" />
|
||
<el-table-column align="left" label="标题/副标题" min-width="400" prop="title">
|
||
<template #default="scope">
|
||
<div>
|
||
<el-link type="primary" :href="getArticlePreviewPath(scope.row.ID)" target="_blank"
|
||
rel="noopener noreferrer">
|
||
<el-text :line-clamp="2"> {{ scope.row.title }}</el-text>
|
||
</el-link>
|
||
</div>
|
||
<el-text :line-clamp="2" size="small">{{ scope.row.subtitle }}</el-text>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column align="left" label="摘要" min-width="400" prop="desc" class-name="text-truncate">
|
||
<template #default="scope">
|
||
<el-text :line-clamp="3">{{ scope.row.desc }}</el-text>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column align="left" label="标签" min-width="150" prop="tags">
|
||
<template #default="scope">
|
||
<el-tag v-for="(item, index) in formatTags(scope.row.tags)" :key="index" size="small"
|
||
style="margin-right: 5px;">
|
||
{{ item }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column align="left" label="时间" width="280">
|
||
<template #default="scope">
|
||
<div><b>发布时间:</b>{{ formatDate(scope.row.publishDate) }}</div>
|
||
<div><b>最后更新:</b>{{ formatDate(scope.row.UpdatedAt) }}</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column align="left" label="所属栏目" prop="categories" width="180">
|
||
<template #default="scope">
|
||
<el-cascader v-model="scope.row.channelIds" :options="channelOptions" style="width:100%"
|
||
:props="{ label: 'title', value: 'ID', emitPath: false, expandTrigger: 'hover', multiple: true }"
|
||
:collapse-tags="true" :max-collapse-tags="3" filterable
|
||
@visible-change="(flag) => { if (!flag) handleChangeChannels(scope.row) }"
|
||
@remove-tag="() => { handleChangeChannels(scope.row) }" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column align=" left" label="文章分类" prop="categories" width="180">
|
||
<template #default="scope">
|
||
<el-cascader v-model="scope.row.categoryIds" :options="categoryOptions" style="width:100%"
|
||
:props="{ label: 'title', value: 'ID', emitPath: false, expandTrigger: 'hover', multiple: true }"
|
||
:collapse-tags="true" :max-collapse-tags="3" filterable
|
||
@visible-change="(flag) => { if (!flag) handleChangeCategories(scope.row) }"
|
||
@remove-tag="() => { handleChangeCategories(scope.row) }" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column align="left" label="作者" min-width="80" prop="author" />
|
||
<el-table-column align="left" label="类型" min-width="80" prop="articleType">
|
||
<template #default="scope">{{ formatArticleType(scope.row.articleType) }}</template>
|
||
</el-table-column>
|
||
<el-table-column align="left" label="状态" width="80">
|
||
<template #default="scope">{{ formatStatus(scope.row.status) }}</template>
|
||
</el-table-column>
|
||
<el-table-column align="left" fixed="right" label="操作" width="80" class="dispose-cell">
|
||
<template #default="scope">
|
||
<div class="flex md-2">
|
||
<el-button v-if="scope.row.status === 1" v-auth="btnAuth.submit" type="success" link icon="check"
|
||
@click="handleRowChange(scope.row, 2)">提审</el-button>
|
||
</div>
|
||
<div class="flex md-2">
|
||
<el-button v-if="scope.row.status === 2" v-auth="btnAuth.review" type="primary" link icon="WindPower"
|
||
@click="handleRowReview(scope.row)">审核</el-button>
|
||
</div>
|
||
<div class="flex md-2">
|
||
<el-button v-if="scope.row.status === 3" v-auth="btnAuth.release" type="success" link icon="top"
|
||
@click="handleRowChange(scope.row, 4)">发布</el-button>
|
||
</div>
|
||
<div class="flex md-2">
|
||
<el-button v-if="scope.row.status === 4" v-auth="btnAuth.cancel" type="warning" link icon="bottom"
|
||
@click="handleRowChange(scope.row, 5)">撤销</el-button>
|
||
</div>
|
||
<div class="flex md-2">
|
||
<el-button v-auth="btnAuth.edit"
|
||
:type="scope.row.status === 1 || scope.row.status === 5 || scope.row.status === 3 ? 'primary' : 'info'"
|
||
link icon="edit" :disabled="scope.row.status === 4 || scope.row.status === 2"
|
||
@click="handleRowEdit(scope.row.ID)">编辑</el-button>
|
||
</div>
|
||
<div class="flex md-2">
|
||
<el-button v-auth="btnAuth.delete" type="danger" link icon="delete"
|
||
@click="handleRowDelete(scope.row.ID)">删除</el-button>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<div class="gva-pagination">
|
||
<el-pagination layout="total, sizes, prev, pager, next, jumper" :current-page="page" :page-size="pageSize"
|
||
:page-sizes="[10, 30, 50, 100]" :total="total" @current-change="handleCurrentChange"
|
||
@size-change="handleSizeChange" />
|
||
</div>
|
||
</div>
|
||
|
||
<ArticleEdit ref="articleEditRef" :title="articleEditTitle" @on-save="handlerSaveArticle" />
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, watch, nextTick } from 'vue'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import { formatDate, getArticlePreviewPath } from '@/utils/format'
|
||
import { getCategoryTree } from '@/api/category'
|
||
import { getChannelTree } from '@/api/channel'
|
||
import { formatTimeToStr } from '@/utils/date'
|
||
import { equalArr } from '@/utils/arr'
|
||
import {
|
||
getArticleList,
|
||
deleteArticle,
|
||
deleteArticleByIds,
|
||
setArticleChannels,
|
||
setArticleCategories,
|
||
submitArticle,
|
||
reviewBackArticle,
|
||
releaseArticle,
|
||
cancelArticle
|
||
} from '@/api/article'
|
||
import ArticleEdit from '@/view/content/components/articleEdit.vue'
|
||
import { useBtnAuth } from '@/utils/btnAuth'
|
||
import { statusOptions, formatArticleType, formatStatus } from '@/utils/options'
|
||
|
||
const btnAuth = useBtnAuth()
|
||
const articleEditTitle = ref('')
|
||
const articleEditRef = ref(false)
|
||
const page = ref(1)
|
||
const total = ref(0)
|
||
const pageSize = ref(10)
|
||
const searchInfo = ref({})
|
||
const searchCategoryId = ref('')
|
||
const searchChannelId = ref('')
|
||
const tableData = ref([])
|
||
const multipleSelection = ref([])// 多选数据
|
||
const elSearchFormRef = ref()
|
||
|
||
const formatTags = (tags) => {
|
||
return tags && tags.split(',')
|
||
}
|
||
|
||
const initSearchInfo = () => {
|
||
const endDate = new Date()
|
||
const startDate = new Date(endDate.getTime() - 7 * 24 * 60 * 60 * 1000)
|
||
|
||
searchChannelId.value = ''
|
||
searchCategoryId.value = ''
|
||
|
||
searchInfo.value = {
|
||
dateRange: [
|
||
formatTimeToStr(startDate, 'yyyy-MM-dd'),
|
||
formatTimeToStr(endDate, 'yyyy-MM-dd'),
|
||
]
|
||
}
|
||
}
|
||
|
||
initSearchInfo()
|
||
|
||
// 多选
|
||
const handleSelectionChange = (val) => {
|
||
multipleSelection.value = val
|
||
}
|
||
|
||
const handleMultiDelete = () => {
|
||
ElMessageBox.confirm('确定要删除所选文章吗?', '请确认', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
}).then(async valid => {
|
||
const IDs = []
|
||
if (multipleSelection.value.length === 0) {
|
||
ElMessage({
|
||
type: 'warning',
|
||
message: '请选择要删除的数据'
|
||
})
|
||
return
|
||
}
|
||
multipleSelection.value &&
|
||
multipleSelection.value.map(item => {
|
||
IDs.push(item.ID)
|
||
})
|
||
const res = await deleteArticleByIds({ IDs })
|
||
if (res.code === 0) {
|
||
ElMessage({
|
||
type: 'success',
|
||
message: '删除成功'
|
||
})
|
||
if (tableData.value.length === IDs.length && page.value > 1) {
|
||
page.value--
|
||
}
|
||
getTableData()
|
||
}
|
||
})
|
||
}
|
||
|
||
// 重置
|
||
const handleResetSearch = () => {
|
||
initSearchInfo()
|
||
getTableData()
|
||
}
|
||
|
||
// 搜索
|
||
const handleSubmitSearch = () => {
|
||
elSearchFormRef.value?.validate(async valid => {
|
||
if (!valid) return
|
||
if (searchInfo.value.status === '') {
|
||
searchInfo.value.status = null
|
||
}
|
||
if (searchChannelId.value !== '') {
|
||
searchInfo.value.channelId = parseInt(searchChannelId.value, 10)
|
||
}
|
||
if (searchCategoryId.value !== '') {
|
||
searchInfo.value.categoryId = parseInt(searchCategoryId.value, 10)
|
||
}
|
||
getTableData()
|
||
})
|
||
}
|
||
|
||
// 分页
|
||
const handleSizeChange = (val) => {
|
||
pageSize.value = val
|
||
getTableData()
|
||
}
|
||
|
||
// 修改页面容量
|
||
const handleCurrentChange = (val) => {
|
||
page.value = val
|
||
getTableData()
|
||
}
|
||
|
||
// 添加
|
||
const handleAdd = () => {
|
||
articleEditTitle.value = '添加文章'
|
||
articleEditRef.value.openPage({ articleId: 0 })
|
||
}
|
||
|
||
// 修改
|
||
const handleRowEdit = (ID) => {
|
||
articleEditTitle.value = '编辑文章 ID:' + ID
|
||
articleEditRef.value.openPage({ articleId: ID })
|
||
}
|
||
|
||
// 保存文章后
|
||
const handlerSaveArticle = () => {
|
||
getTableData()
|
||
}
|
||
|
||
// 删除
|
||
const handleRowDelete = (ID) => {
|
||
ElMessageBox.confirm('此操作将删除文章, 是否继续?', '请确认', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
}).then(async valid => {
|
||
const res = await deleteArticle({ id: ID })
|
||
if (res.code === 0) {
|
||
ElMessage({
|
||
type: 'success',
|
||
message: '删除成功!'
|
||
})
|
||
if (tableData.value.length === 1 && page.value > 1) {
|
||
page.value--
|
||
}
|
||
getTableData()
|
||
}
|
||
})
|
||
}
|
||
|
||
// 提审、发布、撤销
|
||
const handleRowChange = (row, status) => {
|
||
let msg
|
||
if (status === 2) {
|
||
msg = '文章提交审核'
|
||
} else if (status === 4) {
|
||
msg = '发布文章到网站'
|
||
} else if (status === 5) {
|
||
msg = '从网站撤销文章'
|
||
}
|
||
ElMessageBox.confirm('此操作将' + msg + ', 是否继续?', '请确认', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
}).then(async valid => {
|
||
let res
|
||
if (status === 2) {
|
||
res = await submitArticle({ ID: row.ID })
|
||
} else if (status === 4) {
|
||
res = await releaseArticle({ ID: row.ID })
|
||
} else if (status === 5) {
|
||
res = await cancelArticle({ ID: row.ID, status: 2 })
|
||
}
|
||
if (res.code === 0) {
|
||
ElMessage({
|
||
type: 'success',
|
||
message: msg + '成功!'
|
||
})
|
||
row.status = status
|
||
}
|
||
})
|
||
}
|
||
|
||
// 审核文章,弹出选择框
|
||
const handleRowReview = row => {
|
||
ElMessageBox.confirm('请确认是否审核通过,确认请按“是”,否则请按“否”,取消请按“ESC”关闭弹窗', '请确认', {
|
||
distinguishCancelAndClose: true,
|
||
confirmButtonText: '是',
|
||
cancelButtonText: '否',
|
||
type: 'warning'
|
||
}).then(async valid => {
|
||
// 确认通过,发布文章
|
||
const res = await releaseArticle({ ID: row.ID })
|
||
if (res.code === 0) {
|
||
ElMessage({
|
||
type: 'success',
|
||
message: '审核通过!'
|
||
})
|
||
row.status = 4
|
||
}
|
||
}).catch(async (action) => {
|
||
// 确认不通过,状态为草稿
|
||
if (action === 'cancel') {
|
||
const res = await reviewBackArticle({ ID: row.ID })
|
||
if (res.code === 0) {
|
||
ElMessage({
|
||
type: 'warning',
|
||
message: '审核不通过!'
|
||
})
|
||
row.status = 1
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
// ----- 列表更新栏目相关 -----
|
||
const handleChangeChannels = async row => {
|
||
await nextTick()
|
||
const ids = row.channelIds.map(i => { return parseInt(i) })
|
||
if (ids.length === 0) {
|
||
restoreChannels(row, '所属栏目不能为空')
|
||
return
|
||
}
|
||
// 比较是否有变化
|
||
const existArr = row.channels && row.channels.map(i => { return String(i.ID) })
|
||
if (equalArr(existArr, ids)) {
|
||
return
|
||
}
|
||
const res = await setArticleChannels({
|
||
ID: row.ID,
|
||
channelIds: ids
|
||
})
|
||
if (res.code === 0) {
|
||
ElMessage({ type: 'success', message: '所属栏目设置成功' })
|
||
getTableData()
|
||
} else {
|
||
// 恢复回去
|
||
restoreChannels(row, res.message)
|
||
}
|
||
}
|
||
const restoreChannels = (row, message) => {
|
||
row.channelIds = row.channels && row.channels.map(i => { return String(i.ID) })
|
||
ElMessage({ type: 'error', message: message })
|
||
}
|
||
// ----- 列表分类相关 -----
|
||
const handleChangeCategories = async row => {
|
||
await nextTick()
|
||
const ids = row.categoryIds.map(i => { return parseInt(i) })
|
||
if (ids.length === 0) {
|
||
restoreCategories(row, '文章分类不能为空')
|
||
return
|
||
}
|
||
// 比较是否有变化
|
||
const existArr = row.categories && row.categories.map(i => { return String(i.ID) })
|
||
if (equalArr(existArr, ids)) {
|
||
return
|
||
}
|
||
const res = await setArticleCategories({
|
||
ID: row.ID,
|
||
categoryIds: ids
|
||
})
|
||
if (res.code === 0) {
|
||
ElMessage({ type: 'success', message: '文章分类设置成功' })
|
||
getTableData()
|
||
} else {
|
||
// 恢复回去
|
||
restoreCategories(row, res.message)
|
||
}
|
||
}
|
||
const restoreCategories = (row, message) => {
|
||
row.categoryIds = row.categories && row.categories.map(i => { return String(i.ID) })
|
||
ElMessage({ type: 'error', message: message })
|
||
}
|
||
|
||
watch(() => tableData.value, () => {
|
||
// 初始化数据
|
||
setCategoryIds()
|
||
setChannelIds()
|
||
})
|
||
|
||
// ----- 查询 -----
|
||
const getTableData = async valid => {
|
||
const res = await getArticleList({ page: page.value, pageSize: pageSize.value, ...searchInfo.value })
|
||
if (res.code === 0) {
|
||
tableData.value = res.data.list
|
||
total.value = res.data.total
|
||
}
|
||
}
|
||
|
||
const setCategoryIds = () => {
|
||
tableData.value && tableData.value.forEach((row) => {
|
||
row.categoryIds = row.categories && row.categories.map(i => { return String(i.ID) })
|
||
})
|
||
}
|
||
|
||
const setChannelIds = () => {
|
||
tableData.value && tableData.value.forEach((row) => {
|
||
row.channelIds = row.channels && row.channels.map(i => { return String(i.ID) })
|
||
})
|
||
}
|
||
|
||
// options 相关
|
||
const initOptions = (data, optionsData, disabled) => {
|
||
data &&
|
||
data.forEach(item => {
|
||
if (item.children && item.children.length) {
|
||
const option = {
|
||
title: item.title,
|
||
ID: String(item.ID),
|
||
children: []
|
||
}
|
||
initOptions(
|
||
item.children,
|
||
option.children,
|
||
)
|
||
optionsData.push(option)
|
||
} else {
|
||
const option = {
|
||
title: item.title,
|
||
ID: String(item.ID),
|
||
}
|
||
optionsData.push(option)
|
||
}
|
||
})
|
||
}
|
||
|
||
const channelOptions = ref([])
|
||
const categoryOptions = ref([])
|
||
|
||
const getCategoryData = async valid => {
|
||
categoryOptions.value = []
|
||
const res = await getCategoryTree()
|
||
if (res.code === 0) {
|
||
initOptions(res.data.categoryTree, categoryOptions.value, false)
|
||
}
|
||
}
|
||
const getChannelData = async valid => {
|
||
channelOptions.value = []
|
||
const res = await getChannelTree()
|
||
if (res.code === 0) {
|
||
initOptions(res.data.channelTree, channelOptions.value, false)
|
||
}
|
||
}
|
||
|
||
const setOptions = () => {
|
||
getCategoryData()
|
||
getChannelData()
|
||
}
|
||
|
||
setOptions()
|
||
getTableData()
|
||
|
||
</script>
|
||
|
||
<style type="scss">
|
||
.admin-box .el-table td .cell {
|
||
line-height: 28px;
|
||
}
|
||
|
||
.cell button {
|
||
line-height: 22px;
|
||
}
|
||
|
||
.text-truncate .cell {
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
display: -webkit-box;
|
||
-webkit-line-clamp: 4;
|
||
-webkit-box-orient: vertical;
|
||
}
|
||
</style>
|