web-admin/src/view/content/article/index.vue

536 lines
18 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="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>