482 lines
16 KiB
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>
|