491 lines
16 KiB
Vue
491 lines
16 KiB
Vue
<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>
|