|
@@ -0,0 +1,416 @@
|
|
|
|
+<script setup>
|
|
|
|
+import {
|
|
|
|
+ Edit,
|
|
|
|
+ Delete
|
|
|
|
+} from '@element-plus/icons-vue'
|
|
|
|
+
|
|
|
|
+import { ref } from 'vue'
|
|
|
|
+
|
|
|
|
+//文章标签数据模型
|
|
|
|
+const categorys = ref([])
|
|
|
|
+
|
|
|
|
+//用户搜索时选中的分类id
|
|
|
|
+const categoryId=ref('')
|
|
|
|
+
|
|
|
|
+//用户搜索时选中的发布状态
|
|
|
|
+const state=ref('')
|
|
|
|
+
|
|
|
|
+//文章列表数据模型
|
|
|
|
+const articles = ref([])
|
|
|
|
+
|
|
|
|
+const productList = ref([
|
|
|
|
+ { id: '1', label: '工时管家' },
|
|
|
|
+ { id: '2', label: '随访管家' },
|
|
|
|
+ { id: '3', label: '项目管家' },
|
|
|
|
+ { id: '4', label: '客户管家' },
|
|
|
|
+ { id: '5', label: '生产车间管家' },
|
|
|
|
+])
|
|
|
|
+
|
|
|
|
+//分页条数据模型
|
|
|
|
+const pageNum = ref(1)//当前页
|
|
|
|
+const total = ref(20)//总条数
|
|
|
|
+const pageSize = ref(10)//每页条数
|
|
|
|
+
|
|
|
|
+//当每页条数发生了变化,调用此函数
|
|
|
|
+const onSizeChange = (size) => {
|
|
|
|
+ pageSize.value = size
|
|
|
|
+ articleList();
|
|
|
|
+}
|
|
|
|
+//当前页码发生变化,调用此函数
|
|
|
|
+const onCurrentChange = (num) => {
|
|
|
|
+ pageNum.value = num
|
|
|
|
+ articleList();
|
|
|
|
+}
|
|
|
|
+//回显文章标签
|
|
|
|
+import {articleCategoryListService, articleListService,articleAddService, articleDeleteService} from '@/api/article.js'
|
|
|
|
+const articleCategoryList=async()=>{
|
|
|
|
+ let result = await articleCategoryListService();
|
|
|
|
+ categorys.value=result.data;
|
|
|
|
+}
|
|
|
|
+articleCategoryList();
|
|
|
|
+
|
|
|
|
+//获取文章列表数据
|
|
|
|
+const articleList=async()=>{
|
|
|
|
+ let params={
|
|
|
|
+ pageNum:pageNum.value,
|
|
|
|
+ pageSize:pageSize.value,
|
|
|
|
+ categoryId:categoryId.value?categoryId.value:null,
|
|
|
|
+ state:state.value?state.value:null
|
|
|
|
+ }
|
|
|
|
+ let result =await articleListService(params);
|
|
|
|
+ //渲染视图
|
|
|
|
+ total.value=result.data.total;
|
|
|
|
+ articles.value=result.data.items;
|
|
|
|
+
|
|
|
|
+ //处理数据,给数据模型扩展一个属性 categoryName ,分类名称
|
|
|
|
+ for(let i=0;i<articles.value.length;i++){
|
|
|
|
+ let article=articles.value[i];
|
|
|
|
+ for(let j=0;j<categorys.value.length;j++){
|
|
|
|
+ if(article.categoryId==categorys.value[j].id){
|
|
|
|
+ article.categoryName=categorys.value[j].categoryName
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+articleList()
|
|
|
|
+
|
|
|
|
+// 添加文章功能
|
|
|
|
+import {Plus} from '@element-plus/icons-vue'
|
|
|
|
+//控制抽屉是否显示
|
|
|
|
+const visibleDrawer = ref(false)
|
|
|
|
+//添加表单数据模型
|
|
|
|
+const articleModel = ref({
|
|
|
|
+ title: '',
|
|
|
|
+ categoryId: '',
|
|
|
|
+ coverImg: '',
|
|
|
|
+ content:'',
|
|
|
|
+ state:'',
|
|
|
|
+ profile: '',
|
|
|
|
+ productId: ''
|
|
|
|
+})
|
|
|
|
+const fileList = ref([])
|
|
|
|
+
|
|
|
|
+// 文章编辑器组件
|
|
|
|
+import { QuillEditor } from '@vueup/vue-quill'
|
|
|
|
+import '@vueup/vue-quill/dist/vue-quill.snow.css'
|
|
|
|
+import { options } from '../../utils/quillEditorOptions'
|
|
|
|
+
|
|
|
|
+//导入token,方便编辑完文章后保存至服务器
|
|
|
|
+import {userTokenStore} from '@/stores/token.js'
|
|
|
|
+const tokenStore=userTokenStore();
|
|
|
|
+
|
|
|
|
+//上传成功的回调函数
|
|
|
|
+const uploadSuccess=(result)=>{
|
|
|
|
+ //将服务器响应的图片地址赋值给 articleModel 的 coverImg
|
|
|
|
+ articleModel.value.coverImg=result.data;
|
|
|
|
+ console.log(result.data)
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+const fileExceedsLimit = () => {
|
|
|
|
+ ElMessage.warning('只能上传一个文章封面')
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// 添加文章
|
|
|
|
+import {ElMessage, ElMessageBox} from 'element-plus'
|
|
|
|
+const addArticle=async(clickState)=>{
|
|
|
|
+ // 把发布文章yes或草稿no赋值给数据模型
|
|
|
|
+ articleModel.value.state=clickState;
|
|
|
|
+ const { content, title, profile, categoryId, productId } = articleModel.value
|
|
|
|
+ let str = ''
|
|
|
|
+ if(!content) {
|
|
|
|
+ str += '文章内容不能为空,'
|
|
|
|
+ }
|
|
|
|
+ if(!title) {
|
|
|
|
+ str += '文章标题不能为空,'
|
|
|
|
+ }
|
|
|
|
+ if(!profile) {
|
|
|
|
+ str += '文章封面不能为空,'
|
|
|
|
+ }
|
|
|
|
+ if(!categoryId || categoryId.length <= 0) {
|
|
|
|
+ str += '文章标签不能为空,'
|
|
|
|
+ }
|
|
|
|
+ if(fileList.value.length <= 0) {
|
|
|
|
+ str += '文章封面不能为空,'
|
|
|
|
+ }
|
|
|
|
+ if(!productId || productId == '0') {
|
|
|
|
+ str += '文章所属产品不能为空,'
|
|
|
|
+ }
|
|
|
|
+ if(str) {
|
|
|
|
+ ElMessage.warning(str)
|
|
|
|
+ return
|
|
|
|
+ }
|
|
|
|
+ const formVla = { ...articleModel.value, categoryIds: (articleModel.value.categoryId || []).join(',') }
|
|
|
|
+ delete formVla.categoryId
|
|
|
|
+ const formData = new FormData()
|
|
|
|
+ for (const key in formVla) {
|
|
|
|
+ if(key == 'coverImg') {
|
|
|
|
+ let file = fileList.value[0].raw
|
|
|
|
+ if(!isFile(file)) {
|
|
|
|
+ file = base64ToFile(file, 'image.png')
|
|
|
|
+ }
|
|
|
|
+ formData.append('coverImage', file)
|
|
|
|
+ } else {
|
|
|
|
+ formData.append(key, formVla[key])
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ //调用接口
|
|
|
|
+ // let result=await articleAddService(articleModel.value)
|
|
|
|
+ let result=await articleAddService(formData)
|
|
|
|
+ ElMessage.success(result.msg?result.msg:"添加文章成功!")
|
|
|
|
+
|
|
|
|
+ //让添加文章的抽屉消失
|
|
|
|
+ visibleDrawer.value = false;
|
|
|
|
+ //刷新当前列表
|
|
|
|
+ articleList()
|
|
|
|
+}
|
|
|
|
+const isFile = (obj) => {
|
|
|
|
+ return obj instanceof File;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+const base64ToFile = (base64String, fileName) => {
|
|
|
|
+ const arr = base64String.split(',');
|
|
|
|
+ const mime = arr[0].match(/:(.*?);/)[1];
|
|
|
|
+ const bstr = atob(arr[1]);
|
|
|
|
+ let n = bstr.length;
|
|
|
|
+ const u8arr = new Uint8Array(n);
|
|
|
|
+
|
|
|
|
+ while (n--) {
|
|
|
|
+ u8arr[n] = bstr.charCodeAt(n);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return new File([u8arr], fileName, { type: mime });
|
|
|
|
+}
|
|
|
|
+// 修改文章
|
|
|
|
+const editArticle = (row) => {
|
|
|
|
+ const { categoryIds, content, title, profile, id, coverImg, productId } = row
|
|
|
|
+ articleModel.value = {
|
|
|
|
+ categoryId: categoryIds ? JSON.parse(categoryIds) : [],
|
|
|
|
+ content,
|
|
|
|
+ title,
|
|
|
|
+ profile,
|
|
|
|
+ id,
|
|
|
|
+ coverImg: '',
|
|
|
|
+ productId: productId
|
|
|
|
+ }
|
|
|
|
+ if(coverImg) {
|
|
|
|
+ fileList.value = [{
|
|
|
|
+ name: '图片',
|
|
|
|
+ url: `data:image/jpeg;base64, ${coverImg}`,
|
|
|
|
+ raw: `data:image/jpeg;base64, ${coverImg}`,
|
|
|
|
+ }]
|
|
|
|
+ } else {
|
|
|
|
+ fileList.value = []
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ visibleDrawer.value = true
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+const deleteArticle = async (row) => {
|
|
|
|
+ const { id, title } = row
|
|
|
|
+ ElMessageBox.confirm(
|
|
|
|
+ `确定删除【${title}】文章吗?`,
|
|
|
|
+ {
|
|
|
|
+ confirmButtonText: '确定',
|
|
|
|
+ cancelButtonText: '取消',
|
|
|
|
+ type: 'warning',
|
|
|
|
+ }
|
|
|
|
+ ).then(async () => {
|
|
|
|
+ let result=await articleDeleteService({ id })
|
|
|
|
+ ElMessage.success(result.message?result.message:"删除文章成功!")
|
|
|
|
+ articleList()
|
|
|
|
+ })
|
|
|
|
+}
|
|
|
|
+</script>
|
|
|
|
+<template>
|
|
|
|
+ <el-card class="page-container">
|
|
|
|
+ <template #header>
|
|
|
|
+ <div class="header">
|
|
|
|
+ <span>文章管理</span>
|
|
|
|
+ <div class="extra">
|
|
|
|
+ <el-button type="primary" @click="visibleDrawer=true">添加文章</el-button>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ </template>
|
|
|
|
+ <!-- 搜索表单 -->
|
|
|
|
+ <el-form inline>
|
|
|
|
+ <el-form-item label="文章标签:">
|
|
|
|
+ <el-select placeholder="请选择" v-model="categoryId">
|
|
|
|
+ <el-option
|
|
|
|
+ v-for="c in categorys"
|
|
|
|
+ :key="c.id"
|
|
|
|
+ :label="c.categoryName"
|
|
|
|
+ :value="c.id">
|
|
|
|
+ </el-option>
|
|
|
|
+ </el-select>
|
|
|
|
+ </el-form-item>
|
|
|
|
+
|
|
|
|
+ <el-form-item label="发布状态:">
|
|
|
|
+ <el-select placeholder="请选择" v-model="state">
|
|
|
|
+ <el-option label="已发布" value="yes"></el-option>
|
|
|
|
+ <el-option label="草稿" value="no"></el-option>
|
|
|
|
+ </el-select>
|
|
|
|
+ </el-form-item>
|
|
|
|
+ <el-form-item>
|
|
|
|
+ <el-button type="primary" @click="articleList">搜索</el-button>
|
|
|
|
+ <el-button @click="categoryId='';state=''">重置</el-button>
|
|
|
|
+ </el-form-item>
|
|
|
|
+ </el-form>
|
|
|
|
+ <!-- 文章列表 -->
|
|
|
|
+ <el-table :data="articles" style="width: 100%">
|
|
|
|
+ <el-table-column label="文章标题" width="400" prop="title"></el-table-column>
|
|
|
|
+ <el-table-column label="分类" prop="categoryNames"></el-table-column>
|
|
|
|
+ <el-table-column label="发表时间" prop="createTime"> </el-table-column>
|
|
|
|
+ <el-table-column label="状态" prop="state"></el-table-column>
|
|
|
|
+ <el-table-column label="操作" width="100">
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
+ <el-button :icon="Edit" circle plain type="primary" @click="editArticle(row)"></el-button>
|
|
|
|
+ <el-button :icon="Delete" circle plain type="danger" @click="deleteArticle(row)"></el-button>
|
|
|
|
+ </template>
|
|
|
|
+ </el-table-column>
|
|
|
|
+ <template #empty>
|
|
|
|
+ <el-empty description="没有数据" />
|
|
|
|
+ </template>
|
|
|
|
+ </el-table>
|
|
|
|
+ <!-- 分页条 -->
|
|
|
|
+ <el-pagination v-model:current-page="pageNum" v-model:page-size="pageSize" :page-sizes="[3, 5 ,10, 15]"
|
|
|
|
+ layout="jumper, total, sizes, prev, pager, next" background :total="total" @size-change="onSizeChange"
|
|
|
|
+ @current-change="onCurrentChange" style="margin-top: 20px; justify-content: flex-end" />
|
|
|
|
+
|
|
|
|
+ <!-- 抽屉 -->
|
|
|
|
+ <el-drawer v-model="visibleDrawer" :title="`${articleModel.id ? '编辑文章' : '添加文章'}`" direction="rtl" size="50%">
|
|
|
|
+ <!-- 添加文章表单 -->
|
|
|
|
+ <el-form :model="articleModel" label-width="100px" >
|
|
|
|
+ <el-form-item label="文章标题" >
|
|
|
|
+ <el-input v-model="articleModel.title" placeholder="请输入标题"></el-input>
|
|
|
|
+ </el-form-item>
|
|
|
|
+ <el-form-item label="文章简介" >
|
|
|
|
+ <el-input type="textarea"
|
|
|
|
+ :autosize="{ minRows: 2, maxRows: 4}" v-model="articleModel.profile" placeholder="请输入简介"></el-input>
|
|
|
|
+ </el-form-item>
|
|
|
|
+ <el-form-item label="文章标签">
|
|
|
|
+ <el-select placeholder="请选择" multiple v-model="articleModel.categoryId" style="width: 100%;">
|
|
|
|
+ <el-option v-for="c in categorys" :key="c.id" :label="c.categoryName" :value="c.id">
|
|
|
|
+ </el-option>
|
|
|
|
+ </el-select>
|
|
|
|
+ </el-form-item>
|
|
|
|
+ <el-form-item label="所属产品">
|
|
|
|
+ <el-select placeholder="请选择" v-model="articleModel.productId" style="width: 100%;">
|
|
|
|
+ <el-option v-for="c in productList" :key="c.id" :label="c.label" :value="c.id">
|
|
|
|
+ </el-option>
|
|
|
|
+ </el-select>
|
|
|
|
+ </el-form-item>
|
|
|
|
+ <el-form-item label="文章封面">
|
|
|
|
+ <!--element-plus 的文件上传组件
|
|
|
|
+ auto-upload:是否自动上传
|
|
|
|
+ action: 服务器接口路径
|
|
|
|
+ name: 上传的文件字段名,也就是参数名称
|
|
|
|
+ headers: 设置上传的请求头
|
|
|
|
+ on-success: 上传成功的回调函数
|
|
|
|
+ -->
|
|
|
|
+ <el-upload
|
|
|
|
+ v-model:file-list="fileList"
|
|
|
|
+ list-type="picture-card"
|
|
|
|
+ :auto-upload="false"
|
|
|
|
+ :limit="1"
|
|
|
|
+ :on-exceed="fileExceedsLimit"
|
|
|
|
+ >
|
|
|
|
+ <el-icon><Plus /></el-icon>
|
|
|
|
+ </el-upload>
|
|
|
|
+
|
|
|
|
+ </el-form-item>
|
|
|
|
+ <el-form-item label="文章内容">
|
|
|
|
+ <div class="editor">
|
|
|
|
+ <!--富文本编辑器组件-->
|
|
|
|
+ <quill-editor
|
|
|
|
+ theme="snow"
|
|
|
|
+ v-model:content="articleModel.content"
|
|
|
|
+ contentType="html"
|
|
|
|
+ :options="options"
|
|
|
|
+ >
|
|
|
|
+ </quill-editor>
|
|
|
|
+ </div>
|
|
|
|
+ </el-form-item>
|
|
|
|
+ <el-form-item>
|
|
|
|
+ <el-button type="primary" @click="addArticle('已发布')">发布</el-button>
|
|
|
|
+ <el-button type="info" @click="addArticle('草稿')">草稿</el-button>
|
|
|
|
+ </el-form-item>
|
|
|
|
+ </el-form>
|
|
|
|
+ </el-drawer>
|
|
|
|
+ </el-card>
|
|
|
|
+</template>
|
|
|
|
+<style lang="scss" scoped>
|
|
|
|
+.page-container {
|
|
|
|
+ min-height: 100%;
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
+
|
|
|
|
+ .header {
|
|
|
|
+ display: flex;
|
|
|
|
+ align-items: center;
|
|
|
|
+ justify-content: space-between;
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+/* 抽屉样式 */
|
|
|
|
+.avatar-uploader {
|
|
|
|
+ :deep() {
|
|
|
|
+ .avatar {
|
|
|
|
+ width: 178px;
|
|
|
|
+ height: 178px;
|
|
|
|
+ display: block;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ .el-upload {
|
|
|
|
+ border: 1px dashed var(--el-border-color);
|
|
|
|
+ border-radius: 6px;
|
|
|
|
+ cursor: pointer;
|
|
|
|
+ position: relative;
|
|
|
|
+ overflow: hidden;
|
|
|
|
+ transition: var(--el-transition-duration-fast);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ .el-upload:hover {
|
|
|
|
+ border-color: var(--el-color-primary);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ .el-icon.avatar-uploader-icon {
|
|
|
|
+ font-size: 28px;
|
|
|
|
+ color: #8c939d;
|
|
|
|
+ width: 178px;
|
|
|
|
+ height: 178px;
|
|
|
|
+ text-align: center;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+.editor {
|
|
|
|
+ width: 100%;
|
|
|
|
+ :deep(.ql-editor) {
|
|
|
|
+ min-height: 200px;
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.avatar-uploader .el-upload {
|
|
|
|
+ border: 1px dashed #d9d9d9;
|
|
|
|
+ border-radius: 6px;
|
|
|
|
+ cursor: pointer;
|
|
|
|
+ position: relative;
|
|
|
|
+ overflow: hidden;
|
|
|
|
+}
|
|
|
|
+.avatar-uploader .el-upload:hover {
|
|
|
|
+ border-color: #409EFF;
|
|
|
|
+}
|
|
|
|
+.avatar-uploader-icon {
|
|
|
|
+ font-size: 28px;
|
|
|
|
+ color: #8c939d;
|
|
|
|
+ width: 178px;
|
|
|
|
+ height: 178px;
|
|
|
|
+ line-height: 178px;
|
|
|
|
+ text-align: center;
|
|
|
|
+}
|
|
|
|
+.avatar {
|
|
|
|
+ width: 178px;
|
|
|
|
+ height: 178px;
|
|
|
|
+ display: block;
|
|
|
|
+}
|
|
|
|
+</style>
|