web-admin/src/view/content/components/articleEdit.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>