提交 180f38c2 authored 作者: pengxiaohui's avatar pengxiaohui

feat: 增加使用手册

上级 f49a4e99
......@@ -70,6 +70,7 @@
},
"dependencies": {
"axios": "^0.21.1",
"blueimp-md5": "^2.18.0",
"core-js": "^3.8.3",
"cross-env": "^7.0.3",
"element-ui": "^2.15.0",
......
......@@ -111,3 +111,20 @@ export function exportParticipants(params) {
responseType: 'blob'
})
}
/**
* 文件上传
*/
export function fileUpload(formData) {
return httpRequest({
url: 'https://webapp-pub.oss-cn-beijing.aliyuncs.com',
method: 'post',
headers: { 'Content-Type': 'multipart/form-data' },
timeout: 900000,
data: formData,
withCredentials: false
})
}
// 获取oss signature
export function getSignature() {
return httpRequest.get('/api/usercenter/aliyun/get-signature')
}
\ No newline at end of file
......@@ -154,3 +154,16 @@ export function getUserRoles() {
export function getUserPermissions() {
return httpRequest.get('/api/live/admin/v3/system/user/permissions')
}
/* ----------------------------系统使用手册---------------------------- */
/**
* 保存使用手册
*/
export function keepUserManual(data) {
return httpRequest.post('/api/live/admin/v3/system/user-manual', data)
}
/**
* 获取使用手册
*/
export function getUserManual() {
return httpRequest.get('/api/live/admin/v3/system/user-manual')
}
<template>
<div :class="{fullscreen:fullscreen}" class="tinymce-container" :style="{width:containerWidth}">
<textarea :id="tinymceId" class="tinymce-textarea" />
</div>
</template>
<script>
import plugins from './plugins'
import toolbar from './toolbar'
import imageUpload from './plugins/image'
import { mediaUpload, mediaResolver } from './plugins/media'
export default {
name: 'Tinymce',
props: {
id: {
type: String,
default: function() {
return 'vue-tinymce-' + +new Date() + ((Math.random() * 1000).toFixed(0) + '')
}
},
value: {
type: String,
default: ''
},
toolbar: {
type: Array,
required: false,
default() {
return []
}
},
menubar: {
type: String,
default: 'file edit insert view format table'
},
height: {
type: [Number, String],
required: false,
default: 360
},
width: {
type: [Number, String],
required: false,
default: 'auto'
},
readonly: {
type: Boolean,
default: false
}
},
data() {
return {
hasChange: false,
hasInit: false,
tinymceId: this.id,
fullscreen: false
}
},
computed: {
containerWidth() {
const width = this.width
if (/^[\d]+(\.[\d]+)?$/.test(width)) { // matches `100`, `'100'`
return `${width}px`
}
return width
}
},
watch: {
value(val) {
if (!this.hasChange && this.hasInit) {
this.$nextTick(() =>
window.tinymce.get(this.tinymceId).setContent(val || ''))
}
},
readonly(val) {
if (val) {
window.tinymce.editors[this.tinymceId].setMode('readonly')
} else {
window.tinymce.editors[this.tinymceId].setMode('design');
}
}
},
mounted() {
// window.setTimeout(() => {
// if (this.value) {
// window.tinymce.get(this.tinymceId).setContent(this.value || '')
// }
// }, 500)
this.init()
},
activated() {
if (window.tinymce) {
this.initTinymce()
}
},
deactivated() {
this.destroyTinymce()
},
destroyed() {
this.destroyTinymce()
},
methods: {
init() {
this.initTinymce()
},
initTinymce() {
const _this = this
window.tinymce.init({
selector: `#${this.tinymceId}`,
language: 'zh_CN',
readonly: this.readonly,
fontsize_formats: '12px 14px 16px 18px 24px 36px 48px 56px 72px',
font_formats: '微软雅黑=Microsoft YaHei,Helvetica Neue,PingFang SC,sans-serif;苹果苹方=PingFang SC,Microsoft YaHei,sans-serif;宋体=simsun,serif;仿宋体=FangSong,serif;黑体=SimHei,sans-serif;Arial=arial,helvetica,sans-serif;Arial Black=arial black,avant garde;Book Antiqua=book antiqua,palatino;',
images_upload_handler: imageUpload,
file_picker_types: 'media',
file_picker_callback: mediaUpload,
media_url_resolver: mediaResolver,
height: this.height,
body_class: 'panel-body ',
object_resizing: false,
toolbar: this.toolbar.length > 0 ? this.toolbar : toolbar,
menubar: this.menubar,
plugins: plugins,
end_container_on_empty_block: true,
powerpaste_word_import: 'clean',
code_dialog_height: 450,
code_dialog_width: 1000,
advlist_bullet_styles: 'default,circle,disc,square',
advlist_number_styles: 'default,lower-alpha,lower-roman,upper-alpha,upper-roman',
imagetools_cors_hosts: ['www.tinymce.com', 'codepen.io'],
default_link_target: '_blank',
link_title: false,
nonbreaking_force_tab: true, // inserting nonbreaking space &nbsp; need Nonbreaking Space Plugin
init_instance_callback: editor => {
if (_this.value) {
editor.setContent(_this.value)
}
_this.hasInit = true
editor.on('NodeChange Change KeyUp SetContent', () => {
this.hasChange = true
this.$emit('input', editor.getContent())
})
},
setup(editor) {
editor.on('FullscreenStateChanged', (e) => {
_this.fullscreen = e.state
});
editor.on('blur', (e) => {
_this.$emit('blur', e)
})
},
// it will try to keep these URLs intact
// https://www.tiny.cloud/docs-3x/reference/configuration/Configuration3x@convert_urls/
// https://stackoverflow.com/questions/5196205/disable-tinymce-absolute-to-relative-url-conversions
convert_urls: false
// 整合七牛上传
// images_dataimg_filter(img) {
// setTimeout(() => {
// const $image = $(img);
// $image.removeAttr('width');
// $image.removeAttr('height');
// if ($image[0].height && $image[0].width) {
// $image.attr('data-wscntype', 'image');
// $image.attr('data-wscnh', $image[0].height);
// $image.attr('data-wscnw', $image[0].width);
// $image.addClass('wscnph');
// }
// }, 0);
// return img
// },
// images_upload_handler(blobInfo, success, failure, progress) {
// progress(0);
// const token = _this.$store.getters.token;
// getToken(token).then(response => {
// const url = response.data.qiniu_url;
// const formData = new FormData();
// formData.append('token', response.data.qiniu_token);
// formData.append('key', response.data.qiniu_key);
// formData.append('file', blobInfo.blob(), url);
// upload(formData).then(() => {
// success(url);
// progress(100);
// })
// }).catch(err => {
// failure('出现未知问题,刷新页面,或者联系程序员')
// console.log(err);
// });
// },
})
},
destroyTinymce() {
const tinymce = window.tinymce.get(this.tinymceId)
if (this.fullscreen) {
tinymce.execCommand('mceFullScreen')
}
if (tinymce) {
tinymce.destroy()
}
},
setContent(value) {
window.tinymce.get(this.tinymceId).setContent(value)
},
getContent() {
window.tinymce.get(this.tinymceId).getContent()
},
imageSuccessCBK(arr) {
arr.forEach(v => window.tinymce.get(this.tinymceId).insertContent(`<img class="wscnph" src="${v.url}" >`))
}
}
}
</script>
<style scoped>
.tinymce-container {
position: relative;
line-height: normal;
}
.tinymce-container ::v-deep .mce-fullscreen {
z-index: 10000;
}
.tinymce-textarea {
visibility: hidden;
z-index: -1;
}
.editor-custom-btn-container {
position: absolute;
right: 4px;
top: 4px;
/*z-index: 2005;*/
}
.fullscreen ::v-deep .editor-custom-btn-container {
z-index: 10000;
position: fixed;
}
.editor-upload-btn {
display: inline-block;
}
.tinymce-container ::v-deep .tox-statusbar{
display:none;
}
</style>
// Any plugins you want to use has to be imported
// Detail plugins list see https://www.tinymce.com/docs/plugins/
// Custom builds see https://www.tinymce.com/download/custom-builds/
const plugins = ['advlist anchor autolink autosave code codesample directionality emoticons fullscreen hr image imagetools insertdatetime link lists media nonbreaking noneditable pagebreak paste preview print save searchreplace spellchecker tabfocus table template textpattern visualblocks visualchars wordcount quickbars']
export default plugins
import { fileUpload, getSignature } from '@/api/common'
import md5 from 'blueimp-md5'
export default function(blobInfo, succFun, failFun) {
const file = blobInfo.blob()
const fileName = file.name
const key = 'upload/marketing-admin/' + md5(fileName + new Date().getTime()) + fileName.substr(fileName.lastIndexOf('.'))
getSignature()
.then(response => {
const { accessid, policy, signature, host } = response
const data = { key, OSSAccessKeyId: accessid, policy, signature, success_action_status: '200', file }
const fileUrl = `${host}/${key}`
// const formData = new FormData()
// formData.append('key', data.key)
// formData.append('OSSAccessKeyId', data.OSSAccessKeyId)
// formData.append('policy', data.policy)
// formData.append('signature', data.signature)
// formData.append('success_action_status', '200')
// formData.append('file', file)
fileUpload(data).then(res => {
succFun(fileUrl)
}).catch((err) =>
failFun(err.message || '上传出错了')
)
})
.catch(err => {
console(err)
})
}
import { fileUpload, getSignature } from '@/api/common'
import { Message, Loading } from 'element-ui'
import md5 from 'blueimp-md5'
/**
* 以分隔符截取字符串,并返回最后一个截取段
*/
const splitStrLast = function(str, split) {
const fileNameArr = str.split(split)
const last = fileNameArr[fileNameArr.length - 1]
return last
}
const fetchUpload = function(file, callback) {
const loading = Loading.service({
lock: true,
text: '视频上传中',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)',
customClass: 'tinymce-el-loading'
})
const fileName = file.name
const key = 'upload/marketing-admin/' + md5(fileName + new Date().getTime()) + fileName.substr(fileName.lastIndexOf('.'))
getSignature()
.then(response => {
const { accessid, policy, signature, host } = response
const data = { key, OSSAccessKeyId: accessid, policy, signature, success_action_status: '200', file }
const fileUrl = `${host}/${key}`
// const formData = new FormData()
// formData.append('key', data.key)
// formData.append('OSSAccessKeyId', data.OSSAccessKeyId)
// formData.append('policy', data.policy)
// formData.append('signature', data.signature)
// formData.append('success_action_status', '200')
// formData.append('file', file)
fileUpload(data).then(res => {
loading.close()
callback(fileUrl, { title: file.name })
}).catch(err => {
loading.close()
Message({ message: err.message || '上传上传出错了', type: 'error', customClass: 'tinymce-el-message' })
})
})
.catch(err => {
console(err)
loading.close()
})
}
/**
* 视频上传
* @param {function} callback 上传结束回调
* @param {string} value
* @param {*} meta
*/
export function mediaUpload(callback, value, meta) {
if (meta.filetype === 'media') {
const input = document.createElement('input') // 创建一个隐藏的input
input.setAttribute('type', 'file')
input.setAttribute('accept', '.mp4')
input.onchange = function() {
const file = this.files[0]
let errorMsg = ''
if (file && file.name) {
const suffix = splitStrLast(file.name, '.')
if (['mp4'].includes(suffix)) {
if (file.size > 1024 * 1024 * 1024) {
errorMsg = '视频文件大小不能超过 1GB!'
}
} else {
errorMsg = '只支持mp4格式的视频文件'
}
} else {
errorMsg = '请选取视频文件'
}
if (errorMsg) {
Message({ message: errorMsg, type: 'error', customClass: 'tinymce-el-message' })
} else {
fetchUpload(file, callback)
}
}
// 触发点击
input.click()
}
}
/**
* 添加视频回调
* @param {function} data 视频数据
* @param {string} resolve 回调函数
*/
export function mediaResolver(data, resolve) {
try {
const videoUri = encodeURI(data.url)
const embedHtml = `
<p>
<span
data-mce-selected="1"
data-mce-object="video"
data-mce-p-width="100%"
data-mce-p-height="100%"
data-mce-p-controls="controls"
data-mce-p-controlslist="nodownload"
data-mce-p-allowfullscreen="true"
data-mce-p-src=${videoUri} >
<video src=${videoUri} width="100%" height="100%" controls="controls" controlslist="nodownload">
</video>
</span>
</p>
<p style="text-align: left;"></p>
`
resolve({ html: embedHtml })
} catch (e) {
resolve({ html: '' })
}
}
// Here is a list of the toolbar
// Detail list see https://www.tinymce.com/docs/advanced/editor-control-identifiers/#toolbarcontrols
const toolbar = ['fontsizeselect fontselect lineheight bold italic underline strikethrough forecolor backcolor alignleft aligncenter alignright outdent indent blockquote removeformat subscript superscript', 'hr bullist numlist link quickimage charmap preview anchor pagebreak insertdatetime media table emoticons fullscreen code codesample searchreplace']
export default toolbar
......@@ -53,7 +53,6 @@ export default {
},
methods: {
handlleSelect(path) {
console.log(this.$router)
this.$router.push(path)
}
}
......
......@@ -7,6 +7,7 @@
@toggleClick="toggleSideBar"
/>
<div class="right-menu">
<i class="el-icon-question" @click="handleManual" title="使用手册"></i>
<el-dropdown class="avatar-container" trigger="click">
<div class="avatar-wrapper">
<img :src="avatar" class="user-avatar" />
......@@ -56,6 +57,9 @@ export default {
toMeeting() {
this.$router.push('/meeting-create')
},
handleManual(id) {
this.$router.push({ path: '/system/user-manual', query: { id } })
},
async logout() {
await this.$store.dispatch('logout')
let path = this.$route.fullPath
......@@ -96,7 +100,14 @@ export default {
float: right;
height: 100%;
line-height: 52px;
i.el-icon-question{
font-size:20px;
margin-right:16px;
cursor: pointer;
}
i.el-icon-question:hover{
color:#409EFF;
}
&:focus {
outline: none;
}
......
......@@ -8,6 +8,7 @@
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<title>紫荆会议室</title>
<script type="text/javascript" src="https://webapp-pub.ezijing.com/plugins/tinymce/tinymce.min.js"></script>
<meta
name="viewport"
id="viewport"
......
<template>
<div class="manual">
<h5>使用手册 </h5>
<div class="inner">
<el-tabs v-model="tabsActive" type="card">
<el-tab-pane label="使用说明" name="INSTRUCTIONS">
<Tinymce v-if="tinymceFlag" :readonly="isReadonly" ref="editor" v-model="intro_content" :height="600" />
</el-tab-pane>
<el-tab-pane label="常见问题" name="COMMON_PROBLEM">
<Tinymce v-if="tinymceFlag" :readonly="isReadonly" ref="editor" v-model="ques_content" :height="600" />
</el-tab-pane>
</el-tabs>
<el-button type="primary" @click="fetchSubmitManual" size="mini" style="margin-top:20px;" :disabled="btnDisabled">保存</el-button>
<el-button class="readonly" type="primary" @click="isReadonly = !isReadonly" size="mini">{{isReadonly ? '编 辑' : '只 读'}}</el-button>
</div>
</div>
</template>
<script>
import Tinymce from '@/components/appTinymce/index.vue'
import { keepUserManual, getUserManual } from '@/api/system'
export default {
components: { Tinymce },
data() {
return {
tabsActive: 'INSTRUCTIONS',
intro_content: '',
ques_content: '',
isReadonly: true,
btnDisabled: false,
tinymceFlag: false
}
},
mounted() {
this.fetchGetManual()
this.tinymceFlag = false
this.$nextTick(() => {
this.tinymceFlag = true
})
},
destroyed() {
this.$destroy('Tinymce')
},
methods: {
fetchSubmitManual() {
const params = {
type: this.tabsActive
}
if (this.tabsActive === 'INSTRUCTIONS') {
params.info = this.intro_content
} else {
params.info = this.ques_content
}
this.btnDisabled = true
keepUserManual(params).then(res => {
this.btnDisabled = false
if (res.code === 0 && res.data && res.data.status) {
this.$message.success('保存成功!')
} else {
this.$message.success('保存失败!')
}
}).catch(() => {
this.btnDisabled = false
})
},
fetchGetManual() {
getUserManual().then(res => {
if (res.code === 0) {
this.intro_content = res.data.instructions
this.ques_content = res.data.common_problem
}
})
}
}
}
</script>
<style scoped>
.manual {
height: 100%;
}
h5 {
font-size: 16px;
font-family: PingFangSC-Regular, PingFang SC;
color: #333;
font-weight: 400;
line-height: 50px;
text-indent: 40px;
}
.inner {
height: calc(100% - 50px - 10px);
background: #ffffff;
border-radius: 10px;
margin: 0 16px;
box-sizing: border-box;
padding: 14px 14px 0;
}
::v-deep .el-tabs__content{
height:calc(100vh - 240px);
overflow-y:auto;
}
.readonly{
position:absolute;
right:40px;
top:60px;
}
</style>
\ No newline at end of file
<template>
<div class="manual">
<h5>使用手册 </h5>
<div class="inner">
<el-tabs v-model="tabsActive" type="card">
<el-tab-pane label="使用说明" name="INSTRUCTIONS">
<div v-html="intro_content"></div>
</el-tab-pane>
<el-tab-pane label="常见问题" name="COMMON_PROBLEM">
<div v-html="ques_content"></div>
</el-tab-pane>
</el-tabs>
</div>
</div>
</template>
<script>
import { getUserManual } from '@/api/system'
export default {
data() {
return {
tabsActive: 'INSTRUCTIONS',
intro_content: '',
ques_content: ''
}
},
created() {
this.fetchGetManual()
},
methods: {
fetchGetManual() {
getUserManual().then(res => {
if (res.code === 0) {
this.intro_content = res.data.instructions
this.ques_content = res.data.common_problem
}
})
}
}
}
</script>
<style scoped>
.manual {
height: 100%;
}
h5 {
font-size: 16px;
font-family: PingFangSC-Regular, PingFang SC;
color: #333;
font-weight: 400;
line-height: 50px;
text-indent: 40px;
}
.inner {
height: calc(100% - 50px - 10px);
background: #ffffff;
border-radius: 10px;
margin: 0 16px;
box-sizing: border-box;
padding: 14px 14px 0;
}
::v-deep .el-tabs__content{
height:calc(100vh - 180px);
overflow-y:auto;
}
</style>
\ No newline at end of file
......@@ -90,6 +90,19 @@ export default [
name: 'Permission',
component: () => import('@/pages/system/permission/index'),
meta: { title: '权限管理', icon: 'el-icon-finished', permission: 'menus_permissions' }
},
{
path: 'manual',
name: 'Manual',
component: () => import('@/pages/system/manual/index'),
meta: { title: '使用手册', icon: 'el-icon-document', permission: 'menus_manual' }
},
{
path: 'user-manual',
name: 'UserManual',
hidden: true,
component: () => import('@/pages/system/manual/showManual'),
meta: { title: '使用手册', icon: 'el-icon-s-check' }
}
]
},
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论