ArticleManage.vue 15 KB

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