作业设计-课堂展示
This commit is contained in:
parent
820a4e04f7
commit
5f8ea627d0
|
@ -63,7 +63,10 @@
|
|||
"vue-qr": "^4.0.9",
|
||||
"vue-router": "^4.4.0",
|
||||
"xgplayer": "^3.0.19",
|
||||
"xlsx": "^0.18.5"
|
||||
"xlsx": "^0.18.5",
|
||||
"less": "^4.2.0",
|
||||
"less-loader": "^7.3.0",
|
||||
"whiteboard_lyc": "^0.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-toolkit/eslint-config": "^1.0.2",
|
||||
|
|
|
@ -0,0 +1,130 @@
|
|||
<template>
|
||||
<div class="colorPickerContainer">
|
||||
<div class="content">
|
||||
<el-popover
|
||||
:placement="placement"
|
||||
:width="200"
|
||||
trigger="click"
|
||||
:disabled="colorList.length <= 0"
|
||||
>
|
||||
<template #reference>
|
||||
<div class="colorPreview" :style="{ backgroundColor: color }"></div>
|
||||
</template>
|
||||
<div class="colorList">
|
||||
<div
|
||||
class="colorItem"
|
||||
v-for="item in colorList"
|
||||
:key="item"
|
||||
:style="{ backgroundColor: item }"
|
||||
@click="color = item"
|
||||
>
|
||||
<span v-if="!item">无</span>
|
||||
<span v-if="item === 'transparent'">透明</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-popover>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import {
|
||||
strokeColorList,
|
||||
fillColorList,
|
||||
backgroundColorList
|
||||
} from '../constants'
|
||||
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: '颜色'
|
||||
},
|
||||
placement: {
|
||||
type: String,
|
||||
default: 'bottom'
|
||||
},
|
||||
showEmptySelect: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const emits = defineEmits(['change'])
|
||||
|
||||
const color = ref(props.value)
|
||||
watch(
|
||||
() => {
|
||||
return props.value
|
||||
},
|
||||
val => {
|
||||
color.value = val
|
||||
}
|
||||
)
|
||||
const colorList = computed(() => {
|
||||
let list = props.showEmptySelect ? [''] : []
|
||||
switch (props.type) {
|
||||
case 'stroke':
|
||||
list.push(...strokeColorList)
|
||||
break
|
||||
case 'fill':
|
||||
list.push(...fillColorList)
|
||||
break
|
||||
case 'background':
|
||||
list.push(...backgroundColorList)
|
||||
break
|
||||
default:
|
||||
}
|
||||
return list
|
||||
})
|
||||
watch(color, () => {
|
||||
emits('change', color.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.colorPickerContainer {
|
||||
.content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.colorPreview {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 5px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 10px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.colorList {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, auto);
|
||||
grid-gap: 5px;
|
||||
.colorItem {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ddd;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,213 @@
|
|||
<template>
|
||||
<div
|
||||
class="contextmenuContainer"
|
||||
v-if="isShow"
|
||||
:style="{ left: left + 'px', top: top + 'px' }"
|
||||
>
|
||||
<template v-if="isHasActiveElements">
|
||||
<div
|
||||
class="item"
|
||||
:class="{ disabled: !canMoveLevel }"
|
||||
@click="exec('moveUp')"
|
||||
>
|
||||
上移一层
|
||||
</div>
|
||||
<div
|
||||
class="item"
|
||||
:class="{ disabled: !canMoveLevel }"
|
||||
@click="exec('moveDown')"
|
||||
>
|
||||
下移一层
|
||||
</div>
|
||||
<div
|
||||
class="item"
|
||||
:class="{ disabled: !canMoveLevel }"
|
||||
@click="exec('moveTop')"
|
||||
>
|
||||
置于顶层
|
||||
</div>
|
||||
<div
|
||||
class="item"
|
||||
:class="{ disabled: !canMoveLevel }"
|
||||
@click="exec('moveBottom')"
|
||||
>
|
||||
置于底层
|
||||
</div>
|
||||
<div class="splitLine"></div>
|
||||
<div class="item danger" @click="exec('del')">删除</div>
|
||||
<div class="item" @click="exec('copy')">复制</div>
|
||||
<div
|
||||
class="item"
|
||||
:class="{ disabled: groupStatus === 'disabled' }"
|
||||
@click="exec(groupStatus)"
|
||||
>
|
||||
{{ groupBtnText }}
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="item" @click="exec('selectAll')">全部选中</div>
|
||||
<div class="item" @click="exec('backToCenter')">回到中心</div>
|
||||
<div class="item" @click="exec('fit')">显示全部</div>
|
||||
<div class="item" @click="exec('resetZoom')">重置缩放</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
export default {
|
||||
props :{
|
||||
app: {
|
||||
type: Object
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isShow: false,
|
||||
left: 0,
|
||||
top: 0,
|
||||
isHasActiveElements: false,
|
||||
canMoveLevel: false,
|
||||
groupStatus: 'disabled',
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
groupStatus(newValue, oldValue) {
|
||||
switch (newValue) {
|
||||
case 'disabled':
|
||||
this.groupBtnText = '编组'
|
||||
break
|
||||
case 'dogroup':
|
||||
this.groupBtnText = '编组'
|
||||
break
|
||||
case 'ungroup':
|
||||
this.groupBtnText = '取消编组'
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.init();
|
||||
},
|
||||
methods: {
|
||||
init(){
|
||||
this.app.on('contextmenu', this.show)
|
||||
document.body.addEventListener('click', this.hide)
|
||||
},
|
||||
hide() {
|
||||
this.isShow = false
|
||||
this.left = 0
|
||||
this.top = 0
|
||||
},
|
||||
exec(command) {
|
||||
switch (command) {
|
||||
case 'moveUp':
|
||||
this.app.moveUpCurrentElement()
|
||||
break
|
||||
case 'moveDown':
|
||||
this.app.moveDownCurrentElement()
|
||||
break
|
||||
case 'moveTop':
|
||||
this.app.moveTopCurrentElement()
|
||||
break
|
||||
case 'moveBottom':
|
||||
this.app.moveBottomCurrentElement()
|
||||
break
|
||||
case 'del':
|
||||
this.app.deleteCurrentElements()
|
||||
break
|
||||
case 'copy':
|
||||
this.app.copyPasteCurrentElements()
|
||||
break
|
||||
case 'selectAll':
|
||||
this.app.selectAll()
|
||||
break
|
||||
case 'backToCenter':
|
||||
this.app.scrollToCenter()
|
||||
break
|
||||
case 'fit':
|
||||
this.app.fit()
|
||||
break
|
||||
case 'resetZoom':
|
||||
this.app.setZoom(1)
|
||||
case 'dogroup':
|
||||
this.app.dogroup()
|
||||
break
|
||||
case 'ungroup':
|
||||
this.app.ungroup()
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
},
|
||||
show(e, activeElements) {
|
||||
this.isHasActiveElements = activeElements.length > 0
|
||||
this.canMoveLevel = activeElements.length === 1
|
||||
this.left = e.clientX + 10
|
||||
this.top = e.clientY + 10
|
||||
this.isShow = true
|
||||
this.handleGroup(activeElements)
|
||||
},
|
||||
handleGroup(activeElements) {
|
||||
let isGroup = true
|
||||
activeElements.forEach(item => {
|
||||
if (!item.hasGroup()) {
|
||||
isGroup = false
|
||||
}
|
||||
})
|
||||
if (isGroup) {
|
||||
this.groupStatus = 'ungroup'
|
||||
} else if (activeElements.length > 1) {
|
||||
this.groupStatus = 'dogroup'
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.contextmenuContainer {
|
||||
position: fixed;
|
||||
width: 161px;
|
||||
background: #fff;
|
||||
box-shadow: 0 4px 12px 0 hsla(0, 0%, 69%, 0.5);
|
||||
border-radius: 4px;
|
||||
padding-top: 16px;
|
||||
padding-bottom: 16px;
|
||||
font-size: 14px;
|
||||
font-family: PingFangSC-Regular, PingFang SC;
|
||||
font-weight: 400;
|
||||
color: #1a1a1a;
|
||||
|
||||
.splitLine {
|
||||
height: 1px;
|
||||
background-color: #f5f5f5;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.item {
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
padding-left: 16px;
|
||||
cursor: pointer;
|
||||
&.danger {
|
||||
color: #f56c6c;
|
||||
}
|
||||
&:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
&.disabled {
|
||||
color: grey;
|
||||
cursor: not-allowed;
|
||||
&:hover {
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,120 @@
|
|||
// 描边颜色
|
||||
export const strokeColorList = [
|
||||
'#000000',
|
||||
'#343a40',
|
||||
'#495057',
|
||||
'#c92a2a',
|
||||
'#a61e4d',
|
||||
'#862e9c',
|
||||
'#5f3dc4',
|
||||
'#364fc7',
|
||||
'#1864ab',
|
||||
'#0b7285',
|
||||
'#087f5b',
|
||||
'#2b8a3e',
|
||||
'#5c940d',
|
||||
'#e67700',
|
||||
'#d9480f'
|
||||
]
|
||||
|
||||
// 填充颜色
|
||||
export const fillColorList = [
|
||||
'transparent',
|
||||
'#ced4da',
|
||||
'#868e96',
|
||||
'#fa5252',
|
||||
'#e64980',
|
||||
'#be4bdb',
|
||||
'#7950f2',
|
||||
'#4c6ef5',
|
||||
'#228be6',
|
||||
'#15aabf',
|
||||
'#12b886',
|
||||
'#40c057',
|
||||
'#82c91e',
|
||||
'#fab005',
|
||||
'#fd7e14'
|
||||
]
|
||||
|
||||
// 背景颜色
|
||||
export const backgroundColorList = [
|
||||
'#ffffff',
|
||||
'#f8f9fa',
|
||||
'#f1f3f5',
|
||||
'#fff5f5',
|
||||
'#fff0f6',
|
||||
'#f8f0fc',
|
||||
'#f3f0ff',
|
||||
'#edf2ff',
|
||||
'#e7f5ff',
|
||||
'#e3fafc',
|
||||
'#e6fcf5',
|
||||
'#ebfbee',
|
||||
'#f4fce3',
|
||||
'#fff9db',
|
||||
'#fff4e6'
|
||||
]
|
||||
|
||||
// 字体列表
|
||||
export const fontFamilyList = [
|
||||
{
|
||||
name: '微软雅黑',
|
||||
value: '微软雅黑, Microsoft YaHei'
|
||||
},
|
||||
{
|
||||
name: '宋体',
|
||||
value: '宋体, SimSun, Songti SC'
|
||||
},
|
||||
{
|
||||
name: '楷体',
|
||||
value: '楷体, 楷体_GB2312, SimKai, STKaiti'
|
||||
},
|
||||
{
|
||||
name: '黑体',
|
||||
value: '黑体, SimHei, Heiti SC'
|
||||
},
|
||||
{
|
||||
name: '隶书',
|
||||
value: '隶书, SimLi'
|
||||
},
|
||||
{
|
||||
name: 'Andale Mono',
|
||||
value: 'andale mono'
|
||||
},
|
||||
{
|
||||
name: 'Arial',
|
||||
value: 'arial, helvetica, sans-serif'
|
||||
},
|
||||
{
|
||||
name: 'arialBlack',
|
||||
value: 'arial black, avant garde'
|
||||
},
|
||||
{
|
||||
name: 'Comic Sans Ms',
|
||||
value: 'comic sans ms'
|
||||
},
|
||||
{
|
||||
name: 'Impact',
|
||||
value: 'impact, chicago'
|
||||
},
|
||||
{
|
||||
name: 'Times New Roman',
|
||||
value: 'times new roman'
|
||||
},
|
||||
{
|
||||
name: 'Sans-Serif',
|
||||
value: 'sans-serif'
|
||||
},
|
||||
{
|
||||
name: 'serif',
|
||||
value: 'serif'
|
||||
}
|
||||
]
|
||||
|
||||
// 字号
|
||||
export const fontSizeList = [10, 12, 16, 18, 24, 32, 48].map(item => {
|
||||
return {
|
||||
name: item,
|
||||
value: item
|
||||
}
|
||||
})
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="11" height="22"><defs><linearGradient id="a"><stop offset="0"/><stop offset="1" stop-opacity="0"/></linearGradient><radialGradient xlink:href="#a" cx="9.739" cy="9.716" fx="9.739" fy="9.716" r="3.709" gradientUnits="userSpaceOnUse"/></defs><g stroke="#000" fill="none"><g transform="translate(-129.5 -333.862) translate(0 .188)"><rect transform="matrix(.962 0 0 .971 4.943 11.548)" ry="2" rx="2" y="332.362" x="130" height="10.337" width="10.432" opacity=".5"/><g><path d="M132 339.175h6" opacity=".5"/><path d="M135 336.175v6" opacity=".5"/></g></g><g transform="translate(-129.5 -333.862)"><rect width="10.432" height="10.337" x="130" y="332.362" rx="2" ry="2" transform="matrix(.962 0 0 .971 4.943 22.736)" opacity=".5"/><path d="M132 350.362h6" opacity=".5"/></g></g></svg>
|
After Width: | Height: | Size: 867 B |
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* JSON Tree Viewer
|
||||
* http://github.com/summerstyle/jsonTreeViewer
|
||||
*
|
||||
* Copyright 2017 Vera Lobacheva (http://iamvera.com)
|
||||
* Released under the MIT license (LICENSE.txt)
|
||||
*/
|
||||
|
||||
/* Background for the tree. May use for <body> element */
|
||||
.jsontree_bg {
|
||||
background: #FFF;
|
||||
}
|
||||
|
||||
/* Styles for the container of the tree (e.g. fonts, margins etc.) */
|
||||
.jsontree_tree {
|
||||
margin-left: 30px;
|
||||
font-family: 'PT Mono', monospace;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Styles for a list of child nodes */
|
||||
.jsontree_child-nodes {
|
||||
display: none;
|
||||
margin-left: 35px;
|
||||
margin-bottom: 5px;
|
||||
line-height: 2;
|
||||
}
|
||||
.jsontree_node_expanded > .jsontree_value-wrapper > .jsontree_value > .jsontree_child-nodes {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Styles for labels */
|
||||
.jsontree_label-wrapper {
|
||||
float: left;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.jsontree_label {
|
||||
font-weight: normal;
|
||||
vertical-align: top;
|
||||
color: #000;
|
||||
position: relative;
|
||||
padding: 1px;
|
||||
border-radius: 4px;
|
||||
cursor: default;
|
||||
}
|
||||
.jsontree_node_marked > .jsontree_label-wrapper > .jsontree_label {
|
||||
background: #fff2aa;
|
||||
}
|
||||
|
||||
/* Styles for values */
|
||||
.jsontree_value-wrapper {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
}
|
||||
.jsontree_node_complex > .jsontree_value-wrapper {
|
||||
overflow: inherit;
|
||||
}
|
||||
.jsontree_value {
|
||||
vertical-align: top;
|
||||
display: inline;
|
||||
}
|
||||
.jsontree_value_null {
|
||||
color: #777;
|
||||
font-weight: bold;
|
||||
}
|
||||
.jsontree_value_string {
|
||||
color: #025900;
|
||||
font-weight: bold;
|
||||
}
|
||||
.jsontree_value_number {
|
||||
color: #000E59;
|
||||
font-weight: bold;
|
||||
}
|
||||
.jsontree_value_boolean {
|
||||
color: #600100;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Styles for active elements */
|
||||
.jsontree_expand-button {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: -15px;
|
||||
display: block;
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
background-image: url('icons.svg');
|
||||
}
|
||||
.jsontree_node_expanded > .jsontree_label-wrapper > .jsontree_label > .jsontree_expand-button {
|
||||
background-position: 0 -11px;
|
||||
}
|
||||
.jsontree_show-more {
|
||||
cursor: pointer;
|
||||
}
|
||||
.jsontree_node_expanded > .jsontree_value-wrapper > .jsontree_value > .jsontree_show-more {
|
||||
display: none;
|
||||
}
|
||||
.jsontree_node_empty > .jsontree_label-wrapper > .jsontree_label > .jsontree_expand-button,
|
||||
.jsontree_node_empty > .jsontree_value-wrapper > .jsontree_value > .jsontree_show-more {
|
||||
display: none !important;
|
||||
}
|
||||
.jsontree_node_complex > .jsontree_label-wrapper > .jsontree_label {
|
||||
cursor: pointer;
|
||||
}
|
||||
.jsontree_node_empty > .jsontree_label-wrapper > .jsontree_label {
|
||||
cursor: default !important;
|
||||
}
|
|
@ -0,0 +1,822 @@
|
|||
/**
|
||||
* JSON Tree library (a part of jsonTreeViewer)
|
||||
* http://github.com/summerstyle/jsonTreeViewer
|
||||
*
|
||||
* Copyright 2017 Vera Lobacheva (http://iamvera.com)
|
||||
* Released under the MIT license (LICENSE.txt)
|
||||
*/
|
||||
|
||||
var jsonTree = (function() {
|
||||
|
||||
/* ---------- Utilities ---------- */
|
||||
var utils = {
|
||||
|
||||
/*
|
||||
* Returns js-"class" of value
|
||||
*
|
||||
* @param val {any type} - value
|
||||
* @returns {string} - for example, "[object Function]"
|
||||
*/
|
||||
getClass : function(val) {
|
||||
return Object.prototype.toString.call(val);
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks for a type of value (for valid JSON data types).
|
||||
* In other cases - throws an exception
|
||||
*
|
||||
* @param val {any type} - the value for new node
|
||||
* @returns {string} ("object" | "array" | "null" | "boolean" | "number" | "string")
|
||||
*/
|
||||
getType : function(val) {
|
||||
if (val === null) {
|
||||
return 'null';
|
||||
}
|
||||
|
||||
switch (typeof val) {
|
||||
case 'number':
|
||||
return 'number';
|
||||
|
||||
case 'string':
|
||||
return 'string';
|
||||
|
||||
case 'boolean':
|
||||
return 'boolean';
|
||||
}
|
||||
|
||||
switch(utils.getClass(val)) {
|
||||
case '[object Array]':
|
||||
return 'array';
|
||||
|
||||
case '[object Object]':
|
||||
return 'object';
|
||||
}
|
||||
|
||||
throw new Error('Bad type: ' + utils.getClass(val));
|
||||
},
|
||||
|
||||
/**
|
||||
* Applies for each item of list some function
|
||||
* and checks for last element of the list
|
||||
*
|
||||
* @param obj {Object | Array} - a list or a dict with child nodes
|
||||
* @param func {Function} - the function for each item
|
||||
*/
|
||||
forEachNode : function(obj, func) {
|
||||
var type = utils.getType(obj),
|
||||
isLast;
|
||||
|
||||
switch (type) {
|
||||
case 'array':
|
||||
isLast = obj.length - 1;
|
||||
|
||||
obj.forEach(function(item, i) {
|
||||
func(i, item, i === isLast);
|
||||
});
|
||||
|
||||
break;
|
||||
|
||||
case 'object':
|
||||
var keys = Object.keys(obj).sort();
|
||||
|
||||
isLast = keys.length - 1;
|
||||
|
||||
keys.forEach(function(item, i) {
|
||||
func(item, obj[item], i === isLast);
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Implements the kind of an inheritance by
|
||||
* using parent prototype and
|
||||
* creating intermediate constructor
|
||||
*
|
||||
* @param Child {Function} - a child constructor
|
||||
* @param Parent {Function} - a parent constructor
|
||||
*/
|
||||
inherits : (function() {
|
||||
var F = function() {};
|
||||
|
||||
return function(Child, Parent) {
|
||||
F.prototype = Parent.prototype;
|
||||
Child.prototype = new F();
|
||||
Child.prototype.constructor = Child;
|
||||
};
|
||||
})(),
|
||||
|
||||
/*
|
||||
* Checks for a valid type of root node*
|
||||
*
|
||||
* @param {any type} jsonObj - a value for root node
|
||||
* @returns {boolean} - true for an object or an array, false otherwise
|
||||
*/
|
||||
isValidRoot : function(jsonObj) {
|
||||
switch (utils.getType(jsonObj)) {
|
||||
case 'object':
|
||||
case 'array':
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Extends some object
|
||||
*/
|
||||
extend : function(targetObj, sourceObj) {
|
||||
for (var prop in sourceObj) {
|
||||
if (sourceObj.hasOwnProperty(prop)) {
|
||||
targetObj[prop] = sourceObj[prop];
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/* ---------- Node constructors ---------- */
|
||||
|
||||
/**
|
||||
* The factory for creating nodes of defined type.
|
||||
*
|
||||
* ~~~ Node ~~~ is a structure element of an onject or an array
|
||||
* with own label (a key of an object or an index of an array)
|
||||
* and value of any json data type. The root object or array
|
||||
* is a node without label.
|
||||
* {...
|
||||
* [+] "label": value,
|
||||
* ...}
|
||||
*
|
||||
* Markup:
|
||||
* <li class="jsontree_node [jsontree_node_expanded]">
|
||||
* <span class="jsontree_label-wrapper">
|
||||
* <span class="jsontree_label">
|
||||
* <span class="jsontree_expand-button" />
|
||||
* "label"
|
||||
* </span>
|
||||
* :
|
||||
* </span>
|
||||
* <(div|span) class="jsontree_value jsontree_value_(object|array|boolean|null|number|string)">
|
||||
* ...
|
||||
* </(div|span)>
|
||||
* </li>
|
||||
*
|
||||
* @param label {string} - key name
|
||||
* @param val {Object | Array | string | number | boolean | null} - a value of node
|
||||
* @param isLast {boolean} - true if node is last in list of siblings
|
||||
*
|
||||
* @return {Node}
|
||||
*/
|
||||
function Node(label, val, isLast) {
|
||||
var nodeType = utils.getType(val);
|
||||
|
||||
if (nodeType in Node.CONSTRUCTORS) {
|
||||
return new Node.CONSTRUCTORS[nodeType](label, val, isLast);
|
||||
} else {
|
||||
throw new Error('Bad type: ' + utils.getClass(val));
|
||||
}
|
||||
}
|
||||
|
||||
Node.CONSTRUCTORS = {
|
||||
'boolean' : NodeBoolean,
|
||||
'number' : NodeNumber,
|
||||
'string' : NodeString,
|
||||
'null' : NodeNull,
|
||||
'object' : NodeObject,
|
||||
'array' : NodeArray
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
* The constructor for simple types (string, number, boolean, null)
|
||||
* {...
|
||||
* [+] "label": value,
|
||||
* ...}
|
||||
* value = string || number || boolean || null
|
||||
*
|
||||
* Markup:
|
||||
* <li class="jsontree_node">
|
||||
* <span class="jsontree_label-wrapper">
|
||||
* <span class="jsontree_label">"age"</span>
|
||||
* :
|
||||
* </span>
|
||||
* <span class="jsontree_value jsontree_value_(number|boolean|string|null)">25</span>
|
||||
* ,
|
||||
* </li>
|
||||
*
|
||||
* @abstract
|
||||
* @param label {string} - key name
|
||||
* @param val {string | number | boolean | null} - a value of simple types
|
||||
* @param isLast {boolean} - true if node is last in list of parent childNodes
|
||||
*/
|
||||
function _NodeSimple(label, val, isLast) {
|
||||
if (this.constructor === _NodeSimple) {
|
||||
throw new Error('This is abstract class');
|
||||
}
|
||||
|
||||
var self = this,
|
||||
el = document.createElement('li'),
|
||||
labelEl,
|
||||
template = function(label, val) {
|
||||
var str = '\
|
||||
<span class="jsontree_label-wrapper">\
|
||||
<span class="jsontree_label">"' +
|
||||
label +
|
||||
'"</span> : \
|
||||
</span>\
|
||||
<span class="jsontree_value-wrapper">\
|
||||
<span class="jsontree_value jsontree_value_' + self.type + '">' +
|
||||
val +
|
||||
'</span>' +
|
||||
(!isLast ? ',' : '') +
|
||||
'</span>';
|
||||
|
||||
return str;
|
||||
};
|
||||
|
||||
self.label = label;
|
||||
self.isComplex = false;
|
||||
|
||||
el.classList.add('jsontree_node');
|
||||
el.innerHTML = template(label, val);
|
||||
|
||||
self.el = el;
|
||||
|
||||
labelEl = el.querySelector('.jsontree_label');
|
||||
|
||||
labelEl.addEventListener('click', function(e) {
|
||||
if (e.altKey) {
|
||||
self.toggleMarked();
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.shiftKey) {
|
||||
document.getSelection().removeAllRanges();
|
||||
alert(self.getJSONPath());
|
||||
return;
|
||||
}
|
||||
}, false);
|
||||
}
|
||||
|
||||
_NodeSimple.prototype = {
|
||||
constructor : _NodeSimple,
|
||||
|
||||
/**
|
||||
* Mark node
|
||||
*/
|
||||
mark : function() {
|
||||
this.el.classList.add('jsontree_node_marked');
|
||||
},
|
||||
|
||||
/**
|
||||
* Unmark node
|
||||
*/
|
||||
unmark : function() {
|
||||
this.el.classList.remove('jsontree_node_marked');
|
||||
},
|
||||
|
||||
/**
|
||||
* Mark or unmark node
|
||||
*/
|
||||
toggleMarked : function() {
|
||||
this.el.classList.toggle('jsontree_node_marked');
|
||||
},
|
||||
|
||||
/**
|
||||
* Expands parent node of this node
|
||||
*
|
||||
* @param isRecursive {boolean} - if true, expands all parent nodes
|
||||
* (from node to root)
|
||||
*/
|
||||
expandParent : function(isRecursive) {
|
||||
if (!this.parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.parent.expand();
|
||||
this.parent.expandParent(isRecursive);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns JSON-path of this
|
||||
*
|
||||
* @param isInDotNotation {boolean} - kind of notation for returned json-path
|
||||
* (by default, in bracket notation)
|
||||
* @returns {string}
|
||||
*/
|
||||
getJSONPath : function(isInDotNotation) {
|
||||
if (this.isRoot) {
|
||||
return "$";
|
||||
}
|
||||
|
||||
var currentPath;
|
||||
|
||||
if (this.parent.type === 'array') {
|
||||
currentPath = "[" + this.label + "]";
|
||||
} else {
|
||||
currentPath = isInDotNotation ? "." + this.label : "['" + this.label + "']";
|
||||
}
|
||||
|
||||
return this.parent.getJSONPath(isInDotNotation) + currentPath;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
* The constructor for boolean values
|
||||
* {...
|
||||
* [+] "label": boolean,
|
||||
* ...}
|
||||
* boolean = true || false
|
||||
*
|
||||
* @constructor
|
||||
* @param label {string} - key name
|
||||
* @param val {boolean} - value of boolean type, true or false
|
||||
* @param isLast {boolean} - true if node is last in list of parent childNodes
|
||||
*/
|
||||
function NodeBoolean(label, val, isLast) {
|
||||
this.type = "boolean";
|
||||
|
||||
_NodeSimple.call(this, label, val, isLast);
|
||||
}
|
||||
utils.inherits(NodeBoolean,_NodeSimple);
|
||||
|
||||
|
||||
/*
|
||||
* The constructor for number values
|
||||
* {...
|
||||
* [+] "label": number,
|
||||
* ...}
|
||||
* number = 123
|
||||
*
|
||||
* @constructor
|
||||
* @param label {string} - key name
|
||||
* @param val {number} - value of number type, for example 123
|
||||
* @param isLast {boolean} - true if node is last in list of parent childNodes
|
||||
*/
|
||||
function NodeNumber(label, val, isLast) {
|
||||
this.type = "number";
|
||||
|
||||
_NodeSimple.call(this, label, val, isLast);
|
||||
}
|
||||
utils.inherits(NodeNumber,_NodeSimple);
|
||||
|
||||
|
||||
/*
|
||||
* The constructor for string values
|
||||
* {...
|
||||
* [+] "label": string,
|
||||
* ...}
|
||||
* string = "abc"
|
||||
*
|
||||
* @constructor
|
||||
* @param label {string} - key name
|
||||
* @param val {string} - value of string type, for example "abc"
|
||||
* @param isLast {boolean} - true if node is last in list of parent childNodes
|
||||
*/
|
||||
function NodeString(label, val, isLast) {
|
||||
this.type = "string";
|
||||
|
||||
_NodeSimple.call(this, label, '"' + val + '"', isLast);
|
||||
}
|
||||
utils.inherits(NodeString,_NodeSimple);
|
||||
|
||||
|
||||
/*
|
||||
* The constructor for null values
|
||||
* {...
|
||||
* [+] "label": null,
|
||||
* ...}
|
||||
*
|
||||
* @constructor
|
||||
* @param label {string} - key name
|
||||
* @param val {null} - value (only null)
|
||||
* @param isLast {boolean} - true if node is last in list of parent childNodes
|
||||
*/
|
||||
function NodeNull(label, val, isLast) {
|
||||
this.type = "null";
|
||||
|
||||
_NodeSimple.call(this, label, val, isLast);
|
||||
}
|
||||
utils.inherits(NodeNull,_NodeSimple);
|
||||
|
||||
|
||||
/*
|
||||
* The constructor for complex types (object, array)
|
||||
* {...
|
||||
* [+] "label": value,
|
||||
* ...}
|
||||
* value = object || array
|
||||
*
|
||||
* Markup:
|
||||
* <li class="jsontree_node jsontree_node_(object|array) [expanded]">
|
||||
* <span class="jsontree_label-wrapper">
|
||||
* <span class="jsontree_label">
|
||||
* <span class="jsontree_expand-button" />
|
||||
* "label"
|
||||
* </span>
|
||||
* :
|
||||
* </span>
|
||||
* <div class="jsontree_value">
|
||||
* <b>{</b>
|
||||
* <ul class="jsontree_child-nodes" />
|
||||
* <b>}</b>
|
||||
* ,
|
||||
* </div>
|
||||
* </li>
|
||||
*
|
||||
* @abstract
|
||||
* @param label {string} - key name
|
||||
* @param val {Object | Array} - a value of complex types, object or array
|
||||
* @param isLast {boolean} - true if node is last in list of parent childNodes
|
||||
*/
|
||||
function _NodeComplex(label, val, isLast) {
|
||||
if (this.constructor === _NodeComplex) {
|
||||
throw new Error('This is abstract class');
|
||||
}
|
||||
|
||||
var self = this,
|
||||
el = document.createElement('li'),
|
||||
template = function(label, sym) {
|
||||
var comma = (!isLast) ? ',' : '',
|
||||
str = '\
|
||||
<div class="jsontree_value-wrapper">\
|
||||
<div class="jsontree_value jsontree_value_' + self.type + '">\
|
||||
<b>' + sym[0] + '</b>\
|
||||
<span class="jsontree_show-more">…</span>\
|
||||
<ul class="jsontree_child-nodes"></ul>\
|
||||
<b>' + sym[1] + '</b>' +
|
||||
'</div>' + comma +
|
||||
'</div>';
|
||||
|
||||
if (label !== null) {
|
||||
str = '\
|
||||
<span class="jsontree_label-wrapper">\
|
||||
<span class="jsontree_label">' +
|
||||
'<span class="jsontree_expand-button"></span>' +
|
||||
'"' + label +
|
||||
'"</span> : \
|
||||
</span>' + str;
|
||||
}
|
||||
|
||||
return str;
|
||||
},
|
||||
childNodesUl,
|
||||
labelEl,
|
||||
moreContentEl,
|
||||
childNodes = [];
|
||||
|
||||
self.label = label;
|
||||
self.isComplex = true;
|
||||
|
||||
el.classList.add('jsontree_node');
|
||||
el.classList.add('jsontree_node_complex');
|
||||
el.innerHTML = template(label, self.sym);
|
||||
|
||||
childNodesUl = el.querySelector('.jsontree_child-nodes');
|
||||
|
||||
if (label !== null) {
|
||||
labelEl = el.querySelector('.jsontree_label');
|
||||
moreContentEl = el.querySelector('.jsontree_show-more');
|
||||
|
||||
labelEl.addEventListener('click', function(e) {
|
||||
if (e.altKey) {
|
||||
self.toggleMarked();
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.shiftKey) {
|
||||
document.getSelection().removeAllRanges();
|
||||
alert(self.getJSONPath());
|
||||
return;
|
||||
}
|
||||
|
||||
self.toggle(e.ctrlKey || e.metaKey);
|
||||
}, false);
|
||||
|
||||
moreContentEl.addEventListener('click', function(e) {
|
||||
self.toggle(e.ctrlKey || e.metaKey);
|
||||
}, false);
|
||||
|
||||
self.isRoot = false;
|
||||
} else {
|
||||
self.isRoot = true;
|
||||
self.parent = null;
|
||||
|
||||
el.classList.add('jsontree_node_expanded');
|
||||
}
|
||||
|
||||
self.el = el;
|
||||
self.childNodes = childNodes;
|
||||
self.childNodesUl = childNodesUl;
|
||||
|
||||
utils.forEachNode(val, function(label, node, isLast) {
|
||||
self.addChild(new Node(label, node, isLast));
|
||||
});
|
||||
|
||||
self.isEmpty = !Boolean(childNodes.length);
|
||||
if (self.isEmpty) {
|
||||
el.classList.add('jsontree_node_empty');
|
||||
}
|
||||
}
|
||||
|
||||
utils.inherits(_NodeComplex, _NodeSimple);
|
||||
|
||||
utils.extend(_NodeComplex.prototype, {
|
||||
constructor : _NodeComplex,
|
||||
|
||||
/*
|
||||
* Add child node to list of child nodes
|
||||
*
|
||||
* @param child {Node} - child node
|
||||
*/
|
||||
addChild : function(child) {
|
||||
this.childNodes.push(child);
|
||||
this.childNodesUl.appendChild(child.el);
|
||||
child.parent = this;
|
||||
},
|
||||
|
||||
/*
|
||||
* Expands this list of node child nodes
|
||||
*
|
||||
* @param isRecursive {boolean} - if true, expands all child nodes
|
||||
*/
|
||||
expand : function(isRecursive){
|
||||
if (this.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isRoot) {
|
||||
this.el.classList.add('jsontree_node_expanded');
|
||||
}
|
||||
|
||||
if (isRecursive) {
|
||||
this.childNodes.forEach(function(item, i) {
|
||||
if (item.isComplex) {
|
||||
item.expand(isRecursive);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
* Collapses this list of node child nodes
|
||||
*
|
||||
* @param isRecursive {boolean} - if true, collapses all child nodes
|
||||
*/
|
||||
collapse : function(isRecursive) {
|
||||
if (this.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isRoot) {
|
||||
this.el.classList.remove('jsontree_node_expanded');
|
||||
}
|
||||
|
||||
if (isRecursive) {
|
||||
this.childNodes.forEach(function(item, i) {
|
||||
if (item.isComplex) {
|
||||
item.collapse(isRecursive);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
* Expands collapsed or collapses expanded node
|
||||
*
|
||||
* @param {boolean} isRecursive - Expand all child nodes if this node is expanded
|
||||
* and collapse it otherwise
|
||||
*/
|
||||
toggle : function(isRecursive) {
|
||||
if (this.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.el.classList.toggle('jsontree_node_expanded');
|
||||
|
||||
if (isRecursive) {
|
||||
var isExpanded = this.el.classList.contains('jsontree_node_expanded');
|
||||
|
||||
this.childNodes.forEach(function(item, i) {
|
||||
if (item.isComplex) {
|
||||
item[isExpanded ? 'expand' : 'collapse'](isRecursive);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Find child nodes that match some conditions and handle it
|
||||
*
|
||||
* @param {Function} matcher
|
||||
* @param {Function} handler
|
||||
* @param {boolean} isRecursive
|
||||
*/
|
||||
findChildren : function(matcher, handler, isRecursive) {
|
||||
if (this.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.childNodes.forEach(function(item, i) {
|
||||
if (matcher(item)) {
|
||||
handler(item);
|
||||
}
|
||||
|
||||
if (item.isComplex && isRecursive) {
|
||||
item.findChildren(matcher, handler, isRecursive);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/*
|
||||
* The constructor for object values
|
||||
* {...
|
||||
* [+] "label": object,
|
||||
* ...}
|
||||
* object = {"abc": "def"}
|
||||
*
|
||||
* @constructor
|
||||
* @param label {string} - key name
|
||||
* @param val {Object} - value of object type, {"abc": "def"}
|
||||
* @param isLast {boolean} - true if node is last in list of siblings
|
||||
*/
|
||||
function NodeObject(label, val, isLast) {
|
||||
this.sym = ['{', '}'];
|
||||
this.type = "object";
|
||||
|
||||
_NodeComplex.call(this, label, val, isLast);
|
||||
}
|
||||
utils.inherits(NodeObject,_NodeComplex);
|
||||
|
||||
|
||||
/*
|
||||
* The constructor for array values
|
||||
* {...
|
||||
* [+] "label": array,
|
||||
* ...}
|
||||
* array = [1,2,3]
|
||||
*
|
||||
* @constructor
|
||||
* @param label {string} - key name
|
||||
* @param val {Array} - value of array type, [1,2,3]
|
||||
* @param isLast {boolean} - true if node is last in list of siblings
|
||||
*/
|
||||
function NodeArray(label, val, isLast) {
|
||||
this.sym = ['[', ']'];
|
||||
this.type = "array";
|
||||
|
||||
_NodeComplex.call(this, label, val, isLast);
|
||||
}
|
||||
utils.inherits(NodeArray, _NodeComplex);
|
||||
|
||||
|
||||
/* ---------- The tree constructor ---------- */
|
||||
|
||||
/*
|
||||
* The constructor for json tree.
|
||||
* It contains only one Node (Array or Object), without property name.
|
||||
* CSS-styles of .tree define main tree styles like font-family,
|
||||
* font-size and own margins.
|
||||
*
|
||||
* Markup:
|
||||
* <ul class="jsontree_tree clearfix">
|
||||
* {Node}
|
||||
* </ul>
|
||||
*
|
||||
* @constructor
|
||||
* @param jsonObj {Object | Array} - data for tree
|
||||
* @param domEl {DOMElement} - DOM-element, wrapper for tree
|
||||
*/
|
||||
function Tree(jsonObj, domEl) {
|
||||
this.wrapper = document.createElement('ul');
|
||||
this.wrapper.className = 'jsontree_tree clearfix';
|
||||
|
||||
this.rootNode = null;
|
||||
|
||||
this.sourceJSONObj = jsonObj;
|
||||
|
||||
this.loadData(jsonObj);
|
||||
this.appendTo(domEl);
|
||||
}
|
||||
|
||||
Tree.prototype = {
|
||||
constructor : Tree,
|
||||
|
||||
/**
|
||||
* Fill new data in current json tree
|
||||
*
|
||||
* @param {Object | Array} jsonObj - json-data
|
||||
*/
|
||||
loadData : function(jsonObj) {
|
||||
if (!utils.isValidRoot(jsonObj)) {
|
||||
alert('The root should be an object or an array');
|
||||
return;
|
||||
}
|
||||
|
||||
this.sourceJSONObj = jsonObj;
|
||||
|
||||
this.rootNode = new Node(null, jsonObj, 'last');
|
||||
this.wrapper.innerHTML = '';
|
||||
this.wrapper.appendChild(this.rootNode.el);
|
||||
},
|
||||
|
||||
/**
|
||||
* Appends tree to DOM-element (or move it to new place)
|
||||
*
|
||||
* @param {DOMElement} domEl
|
||||
*/
|
||||
appendTo : function(domEl) {
|
||||
domEl.appendChild(this.wrapper);
|
||||
},
|
||||
|
||||
/**
|
||||
* Expands all tree nodes (objects or arrays) recursively
|
||||
*
|
||||
* @param {Function} filterFunc - 'true' if this node should be expanded
|
||||
*/
|
||||
expand : function(filterFunc) {
|
||||
if (this.rootNode.isComplex) {
|
||||
if (typeof filterFunc == 'function') {
|
||||
this.rootNode.childNodes.forEach(function(item, i) {
|
||||
if (item.isComplex && filterFunc(item)) {
|
||||
item.expand();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.rootNode.expand('recursive');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Collapses all tree nodes (objects or arrays) recursively
|
||||
*/
|
||||
collapse : function() {
|
||||
if (typeof this.rootNode.collapse === 'function') {
|
||||
this.rootNode.collapse('recursive');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the source json-string (pretty-printed)
|
||||
*
|
||||
* @param {boolean} isPrettyPrinted - 'true' for pretty-printed string
|
||||
* @returns {string} - for exemple, '{"a":2,"b":3}'
|
||||
*/
|
||||
toSourceJSON : function(isPrettyPrinted) {
|
||||
if (!isPrettyPrinted) {
|
||||
return JSON.stringify(this.sourceJSONObj);
|
||||
}
|
||||
|
||||
var DELIMETER = "[%^$#$%^%]",
|
||||
jsonStr = JSON.stringify(this.sourceJSONObj, null, DELIMETER);
|
||||
|
||||
jsonStr = jsonStr.split("\n").join("<br />");
|
||||
jsonStr = jsonStr.split(DELIMETER).join(" ");
|
||||
|
||||
return jsonStr;
|
||||
},
|
||||
|
||||
/**
|
||||
* Find all nodes that match some conditions and handle it
|
||||
*/
|
||||
findAndHandle : function(matcher, handler) {
|
||||
this.rootNode.findChildren(matcher, handler, 'isRecursive');
|
||||
},
|
||||
|
||||
/**
|
||||
* Unmark all nodes
|
||||
*/
|
||||
unmarkAll : function() {
|
||||
this.rootNode.findChildren(function(node) {
|
||||
return true;
|
||||
}, function(node) {
|
||||
node.unmark();
|
||||
}, 'isRecursive');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/* ---------- Public methods ---------- */
|
||||
return {
|
||||
/**
|
||||
* Creates new tree by data and appends it to the DOM-element
|
||||
*
|
||||
* @param jsonObj {Object | Array} - json-data
|
||||
* @param domEl {DOMElement} - the wrapper element
|
||||
* @returns {Tree}
|
||||
*/
|
||||
create : function(jsonObj, domEl) {
|
||||
return new Tree(jsonObj, domEl);
|
||||
}
|
||||
};
|
||||
})();
|
||||
export default {
|
||||
jsonTree
|
||||
}
|
|
@ -0,0 +1,913 @@
|
|||
<template>
|
||||
<div class="whiteboart-container" :style="{ height: height + 'px' }">
|
||||
<div class="canvasBox" ref="box"></div>
|
||||
|
||||
<div class="footerLeft" @click.stop
|
||||
:style="type == 'design' ? ['top: 10px', 'justify-content: space-between'] : ['bottom: 10px', 'justify-content: center']">
|
||||
<div class="left">
|
||||
<!-- 前进回退 -->
|
||||
<div class="blockBox" v-if="!readonly && width >= 1000">
|
||||
<el-tooltip effect="light" content="回退" placement="top">
|
||||
<el-button :icon="RefreshLeft" circle :disabled="!canUndo" @click="undo" />
|
||||
</el-tooltip>
|
||||
<el-tooltip effect="light" content="前进" placement="top">
|
||||
<el-button :icon="RefreshRight" circle :disabled="!canRedo" @click="redo" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div class="blockBox">
|
||||
<el-button @click="currentType = 'selection'"><el-image src="/src/assets/icons/pngjpg/mouse-pointer.png"
|
||||
style="width: 14px; height: 14px; color: silver" /></el-button>
|
||||
</div>
|
||||
<template v-if="type == 'design'">
|
||||
<el-radio-group v-model="currentType" @change="onCurrentTypeChange">
|
||||
<el-tooltip effect="light" content="画笔" placement="top">
|
||||
<el-radio-button label="画笔" value="freedraw">
|
||||
<svg t="1719045569796" class="icon" viewBox="0 0 1031 1024" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" p-id="11317" width="14" height="14">
|
||||
<path
|
||||
d="M1001.476376 77.351719c-26.164542-24.960575-25.568765-24.364798-57.095299-53.495813-30.930758-29.118603-98.129443-36.255515-139.759364 5.349581S127.223222 709.769044 127.223222 709.769044 8.303642 983.181056 1.737683 998.646435 8.303642 1028.956592 23.198068 1022.415456s301.525241-125.361418 301.525241-125.361418l676.802715-676.455178c51.745718-51.708482 26.164542-118.286566 0-143.247141zM79.635531 942.171737l76.13534-175.34463 102.883247 99.854714z m233.730779-106.991627l-120.123545-116.499234 466.853369-464.83021 116.573707 112.949397z m510.283032-512.35584l-119.54018-116.499235 52.937272-52.900036 119.540181 116.561295z m135.601336-132.535567l-45.19217 45.179759-123.71062-118.894755s26.760319-26.15213 46.383725-45.167347 57.691076-17.836075 77.314483 1.241203c19.623406 18.431852 20.81496 19.610994 42.200873 40.413542 25.568765 24.364798 21.410737 58.845394 2.966473 77.264834z m0 0"
|
||||
fill="#848282" p-id="11318"></path>
|
||||
</svg>
|
||||
</el-radio-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip effect="light" content="文字" placement="top">
|
||||
<el-radio-button label="文字" value="text">
|
||||
<svg t="1719046751133" class="icon" viewBox="0 0 1024 1024" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" p-id="16523" width="14" height="14">
|
||||
<path
|
||||
d="M429.056 919.552h166.4l-0.512-1.024c-32.768-40.96-50.688-92.16-50.688-144.384V228.352h154.112c44.032 0 87.04 14.848 121.856 42.496l11.776 9.216V154.112s-128 20.48-319.488 20.48-321.024-20.48-321.024-20.48v125.44l8.192-6.656c35.328-28.672 79.36-44.544 124.928-44.544h155.648v545.28c0 52.736-17.92 103.424-50.688 144.384l-0.512 1.536z"
|
||||
fill="#848282" p-id="16524"></path>
|
||||
</svg>
|
||||
</el-radio-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip effect="light" content="图片" placement="top">
|
||||
<el-radio-button label="图片" value="image">
|
||||
<svg t="1719045309869" class="icon" viewBox="0 0 1024 1024" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" p-id="6679" width="14" height="14">
|
||||
<path
|
||||
d="M867.90864 574.538232V257.779543a50.844091 50.844091 0 0 0-50.844092-50.844091h-610.129096a50.844091 50.844091 0 0 0-50.844092 50.844091v499.797418l430.141013-257.779543a152.532274 152.532274 0 0 1 157.108243 0z m0 118.466733l-177.445879-106.264151a50.844091 50.844091 0 0 0-50.844092 0L254.220457 817.064548h562.844091a50.844091 50.844091 0 0 0 50.844092-50.844091z m-660.973188-587.757696h610.129096a152.532274 152.532274 0 0 1 152.532274 152.532274v508.440914a152.532274 152.532274 0 0 1-152.532274 152.532274h-610.129096a152.532274 152.532274 0 0 1-152.532274-152.532274v-508.440914a152.532274 152.532274 0 0 1 152.532274-152.532274z m127.110228 355.90864a76.266137 76.266137 0 1 1 76.266137-76.266137 76.266137 76.266137 0 0 1-76.266137 76.266137z"
|
||||
fill="#848282" p-id="6680"></path>
|
||||
</svg>
|
||||
</el-radio-button>
|
||||
</el-tooltip>
|
||||
</el-radio-group>
|
||||
</template>
|
||||
<div class="blockBox">
|
||||
<el-button @click="currentType = 'selection'" style="color:#848282" :icon="Camera" disabled></el-button>
|
||||
</div>
|
||||
<div class="blockBox" v-if="!readonly">
|
||||
<el-dropdown @command="handleToolTypeChange" placement="top">
|
||||
<el-button>{{ type == 'design' ? '形状' : '工具' }}</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="rectangle">矩形</el-dropdown-item>
|
||||
<el-dropdown-item command="diamond">菱形</el-dropdown-item>
|
||||
<el-dropdown-item command="triangle">三角形</el-dropdown-item>
|
||||
<el-dropdown-item command="circle">圆形</el-dropdown-item>
|
||||
<el-dropdown-item command="line">线段</el-dropdown-item>
|
||||
<el-dropdown-item command="arrow">箭头</el-dropdown-item>
|
||||
<template v-if="type != 'design'">
|
||||
<el-dropdown-item command="freedraw">画笔</el-dropdown-item>
|
||||
<el-dropdown-item command="text">文字</el-dropdown-item>
|
||||
<el-dropdown-item command="image">图片</el-dropdown-item>
|
||||
</template>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
<!-- AI -->
|
||||
<div class="blockBox">
|
||||
<el-dropdown @command="handleToolTypeChange" placement="top">
|
||||
<el-button type="warning">AI</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="rectangle">教学大模型</el-dropdown-item>
|
||||
<el-dropdown-item disabled command="diamond">单张图片创作</el-dropdown-item>
|
||||
<el-dropdown-item disabled command="triandle">连环画创作</el-dropdown-item>
|
||||
<el-dropdown-item disabled command="circle">视频创作</el-dropdown-item>
|
||||
<el-dropdown-item disabled command="line">音乐创作</el-dropdown-item>
|
||||
<el-dropdown-item disabled command="arrow">语音</el-dropdown-item>
|
||||
<el-dropdown-item disabled command="freedraw">多语言翻译</el-dropdown-item>
|
||||
<el-dropdown-item disabled command="text">数字人</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
|
||||
<!-- 只读、编辑模式切换 -->
|
||||
<!-- <div class="blockBox">
|
||||
<el-tooltip effect="light" :content="elReadonly ? '元素设置为可编辑' : '元素设置为只读'" placement="top">
|
||||
<el-button :icon="elReadonly ? View : Edit" circle @click="elementModeChange" />
|
||||
</el-tooltip>
|
||||
</div> -->
|
||||
|
||||
<template v-if="!readonly">
|
||||
<!-- 描边 -->
|
||||
<div class="blockBox">
|
||||
<el-tooltip effect="light" content="描边" placement="top">
|
||||
<ColorPicker type="stroke" :value="activeElement?.style.strokeStyle"
|
||||
@change="updateStyle('strokeStyle', $event)"></ColorPicker>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<!-- 填充 -->
|
||||
<div class="blockBox">
|
||||
<el-tooltip effect="light" content="填充" placement="top">
|
||||
<ColorPicker type="fill" :value="activeElement?.style.fillStyle"
|
||||
@change="updateStyle('fillStyle', $event)">
|
||||
</ColorPicker>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<!-- 边框样式 -->
|
||||
<div class="blockBox">
|
||||
<el-dropdown @command="updateStyle('lineDash', $event)" placement="top">
|
||||
<el-button><el-image src="/src/assets/icons/pngjpg/borderstyle.png"
|
||||
style="width: 14px; height: 14px"></el-image></el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="0">实线</el-dropdown-item>
|
||||
<el-dropdown-item command="1">大虚线</el-dropdown-item>
|
||||
<el-dropdown-item command="2">小虚线</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
<!--透明度-->
|
||||
<div class="blockBox" style="width: 120px"
|
||||
v-if="type == 'design' ? true : ['image'].includes(activeElement?.type) || hasSelectedElements">
|
||||
<el-tooltip effect="light" content="透明度" placement="top">
|
||||
<el-input-number v-model="globalAlpha" :min="0" :max="1" :step="0.1"
|
||||
@change="updateStyle('globalAlpha', $event)"></el-input-number>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<!-- 边框粗细 -->
|
||||
<div class="blockBox">
|
||||
<el-dropdown @command="updateStyle('lineWidth', $event)" placement="top">
|
||||
<el-button><el-image src="/src/assets/icons/pngjpg/borderwidth.png"
|
||||
style="width: 14px; height: 14px"></el-image></el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="small">细</el-dropdown-item>
|
||||
<el-dropdown-item command="middle">正常</el-dropdown-item>
|
||||
<el-dropdown-item command="large">粗</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
<!-- 字体 -->
|
||||
<div class="blockBox"
|
||||
v-if="type == 'design' ? true : ['text'].includes(activeElement?.type) || hasSelectedElements">
|
||||
<div class="styleBlockContent">
|
||||
<el-select v-model="fontFamily" placeholder="字体" @change="updateStyle('fontFamily', $event)"
|
||||
style="width: 110px">
|
||||
<el-option v-for="item in fontFamilyList" :key="item.value" :label="item.name" :value="item.value"
|
||||
:style="{ fontFamily: item.value }"></el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 字号 -->
|
||||
<div class="blockBox"
|
||||
v-if="type == 'design' ? true : ['text'].includes(activeElement?.type) || hasSelectedElements">
|
||||
<div class="styleBlockContent">
|
||||
<el-select v-model="fontSize" placeholder="字号" @change="updateStyle('fontSize', $event)"
|
||||
style="width: 80px">
|
||||
<el-option v-for="item in fontSizeList" :key="item.value" :label="item.name" :value="item.value"
|
||||
:style="{ fontSize: item.value }"></el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 滚动 -->
|
||||
<div v-if="width >= 1000" class="blockBox">
|
||||
<template v-if="type == 'design'">
|
||||
<el-tooltip effect="light" content="滚动至中心" placement="top">
|
||||
<el-button icon="Operation" @click="scrollToCenter" />
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-button @click="scrollToCenter">居中</el-button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 缩放 -->
|
||||
<!-- <div v-if="width>=1000" class="blockBox">
|
||||
<el-tooltip effect="light" content="缩小" placement="top">
|
||||
<el-button :icon="ZoomOut" circle @click="zoomOut" />
|
||||
</el-tooltip>
|
||||
<el-tooltip effect="light" content="放大" placement="top">
|
||||
<el-button :icon="ZoomIn" circle @click="zoomIn" />
|
||||
</el-tooltip>
|
||||
</div> -->
|
||||
|
||||
<!-- 橡皮擦、显示网格、清空 -->
|
||||
<div v-if="width >= 1000" class="blockBox">
|
||||
<!-- 橡皮擦 -->
|
||||
<el-tooltip effect="light" :content="currentType === 'eraser' ? '关闭橡皮擦' : '橡皮擦'" placement="top">
|
||||
<el-button v-if="!readonly" :icon="Remove" circle :type="currentType === 'eraser' ? 'primary' : null"
|
||||
@click="toggleEraser" />
|
||||
</el-tooltip>
|
||||
<!-- 清空 -->
|
||||
<el-tooltip effect="light" content="清空" placement="top">
|
||||
<el-button v-if="!readonly" :icon="Delete" circle @click="empty" />
|
||||
</el-tooltip>
|
||||
<!-- 网格 -->
|
||||
<el-tooltip effect="light" :content="showGrid ? '隐藏网格' : '显示网格'" placement="top">
|
||||
<el-button :icon="Grid" circle :type="showGrid ? 'primary' : null" @click="toggleGrid" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<!-- 保存提交 -->
|
||||
<div class="blockBox" v-if="!readonly && isShowSave">
|
||||
<el-tooltip effect="light" :content="type == 'design' ? '保存底板' : '保存'" placement="top">
|
||||
<el-button type="success" style="margin-right: 10px" @click="onSave">{{ type == 'design' ? '保存底板' : '保存'
|
||||
}}</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<!-- 点赞、评价等 -->
|
||||
<template v-if="allowComment == true">
|
||||
<div class="blockBox" style="margin-left: 50px">
|
||||
<el-tooltip effect="light" content="评价" placement="top">
|
||||
<el-button :icon="ChromeFilled" type="success" style="margin-right: 10px" @click="onSave">写评价</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip effect="light" content="放大" placement="top">
|
||||
<el-button circle type="success" @click="zoomIn"><el-image src="/src/assets/icons/pngjpg/img-thumbup.png"
|
||||
style="height: 14px; width: 14px"></el-image></el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
</div>
|
||||
<!-- 课件设计才会有关闭按钮-->
|
||||
<el-button v-if="type == 'design'" @click="closeBoard">关闭</el-button>
|
||||
</div>
|
||||
<!-- 导出图片弹窗 -->
|
||||
<el-dialog v-model="exportImageDialogVisible" title="导出为图片" :width="800">
|
||||
<div class="exportImageContainer">
|
||||
<div class="imagePreviewBox">
|
||||
<img :src="exportImageUrl" alt="" />
|
||||
</div>
|
||||
<div class="handleBox">
|
||||
<el-checkbox v-model="exportOnlySelected" label="仅导出被选中" size="large" @change="reRenderExportImage"
|
||||
style="margin-right: 10px" />
|
||||
<el-checkbox v-model="exportRenderBackground" label="背景" size="large" @change="reRenderExportImage"
|
||||
style="margin-right: 10px" />
|
||||
<el-input v-model="exportFileName" style="width: 150px; margin-right: 10px"></el-input>
|
||||
<el-input-number v-model="exportImagePaddingX" :min="10" :max="100" :step="5" controls-position="right"
|
||||
@change="reRenderExportImage" style="margin-right: 10px" />
|
||||
<el-input-number v-model="exportImagePaddingY" :min="10" :max="100" :step="5" controls-position="right"
|
||||
@change="reRenderExportImage" style="margin-right: 10px" />
|
||||
<el-button type="primary" @click="downloadExportImage">下载</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
<!-- 导出json弹窗 -->
|
||||
<el-dialog v-model="exportJsonDialogVisible" title="导出为json" :width="800">
|
||||
<div class="exportJsonContainer">
|
||||
<div class="jsonPreviewBox" ref="jsonPreviewBox"></div>
|
||||
<div class="handleBox">
|
||||
<el-input v-model="exportFileName" style="width: 150px; margin-right: 10px"></el-input>
|
||||
<el-button type="primary" @click="downloadExportJson">下载</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<Contextmenu v-if="appInstance" :app="appInstance"></Contextmenu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref, watch, toRaw, nextTick, computed, reactive, defineProps, defineEmits } from 'vue'
|
||||
import TinyWhiteboard from 'whiteboard_lyc'
|
||||
import ColorPicker from './components/ColorPicker.vue'
|
||||
import {
|
||||
Camera,
|
||||
Delete,
|
||||
CopyDocument,
|
||||
ZoomIn,
|
||||
ZoomOut,
|
||||
Remove,
|
||||
RefreshLeft,
|
||||
RefreshRight,
|
||||
Download,
|
||||
Upload,
|
||||
CaretTop,
|
||||
CaretBottom,
|
||||
Minus,
|
||||
Grid,
|
||||
View,
|
||||
Edit,
|
||||
QuestionFilled, ChromeFilled
|
||||
} from '@element-plus/icons-vue'
|
||||
import Contextmenu from './components/Contextmenu.vue'
|
||||
import { fontFamilyList, fontSizeList } from './constants'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 700,
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 1000,
|
||||
},
|
||||
data: {
|
||||
type: [String, Object],
|
||||
default: ''
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
allowComment: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
type: {
|
||||
type: [String, undefined],
|
||||
default: undefined
|
||||
},
|
||||
isShowSave: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
// 定义要发送的emit事件
|
||||
const emit = defineEmits(['handleSave', 'update:modelValue'])
|
||||
|
||||
// 当前操作类型
|
||||
const currentType = ref('selection')
|
||||
|
||||
// dom节点
|
||||
const box = ref(null)
|
||||
|
||||
// 应用实例
|
||||
let app = null
|
||||
const appInstance = ref(null)
|
||||
// 当前激活的元素
|
||||
const activeElement = ref(null)
|
||||
// 当前多选的元素
|
||||
const selectedElements = ref([])
|
||||
const hasSelectedElements = computed(() => {
|
||||
return selectedElements.value.length > 0
|
||||
})
|
||||
// 描边宽度
|
||||
const lineWidth = ref('small')
|
||||
// 字体
|
||||
const fontFamily = ref('微软雅黑, Microsoft YaHei')
|
||||
// 字号
|
||||
const fontSize = ref(18)
|
||||
// 边框样式
|
||||
const lineDash = ref(0)
|
||||
// 透明度
|
||||
const globalAlpha = ref(1)
|
||||
// 角度
|
||||
const rotate = ref(0)
|
||||
// 当前缩放
|
||||
const currentZoom = ref(100)
|
||||
// 缩放允许前进后退
|
||||
const canUndo = ref(false)
|
||||
const canRedo = ref(false)
|
||||
// 图片导出弹窗
|
||||
const exportImageDialogVisible = ref(false)
|
||||
const exportImageUrl = ref('')
|
||||
const exportOnlySelected = ref(false)
|
||||
const exportRenderBackground = ref(true)
|
||||
const exportFileName = ref('未命名')
|
||||
const exportImagePaddingX = ref(10)
|
||||
const exportImagePaddingY = ref(10)
|
||||
// json导出弹窗
|
||||
const exportJsonDialogVisible = ref(false)
|
||||
const exportJsonData = ref('')
|
||||
const tree = ref(null)
|
||||
const jsonPreviewBox = ref(null)
|
||||
// 背景颜色
|
||||
const backgroundColor = ref('')
|
||||
// 当前滚动距离
|
||||
const scroll = reactive({
|
||||
x: 0,
|
||||
y: 0
|
||||
})
|
||||
// 切换显示网格
|
||||
const showGrid = ref(false)
|
||||
// 模式切换
|
||||
// const readonly = ref(false)
|
||||
|
||||
// 设置单个元素是否只读
|
||||
const elReadonly = ref(false)
|
||||
|
||||
|
||||
|
||||
// 通知app更当前类型
|
||||
watch(currentType, () => {
|
||||
elReadonly.value = false
|
||||
app.updateCurrentType(currentType.value)
|
||||
})
|
||||
|
||||
// 元素角度变化
|
||||
const onElementRotateChange = elementRotate => {
|
||||
rotate.value = elementRotate
|
||||
}
|
||||
|
||||
// 修改元素角度
|
||||
const onRotateChange = rotate => {
|
||||
app.updateActiveElementRotate(rotate)
|
||||
}
|
||||
|
||||
// 数字输入框聚焦事件
|
||||
const onInputNumberFocus = () => {
|
||||
// 解绑快捷键按键事件,防止冲突
|
||||
app.keyCommand.unBindEvent()
|
||||
}
|
||||
|
||||
// 数字输入框失焦事件
|
||||
const onInputNumberBlur = () => {
|
||||
// 重新绑定快捷键按键事件
|
||||
app.keyCommand.bindEvent()
|
||||
}
|
||||
|
||||
// 更新样式
|
||||
const updateStyle = (key, value) => {
|
||||
app.setCurrentElementsStyle({
|
||||
[key]: value
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// 选择工具
|
||||
const handleToolTypeChange = (key) => {
|
||||
currentType.value = key;
|
||||
app.cancelActiveElement()
|
||||
}
|
||||
// 类型变化
|
||||
const onCurrentTypeChange = () => {
|
||||
// 清除激活项
|
||||
app.cancelActiveElement()
|
||||
}
|
||||
|
||||
// 删除元素
|
||||
const deleteElement = () => {
|
||||
app.deleteCurrentElements()
|
||||
}
|
||||
|
||||
// 复制元素
|
||||
const copyElement = () => {
|
||||
app.copyPasteCurrentElements()
|
||||
}
|
||||
|
||||
// 放大
|
||||
const zoomIn = () => {
|
||||
app.zoomIn()
|
||||
}
|
||||
|
||||
// 缩小
|
||||
const zoomOut = () => {
|
||||
app.zoomOut()
|
||||
}
|
||||
|
||||
// 恢复初始缩放
|
||||
const resetZoom = () => {
|
||||
app.setZoom(1)
|
||||
}
|
||||
|
||||
// 橡皮擦
|
||||
const toggleEraser = () => {
|
||||
currentType.value = currentType.value === 'eraser' ? 'selection' : 'eraser'
|
||||
}
|
||||
|
||||
// 回退
|
||||
const undo = () => {
|
||||
app.undo()
|
||||
}
|
||||
|
||||
// 前进
|
||||
const redo = () => {
|
||||
app.redo()
|
||||
}
|
||||
|
||||
// 清空
|
||||
const empty = () => {
|
||||
app.empty()
|
||||
}
|
||||
|
||||
// 回到中心
|
||||
const backToCenter = () => {
|
||||
|
||||
app.scrollToCenter()
|
||||
}
|
||||
|
||||
// 显示全部
|
||||
const showFit = () => {
|
||||
let elementList = app.elements.elementList
|
||||
let { maxx, maxy } = TinyWhiteboard.utils.getMultiElementRectInfo(elementList)
|
||||
if (maxx >= app.width || maxy >= app.height) {
|
||||
app.fit()
|
||||
}
|
||||
else {
|
||||
backToCenter()
|
||||
}
|
||||
}
|
||||
|
||||
// 导入
|
||||
const importFromJson = () => {
|
||||
let el = document.createElement('input')
|
||||
el.type = 'file'
|
||||
el.accept = 'application/json'
|
||||
el.addEventListener('input', () => {
|
||||
let reader = new FileReader()
|
||||
reader.onload = () => {
|
||||
el.value = null
|
||||
if (reader.result) {
|
||||
app.setData(JSON.parse(reader.result))
|
||||
}
|
||||
}
|
||||
reader.readAsText(el.files[0])
|
||||
})
|
||||
el.click()
|
||||
}
|
||||
|
||||
// 导出
|
||||
const handleExportCommand = type => {
|
||||
if (type === 'png') {
|
||||
exportImageUrl.value = app.exportImage({
|
||||
renderBg: exportRenderBackground.value,
|
||||
paddingX: exportImagePaddingX.value,
|
||||
paddingY: exportImagePaddingY.value,
|
||||
onlySelected: exportOnlySelected.value
|
||||
})
|
||||
exportImageDialogVisible.value = true
|
||||
} else if (type === 'json') {
|
||||
exportJsonData.value = app.exportJson()
|
||||
exportJsonDialogVisible.value = true
|
||||
nextTick(() => {
|
||||
if (!tree.value) {
|
||||
tree.value = jsonTree.create(exportJsonData.value, jsonPreviewBox.value)
|
||||
} else {
|
||||
tree.value.loadData(exportJsonData.value)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 重新生成导出图片
|
||||
const reRenderExportImage = () => {
|
||||
exportImageUrl.value = app.exportImage({
|
||||
renderBg: exportRenderBackground.value,
|
||||
paddingX: exportImagePaddingX.value,
|
||||
paddingY: exportImagePaddingY.value,
|
||||
onlySelected: exportOnlySelected.value
|
||||
})
|
||||
}
|
||||
|
||||
// 下载导出的图片
|
||||
const downloadExportImage = () => {
|
||||
TinyWhiteboard.utils.downloadFile(
|
||||
exportImageUrl.value,
|
||||
exportFileName.value + '.png'
|
||||
)
|
||||
}
|
||||
|
||||
// 下载导出的json
|
||||
const downloadExportJson = () => {
|
||||
let str = JSON.stringify(exportJsonData.value, null, 4)
|
||||
let blob = new Blob([str])
|
||||
TinyWhiteboard.utils.downloadFile(
|
||||
URL.createObjectURL(blob),
|
||||
exportFileName.value + '.json'
|
||||
)
|
||||
}
|
||||
|
||||
// 更新背景颜色
|
||||
const setBackgroundColor = value => {
|
||||
app.setBackgroundColor(value)
|
||||
}
|
||||
|
||||
// 滚动至中心
|
||||
const scrollToCenter = () => {
|
||||
app.scrollToCenter()
|
||||
}
|
||||
|
||||
// 切换显示网格
|
||||
const toggleGrid = () => {
|
||||
if (showGrid.value) {
|
||||
showGrid.value = false
|
||||
app.hideGrid()
|
||||
} else {
|
||||
showGrid.value = true
|
||||
app.showGrid()
|
||||
}
|
||||
}
|
||||
|
||||
const elementModeChange = () => {
|
||||
elReadonly.value = !elReadonly.value
|
||||
updateStyle('elReadonly', elReadonly.value)
|
||||
}
|
||||
|
||||
// 模式切换
|
||||
const toggleMode = () => {
|
||||
if (readonly.value) {
|
||||
readonly.value = false
|
||||
app.setEditMode()
|
||||
} else {
|
||||
readonly.value = true
|
||||
app.setReadonlyMode()
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
const closeBoard = () => {
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
|
||||
const onSave = async () => {
|
||||
|
||||
let dataJson = app.exportJson()
|
||||
// 给每一个元素添加elReadonly 属性 学生端则不可操作这些元素
|
||||
dataJson.elements.map(item => {
|
||||
item.style.elReadonly = true
|
||||
})
|
||||
let base64 = await app.exportImage({
|
||||
type: 'image/jpeg',
|
||||
renderBg: exportRenderBackground.value,
|
||||
paddingX: 0,
|
||||
paddingY: 0,
|
||||
onlySelected: exportOnlySelected.value,
|
||||
backgroundColor: '#ffffff'
|
||||
})
|
||||
emit('handleSave', {
|
||||
json: dataJson,
|
||||
base64
|
||||
})
|
||||
}
|
||||
|
||||
const setCanvasData = (storeData) => {
|
||||
storeData = JSON.parse(storeData)
|
||||
;[['backgroundColor', ''], ['strokeStyle', '#000000'], ['fontFamily', '微软雅黑, Microsoft YaHei'], ['dragStrokeStyle', '#666'], ['fillStyle', 'transparent'], ['fontSize', 18]].forEach((item) => {
|
||||
if (storeData.state[item[0]] === undefined) {
|
||||
storeData.state[item[0]] = item[1]
|
||||
}
|
||||
})
|
||||
currentZoom.value = parseInt(storeData.state.scale * 100)
|
||||
scroll.x = parseInt(storeData.state.scrollX)
|
||||
scroll.y = parseInt(storeData.state.scrollY)
|
||||
showGrid.value = storeData.state.showGrid
|
||||
readonly.value = storeData.state.readonly
|
||||
app.setData(storeData)
|
||||
}
|
||||
|
||||
|
||||
const getCanvasJson = () => {
|
||||
let canvasJson = app.exportJson()
|
||||
// 给每一个元素添加elReadonly 属性 学生端则不可操作这些元素
|
||||
canvasJson.elements.forEach(item => {
|
||||
item.style.elReadonly = true
|
||||
})
|
||||
return canvasJson
|
||||
}
|
||||
|
||||
const getCanvasBase64 = async () =>{
|
||||
let base64 = await app.exportImage({
|
||||
type: 'image/jpeg',
|
||||
renderBg: exportRenderBackground.value,
|
||||
paddingX: 0,
|
||||
paddingY: 0,
|
||||
onlySelected: exportOnlySelected.value,
|
||||
backgroundColor: '#ffffff'
|
||||
})
|
||||
return base64
|
||||
}
|
||||
|
||||
watch(() => props.data, (newVal) => {
|
||||
if (newVal) {
|
||||
setCanvasData(newVal)
|
||||
}
|
||||
else {
|
||||
empty()
|
||||
}
|
||||
})
|
||||
|
||||
// dom元素挂载完成
|
||||
onMounted(() => {
|
||||
// 创建实例
|
||||
app = new TinyWhiteboard({
|
||||
container: box.value,
|
||||
drawType: currentType.value,
|
||||
state: {
|
||||
// backgroundColor: '#121212',
|
||||
// strokeStyle: '#fff',
|
||||
// fontFamily: '楷体, 楷体_GB2312, SimKai, STKaiti',
|
||||
// dragStrokeStyle: '#999'
|
||||
}
|
||||
})
|
||||
let storeData = localStorage.getItem('TINY_WHITEBOARD_DATA')
|
||||
if (props.data) {
|
||||
setCanvasData(props.data)
|
||||
}
|
||||
// 监听app内部修改类型事件
|
||||
app.on('currentTypeChange', type => {
|
||||
currentType.value = type
|
||||
})
|
||||
// 监听元素激活事件
|
||||
app.on('activeElementChange', element => {
|
||||
if (activeElement.value) {
|
||||
activeElement.value.off('elementRotateChange', onElementRotateChange)
|
||||
}
|
||||
activeElement.value = element
|
||||
if (element) {
|
||||
let { style, rotate: elementRotate } = element
|
||||
lineWidth.value = style.lineWidth
|
||||
fontFamily.value = style.fontFamily
|
||||
fontSize.value = style.fontSize
|
||||
lineDash.value = style.lineDash
|
||||
globalAlpha.value = style.globalAlpha
|
||||
rotate.value = elementRotate
|
||||
element.on('elementRotateChange', onElementRotateChange)
|
||||
}
|
||||
})
|
||||
// 元素多选变化
|
||||
app.on('multiSelectChange', elements => {
|
||||
selectedElements.value = elements
|
||||
})
|
||||
// 缩放变化
|
||||
app.on('zoomChange', scale => {
|
||||
currentZoom.value = parseInt(scale * 100)
|
||||
})
|
||||
// 监听前进后退事件
|
||||
app.on('shuttle', (index, length) => {
|
||||
canUndo.value = index > 0
|
||||
canRedo.value = index < length - 1
|
||||
})
|
||||
// 监听数据变化
|
||||
app.on('change', data => {
|
||||
showGrid.value = data.state.showGrid
|
||||
// localStorage.setItem('TINY_WHITEBOARD_DATA', JSON.stringify(data))
|
||||
})
|
||||
// 监听滚动变化
|
||||
app.on('scrollChange', (x, y) => {
|
||||
scroll.y = parseInt(y)
|
||||
scroll.x = parseInt(x)
|
||||
})
|
||||
appInstance.value = app
|
||||
// 窗口尺寸变化
|
||||
let resizeTimer = null
|
||||
window.addEventListener('resize', () => {
|
||||
clearTimeout(resizeTimer)
|
||||
resizeTimer = setTimeout(() => {
|
||||
app.resize()
|
||||
}, 300)
|
||||
})
|
||||
})
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({
|
||||
backToCenter,
|
||||
resetZoom,
|
||||
showFit,
|
||||
getCanvasJson,
|
||||
getCanvasBase64,
|
||||
setCanvasData
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
ul,
|
||||
ol {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.v-enter-active,
|
||||
.v-leave-active {
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.v-enter-from,
|
||||
.v-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-300px);
|
||||
}
|
||||
</style>
|
||||
<style lang="less" scoped>
|
||||
.whiteboart-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
|
||||
.canvasBox {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: translate(-50%, -50%);
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
top: 50px;
|
||||
width: 250px;
|
||||
background-color: #fff;
|
||||
|
||||
.elementStyle {
|
||||
padding: 10px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25);
|
||||
border-radius: 4px;
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.footerLeft {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
padding: 0 10px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
|
||||
.blockBox {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 5px;
|
||||
|
||||
.zoom {
|
||||
width: 40px;
|
||||
margin: 0 10px;
|
||||
user-select: none;
|
||||
color: #606266;
|
||||
cursor: pointer;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #fff;
|
||||
border-radius: 5px;
|
||||
padding: 0 5px;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.exportImageContainer {
|
||||
.imagePreviewBox {
|
||||
height: 400px;
|
||||
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAMUlEQVQ4T2NkYGAQYcAP3uCTZhw1gGGYhAGBZIA/nYDCgBDAm9BGDWAAJyRCgLaBCAAgXwixzAS0pgAAAABJRU5ErkJggg==') 0;
|
||||
padding: 10px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: scale-down;
|
||||
}
|
||||
}
|
||||
|
||||
.handleBox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 50px;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.exportJsonContainer {
|
||||
.jsonPreviewBox {
|
||||
height: 400px;
|
||||
overflow: auto;
|
||||
background-color: #f5f5f5;
|
||||
font-size: 14px;
|
||||
color: #000;
|
||||
|
||||
/deep/ .jsontree_tree {
|
||||
font-family: 'Trebuchet MS', Arial, sans-serif !important;
|
||||
}
|
||||
}
|
||||
|
||||
.handleBox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 50px;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.helpDialogContent {
|
||||
height: 500px;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
|
@ -18,6 +18,18 @@
|
|||
<!-- 作业资源 -->
|
||||
<el-row class="middle">
|
||||
<el-col :span="24" style="height: 100%; overflow: hidden;">
|
||||
<div v-if="classWorkForm.worktype == '课堂展示'" style="height: 100%; display: flex; flex-direction: column;">
|
||||
<div style="flex: 0 0 auto;">
|
||||
<el-form-item label="问题">
|
||||
<el-input v-model="question" type="text" placeholder="请输入问题" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div v-loading="boardLoading" class="board-wrap" style="height: 100%; flex: 1; overflow: hidden;">
|
||||
<!-- <whiteboard v-if="isShowBoard" ref="boardref" :height="mainHeight - 150" :isShowSave="false" :data="whiteboardObj"/> -->
|
||||
<whiteboard ref="boardref" :height="mainHeight - 150" :isShowSave="false" :data="whiteboardObj"/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<el-form-item label="作业资源:" class="el-form-work-list">
|
||||
<!-- 左侧作业资源 -->
|
||||
<el-col :span="15" class="work-left">
|
||||
|
@ -190,12 +202,13 @@
|
|||
</div>
|
||||
</el-col>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<!-- 作业说明 -->
|
||||
<el-row class="bottom">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="作业说明" style="margin: 10px 0;">
|
||||
<el-form-item v-if="classWorkForm.worktype != '课堂展示'" label="作业说明" style="margin: 10px 0;">
|
||||
<el-col :span="15" style="padding: 0px">
|
||||
<!-- <el-input v-model="classWorkForm.title" type="textarea" :rows="1" placeholder="请输入作业说明"/> -->
|
||||
<el-input v-model="classWorkForm.title" style="width: 400px" placeholder="请输入作业说明"/>
|
||||
|
@ -246,6 +259,7 @@ import { processList } from '@/hooks/useProcessList'
|
|||
import { getCurrentTime } from '@/utils/date'
|
||||
import FlowChart from "@/components/Flowchart/index.vue";
|
||||
import FileUpload from "@/components/FileUpload/index.vue";
|
||||
import whiteboard from '@/components/whiteboard/whiteboard.vue'
|
||||
|
||||
|
||||
|
||||
|
@ -350,7 +364,8 @@ const chooseWorkLists = ref([]); // 框架梳理、?课堂展示
|
|||
const whiteboardObj = ref(''); // 作业资源 - 课堂展示 白板
|
||||
// 课堂展示-------
|
||||
const boardLoading = ref(false);
|
||||
const question = ref(''); // 课堂展示
|
||||
const isShowBoard = ref(false); // 是否展示白板
|
||||
const question = ref(''); // 课堂展示 --问题说明
|
||||
const prevReadMsg = reactive({
|
||||
visible: false,
|
||||
type: ""
|
||||
|
@ -809,17 +824,18 @@ const handleClassWorkSave = async () => {
|
|||
|
||||
if (classWorkForm.worktype === "课堂展示") {
|
||||
boardLoading.value = true
|
||||
let canvasJson = this.$refs.boardref.getCanvasJson()
|
||||
let canvasBase64 = await this.$refs.boardref.getCanvasBase64()
|
||||
let canvasJson = proxy.$refs.boardref.getCanvasJson()
|
||||
let canvasBase64 = await proxy.$refs.boardref.getCanvasBase64()
|
||||
// 课堂展示提交内容
|
||||
formObj.worktag = question.value;
|
||||
formObj.workcodes = JSON.stringify({json: canvasJson, base64: canvasBase64});
|
||||
formObj.entpcourseworklist = JSON.stringify([{'id':-1, 'score': '10'}]);
|
||||
// cform.worktag = question.value;
|
||||
cform.title = question.value;
|
||||
cform.workcodes = JSON.stringify({json: canvasJson, base64: canvasBase64});
|
||||
cform.entpcourseworklist = JSON.stringify([{'id':-1, 'score': '10'}]);
|
||||
try {
|
||||
addClassworkReturnId(formObj).then(() => {
|
||||
addClassworkReturnId(cform).then(() => {
|
||||
ElMessage({ type: 'success', message: '作业设计成功!'});
|
||||
// 重置提交表单
|
||||
classWorkForm.worktype = "习题训练";
|
||||
classWorkForm.worktype = "课堂展示";
|
||||
classWorkForm.uniquekey = props.uniquekey, // 作业唯一标识 作业名称
|
||||
classWorkForm.title = "";
|
||||
classWorkForm.quizlist = [], // 作业习题列表内容
|
||||
|
@ -1024,5 +1040,11 @@ watch(() => props.bookobj.levelSecondId, (newVal) => {
|
|||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.board-wrap {
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
background-color: rgb(231, 231, 231)
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
|
|
Loading…
Reference in New Issue