📝个人主页:哈__
期待您的关注
目录
一、🔥今日目标
二、🐻前端Vue模块的改造
BUG修改
1.wangeditor无法展示问题
2.弹窗无法正常关闭问题
2.1 添加admin-doc.vue
2.1.1 点击admin-ebook中的路由跳转到admin-doc
2.2.2 进入到admin-doc中调用初始化查询方法
2.2.3 文档编辑中的一些细节
2.2.4 文档的删除
2.2.5 admin-doc.vue全部代码
2.2 添加DocView.vue
2.3 添加新的路由规则
一、🔥今日目标
上一次带大家把前端的分类管理模块做了出来,我们可以实现网站的分类功能,以及分类的树形结构展示功能。到此为止已经带大家做了电子书管理模块、分类模块,那么只要再把文档管理模块也做出来,我们就可以初步实现电子书这整个一套流程了。我们可以编辑添加电子书,实现分类,并且真正的往电子书的文档模块中添加内容。【wiki知识库】05.分类管理实现--前端Vue模块-CSDN博客
我们今天就要实现下方图片中箭头指向的功能
除此之外,还有主页的展示功能。
二、🐻前端Vue模块的改造
在此之前我要要说一件事情,我在做这个模块的时候出现了问题,一个是我们之后要使用的文本编辑器wangeditor无法正常展示,还有一个是弹窗无法关闭的问题。这里我把解决方法告诉大家。
BUG修改
1.wangeditor无法展示问题
出现这个问题可能是版本的问题,进入到我们的web目录中,打开终端窗口然后输入下方指令。重新安装wangeditor。
npm i wangeditor@4.6.3 --save
2.弹窗无法正常关闭问题
这个问题是wangeditor和vue版本兼容的问题。我们需要修改一下package.json。将vue版本改成下方图中所示,注意前边的符号。
2.1 添加admin-doc.vue
2.1.1 点击admin-ebook中的路由跳转到admin-doc
还记得我当初在admin-ebook.vue中写的一个router吗?在我们点击文档管理跳转到对应的组件的时候,我们是有传一个ebookId进来的,我们这里使用的是路由传参。
2.2.2 进入到admin-doc中调用初始化查询方法
进入到这个页面呢调用了两个方法,一个是editor.create(),用于加载我们的文本编辑器,另外一个方法调用的是handleQuery()方法,向后端发送查询请求。
onMounted(() => { editor.create(); handleQuery(); });
这个方法进入的时候,修改了一个变量loading,我们在进行信息查询的时候会给用户一个反馈,告诉用户稍等,我们修改为true之后就会有页面数据加载的效果。
level1变量我们之前也说过,用于保存树形结构的数据。之后呢就会发送一个ajax请求,等我们的数据返回来之后就要把loading改为false了。
至于下边的treeSelectData,在我们修改一个文档的父文档的时候,如果我们要把这个文档作为根文档,也就是一级文档,我们需要把这个文档的父文档设置为无,但是我们的level中存储的是数据库中查出来的数据,没有无这个选项,所以我们新加一个变量来存储level1的结果和无,这样不会影响我们查出来的数据。
const handleQuery = () => { loading.value = true; // 如果不清空现有数据,则编辑保存重新加载数据后,再点编辑,则列表显示的还是编辑前的数据 level1.value = []; axios.get("/doc/all/" + route.query.ebookId).then((response) => { loading.value = false; const data = response.data; if (data.success) { docs.value = data.content; console.log("原始数组:", docs.value); level1.value = []; level1.value = Tool.array2Tree(docs.value, 0); console.log("树形结构:", level1); // 父文档下拉框初始化,相当于点击新增 treeSelectData.value = Tool.copy(level1.value) || []; // 为选择树添加一个"无" treeSelectData.value.unshift({id: 0, name: '无'}); } else { message.error(data.message); } }); };
2.2.3 文档编辑中的一些细节
在我们进行文档编辑的时候,我们不可能把该文档的父文档改为自己,或者改为它的子文档,这是一个循环错误。所以我们在修改一个文档的时候要把这个文档的子文档和自己设置为不可选中。
来看看这个方法,我们把树形结构的数据,还有我们要编辑的文档的id传进来,首先进行for循环去找到这个结点,然后我们把这个节点设置为不可见,然后我们去遍历这个节点的子节点,递归调用。
/** * 将某节点及其子孙节点全部置为disabled */ const setDisable = (treeSelectData: any, id: any) => { // console.log(treeSelectData, id); // 遍历数组,即遍历某一层节点 for (let i = 0; i < treeSelectData.length; i++) { const node = treeSelectData[i]; if (node.id === id) { // 如果当前节点就是目标节点 console.log("disabled", node); // 将目标节点设置为disabled node.disabled = true; // 遍历所有子节点,将所有子节点全部都加上disabled const children = node.children; if (Tool.isNotEmpty(children)) { for (let j = 0; j < children.length; j++) { setDisable(children, children[j].id) } } } else { // 如果当前节点不是目标节点,则到其子节点再找找看。 const children = node.children; if (Tool.isNotEmpty(children)) { setDisable(children, id); } } } };
2.2.4 文档的删除
文档的删除并不只是该文档简单删除就完了,这个文档删掉之后,这个文档的所有子文档都要删除。我们之前分类管理模块也处理过这样的删除,但是我们是后端处理的删除逻辑,后端处理起来呢比较麻烦,这里我们可以使用前端处理一下。既然要删除子分支,我们就把这个要删除的文档的子文档的id都查出来一起传到后端。
/** * 查找整根树枝 */ const getDeleteIds = (treeSelectData: any, id: any) => { // console.log(treeSelectData, id); // 遍历数组,即遍历某一层节点 for (let i = 0; i < treeSelectData.length; i++) { const node = treeSelectData[i]; if (node.id === id) { // 如果当前节点就是目标节点 console.log("delete", node); // 将目标ID放入结果集ids // node.disabled = true; deleteIds.push(id); deleteNames.push(node.name); // 遍历所有子节点 const children = node.children; if (Tool.isNotEmpty(children)) { for (let j = 0; j < children.length; j++) { getDeleteIds(children, children[j].id) } } } else { // 如果当前节点不是目标节点,则到其子节点再找找看。 const children = node.children; if (Tool.isNotEmpty(children)) { getDeleteIds(children, id); } } } };
2.2.5 admin-doc.vue全部代码
<template> <a-layout> <a-layout-content :style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }" > <a-row :gutter="24"> <a-col :span="8"> <p> <a-form layout="inline" :model="param"> <a-form-item> <a-button type="primary" @click="handleQuery()"> 查询 </a-button> </a-form-item> <a-form-item> <a-button type="primary" @click="add()"> 新增 </a-button> </a-form-item> </a-form> </p> <a-table v-if="level1.length > 0" :columns="columns" :row-key="record => record.id" :data-source="level1" :loading="loading" :pagination="false" size="small" :defaultExpandAllRows="true" > <template #name="{ text, record }"> {{record.sort}} {{text}} </template> <template v-slot:action="{ text, record }"> <a-space size="small"> <a-button type="primary" @click="edit(record)" size="small"> 编辑 </a-button> <a-popconfirm title="删除后不可恢复,确认删除?" ok-text="是" cancel-text="否" @confirm="handleDelete(record.id)" > <a-button type="danger" size="small"> 删除 </a-button> </a-popconfirm> </a-space> </template> </a-table> </a-col> <a-col :span="16"> <p> <a-form layout="inline" :model="param"> <a-form-item> <a-button type="primary" @click="handleSave()"> 保存 </a-button> </a-form-item> </a-form> </p> <a-form :model="doc" layout="vertical"> <a-form-item> <a-input v-model:value="doc.name" placeholder="名称"/> </a-form-item> <a-form-item> <a-tree-select v-model:value="doc.parent" style="width: 100%" :dropdown-style="{ maxHeight: '400px', overflow: 'auto' }" :tree-data="treeSelectData" placeholder="请选择父文档" tree-default-expand-all :replaceFields="{title: 'name', key: 'id', value: 'id'}" > </a-tree-select> </a-form-item> <a-form-item> <a-input v-model:value="doc.sort" placeholder="顺序"/> </a-form-item> <a-form-item> <a-button type="primary" @click="handlePreviewContent()"> <EyeOutlined /> 内容预览 </a-button> </a-form-item> <a-form-item> <div id="content"></div> </a-form-item> </a-form> </a-col> </a-row> <a-drawer width="900" placement="right" :closable="false" :visible="drawerVisible" @close="onDrawerClose"> <div class="wangeditor" :innerHTML="previewHtml"></div> </a-drawer> </a-layout-content> </a-layout> </template> <script lang="ts"> import { defineComponent, onMounted, ref, createVNode } from 'vue'; import axios from 'axios'; import {message, Modal} from 'ant-design-vue'; import {Tool} from "@/util/tool"; import {useRoute} from "vue-router"; import ExclamationCircleOutlined from "@ant-design/icons-vue/ExclamationCircleOutlined"; import E from 'wangeditor' export default defineComponent({ name: 'AdminDoc', setup() { const route = useRoute(); console.log("路由:", route); console.log("route.path:", route.path); console.log("route.query:", route.query); console.log("route.param:", route.params); console.log("route.fullPath:", route.fullPath); console.log("route.name:", route.name); console.log("route.meta:", route.meta); const param = ref(); param.value = {}; const docs = ref(); const loading = ref(false); // 因为树选择组件的属性状态,会随当前编辑的节点而变化,所以单独声明一个响应式变量 const treeSelectData = ref(); treeSelectData.value = []; const columns = [ { title: '名称', dataIndex: 'name', slots: { customRender: 'name' } }, { title: 'Action', key: 'action', slots: { customRender: 'action' } } ]; const level1 = ref(); // 一级文档树,children属性就是二级文档 level1.value = []; /** * 数据查询 **/ const handleQuery = () => { loading.value = true; // 如果不清空现有数据,则编辑保存重新加载数据后,再点编辑,则列表显示的还是编辑前的数据 level1.value = []; axios.get("/doc/all/" + route.query.ebookId).then((response) => { loading.value = false; const data = response.data; if (data.success) { docs.value = data.content; console.log("原始数组:", docs.value); level1.value = []; level1.value = Tool.array2Tree(docs.value, 0); console.log("树形结构:", level1); // 父文档下拉框初始化,相当于点击新增 treeSelectData.value = Tool.copy(level1.value) || []; // 为选择树添加一个"无" treeSelectData.value.unshift({id: 0, name: '无'}); } else { message.error(data.message); } }); }; // -------- 表单 --------- const doc = ref(); doc.value = { ebookId: route.query.ebookId }; const modalVisible = ref(false); const modalLoading = ref(false); const editor = new E('#content'); editor.config.zIndex = 0; // 显示上传图片按钮,转成Base64存储,同时也支持拖拽图片 editor.config.uploadImgShowBase64 = true; const handleSave = () => { modalLoading.value = true; doc.value.content = editor.txt.html(); axios.post("/doc/save", doc.value).then((response) => { modalLoading.value = false; const data = response.data; // data = commonResp if (data.success) { // modalVisible.value = false; message.success("保存成功!"); // 重新加载列表 handleQuery(); } else { message.error(data.message); } }); }; /** * 将某节点及其子孙节点全部置为disabled */ const setDisable = (treeSelectData: any, id: any) => { // console.log(treeSelectData, id); // 遍历数组,即遍历某一层节点 for (let i = 0; i < treeSelectData.length; i++) { const node = treeSelectData[i]; if (node.id === id) { // 如果当前节点就是目标节点 console.log("disabled", node); // 将目标节点设置为disabled node.disabled = true; // 遍历所有子节点,将所有子节点全部都加上disabled const children = node.children; if (Tool.isNotEmpty(children)) { for (let j = 0; j < children.length; j++) { setDisable(children, children[j].id) } } } else { // 如果当前节点不是目标节点,则到其子节点再找找看。 const children = node.children; if (Tool.isNotEmpty(children)) { setDisable(children, id); } } } }; const deleteIds: Array<string> = []; const deleteNames: Array<string> = []; /** * 查找整根树枝 */ const getDeleteIds = (treeSelectData: any, id: any) => { // console.log(treeSelectData, id); // 遍历数组,即遍历某一层节点 for (let i = 0; i < treeSelectData.length; i++) { const node = treeSelectData[i]; if (node.id === id) { // 如果当前节点就是目标节点 console.log("delete", node); // 将目标ID放入结果集ids // node.disabled = true; deleteIds.push(id); deleteNames.push(node.name); // 遍历所有子节点 const children = node.children; if (Tool.isNotEmpty(children)) { for (let j = 0; j < children.length; j++) { getDeleteIds(children, children[j].id) } } } else { // 如果当前节点不是目标节点,则到其子节点再找找看。 const children = node.children; if (Tool.isNotEmpty(children)) { getDeleteIds(children, id); } } } }; /** * 内容查询 **/ const handleQueryContent = () => { axios.get("/doc/find-content/" + doc.value.id).then((response) => { const data = response.data; if (data.success) { editor.txt.html(data.content) } else { message.error(data.message); } }); }; /** * 编辑 */ const edit = (record: any) => { // 清空富文本框 editor.txt.html(""); modalVisible.value = true; doc.value = Tool.copy(record); handleQueryContent(); // 不能选择当前节点及其所有子孙节点,作为父节点,会使树断开 treeSelectData.value = Tool.copy(level1.value); setDisable(treeSelectData.value, record.id); // 为选择树添加一个"无" treeSelectData.value.unshift({id: 0, name: '无'}); }; /** * 新增 */ const add = () => { // 清空富文本框 editor.txt.html(""); modalVisible.value = true; doc.value = { ebookId: route.query.ebookId }; treeSelectData.value = Tool.copy(level1.value) || []; // 为选择树添加一个"无" treeSelectData.value.unshift({id: 0, name: '无'}); }; const handleDelete = (id: number) => { // console.log(level1, level1.value, id) // 清空数组,否则多次删除时,数组会一直增加 deleteIds.length = 0; deleteNames.length = 0; getDeleteIds(level1.value, id); Modal.confirm({ title: '重要提醒', icon: createVNode(ExclamationCircleOutlined), content: '将删除:【' + deleteNames.join(",") + "】删除后不可恢复,确认删除?", onOk() { // console.log(ids) axios.delete("/doc/delete/" + deleteIds.join(",")).then((response) => { const data = response.data; // data = commonResp if (data.success) { // 重新加载列表 handleQuery(); } else { message.error(data.message); } }); }, }); }; // ----------------富文本预览-------------- const drawerVisible = ref(false); const previewHtml = ref(); const handlePreviewContent = () => { const html = editor.txt.html(); previewHtml.value = html; drawerVisible.value = true; }; const onDrawerClose = () => { drawerVisible.value = false; }; onMounted(() => { editor.create(); handleQuery(); }); return { param, // docs, level1, columns, loading, handleQuery, edit, add, doc, modalVisible, modalLoading, handleSave, handleDelete, treeSelectData, drawerVisible, previewHtml, handlePreviewContent, onDrawerClose, } } }); </script> <style scoped> img { width: 50px; height: 50px; } </style>
2.2 添加DocView.vue
这个组件呢就是为了在主页展示文档数据。但是其中的一些功能后端还未实现。
<template> <a-layout> <a-layout-content :style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"> <h3 v-if="level1.length === 0">对不起,找不到相关文档!</h3> <a-row> <a-col :span="6"> <a-tree v-if="level1.length > 0" :tree-data="level1" @select="onSelect" :replaceFields="{title: 'name', key: 'id', value: 'id'}" :defaultExpandAll="true" :defaultSelectedKeys="defaultSelectedKeys" > </a-tree> </a-col> <a-col :span="18"> <div> <h2>{{doc.name}}</h2> <div> <span>阅读数:{{doc.viewCount}}</span> <span>点赞数:{{doc.voteCount}}</span> </div> <a-divider style="height: 2px; background-color: #9999cc"/> </div> <div class="wangeditor" :innerHTML="html"></div> <div class="vote-div"> <a-button type="primary" shape="round" :size="'large'" @click="vote"> <template #icon><LikeOutlined /> 点赞:{{doc.voteCount}} </template> </a-button> </div> </a-col> </a-row> </a-layout-content> </a-layout> </template> <script lang="ts"> import { defineComponent, onMounted, ref, createVNode } from 'vue'; import axios from 'axios'; import {message} from 'ant-design-vue'; import {Tool} from "@/util/tool"; import {useRoute} from "vue-router"; export default defineComponent({ name: 'Doc', setup() { const route = useRoute(); const docs = ref(); const html = ref(); const defaultSelectedKeys = ref(); defaultSelectedKeys.value = []; // 当前选中的文档 const doc = ref(); doc.value = {}; const level1 = ref(); // 一级文档树,children属性就是二级文档 level1.value = []; /** * 内容查询 **/ const handleQueryContent = (id: number) => { axios.get("/doc/find-content/" + id).then((response) => { const data = response.data; if (data.success) { html.value = data.content; } else { message.error(data.message); } }); }; /** * 数据查询 **/ const handleQuery = () => { axios.get("/doc/all/" + route.query.ebookId).then((response) => { const data = response.data; if (data.success) { docs.value = data.content; level1.value = []; level1.value = Tool.array2Tree(docs.value, 0); if (Tool.isNotEmpty(level1)) { defaultSelectedKeys.value = [level1.value[0].id]; handleQueryContent(level1.value[0].id); // 初始显示文档信息 doc.value = level1.value[0]; } } else { message.error(data.message); } }); }; const onSelect = (selectedKeys: any, info: any) => { console.log('selected', selectedKeys, info); if (Tool.isNotEmpty(selectedKeys)) { // 选中某一节点时,加载该节点的文档信息 doc.value = info.selectedNodes[0].props; // 加载内容 handleQueryContent(selectedKeys[0]); } }; // 点赞 const vote = () => { axios.get('/doc/vote/' + doc.value.id).then((response) => { const data = response.data; if (data.success) { doc.value.voteCount++; } else { message.error(data.message); } }); }; onMounted(() => { handleQuery(); }); return { level1, html, onSelect, defaultSelectedKeys, doc, vote } } }); </script> <style> /* table 样式 */ .wangeditor table { border-top: 1px solid #ccc; border-left: 1px solid #ccc; } .wangeditor table td, .wangeditor table th { border-bottom: 1px solid #ccc; border-right: 1px solid #ccc; padding: 3px 5px; } .wangeditor table th { border-bottom: 2px solid #ccc; text-align: center; } /* blockquote 样式 */ .wangeditor blockquote { display: block; border-left: 8px solid #d0e5f2; padding: 5px 10px; margin: 10px 0; line-height: 1.4; font-size: 100%; background-color: #f1f1f1; } /* code 样式 */ .wangeditor code { display: inline-block; *display: inline; *zoom: 1; background-color: #f1f1f1; border-radius: 3px; padding: 3px 5px; margin: 0 3px; } .wangeditor pre code { display: block; } /* ul ol 样式 */ .wangeditor ul, ol { margin: 10px 0 10px 20px; } /* 和antdv p冲突,覆盖掉 */ .wangeditor blockquote p { font-family:"YouYuan"; margin: 20px 10px !important; font-size: 16px !important; font-weight:600; } /* 点赞 */ .vote-div { padding: 15px; text-align: center; } /* 图片自适应 */ .wangeditor img { max-width: 100%; height: auto; } /* 视频自适应 */ .wangeditor iframe { width: 100%; height: 400px; } </style>
2.3 添加新的路由规则
在router下的index.js中新增两个路由规则。
{ path: '/doc', name: 'doc', component:DocView }, { path: '/admin/doc', name: 'AdminDoc', component: AdminDoc }
后续我会把后端的代码补上的。