Merge branch 'main' into zouyf_dev

This commit is contained in:
“zouyf” 2024-09-24 16:54:09 +08:00
commit b6d9082e1b
29 changed files with 5423 additions and 569 deletions

View File

@ -26,6 +26,14 @@
"@electron/remote": "^2.1.2",
"@element-plus/icons-vue": "^2.3.1",
"@vitejs/plugin-vue-jsx": "^4.0.0",
"@antv/x6": "^2.18.1",
"@antv/x6-plugin-clipboard": "^2.1.6",
"@antv/x6-plugin-dnd": "^2.1.1",
"@antv/x6-plugin-export": "^2.1.6",
"@antv/x6-plugin-keyboard": "^2.2.3",
"@antv/x6-plugin-selection": "^2.2.2",
"@antv/x6-plugin-snapline": "^2.1.7",
"@antv/x6-plugin-transform": "^2.1.8",
"@vue-office/docx": "^1.6.2",
"@vue-office/excel": "^1.7.11",
"@vue-office/pdf": "^2.0.2",
@ -39,7 +47,7 @@
"electron-store": "8.0.0",
"electron-updater": "^6.1.7",
"element-china-area-data": "^6.1.0",
"element-plus": "^2.7.6",
"element-plus": "^2.8.0",
"fabric": "^5.3.0",
"im_electron_sdk": "^8.0.5904",
"js-cookie": "^3.0.5",
@ -55,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",

View File

@ -62,3 +62,108 @@ export function updateClassworkdata(data) {
data: data
})
}
// 修改classwork
export function updateClasswork(data) {
return request({
url: '/education/classwork',
method: 'put',
data: data
})
}
// 查询evaluationclue列表
export function listEvaluationclue(query) {
return request({
url: '/education/evaluationclue/list',
method: 'get',
params: query
})
}
// 查询evaluationclue详细
export function getEvaluationclue(id) {
return request({
url: '/education/evaluationclue/' + id,
method: 'get'
})
}
// 新增evaluationclue
export function addEvaluationclueReturnId(data) {
return request({
url: '/education/evaluationclue/addReturnId',
method: 'post',
data: data
})
}
// 新增evaluationclue
export function addEvaluationclue(data) {
return request({
url: '/education/evaluationclue',
method: 'post',
data: data
})
}
// 修改evaluationclue
export function updateEvaluationclue(data) {
return request({
url: '/education/evaluationclue',
method: 'put',
data: data
})
}
// 删除evaluationclue
export function delEvaluationclue(id) {
return request({
url: '/education/evaluationclue/' + id,
method: 'delete'
})
}
// 新增evaluationclue保存base64图片
export function saveBase64File(data) {
return request({
url: '/education/evaluationclue/saveBase64File',
method: 'post',
data: data
})
}
// 新增evaluationclue上传
export function saveEvaluationClueUploadFile(data) {
return request({
url: '/education/evaluationclue/saveUploadFile',
method: 'post',
data: data
})
}
// 读取文件内容
export function readFile(data) {
return fetch(import.meta.env.VITE_APP_RES_FILE_PATH + data.cluelink, {
method: "get",
headers: {
'Content-Type': 'text/plain', // 请求头设置为纯文本
'Accept': 'text/plain' // 接受头设置为纯文本
},
})
.then(response => response.text())
.then(text => {
return Promise.resolve(text);
})
.catch(error => {
console.error('读取文件出错:', error);
return Promise.reject();
});
/*return request({
url: '/education/evaluationclue/readFile',
method: 'post',
data: data
})*/
}

View File

@ -0,0 +1,53 @@
import request from '@/utils/request'
// 查询KnowledgePoint列表
export function listKnowledgePoint(query) {
return request({
url: '/point/list',
method: 'get',
params: query
})
}
// 查询KnowledgePoint详细
export function getKnowledgePoint(id) {
return request({
url: '/point/' + id,
method: 'get'
})
}
// 新增KnowledgePoint
export function addKnowledgePointBase(data) {
return request({
url: '/point/addBase',
method: 'post',
data: data
})
}
// 新增KnowledgePoint
export function addKnowledgePoint(data) {
return request({
url: '/point/add',
method: 'post',
data: data
})
}
// 修改KnowledgePoint
export function updateKnowledgePoint(data) {
return request({
url: '/point/update',
method: 'put',
data: data
})
}
// 删除KnowledgePoint
export function delKnowledgePoint(id) {
return request({
url: '/point/' + id,
method: 'delete'
})
}

View File

@ -0,0 +1,208 @@
<template>
<div class="upload-file">
<el-upload
ref="fileUpload"
multiple
:action="uploadFileUrl"
:before-upload="handleBeforeUpload"
:file-list="fileList"
:limit="limit"
:on-error="handleUploadError"
:on-exceed="handleExceed"
:on-success="handleUploadSuccess"
:show-file-list="false"
:headers="headers"
class="upload-file-uploader"
>
<!-- 上传按钮 -->
<el-button type="primary">选取文件</el-button>
</el-upload>
<!-- 上传提示 -->
<div class="el-upload__tip" v-if="showTip">
请上传
<template v-if="fileSize"> 大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b> </template>
<template v-if="fileType"> 格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b> </template>
的文件
</div>
<!-- 文件列表 -->
<transition-group class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul">
<li :key="file.uid" class="el-upload-list__item ele-upload-list__item-content" v-for="(file, index) in fileList">
<el-link :href="`${file.url}`" :underline="false" target="_blank">
<span class="el-icon-document"> {{ file.name }} </span>
</el-link>
<div class="ele-upload-list__item-content-action">
<el-link :underline="false" @click="handleDelete(index)" type="danger">删除</el-link>
</div>
</li>
</transition-group>
</div>
</template>
<script setup>
import { onMounted, ref, nextTick,computed, watch, getCurrentInstance } from 'vue'
import { getToken } from "@/utils/auth";
const props = defineProps({
modelValue: [String, Object, Array],
//
limit: {
type: Number,
default: 5,
},
// (MB)
fileSize: {
type: Number,
default: 5,
},
// , ['png', 'jpg', 'jpeg']
fileType: {
type: Array,
default: () => ["doc", "xls", "ppt", "txt", "pdf"],
},
//
isShowTip: {
type: Boolean,
default: true
}
});
const { proxy } = getCurrentInstance();
const emit = defineEmits();
const number = ref(0);
const uploadList = ref([]);
const baseUrl = import.meta.env.VITE_APP_BASE_API;
const uploadFileUrl = ref(import.meta.env.VITE_APP_BASE_API + "/common/upload"); //
const headers = ref({ Authorization: "Bearer " + getToken() });
const fileList = ref([]);
const showTip = computed(
() => props.isShowTip && (props.fileType || props.fileSize)
);
watch(() => props.modelValue, val => {
if (val) {
let temp = 1;
//
const list = Array.isArray(val) ? val : props.modelValue.split(',');
//
fileList.value = list.map(item => {
if (typeof item === "string") {
item = { name: item, url: item };
}
item.uid = item.uid || new Date().getTime() + temp++;
return item;
});
} else {
fileList.value = [];
return [];
}
},{ deep: true, immediate: true });
//
const handleBeforeUpload = (file) =>{
//
if (props.fileType.length) {
const fileName = file.name.split('.');
const fileExt = fileName[fileName.length - 1];
const isTypeOk = props.fileType.indexOf(fileExt) >= 0;
if (!isTypeOk) {
proxy.$modal.msgError(`文件格式不正确, 请上传${props.fileType.join("/")}格式文件!`);
return false;
}
}
//
if (props.fileSize) {
const isLt = file.size / 1024 / 1024 < props.fileSize;
if (!isLt) {
proxy.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`);
return false;
}
}
proxy.$modal.loading("正在上传文件,请稍候...");
number.value++;
return true;
}
//
function handleExceed() {
proxy.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`);
}
//
function handleUploadError(err) {
proxy.$modal.msgError("上传文件失败");
}
//
function handleUploadSuccess(res, file) {
if (res.code === 200) {
uploadList.value.push({ name: res.fileName, url: res.url });
uploadedSuccessfully();
} else {
number.value--;
proxy.$modal.closeLoading();
proxy.$modal.msgError(res.msg);
proxy.$refs.fileUpload.handleRemove(file);
uploadedSuccessfully();
}
}
//
function handleDelete(index) {
fileList.value.splice(index, 1);
emit("update:modelValue", fileList.value);
}
//
function uploadedSuccessfully() {
if (number.value > 0 && uploadList.value.length === number.value) {
fileList.value = fileList.value.filter(f => f.url !== undefined).concat(uploadList.value);
uploadList.value = [];
number.value = 0;
emit("update:modelValue", fileList.value);
proxy.$modal.closeLoading();
}
}
//
function getFileName(name) {
if (name.lastIndexOf("/") > -1) {
return name.slice(name.lastIndexOf("/") + 1);
} else {
return "";
}
}
//
function listToString(list, separator) {
let strs = "";
separator = separator || ",";
for (let i in list) {
if (list[i].url) {
strs += list[i].url + separator;
}
}
return strs != '' ? strs.substr(0, strs.length - 1) : '';
}
</script>
<style scoped lang="scss">
.upload-file-uploader {
margin-bottom: 5px;
}
.upload-file-list .el-upload-list__item {
border: 1px solid #e4e7ed;
line-height: 2;
margin-bottom: 10px;
position: relative;
padding-left: 10px;
}
.upload-file-list .ele-upload-list__item-content {
display: flex;
justify-content: space-between;
align-items: center;
color: inherit;
}
.ele-upload-list__item-content-action .el-link {
margin-right: 10px;
}
</style>

View File

@ -0,0 +1,100 @@
<template>
<el-card
class="flow-contextmenu"
ref="flowMenu"
v-show="visible"
:style="menuStyle"
body-style="padding: 12px 0 12px 12px"
>
<el-cascader-panel
:props="{ expandTrigger: 'hover' }"
:options="options"
:border="false"
v-model="select"
@change="handleMenuClick"
>
<template v-slot="{ node, data }">
<span class="flow-contextmenu__node">{{ data.label }}</span>
</template>
</el-cascader-panel>
</el-card>
</template>
<script>
export default {
props: {
// /
visible: {
type: Boolean,
default: false
},
//
position: {
type: Object,
default: () => ({})
}
},
computed: {
menuStyle() {
return {
...this.position
}
}
},
watch: {
visible: {
handler() {
this.select = []
}
}
},
data() {
return {
select: [],
options: [
{
value: 'name',
label: '随机name'
},
{
value: 'color',
label: '随机color'
},
{
value: 'remove',
label: '删除'
}
]
}
},
methods: {
handleMenuClick(action) {
this.$emit('onMenuClick', action)
this.$emit('update:visible', false)
}
}
}
</script>
<style lang="scss" scoped>
.flow-contextmenu {
min-width: 150px;
position: fixed;
user-select: none;
z-index: 99;
:deep(.el-cascader-menu) {
min-width: auto;
.el-cascader-node {
z-index: 10;
margin-right: 10px;
padding-right: 12px;
padding-left: 14px;
}
}
.flow-contextmenu__node {
display: inline-block;
min-width: 60px;
}
}
</style>

View File

@ -0,0 +1,91 @@
<template>
<div>
<el-drawer
:model-value="drawer"
:title="drawerTitle"
:modal="false"
:before-close="handleClose"
direction="rtl">
<el-form :model="form" size="large">
<el-form-item label="节点名称">
<el-input v-model="form.label" />
</el-form-item>
<el-form-item label="节点背景色">
<el-color-picker v-model="form.bgcolor" />
</el-form-item>
<el-form-item label="节点边框颜色">
<el-color-picker v-model="form.borderColor"/>
</el-form-item>
<el-form-item label="节点文字颜色">
<el-color-picker v-model="form.textColor"/>
</el-form-item>
<div class="save-row">
<el-space :size="30">
<el-button type="primary" @click="updateNode" size="default">保存</el-button>
<el-button @click="closeDrawer" size="default">关闭</el-button>
</el-space>
</div>
</el-form>
</el-drawer>
</div>
</template>
<script>
export default {
name: 'flow-drawer',
props: {
drawerTitle: {
type: String,
default: '节点编辑'
},
drawer: {
type: Boolean,
default: false,
},
form: {
type: Object,
default: ()=>{
return {
label: '',
bgcolor: '',
borderColor: '',
textColor: ''
}
}
}
},
data() {
return {
};
},
mounted() {
},
methods: {
handleClose(done){
this.closeDrawer()
done();
},
closeDrawer(){
this.$emit('closeDrawer', false)
},
updateNode(){
this.$emit('updateNode', this.form)
}
},
};
</script>
<style scoped lang="scss">
:deep(.el-form-item__label) {
font-weight: normal;
}
.save-row {
margin-top: 100px;
text-align: center;
}
</style>

View File

@ -0,0 +1,235 @@
<template>
<div class="flow-library">
<div class="flow-library-title">节点库</div>
<div class="flow-library-list">
<div class="node-item" v-for="item in list" :key="item.name" :data-shape="item.shape"
:data-name="item.name"
@mousedown.stop="handleonAddNode"
@touchstart.stop="handleonAddNode">
<div
:class="item.class"
>
<template v-if="item.class == 'parallelogram' || item.class == 'diamond'">
<div :class="item.class + '-text'"> {{ item.name }} </div>
</template>
<template v-else>
{{ item.name }}
</template>
</div>
</div>
</div>
<!-- <el-space wrap :size="30" class="ant-flow-save">
<el-button type="success" @click="handleSave('img')">下载为图片</el-button>
<el-button type="primary" @click="handleSave">保存</el-button>
</el-space> -->
</div>
</template>
<script>
export default {
name: 'FlowLibrary',
data() {
return {
//
list: [
{
name: '开始',
shape: 'custom-rect',
class: 'elliptic'
},
{
name: '过程',
shape: 'custom-rect',
class: 'rectangle'
},
{
name: '可选过程',
shape: 'custom-rect',
class: 'quadrilateral'
},
{
name: '决策',
shape: 'custom-polygon',
class: 'diamond'
},
{
name: '数据',
shape: 'custom-polygon',
class: 'parallelogram'
},
{
name: '连接',
shape: 'custom-circle',
class: 'round'
},
],
}
},
created() {
},
methods: {
handleonAddNode(e) {
this.$emit('onAddNode', e)
},
//
handleSave(str){
this.$emit('handleSave', str)
}
}
}
</script>
<style scoped lang="scss">
@mixin nodeColor {
background: #EFF4FF;
display: flex;
border: solid 1px #5F95FF;
justify-content: center;
align-items: center;
font-size: 12px;
color: #333;
}
.flow-library {
user-select: none;
width: 250px;
height: 100%;
overflow-y: auto;
box-sizing: border-box;
border-right: 1px solid #dcdfe6;
display: flex;
flex-direction: column;
position: relative;
padding: 0 15px;
&-title {
line-height: 48px;
font-weight: bold;
}
&-group {
display: flex;
flex-wrap: wrap;
padding: 8px 0 8px 16px;
}
&-item {
width: 72px;
float: left;
text-align: center;
margin-right: 8px;
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 8px;
&__img {
width: 64px;
height: 64px;
margin-top: 8px;
background-repeat: no-repeat;
background-size: contain;
cursor: pointer;
position: relative;
&--setting-shape {
background-size: 32px;
background-color: #5f95ff;
background-position: 50% 15%;
.flow-library-item__name {
position: absolute;
height: auto;
width: 100%;
font-size: 12px;
cursor: pointer;
text-align: center;
bottom: 4px;
opacity: 1;
}
}
}
&__name {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
cursor: pointer;
text-align: center;
opacity: 0;
}
}
&-list {
width: 100%;
display: flex;
flex-wrap: wrap;
}
}
.ant-flow-save {
width: 100%;
text-align: center;
position: absolute;
left: 0;
bottom: 30px;
display: flex;
justify-content: center;
}
.node-item{
width: 50%;
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 20px;
}
.elliptic{
width: 70px;
height: 35px;
border-radius: 20px;
@include nodeColor;
}
.quadrilateral{
width: 60px;
height: 35px;
@include nodeColor;
border-radius: 8px;
}
.rectangle{
width: 80px;
height: 35px;
@include nodeColor;
}
.round{
width: 45px;
height: 45px;
border-radius: 50%;
@include nodeColor;
}
.parallelogram{
width: 55px;
height: 35px;
transform: skewX(-45deg);
@include nodeColor;
position: relative;
line-height: 35px;
.parallelogram-text{
transform: skewX(45deg);
white-space: nowrap;
text-align: center;
}
}
.diamond {
@include nodeColor;
width: 40px;
height: 40px;
transform: rotateZ(45deg)skew(-12deg, -12deg);
.diamond-text{
transform: rotate(-45deg)skew(0deg,0deg);
}
}
</style>

View File

@ -0,0 +1,333 @@
import { Shape } from '@antv/x6'
/**
* @desc 初始化面板配置
* @param check 查看模式
*/
export const graphOptions = (check = false) => {
return {
container: document.getElementById('flow-container'),
// 定制节点和边的交互行为 ==> boolean 节点或边是否可交互
interacting: check
? {
nodeMovable: false,
edgeMovable: false,
magnetConnectable: false,
vertexDeletable: false
}
: true,
// 对齐线
snapline: true,
// 显示网格 // 'dot' | 'fixedDot' | 'mesh'
grid: {
visible: true,
size: 20, // 网格大小
type: 'mesh',
args: {
color: '#e9e9e9',
thickness: 2 // 网格线宽度/网格点大小
}
},
// 平移
panning: true,
// 滚轮缩放 MouseWheel
mousewheel: {
enabled: true,
zoomAtMousePosition: true,
modifiers: ['ctrl', 'meta'],
maxScale: 3,
minScale: 0.3
},
// 连线规则
connecting: {
// 路由类型
router: {
// 连线类型在此修改
// 曼哈顿路由 'manhattan' 路由是正交路由 'orth' 的智能版本,该路由由水平或垂直的正交线段组成,并自动避开路径上的其他节点(障碍)。
name: 'manhattan',
args: {
padding: 1
}
},
// 圆角连接器,将起点、路由点、终点通过直线按顺序连接,并在线段连接处通过圆弧连接(倒圆角)。
connector: {
name: 'rounded',
args: {
radius: 8
}
},
anchor: 'center',
connectionPoint: 'anchor',
// 是否允许连接到画布空白位置的点,默认为 true。
allowBlank: false,
// 距离节点或者连接桩 20px 时会触发自动吸附
snap: {
radius: 20
},
// 拽出新的边
createEdge() {
return new Shape.Edge({
// markup: [
// {
// tagName: 'path',
// selector: 'stroke'
// }
// ],
// connector: { name: 'rounded' },
// attrs: {
// stroke: {
// fill: 'none',
// connection: true,
// strokeWidth: 4,
// strokeLinecap: 'round',
// stroke: '#666'
// }
// },
attrs: {
line: {
stroke: '#A2B1C3',
strokeWidth: 3,
targetMarker: {
name: 'block',
width: 12,
height: 8,
},
},
},
zIndex: 0
})
},
validateConnection({ targetMagnet }) {
return !!targetMagnet
}
},
// 连线高亮
highlighting: {
// 连线过程中,自动吸附到链接桩时被使用。
magnetAdsorbed: {
name: 'stroke',
args: {
attrs: {
width: 12,
r: 6,
magnet: true,
stroke: '#008CFF',
strokeWidth: 2,
fill: '#0F67FF'
}
}
}
},
rotating: false, // 不能旋转
keyboard: !check, // 按键操作
clipboard: true, // 剪切板
autoResize: true,
onToolItemCreated({ tool }) {
const options = tool.options
if (options && options.index % 2 === 1) {
tool.setAttrs({ fill: 'red' })
}
}
}
}
// 链接桩样式
export const portStyle = {
// width: 12,
// r: 6, // 半径
// // 当 magnet 属性为 true 时,表示该元素可以被链接,即在连线过程中可以被当做连线的起点或终点,与链接桩类似。
// magnet: true,
// stroke: '#008CFF',
// strokeWidth: 2,
// fill: '#fff',
// zIndex: 1,
// style: {
// visibility: 'hidden',
// },
r: 6,
magnet: true,
stroke: '#5F95FF',
strokeWidth: 2,
fill: '#fff',
style: {
visibility: 'hidden',
},
}
// 链接桩配置
export const ports = {
// 设置链接桩分组
groups: {
top: {
// 定义连接柱的位置,如果不配置,将显示为默认样式
position: 'top',
// 定义连接柱的样式
attrs: {
circle: {
...portStyle
}
}
},
right: {
position: 'right',
attrs: {
circle: {
...portStyle
}
}
},
bottom: {
position: 'bottom',
attrs: {
circle: {
...portStyle
}
}
},
left: {
position: 'left',
attrs: {
circle: {
...portStyle
}
}
},
absolute: {
position: 'absolute',
attrs: {
circle: {
r: 6,
magnet: true,
stroke: '#008CFF',
strokeWidth: 2,
fill: '#fff'
}
}
}
},
// 链接桩
items: [
{
group: 'top'
},
{
group: 'right'
},
{
group: 'bottom'
},
{
group: 'left'
}
]
}
// 动态计算宽高比
export const transformToPercent = (target, sum, font) => {
// https://x6.antv.vision/zh/docs/tutorial/intermediate/attrs
// 相对节点的大小
const percent = (target / sum).toFixed(2) * 100
return `${percent}${font ? 'px' : '%'}`
}
// 注册节点配置信息 注册以后就可以像使用内置节点那样使用该节点
export const registerNodeOpeions = {
'custom-rect': {
inherit: 'rect',
width: 70,
height: 40,
attrs: {
body: {
strokeWidth: 1,
stroke: '#5F95FF',
fill: '#EFF4FF',
},
text: {
fontSize: 12,
fill: '#262626',
},
},
ports: { ...ports },
},
'custom-polygon' : {
inherit: 'polygon',
width: 70,
height: 40,
attrs: {
body: {
strokeWidth: 1,
stroke: '#5F95FF',
fill: '#EFF4FF',
},
text: {
fontSize: 12,
fill: '#262626',
},
},
ports: {
...ports,
items: [
{
group: 'top',
},
{
group: 'bottom',
},
],
},
},
'custom-circle' : {
inherit: 'circle',
width: 50,
height: 50,
attrs: {
body: {
strokeWidth: 1,
stroke: '#5F95FF',
fill: '#EFF4FF',
},
text: {
fontSize: 12,
fill: '#262626',
},
},
ports: { ...ports },
},
}
// 图形变换配置
export const transFormOptions = {
// 调整尺寸
resizing: {
enabled: true,
minWidth: 1,
maxWidth: 200,
minHeight: 1,
maxHeight: 150,
restrict: false,
preserveAspectRatio: false,
},
// 调整角度---旋转
rotating: {
enabled: true,
}
}
// 拖动添加节点样式配置
export const addNodeAttrStyle = {
'可选过程': {
rx: 6,
ry: 6,
},
'开始': {
rx: 20,
ry: 26,
},
'决策': {
refPoints: '0,10 10,0 20,10 10,20',
},
'数据': {
refPoints: '10,0 40,0 30,20 0,20',
}
}

View File

@ -0,0 +1,391 @@
<template>
<div v-loading="flowLoading" class="ant-flow" :style="{ height: 70 + 'vh' }">
<!-- 左侧节点库 -->
<flow-library @onAddNode="onAddNode" />
<!--画布-->
<div id="flow-container"></div>
<!--右侧抽屉-->
<FlowDrawer :drawer="drawer" :form="form" @closeDrawer="closeDrawer" @updateNode="updateNode" />
</div>
</template>
<script>
import { Graph } from '@antv/x6'
//
import { Keyboard } from '@antv/x6-plugin-keyboard'
//
import { Dnd } from '@antv/x6-plugin-dnd'
//
import { Selection } from '@antv/x6-plugin-selection'
//
import { Transform } from '@antv/x6-plugin-transform'
// 线
import { Snapline } from '@antv/x6-plugin-snapline'
//
import { Clipboard } from '@antv/x6-plugin-clipboard'
//
import { Export } from '@antv/x6-plugin-export'
import FlowLibrary from '@/components/Flowchart/FlowLibrary.vue'
import FlowContentMenu from '@/components/Flowchart/FlowContentMenu.vue'
import FlowDrawer from '@/components/Flowchart/FlowDrawer.vue'
import { graphOptions, ports, registerNodeOpeions, transFormOptions, addNodeAttrStyle } from '@/components/Flowchart/config'
let graph = null
let dnd = null
let selector = null
export default {
name: 'X6',
components: { FlowContentMenu, FlowLibrary, FlowDrawer },
title: 'Antv/X6流程图',
props: {
flowHeight: {
type: Number,
default: 800
},
dataSource: {
type: Object,
default: () => {
return {}
}
}
},
data() {
return {
drawer: false,
form: {
label: '',
bgcolor: '',
borderColor: '',
textColor: ''
},
currentNode: null,
flowLoading: false
}
},
watch: {
dataSource(newVal){
this.flowLoading = true
this.updateConfigure(newVal)
}
},
mounted() {
this.flowLoading = true
this.$nextTick(()=>{
if(graph){
graph.clearCells()
}
this.init()
})
},
beforeDestroy() {
this.destoryFlow()
},
methods: {
/**
* 初始化画布
*/
init() {
//
graph = new Graph(graphOptions(false))
// 使
this.initPlugin()
//
this.registerNode()
//
graph.fromJSON(this.dataSource)
//
graph.centerContent()
//
this.initEvent()
//
dnd = new Dnd({
target: graph,
scaled: false,
})
this.flowLoading = false
},
/**
* 注册自定义节点
*/
registerNode() {
Graph.registerNode('custom-rect',registerNodeOpeions['custom-rect'], true)
Graph.registerNode('custom-polygon',registerNodeOpeions['custom-polygon'], true)
Graph.registerNode('custom-circle',registerNodeOpeions['custom-circle'], true)
},
/**
* 快捷键与事件
*/
initEvent() {
// ...
graph.on('cell:click', e => {
this.showPorts(false)
})
//
graph.on('node:dblclick', e => {
const { node } = e
this.currentNode = node
this.form.label = node.attr('text/text')
this.form.bgcolor = node.attr('body/fill')
this.form.borderColor = node.attr('body/stroke')
this.form.textColor = node.attr('text/fill')
this.drawer = true
})
// Edge
graph.on('cell:mouseenter', ({ cell }) => {
if (cell.isEdge()) {
// https://x6.antv.vision/zh/docs/tutorial/intermediate/tools
// 1vertices
// 2segments 线线线
cell.addTools([
'vertices',
'segments',
{
name: 'button-remove',
args: {
x: '30%',
y: '50%'
}
}
])
}
})
graph.on('cell:mouseleave', ({ cell }) => {
if (cell.isEdge()) {
cell.removeTool('vertices')
cell.removeTool('segments')
cell.removeTool('button-remove')
}
})
//
graph.on('node:mouseenter', () => {
this.showPorts(true)
})
graph.on('node:mouseleave', () => {
this.showPorts(false)
})
//
graph.on('blank:click', () => {
graph.cleanSelection && graph.cleanSelection()
})
/**
* 基础操作S
*/
graph.bindKey(['meta+c', 'ctrl+c'], () => {
const cells = graph.getSelectedCells()
if (cells.length) {
graph.copy(cells)
}
return false
})
graph.bindKey(['meta+x', 'ctrl+x'], () => {
const cells = graph.getSelectedCells()
if (cells.length) {
graph.cut(cells)
}
return false
})
graph.bindKey(['meta+v', 'ctrl+v'], () => {
if (!graph.isClipboardEmpty()) {
const cells = graph.paste({ offset: 32 })
graph.cleanSelection()
graph.select(cells)
}
return false
})
//undo redo
graph.bindKey(['meta+z', 'ctrl+z'], () => {
if (graph.history.canUndo()) {
graph.history.undo()
}
return false
})
graph.bindKey(['meta+shift+z', 'ctrl+shift+z'], () => {
if (graph.history.canRedo()) {
graph.history.redo()
}
return false
})
// select all
graph.bindKey(['meta+shift+a', 'ctrl+shift+a'], () => {
const nodes = graph.getNodes()
if (nodes) {
graph.select(nodes)
}
})
// delete
graph.bindKey(['backspace', 'delete'], () => {
//
const cells = graph.getSelectedCells()
if (cells.length) {
graph.removeCells(cells)
}
})
// zoom
graph.bindKey(['ctrl+1', 'meta+1'], () => {
const zoom = graph.zoom()
if (zoom < 1.5) {
graph.zoom(0.1)
}
})
graph.bindKey(['ctrl+2', 'meta+2'], () => {
const zoom = graph.zoom()
if (zoom > 0.5) {
graph.zoom(-0.1)
}
})
/**
* 基础操作E
*/
},
// /
showPorts(show) {
const container = document.getElementById('flow-container')
const ports = container.querySelectorAll('.x6-port-body')
for (let i = 0, len = ports.length; i < len; i = i + 1) {
ports[i].style.visibility = show ? 'visible' : 'hidden'
}
},
/**
* 使用插件
*/
initPlugin(){
graph.use(
new Keyboard({
enabled: true,
global: true,
}),
).use(new Selection({
multiple: true,
showNodeSelectionBox: true,
})).use(new Snapline({
enabled: true,
clean: false,
})).use(new Transform(transFormOptions)).use(new Clipboard({
enabled: true,
})).use(new Export())
},
/**
* 添加节点
*/
onAddNode(e) {
const target = e && e.target.closest('.node-item')
if (target) {
const name = target.getAttribute('data-name')
const shape = target.getAttribute('data-shape')
let nodeOptions = {
shape,
label: name,
ports: { ...ports },
}
if(addNodeAttrStyle[name]){
nodeOptions.attrs = {
body: addNodeAttrStyle[name]
}
}
const newNode = graph.createNode(nodeOptions)
dnd.start(newNode, e)
}
},
/**
* 保存
*/
handleSave() {
const res = graph.toJSON()
return res
},
/**
* 关闭抽屉
*/
closeDrawer(data) {
this.drawer = data
},
/**
* 更新节点-节点名称/背景色/边框/节点颜色
*/
updateNode(form) {
this.currentNode.attr('text/text', form.label)
this.currentNode.attr('text/fill', form.textColor)
this.currentNode.attr(`body/fill`, form.bgcolor)
this.currentNode.attr(`body/stroke`, form.borderColor)
this.drawer = false
},
updateConfigure(data) {
graph.fromJSON(data)
graph.centerContent()
this.flowLoading = false
},
destoryFlow() {
//
graph && graph.dispose()
graph = null
dnd = null
selector = null
},
/**
* 获取PNG
*/
getBase64Png(){
return new Promise((resolve, reject)=>{
graph.toPNG((dataUri)=>{
resolve(dataUri)
})
})
},
},
}
</script>
<style scoped lang="scss">
.ant-flow {
width: 100%;
display: flex;
overflow: hidden;
position: relative;
#flow-container {
width: 100%;
flex: 1;
height: 100%;
overflow: hidden;
}
}
#stencil{
user-select: none;
width: 250px;
height: 100%;
overflow-y: auto;
box-sizing: border-box;
border-right: 1px solid #dcdfe6;
display: flex;
flex-direction: column;
position: relative;
}
</style>

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

@ -15,6 +15,7 @@ import App from './App.vue'
import router from './router'
import log from 'electron-log/renderer' // 渲染进程日志-文件记录
import customComponent from '@/components/common' // 自定义组件
import plugins from './plugins' // plugins插件
if(process.env.NODE_ENV != 'development') { // 非开发环境,将日志打印到日志文件
Object.assign(console, log.functions) // 渲染进程日志-控制台替换
@ -40,4 +41,5 @@ app.use(router)
.use(store)
.use(ElementPlus, { locale: zhLocale })
.use(customComponent) // 自定义组件
.use(plugins)
.mount('#app')

View File

@ -0,0 +1,18 @@
// import tab from './tab'
// import auth from './auth'
// import cache from './cache'
import modal from './modal'
// import download from './download'
export default function installPlugins(app){
// 页签操作
// app.config.globalProperties.$tab = tab
// // 认证对象
// app.config.globalProperties.$auth = auth
// // 缓存对象
// app.config.globalProperties.$cache = cache
// 模态框对象
app.config.globalProperties.$modal = modal
// 下载文件
// app.config.globalProperties.$download = download
}

View File

@ -0,0 +1,82 @@
import { ElMessage, ElMessageBox, ElNotification, ElLoading } from 'element-plus'
let loadingInstance;
export default {
// 消息提示
msg(content) {
ElMessage.info(content)
},
// 错误消息
msgError(content) {
ElMessage.error(content)
},
// 成功消息
msgSuccess(content) {
ElMessage.success(content)
},
// 警告消息
msgWarning(content) {
ElMessage.warning(content)
},
// 弹出提示
alert(content) {
ElMessageBox.alert(content, "系统提示")
},
// 错误提示
alertError(content) {
ElMessageBox.alert(content, "系统提示", { type: 'error' })
},
// 成功提示
alertSuccess(content) {
ElMessageBox.alert(content, "系统提示", { type: 'success' })
},
// 警告提示
alertWarning(content) {
ElMessageBox.alert(content, "系统提示", { type: 'warning' })
},
// 通知提示
notify(content) {
ElNotification.info(content)
},
// 错误通知
notifyError(content) {
ElNotification.error(content);
},
// 成功通知
notifySuccess(content) {
ElNotification.success(content)
},
// 警告通知
notifyWarning(content) {
ElNotification.warning(content)
},
// 确认窗体
confirm(content) {
return ElMessageBox.confirm(content, "系统提示", {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: "warning",
})
},
// 提交内容
prompt(content) {
return ElMessageBox.prompt(content, "系统提示", {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: "warning",
})
},
// 打开遮罩层
loading(content) {
loadingInstance = ElLoading.service({
lock: true,
text: content,
background: "rgba(0, 0, 0, 0.7)",
})
},
// 关闭遮罩层
closeLoading() {
loadingInstance.close();
}
}

View File

@ -82,9 +82,9 @@ export const constantRoutes = [
},
{
path: '/classTaskAssign',
component: () => import('@/views/classTaskAssign/index.vue'),
component: () => import('@/views/classTask/classTaskAssign.vue'),
name: 'classTaskAssign',
meta: {title: '作业设计'},
meta: {title: '作业布置'},
},
{
path: '/classTask',

View File

@ -142,8 +142,9 @@ const getClassWorkList = () => {
edusubject: userStore.edusubject,//
deaddate: tabActive.value === '进行中'? getTomorrow() : EndDate.value,//
status: '1', // 1-
orderby: 'concat(deaddate,uniquekey) DESC',
pageSize: 100
// orderby: 'concat(deaddate,uniquekey) DESC',
orderby: 'uniquekey DESC',
pageSize: 100,
}).then((response) => {
for (var i = 0; i < response.rows.length; i++) {
//
@ -325,7 +326,7 @@ const escapeHtmlQuotes = (str) => {
// replace,
const regex1 = /\\+/g; //
let result = str.replace(regex1, '\\');
result = str.replace(/(?<!\\)\n/g, '<br />'); //\n\\n \\n
return result;
}
const pollingST = ref(null) //
@ -373,7 +374,8 @@ const getStudentVisible = async () => {
edusubject: userStore.edusubject,//
deaddate: tabActive.value === '进行中'? getTomorrow() : EndDate.value,//
status: '1', // 1-
orderby: 'concat(deaddate,uniquekey) DESC',
// orderby: 'concat(deaddate,uniquekey) DESC',
orderby: 'uniquekey DESC',
pageSize: 100
})
const curWorkList = response.rows

View File

@ -0,0 +1,527 @@
<template>
<div class="page-classTaskAssign flex">
<el-menu
default-active="1"
class="el-menu-vertical-demo"
:collapse="isCollapse"
>
<!--左侧 教材 目录-->
<div v-if="!isCollapse" style="height: 100%;overflow: hidden;">
<ChooseTextbook @change-book="getData" @node-click="getData" />
</div>
</el-menu>
<div class="page-right" :style="{'margin-left': isCollapse ? '0' : '20px'}">
<!-- 标题 -->
<el-row style="align-items: center; margin-bottom: 0px; flex: 0 0 auto">
<el-col :span="12" style="padding-left: 20px; text-align: left;">
<div class="unit-top-left" @click="isCollapse = !isCollapse">
<i v-if="!isCollapse" class="iconfont icon-xiangzuo"></i>
<span>课程目录</span>
<i v-if="isCollapse" class="iconfont icon-xiangyou"></i>
</div>
</el-col>
<el-col :span="12">
<div class="classtype-right">
<!-- <el-button type="primary" icon="Postcard" @click="handleNewClassWorkDialog" style="margin-left: 20px; margin-top: 10px">{{initDataProps.queryType!=='single'?'设计新作业':'设计新活动'}}</el-button>
<el-button v-if="initDataProps.queryType!=='single'" type="success" icon="Promotion" @click="handleTaskAssignToAllClass()" style="margin-left: 20px; margin-top: 10px">一键推送</el-button>
<el-button type="danger" icon="delete" @click="handleDelete" style="margin-left: 20px; margin-top: 10px">删除</el-button> -->
<el-button type="primary" @click="handleNewClassWorkDialog" style="margin-left: 20px; margin-top: 10px">设计新作业</el-button>
<el-button type="success" @click="handleTaskAssignToAllClass()" style="margin-left: 20px; margin-top: 10px">一键推送</el-button>
<el-button type="danger" @click="handleDelete" style="margin-left: 20px; margin-top: 10px">删除</el-button>
</div>
</el-col>
</el-row>
<!-- 作业类型:内容 -->
<div style="flex: 1;overflow: hidden;">
<el-table
ref="taskTable"
:data="taskList"
:tree-props="{checkStrictly: true}"
row-key="id"
style="width: 100%;height: 100%; border: 1px solid #dcdfe6;border-radius: 3px;flex:1"
:row-class-name="tableRowClassName"
>
<el-table-column type="selection" min-width="10%" align="center" :selectable="selectable"/>
<el-table-column :label="props.initDataProps.queryType!=='single'?'作业名称':'活动名称'" prop="uniquekey" min-width="18%" align="center">
</el-table-column>
<el-table-column :label="props.initDataProps.queryType!=='single'?'作业类型':'活动类型'" align="center" prop="worktype" min-width="20%" sortable>
<template #default="scope">
<el-tag :type="scope.row.workclass" size="large">{{ scope.row.worktype }}</el-tag>
</template>
</el-table-column>
<el-table-column v-if="props.initDataProps.queryType!=='single'" label="作业内容" align="center" min-width="20%">
<template #default="scope">
<div style="border: 1px solid #ccc; width: 100%; height: auto; display: flex; justify-content: space-between; padding-left: 10px">
<div style="display: flex; margin-top: 5px">
<el-tag v-if="scope.row.entpcourseworklistarray.length>0" effect="dark" type="warning" size="small" style="margin-left: 5px" round>{{ scope.row.entpcourseworklistarray.length }}</el-tag>
</div>
<div>
<div v-if="scope.row.status == '10'">
<el-button v-hasPermi="['teaching:classwork:edit']" text @click="newHandleWorkEdit2ClassWorkQuizAdd(scope.row, scope.$index)">编辑</el-button>
</div>
<div v-else>
<el-button v-hasPermi="['teaching:classwork:edit']" text @click="handleWorkEdit(scope.row, scope.$index)">查看详情</el-button>
</div>
</div>
</div>
</template>
</el-table-column>
<el-table-column v-if="props.initDataProps.queryType!=='single'" label="作业说明" align="center" min-width="36%">
<template #default="scope">
<div style="border: 1px solid #ccc; width: 100%; height: auto; display: flex; justify-content: space-between; padding-left: 10px">
<div style="display: flex; margin-top: 5px">
<div v-html="scope.row.title" style="max-width: 200px" class="singe-line"></div>
</div>
<div>
<div v-if="scope.row.status == '10'">
<el-button text @click="handleWorkTitleEdit(scope.row, scope.$index)" v-hasPermi="['teaching:classwork:edit']">编辑</el-button>
</div>
<div v-else>
<el-button text @click="handleWorkTitleEdit(scope.row, scope.$index)" v-hasPermi="['teaching:classwork:edit']">查看详情</el-button>
</div>
</div>
</div>
</template>
</el-table-column>
<el-table-column v-if="props.initDataProps.queryType!=='single'" label="创建时间" align="center" prop="timestamp" min-width="30%" sortable>
<template #default="scope">
<el-tag size="large">{{ scope.row.timestamp }}</el-tag>
</template>
</el-table-column>
<el-table-column v-if="props.initDataProps.queryType!=='single'" label="推送配置" align="left" min-width="16%">
<template #default="scope">
<div style="display: flex">
<el-button link icon="Setting" @click="scope.row.status == '10' ? openClassWorkConfigDialog(scope.row, -1,'item') : ''" :style="formatStyle(scope.row)" v-hasPermi="['teaching:classwork:edit']">{{scope.row.status == '10'? '推送' : '已推送'}}</el-button>
</div>
</template>
</el-table-column>
</el-table>
</div>
<!-- 作业内容编辑 -->
<el-dialog v-model="workEdit" title="作业内容详情" width="90%" append-to-body>
<div v-if="currentTag=='学习目标定位'" style="display: flex;">
<degreeevolution :attainmentList="attainmentList" :show-class="true" :courseQualityList="courseQualityList"/>
</div>
<!-- 课标研读 目标设定 教材研读 框架梳理 学科定位 -->
<div v-if="currentTag=='习题训练'" :style="{'padding': '15px', 'overflow': 'auto'}">
<el-table :data="workConfObj.quizlist" style="width: 100%;">
<el-table-column type="index" width="60" />
<el-table-column label="题目内容" align="left" >
<template #default="scope">
<div>
<div v-html="scope.row.titleFormat" style="overflow: hidden; text-overflow: ellipsis; font-weight:700"></div>
<div v-html="scope.row.workdescFormat" style="overflow: hidden; text-overflow: ellipsis; margin-top: 6px;"></div>
</div>
</template>
</el-table-column>
<el-table-column label="分值" align="center" width="180">
<template #default="scope">
<el-input-number v-model="scope.row.score" :min="1" :max="100" :disabled="taskParams.viewkey=='作业反馈'||checkTaskAssigned(currentTask)"></el-input-number >
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="100">
<template #default="scope">
<el-button @click="handleWorkConfigQuizMinus(scope.$index)" :disabled="taskParams.viewkey=='作业反馈'||checkTaskAssigned(currentTask)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div slot="footer" class="dialog-footer" style="text-align: right; margin-top: 20px;">
<div style="display: flex">
<el-button v-if="currentTag=='习题训练'" style="margin-right: auto" type="primary"
@click="handleWorkEdit2ClassWorkQuizAdd" :disabled="taskParams.viewkey=='作业反馈'||checkTaskAssigned(currentTask)">添加作业</el-button>
<el-button type="primary" style="margin-left: auto" @click="submitStudy('submit')"
:disabled="taskParams.viewkey=='作业反馈'||checkTaskAssigned(currentTask)"> </el-button>
</div>
</div>
</el-dialog>
<!-- 作业说明编辑 -->
<el-dialog v-model="currentWorkEdit.workTitleEdit" title="作业说明编辑" width="70%" append-to-body>
<el-input v-model="currentTitle" type="textarea" rows="5" placeholder="请输入作业说明" :disabled="taskParams.viewkey=='作业反馈'||checkTaskAssigned(currentTask)"/>
<div slot="footer" class="dialog-footer" style="text-align: right; margin-top: 20px;">
<el-button type="primary" @click="submitWorkTitle('submit')"
:disabled="taskParams.viewkey=='作业反馈'||checkTaskAssigned(currentTask)"> </el-button>
</div>
</el-dialog>
</div>
</div>
</template>
<script setup>
import { onMounted, ref, toRaw,watch, reactive } from 'vue'
import ChooseTextbook from '@/components/choose-textbook/index.vue'
import { homeworklist, listEntpcoursework, listClassworkeval } from '@/api/teaching/classwork'
import { useGetHomework } from '@/hooks/useGetHomework'
import { processList } from '@/hooks/useProcessList'
import { getCurrentTime } from '@/utils/date'
import useUserStore from '@/store/modules/user'
const userStore = useUserStore().user
const props = defineProps({
initDataProps: {
type: Object,
default: function () {
return {}
}
}
})
// ---------------------------------------------------
const isCollapse = ref(false)
const courseObj = reactive({
// : id,id,id,
textbookId: '',
levelFirstId: '',
levelSecondId: '',
coursetitle:'',
node: null, //
//
})
const taskList = ref([]);
const loading = ref(false);
//
const workEdit = ref(false);
const currentWorkEdit = reactive({
workTitleEdit: false,
currentTask: {},
currentTitle: '',
currentIndex: 0,
})//
const currentTag = ref('');//
const workConfObj = reactive({
quizlist: [], // list
});
// ---------------------------------------------------
//
const getData = (data) => {
const { textBook, node } = data
let textbookId = textBook.curBookId
let levelSecondId = node.id
let levelFirstId
if (node.parentNode) {
levelFirstId = node.parentNode.id
} else {
levelFirstId = node.id
levelSecondId = ''
}
courseObj.textbookId = textbookId //
courseObj.levelFirstId = levelFirstId //
courseObj.levelSecondId = levelSecondId //
courseObj.coursetitle = node.itemtitle // (/)
courseObj.node = node; //
// ID
localStorage.setItem('unitId', JSON.stringify({ levelFirstId, levelSecondId}))
}
/**
* 1.获取作业列表
*/
const getTaskList = async () => {
const { chapterId } = await useGetHomework(courseObj.node)
// this.entpcourseid = chapterId
//
homeworklist({entpcourseid: chapterId, orderby: "concat(deaddate,uniquekey) DESC" , edituserid: userStore.userId, pageSize: 100}).then(res => {
let model = [];
let mission = [];
for (let item of res.rows){
item.taskconfig = [];
//
if (item.timelength == null) {
item.timelength = 1;
}
if (item.weights == null) {
item.weights = 1;
}
// UI
if (item.worktype == '学习目标定位') {
item.workclass = 'success';
item.workcodesList = JSON.parse(item.workcodes);
} else if (item.worktype == '教材研读') {
item.workclass = 'primary';
} else if (item.worktype == '框架梳理') {
item.workclass = 'warning';
} else if (item.worktype == '学科定位') {
item.workclass = 'info';
} else if (item.worktype == '习题训练') {
item.workclass = 'danger';
} else {
item.workclass = 'primary';
}
//
if (item.entpcourseworklist != '') {
item.entpcourseworklistarray = JSON.parse('['+item.entpcourseworklist+']');
} else {
item.entpcourseworklistarray = [];
}
//
if (item.status == '10') {
// ,
model.push(item);
continue;
}
else if (item.status == '1') {
// ,
let ss = [];
if (item.classworkdatastudentids != null) {
ss = JSON.parse('['+ item.classworkdatastudentids+']');
}
const js = {
id: item.id,
classid: item.classid,
classcaption: item.classcaption,
parentid: 0,
worktype: '',
workkey: item.workkey,
worktag: '',
entpcourseid: 0,
evalid: 0,
edusubject: '',
edudegree: '',
workdate: '',
title: '',
workcodes: '',
studentlist: ss,
deaddate: item.deaddate,
timelength: item.timelength,
weights: item.weights,
feedtype: item.feedtype
}
item.taskconfig.push(js);
mission.push(item);
}
}
//
let list = [];
for (let item of model){
item.children = [];
for (let child of mission) {
if (item.id == child.parentid) {
item.children.push(child);
}
}
list.push(item);
}
console.log(list,'========================')
taskList.value = list;
loading.value = false;
})
}
// /
const handleWorkTitleEdit = (row, index) => {
currentWorkEdit.workTitleEdit = true;
currentWorkEdit.currentTask = row;
currentWorkEdit.currentTitle = row.title;
currentWorkEdit.currentIndex = index;
};
//
const handleWorkEdit = (row, index) =>{
workEdit.value = true
// this.currentTask = row;
// this.currentIndex = index;
currentTag.value = row.worktype;
this.attainmentList = row.workcodesList?.attlist;
this.courseQualityList = row.workcodesList?.qualist;
if (row.worktype == '框架梳理') {
this.$nextTick(()=>{
this.getFlowData()
})
}
// if (currentTag.value.worktype == '') {
// // TODO
// rootid:entpcourseworkid rootid: row.entpcourseworklistarray[0].id,
// listEvaluationclue({ cluegroup: 'graph', edusubject: this.courseObj.edusubject, pageSize: 1000 }).then((res) => {
// var glist = [];
// for (var i = 0; i < res.rows.length; i++) {
// glist.push(res.rows[i]);
// }
// this.isEditable = false;
// this.preKnowList = glist;
// this.$refs.jsMind.updateFromParent(this.preKnowList, this.courseObj.edusubject);
// this.$refs.jsMind.initJsMindMap();
// })
// }
// //
if (currentTag.value.worktype == '习题训练') {
var idlist = JSON.parse('['+row.entpcourseworklist+']');
var ids = [];
for (var i=0; i<idlist.length; i++) {
ids.push(idlist[i].id);
}
listEntpcoursework({ids: ids.join(","), pageSize: 50}).then(idres => {
for (var i=0; i<idlist.length; i++) {
for (var j=0; j<idres.rows.length; j++) {
if (idres.rows[j].id == idlist[i].id) {
idres.rows[j].classworkevalid = idlist[i].classworkevalid;
}
}
}
workConfObj.quizlist = idres.rows;
//
listClassworkeval({workid: row.id, workdataid: 0, pageSize: 50}).then(idres => {
idres.rows.forEach(item => {
const quizItem = workConfObj.quizlist.find(quiz => quiz.id === item.entpcourseworkid);
if (quizItem) {
quizItem.score = item.score;
quizItem.scoreOrigin = item.score;
quizItem.evalid = item.id;
}
});
});
//
processList(workConfObj.quizlist);
})
}
//
if(currentTag.value.worktype == '常规作业' || row.worktype == '课堂展示'){
console.log(row,'常规作业-课堂展示');
// workcodes
if(row.workcodes != ''){
const teachWorkFileList = JSON.parse(row.workcodes);
teachWorkFileList&&teachWorkFileList.forEach(item => {
if(item.name.indexOf('jpg') > -1 || item.name.indexOf('jpeg') > -1 || item.name.indexOf('png') > -1){
this.teachImageList.push(item);
}else{
this.teachFileList.push(item);
}
})
this.teacherFeedContentList.push(teachWorkFileList);
}
}
};
/**
* 表格子项背景暗调
*/
const tableRowClassName=({row,rowIndex})=>{
if (row.status == '10') {
//
return 'father-row'
} else {
//
return 'son-row'
}
};
const selectable=(row, index)=>{
return row.status == '10';
};
//
const formatStyle = (row) =>{
//taskconfig,
//taskconfigid0绿
//taskconfigid0
if (!row.taskconfig|| row.taskconfig.length === 0) {
return {};
}
for (var i=0; i<row.taskconfig.length; i++) {
if (row.taskconfig[i].id == 0) {
return {backgroundColor: '#fde2e2',color: '#f56c6c'};
}
}
return {backgroundColor: '#e1f3d8',color: '#67c23a'};
};
onMounted(() => {
})
watch(() => courseObj.node, (newVal) => {
console.log(courseObj,'课程选择')
//
getTaskList();
})
</script>
<style>
.el-table .hidden-row {
display: none !important;
/* color: #ccc !important; */
}
.el-table .father-row {
--el-table-tr-bg-color: #fff;
}
.el-table .son-row {
--el-table-tr-bg-color: #f0f0f08a;
}
</style>
<style lang="scss" scoped>
.page-classTaskAssign {
padding-top: 10px;
height: 100%;
overflow: hidden;
.el-menu--collapse {
width: 0px;
min-height: 100%;
}
.el-menu-vertical-demo:not(.el-menu--collapse) {
width: 300px;
min-height: 100%;
}
.unit-top-left {
cursor: pointer;
.icon-xiangzuo {
margin-right: 5px;
}
}
.classtype-right{
padding: 5px;
margin-bottom: 0px !important;
}
.el-form-item--default{
margin-bottom: 0px !important;
}
.page-right {
min-width: 0;
display: flex;
flex-direction: column;
flex: 1;
margin-left: 20px;
height: 100%;
background: #ffffff;
border-radius: 10px;
box-shadow: 0px 0px 20px 0px rgba(99, 99, 99, 0.06);
}
}
</style>

View File

@ -1,7 +1,7 @@
<template>
<el-form ref="classWorkFormScoreRef" :model="classWorkFormScore">
<!-- <div class="teacher_content" :style="{ height: dialogProps.maxheight + 'px' }"> -->
<div class="teacher_content" :style="{ height: '75vh' }">
<div class="teacher_content" :style="{ height: '72vh' }">
<div style="font-size: 18px; width: 100%; padding: 5px 10px" class="sticky">
{{ classWorkFormScore.name }} 答题详情
</div>
@ -88,8 +88,11 @@
<el-col :span="6" style="padding: 10px">
<!-- <span>学生答案{{ stuItem.feedcontent }}</span> -->
<span>学生答案
<span v-if="stuItem.feedcontent !=''" style="background-color: red; color: white; padding: 0 5px; border-radius: 5px;">
{{ formatFeedContent(stuItem, quItem) }}
<span
v-if="stuItem.feedcontent !=''"
style="background-color: red; color: white; padding: 0 5px; border-radius: 5px;"
v-html="formatFeedContent(stuItem, quItem)"
>
</span>
</span>
</el-col>
@ -385,7 +388,7 @@
v-model="fileReadopen"
title="文件预览"
width="80%"
:style="{ height: '75vh' }"
:style="{ height: '72vh' }"
append-to-body
>
<div class="file-read-dialog">

View File

@ -58,7 +58,7 @@
<!-- 如果当前学习没有试题 :height="mainHeight"-->
<div
v-if="classWorkAnalysis.view == 'studentview'"
style="width: 100%; height:75vh; "
style="width: 100%; height:73vh; "
class="clwk_dialog_view"
>
<div class="view_table">
@ -443,6 +443,9 @@ const getStudentClassWorkDataDetail = (row) => {
// <el-table-column label="" prop="rightanswer"
// +
wevalres.rows[w].worktitle = wevalres.rows[w].worktitle.replace(/!@#\$%/g, '')
// feedcontent\r<br />
wevalres.rows[w].feedcontent = wevalres.rows[w].feedcontent.replace(/(?<!\\)\n/g, '<br />'); //\n\\n \\n
}
}
classWorkAnalysis.activeStudentQuizlist = wevalres.rows
@ -523,6 +526,7 @@ const escapeHtmlQuotes = (str) => {
// replace,
const regex1 = /\\+/g; //
let result = str.replace(regex1, '\\');
result = str.replace(/(?<!\\)\n/g, '<br />'); //\n\\n \\n
return result;
}
@ -654,19 +658,19 @@ defineExpose({
.clwk_dialog_view {
display: flex;
flex-direction: row;
justify-content: space-around;
// align-items: center;
justify-content: flex-start;
overflow: hidden;
}
.view_table {
flex: 1;
flex: 0 0 auto;
height: 100%;
overflow-y: auto;
overflow: hidden;
}
.view_teachrting {
flex: 2;
flex: 1;
height: 100%;
overflow-y: auto;
// overflow-y: auto;
overflow: hidden;
}
}

View File

@ -1,23 +1,23 @@
<template>
<div class="page-resource flex">
<div class="page-newcalsetask flex">
<el-menu
default-active="1"
class="el-menu-vertical-demo"
:collapse="isCollapse"
>
<!--左侧 教材 目录-->
<div v-if="!isCollapse">
<div v-if="!isCollapse" style="height: 100%;overflow: hidden;">
<ChooseTextbook @change-book="getData" @node-click="getData" />
</div>
</el-menu>
<div class="page-right" :style="{'margin-left': isCollapse ? '0' : '20px'}">
<!-- 标题 -->
<el-row style="align-items: center; margin-bottom: 0px">
<el-row style="align-items: center; margin-bottom: 0px; flex: 0 0 auto">
<el-col :span="12" style="padding-left: 20px; text-align: left;">
<div class="unit-top-left" @click="isCollapse = !isCollapse">
<i v-if="!isCollapse" class="iconfont icon-xiangzuo"></i>
<span>作业范围</span>
<span>课程目录</span>
<i v-if="isCollapse" class="iconfont icon-xiangyou"></i>
</div>
</el-col>
@ -30,33 +30,19 @@
</el-col>
</el-row>
<!-- 作业类型:内容 -->
<task-type-view :bookobj="courseObj" />
<task-type-view :bookobj="courseObj" :uniquekey="classWorkForm.uniquekey" style="flex: 1; overflow: hidden;"/>
</div>
</div>
</template>
<script setup>
import { onMounted, ref, toRaw,watch, reactive } from 'vue'
// import useResoureStore from './store'
import ChooseTextbook from '@/components/choose-textbook/index.vue'
import Third from '@/components/choose-textbook/third.vue'
// import ResoureSearch from './container/resoure-search.vue'
// import ResoureList from './container/resoure-list.vue'
// import ThirdList from './container/third-list.vue'
import TaskTypeView from '@/views/classTask/container/newTask/taskTypeView.vue'
import uploadDialog from '@/components/upload-dialog/index.vue'
// import { createWindow } from '@/utils/tool'
import { useToolState } from '@/store/modules/tool'
import { getCurrentTime } from '@/utils/date'
import useUserStore from '@/store/modules/user'
const userStore = useUserStore().user
// const sourceStore = useResoureStore()
const isDialogOpen = ref(false)
const toolStore = useToolState()
const openDialog = () => {
isDialogOpen.value = true
}
// ---------------------------------------------------
const classWorkForm = reactive({
// uniquekey: userStore.edusubject+'-' + getCurrentTime('MMDD')+'-'+(this.taskList.length+1),
@ -72,6 +58,7 @@ const courseObj = reactive({
levelFirstId: '',
levelSecondId: '',
coursetitle:'',
node: null, //
//
})
@ -95,6 +82,7 @@ const getData = (data) => {
courseObj.levelFirstId = levelFirstId //
courseObj.levelSecondId = levelSecondId //
courseObj.coursetitle = node.itemtitle // (/)
courseObj.node = node; //
// ID
localStorage.setItem('unitId', JSON.stringify({ levelFirstId, levelSecondId}))
@ -116,9 +104,10 @@ const init = () => {
</script>
<style lang="scss" scoped>
.page-resource {
.page-newcalsetask {
padding-top: 10px;
height: 100%;
overflow: hidden;
.el-menu--collapse {
width: 0px;

View File

@ -1,242 +0,0 @@
<template>
<div v-loading="isLoading" class="page-resource flex">
<!--左侧 教材 目录-->
<ChooseTextbook @change-book="getData" @node-click="getData" />
<!--右侧 作业设计/布置 列表 -->
<div class="page-right">
<div class="prepare-body-header">
<el-button @click="handleOutLink('design')">作业设计</el-button>
<el-button @click="handleOutLink('assign')">作业布置</el-button>
<label style="font-size: 15px; margin-left: 20px">{{ listClassWork.length }}个作业</label>&nbsp;
<el-select
v-model="queryParams.workType"
placeholder="作业类型"
size="small"
@change="queryClassWorkByParams"
style="width: 100px; margin-left: auto;"
>
<template v-for="(item, index) in listWorkType" :key="index">
<el-option :label="item.label" :value="item" />
</template>
</el-select>
</div>
<div class="prepare-work-wrap">
<FileListItem
v-for="(item, index) in desingDataList"
:key="index"
:item="item"
:index="index"
@on-set="openSet"
@on-delhomework="delhomework"
>
</FileListItem>
</div>
</div>
<SetHomework v-model="setAssingDialog" :entpcourseid="entpcourseid" :row="curClassWork" />
</div>
</template>
<script setup>
import {ref, onMounted, reactive, watch, nextTick, getCurrentInstance, computed} from 'vue'
import { ElMessage } from 'element-plus'
import { homeworklist, delClasswork } from '@/api/teaching/classwork'
import useResoureStore from '@/views/resource/store'
import useUserStore from '@/store/modules/user'
import useClassTaskStore from "@/store/modules/classTask";
import outLink from '@/utils/linkConfig'
import ChooseTextbook from '@/components/choose-textbook/index.vue'
import FileListItem from '@/views/prepare/container/file-list-item.vue'
import SetHomework from '@/components/set-homework/index.vue'
const { ipcRenderer } = require('electron')
const userStore = useUserStore().user
const classTaskStore = useClassTaskStore()
const {proxy} = getCurrentInstance();
const sourceStore = useResoureStore();
//
const curNode = ref({});
const isLoading = ref(false);
const listClassWork = ref([]);
const listWorkType = ref(['不限', '习题训练', '框架梳理', '课堂展示', '常规作业']);
const isOpenHomework = ref(false);
const curClassWork = ref({});
const setAssingDialog = ref(false);
const entpcourseid = ref(0);
const queryParams = reactive({
workType: '不限',
total: 0,
});
/**
* @desc: 更新作业任务
* @return: {*}
* @param {*} computed
*/
const desingDataList = computed(() => {
return listClassWork.value;
})
/**
* @desc: 选中单元章节后的回调, 获取单元章节信息
* @return: {*}
* @param {*} data
*/
const getData = async (data) => {
if (curNode.value.id == data.node.id) {
return;
}
// 1.
curNode.value = data.node;
listClassWork.value = [];
isLoading.value = true;
console.log(curNode.value);
// 2.
await getClassWorkList();
isLoading.value = false;
}
/**
* @desc: 根据作业类型查询作业模板
* @return: {*}
*/
const queryClassWorkByParams = async () => {
// 1.
listClassWork.value = [];
isLoading.value = true;
// 2.[]
const params = {
worktype: queryParams.workType, // []
}
if (queryParams.workType == '不限') {
delete params.worktype;
}
await getClassWorkList(params);
isLoading.value = false;
}
/**
* @desc: 获取作业设计模板
* @return: {*}
*/
const getClassWorkList = async(params) => {
// homeworklist
let query = {
evalid: curNode.value.id,
edituserid: userStore.userId,
edustage: userStore.edustage,
edusubject: userStore.edusubject,
status: '10',
orderby: 'concat(worktype,uniquekey) DESC',
pageSize: 100,
}
// query
if (params !== null && params !== undefined) {
query = {...query, ...params};
}
const res = await homeworklist(query);
if (res.rows && res.rows.length > 0) {
for (const item of res.rows) {
item.fileShowName = item.uniquekey;
}
listClassWork.value = res.rows;
//TODO total
queryParams.total = res.total
}
}
/**
* @desc: 作业设计/布置
* @return: {*}
* @param {*} key design-设计 assign-布置. 当key为设计时, url需再增加openDialog字段以便自动打开[设计新作业]
*/
const handleOutLink = (key) => {
isOpenHomework.value = true;
// key linkConfig.js
let configObj = outLink()['homeWork']
let fullPath = configObj.fullPath;
//urlunitId ID
let unitId = curNode.value.id;
fullPath += `&unitId=${unitId}`;
// , openDialog[]
if (key == 'design') {
fullPath += `&openDialog=newClassTask`;
}
//
ipcRenderer.send('openWindow', {
key,
fullPath: fullPath,
cookieData: { ...configObj.data }
})
}
const openSet = (item) => {
//
curClassWork.value = item;
setAssingDialog.value = true;
}
const delhomework = (item) => {
isLoading.value = true
delClasswork(item.id)
.then(async(res) => {
ElMessage.success('删除成功');
isLoading.value = false;
await getClassWorkList();
})
.catch(() => {
isLoading.value = false;
})
}
</script>
<style lang="scss" scoped>
.page-resource {
padding-top: 10px;
height: 100%;
//
.page-right {
min-width: 0;
display: flex;
flex-direction: column;
flex: 1;
margin-left: 20px;
height: 100%;
background: #ffffff;
border-radius: 10px;
box-shadow: 0px 0px 20px 0px rgba(99, 99, 99, 0.06);
.prepare-body-header {
padding: 10px;
display: flex;
align-items: center;
flex-wrap: wrap;
justify-content: space-between;
}
.prepare-work-wrap{
flex: 1;
overflow: auto;
}
}
}
</style>

View File

@ -105,6 +105,10 @@ const menuList = [{
icon: 'icon-jiaoxuefansi',
isOuter: true,
path: '/teaching/classtaskassign?titleName=作业布置&openDialog=newClassTask',
// path: '/newClassTask'
//path: '/classTaskAssign'
//isOuter: true,
//path: '/teaching/classtaskassign?titleName=&&openDialog=newClassTask'
id: '2-1'
},
{
@ -113,6 +117,7 @@ const menuList = [{
isOuter: true,
path: '/teaching/classtaskassign?titleName=作业布置',
id: '2-2'
// path: '/classTaskAssign'
},
{
name: '作业批改',

View File

@ -51,11 +51,11 @@
<examReview
:loading="loading"
:listExamQuestion="listExamQuestion"
v-if="curTask.viewkey=='真题回顾'"
v-if="curTask.viewkey=='真题回顾' "
/>
<pointAnalysis
v-else-if="curTask.viewkey=='考点分析'"
v-else-if="curTask.viewkey=='考点分析' "
/>
<examMocks v-else