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

482 lines
16 KiB
Vue

<template>
<div>
<div class="gva-search-box">
<el-form ref="elSearchFormRef" :inline="true" :model="searchInfo" label-width="90px" class="demo-form-inline"
@keyup.enter="handleSubmitSearch">
<el-form-item label="创建日期" prop="createdAt" style="width:300px">
<template #label>
<span>
<el-tooltip content="搜索范围是创建开始日期(包含)至创建结束日期(包含)" placement="top-start">
<el-icon>
<QuestionFilled />
</el-icon>
</el-tooltip>
创建日期
</span>
</template>
<el-date-picker v-model="searchInfo.dateRange" type="daterange" value-format="YYYY-MM-DD" :clearable="false"
:editable="false" />
</el-form-item>
<el-form-item label="关键词" style="width:300px">
<template #label>
<span>
<el-tooltip content="从标题、副标题、摘要中搜索" placement="top-start">
<el-icon>
<QuestionFilled />
</el-icon>
</el-tooltip>
关键词
</span>
</template>
<el-input v-model="searchInfo.keyword" class="keyword" placeholder="请输入" clearable style="width:100%" />
</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:300px">
<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 icon="delete" style="margin-left: 10px;" :disabled="!multipleSelection.length"
@click="handleMultiDelete">删除</el-button>
<el-button icon="plus" style="margin-left: 10px;" :disabled="!multipleSelection.length"
@click="handleMultiPublish">发布</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="270" prop="title">
<template #default="scope">
{{ scope.row.title }}
<div><small>{{ scope.row.subtitle }}</small></div>
</template>
</el-table-column>
<el-table-column align="left" label="摘要" min-width="400" prop="desc" class-name="text-truncate">
<template #default="scope">
<div>{{ scope.row.desc }}</div>
</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">
<template #default="scope">
<div>
<el-button type="primary" link icon="memo" :disabled="scope.row.status === 2"
@click="handleRowPreview(scope.row)">预览</el-button>
</div>
<div>
<el-button type="primary" link icon="plus" :disabled="scope.row.status === 2"
@click="handleRowChange(scope.row)">发布</el-button>
</div>
<div>
<el-button type="primary" link icon="edit" @click="handleRowEdit(scope.row.ID)">编辑</el-button>
</div>
<div>
<el-button type="primary" 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>
</div>
</template>
<script setup>
import { ref, watch, nextTick } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { formatDate } from '@/utils/format'
import { getSourceList } from '@/api/source'
import { getCategoryTree } from '@/api/category'
import { getChannelTree } from '@/api/channel'
import { formatTimeToStr } from '@/utils/date'
import { equalArr } from '@/utils/arr'
import {
getArticleList,
releaseArticle,
deleteArticle,
setArticleChannels,
setArticleCategories
} from '@/api/article'
const router = useRouter()
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 articleTypeOptions = ref([
{ key: 1, label: '图文' },
{ key: 2, label: '视频' },
])
const statusOptions = ref([
{ key: 1, label: '未发布' },
{ key: 2, label: '已发布' },
])
const formatArticleType = (value) => {
const rowLabel = articleTypeOptions.value.filter(item => item.key === value)
return rowLabel && rowLabel[0] && rowLabel[0].label
}
const formatStatus = (value) => {
const rowLabel = statusOptions.value.filter(item => item.key === value)
return rowLabel && rowLabel[0] && rowLabel[0].label
}
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 = () => {
// @todo 批量删除
}
const handleMultiPublish = () => {
// @todo 批量发布
}
// 重置
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 = () => {
router.push({ name: 'articleEdit' })
}
// 修改
const handleRowEdit = (ID) => {
const query = { id: ID }
router.push({ name: 'articleEdit', query })
}
// 删除
const handleRowDelete = (ID) => {
ElMessageBox.confirm('此操作将永久删除文章, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
const res = await deleteArticle({ ID })
if (res.code === 0) {
ElMessage({
type: 'success',
message: '删除成功!'
})
if (tableData.value.length === 1 && page.value > 1) {
page.value--
}
getTableData()
}
})
}
// 发布
const handleRowChange = (row) => {
ElMessageBox.confirm('此操作将发布文章, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
const res = await releaseArticle({ ID: row.ID, status: 2 })
if (res.code === 0) {
ElMessage({
type: 'success',
message: '发布成功!'
})
if (tableData.value.length === 1 && page.value > 1) {
page.value--
}
getTableData()
}
})
}
// 预览
const handleRowPreview = (row) => {
ElMessage({
type: 'success',
message: '开发中。。。'
})
}
// ----- 列表更新栏目相关 -----
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 () => {
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 () => {
categoryOptions.value = []
const res = await getCategoryTree()
if (res.code === 0) {
initOptions(res.data.categoryTree, categoryOptions.value, false)
}
}
const getChannelData = async () => {
channelOptions.value = []
const res = await getChannelTree()
if (res.code === 0) {
initOptions(res.data.channelTree, channelOptions.value, false)
}
}
const getSourceData = async () => {
channelOptions.value = []
const res = await getSourceList({ page: 1, pageSize: 999 })
if (res.code === 0) {
res.data.list && res.data.list.map(item => item.name)
}
}
const setOptions = () => {
getCategoryData()
getChannelData()
getSourceData()
}
setOptions()
getTableData()
</script>
<style type="scss">
.admin-box .el-table td .cell {
line-height: 28px;
}
.text-truncate .cell {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
}
</style>