baigl #261

Merged
baigl merged 14 commits from baigl into main 2024-09-24 16:34:39 +08:00
9 changed files with 2504 additions and 173 deletions
Showing only changes of commit 5f8ea627d0 - Show all commits

View File

@ -63,7 +63,10 @@
"vue-qr": "^4.0.9", "vue-qr": "^4.0.9",
"vue-router": "^4.4.0", "vue-router": "^4.4.0",
"xgplayer": "^3.0.19", "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": { "devDependencies": {
"@electron-toolkit/eslint-config": "^1.0.2", "@electron-toolkit/eslint-config": "^1.0.2",

View File

@ -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>

View File

@ -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>

View File

@ -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
}
})

View File

@ -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

View File

@ -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;
}

View File

@ -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">&hellip;</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("&nbsp;&nbsp;&nbsp;&nbsp;");
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
}

View File

@ -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>

View File

@ -18,6 +18,18 @@
<!-- 作业资源 --> <!-- 作业资源 -->
<el-row class="middle"> <el-row class="middle">
<el-col :span="24" style="height: 100%; overflow: hidden;"> <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-form-item label="作业资源:" class="el-form-work-list">
<!-- 左侧作业资源 --> <!-- 左侧作业资源 -->
<el-col :span="15" class="work-left"> <el-col :span="15" class="work-left">
@ -190,12 +202,13 @@
</div> </div>
</el-col> </el-col>
</el-form-item> </el-form-item>
</div>
</el-col> </el-col>
</el-row> </el-row>
<!-- 作业说明 --> <!-- 作业说明 -->
<el-row class="bottom"> <el-row class="bottom">
<el-col :span="12"> <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-col :span="15" style="padding: 0px">
<!-- <el-input v-model="classWorkForm.title" type="textarea" :rows="1" placeholder="请输入作业说明"/> --> <!-- <el-input v-model="classWorkForm.title" type="textarea" :rows="1" placeholder="请输入作业说明"/> -->
<el-input v-model="classWorkForm.title" style="width: 400px" 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 { getCurrentTime } from '@/utils/date'
import FlowChart from "@/components/Flowchart/index.vue"; import FlowChart from "@/components/Flowchart/index.vue";
import FileUpload from "@/components/FileUpload/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 whiteboardObj = ref(''); // -
// ------- // -------
const boardLoading = ref(false); const boardLoading = ref(false);
const question = ref(''); // const isShowBoard = ref(false); //
const question = ref(''); // --
const prevReadMsg = reactive({ const prevReadMsg = reactive({
visible: false, visible: false,
type: "" type: ""
@ -809,17 +824,18 @@ const handleClassWorkSave = async () => {
if (classWorkForm.worktype === "课堂展示") { if (classWorkForm.worktype === "课堂展示") {
boardLoading.value = true boardLoading.value = true
let canvasJson = this.$refs.boardref.getCanvasJson() let canvasJson = proxy.$refs.boardref.getCanvasJson()
let canvasBase64 = await this.$refs.boardref.getCanvasBase64() let canvasBase64 = await proxy.$refs.boardref.getCanvasBase64()
// //
formObj.worktag = question.value; // cform.worktag = question.value;
formObj.workcodes = JSON.stringify({json: canvasJson, base64: canvasBase64}); cform.title = question.value;
formObj.entpcourseworklist = JSON.stringify([{'id':-1, 'score': '10'}]); cform.workcodes = JSON.stringify({json: canvasJson, base64: canvasBase64});
cform.entpcourseworklist = JSON.stringify([{'id':-1, 'score': '10'}]);
try { try {
addClassworkReturnId(formObj).then(() => { addClassworkReturnId(cform).then(() => {
ElMessage({ type: 'success', message: '作业设计成功!'}); ElMessage({ type: 'success', message: '作业设计成功!'});
// //
classWorkForm.worktype = "习题训练"; classWorkForm.worktype = "课堂展示";
classWorkForm.uniquekey = props.uniquekey, // classWorkForm.uniquekey = props.uniquekey, //
classWorkForm.title = ""; classWorkForm.title = "";
classWorkForm.quizlist = [], // classWorkForm.quizlist = [], //
@ -1024,5 +1040,11 @@ watch(() => props.bookobj.levelSecondId, (newVal) => {
box-sizing: border-box; box-sizing: border-box;
} }
.board-wrap {
padding: 10px;
box-sizing: border-box;
background-color: rgb(231, 231, 231)
}
} }
</style> </style>