增加文章编辑器

This commit is contained in:
zyy 2021-05-14 13:14:08 +08:00
parent 02241f455a
commit af9eb2c8b6
10 changed files with 687 additions and 93 deletions

View File

@ -1,93 +1,94 @@
{ {
"name": "vue-admin-template", "name": "vue-admin-template",
"version": "4.1.0", "version": "4.1.0",
"description": "A vue admin template with Element UI & axios & iconfont & permission control & lint", "description": "A vue admin template with Element UI & axios & iconfont & permission control & lint",
"author": "Pan <panfree23@gmail.com>", "author": "Pan <panfree23@gmail.com>",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"start": "vue-cli-service serve --open", "start": "vue-cli-service serve --open",
"dev": "node --max_old_space_size=4096 node_modules/@vue/cli-service/bin/vue-cli-service.js serve", "dev": "node --max_old_space_size=4096 node_modules/@vue/cli-service/bin/vue-cli-service.js serve",
"build": "vue-cli-service build --mode production", "build": "vue-cli-service build --mode production",
"test": "vue-cli-service build --mode staging", "test": "vue-cli-service build --mode staging",
"local": "vue-cli-service build --mode", "local": "vue-cli-service build --mode",
"preview": "node build/index.js --preview", "preview": "node build/index.js --preview",
"lint": "eslint --ext .js,.vue src", "lint": "eslint --ext .js,.vue src",
"test:unit": "jest --clearCache && vue-cli-service test:unit", "test:unit": "jest --clearCache && vue-cli-service test:unit",
"test:ci": "npm run lint && npm run test:unit", "test:ci": "npm run lint && npm run test:unit",
"svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml" "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml"
}, },
"dependencies": { "dependencies": {
"axios": "0.18.0", "axios": "0.18.0",
"echarts": "^4.7.0", "echarts": "^4.7.0",
"element-ui": "^2.12.0", "element-ui": "^2.12.0",
"file-saver": "^1.3.3", "file-saver": "^1.3.3",
"js-cookie": "2.2.0", "js-cookie": "2.2.0",
"js-md5": "^0.7.3", "js-md5": "^0.7.3",
"lodash": "^4.17.11", "lodash": "^4.17.11",
"normalize.css": "7.0.0", "normalize.css": "7.0.0",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"path-to-regexp": "2.4.0", "path-to-regexp": "2.4.0",
"qrcode.vue": "^1.6.2", "qrcode.vue": "^1.6.2",
"qs": "^6.9.3", "qs": "^6.9.3",
"quill-emoji": "^0.1.8", "quill-emoji": "^0.1.8",
"quill-image-extend-module": "^1.1.2", "quill-image-extend-module": "^1.1.2",
"recordrtc": "^5.5.9", "recordrtc": "^5.5.9",
"script-loader": "^0.7.2", "script-loader": "^0.7.2",
"sessionstorage": "^0.1.0", "sessionstorage": "^0.1.0",
"stompjs": "^2.3.3", "stompjs": "^2.3.3",
"storejs": "^1.0.25", "storejs": "^1.0.25",
"three": "^0.107.0", "three": "^0.107.0",
"video.js": "^7.8.4", "video.js": "^7.8.4",
"vue": "^2.6.10", "vue": "^2.6.10",
"vue-i18n": "^8.12.0", "vue-i18n": "^8.12.0",
"vue-quill-editor": "^3.0.6", "vue-quill-editor": "^3.0.6",
"vue-router": "^3.1.6", "vue-router": "^3.1.6",
"vuedraggable": "^2.24.3", "vuedraggable": "^2.24.3",
"vuex": "^3.1.0", "vuex": "^3.1.0",
"xlsx": "^0.14.2", "wangeditor": "^4.6.17",
"zrender": "^4.0.4" "xlsx": "^0.14.2",
}, "zrender": "^4.0.4"
"devDependencies": { },
"@babel/core": "7.0.0", "devDependencies": {
"@babel/register": "7.0.0", "@babel/core": "7.0.0",
"@vue/cli-plugin-babel": "3.6.0", "@babel/register": "7.0.0",
"@vue/cli-plugin-eslint": "3.6.0", "@vue/cli-plugin-babel": "3.6.0",
"@vue/cli-plugin-unit-jest": "3.6.3", "@vue/cli-plugin-eslint": "3.6.0",
"@vue/cli-service": "3.6.0", "@vue/cli-plugin-unit-jest": "3.6.3",
"@vue/test-utils": "1.0.0-beta.29", "@vue/cli-service": "3.6.0",
"autoprefixer": "^9.5.1", "@vue/test-utils": "1.0.0-beta.29",
"babel-core": "7.0.0-bridge.0", "autoprefixer": "^9.5.1",
"babel-eslint": "10.0.1", "babel-core": "7.0.0-bridge.0",
"babel-jest": "23.6.0", "babel-eslint": "10.0.1",
"babel-plugin-syntax-dynamic-import": "^6.18.0", "babel-jest": "23.6.0",
"babel-preset-env": "1.6.1", "babel-plugin-syntax-dynamic-import": "^6.18.0",
"babel-preset-stage-2": "6.24.1", "babel-preset-env": "1.6.1",
"chalk": "2.4.2", "babel-preset-stage-2": "6.24.1",
"compression-webpack-plugin": "^3.1.0", "chalk": "2.4.2",
"connect": "3.6.6", "compression-webpack-plugin": "^3.1.0",
"copy-webpack-plugin": "^4.5.2", "connect": "3.6.6",
"eslint": "5.15.3", "copy-webpack-plugin": "^4.5.2",
"eslint-plugin-vue": "5.2.2", "eslint": "5.15.3",
"file-loader": "^3.0.1", "eslint-plugin-vue": "5.2.2",
"html-webpack-plugin": "3.2.0", "file-loader": "^3.0.1",
"mockjs": "1.0.1-beta3", "html-webpack-plugin": "3.2.0",
"node-sass": "^4.9.0", "mockjs": "1.0.1-beta3",
"runjs": "^4.3.2", "node-sass": "^4.9.0",
"sass-loader": "^7.1.0", "runjs": "^4.3.2",
"script-ext-html-webpack-plugin": "2.1.3", "sass-loader": "^7.1.0",
"script-loader": "^0.7.2", "script-ext-html-webpack-plugin": "2.1.3",
"serve-static": "^1.13.2", "script-loader": "^0.7.2",
"svg-sprite-loader": "4.1.3", "serve-static": "^1.13.2",
"svgo": "1.2.2", "svg-sprite-loader": "4.1.3",
"vue-template-compiler": "^2.6.11" "svgo": "1.2.2",
}, "vue-template-compiler": "^2.6.11"
"engines": { },
"node": ">=8.9", "engines": {
"npm": ">= 3.0.0" "node": ">=8.9",
}, "npm": ">= 3.0.0"
"browserslist": [ },
"> 1%", "browserslist": [
"last 2 versions", "> 1%",
"not ie <= 8" "last 2 versions",
] "not ie <= 8"
]
} }

68
src/api/editor.js Normal file
View File

@ -0,0 +1,68 @@
import request from '@/utils/request';
// 获取文章列表
export function getDoc() {
return request({
url: `/api/doc`,
method: 'get'
});
}
// 获取发布内容
export function getDocById(id) {
return request({
url: `/api/doc/${id}`,
method: 'get'
});
}
// 获取草稿列表
export function getDocDraft() {
return request({
url: `/api/doc/draft`,
method: 'get'
});
}
// 创建
export function postDocDraft(data) {
return request({
url: `/api/doc/draft`,
method: 'post',
data
});
}
// 获取草稿数据id
export function getDocDraftById(id) {
return request({
url: `/api/doc/draft/${id}`,
method: 'get'
});
}
// 更新草稿数据
export function putDocDraftById(id, data) {
return request({
url: `/api/doc/draft/${id}`,
method: 'put',
data
});
}
// 删除草稿数据
export function deleteDocDraftById(id) {
return request({
url: `/api/doc/draft/${id}`,
method: 'delete'
});
}
// 保存内容根据id
export function putDocDraftByIdData(id, data) {
return request({
url: `/api/doc/draft/${id}/data`,
method: 'put',
data
});
}
// 草稿发布
export function putDocDraftByIdPublish(id) {
return request({
url: `/api/doc/draft/${id}/publish`,
method: 'put'
});
}

25
src/api/upload.js Normal file
View File

@ -0,0 +1,25 @@
import request from '@/utils/request';
export const productIdentify = '00001&appSecret=joylink00001';
export const pictureUrl = `/api/upload/PICTURE?appId=${productIdentify}`;
// export const modelUrl = `/api/upload/model?appId=${productIdentify}`;
// export const attachmentUrl = `/api/upload/attachment?appId=${productIdentify}`;
// export const meansUrl = `/api/upload/means?appId=${productIdentify}`;
// export const regulationUrl = `/api/upload/regulation?appId=${productIdentify}`;
export function getUrl(relatedUrl) {
return `${process.env.VUE_APP_UPLOAD_API}${relatedUrl}`;
}
export function uploadFile(url, data) {
return request({
headers: {
'Content-Type': 'multipart/form-data'
},
url,
method: 'post',
data: data
});
}

View File

@ -4,6 +4,9 @@ export default {
designhomePage: '公共地图', designhomePage: '公共地图',
designUserPage: '个人地图', designUserPage: '个人地图',
newDesignUserPage: '地图绘制', newDesignUserPage: '地图绘制',
newDesignEditor: '编辑器',
newDesignEditorList: '图文列表',
newDesignDraftEditorList: '文章草稿',
mapManage: '地图管理', mapManage: '地图管理',
skinManage: '皮肤管理', skinManage: '皮肤管理',

View File

@ -172,6 +172,10 @@ const VoiceTraining = () => import('@/views/system/voiceTraining/index');
const SceneTrainingResult = () => import('@/views/drts/scene/trainingResult'); const SceneTrainingResult = () => import('@/views/drts/scene/trainingResult');
const SceneVoiceTraining = () => import('@/views/drts/scene/voiceTraining'); const SceneVoiceTraining = () => import('@/views/drts/scene/voiceTraining');
const Ueditor = () => import('@/views/editor/index');
const UeditorList = () => import('@/views/editor/list');
const UeditorDraftList = () => import('@/views/editor/listDraft');
// import { GenerateRouteProjectList } from '@/scripts/ProjectConfig'; // import { GenerateRouteProjectList } from '@/scripts/ProjectConfig';
// import { getSessionStorage } from '@/utils/auth'; // import { getSessionStorage } from '@/utils/auth';
@ -1132,6 +1136,37 @@ export const asyncRouter = [
// } // }
] ]
}, },
{
path: '/editor',
component: Layout,
meta: {
i18n: 'router.newDesignEditor',
roles: [admin, user]
},
children: [
{
path: 'list',
component: UeditorList,
meta: {
i18n: 'router.newDesignEditorList'
}
},
{
path: 'listDraft',
component: UeditorDraftList,
meta: {
i18n: 'router.newDesignDraftEditorList'
}
},
{
path: '',
component: Ueditor,
meta: {
i18n: 'router.newDesignEditor'
}
}
]
},
{ // 新个人地图 { // 新个人地图
path: '/design', path: '/design',
component: Layout, component: Layout,

View File

@ -2,8 +2,8 @@ export function getBaseUrl() {
let BASE_API; let BASE_API;
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
// BASE_API = 'https://joylink.club/jlcloud'; // BASE_API = 'https://joylink.club/jlcloud';
BASE_API = 'https://test.joylink.club/jlcloud'; // BASE_API = 'https://test.joylink.club/jlcloud';
// BASE_API = 'http://192.168.8.107:9000'; // 袁琪 BASE_API = 'http://192.168.3.119:9000'; // 袁琪
// BASE_API = 'http://192.168.3.83:9000'; // 旭强 有线 // BASE_API = 'http://192.168.3.83:9000'; // 旭强 有线
// BASE_API = 'http://192.168.8.114:9000'; // 旭强 无线 // BASE_API = 'http://192.168.8.114:9000'; // 旭强 无线
// BASE_API = 'http://192.168.3.120:9000'; // 张赛 // BASE_API = 'http://192.168.3.120:9000'; // 张赛

116
src/views/editor/edit.vue Normal file
View File

@ -0,0 +1,116 @@
<template>
<el-dialog v-dialogDrag :title="title" :visible.sync="dialogVisible" width="30%" :before-close="handleClose" center>
<data-form ref="dataform" :form="form" :form-model="formModel" :rules="rules" />
<span slot="footer" class="dialog-footer">
<el-button type="primary" @click="doSave">{{ $t('global.confirm') }}</el-button>
<el-button @click="handleClose">{{ $t('global.cancel') }}</el-button>
</span>
</el-dialog>
</template>
<script>
// import { postOperateStepData, putOperateStepData, getPlaceholderList } from '@/api/management/operation';
import { postDocDraft, getDocDraftById, putDocDraftById } from '@/api/editor';
export default {
name: 'TrainingDetailEdit',
props: {
type: {
type: String,
required: true
}
},
data() {
return {
dialogVisible: false,
formModel: {
id: '',
title: ''
},
placeholderList: []
};
},
computed: {
form() {
// const isAdd = this.type === 'ADD';
const form = {
labelWidth: '80px',
items: [
{ prop: 'title', label: '标题', type: 'text', required: true }
]
};
return form;
},
rules() {
const crules = {
title: [
{
required: true,
message: '请输入文章标题',
trigger: 'blur'
}
]
};
return crules;
},
title() {
if (this.type === 'ADD') {
return '创建文章';
} else {
return '更新文章';
}
}
},
methods: {
async show(data) {
this.dialogVisible = true;
console.log(data);
if (this.type != 'ADD') {
const res = await getDocDraftById(data.id);
this.formModel.title = res.data.title;
this.formModel.id = data.id;
} else {
this.formModel.title = '';
this.formModel.id = '';
}
},
doSave() {
const self = this;
this.$refs.dataform.validateForm(() => {
if (self.type === 'ADD') {
self.create();
} else {
self.update();
}
});
},
create() {
const self = this;
postDocDraft(this.formModel).then(() => {
self.$message.success('创建文章成功!');
self.handleClose();
self.$emit('reloadTable');
}).catch(error => {
self.$message.error(`创建文章失败:${error.message}`);
});
},
update() {
const self = this;
putDocDraftById(this.formModel.id, this.formModel).then(() => {
self.$message.success('更新文章成功!');
self.handleClose();
self.$emit('reloadTable');
}).catch(error => {
self.$message.error(`更新文章失败:${error.message}`);
});
},
handleClose() {
this.formModel = {
title: ''
};
this.$refs.dataform.resetForm();
this.dialogVisible = false;
}
}
};
</script>

151
src/views/editor/index.vue Normal file
View File

@ -0,0 +1,151 @@
<template>
<div id="wangeditor">
<div ref="editorElem" style="text-align:left;" />
<button v-if="draft" type="button" class="btn" @click="saveDraftTitle">保存</button>
<!-- <button type="button" class="btn" @click="getEditorData">获取当前内容</button>
<button type="button" class="btn" @click="setEditorData">设置当前内容</button>
<h3>内容预览</h3>
<div class="content_html1" v-html="editorContent" /> -->
</div>
</template>
<script>
import E from 'wangeditor';
import { uploadFile, pictureUrl } from '@/api/upload';
import { putDocDraftByIdData, getDocDraftById } from '@/api/editor';
export default {
name: 'Editor',
// catchDatahtml
// props: ['catchData'],
data() {
return {
editor: null,
formData: '',
editorContent: ''
};
}, //
computed: {
action() {
return `${process.env.VUE_APP_UPLOAD_API}${pictureUrl}`;
},
draft() {
return this.$route.query.draft;
},
docId() {
return this.$route.query.docId;
}
},
mounted() {
this.editor = new E(this.$refs.editorElem);
this.editor.config.height = 450;
this.editor.config.uploadImgServer = '/upload-img';
// html
this.editor.config.onchange = html => {
this.editorContent = html;
};
// this.editor.config.showLinkImg = false; //
this.editor.config.uploadImgShowBase64 = true;
const action = this.action;
const that = this;
this.editor.config.customUploadImg = function (resultFiles, insertImgFn) {
that.formData = new FormData();
resultFiles.forEach(file => {
that.formData.append('file', file);
});
uploadFile(action, that.formData).then(resp => {
const imgUrl = process.env.VUE_APP_VOICE_API + resp.data;
insertImgFn(imgUrl);
}).catch(error => { console.log(error); });
};
this.editor.config.menus = [
//
'head', //
'bold', //
'fontSize', //
'fontName', //
'italic', //
'underline', // 线
'strikeThrough', // 线
'indent',
'lineHeight',
'foreColor', //
'backColor', //
'link', //
'list', //
'justify', //
'quote', //
'emoticon', //
'image', //
'table', //
'code', //
'splitLine',
'undo', //
'redo' //
];
this.editor.create(); //
this.handleEdit();
},
beforeDestroy() {
// API
this.editor.destroy();
this.editor = null;
},
methods: {
async handleEdit() {
try {
const res = await getDocDraftById(this.docId);
this.editor && this.editor.txt.html(res.data.content); //
} catch (error) {
console.log(error);
}
},
saveDraftTitle() {
const data = this.editor.txt.html();
putDocDraftByIdData(this.docId, { content: data }).then(res => {
this.$message.success('保存文章成功!');
setTimeout(() => {
this.$router.push({path : '/editor/listDraft'});
}, 2000);
}).catch(error => {
this.$message.error(`保存失败:${error.message}`);
});
},
getEditorData() {
const data = this.editor.txt.html();
console.log(data);
},
setEditorData() {
const html = '<h1 id="gt6dj" style="text-align:center;">测试标题</h1><hr/><p data-we-empty-p="" style="text-align:center;"><img src="https://oss.joylink.club/oss/ddy/picture/2021-05-10/2720-44302.jpg" style="max-width:100%;" contenteditable="false" width="473" height="196.83"/><br/>工艺数据</p>';
this.editor && this.editor.txt.html(html); //
}
}
};
</script>
<style lang="scss" scoped>
#wangeditor{
z-index: 2;
position: absolute;
width: 100%;
height: 100%;
}
.content_html {
width: 100%;
height: 200px;
overflow: auto;
background: red;
}
.content_html1{
width: 100%;
height: 300px;
overflow: auto;
background: chartreuse;
}
</style>

87
src/views/editor/list.vue Normal file
View File

@ -0,0 +1,87 @@
<template>
<div>
<el-card>
<el-form
ref="queryForm"
:model="formModel"
size="small"
style="padding: 6px 20px 6px 20px;overflow: hidden;"
>
<el-form-item label="标题" style="margin-bottom: 0;float: left; width: 300px;">
<el-input v-model="formModel.title" style="width: 240px;" />
</el-form-item>
<el-button style="margin-right: 10px; float: right;" type="primary" size="small" @click="reloadTable">查询</el-button>
</el-form>
</el-card>
<el-table :data="tableData" border style="width: 100%">
<el-table-column label="姓名">
<template slot-scope="scope">
<span style="margin-left: 10px">{{ scope.row.title }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="450">
<template slot-scope="scope">
<el-button size="mini" @click="handleShow(scope.$index, scope.row)">查看</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog
title="文章"
:visible.sync="dialogVisible"
width="100%"
top="0"
:fullscreen="true"
:before-close="beforeClose"
>
<div class="content_html1" v-html="editorContent" />
<span slot="footer" class="dialog-footer">
<el-button type="primary" @click="beforeClose">关闭</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import { getDoc, getDocById } from '@/api/editor';
export default {
name: 'CacheControl',
components: {
},
data() {
return {
tableData: [],
formModel: {
title: ''
},
dialogVisible: false,
editorContent: ''
};
},
created() {
this.reloadTable();
},
methods: {
handleShow(row, data) {
console.log(data);
this.dialogVisible = true;
getDocById(data.id).then(res => {
this.editorContent = res.data.content;
}).catch(error => {
console.log(error);
});
},
reloadTable() {
this.tableData = [];
getDoc().then(res => {
this.tableData = res.data;
}).catch(error => {
console.log(error);
});
},
beforeClose() {
this.dialogVisible = false;
}
}
};
</script>

View File

@ -0,0 +1,108 @@
<template>
<div>
<!-- <QueryListPage ref="queryListPage" :pager-config="pagerConfig" :query-form="queryForm" :query-list="queryList" /> -->
<el-card>
<el-form
ref="queryForm"
:model="formModel"
size="small"
style="padding: 6px 20px 6px 20px;overflow: hidden;"
>
<el-form-item label="标题" style="margin-bottom: 0;float: left; width: 300px;">
<el-input v-model="formModel.title" style="width: 240px;" />
</el-form-item>
<el-button style="margin-right: 10px; float: right;" type="primary" size="small" @click="createTitle">创建</el-button>
<el-button style="margin-right: 10px; float: right;" type="primary" size="small" @click="query">查询</el-button>
</el-form>
</el-card>
<el-table :data="tableData" border style="width: 100%">
<el-table-column label="姓名">
<template slot-scope="scope">
<!-- <i class="el-icon-time" /> -->
<span style="margin-left: 10px">{{ scope.row.title }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="450">
<template slot-scope="scope">
<el-button size="mini" @click="handleEdit(scope.$index, scope.row)">编辑</el-button>
<el-button size="mini" @click="handleUpdate(scope.$index, scope.row)">更新</el-button>
<el-button size="mini" @click="handlePublish(scope.$index, scope.row)">发布</el-button>
<el-button size="mini" type="danger" @click="handleDelete(scope.$index, scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<Edit ref="edits" type="ADD" @reloadTable="reloadTable" />
<Edit ref="edit" type="EDIT" @reloadTable="reloadTable" />
</div>
</template>
<script>
import { getDocDraft, putDocDraftByIdPublish, deleteDocDraftById } from '@/api/editor';
import Edit from './edit';
export default {
name: 'CacheControl',
components: {
Edit
},
data() {
return {
tableData: [],
formModel: {
title: ''
}
};
},
created() {
this.query();
},
methods: {
query() {
this.reloadTable();
},
createTitle() {
this.$refs.edits.show();
},
//
handleEdit(row, data) {
this.$router.push({path : '/editor', query: {docId: data.id, draft: 1}});
},
//
handleUpdate(row, data) {
this.$refs.edit.show(data);
},
// 稿
handlePublish(row, data) {
console.log(data);
putDocDraftByIdPublish(data.id).then(res => {
this.$message.success('创建文章成功!');
this.reloadTable();
}).catch(error => {
this.$message.error(`发布失败: ${error.data}`);
});
},
handleDelete(row, data) {
this.$confirm('是否确认删除文章?', this.$t('tip.hint'), {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
deleteDocDraftById(data.id).then(res => {
this.$message.success('删除文章成功!');
this.reloadTable();
}).catch(error => {
this.$message.error(`删除失败: ${error.data}`);
});
}).catch(() => {});
},
reloadTable() {
this.tableData = [];
getDocDraft().then(res => {
this.tableData = res.data;
}).catch(error => {
console.log(error);
});
}
}
};
</script>