parent
eaa9caa47c
commit
41e5d73c9b
|
|
@ -222,7 +222,7 @@ module.exports = {
|
|||
}
|
||||
],
|
||||
'space-before-blocks': [2, 'always'],
|
||||
'space-before-function-paren': [2, 'never'],
|
||||
'space-before-function-paren': [2, 'always'],
|
||||
'space-in-parens': [2, 'never'],
|
||||
'space-infix-ops': 2,
|
||||
'space-unary-ops': [
|
||||
|
|
|
|||
|
|
@ -35,6 +35,18 @@ export const deleteArticle = (data) => {
|
|||
})
|
||||
}
|
||||
|
||||
// @Summary 批量删除文章
|
||||
// @Param data body request.IdsReq true "批量删除文章"
|
||||
// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}"
|
||||
// @Router /product/deleteProduct [delete]
|
||||
export const deleteArticleByIds = (params) => {
|
||||
return service({
|
||||
url: '/cms/article/deleteByIds',
|
||||
method: 'delete',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary 修改文章
|
||||
// @Produce application/json
|
||||
// @Param menu Object
|
||||
|
|
@ -47,7 +59,7 @@ export const updateArticle = (data) => {
|
|||
})
|
||||
}
|
||||
|
||||
// @Tags 文章文章
|
||||
// @Tags 文章
|
||||
// @Summary 根据id获取文章
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
|
|
@ -63,14 +75,50 @@ export const getArticleById = (params) => {
|
|||
})
|
||||
}
|
||||
|
||||
// @Summary 发布文章
|
||||
// @Summary 文章提审
|
||||
// @Produce application/json
|
||||
// @Param menu Object
|
||||
// @Router /cms/article/submit [put]
|
||||
export const submitArticle = (data) => {
|
||||
return service({
|
||||
url: '/cms/article/submit',
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary 文章审核
|
||||
// @Produce application/json
|
||||
// @Param menu Object
|
||||
// @Router /cms/article/review [put]
|
||||
export const reviewArticle = (data) => {
|
||||
return service({
|
||||
url: '/cms/article/review',
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary 文章发布
|
||||
// @Produce application/json
|
||||
// @Param menu Object
|
||||
// @Router /cms/article/release [put]
|
||||
export const releaseArticle = (data) => {
|
||||
return service({
|
||||
url: '/cms/article/release',
|
||||
method: 'post',
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary 文章撤销
|
||||
// @Produce application/json
|
||||
// @Param menu Object
|
||||
// @Router /cms/article/cancel [put]
|
||||
export const cancelArticle = (data) => {
|
||||
return service({
|
||||
url: '/cms/article/cancel',
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
import service from '@/utils/request'
|
||||
|
||||
// @Summary 获取列表数据
|
||||
// @Produce application/json
|
||||
// @Router /cms/tag/getList [post]
|
||||
export const getTagList = (data) => {
|
||||
return service({
|
||||
url: '/cms/tag/getList',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
|
@ -72,8 +72,6 @@ const emit = defineEmits(['on-select', 'on-before-upload', 'on-upload-success',
|
|||
const props = defineProps({
|
||||
category: { type: String, required: true },
|
||||
size: { type: String, default: '800px' },
|
||||
target: { type: Object, default: null },
|
||||
targetKey: { type: String, default: '' },
|
||||
})
|
||||
|
||||
const drawer = ref(false)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ const emit = defineEmits([
|
|||
'on-success',
|
||||
])
|
||||
const props = defineProps({
|
||||
category: {
|
||||
type: String, required: true
|
||||
},
|
||||
imageUrl: {
|
||||
type: String,
|
||||
default: ''
|
||||
|
|
|
|||
|
|
@ -1,417 +0,0 @@
|
|||
<template>
|
||||
|
||||
<div v-loading="fullscreenLoading">
|
||||
<div class="gva-form-box">
|
||||
<div v-if="showErrMessage != ''">
|
||||
{{ showErrMessage }}
|
||||
</div>
|
||||
<el-form v-else ref="elEditFormRef" label-position="top" :model="editForm" :rules="formRules">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="17">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="16">
|
||||
<el-form-item label="标题" prop="title">
|
||||
<el-input v-model="editForm.title" autocomplete="off" :show-word-limit="true" maxlength="50" />
|
||||
</el-form-item>
|
||||
<el-form-item label="副标题" prop="subtitle">
|
||||
<el-input v-model="editForm.subtitle" autocomplete="off" :show-word-limit="true" maxlength="50" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="文章类型" prop="articleType">
|
||||
<el-select v-model="editForm.articleType" placeholder="请选择">
|
||||
<el-option v-for="item in articleTypeOptions" :key="item.key" :label="item.label"
|
||||
:value="item.key" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="作者" prop="author">
|
||||
<el-input v-model="editForm.author" autocomplete="off" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="所属栏目" prop="channels">
|
||||
<el-cascader v-model="channelIdList" :options="channelOptions" style="width:100%"
|
||||
:props="{ label: 'title', value: 'ID', emitPath: false, expandTrigger: 'hover', multiple: true }"
|
||||
:collapse-tags="true" :collapse-tags-tooltip="true" :max-collapse-tags="3" filterable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="文章分类" prop="categories">
|
||||
<el-cascader v-model="categoryIdList" :options="categoryOptions" style="width:100%"
|
||||
:props="{ label: 'title', value: 'ID', emitPath: false, expandTrigger: 'hover', multiple: true }"
|
||||
:collapse-tags="true" :collapse-tags-tooltip="true" :max-collapse-tags="2" filterable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="文章内容" prop="content">
|
||||
<RichEdit v-model="editForm.content" style="height: 50rem;" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="1" align="center">
|
||||
<el-divider direction="vertical" style="height: 100%" />
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="摘要" prop="desc">
|
||||
<el-input v-model="editForm.desc" type="textarea" rows="7" :show-word-limit="true" maxlength="100"
|
||||
autocomplete="off" resize="none" />
|
||||
</el-form-item>
|
||||
<el-form-item label="来源" prop="source">
|
||||
<el-select v-model="editForm.source" placeholder="请选择" allow-create clearable filterable>
|
||||
<el-option v-for="item in sourceOptions" :key="item" :label="item" :value="item" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="跳转链接" prop="source">
|
||||
<el-input v-model="editForm.url" autocomplete="off" placeholder="https://" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="发布时间">
|
||||
<el-date-picker v-model="editForm.publishDate" type="datetime" placeholder="请选择" style="width:100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="文章标签" prop="tags">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<el-tag v-for="(item, index) in tagList" :key="index" type="info" closable size="large"
|
||||
@close="tagList.splice(index, 1)">
|
||||
{{ item }}
|
||||
</el-tag>
|
||||
<el-input v-if="tagInputFlag" ref="InputRef" v-model="inputTag" class="w-20" size="default"
|
||||
style="width: 80px;" @keyup.enter="handleTagOK" @keyup.esc="handleTagCancel" @blur="handleTagOK" />
|
||||
<el-button v-else type="primary" plain icon="plus" class="button-new-tag" size="default"
|
||||
style="width: 80px;" @click="handleTagAdd">
|
||||
添加
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="图片(首张图可用来做栏目列表用)" prop="imgs">
|
||||
<ChooseImg ref="chooseImg" category="article_imgs" size="800px" @on-select="handleSelectImg" />
|
||||
<template #label>
|
||||
<div style="margin-bottom: 10px;">图片(首张图可用来做栏目列表用)</div>
|
||||
<div class="flex gap-3">
|
||||
<el-button type="success" plain icon="folder" @click="handleChooseImg">媒体库</el-button>
|
||||
<upload-common class="upload-btn-media-library" category="article_imgs"
|
||||
@on-success="handleImgUpload" />
|
||||
<el-tooltip effect="dark" content="图片超过 512K 或者 长宽 > 1080 会自动压缩后再上传" placement="top-start">
|
||||
<upload-image :file-size="512" :max-w-h="1080" category="article_imgs"
|
||||
@on-success="handleImgUpload" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<div class="media">
|
||||
<div v-for="(item, key) in imgFileList" :key="key" class="media-box">
|
||||
<div class="img-box-list">
|
||||
<el-image :key="key" :src="getUrl(item.url)" fit="cover" :preview-src-list="[getUrl(item.url)]"
|
||||
hide-on-click-modal style="width: 138px; height: 138px;">
|
||||
<template #error>
|
||||
<div class="img-box-list">
|
||||
<el-icon>
|
||||
<picture />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
<div class="img-title">
|
||||
<el-button size="small" type="default" icon="delete" @click="handleImgRemove(item)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="24">
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="check" style="width: 150px;" @click="handleFormSubmit">保存</el-button>
|
||||
<el-button type="default" icon="close" @click="handleFormClose">关闭</el-button>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { getUrl } from '@/utils/image'
|
||||
import { reactive, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import RichEdit from '@/components/richtext/rich-edit.vue'
|
||||
import ChooseImg from '@/components/chooseImg/index.vue'
|
||||
import { getSourceList } from '@/api/source'
|
||||
import { getCategoryTree } from '@/api/category'
|
||||
import { getChannelTree } from '@/api/channel'
|
||||
import UploadCommon from '@/components/upload/common.vue'
|
||||
import UploadImage from '@/components/upload/image.vue'
|
||||
import {
|
||||
addArticle,
|
||||
updateArticle,
|
||||
getArticleById,
|
||||
} from '@/api/article'
|
||||
import { getFetcherArticleById } from '@/api/fetcher'
|
||||
|
||||
const showErrMessage = ref('')
|
||||
|
||||
const fullscreenLoading = ref(true)
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const isEdit = ref(false) // 确定是修改还是增加 isEdit = true 为修改,否则为增加
|
||||
const elEditFormRef = ref()
|
||||
const formRules = reactive({
|
||||
articleType: [
|
||||
{ required: true, message: '请输选择文章类型', trigger: 'blur' }
|
||||
],
|
||||
author: [
|
||||
{ required: true, message: '请输入作者', trigger: 'blur' }
|
||||
],
|
||||
title: [
|
||||
{ required: true, message: '请输入文章名称', trigger: 'blur' }
|
||||
],
|
||||
subtitle: [
|
||||
{ required: true, message: '请输入文章副标题', trigger: 'blur' }
|
||||
],
|
||||
content: [
|
||||
{ required: true, message: '请输入文章内容', trigger: 'blur' }
|
||||
],
|
||||
})
|
||||
const articleTypeOptions = ref([
|
||||
{ key: 1, label: '图文' },
|
||||
{ key: 2, label: '视频' },
|
||||
])
|
||||
const editForm = ref({
|
||||
ID: 0,
|
||||
articleType: '',
|
||||
title: '',
|
||||
subtitle: '',
|
||||
author: '',
|
||||
channels: [],
|
||||
categories: [],
|
||||
desc: '',
|
||||
source: '',
|
||||
url: '',
|
||||
publishDate: '',
|
||||
articleTime: '',
|
||||
tags: '',
|
||||
imgs: '',
|
||||
})
|
||||
// options 相关
|
||||
const initOptions = (data, optionsData) => {
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 以下为 options 使用
|
||||
const channelOptions = ref([])
|
||||
const categoryOptions = ref([])
|
||||
const sourceOptions = 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 () => {
|
||||
sourceOptions.value = []
|
||||
const res = await getSourceList({ page: 1, pageSize: 999 })
|
||||
if (res.code === 0) {
|
||||
sourceOptions.value = res.data.list && res.data.list.map(item => item.name)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化方法
|
||||
const initPage = async () => {
|
||||
getCategoryData()
|
||||
getChannelData()
|
||||
getSourceData()
|
||||
|
||||
// 建议通过url传参获取目标数据ID 调用 find方法进行查询数据操作 从而决定本页面是create还是update 以下为id作为url参数示例
|
||||
if (route.query && route.query.id && route.query.id > 0) {
|
||||
const res = await getArticleById({ ID: route.query.id })
|
||||
if (res.code === 0) {
|
||||
// 初始化表单数据
|
||||
editForm.value = res.data.article
|
||||
isEdit.value = true
|
||||
// 初始化关联数据
|
||||
const { categories, channels, imgs, tags } = res.data.article
|
||||
categoryIdList.value = categories && categories.map(item => String(item.ID))
|
||||
channelIdList.value = channels && channels.map(item => String(item.ID))
|
||||
if (imgs && (imgs !== '')) {
|
||||
imgFileList.value = JSON.parse(imgs)
|
||||
}
|
||||
tagList.value = tags && tags.split(',')
|
||||
fullscreenLoading.value = false
|
||||
} else {
|
||||
ElMessage({
|
||||
type: 'error',
|
||||
message: '获取数据失败'
|
||||
})
|
||||
showErrMessage.value = '获取数据失败'
|
||||
}
|
||||
} else if (route.query && route.query.fetcherId && route.query.fetcherId > 0) {
|
||||
// 从爬虫数据中获取内容并填充至文章内容中
|
||||
const res = await getFetcherArticleById({ ID: route.query.fetcherId })
|
||||
if (res.code === 0 && res.data && res.data.article) {
|
||||
console.log(res.data)
|
||||
const { title, author, source, content, publicTime } = res.data.article
|
||||
editForm.value.title = title
|
||||
editForm.value.subtitle = title
|
||||
editForm.value.author = author
|
||||
editForm.value.source = source
|
||||
editForm.value.content = content
|
||||
editForm.value.publishDate = publicTime
|
||||
editForm.value.articleTime = publicTime
|
||||
} else {
|
||||
ElMessage({
|
||||
type: 'error',
|
||||
message: '获取数据失败'
|
||||
})
|
||||
showErrMessage.value = '获取数据失败'
|
||||
}
|
||||
}
|
||||
fullscreenLoading.value = false
|
||||
}
|
||||
|
||||
initPage()
|
||||
|
||||
// ---- 以下为图片相关操作 ----
|
||||
const chooseImg = ref(null)
|
||||
const imgFileList = ref([])
|
||||
const handleChooseImg = (event) => {
|
||||
chooseImg.value.open()
|
||||
event.preventDefault()
|
||||
return false
|
||||
}
|
||||
const handleSelectImg = (file) => {
|
||||
imgFileList.value.push({
|
||||
key: file.key,
|
||||
name: file.name,
|
||||
url: file.url,
|
||||
})
|
||||
}
|
||||
const handleImgRemove = (file) => {
|
||||
imgFileList.value = imgFileList.value.filter(item => item.key !== file.key)
|
||||
}
|
||||
const handleImgUpload = (file) => {
|
||||
imgFileList.value.push({
|
||||
key: file.key,
|
||||
name: file.name,
|
||||
url: file.url,
|
||||
})
|
||||
}
|
||||
|
||||
// ---- 以下为tag相关操作 ----
|
||||
const tagInputFlag = ref(false)
|
||||
const inputTag = ref('')
|
||||
const tagList = ref([])
|
||||
const handleTagOK = (key) => {
|
||||
const val = inputTag.value.trim()
|
||||
if (val !== '' && !tagList.value.includes(val)) {
|
||||
tagList.value.push(val)
|
||||
tagInputFlag.value = false
|
||||
inputTag.value = ''
|
||||
}
|
||||
}
|
||||
const handleTagCancel = (key) => {
|
||||
if (key.keyCode === 27) {
|
||||
// 按下esc
|
||||
tagInputFlag.value = false
|
||||
inputTag.value = ''
|
||||
}
|
||||
}
|
||||
const handleTagAdd = () => {
|
||||
tagInputFlag.value = true
|
||||
}
|
||||
|
||||
const categoryIdList = ref([])
|
||||
const channelIdList = ref([])
|
||||
|
||||
// 添加/保存修改
|
||||
const handleFormSubmit = async () => {
|
||||
const rel = {}
|
||||
rel.categoryIds = categoryIdList.value.map(item => parseInt(item, 10))
|
||||
rel.channelIds = channelIdList.value.map(item => parseInt(item, 10))
|
||||
rel.imgList = imgFileList.value
|
||||
rel.tagList = tagList.value
|
||||
if (editForm.value.publishDate === '') {
|
||||
editForm.value.publishDate = null
|
||||
}
|
||||
if (editForm.value.articleTime === '') {
|
||||
editForm.value.articleTime = null
|
||||
}
|
||||
elEditFormRef.value.validate(async valid => {
|
||||
if (valid) {
|
||||
let res
|
||||
if (isEdit.value) {
|
||||
res = await updateArticle({ article: editForm.value, rel: rel })
|
||||
} else {
|
||||
res = await addArticle({ article: editForm.value, rel: rel })
|
||||
}
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: isEdit.value ? '修改成功' : '添加成功!'
|
||||
})
|
||||
if (!isEdit.value && res.data && res.data.article) {
|
||||
const query = { id: res.data.article.ID }
|
||||
router.replace({ name: 'articleEdit', query })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 返回按钮
|
||||
const handleFormClose = () => {
|
||||
router.go(-1)
|
||||
}
|
||||
|
||||
</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;
|
||||
}
|
||||
|
||||
.img-item {
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,36 +1,23 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="gva-search-box">
|
||||
<el-form ref="elSearchFormRef" :inline="true" :model="searchInfo" label-width="90px" class="demo-form-inline"
|
||||
<el-form ref="elSearchFormRef" :inline="true" :model="searchInfo" label-width="0" 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-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 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 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-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 />
|
||||
|
|
@ -39,9 +26,9 @@
|
|||
<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-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>
|
||||
|
|
@ -56,8 +43,6 @@
|
|||
<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 -->
|
||||
|
|
@ -114,21 +99,24 @@
|
|||
<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">
|
||||
<el-table-column align="left" fixed="right" label="操作" width="140">
|
||||
<template #default="scope">
|
||||
<div>
|
||||
<el-button type="primary" link icon="memo" :disabled="scope.row.status === 2"
|
||||
@click="handleRowPreview(scope.row)">预览</el-button>
|
||||
<div class="flex md-2">
|
||||
<el-button type="primary" link icon="memo" @click="handleRowPreview(scope.row)">预览</el-button>
|
||||
<el-button v-if="scope.row.status === 1" type="success" link icon="check"
|
||||
@click="handleRowChange(scope.row, 2)">提审</el-button>
|
||||
<el-button v-if="scope.row.status === 2" type="primary" link icon="WindPower"
|
||||
@click="handleRowReview(scope.row)">审核</el-button>
|
||||
<el-button v-if="scope.row.status === 3" type="success" link icon="top"
|
||||
@click="handleRowChange(scope.row, 4)">发布</el-button>
|
||||
<el-button v-if="scope.row.status === 4" type="warning" link icon="bottom"
|
||||
@click="handleRowChange(scope.row, 5)">撤销</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 class="flex">
|
||||
<el-button :type="scope.row.status === 1 || scope.row.status === 5 ? 'primary' : 'info'" link icon="edit"
|
||||
:disabled="scope.row.status === 4 || scope.row.status === 2"
|
||||
@click="handleRowEdit(scope.row.ID)">编辑</el-button>
|
||||
<el-button type="danger" link icon="delete" @click="handleRowDelete(scope.row.ID)">删除</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
|
@ -139,12 +127,13 @@
|
|||
@size-change="handleSizeChange" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ArticleEdit ref="articleEditRef" :title="articleEditTitle" @on-save="handlerSaveArticle" />
|
||||
</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'
|
||||
|
|
@ -154,14 +143,19 @@ import { formatTimeToStr } from '@/utils/date'
|
|||
import { equalArr } from '@/utils/arr'
|
||||
import {
|
||||
getArticleList,
|
||||
releaseArticle,
|
||||
deleteArticle,
|
||||
deleteArticleByIds,
|
||||
setArticleChannels,
|
||||
setArticleCategories
|
||||
setArticleCategories,
|
||||
submitArticle,
|
||||
reviewArticle,
|
||||
releaseArticle,
|
||||
cancelArticle
|
||||
} from '@/api/article'
|
||||
import ArticleEdit from '@/view/content/components/articleEdit.vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const articleEditTitle = ref('')
|
||||
const articleEditRef = ref(false)
|
||||
const page = ref(1)
|
||||
const total = ref(0)
|
||||
const pageSize = ref(10)
|
||||
|
|
@ -176,8 +170,11 @@ const articleTypeOptions = ref([
|
|||
{ key: 2, label: '视频' },
|
||||
])
|
||||
const statusOptions = ref([
|
||||
{ key: 1, label: '未发布' },
|
||||
{ key: 2, label: '已发布' },
|
||||
{ key: 1, label: '草稿' },
|
||||
{ key: 2, label: '待审核' },
|
||||
{ key: 3, label: '待发布' },
|
||||
{ key: 4, label: '已发布' },
|
||||
{ key: 5, label: '已撤销' },
|
||||
])
|
||||
|
||||
const formatArticleType = (value) => {
|
||||
|
|
@ -217,10 +214,35 @@ const handleSelectionChange = (val) => {
|
|||
}
|
||||
|
||||
const handleMultiDelete = () => {
|
||||
// @todo 批量删除
|
||||
}
|
||||
const handleMultiPublish = () => {
|
||||
// @todo 批量发布
|
||||
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()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 重置
|
||||
|
|
@ -231,7 +253,7 @@ const handleResetSearch = () => {
|
|||
|
||||
// 搜索
|
||||
const handleSubmitSearch = () => {
|
||||
elSearchFormRef.value?.validate(async (valid) => {
|
||||
elSearchFormRef.value?.validate(async valid => {
|
||||
if (!valid) return
|
||||
if (searchInfo.value.status === '') {
|
||||
searchInfo.value.status = null
|
||||
|
|
@ -260,22 +282,28 @@ const handleCurrentChange = (val) => {
|
|||
|
||||
// 添加
|
||||
const handleAdd = () => {
|
||||
router.push({ name: 'articleEdit' })
|
||||
articleEditTitle.value = '添加文章'
|
||||
articleEditRef.value.openPage({ articleId: 0 })
|
||||
}
|
||||
|
||||
// 修改
|
||||
const handleRowEdit = (ID) => {
|
||||
const query = { id: ID }
|
||||
router.push({ name: 'articleEdit', query })
|
||||
articleEditTitle.value = '编辑文章 ID:' + ID
|
||||
articleEditRef.value.openPage({ articleId: ID })
|
||||
}
|
||||
|
||||
// 保存文章后
|
||||
const handlerSaveArticle = () => {
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleRowDelete = (ID) => {
|
||||
ElMessageBox.confirm('此操作将永久删除文章, 是否继续?', '提示', {
|
||||
ElMessageBox.confirm('此操作将删除文章, 是否继续?', '请确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
}).then(async valid => {
|
||||
const res = await deleteArticle({ ID })
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
|
|
@ -290,23 +318,67 @@ const handleRowDelete = (ID) => {
|
|||
})
|
||||
}
|
||||
|
||||
// 发布
|
||||
const handleRowChange = (row) => {
|
||||
ElMessageBox.confirm('此操作将发布文章, 是否继续?', '提示', {
|
||||
// 提审、发布、撤销
|
||||
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 () => {
|
||||
const res = await releaseArticle({ ID: row.ID, status: 2 })
|
||||
}).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: '发布成功!'
|
||||
message: msg + '成功!'
|
||||
})
|
||||
if (tableData.value.length === 1 && page.value > 1) {
|
||||
page.value--
|
||||
row.status = status
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 审核文章,弹出选择框
|
||||
const handleRowReview = row => {
|
||||
ElMessageBox.confirm('请确认是否审核通过,确认请按“是”,否则请按“否”,取消请按“ESC”关闭弹窗', '请确认', {
|
||||
distinguishCancelAndClose: true,
|
||||
confirmButtonText: '是',
|
||||
cancelButtonText: '否',
|
||||
type: 'warning'
|
||||
}).then(async valid => {
|
||||
// 确认通过,状态为待发布
|
||||
const res = await reviewArticle({ ID: row.ID, status: 3 })
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '审核确认通过!'
|
||||
})
|
||||
row.status = 3
|
||||
}
|
||||
}).catch(async (action) => {
|
||||
// 确认不通过,状态为待提审
|
||||
if (action === 'cancel') {
|
||||
const res = await reviewArticle({ ID: row.ID, status: 1 })
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'warning',
|
||||
message: '审核确认不通过!'
|
||||
})
|
||||
row.status = 1
|
||||
}
|
||||
getTableData()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -314,13 +386,13 @@ const handleRowChange = (row) => {
|
|||
// 预览
|
||||
const handleRowPreview = (row) => {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
type: 'warning',
|
||||
message: '开发中。。。'
|
||||
})
|
||||
}
|
||||
|
||||
// ----- 列表更新栏目相关 -----
|
||||
const handleChangeChannels = async (row) => {
|
||||
const handleChangeChannels = async row => {
|
||||
await nextTick()
|
||||
const ids = row.channelIds.map(i => { return parseInt(i) })
|
||||
if (ids.length === 0) {
|
||||
|
|
@ -349,7 +421,7 @@ const restoreChannels = (row, message) => {
|
|||
ElMessage({ type: 'error', message: message })
|
||||
}
|
||||
// ----- 列表分类相关 -----
|
||||
const handleChangeCategories = async (row) => {
|
||||
const handleChangeCategories = async row => {
|
||||
await nextTick()
|
||||
const ids = row.categoryIds.map(i => { return parseInt(i) })
|
||||
if (ids.length === 0) {
|
||||
|
|
@ -385,7 +457,7 @@ watch(() => tableData.value, () => {
|
|||
})
|
||||
|
||||
// ----- 查询 -----
|
||||
const getTableData = async () => {
|
||||
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
|
||||
|
|
@ -433,21 +505,21 @@ const initOptions = (data, optionsData, disabled) => {
|
|||
const channelOptions = ref([])
|
||||
const categoryOptions = ref([])
|
||||
|
||||
const getCategoryData = async () => {
|
||||
const getCategoryData = async valid => {
|
||||
categoryOptions.value = []
|
||||
const res = await getCategoryTree()
|
||||
if (res.code === 0) {
|
||||
initOptions(res.data.categoryTree, categoryOptions.value, false)
|
||||
}
|
||||
}
|
||||
const getChannelData = async () => {
|
||||
const getChannelData = async valid => {
|
||||
channelOptions.value = []
|
||||
const res = await getChannelTree()
|
||||
if (res.code === 0) {
|
||||
initOptions(res.data.channelTree, channelOptions.value, false)
|
||||
}
|
||||
}
|
||||
const getSourceData = async () => {
|
||||
const getSourceData = async valid => {
|
||||
channelOptions.value = []
|
||||
const res = await getSourceList({ page: 1, pageSize: 999 })
|
||||
if (res.code === 0) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,490 @@
|
|||
<template>
|
||||
<el-drawer v-model="showDrawer" size="90%" :show-close="false" :close-on-press-escape="false"
|
||||
:close-on-click-modal="false">
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-lg">{{ props.title }}</span>
|
||||
<div>
|
||||
<el-button type="primary" style="width: 120px;" @click="handleFormSubmit">确 定</el-button>
|
||||
<el-button @click="handleFormClose">取 消</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-loading="fullscreenLoading">
|
||||
<div class="gva-form-box">
|
||||
<div v-if="showErrMessage != ''">
|
||||
{{ showErrMessage }}
|
||||
</div>
|
||||
<el-form v-else ref="elEditFormRef" label-position="top" :model="editForm" :rules="formRules">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="17">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="24">
|
||||
<el-form-item label="标题" prop="title">
|
||||
<el-input v-model="editForm.title" autocomplete="off" :show-word-limit="true" maxlength="50" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="16">
|
||||
<el-form-item label="副标题" prop="subtitle">
|
||||
<el-input v-model="editForm.subtitle" autocomplete="off" :show-word-limit="true" maxlength="50" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="文章类型" prop="articleType">
|
||||
<el-select v-model="editForm.articleType" placeholder="请选择">
|
||||
<el-option v-for="item in articleTypeOptions" :key="item.key" :label="item.label"
|
||||
:value="item.key" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="所属栏目" prop="channels">
|
||||
<el-cascader v-model="channelIdList" :options="channelOptions" style="width:100%"
|
||||
:props="{ label: 'title', value: 'ID', emitPath: false, expandTrigger: 'hover', multiple: true }"
|
||||
:collapse-tags="true" :collapse-tags-tooltip="true" :max-collapse-tags="3" filterable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="文章分类" prop="categories">
|
||||
<el-cascader v-model="categoryIdList" :options="categoryOptions" style="width:100%"
|
||||
:props="{ label: 'title', value: 'ID', emitPath: false, expandTrigger: 'hover', multiple: true }"
|
||||
:collapse-tags="true" :collapse-tags-tooltip="true" :max-collapse-tags="2" filterable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="文章内容" prop="content">
|
||||
<RichEdit v-model="editForm.content" style="height: 50rem;" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="1" align="center">
|
||||
<el-divider direction="vertical" style="height: 100%" />
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="摘要" prop="desc">
|
||||
<el-input v-model="editForm.desc" type="textarea" rows="7" :show-word-limit="true" maxlength="100"
|
||||
autocomplete="off" resize="none" />
|
||||
</el-form-item>
|
||||
<el-form-item label="作者" prop="author">
|
||||
<el-input v-model="editForm.author" autocomplete="off" />
|
||||
</el-form-item>
|
||||
<el-form-item label="来源" prop="source">
|
||||
<el-select v-model="editForm.source" placeholder="请选择" allow-create clearable filterable>
|
||||
<el-option v-for="item in sourceOptions" :key="item" :label="item" :value="item" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="跳转链接" prop="source">
|
||||
<el-input v-model="editForm.url" autocomplete="off" placeholder="https://" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="发布时间">
|
||||
<el-date-picker v-model="editForm.publishDate" type="datetime" placeholder="请选择" style="width:100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="文章标签" prop="tags">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<el-tag v-for="(item, index) in tagList" :key="index" type="info" closable size="large"
|
||||
@close="tagList.splice(index, 1)">
|
||||
{{ item }}
|
||||
</el-tag>
|
||||
<el-select v-model="selectTag" filterable remote allow-create reserve-keyword placeholder="请输入"
|
||||
:remote-method="loadRemoteTag" :loading="loadingTag" style="width: 100px" @change="handleSelectTag">
|
||||
<el-option v-for="item in tagOptions" :key="item" :label="item" :value="item" />
|
||||
</el-select>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="图片(首张图可用来做栏目列表用)" prop="imgs">
|
||||
<ChooseImg ref="chooseImg" category="article_imgs" size="800px" @on-select="handleSelectImg" />
|
||||
<template #label>
|
||||
<div style="margin-bottom: 10px;">图片(首张图可用来做栏目列表用)</div>
|
||||
<div class="flex gap-3">
|
||||
<el-button type="success" plain icon="folder" @click="handleChooseImg">媒体库</el-button>
|
||||
<upload-common class="upload-btn-media-library" category="article_imgs"
|
||||
@on-success="handleImgUpload" />
|
||||
<el-tooltip effect="dark" content="图片超过 512K 或者 长宽 > 1080 会自动压缩后再上传" placement="top-start">
|
||||
<upload-image :file-size="512" :max-w-h="1080" category="article_imgs"
|
||||
@on-success="handleImgUpload" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<div class="media">
|
||||
<div v-for="(item, key) in imgFileList" :key="key" class="media-box">
|
||||
<div class="img-box-list">
|
||||
<el-image :key="key" :src="getUrl(item.url)" fit="cover" :preview-src-list="[getUrl(item.url)]"
|
||||
hide-on-click-modal style="width: 138px; height: 138px;">
|
||||
<template #error>
|
||||
<div class="img-box-list">
|
||||
<el-icon>
|
||||
<picture />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
<div class="img-title">
|
||||
<el-button size="small" type="default" icon="delete" @click="handleImgRemove(item)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { getUrl } from '@/utils/image'
|
||||
import { reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import RichEdit from '@/components/richtext/rich-edit.vue'
|
||||
import ChooseImg from '@/components/chooseImg/index.vue'
|
||||
import { getSourceList } from '@/api/source'
|
||||
import { getCategoryTree } from '@/api/category'
|
||||
import { getChannelTree } from '@/api/channel'
|
||||
import { getTagList } from '@/api/tag'
|
||||
import UploadCommon from '@/components/upload/common.vue'
|
||||
import UploadImage from '@/components/upload/image.vue'
|
||||
import {
|
||||
addArticle,
|
||||
updateArticle,
|
||||
getArticleById,
|
||||
} from '@/api/article'
|
||||
import { getFetcherArticleById } from '@/api/fetcher'
|
||||
|
||||
// 组件定义
|
||||
defineOptions({
|
||||
name: 'ArticleEdit',
|
||||
})
|
||||
const emit = defineEmits(['on-save', 'on-close'])
|
||||
const props = defineProps({
|
||||
title: { type: String, default: '' }
|
||||
})
|
||||
|
||||
const showDrawer = ref(false)
|
||||
const showErrMessage = ref('')
|
||||
const fullscreenLoading = ref(true)
|
||||
const isEdit = ref(false) // 确定是修改还是增加 isEdit = true 为修改,否则为增加
|
||||
const fetcherArticleId = ref(0) // 记录从抓取库导入的文章id
|
||||
const elEditFormRef = ref()
|
||||
const checkCategories = (rule, value, callback) => {
|
||||
if (categoryIdList.value.length === 0) {
|
||||
callback(new Error(rule.message))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
const checkChannels = (rule, value, callback) => {
|
||||
if (channelIdList.value.length === 0) {
|
||||
callback(new Error(rule.message))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
const formRules = reactive({
|
||||
articleType: [
|
||||
{ required: true, message: '请输选择文章类型', trigger: 'blur' }
|
||||
],
|
||||
title: [
|
||||
{ required: true, message: '请输入文章名称', trigger: 'blur' }
|
||||
],
|
||||
subtitle: [
|
||||
{ required: true, message: '请输入文章副标题', trigger: 'blur' }
|
||||
],
|
||||
categories: [
|
||||
{ validator: checkCategories, message: '请选择文章分类', trigger: 'blur' }
|
||||
],
|
||||
channels: [
|
||||
{ validator: checkChannels, message: '请选择所属栏目', trigger: 'blur' }
|
||||
],
|
||||
content: [
|
||||
{ required: true, message: '请输入文章内容', trigger: 'blur' }
|
||||
],
|
||||
})
|
||||
const articleTypeOptions = ref([
|
||||
{ key: 1, label: '图文' },
|
||||
{ key: 2, label: '视频' },
|
||||
])
|
||||
const editForm = ref({
|
||||
ID: 0,
|
||||
articleType: '',
|
||||
title: '',
|
||||
subtitle: '',
|
||||
author: '',
|
||||
channels: [],
|
||||
categories: [],
|
||||
desc: '',
|
||||
source: '',
|
||||
url: '',
|
||||
publishDate: '',
|
||||
articleTime: '',
|
||||
tags: '',
|
||||
imgs: '',
|
||||
})
|
||||
// options 相关
|
||||
const initOptions = (data, optionsData) => {
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 以下为 options 使用
|
||||
const channelOptions = ref([])
|
||||
const categoryOptions = ref([])
|
||||
const sourceOptions = 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 () => {
|
||||
sourceOptions.value = []
|
||||
const res = await getSourceList({ page: 1, pageSize: 999 })
|
||||
if (res.code === 0) {
|
||||
sourceOptions.value = res.data.list && res.data.list.map(item => item.name)
|
||||
}
|
||||
}
|
||||
|
||||
const emptyForm = () => {
|
||||
channelIdList.value = []
|
||||
categoryIdList.value = []
|
||||
imgFileList.value = []
|
||||
tagList.value = []
|
||||
editForm.value = {
|
||||
ID: 0,
|
||||
articleType: '',
|
||||
title: '',
|
||||
subtitle: '',
|
||||
author: '',
|
||||
channels: [],
|
||||
categories: [],
|
||||
desc: '',
|
||||
source: '',
|
||||
url: '',
|
||||
publishDate: '',
|
||||
articleTime: '',
|
||||
tags: '',
|
||||
imgs: '',
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化方法
|
||||
const openPage = async (params) => {
|
||||
showDrawer.value = true
|
||||
isEdit.value = false
|
||||
|
||||
emptyForm()
|
||||
getCategoryData()
|
||||
getChannelData()
|
||||
getSourceData()
|
||||
|
||||
// 建议通过url传参获取目标数据ID 调用 find方法进行查询数据操作 从而决定本页面是create还是update 以下为id作为url参数示例
|
||||
if (params.articleId && params.articleId > 0) {
|
||||
// 从文章库编辑
|
||||
isEdit.value = true
|
||||
initFormByArticle(params.articleId)
|
||||
} else if (params.fetcherId && params.fetcherId > 0) {
|
||||
// 从爬虫库导入
|
||||
initFormByFetcher(params.fetcherId)
|
||||
} else {
|
||||
// 新建
|
||||
}
|
||||
fullscreenLoading.value = false
|
||||
}
|
||||
|
||||
const initFormByArticle = async (id) => {
|
||||
const res = await getArticleById({ ID: id })
|
||||
if (res.code === 0) {
|
||||
// 初始化表单数据
|
||||
editForm.value = res.data.article
|
||||
// 初始化关联数据
|
||||
const { categories, channels, imgs, tags } = res.data.article
|
||||
categoryIdList.value = categories && categories.map(item => String(item.ID))
|
||||
channelIdList.value = channels && channels.map(item => String(item.ID))
|
||||
if (imgs && (imgs !== '')) {
|
||||
imgFileList.value = JSON.parse(imgs)
|
||||
}
|
||||
if (tags !== '') {
|
||||
tagList.value = tags.split(',')
|
||||
}
|
||||
fullscreenLoading.value = false
|
||||
} else {
|
||||
ElMessage({
|
||||
type: 'error',
|
||||
message: '获取数据失败'
|
||||
})
|
||||
showErrMessage.value = '获取数据失败'
|
||||
}
|
||||
}
|
||||
|
||||
// 从爬虫数据中获取内容并填充至文章内容中
|
||||
const initFormByFetcher = async (id) => {
|
||||
const res = await getFetcherArticleById({ ID: id })
|
||||
if (res.code === 0 && res.data && res.data.article) {
|
||||
fetcherArticleId.value = id
|
||||
const { title, author, source, content, publicTime } = res.data.article
|
||||
editForm.value.title = title
|
||||
editForm.value.subtitle = title
|
||||
editForm.value.author = author
|
||||
editForm.value.source = source
|
||||
editForm.value.content = content
|
||||
editForm.value.publishDate = publicTime
|
||||
editForm.value.articleTime = publicTime
|
||||
editForm.value.articleType = 1
|
||||
} else {
|
||||
ElMessage({
|
||||
type: 'error',
|
||||
message: '获取数据失败'
|
||||
})
|
||||
showErrMessage.value = '获取数据失败'
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 以下为图片相关操作 ----
|
||||
const chooseImg = ref(null)
|
||||
const imgFileList = ref([])
|
||||
const handleChooseImg = () => {
|
||||
chooseImg.value.open()
|
||||
return false
|
||||
}
|
||||
const handleSelectImg = (file) => {
|
||||
imgFileList.value.push({
|
||||
key: file.key,
|
||||
name: file.name,
|
||||
url: file.url,
|
||||
})
|
||||
}
|
||||
const handleImgRemove = (file) => {
|
||||
imgFileList.value = imgFileList.value.filter(item => item.key !== file.key)
|
||||
}
|
||||
const handleImgUpload = (file) => {
|
||||
imgFileList.value.push({
|
||||
key: file.key,
|
||||
name: file.name,
|
||||
url: file.url,
|
||||
})
|
||||
}
|
||||
|
||||
// ---- 以下为tag相关操作 ----
|
||||
const tagInputFlag = ref(false)
|
||||
const selectTag = ref('')
|
||||
const tagList = ref([])
|
||||
const tagOptions = ref([])
|
||||
const loadingTag = ref(false)
|
||||
const loadRemoteTag = (query) => {
|
||||
if (query) {
|
||||
loadingTag.value = true
|
||||
setTimeout(async () => {
|
||||
const res = await getTagList({ page: 1, pageSize: 30, keyword: query })
|
||||
if (res.code === 0) {
|
||||
tagOptions.value = res.data.list.map(item => item.name)
|
||||
}
|
||||
loadingTag.value = false
|
||||
}, 200)
|
||||
} else {
|
||||
tagOptions.value = []
|
||||
}
|
||||
}
|
||||
const handleSelectTag = (val) => {
|
||||
if (!tagList.value.includes(val)) {
|
||||
tagList.value.push(val)
|
||||
}
|
||||
tagInputFlag.value = false
|
||||
selectTag.value = ''
|
||||
}
|
||||
const categoryIdList = ref([])
|
||||
const channelIdList = ref([])
|
||||
|
||||
// 添加/保存修改
|
||||
const handleFormSubmit = async valid => {
|
||||
if (editForm.value.publishDate === '') {
|
||||
editForm.value.publishDate = null
|
||||
}
|
||||
if (editForm.value.articleTime === '') {
|
||||
editForm.value.articleTime = null
|
||||
}
|
||||
elEditFormRef.value.validate(async valid => {
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
const rel = {
|
||||
categoryIds: categoryIdList.value.map(item => parseInt(item, 10)),
|
||||
channelIds: channelIdList.value.map(item => parseInt(item, 10)),
|
||||
imgList: imgFileList.value,
|
||||
tagList: tagList.value,
|
||||
fetcherArticleId: fetcherArticleId.value
|
||||
}
|
||||
let res
|
||||
if (isEdit.value) {
|
||||
res = await updateArticle({ article: editForm.value, rel: rel })
|
||||
} else {
|
||||
res = await addArticle({ article: editForm.value, rel: rel })
|
||||
}
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: isEdit.value ? '修改成功' : '添加成功!'
|
||||
})
|
||||
showDrawer.value = false
|
||||
emit('on-save')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 返回按钮
|
||||
const handleFormClose = () => {
|
||||
showDrawer.value = false
|
||||
emit('on-close')
|
||||
}
|
||||
|
||||
// 对外暴露方法
|
||||
defineExpose({ openPage })
|
||||
</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;
|
||||
}
|
||||
|
||||
.img-item {
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -17,6 +17,11 @@
|
|||
<el-date-picker v-model="searchInfo.dateRange" type="daterange" value-format="YYYY-MM-DD" :clearable="false"
|
||||
:editable="false" />
|
||||
</el-form-item>
|
||||
<!-- <el-form-item prop="state" style="width:200px">
|
||||
<el-select v-model="searchInfo.state" 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 label="关键词" style="width:300px">
|
||||
<template #label>
|
||||
<span>
|
||||
|
|
@ -40,8 +45,8 @@
|
|||
<!-- 由于此处菜单跟左侧列表一一对应所以不需要分页 pageSize默认999 -->
|
||||
<el-table ref="multipleTable" :data="tableData" row-key="ID">
|
||||
<el-table-column align="left" label="ID" min-width="60" prop="ID" />
|
||||
<el-table-column align="left" label="标题" min-width="270" prop="title" />
|
||||
<el-table-column align="left" label="来源" min-width="120" prop="source">
|
||||
<el-table-column align="left" label="标题" min-width="300" prop="title" />
|
||||
<el-table-column align="left" label="来源" min-width="240" prop="source">
|
||||
<template #default="scope">
|
||||
<a :href="scope.row.sourceUrl" target="_blank">{{ scope.row.source }}</a>
|
||||
</template>
|
||||
|
|
@ -52,6 +57,11 @@
|
|||
{{ formatDate(scope.row.createTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="状态" min-width="80" prop="state">
|
||||
<template #default="scope">
|
||||
{{ formatStatus(scope.row.state) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="发布时间" min-width="120" prop="createtime">
|
||||
<template #default="scope">
|
||||
{{ formatDate(scope.row.publicTime) }}
|
||||
|
|
@ -60,8 +70,8 @@
|
|||
<el-table-column align="left" fixed="right" label="操作" width="80">
|
||||
<template #default="scope">
|
||||
<div>
|
||||
<el-button type="primary" link icon="plus" :disabled="scope.row.status === 2"
|
||||
@click="handleRowEdit(scope.row.ID)">发布</el-button>
|
||||
<el-button :type="scope.row.state === 1 ? 'info' : 'primary'" link icon="plus"
|
||||
:disabled="scope.row.state === 1" @click="handleRowEdit(scope.row.ID)">导入</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
|
@ -72,17 +82,20 @@
|
|||
@size-change="handleSizeChange" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ArticleEdit ref="articleEditRef" title="导入文章" @on-save="handlerSaveArticle" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { formatDate } from '@/utils/format'
|
||||
import { formatTimeToStr } from '@/utils/date'
|
||||
import { getFetcherArticleList } from '@/api/fetcher'
|
||||
import ArticleEdit from '@/view/content/components/articleEdit.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const showDrawer = ref(false)
|
||||
const articleEditRef = ref(null)
|
||||
|
||||
const page = ref(1)
|
||||
const total = ref(0)
|
||||
|
|
@ -90,6 +103,15 @@ const pageSize = ref(10)
|
|||
const searchInfo = ref({})
|
||||
const tableData = ref([])
|
||||
const elSearchFormRef = ref()
|
||||
const statusOptions = ref([
|
||||
{ key: 0, label: '未导入' },
|
||||
{ key: 1, label: '已导入' },
|
||||
])
|
||||
|
||||
const formatStatus = (value) => {
|
||||
const rowLabel = statusOptions.value.filter(item => item.key === value)
|
||||
return rowLabel && rowLabel[0] && rowLabel[0].label
|
||||
}
|
||||
|
||||
const initSearchInfo = () => {
|
||||
const endDate = new Date()
|
||||
|
|
@ -98,7 +120,7 @@ const initSearchInfo = () => {
|
|||
dateRange: [
|
||||
formatTimeToStr(startDate, 'yyyy-MM-dd'),
|
||||
formatTimeToStr(endDate, 'yyyy-MM-dd'),
|
||||
]
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -132,13 +154,16 @@ const handleCurrentChange = (val) => {
|
|||
|
||||
// 修改
|
||||
const handleRowEdit = (ID) => {
|
||||
const query = { fetcherId: ID }
|
||||
router.push({ name: 'articleEdit', query })
|
||||
// const query = { fetcherId: ID }
|
||||
// router.push({ name: 'articleEdit', query })
|
||||
showDrawer.value = true
|
||||
articleEditRef.value.openPage({ fetcherId: ID })
|
||||
}
|
||||
|
||||
// ----- 查询 -----
|
||||
const getTableData = async () => {
|
||||
const res = await getFetcherArticleList({ page: page.value, pageSize: pageSize.value, ...searchInfo.value })
|
||||
const state = 0
|
||||
const res = await getFetcherArticleList({ page: page.value, pageSize: pageSize.value, ...searchInfo.value, state })
|
||||
if (res.code === 0) {
|
||||
tableData.value = res.data.list
|
||||
total.value = res.data.total
|
||||
|
|
@ -146,6 +171,11 @@ const getTableData = async () => {
|
|||
}
|
||||
getTableData()
|
||||
|
||||
// 保存文章后
|
||||
const handlerSaveArticle = () => {
|
||||
getTableData()
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style type="scss">
|
||||
|
|
|
|||
Loading…
Reference in New Issue