ArticleManage.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483
  1. <script setup>
  2. import {
  3. Edit,
  4. Delete
  5. } from '@element-plus/icons-vue'
  6. // 文章编辑器组件
  7. import { QuillEditor, Quill } from "@vueup/vue-quill";
  8. import "@vueup/vue-quill/dist/vue-quill.snow.css";
  9. // import { ImageDrop } from 'quill-image-drop-module';
  10. // import ImageResize from 'quill-image-resize-module';
  11. // Quill.register('modules/ImageDrop', ImageDrop);
  12. // Quill.register('modules/imageResize', ImageResize);
  13. import { ref } from 'vue'
  14. //文章标签数据模型
  15. const categorys = ref([])
  16. //用户搜索时选中的分类id
  17. const categoryId=ref('')
  18. //用户搜索时选中的发布状态
  19. const state=ref('')
  20. //文章列表数据模型
  21. const articles = ref([])
  22. const productList = ref([
  23. { id: '1', label: '工时管家' },
  24. { id: '2', label: '随访管家' },
  25. { id: '3', label: '项目管家' },
  26. { id: '4', label: '客户管家' },
  27. { id: '5', label: '生产车间管家' },
  28. ])
  29. const quilleditorKey = ref(1)
  30. //分页条数据模型
  31. const pageNum = ref(1)//当前页
  32. const total = ref(20)//总条数
  33. const pageSize = ref(3)//每页条数
  34. //当每页条数发生了变化,调用此函数
  35. const onSizeChange = (size) => {
  36. pageSize.value = size
  37. articleList();
  38. }
  39. //当前页码发生变化,调用此函数
  40. const onCurrentChange = (num) => {
  41. pageNum.value = num
  42. articleList();
  43. }
  44. //回显文章标签
  45. import {articleCategoryListService, articleListService,articleAddService, articleDeleteService,uploadFileService} from '@/api/article.js'
  46. const articleCategoryList=async()=>{
  47. let result = await articleCategoryListService();
  48. categorys.value=result.data;
  49. }
  50. articleCategoryList();
  51. //获取文章列表数据
  52. const articleList=async()=>{
  53. let params={
  54. pageNum:pageNum.value,
  55. pageSize:pageSize.value,
  56. categoryId:categoryId.value?categoryId.value:null,
  57. state:state.value?state.value:null
  58. }
  59. let result =await articleListService(params);
  60. //渲染视图
  61. total.value=result.data.total;
  62. articles.value=result.data.items;
  63. //处理数据,给数据模型扩展一个属性 categoryName ,分类名称
  64. for(let i=0;i<articles.value.length;i++){
  65. let article=articles.value[i];
  66. for(let j=0;j<categorys.value.length;j++){
  67. if(article.categoryId==categorys.value[j].id){
  68. article.categoryName=categorys.value[j].categoryName
  69. }
  70. }
  71. }
  72. }
  73. articleList()
  74. // 添加文章功能
  75. import {Plus} from '@element-plus/icons-vue'
  76. //控制抽屉是否显示
  77. const visibleDrawer = ref(false)
  78. //添加表单数据模型
  79. const articleModel = ref({
  80. title: '',
  81. categoryId: '',
  82. coverImg: '',
  83. content:'',
  84. state:'',
  85. profile: '',
  86. productId: ''
  87. })
  88. const fileList = ref([])
  89. const options = ref({
  90. theme: "snow",
  91. bounds: document.body,
  92. debug: "warn",
  93. modules: {
  94. // 工具栏配置
  95. toolbar: [
  96. ["bold", "italic", "underline", "strike"], // 加粗 斜体 下划线 删除线
  97. ["blockquote", "code-block"], // 引用 代码块
  98. [{ list: "ordered" }, { list: "bullet" }], // 有序、无序列表
  99. [{ indent: "-1" }, { indent: "+1" }], // 缩进
  100. [{ size: ["small", false, "large", "huge"] }], // 字体大小
  101. [{ header: [1, 2, 3, 4, 5, 6, false] }], // 标题
  102. [{ color: [] }, { background: [] }], // 字体颜色、字体背景颜色
  103. [{ align: [] }], // 对齐方式
  104. ["clean"], // 清除文本格式
  105. ["link", "image"] // 链接、图片、视频
  106. ],
  107. // ImageDrop: true, // PS:因为QuillEditor自带可拖拽此配置可以不开,开启拖动会复制图片
  108. // todo 富文本导入图片是否需要缩放拖拽
  109. // imageResize: {
  110. // displayStyles: {
  111. // backgroundColor: 'black',
  112. // border: 'none',
  113. // color: 'white',
  114. // },
  115. // modules: ['Resize', 'DisplaySize', 'Toolbar'],
  116. // },
  117. },
  118. placeholder: "请输入内容",
  119. // readOnly: props.readOnly
  120. });
  121. //导入token,方便编辑完文章后保存至服务器
  122. import {userTokenStore} from '@/stores/token.js'
  123. const tokenStore=userTokenStore();
  124. //上传成功的回调函数
  125. const uploadSuccess=(result)=>{
  126. //将服务器响应的图片地址赋值给 articleModel 的 coverImg
  127. articleModel.value.coverImg=result.data;
  128. console.log(result.data)
  129. }
  130. const fileExceedsLimit = () => {
  131. ElMessage.warning('只能上传一个文章封面')
  132. }
  133. // 添加文章
  134. import {ElMessage, ElMessageBox} from 'element-plus'
  135. const addArticle=async(clickState)=>{
  136. // 把发布文章yes或草稿no赋值给数据模型
  137. articleModel.value.state=clickState;
  138. const { content, title, profile, categoryId, productId } = articleModel.value
  139. let str = ''
  140. if(!content) {
  141. str += '文章内容不能为空,'
  142. }
  143. if(!title) {
  144. str += '文章标题不能为空,'
  145. }
  146. if(!profile) {
  147. str += '文章封面不能为空,'
  148. }
  149. if(!categoryId || categoryId.length <= 0) {
  150. str += '文章标签不能为空,'
  151. }
  152. if(fileList.value.length <= 0) {
  153. str += '文章封面不能为空,'
  154. }
  155. if(!productId || productId == '0') {
  156. str += '文章所属产品不能为空,'
  157. }
  158. if(str) {
  159. ElMessage.warning(str)
  160. return
  161. }
  162. const formVla = { ...articleModel.value, categoryIds: (articleModel.value.categoryId || []).join(',') }
  163. delete formVla.categoryId
  164. const formData = new FormData()
  165. for (const key in formVla) {
  166. if(key == 'coverImg') {
  167. let file = fileList.value[0].raw
  168. // if(!isFile(file)) {
  169. // file = base64ToFile(file, 'image.png')
  170. // }
  171. formData.append('coverImage', file)
  172. } else {
  173. formData.append(key, formVla[key])
  174. }
  175. }
  176. //调用接口
  177. // let result=await articleAddService(articleModel.value)
  178. let result=await articleAddService(formData)
  179. ElMessage.success(result.msg?result.msg:"添加文章成功!")
  180. //让添加文章的抽屉消失
  181. visibleDrawer.value = false;
  182. //刷新当前列表
  183. articleList()
  184. }
  185. const isFile = (obj) => {
  186. return obj instanceof File;
  187. }
  188. // 图片上传
  189. const fileUploadArticle = async (row) => {
  190. const file = row.file
  191. const formData = new FormData()
  192. formData.append('multipartFile', file)
  193. const res = await uploadFileService(formData)
  194. console.log(res, '<===== 上传后的文件')
  195. fileList.value = [{
  196. name: '图片',
  197. url: res.data,
  198. raw: res.data
  199. }]
  200. }
  201. const base64ToFile = (base64String, fileName) => {
  202. const arr = base64String.split(',');
  203. const mime = arr[0].match(/:(.*?);/)[1];
  204. const bstr = atob(arr[1]);
  205. let n = bstr.length;
  206. const u8arr = new Uint8Array(n);
  207. while (n--) {
  208. u8arr[n] = bstr.charCodeAt(n);
  209. }
  210. return new File([u8arr], fileName, { type: mime });
  211. }
  212. const addArticleAdministration = () => {
  213. articleModel.value = {
  214. categoryId: [],
  215. content: ' ',
  216. title: '',
  217. profile: '',
  218. coverImg: '',
  219. productId: '',
  220. }
  221. fileList.value = []
  222. quilleditorKey.value = quilleditorKey.value++
  223. visibleDrawer.value = true
  224. }
  225. // 修改文章
  226. const editArticle = (row) => {
  227. const { categoryIds, content, title, profile, id, coverImg, coverImgUrl, productId } = row
  228. articleModel.value = {
  229. categoryId: categoryIds ? JSON.parse(categoryIds) : [],
  230. content,
  231. title,
  232. profile,
  233. id,
  234. coverImg: '',
  235. productId: productId
  236. }
  237. if(coverImgUrl) {
  238. fileList.value = [{
  239. name: '图片',
  240. // url: `data:image/jpeg;base64, ${coverImg}`,
  241. // raw: `data:image/jpeg;base64, ${coverImg}`,
  242. url: `${coverImgUrl}`,
  243. raw: `${coverImgUrl}`,
  244. }]
  245. } else {
  246. fileList.value = []
  247. }
  248. visibleDrawer.value = true
  249. }
  250. const deleteArticle = async (row) => {
  251. const { id, title } = row
  252. ElMessageBox.confirm(
  253. `确定删除【${title}】文章吗?`,
  254. {
  255. confirmButtonText: '确定',
  256. cancelButtonText: '取消',
  257. type: 'warning',
  258. }
  259. ).then(async () => {
  260. let result=await articleDeleteService({ id })
  261. ElMessage.success(result.message?result.message:"删除文章成功!")
  262. articleList()
  263. })
  264. }
  265. </script>
  266. <template>
  267. <el-card class="page-container">
  268. <template #header>
  269. <div class="header">
  270. <span>文章管理</span>
  271. <div class="extra">
  272. <el-button type="primary" @click="addArticleAdministration()">添加文章</el-button>
  273. </div>
  274. </div>
  275. </template>
  276. <!-- 搜索表单 -->
  277. <el-form inline>
  278. <el-form-item label="文章标签:">
  279. <el-select placeholder="请选择" v-model="categoryId">
  280. <el-option
  281. v-for="c in categorys"
  282. :key="c.id"
  283. :label="c.categoryName"
  284. :value="c.id">
  285. </el-option>
  286. </el-select>
  287. </el-form-item>
  288. <el-form-item label="发布状态:">
  289. <el-select placeholder="请选择" v-model="state">
  290. <el-option label="已发布" value="yes"></el-option>
  291. <el-option label="草稿" value="no"></el-option>
  292. </el-select>
  293. </el-form-item>
  294. <el-form-item>
  295. <el-button type="primary" @click="articleList">搜索</el-button>
  296. <el-button @click="categoryId='';state=''">重置</el-button>
  297. </el-form-item>
  298. </el-form>
  299. <!-- 文章列表 -->
  300. <el-table :data="articles" style="width: 100%">
  301. <el-table-column label="文章标题" width="400" prop="title"></el-table-column>
  302. <el-table-column label="分类" prop="categoryNames"></el-table-column>
  303. <el-table-column label="发表时间" prop="createTime"> </el-table-column>
  304. <el-table-column label="状态" prop="state"></el-table-column>
  305. <el-table-column label="操作" width="100">
  306. <template #default="{ row }">
  307. <el-button :icon="Edit" circle plain type="primary" @click="editArticle(row)"></el-button>
  308. <el-button :icon="Delete" circle plain type="danger" @click="deleteArticle(row)"></el-button>
  309. </template>
  310. </el-table-column>
  311. <template #empty>
  312. <el-empty description="没有数据" />
  313. </template>
  314. </el-table>
  315. <!-- 分页条 -->
  316. <el-pagination v-model:current-page="pageNum" v-model:page-size="pageSize" :page-sizes="[3, 5 ,10, 15]"
  317. layout="jumper, total, sizes, prev, pager, next" background :total="total" @size-change="onSizeChange"
  318. @current-change="onCurrentChange" style="margin-top: 20px; justify-content: flex-end" />
  319. <!-- 抽屉 -->
  320. <el-drawer v-model="visibleDrawer" :title="`${articleModel.id ? '编辑文章' : '添加文章'}`" direction="rtl" size="50%">
  321. <!-- 添加文章表单 -->
  322. <el-form :model="articleModel" label-width="100px" >
  323. <el-form-item label="文章标题" >
  324. <el-input v-model="articleModel.title" placeholder="请输入标题" :maxlength="100"></el-input>
  325. </el-form-item>
  326. <el-form-item label="文章简介" >
  327. <el-input type="textarea"
  328. :autosize="{ minRows: 2, maxRows: 4}" v-model="articleModel.profile" placeholder="请输入简介" :maxlength="250"></el-input>
  329. </el-form-item>
  330. <el-form-item label="文章标签">
  331. <el-select placeholder="请选择" multiple v-model="articleModel.categoryId" style="width: 100%;">
  332. <el-option v-for="c in categorys" :key="c.id" :label="c.categoryName" :value="c.id">
  333. </el-option>
  334. </el-select>
  335. </el-form-item>
  336. <el-form-item label="所属产品">
  337. <el-select placeholder="请选择" v-model="articleModel.productId" style="width: 100%;">
  338. <el-option v-for="c in productList" :key="c.id" :label="c.label" :value="c.id">
  339. </el-option>
  340. </el-select>
  341. </el-form-item>
  342. <el-form-item label="文章封面">
  343. <!--element-plus 的文件上传组件
  344. auto-upload:是否自动上传
  345. action: 服务器接口路径
  346. name: 上传的文件字段名,也就是参数名称
  347. headers: 设置上传的请求头
  348. on-success: 上传成功的回调函数
  349. -->
  350. <el-upload
  351. v-model:file-list="fileList"
  352. list-type="picture-card"
  353. :auto-upload="true"
  354. :limit="1"
  355. :on-exceed="fileExceedsLimit"
  356. :http-request="fileUploadArticle"
  357. >
  358. <el-icon><Plus /></el-icon>
  359. </el-upload>
  360. </el-form-item>
  361. <el-form-item label="文章内容">
  362. <div class="editor">
  363. <!--富文本编辑器组件-->
  364. <quill-editor
  365. theme="snow"
  366. v-model:content="articleModel.content"
  367. contentType="html"
  368. :options="options"
  369. :key="quilleditorKey"
  370. >
  371. </quill-editor>
  372. </div>
  373. </el-form-item>
  374. <el-form-item>
  375. <el-button type="primary" @click="addArticle('已发布')">发布</el-button>
  376. <el-button type="info" @click="addArticle('草稿')">草稿</el-button>
  377. </el-form-item>
  378. </el-form>
  379. </el-drawer>
  380. </el-card>
  381. </template>
  382. <style lang="scss" scoped>
  383. .page-container {
  384. min-height: 100%;
  385. box-sizing: border-box;
  386. .header {
  387. display: flex;
  388. align-items: center;
  389. justify-content: space-between;
  390. }
  391. }
  392. /* 抽屉样式 */
  393. .avatar-uploader {
  394. :deep() {
  395. .avatar {
  396. width: 178px;
  397. height: 178px;
  398. display: block;
  399. }
  400. .el-upload {
  401. border: 1px dashed var(--el-border-color);
  402. border-radius: 6px;
  403. cursor: pointer;
  404. position: relative;
  405. overflow: hidden;
  406. transition: var(--el-transition-duration-fast);
  407. }
  408. .el-upload:hover {
  409. border-color: var(--el-color-primary);
  410. }
  411. .el-icon.avatar-uploader-icon {
  412. font-size: 28px;
  413. color: #8c939d;
  414. width: 178px;
  415. height: 178px;
  416. text-align: center;
  417. }
  418. }
  419. }
  420. .editor {
  421. width: 100%;
  422. :deep(.ql-editor) {
  423. min-height: 200px;
  424. }
  425. }
  426. .avatar-uploader .el-upload {
  427. border: 1px dashed #d9d9d9;
  428. border-radius: 6px;
  429. cursor: pointer;
  430. position: relative;
  431. overflow: hidden;
  432. }
  433. .avatar-uploader .el-upload:hover {
  434. border-color: #409EFF;
  435. }
  436. .avatar-uploader-icon {
  437. font-size: 28px;
  438. color: #8c939d;
  439. width: 178px;
  440. height: 178px;
  441. line-height: 178px;
  442. text-align: center;
  443. }
  444. .avatar {
  445. width: 178px;
  446. height: 178px;
  447. display: block;
  448. }
  449. </style>