Merge branch 'main' into qinqing_dev
This commit is contained in:
commit
8fd5edf141
|
@ -1,5 +1,5 @@
|
||||||
# 页面标题
|
# 页面标题
|
||||||
VITE_APP_TITLE = 文枢课堂
|
VITE_APP_TITLE = AIX智慧课堂
|
||||||
|
|
||||||
# 生产环境配置
|
# 生产环境配置
|
||||||
VITE_APP_ENV = 'production'
|
VITE_APP_ENV = 'production'
|
|
@ -1,5 +1,5 @@
|
||||||
# 页面标题
|
# 页面标题
|
||||||
VITE_APP_TITLE = AIX智慧课堂
|
VITE_APP_TITLE = 文枢课堂
|
||||||
|
|
||||||
# 生产环境配置
|
# 生产环境配置
|
||||||
VITE_APP_ENV = 'production'
|
VITE_APP_ENV = 'production'
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
appId: com.electron.app
|
appId: com.electron.app
|
||||||
productName: 文枢课堂
|
productName: AIx
|
||||||
directories:
|
directories:
|
||||||
output: dist
|
output: dist
|
||||||
buildResources: build
|
buildResources: build
|
||||||
win:
|
win:
|
||||||
executableName: 文枢课堂
|
executableName: AIx
|
||||||
icon: resources/logo2.ico
|
icon: resources/logo2.ico
|
||||||
files:
|
files:
|
||||||
- '!**/.vscode/*'
|
- '!**/.vscode/*'
|
|
@ -1,10 +1,10 @@
|
||||||
appId: com.electron.app
|
appId: com.electron.app
|
||||||
productName: AIx
|
productName: 文枢课堂
|
||||||
directories:
|
directories:
|
||||||
output: dist
|
output: dist
|
||||||
buildResources: build
|
buildResources: build
|
||||||
win:
|
win:
|
||||||
executableName: AIx
|
executableName: 文枢课堂
|
||||||
icon: resources/logo2.ico
|
icon: resources/logo2.ico
|
||||||
files:
|
files:
|
||||||
- '!**/.vscode/*'
|
- '!**/.vscode/*'
|
||||||
|
|
17
package.json
17
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "aix-win",
|
"name": "aix-win",
|
||||||
"version": "2.0.6",
|
"version": "2.1.1",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": "example.com",
|
"author": "example.com",
|
||||||
|
@ -26,6 +26,14 @@
|
||||||
"@electron/remote": "^2.1.2",
|
"@electron/remote": "^2.1.2",
|
||||||
"@element-plus/icons-vue": "^2.3.1",
|
"@element-plus/icons-vue": "^2.3.1",
|
||||||
"@vitejs/plugin-vue-jsx": "^4.0.0",
|
"@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/docx": "^1.6.2",
|
||||||
"@vue-office/excel": "^1.7.11",
|
"@vue-office/excel": "^1.7.11",
|
||||||
"@vue-office/pdf": "^2.0.2",
|
"@vue-office/pdf": "^2.0.2",
|
||||||
|
@ -39,7 +47,7 @@
|
||||||
"electron-store": "8.0.0",
|
"electron-store": "8.0.0",
|
||||||
"electron-updater": "^6.1.7",
|
"electron-updater": "^6.1.7",
|
||||||
"element-china-area-data": "^6.1.0",
|
"element-china-area-data": "^6.1.0",
|
||||||
"element-plus": "^2.7.6",
|
"element-plus": "^2.8.0",
|
||||||
"fabric": "^5.3.0",
|
"fabric": "^5.3.0",
|
||||||
"im_electron_sdk": "^8.0.5904",
|
"im_electron_sdk": "^8.0.5904",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
|
@ -55,7 +63,10 @@
|
||||||
"vue-qr": "^4.0.9",
|
"vue-qr": "^4.0.9",
|
||||||
"vue-router": "^4.4.0",
|
"vue-router": "^4.4.0",
|
||||||
"xgplayer": "^3.0.19",
|
"xgplayer": "^3.0.19",
|
||||||
"xlsx": "^0.18.5"
|
"xlsx": "^0.18.5",
|
||||||
|
"less": "^4.2.0",
|
||||||
|
"less-loader": "^7.3.0",
|
||||||
|
"whiteboard_lyc": "^0.0.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@electron-toolkit/eslint-config": "^1.0.2",
|
"@electron-toolkit/eslint-config": "^1.0.2",
|
||||||
|
|
|
@ -273,9 +273,14 @@ function handleAll() {
|
||||||
})
|
})
|
||||||
// 用于监听-状态管理变化-同步所有窗口
|
// 用于监听-状态管理变化-同步所有窗口
|
||||||
ipcMain.handle('pinia-state-change', (e, storeName, jsonStr) => {
|
ipcMain.handle('pinia-state-change', (e, storeName, jsonStr) => {
|
||||||
|
console.log('pinia-state-change-1', storeName, jsonStr)
|
||||||
|
|
||||||
for(const curWin of BrowserWindow.getAllWindows()){
|
for(const curWin of BrowserWindow.getAllWindows()){
|
||||||
const id = curWin.webContents.id
|
const id = curWin.webContents.id
|
||||||
const bool = id !== e.sender.id && !curWin.isDestroyed()
|
const bool = id !== e.sender.id && !curWin.isDestroyed()
|
||||||
|
if (id === e.sender.id) {
|
||||||
|
console.log('pinia-state-change-2', 'windows-send', curWin.type)
|
||||||
|
}
|
||||||
if (bool) { // 除了消息发送窗口和销毁的窗口 其他都发送
|
if (bool) { // 除了消息发送窗口和销毁的窗口 其他都发送
|
||||||
curWin.webContents.send('pinia-state-set', storeName, jsonStr)
|
curWin.webContents.send('pinia-state-set', storeName, jsonStr)
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,3 +62,108 @@ export function updateClassworkdata(data) {
|
||||||
data: 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
|
||||||
|
})*/
|
||||||
|
}
|
||||||
|
|
|
@ -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'
|
||||||
|
})
|
||||||
|
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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',
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
||||||
|
// 1、vertices 路径点工具,在路径点位置渲染一个小圆点,
|
||||||
|
// 2、segments 线段工具。在边的每条线段的中心渲染一个工具条,可以拖动工具条调整线段两端的路径点的位置。
|
||||||
|
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>
|
|
@ -0,0 +1,130 @@
|
||||||
|
<template>
|
||||||
|
<div class="colorPickerContainer">
|
||||||
|
<div class="content">
|
||||||
|
<el-popover
|
||||||
|
:placement="placement"
|
||||||
|
:width="200"
|
||||||
|
trigger="click"
|
||||||
|
:disabled="colorList.length <= 0"
|
||||||
|
>
|
||||||
|
<template #reference>
|
||||||
|
<div class="colorPreview" :style="{ backgroundColor: color }"></div>
|
||||||
|
</template>
|
||||||
|
<div class="colorList">
|
||||||
|
<div
|
||||||
|
class="colorItem"
|
||||||
|
v-for="item in colorList"
|
||||||
|
:key="item"
|
||||||
|
:style="{ backgroundColor: item }"
|
||||||
|
@click="color = item"
|
||||||
|
>
|
||||||
|
<span v-if="!item">无</span>
|
||||||
|
<span v-if="item === 'transparent'">透明</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-popover>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import {
|
||||||
|
strokeColorList,
|
||||||
|
fillColorList,
|
||||||
|
backgroundColorList
|
||||||
|
} from '../constants'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
value: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
default: '颜色'
|
||||||
|
},
|
||||||
|
placement: {
|
||||||
|
type: String,
|
||||||
|
default: 'bottom'
|
||||||
|
},
|
||||||
|
showEmptySelect: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emits = defineEmits(['change'])
|
||||||
|
|
||||||
|
const color = ref(props.value)
|
||||||
|
watch(
|
||||||
|
() => {
|
||||||
|
return props.value
|
||||||
|
},
|
||||||
|
val => {
|
||||||
|
color.value = val
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const colorList = computed(() => {
|
||||||
|
let list = props.showEmptySelect ? [''] : []
|
||||||
|
switch (props.type) {
|
||||||
|
case 'stroke':
|
||||||
|
list.push(...strokeColorList)
|
||||||
|
break
|
||||||
|
case 'fill':
|
||||||
|
list.push(...fillColorList)
|
||||||
|
break
|
||||||
|
case 'background':
|
||||||
|
list.push(...backgroundColorList)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
})
|
||||||
|
watch(color, () => {
|
||||||
|
emits('change', color.value)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.colorPickerContainer {
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.colorPreview {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 5px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorList {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(5, auto);
|
||||||
|
grid-gap: 5px;
|
||||||
|
.colorItem {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,213 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="contextmenuContainer"
|
||||||
|
v-if="isShow"
|
||||||
|
:style="{ left: left + 'px', top: top + 'px' }"
|
||||||
|
>
|
||||||
|
<template v-if="isHasActiveElements">
|
||||||
|
<div
|
||||||
|
class="item"
|
||||||
|
:class="{ disabled: !canMoveLevel }"
|
||||||
|
@click="exec('moveUp')"
|
||||||
|
>
|
||||||
|
上移一层
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="item"
|
||||||
|
:class="{ disabled: !canMoveLevel }"
|
||||||
|
@click="exec('moveDown')"
|
||||||
|
>
|
||||||
|
下移一层
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="item"
|
||||||
|
:class="{ disabled: !canMoveLevel }"
|
||||||
|
@click="exec('moveTop')"
|
||||||
|
>
|
||||||
|
置于顶层
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="item"
|
||||||
|
:class="{ disabled: !canMoveLevel }"
|
||||||
|
@click="exec('moveBottom')"
|
||||||
|
>
|
||||||
|
置于底层
|
||||||
|
</div>
|
||||||
|
<div class="splitLine"></div>
|
||||||
|
<div class="item danger" @click="exec('del')">删除</div>
|
||||||
|
<div class="item" @click="exec('copy')">复制</div>
|
||||||
|
<div
|
||||||
|
class="item"
|
||||||
|
:class="{ disabled: groupStatus === 'disabled' }"
|
||||||
|
@click="exec(groupStatus)"
|
||||||
|
>
|
||||||
|
{{ groupBtnText }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="item" @click="exec('selectAll')">全部选中</div>
|
||||||
|
<div class="item" @click="exec('backToCenter')">回到中心</div>
|
||||||
|
<div class="item" @click="exec('fit')">显示全部</div>
|
||||||
|
<div class="item" @click="exec('resetZoom')">重置缩放</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props :{
|
||||||
|
app: {
|
||||||
|
type: Object
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isShow: false,
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
isHasActiveElements: false,
|
||||||
|
canMoveLevel: false,
|
||||||
|
groupStatus: 'disabled',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
groupStatus(newValue, oldValue) {
|
||||||
|
switch (newValue) {
|
||||||
|
case 'disabled':
|
||||||
|
this.groupBtnText = '编组'
|
||||||
|
break
|
||||||
|
case 'dogroup':
|
||||||
|
this.groupBtnText = '编组'
|
||||||
|
break
|
||||||
|
case 'ungroup':
|
||||||
|
this.groupBtnText = '取消编组'
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.init();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
init(){
|
||||||
|
this.app.on('contextmenu', this.show)
|
||||||
|
document.body.addEventListener('click', this.hide)
|
||||||
|
},
|
||||||
|
hide() {
|
||||||
|
this.isShow = false
|
||||||
|
this.left = 0
|
||||||
|
this.top = 0
|
||||||
|
},
|
||||||
|
exec(command) {
|
||||||
|
switch (command) {
|
||||||
|
case 'moveUp':
|
||||||
|
this.app.moveUpCurrentElement()
|
||||||
|
break
|
||||||
|
case 'moveDown':
|
||||||
|
this.app.moveDownCurrentElement()
|
||||||
|
break
|
||||||
|
case 'moveTop':
|
||||||
|
this.app.moveTopCurrentElement()
|
||||||
|
break
|
||||||
|
case 'moveBottom':
|
||||||
|
this.app.moveBottomCurrentElement()
|
||||||
|
break
|
||||||
|
case 'del':
|
||||||
|
this.app.deleteCurrentElements()
|
||||||
|
break
|
||||||
|
case 'copy':
|
||||||
|
this.app.copyPasteCurrentElements()
|
||||||
|
break
|
||||||
|
case 'selectAll':
|
||||||
|
this.app.selectAll()
|
||||||
|
break
|
||||||
|
case 'backToCenter':
|
||||||
|
this.app.scrollToCenter()
|
||||||
|
break
|
||||||
|
case 'fit':
|
||||||
|
this.app.fit()
|
||||||
|
break
|
||||||
|
case 'resetZoom':
|
||||||
|
this.app.setZoom(1)
|
||||||
|
case 'dogroup':
|
||||||
|
this.app.dogroup()
|
||||||
|
break
|
||||||
|
case 'ungroup':
|
||||||
|
this.app.ungroup()
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
},
|
||||||
|
show(e, activeElements) {
|
||||||
|
this.isHasActiveElements = activeElements.length > 0
|
||||||
|
this.canMoveLevel = activeElements.length === 1
|
||||||
|
this.left = e.clientX + 10
|
||||||
|
this.top = e.clientY + 10
|
||||||
|
this.isShow = true
|
||||||
|
this.handleGroup(activeElements)
|
||||||
|
},
|
||||||
|
handleGroup(activeElements) {
|
||||||
|
let isGroup = true
|
||||||
|
activeElements.forEach(item => {
|
||||||
|
if (!item.hasGroup()) {
|
||||||
|
isGroup = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (isGroup) {
|
||||||
|
this.groupStatus = 'ungroup'
|
||||||
|
} else if (activeElements.length > 1) {
|
||||||
|
this.groupStatus = 'dogroup'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.contextmenuContainer {
|
||||||
|
position: fixed;
|
||||||
|
width: 161px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 4px 12px 0 hsla(0, 0%, 69%, 0.5);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding-top: 16px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: PingFangSC-Regular, PingFang SC;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #1a1a1a;
|
||||||
|
|
||||||
|
.splitLine {
|
||||||
|
height: 1px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
height: 28px;
|
||||||
|
line-height: 28px;
|
||||||
|
padding-left: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
&.danger {
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
&.disabled {
|
||||||
|
color: grey;
|
||||||
|
cursor: not-allowed;
|
||||||
|
&:hover {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,120 @@
|
||||||
|
// 描边颜色
|
||||||
|
export const strokeColorList = [
|
||||||
|
'#000000',
|
||||||
|
'#343a40',
|
||||||
|
'#495057',
|
||||||
|
'#c92a2a',
|
||||||
|
'#a61e4d',
|
||||||
|
'#862e9c',
|
||||||
|
'#5f3dc4',
|
||||||
|
'#364fc7',
|
||||||
|
'#1864ab',
|
||||||
|
'#0b7285',
|
||||||
|
'#087f5b',
|
||||||
|
'#2b8a3e',
|
||||||
|
'#5c940d',
|
||||||
|
'#e67700',
|
||||||
|
'#d9480f'
|
||||||
|
]
|
||||||
|
|
||||||
|
// 填充颜色
|
||||||
|
export const fillColorList = [
|
||||||
|
'transparent',
|
||||||
|
'#ced4da',
|
||||||
|
'#868e96',
|
||||||
|
'#fa5252',
|
||||||
|
'#e64980',
|
||||||
|
'#be4bdb',
|
||||||
|
'#7950f2',
|
||||||
|
'#4c6ef5',
|
||||||
|
'#228be6',
|
||||||
|
'#15aabf',
|
||||||
|
'#12b886',
|
||||||
|
'#40c057',
|
||||||
|
'#82c91e',
|
||||||
|
'#fab005',
|
||||||
|
'#fd7e14'
|
||||||
|
]
|
||||||
|
|
||||||
|
// 背景颜色
|
||||||
|
export const backgroundColorList = [
|
||||||
|
'#ffffff',
|
||||||
|
'#f8f9fa',
|
||||||
|
'#f1f3f5',
|
||||||
|
'#fff5f5',
|
||||||
|
'#fff0f6',
|
||||||
|
'#f8f0fc',
|
||||||
|
'#f3f0ff',
|
||||||
|
'#edf2ff',
|
||||||
|
'#e7f5ff',
|
||||||
|
'#e3fafc',
|
||||||
|
'#e6fcf5',
|
||||||
|
'#ebfbee',
|
||||||
|
'#f4fce3',
|
||||||
|
'#fff9db',
|
||||||
|
'#fff4e6'
|
||||||
|
]
|
||||||
|
|
||||||
|
// 字体列表
|
||||||
|
export const fontFamilyList = [
|
||||||
|
{
|
||||||
|
name: '微软雅黑',
|
||||||
|
value: '微软雅黑, Microsoft YaHei'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '宋体',
|
||||||
|
value: '宋体, SimSun, Songti SC'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '楷体',
|
||||||
|
value: '楷体, 楷体_GB2312, SimKai, STKaiti'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '黑体',
|
||||||
|
value: '黑体, SimHei, Heiti SC'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '隶书',
|
||||||
|
value: '隶书, SimLi'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Andale Mono',
|
||||||
|
value: 'andale mono'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Arial',
|
||||||
|
value: 'arial, helvetica, sans-serif'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'arialBlack',
|
||||||
|
value: 'arial black, avant garde'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Comic Sans Ms',
|
||||||
|
value: 'comic sans ms'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Impact',
|
||||||
|
value: 'impact, chicago'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Times New Roman',
|
||||||
|
value: 'times new roman'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Sans-Serif',
|
||||||
|
value: 'sans-serif'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'serif',
|
||||||
|
value: 'serif'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// 字号
|
||||||
|
export const fontSizeList = [10, 12, 16, 18, 24, 32, 48].map(item => {
|
||||||
|
return {
|
||||||
|
name: item,
|
||||||
|
value: item
|
||||||
|
}
|
||||||
|
})
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="11" height="22"><defs><linearGradient id="a"><stop offset="0"/><stop offset="1" stop-opacity="0"/></linearGradient><radialGradient xlink:href="#a" cx="9.739" cy="9.716" fx="9.739" fy="9.716" r="3.709" gradientUnits="userSpaceOnUse"/></defs><g stroke="#000" fill="none"><g transform="translate(-129.5 -333.862) translate(0 .188)"><rect transform="matrix(.962 0 0 .971 4.943 11.548)" ry="2" rx="2" y="332.362" x="130" height="10.337" width="10.432" opacity=".5"/><g><path d="M132 339.175h6" opacity=".5"/><path d="M135 336.175v6" opacity=".5"/></g></g><g transform="translate(-129.5 -333.862)"><rect width="10.432" height="10.337" x="130" y="332.362" rx="2" ry="2" transform="matrix(.962 0 0 .971 4.943 22.736)" opacity=".5"/><path d="M132 350.362h6" opacity=".5"/></g></g></svg>
|
After Width: | Height: | Size: 867 B |
|
@ -0,0 +1,107 @@
|
||||||
|
/*
|
||||||
|
* JSON Tree Viewer
|
||||||
|
* http://github.com/summerstyle/jsonTreeViewer
|
||||||
|
*
|
||||||
|
* Copyright 2017 Vera Lobacheva (http://iamvera.com)
|
||||||
|
* Released under the MIT license (LICENSE.txt)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Background for the tree. May use for <body> element */
|
||||||
|
.jsontree_bg {
|
||||||
|
background: #FFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Styles for the container of the tree (e.g. fonts, margins etc.) */
|
||||||
|
.jsontree_tree {
|
||||||
|
margin-left: 30px;
|
||||||
|
font-family: 'PT Mono', monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Styles for a list of child nodes */
|
||||||
|
.jsontree_child-nodes {
|
||||||
|
display: none;
|
||||||
|
margin-left: 35px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
line-height: 2;
|
||||||
|
}
|
||||||
|
.jsontree_node_expanded > .jsontree_value-wrapper > .jsontree_value > .jsontree_child-nodes {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Styles for labels */
|
||||||
|
.jsontree_label-wrapper {
|
||||||
|
float: left;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
.jsontree_label {
|
||||||
|
font-weight: normal;
|
||||||
|
vertical-align: top;
|
||||||
|
color: #000;
|
||||||
|
position: relative;
|
||||||
|
padding: 1px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.jsontree_node_marked > .jsontree_label-wrapper > .jsontree_label {
|
||||||
|
background: #fff2aa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Styles for values */
|
||||||
|
.jsontree_value-wrapper {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.jsontree_node_complex > .jsontree_value-wrapper {
|
||||||
|
overflow: inherit;
|
||||||
|
}
|
||||||
|
.jsontree_value {
|
||||||
|
vertical-align: top;
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
.jsontree_value_null {
|
||||||
|
color: #777;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.jsontree_value_string {
|
||||||
|
color: #025900;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.jsontree_value_number {
|
||||||
|
color: #000E59;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.jsontree_value_boolean {
|
||||||
|
color: #600100;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Styles for active elements */
|
||||||
|
.jsontree_expand-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 3px;
|
||||||
|
left: -15px;
|
||||||
|
display: block;
|
||||||
|
width: 11px;
|
||||||
|
height: 11px;
|
||||||
|
background-image: url('icons.svg');
|
||||||
|
}
|
||||||
|
.jsontree_node_expanded > .jsontree_label-wrapper > .jsontree_label > .jsontree_expand-button {
|
||||||
|
background-position: 0 -11px;
|
||||||
|
}
|
||||||
|
.jsontree_show-more {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.jsontree_node_expanded > .jsontree_value-wrapper > .jsontree_value > .jsontree_show-more {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.jsontree_node_empty > .jsontree_label-wrapper > .jsontree_label > .jsontree_expand-button,
|
||||||
|
.jsontree_node_empty > .jsontree_value-wrapper > .jsontree_value > .jsontree_show-more {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
.jsontree_node_complex > .jsontree_label-wrapper > .jsontree_label {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.jsontree_node_empty > .jsontree_label-wrapper > .jsontree_label {
|
||||||
|
cursor: default !important;
|
||||||
|
}
|
|
@ -0,0 +1,822 @@
|
||||||
|
/**
|
||||||
|
* JSON Tree library (a part of jsonTreeViewer)
|
||||||
|
* http://github.com/summerstyle/jsonTreeViewer
|
||||||
|
*
|
||||||
|
* Copyright 2017 Vera Lobacheva (http://iamvera.com)
|
||||||
|
* Released under the MIT license (LICENSE.txt)
|
||||||
|
*/
|
||||||
|
|
||||||
|
var jsonTree = (function() {
|
||||||
|
|
||||||
|
/* ---------- Utilities ---------- */
|
||||||
|
var utils = {
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Returns js-"class" of value
|
||||||
|
*
|
||||||
|
* @param val {any type} - value
|
||||||
|
* @returns {string} - for example, "[object Function]"
|
||||||
|
*/
|
||||||
|
getClass : function(val) {
|
||||||
|
return Object.prototype.toString.call(val);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks for a type of value (for valid JSON data types).
|
||||||
|
* In other cases - throws an exception
|
||||||
|
*
|
||||||
|
* @param val {any type} - the value for new node
|
||||||
|
* @returns {string} ("object" | "array" | "null" | "boolean" | "number" | "string")
|
||||||
|
*/
|
||||||
|
getType : function(val) {
|
||||||
|
if (val === null) {
|
||||||
|
return 'null';
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (typeof val) {
|
||||||
|
case 'number':
|
||||||
|
return 'number';
|
||||||
|
|
||||||
|
case 'string':
|
||||||
|
return 'string';
|
||||||
|
|
||||||
|
case 'boolean':
|
||||||
|
return 'boolean';
|
||||||
|
}
|
||||||
|
|
||||||
|
switch(utils.getClass(val)) {
|
||||||
|
case '[object Array]':
|
||||||
|
return 'array';
|
||||||
|
|
||||||
|
case '[object Object]':
|
||||||
|
return 'object';
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Bad type: ' + utils.getClass(val));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies for each item of list some function
|
||||||
|
* and checks for last element of the list
|
||||||
|
*
|
||||||
|
* @param obj {Object | Array} - a list or a dict with child nodes
|
||||||
|
* @param func {Function} - the function for each item
|
||||||
|
*/
|
||||||
|
forEachNode : function(obj, func) {
|
||||||
|
var type = utils.getType(obj),
|
||||||
|
isLast;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'array':
|
||||||
|
isLast = obj.length - 1;
|
||||||
|
|
||||||
|
obj.forEach(function(item, i) {
|
||||||
|
func(i, item, i === isLast);
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'object':
|
||||||
|
var keys = Object.keys(obj).sort();
|
||||||
|
|
||||||
|
isLast = keys.length - 1;
|
||||||
|
|
||||||
|
keys.forEach(function(item, i) {
|
||||||
|
func(item, obj[item], i === isLast);
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements the kind of an inheritance by
|
||||||
|
* using parent prototype and
|
||||||
|
* creating intermediate constructor
|
||||||
|
*
|
||||||
|
* @param Child {Function} - a child constructor
|
||||||
|
* @param Parent {Function} - a parent constructor
|
||||||
|
*/
|
||||||
|
inherits : (function() {
|
||||||
|
var F = function() {};
|
||||||
|
|
||||||
|
return function(Child, Parent) {
|
||||||
|
F.prototype = Parent.prototype;
|
||||||
|
Child.prototype = new F();
|
||||||
|
Child.prototype.constructor = Child;
|
||||||
|
};
|
||||||
|
})(),
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Checks for a valid type of root node*
|
||||||
|
*
|
||||||
|
* @param {any type} jsonObj - a value for root node
|
||||||
|
* @returns {boolean} - true for an object or an array, false otherwise
|
||||||
|
*/
|
||||||
|
isValidRoot : function(jsonObj) {
|
||||||
|
switch (utils.getType(jsonObj)) {
|
||||||
|
case 'object':
|
||||||
|
case 'array':
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extends some object
|
||||||
|
*/
|
||||||
|
extend : function(targetObj, sourceObj) {
|
||||||
|
for (var prop in sourceObj) {
|
||||||
|
if (sourceObj.hasOwnProperty(prop)) {
|
||||||
|
targetObj[prop] = sourceObj[prop];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/* ---------- Node constructors ---------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The factory for creating nodes of defined type.
|
||||||
|
*
|
||||||
|
* ~~~ Node ~~~ is a structure element of an onject or an array
|
||||||
|
* with own label (a key of an object or an index of an array)
|
||||||
|
* and value of any json data type. The root object or array
|
||||||
|
* is a node without label.
|
||||||
|
* {...
|
||||||
|
* [+] "label": value,
|
||||||
|
* ...}
|
||||||
|
*
|
||||||
|
* Markup:
|
||||||
|
* <li class="jsontree_node [jsontree_node_expanded]">
|
||||||
|
* <span class="jsontree_label-wrapper">
|
||||||
|
* <span class="jsontree_label">
|
||||||
|
* <span class="jsontree_expand-button" />
|
||||||
|
* "label"
|
||||||
|
* </span>
|
||||||
|
* :
|
||||||
|
* </span>
|
||||||
|
* <(div|span) class="jsontree_value jsontree_value_(object|array|boolean|null|number|string)">
|
||||||
|
* ...
|
||||||
|
* </(div|span)>
|
||||||
|
* </li>
|
||||||
|
*
|
||||||
|
* @param label {string} - key name
|
||||||
|
* @param val {Object | Array | string | number | boolean | null} - a value of node
|
||||||
|
* @param isLast {boolean} - true if node is last in list of siblings
|
||||||
|
*
|
||||||
|
* @return {Node}
|
||||||
|
*/
|
||||||
|
function Node(label, val, isLast) {
|
||||||
|
var nodeType = utils.getType(val);
|
||||||
|
|
||||||
|
if (nodeType in Node.CONSTRUCTORS) {
|
||||||
|
return new Node.CONSTRUCTORS[nodeType](label, val, isLast);
|
||||||
|
} else {
|
||||||
|
throw new Error('Bad type: ' + utils.getClass(val));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Node.CONSTRUCTORS = {
|
||||||
|
'boolean' : NodeBoolean,
|
||||||
|
'number' : NodeNumber,
|
||||||
|
'string' : NodeString,
|
||||||
|
'null' : NodeNull,
|
||||||
|
'object' : NodeObject,
|
||||||
|
'array' : NodeArray
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The constructor for simple types (string, number, boolean, null)
|
||||||
|
* {...
|
||||||
|
* [+] "label": value,
|
||||||
|
* ...}
|
||||||
|
* value = string || number || boolean || null
|
||||||
|
*
|
||||||
|
* Markup:
|
||||||
|
* <li class="jsontree_node">
|
||||||
|
* <span class="jsontree_label-wrapper">
|
||||||
|
* <span class="jsontree_label">"age"</span>
|
||||||
|
* :
|
||||||
|
* </span>
|
||||||
|
* <span class="jsontree_value jsontree_value_(number|boolean|string|null)">25</span>
|
||||||
|
* ,
|
||||||
|
* </li>
|
||||||
|
*
|
||||||
|
* @abstract
|
||||||
|
* @param label {string} - key name
|
||||||
|
* @param val {string | number | boolean | null} - a value of simple types
|
||||||
|
* @param isLast {boolean} - true if node is last in list of parent childNodes
|
||||||
|
*/
|
||||||
|
function _NodeSimple(label, val, isLast) {
|
||||||
|
if (this.constructor === _NodeSimple) {
|
||||||
|
throw new Error('This is abstract class');
|
||||||
|
}
|
||||||
|
|
||||||
|
var self = this,
|
||||||
|
el = document.createElement('li'),
|
||||||
|
labelEl,
|
||||||
|
template = function(label, val) {
|
||||||
|
var str = '\
|
||||||
|
<span class="jsontree_label-wrapper">\
|
||||||
|
<span class="jsontree_label">"' +
|
||||||
|
label +
|
||||||
|
'"</span> : \
|
||||||
|
</span>\
|
||||||
|
<span class="jsontree_value-wrapper">\
|
||||||
|
<span class="jsontree_value jsontree_value_' + self.type + '">' +
|
||||||
|
val +
|
||||||
|
'</span>' +
|
||||||
|
(!isLast ? ',' : '') +
|
||||||
|
'</span>';
|
||||||
|
|
||||||
|
return str;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.label = label;
|
||||||
|
self.isComplex = false;
|
||||||
|
|
||||||
|
el.classList.add('jsontree_node');
|
||||||
|
el.innerHTML = template(label, val);
|
||||||
|
|
||||||
|
self.el = el;
|
||||||
|
|
||||||
|
labelEl = el.querySelector('.jsontree_label');
|
||||||
|
|
||||||
|
labelEl.addEventListener('click', function(e) {
|
||||||
|
if (e.altKey) {
|
||||||
|
self.toggleMarked();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.shiftKey) {
|
||||||
|
document.getSelection().removeAllRanges();
|
||||||
|
alert(self.getJSONPath());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
_NodeSimple.prototype = {
|
||||||
|
constructor : _NodeSimple,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark node
|
||||||
|
*/
|
||||||
|
mark : function() {
|
||||||
|
this.el.classList.add('jsontree_node_marked');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unmark node
|
||||||
|
*/
|
||||||
|
unmark : function() {
|
||||||
|
this.el.classList.remove('jsontree_node_marked');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark or unmark node
|
||||||
|
*/
|
||||||
|
toggleMarked : function() {
|
||||||
|
this.el.classList.toggle('jsontree_node_marked');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expands parent node of this node
|
||||||
|
*
|
||||||
|
* @param isRecursive {boolean} - if true, expands all parent nodes
|
||||||
|
* (from node to root)
|
||||||
|
*/
|
||||||
|
expandParent : function(isRecursive) {
|
||||||
|
if (!this.parent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.parent.expand();
|
||||||
|
this.parent.expandParent(isRecursive);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns JSON-path of this
|
||||||
|
*
|
||||||
|
* @param isInDotNotation {boolean} - kind of notation for returned json-path
|
||||||
|
* (by default, in bracket notation)
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
getJSONPath : function(isInDotNotation) {
|
||||||
|
if (this.isRoot) {
|
||||||
|
return "$";
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentPath;
|
||||||
|
|
||||||
|
if (this.parent.type === 'array') {
|
||||||
|
currentPath = "[" + this.label + "]";
|
||||||
|
} else {
|
||||||
|
currentPath = isInDotNotation ? "." + this.label : "['" + this.label + "']";
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.parent.getJSONPath(isInDotNotation) + currentPath;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The constructor for boolean values
|
||||||
|
* {...
|
||||||
|
* [+] "label": boolean,
|
||||||
|
* ...}
|
||||||
|
* boolean = true || false
|
||||||
|
*
|
||||||
|
* @constructor
|
||||||
|
* @param label {string} - key name
|
||||||
|
* @param val {boolean} - value of boolean type, true or false
|
||||||
|
* @param isLast {boolean} - true if node is last in list of parent childNodes
|
||||||
|
*/
|
||||||
|
function NodeBoolean(label, val, isLast) {
|
||||||
|
this.type = "boolean";
|
||||||
|
|
||||||
|
_NodeSimple.call(this, label, val, isLast);
|
||||||
|
}
|
||||||
|
utils.inherits(NodeBoolean,_NodeSimple);
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The constructor for number values
|
||||||
|
* {...
|
||||||
|
* [+] "label": number,
|
||||||
|
* ...}
|
||||||
|
* number = 123
|
||||||
|
*
|
||||||
|
* @constructor
|
||||||
|
* @param label {string} - key name
|
||||||
|
* @param val {number} - value of number type, for example 123
|
||||||
|
* @param isLast {boolean} - true if node is last in list of parent childNodes
|
||||||
|
*/
|
||||||
|
function NodeNumber(label, val, isLast) {
|
||||||
|
this.type = "number";
|
||||||
|
|
||||||
|
_NodeSimple.call(this, label, val, isLast);
|
||||||
|
}
|
||||||
|
utils.inherits(NodeNumber,_NodeSimple);
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The constructor for string values
|
||||||
|
* {...
|
||||||
|
* [+] "label": string,
|
||||||
|
* ...}
|
||||||
|
* string = "abc"
|
||||||
|
*
|
||||||
|
* @constructor
|
||||||
|
* @param label {string} - key name
|
||||||
|
* @param val {string} - value of string type, for example "abc"
|
||||||
|
* @param isLast {boolean} - true if node is last in list of parent childNodes
|
||||||
|
*/
|
||||||
|
function NodeString(label, val, isLast) {
|
||||||
|
this.type = "string";
|
||||||
|
|
||||||
|
_NodeSimple.call(this, label, '"' + val + '"', isLast);
|
||||||
|
}
|
||||||
|
utils.inherits(NodeString,_NodeSimple);
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The constructor for null values
|
||||||
|
* {...
|
||||||
|
* [+] "label": null,
|
||||||
|
* ...}
|
||||||
|
*
|
||||||
|
* @constructor
|
||||||
|
* @param label {string} - key name
|
||||||
|
* @param val {null} - value (only null)
|
||||||
|
* @param isLast {boolean} - true if node is last in list of parent childNodes
|
||||||
|
*/
|
||||||
|
function NodeNull(label, val, isLast) {
|
||||||
|
this.type = "null";
|
||||||
|
|
||||||
|
_NodeSimple.call(this, label, val, isLast);
|
||||||
|
}
|
||||||
|
utils.inherits(NodeNull,_NodeSimple);
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The constructor for complex types (object, array)
|
||||||
|
* {...
|
||||||
|
* [+] "label": value,
|
||||||
|
* ...}
|
||||||
|
* value = object || array
|
||||||
|
*
|
||||||
|
* Markup:
|
||||||
|
* <li class="jsontree_node jsontree_node_(object|array) [expanded]">
|
||||||
|
* <span class="jsontree_label-wrapper">
|
||||||
|
* <span class="jsontree_label">
|
||||||
|
* <span class="jsontree_expand-button" />
|
||||||
|
* "label"
|
||||||
|
* </span>
|
||||||
|
* :
|
||||||
|
* </span>
|
||||||
|
* <div class="jsontree_value">
|
||||||
|
* <b>{</b>
|
||||||
|
* <ul class="jsontree_child-nodes" />
|
||||||
|
* <b>}</b>
|
||||||
|
* ,
|
||||||
|
* </div>
|
||||||
|
* </li>
|
||||||
|
*
|
||||||
|
* @abstract
|
||||||
|
* @param label {string} - key name
|
||||||
|
* @param val {Object | Array} - a value of complex types, object or array
|
||||||
|
* @param isLast {boolean} - true if node is last in list of parent childNodes
|
||||||
|
*/
|
||||||
|
function _NodeComplex(label, val, isLast) {
|
||||||
|
if (this.constructor === _NodeComplex) {
|
||||||
|
throw new Error('This is abstract class');
|
||||||
|
}
|
||||||
|
|
||||||
|
var self = this,
|
||||||
|
el = document.createElement('li'),
|
||||||
|
template = function(label, sym) {
|
||||||
|
var comma = (!isLast) ? ',' : '',
|
||||||
|
str = '\
|
||||||
|
<div class="jsontree_value-wrapper">\
|
||||||
|
<div class="jsontree_value jsontree_value_' + self.type + '">\
|
||||||
|
<b>' + sym[0] + '</b>\
|
||||||
|
<span class="jsontree_show-more">…</span>\
|
||||||
|
<ul class="jsontree_child-nodes"></ul>\
|
||||||
|
<b>' + sym[1] + '</b>' +
|
||||||
|
'</div>' + comma +
|
||||||
|
'</div>';
|
||||||
|
|
||||||
|
if (label !== null) {
|
||||||
|
str = '\
|
||||||
|
<span class="jsontree_label-wrapper">\
|
||||||
|
<span class="jsontree_label">' +
|
||||||
|
'<span class="jsontree_expand-button"></span>' +
|
||||||
|
'"' + label +
|
||||||
|
'"</span> : \
|
||||||
|
</span>' + str;
|
||||||
|
}
|
||||||
|
|
||||||
|
return str;
|
||||||
|
},
|
||||||
|
childNodesUl,
|
||||||
|
labelEl,
|
||||||
|
moreContentEl,
|
||||||
|
childNodes = [];
|
||||||
|
|
||||||
|
self.label = label;
|
||||||
|
self.isComplex = true;
|
||||||
|
|
||||||
|
el.classList.add('jsontree_node');
|
||||||
|
el.classList.add('jsontree_node_complex');
|
||||||
|
el.innerHTML = template(label, self.sym);
|
||||||
|
|
||||||
|
childNodesUl = el.querySelector('.jsontree_child-nodes');
|
||||||
|
|
||||||
|
if (label !== null) {
|
||||||
|
labelEl = el.querySelector('.jsontree_label');
|
||||||
|
moreContentEl = el.querySelector('.jsontree_show-more');
|
||||||
|
|
||||||
|
labelEl.addEventListener('click', function(e) {
|
||||||
|
if (e.altKey) {
|
||||||
|
self.toggleMarked();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.shiftKey) {
|
||||||
|
document.getSelection().removeAllRanges();
|
||||||
|
alert(self.getJSONPath());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.toggle(e.ctrlKey || e.metaKey);
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
moreContentEl.addEventListener('click', function(e) {
|
||||||
|
self.toggle(e.ctrlKey || e.metaKey);
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
self.isRoot = false;
|
||||||
|
} else {
|
||||||
|
self.isRoot = true;
|
||||||
|
self.parent = null;
|
||||||
|
|
||||||
|
el.classList.add('jsontree_node_expanded');
|
||||||
|
}
|
||||||
|
|
||||||
|
self.el = el;
|
||||||
|
self.childNodes = childNodes;
|
||||||
|
self.childNodesUl = childNodesUl;
|
||||||
|
|
||||||
|
utils.forEachNode(val, function(label, node, isLast) {
|
||||||
|
self.addChild(new Node(label, node, isLast));
|
||||||
|
});
|
||||||
|
|
||||||
|
self.isEmpty = !Boolean(childNodes.length);
|
||||||
|
if (self.isEmpty) {
|
||||||
|
el.classList.add('jsontree_node_empty');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.inherits(_NodeComplex, _NodeSimple);
|
||||||
|
|
||||||
|
utils.extend(_NodeComplex.prototype, {
|
||||||
|
constructor : _NodeComplex,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Add child node to list of child nodes
|
||||||
|
*
|
||||||
|
* @param child {Node} - child node
|
||||||
|
*/
|
||||||
|
addChild : function(child) {
|
||||||
|
this.childNodes.push(child);
|
||||||
|
this.childNodesUl.appendChild(child.el);
|
||||||
|
child.parent = this;
|
||||||
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Expands this list of node child nodes
|
||||||
|
*
|
||||||
|
* @param isRecursive {boolean} - if true, expands all child nodes
|
||||||
|
*/
|
||||||
|
expand : function(isRecursive){
|
||||||
|
if (this.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.isRoot) {
|
||||||
|
this.el.classList.add('jsontree_node_expanded');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRecursive) {
|
||||||
|
this.childNodes.forEach(function(item, i) {
|
||||||
|
if (item.isComplex) {
|
||||||
|
item.expand(isRecursive);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Collapses this list of node child nodes
|
||||||
|
*
|
||||||
|
* @param isRecursive {boolean} - if true, collapses all child nodes
|
||||||
|
*/
|
||||||
|
collapse : function(isRecursive) {
|
||||||
|
if (this.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.isRoot) {
|
||||||
|
this.el.classList.remove('jsontree_node_expanded');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRecursive) {
|
||||||
|
this.childNodes.forEach(function(item, i) {
|
||||||
|
if (item.isComplex) {
|
||||||
|
item.collapse(isRecursive);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Expands collapsed or collapses expanded node
|
||||||
|
*
|
||||||
|
* @param {boolean} isRecursive - Expand all child nodes if this node is expanded
|
||||||
|
* and collapse it otherwise
|
||||||
|
*/
|
||||||
|
toggle : function(isRecursive) {
|
||||||
|
if (this.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.el.classList.toggle('jsontree_node_expanded');
|
||||||
|
|
||||||
|
if (isRecursive) {
|
||||||
|
var isExpanded = this.el.classList.contains('jsontree_node_expanded');
|
||||||
|
|
||||||
|
this.childNodes.forEach(function(item, i) {
|
||||||
|
if (item.isComplex) {
|
||||||
|
item[isExpanded ? 'expand' : 'collapse'](isRecursive);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find child nodes that match some conditions and handle it
|
||||||
|
*
|
||||||
|
* @param {Function} matcher
|
||||||
|
* @param {Function} handler
|
||||||
|
* @param {boolean} isRecursive
|
||||||
|
*/
|
||||||
|
findChildren : function(matcher, handler, isRecursive) {
|
||||||
|
if (this.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.childNodes.forEach(function(item, i) {
|
||||||
|
if (matcher(item)) {
|
||||||
|
handler(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.isComplex && isRecursive) {
|
||||||
|
item.findChildren(matcher, handler, isRecursive);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The constructor for object values
|
||||||
|
* {...
|
||||||
|
* [+] "label": object,
|
||||||
|
* ...}
|
||||||
|
* object = {"abc": "def"}
|
||||||
|
*
|
||||||
|
* @constructor
|
||||||
|
* @param label {string} - key name
|
||||||
|
* @param val {Object} - value of object type, {"abc": "def"}
|
||||||
|
* @param isLast {boolean} - true if node is last in list of siblings
|
||||||
|
*/
|
||||||
|
function NodeObject(label, val, isLast) {
|
||||||
|
this.sym = ['{', '}'];
|
||||||
|
this.type = "object";
|
||||||
|
|
||||||
|
_NodeComplex.call(this, label, val, isLast);
|
||||||
|
}
|
||||||
|
utils.inherits(NodeObject,_NodeComplex);
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The constructor for array values
|
||||||
|
* {...
|
||||||
|
* [+] "label": array,
|
||||||
|
* ...}
|
||||||
|
* array = [1,2,3]
|
||||||
|
*
|
||||||
|
* @constructor
|
||||||
|
* @param label {string} - key name
|
||||||
|
* @param val {Array} - value of array type, [1,2,3]
|
||||||
|
* @param isLast {boolean} - true if node is last in list of siblings
|
||||||
|
*/
|
||||||
|
function NodeArray(label, val, isLast) {
|
||||||
|
this.sym = ['[', ']'];
|
||||||
|
this.type = "array";
|
||||||
|
|
||||||
|
_NodeComplex.call(this, label, val, isLast);
|
||||||
|
}
|
||||||
|
utils.inherits(NodeArray, _NodeComplex);
|
||||||
|
|
||||||
|
|
||||||
|
/* ---------- The tree constructor ---------- */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The constructor for json tree.
|
||||||
|
* It contains only one Node (Array or Object), without property name.
|
||||||
|
* CSS-styles of .tree define main tree styles like font-family,
|
||||||
|
* font-size and own margins.
|
||||||
|
*
|
||||||
|
* Markup:
|
||||||
|
* <ul class="jsontree_tree clearfix">
|
||||||
|
* {Node}
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @constructor
|
||||||
|
* @param jsonObj {Object | Array} - data for tree
|
||||||
|
* @param domEl {DOMElement} - DOM-element, wrapper for tree
|
||||||
|
*/
|
||||||
|
function Tree(jsonObj, domEl) {
|
||||||
|
this.wrapper = document.createElement('ul');
|
||||||
|
this.wrapper.className = 'jsontree_tree clearfix';
|
||||||
|
|
||||||
|
this.rootNode = null;
|
||||||
|
|
||||||
|
this.sourceJSONObj = jsonObj;
|
||||||
|
|
||||||
|
this.loadData(jsonObj);
|
||||||
|
this.appendTo(domEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
Tree.prototype = {
|
||||||
|
constructor : Tree,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fill new data in current json tree
|
||||||
|
*
|
||||||
|
* @param {Object | Array} jsonObj - json-data
|
||||||
|
*/
|
||||||
|
loadData : function(jsonObj) {
|
||||||
|
if (!utils.isValidRoot(jsonObj)) {
|
||||||
|
alert('The root should be an object or an array');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sourceJSONObj = jsonObj;
|
||||||
|
|
||||||
|
this.rootNode = new Node(null, jsonObj, 'last');
|
||||||
|
this.wrapper.innerHTML = '';
|
||||||
|
this.wrapper.appendChild(this.rootNode.el);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends tree to DOM-element (or move it to new place)
|
||||||
|
*
|
||||||
|
* @param {DOMElement} domEl
|
||||||
|
*/
|
||||||
|
appendTo : function(domEl) {
|
||||||
|
domEl.appendChild(this.wrapper);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expands all tree nodes (objects or arrays) recursively
|
||||||
|
*
|
||||||
|
* @param {Function} filterFunc - 'true' if this node should be expanded
|
||||||
|
*/
|
||||||
|
expand : function(filterFunc) {
|
||||||
|
if (this.rootNode.isComplex) {
|
||||||
|
if (typeof filterFunc == 'function') {
|
||||||
|
this.rootNode.childNodes.forEach(function(item, i) {
|
||||||
|
if (item.isComplex && filterFunc(item)) {
|
||||||
|
item.expand();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.rootNode.expand('recursive');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collapses all tree nodes (objects or arrays) recursively
|
||||||
|
*/
|
||||||
|
collapse : function() {
|
||||||
|
if (typeof this.rootNode.collapse === 'function') {
|
||||||
|
this.rootNode.collapse('recursive');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the source json-string (pretty-printed)
|
||||||
|
*
|
||||||
|
* @param {boolean} isPrettyPrinted - 'true' for pretty-printed string
|
||||||
|
* @returns {string} - for exemple, '{"a":2,"b":3}'
|
||||||
|
*/
|
||||||
|
toSourceJSON : function(isPrettyPrinted) {
|
||||||
|
if (!isPrettyPrinted) {
|
||||||
|
return JSON.stringify(this.sourceJSONObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
var DELIMETER = "[%^$#$%^%]",
|
||||||
|
jsonStr = JSON.stringify(this.sourceJSONObj, null, DELIMETER);
|
||||||
|
|
||||||
|
jsonStr = jsonStr.split("\n").join("<br />");
|
||||||
|
jsonStr = jsonStr.split(DELIMETER).join(" ");
|
||||||
|
|
||||||
|
return jsonStr;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all nodes that match some conditions and handle it
|
||||||
|
*/
|
||||||
|
findAndHandle : function(matcher, handler) {
|
||||||
|
this.rootNode.findChildren(matcher, handler, 'isRecursive');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unmark all nodes
|
||||||
|
*/
|
||||||
|
unmarkAll : function() {
|
||||||
|
this.rootNode.findChildren(function(node) {
|
||||||
|
return true;
|
||||||
|
}, function(node) {
|
||||||
|
node.unmark();
|
||||||
|
}, 'isRecursive');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/* ---------- Public methods ---------- */
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* Creates new tree by data and appends it to the DOM-element
|
||||||
|
*
|
||||||
|
* @param jsonObj {Object | Array} - json-data
|
||||||
|
* @param domEl {DOMElement} - the wrapper element
|
||||||
|
* @returns {Tree}
|
||||||
|
*/
|
||||||
|
create : function(jsonObj, domEl) {
|
||||||
|
return new Tree(jsonObj, domEl);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
export default {
|
||||||
|
jsonTree
|
||||||
|
}
|
|
@ -0,0 +1,913 @@
|
||||||
|
<template>
|
||||||
|
<div class="whiteboart-container" :style="{ height: height + 'px' }">
|
||||||
|
<div class="canvasBox" ref="box"></div>
|
||||||
|
|
||||||
|
<div class="footerLeft" @click.stop
|
||||||
|
:style="type == 'design' ? ['top: 10px', 'justify-content: space-between'] : ['bottom: 10px', 'justify-content: center']">
|
||||||
|
<div class="left">
|
||||||
|
<!-- 前进回退 -->
|
||||||
|
<div class="blockBox" v-if="!readonly && width >= 1000">
|
||||||
|
<el-tooltip effect="light" content="回退" placement="top">
|
||||||
|
<el-button :icon="RefreshLeft" circle :disabled="!canUndo" @click="undo" />
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip effect="light" content="前进" placement="top">
|
||||||
|
<el-button :icon="RefreshRight" circle :disabled="!canRedo" @click="redo" />
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
<div class="blockBox">
|
||||||
|
<el-button @click="currentType = 'selection'"><el-image src="/src/assets/icons/pngjpg/mouse-pointer.png"
|
||||||
|
style="width: 14px; height: 14px; color: silver" /></el-button>
|
||||||
|
</div>
|
||||||
|
<template v-if="type == 'design'">
|
||||||
|
<el-radio-group v-model="currentType" @change="onCurrentTypeChange">
|
||||||
|
<el-tooltip effect="light" content="画笔" placement="top">
|
||||||
|
<el-radio-button label="画笔" value="freedraw">
|
||||||
|
<svg t="1719045569796" class="icon" viewBox="0 0 1031 1024" version="1.1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg" p-id="11317" width="14" height="14">
|
||||||
|
<path
|
||||||
|
d="M1001.476376 77.351719c-26.164542-24.960575-25.568765-24.364798-57.095299-53.495813-30.930758-29.118603-98.129443-36.255515-139.759364 5.349581S127.223222 709.769044 127.223222 709.769044 8.303642 983.181056 1.737683 998.646435 8.303642 1028.956592 23.198068 1022.415456s301.525241-125.361418 301.525241-125.361418l676.802715-676.455178c51.745718-51.708482 26.164542-118.286566 0-143.247141zM79.635531 942.171737l76.13534-175.34463 102.883247 99.854714z m233.730779-106.991627l-120.123545-116.499234 466.853369-464.83021 116.573707 112.949397z m510.283032-512.35584l-119.54018-116.499235 52.937272-52.900036 119.540181 116.561295z m135.601336-132.535567l-45.19217 45.179759-123.71062-118.894755s26.760319-26.15213 46.383725-45.167347 57.691076-17.836075 77.314483 1.241203c19.623406 18.431852 20.81496 19.610994 42.200873 40.413542 25.568765 24.364798 21.410737 58.845394 2.966473 77.264834z m0 0"
|
||||||
|
fill="#848282" p-id="11318"></path>
|
||||||
|
</svg>
|
||||||
|
</el-radio-button>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip effect="light" content="文字" placement="top">
|
||||||
|
<el-radio-button label="文字" value="text">
|
||||||
|
<svg t="1719046751133" class="icon" viewBox="0 0 1024 1024" version="1.1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg" p-id="16523" width="14" height="14">
|
||||||
|
<path
|
||||||
|
d="M429.056 919.552h166.4l-0.512-1.024c-32.768-40.96-50.688-92.16-50.688-144.384V228.352h154.112c44.032 0 87.04 14.848 121.856 42.496l11.776 9.216V154.112s-128 20.48-319.488 20.48-321.024-20.48-321.024-20.48v125.44l8.192-6.656c35.328-28.672 79.36-44.544 124.928-44.544h155.648v545.28c0 52.736-17.92 103.424-50.688 144.384l-0.512 1.536z"
|
||||||
|
fill="#848282" p-id="16524"></path>
|
||||||
|
</svg>
|
||||||
|
</el-radio-button>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip effect="light" content="图片" placement="top">
|
||||||
|
<el-radio-button label="图片" value="image">
|
||||||
|
<svg t="1719045309869" class="icon" viewBox="0 0 1024 1024" version="1.1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg" p-id="6679" width="14" height="14">
|
||||||
|
<path
|
||||||
|
d="M867.90864 574.538232V257.779543a50.844091 50.844091 0 0 0-50.844092-50.844091h-610.129096a50.844091 50.844091 0 0 0-50.844092 50.844091v499.797418l430.141013-257.779543a152.532274 152.532274 0 0 1 157.108243 0z m0 118.466733l-177.445879-106.264151a50.844091 50.844091 0 0 0-50.844092 0L254.220457 817.064548h562.844091a50.844091 50.844091 0 0 0 50.844092-50.844091z m-660.973188-587.757696h610.129096a152.532274 152.532274 0 0 1 152.532274 152.532274v508.440914a152.532274 152.532274 0 0 1-152.532274 152.532274h-610.129096a152.532274 152.532274 0 0 1-152.532274-152.532274v-508.440914a152.532274 152.532274 0 0 1 152.532274-152.532274z m127.110228 355.90864a76.266137 76.266137 0 1 1 76.266137-76.266137 76.266137 76.266137 0 0 1-76.266137 76.266137z"
|
||||||
|
fill="#848282" p-id="6680"></path>
|
||||||
|
</svg>
|
||||||
|
</el-radio-button>
|
||||||
|
</el-tooltip>
|
||||||
|
</el-radio-group>
|
||||||
|
</template>
|
||||||
|
<div class="blockBox">
|
||||||
|
<el-button @click="currentType = 'selection'" style="color:#848282" :icon="Camera" disabled></el-button>
|
||||||
|
</div>
|
||||||
|
<div class="blockBox" v-if="!readonly">
|
||||||
|
<el-dropdown @command="handleToolTypeChange" placement="top">
|
||||||
|
<el-button>{{ type == 'design' ? '形状' : '工具' }}</el-button>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item command="rectangle">矩形</el-dropdown-item>
|
||||||
|
<el-dropdown-item command="diamond">菱形</el-dropdown-item>
|
||||||
|
<el-dropdown-item command="triangle">三角形</el-dropdown-item>
|
||||||
|
<el-dropdown-item command="circle">圆形</el-dropdown-item>
|
||||||
|
<el-dropdown-item command="line">线段</el-dropdown-item>
|
||||||
|
<el-dropdown-item command="arrow">箭头</el-dropdown-item>
|
||||||
|
<template v-if="type != 'design'">
|
||||||
|
<el-dropdown-item command="freedraw">画笔</el-dropdown-item>
|
||||||
|
<el-dropdown-item command="text">文字</el-dropdown-item>
|
||||||
|
<el-dropdown-item command="image">图片</el-dropdown-item>
|
||||||
|
</template>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</div>
|
||||||
|
<!-- AI -->
|
||||||
|
<div class="blockBox">
|
||||||
|
<el-dropdown @command="handleToolTypeChange" placement="top">
|
||||||
|
<el-button type="warning">AI</el-button>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item command="rectangle">教学大模型</el-dropdown-item>
|
||||||
|
<el-dropdown-item disabled command="diamond">单张图片创作</el-dropdown-item>
|
||||||
|
<el-dropdown-item disabled command="triandle">连环画创作</el-dropdown-item>
|
||||||
|
<el-dropdown-item disabled command="circle">视频创作</el-dropdown-item>
|
||||||
|
<el-dropdown-item disabled command="line">音乐创作</el-dropdown-item>
|
||||||
|
<el-dropdown-item disabled command="arrow">语音</el-dropdown-item>
|
||||||
|
<el-dropdown-item disabled command="freedraw">多语言翻译</el-dropdown-item>
|
||||||
|
<el-dropdown-item disabled command="text">数字人</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 只读、编辑模式切换 -->
|
||||||
|
<!-- <div class="blockBox">
|
||||||
|
<el-tooltip effect="light" :content="elReadonly ? '元素设置为可编辑' : '元素设置为只读'" placement="top">
|
||||||
|
<el-button :icon="elReadonly ? View : Edit" circle @click="elementModeChange" />
|
||||||
|
</el-tooltip>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<template v-if="!readonly">
|
||||||
|
<!-- 描边 -->
|
||||||
|
<div class="blockBox">
|
||||||
|
<el-tooltip effect="light" content="描边" placement="top">
|
||||||
|
<ColorPicker type="stroke" :value="activeElement?.style.strokeStyle"
|
||||||
|
@change="updateStyle('strokeStyle', $event)"></ColorPicker>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
<!-- 填充 -->
|
||||||
|
<div class="blockBox">
|
||||||
|
<el-tooltip effect="light" content="填充" placement="top">
|
||||||
|
<ColorPicker type="fill" :value="activeElement?.style.fillStyle"
|
||||||
|
@change="updateStyle('fillStyle', $event)">
|
||||||
|
</ColorPicker>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
<!-- 边框样式 -->
|
||||||
|
<div class="blockBox">
|
||||||
|
<el-dropdown @command="updateStyle('lineDash', $event)" placement="top">
|
||||||
|
<el-button><el-image src="/src/assets/icons/pngjpg/borderstyle.png"
|
||||||
|
style="width: 14px; height: 14px"></el-image></el-button>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item command="0">实线</el-dropdown-item>
|
||||||
|
<el-dropdown-item command="1">大虚线</el-dropdown-item>
|
||||||
|
<el-dropdown-item command="2">小虚线</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</div>
|
||||||
|
<!--透明度-->
|
||||||
|
<div class="blockBox" style="width: 120px"
|
||||||
|
v-if="type == 'design' ? true : ['image'].includes(activeElement?.type) || hasSelectedElements">
|
||||||
|
<el-tooltip effect="light" content="透明度" placement="top">
|
||||||
|
<el-input-number v-model="globalAlpha" :min="0" :max="1" :step="0.1"
|
||||||
|
@change="updateStyle('globalAlpha', $event)"></el-input-number>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 边框粗细 -->
|
||||||
|
<div class="blockBox">
|
||||||
|
<el-dropdown @command="updateStyle('lineWidth', $event)" placement="top">
|
||||||
|
<el-button><el-image src="/src/assets/icons/pngjpg/borderwidth.png"
|
||||||
|
style="width: 14px; height: 14px"></el-image></el-button>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item command="small">细</el-dropdown-item>
|
||||||
|
<el-dropdown-item command="middle">正常</el-dropdown-item>
|
||||||
|
<el-dropdown-item command="large">粗</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</div>
|
||||||
|
<!-- 字体 -->
|
||||||
|
<div class="blockBox"
|
||||||
|
v-if="type == 'design' ? true : ['text'].includes(activeElement?.type) || hasSelectedElements">
|
||||||
|
<div class="styleBlockContent">
|
||||||
|
<el-select v-model="fontFamily" placeholder="字体" @change="updateStyle('fontFamily', $event)"
|
||||||
|
style="width: 110px">
|
||||||
|
<el-option v-for="item in fontFamilyList" :key="item.value" :label="item.name" :value="item.value"
|
||||||
|
:style="{ fontFamily: item.value }"></el-option>
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 字号 -->
|
||||||
|
<div class="blockBox"
|
||||||
|
v-if="type == 'design' ? true : ['text'].includes(activeElement?.type) || hasSelectedElements">
|
||||||
|
<div class="styleBlockContent">
|
||||||
|
<el-select v-model="fontSize" placeholder="字号" @change="updateStyle('fontSize', $event)"
|
||||||
|
style="width: 80px">
|
||||||
|
<el-option v-for="item in fontSizeList" :key="item.value" :label="item.name" :value="item.value"
|
||||||
|
:style="{ fontSize: item.value }"></el-option>
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<!-- 滚动 -->
|
||||||
|
<div v-if="width >= 1000" class="blockBox">
|
||||||
|
<template v-if="type == 'design'">
|
||||||
|
<el-tooltip effect="light" content="滚动至中心" placement="top">
|
||||||
|
<el-button icon="Operation" @click="scrollToCenter" />
|
||||||
|
</el-tooltip>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<el-button @click="scrollToCenter">居中</el-button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 缩放 -->
|
||||||
|
<!-- <div v-if="width>=1000" class="blockBox">
|
||||||
|
<el-tooltip effect="light" content="缩小" placement="top">
|
||||||
|
<el-button :icon="ZoomOut" circle @click="zoomOut" />
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip effect="light" content="放大" placement="top">
|
||||||
|
<el-button :icon="ZoomIn" circle @click="zoomIn" />
|
||||||
|
</el-tooltip>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<!-- 橡皮擦、显示网格、清空 -->
|
||||||
|
<div v-if="width >= 1000" class="blockBox">
|
||||||
|
<!-- 橡皮擦 -->
|
||||||
|
<el-tooltip effect="light" :content="currentType === 'eraser' ? '关闭橡皮擦' : '橡皮擦'" placement="top">
|
||||||
|
<el-button v-if="!readonly" :icon="Remove" circle :type="currentType === 'eraser' ? 'primary' : null"
|
||||||
|
@click="toggleEraser" />
|
||||||
|
</el-tooltip>
|
||||||
|
<!-- 清空 -->
|
||||||
|
<el-tooltip effect="light" content="清空" placement="top">
|
||||||
|
<el-button v-if="!readonly" :icon="Delete" circle @click="empty" />
|
||||||
|
</el-tooltip>
|
||||||
|
<!-- 网格 -->
|
||||||
|
<el-tooltip effect="light" :content="showGrid ? '隐藏网格' : '显示网格'" placement="top">
|
||||||
|
<el-button :icon="Grid" circle :type="showGrid ? 'primary' : null" @click="toggleGrid" />
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
<!-- 保存提交 -->
|
||||||
|
<div class="blockBox" v-if="!readonly && isShowSave">
|
||||||
|
<el-tooltip effect="light" :content="type == 'design' ? '保存底板' : '保存'" placement="top">
|
||||||
|
<el-button type="success" style="margin-right: 10px" @click="onSave">{{ type == 'design' ? '保存底板' : '保存'
|
||||||
|
}}</el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
<!-- 点赞、评价等 -->
|
||||||
|
<template v-if="allowComment == true">
|
||||||
|
<div class="blockBox" style="margin-left: 50px">
|
||||||
|
<el-tooltip effect="light" content="评价" placement="top">
|
||||||
|
<el-button :icon="ChromeFilled" type="success" style="margin-right: 10px" @click="onSave">写评价</el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip effect="light" content="放大" placement="top">
|
||||||
|
<el-button circle type="success" @click="zoomIn"><el-image src="/src/assets/icons/pngjpg/img-thumbup.png"
|
||||||
|
style="height: 14px; width: 14px"></el-image></el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<!-- 课件设计才会有关闭按钮-->
|
||||||
|
<el-button v-if="type == 'design'" @click="closeBoard">关闭</el-button>
|
||||||
|
</div>
|
||||||
|
<!-- 导出图片弹窗 -->
|
||||||
|
<el-dialog v-model="exportImageDialogVisible" title="导出为图片" :width="800">
|
||||||
|
<div class="exportImageContainer">
|
||||||
|
<div class="imagePreviewBox">
|
||||||
|
<img :src="exportImageUrl" alt="" />
|
||||||
|
</div>
|
||||||
|
<div class="handleBox">
|
||||||
|
<el-checkbox v-model="exportOnlySelected" label="仅导出被选中" size="large" @change="reRenderExportImage"
|
||||||
|
style="margin-right: 10px" />
|
||||||
|
<el-checkbox v-model="exportRenderBackground" label="背景" size="large" @change="reRenderExportImage"
|
||||||
|
style="margin-right: 10px" />
|
||||||
|
<el-input v-model="exportFileName" style="width: 150px; margin-right: 10px"></el-input>
|
||||||
|
<el-input-number v-model="exportImagePaddingX" :min="10" :max="100" :step="5" controls-position="right"
|
||||||
|
@change="reRenderExportImage" style="margin-right: 10px" />
|
||||||
|
<el-input-number v-model="exportImagePaddingY" :min="10" :max="100" :step="5" controls-position="right"
|
||||||
|
@change="reRenderExportImage" style="margin-right: 10px" />
|
||||||
|
<el-button type="primary" @click="downloadExportImage">下载</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
<!-- 导出json弹窗 -->
|
||||||
|
<el-dialog v-model="exportJsonDialogVisible" title="导出为json" :width="800">
|
||||||
|
<div class="exportJsonContainer">
|
||||||
|
<div class="jsonPreviewBox" ref="jsonPreviewBox"></div>
|
||||||
|
<div class="handleBox">
|
||||||
|
<el-input v-model="exportFileName" style="width: 150px; margin-right: 10px"></el-input>
|
||||||
|
<el-button type="primary" @click="downloadExportJson">下载</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 右键菜单 -->
|
||||||
|
<Contextmenu v-if="appInstance" :app="appInstance"></Contextmenu>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { onMounted, ref, watch, toRaw, nextTick, computed, reactive, defineProps, defineEmits } from 'vue'
|
||||||
|
import TinyWhiteboard from 'whiteboard_lyc'
|
||||||
|
import ColorPicker from './components/ColorPicker.vue'
|
||||||
|
import {
|
||||||
|
Camera,
|
||||||
|
Delete,
|
||||||
|
CopyDocument,
|
||||||
|
ZoomIn,
|
||||||
|
ZoomOut,
|
||||||
|
Remove,
|
||||||
|
RefreshLeft,
|
||||||
|
RefreshRight,
|
||||||
|
Download,
|
||||||
|
Upload,
|
||||||
|
CaretTop,
|
||||||
|
CaretBottom,
|
||||||
|
Minus,
|
||||||
|
Grid,
|
||||||
|
View,
|
||||||
|
Edit,
|
||||||
|
QuestionFilled, ChromeFilled
|
||||||
|
} from '@element-plus/icons-vue'
|
||||||
|
import Contextmenu from './components/Contextmenu.vue'
|
||||||
|
import { fontFamilyList, fontSizeList } from './constants'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
type: Number,
|
||||||
|
default: 700,
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
type: Number,
|
||||||
|
default: 1000,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: [String, Object],
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
readonly: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
allowComment: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: [String, undefined],
|
||||||
|
default: undefined
|
||||||
|
},
|
||||||
|
isShowSave: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 定义要发送的emit事件
|
||||||
|
const emit = defineEmits(['handleSave', 'update:modelValue'])
|
||||||
|
|
||||||
|
// 当前操作类型
|
||||||
|
const currentType = ref('selection')
|
||||||
|
|
||||||
|
// dom节点
|
||||||
|
const box = ref(null)
|
||||||
|
|
||||||
|
// 应用实例
|
||||||
|
let app = null
|
||||||
|
const appInstance = ref(null)
|
||||||
|
// 当前激活的元素
|
||||||
|
const activeElement = ref(null)
|
||||||
|
// 当前多选的元素
|
||||||
|
const selectedElements = ref([])
|
||||||
|
const hasSelectedElements = computed(() => {
|
||||||
|
return selectedElements.value.length > 0
|
||||||
|
})
|
||||||
|
// 描边宽度
|
||||||
|
const lineWidth = ref('small')
|
||||||
|
// 字体
|
||||||
|
const fontFamily = ref('微软雅黑, Microsoft YaHei')
|
||||||
|
// 字号
|
||||||
|
const fontSize = ref(18)
|
||||||
|
// 边框样式
|
||||||
|
const lineDash = ref(0)
|
||||||
|
// 透明度
|
||||||
|
const globalAlpha = ref(1)
|
||||||
|
// 角度
|
||||||
|
const rotate = ref(0)
|
||||||
|
// 当前缩放
|
||||||
|
const currentZoom = ref(100)
|
||||||
|
// 缩放允许前进后退
|
||||||
|
const canUndo = ref(false)
|
||||||
|
const canRedo = ref(false)
|
||||||
|
// 图片导出弹窗
|
||||||
|
const exportImageDialogVisible = ref(false)
|
||||||
|
const exportImageUrl = ref('')
|
||||||
|
const exportOnlySelected = ref(false)
|
||||||
|
const exportRenderBackground = ref(true)
|
||||||
|
const exportFileName = ref('未命名')
|
||||||
|
const exportImagePaddingX = ref(10)
|
||||||
|
const exportImagePaddingY = ref(10)
|
||||||
|
// json导出弹窗
|
||||||
|
const exportJsonDialogVisible = ref(false)
|
||||||
|
const exportJsonData = ref('')
|
||||||
|
const tree = ref(null)
|
||||||
|
const jsonPreviewBox = ref(null)
|
||||||
|
// 背景颜色
|
||||||
|
const backgroundColor = ref('')
|
||||||
|
// 当前滚动距离
|
||||||
|
const scroll = reactive({
|
||||||
|
x: 0,
|
||||||
|
y: 0
|
||||||
|
})
|
||||||
|
// 切换显示网格
|
||||||
|
const showGrid = ref(false)
|
||||||
|
// 模式切换
|
||||||
|
// const readonly = ref(false)
|
||||||
|
|
||||||
|
// 设置单个元素是否只读
|
||||||
|
const elReadonly = ref(false)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 通知app更当前类型
|
||||||
|
watch(currentType, () => {
|
||||||
|
elReadonly.value = false
|
||||||
|
app.updateCurrentType(currentType.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 元素角度变化
|
||||||
|
const onElementRotateChange = elementRotate => {
|
||||||
|
rotate.value = elementRotate
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改元素角度
|
||||||
|
const onRotateChange = rotate => {
|
||||||
|
app.updateActiveElementRotate(rotate)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 数字输入框聚焦事件
|
||||||
|
const onInputNumberFocus = () => {
|
||||||
|
// 解绑快捷键按键事件,防止冲突
|
||||||
|
app.keyCommand.unBindEvent()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 数字输入框失焦事件
|
||||||
|
const onInputNumberBlur = () => {
|
||||||
|
// 重新绑定快捷键按键事件
|
||||||
|
app.keyCommand.bindEvent()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新样式
|
||||||
|
const updateStyle = (key, value) => {
|
||||||
|
app.setCurrentElementsStyle({
|
||||||
|
[key]: value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 选择工具
|
||||||
|
const handleToolTypeChange = (key) => {
|
||||||
|
currentType.value = key;
|
||||||
|
app.cancelActiveElement()
|
||||||
|
}
|
||||||
|
// 类型变化
|
||||||
|
const onCurrentTypeChange = () => {
|
||||||
|
// 清除激活项
|
||||||
|
app.cancelActiveElement()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除元素
|
||||||
|
const deleteElement = () => {
|
||||||
|
app.deleteCurrentElements()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复制元素
|
||||||
|
const copyElement = () => {
|
||||||
|
app.copyPasteCurrentElements()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 放大
|
||||||
|
const zoomIn = () => {
|
||||||
|
app.zoomIn()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缩小
|
||||||
|
const zoomOut = () => {
|
||||||
|
app.zoomOut()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 恢复初始缩放
|
||||||
|
const resetZoom = () => {
|
||||||
|
app.setZoom(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 橡皮擦
|
||||||
|
const toggleEraser = () => {
|
||||||
|
currentType.value = currentType.value === 'eraser' ? 'selection' : 'eraser'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 回退
|
||||||
|
const undo = () => {
|
||||||
|
app.undo()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 前进
|
||||||
|
const redo = () => {
|
||||||
|
app.redo()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空
|
||||||
|
const empty = () => {
|
||||||
|
app.empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 回到中心
|
||||||
|
const backToCenter = () => {
|
||||||
|
|
||||||
|
app.scrollToCenter()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示全部
|
||||||
|
const showFit = () => {
|
||||||
|
let elementList = app.elements.elementList
|
||||||
|
let { maxx, maxy } = TinyWhiteboard.utils.getMultiElementRectInfo(elementList)
|
||||||
|
if (maxx >= app.width || maxy >= app.height) {
|
||||||
|
app.fit()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
backToCenter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导入
|
||||||
|
const importFromJson = () => {
|
||||||
|
let el = document.createElement('input')
|
||||||
|
el.type = 'file'
|
||||||
|
el.accept = 'application/json'
|
||||||
|
el.addEventListener('input', () => {
|
||||||
|
let reader = new FileReader()
|
||||||
|
reader.onload = () => {
|
||||||
|
el.value = null
|
||||||
|
if (reader.result) {
|
||||||
|
app.setData(JSON.parse(reader.result))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reader.readAsText(el.files[0])
|
||||||
|
})
|
||||||
|
el.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出
|
||||||
|
const handleExportCommand = type => {
|
||||||
|
if (type === 'png') {
|
||||||
|
exportImageUrl.value = app.exportImage({
|
||||||
|
renderBg: exportRenderBackground.value,
|
||||||
|
paddingX: exportImagePaddingX.value,
|
||||||
|
paddingY: exportImagePaddingY.value,
|
||||||
|
onlySelected: exportOnlySelected.value
|
||||||
|
})
|
||||||
|
exportImageDialogVisible.value = true
|
||||||
|
} else if (type === 'json') {
|
||||||
|
exportJsonData.value = app.exportJson()
|
||||||
|
exportJsonDialogVisible.value = true
|
||||||
|
nextTick(() => {
|
||||||
|
if (!tree.value) {
|
||||||
|
tree.value = jsonTree.create(exportJsonData.value, jsonPreviewBox.value)
|
||||||
|
} else {
|
||||||
|
tree.value.loadData(exportJsonData.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新生成导出图片
|
||||||
|
const reRenderExportImage = () => {
|
||||||
|
exportImageUrl.value = app.exportImage({
|
||||||
|
renderBg: exportRenderBackground.value,
|
||||||
|
paddingX: exportImagePaddingX.value,
|
||||||
|
paddingY: exportImagePaddingY.value,
|
||||||
|
onlySelected: exportOnlySelected.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下载导出的图片
|
||||||
|
const downloadExportImage = () => {
|
||||||
|
TinyWhiteboard.utils.downloadFile(
|
||||||
|
exportImageUrl.value,
|
||||||
|
exportFileName.value + '.png'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下载导出的json
|
||||||
|
const downloadExportJson = () => {
|
||||||
|
let str = JSON.stringify(exportJsonData.value, null, 4)
|
||||||
|
let blob = new Blob([str])
|
||||||
|
TinyWhiteboard.utils.downloadFile(
|
||||||
|
URL.createObjectURL(blob),
|
||||||
|
exportFileName.value + '.json'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新背景颜色
|
||||||
|
const setBackgroundColor = value => {
|
||||||
|
app.setBackgroundColor(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 滚动至中心
|
||||||
|
const scrollToCenter = () => {
|
||||||
|
app.scrollToCenter()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换显示网格
|
||||||
|
const toggleGrid = () => {
|
||||||
|
if (showGrid.value) {
|
||||||
|
showGrid.value = false
|
||||||
|
app.hideGrid()
|
||||||
|
} else {
|
||||||
|
showGrid.value = true
|
||||||
|
app.showGrid()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const elementModeChange = () => {
|
||||||
|
elReadonly.value = !elReadonly.value
|
||||||
|
updateStyle('elReadonly', elReadonly.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模式切换
|
||||||
|
const toggleMode = () => {
|
||||||
|
if (readonly.value) {
|
||||||
|
readonly.value = false
|
||||||
|
app.setEditMode()
|
||||||
|
} else {
|
||||||
|
readonly.value = true
|
||||||
|
app.setReadonlyMode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭弹窗
|
||||||
|
const closeBoard = () => {
|
||||||
|
emit('update:modelValue', false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSave = async () => {
|
||||||
|
|
||||||
|
let dataJson = app.exportJson()
|
||||||
|
// 给每一个元素添加elReadonly 属性 学生端则不可操作这些元素
|
||||||
|
dataJson.elements.map(item => {
|
||||||
|
item.style.elReadonly = true
|
||||||
|
})
|
||||||
|
let base64 = await app.exportImage({
|
||||||
|
type: 'image/jpeg',
|
||||||
|
renderBg: exportRenderBackground.value,
|
||||||
|
paddingX: 0,
|
||||||
|
paddingY: 0,
|
||||||
|
onlySelected: exportOnlySelected.value,
|
||||||
|
backgroundColor: '#ffffff'
|
||||||
|
})
|
||||||
|
emit('handleSave', {
|
||||||
|
json: dataJson,
|
||||||
|
base64
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const setCanvasData = (storeData) => {
|
||||||
|
storeData = JSON.parse(storeData)
|
||||||
|
;[['backgroundColor', ''], ['strokeStyle', '#000000'], ['fontFamily', '微软雅黑, Microsoft YaHei'], ['dragStrokeStyle', '#666'], ['fillStyle', 'transparent'], ['fontSize', 18]].forEach((item) => {
|
||||||
|
if (storeData.state[item[0]] === undefined) {
|
||||||
|
storeData.state[item[0]] = item[1]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
currentZoom.value = parseInt(storeData.state.scale * 100)
|
||||||
|
scroll.x = parseInt(storeData.state.scrollX)
|
||||||
|
scroll.y = parseInt(storeData.state.scrollY)
|
||||||
|
showGrid.value = storeData.state.showGrid
|
||||||
|
readonly.value = storeData.state.readonly
|
||||||
|
app.setData(storeData)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const getCanvasJson = () => {
|
||||||
|
let canvasJson = app.exportJson()
|
||||||
|
// 给每一个元素添加elReadonly 属性 学生端则不可操作这些元素
|
||||||
|
canvasJson.elements.forEach(item => {
|
||||||
|
item.style.elReadonly = true
|
||||||
|
})
|
||||||
|
return canvasJson
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCanvasBase64 = async () =>{
|
||||||
|
let base64 = await app.exportImage({
|
||||||
|
type: 'image/jpeg',
|
||||||
|
renderBg: exportRenderBackground.value,
|
||||||
|
paddingX: 0,
|
||||||
|
paddingY: 0,
|
||||||
|
onlySelected: exportOnlySelected.value,
|
||||||
|
backgroundColor: '#ffffff'
|
||||||
|
})
|
||||||
|
return base64
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.data, (newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
setCanvasData(newVal)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
empty()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// dom元素挂载完成
|
||||||
|
onMounted(() => {
|
||||||
|
// 创建实例
|
||||||
|
app = new TinyWhiteboard({
|
||||||
|
container: box.value,
|
||||||
|
drawType: currentType.value,
|
||||||
|
state: {
|
||||||
|
// backgroundColor: '#121212',
|
||||||
|
// strokeStyle: '#fff',
|
||||||
|
// fontFamily: '楷体, 楷体_GB2312, SimKai, STKaiti',
|
||||||
|
// dragStrokeStyle: '#999'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
let storeData = localStorage.getItem('TINY_WHITEBOARD_DATA')
|
||||||
|
if (props.data) {
|
||||||
|
setCanvasData(props.data)
|
||||||
|
}
|
||||||
|
// 监听app内部修改类型事件
|
||||||
|
app.on('currentTypeChange', type => {
|
||||||
|
currentType.value = type
|
||||||
|
})
|
||||||
|
// 监听元素激活事件
|
||||||
|
app.on('activeElementChange', element => {
|
||||||
|
if (activeElement.value) {
|
||||||
|
activeElement.value.off('elementRotateChange', onElementRotateChange)
|
||||||
|
}
|
||||||
|
activeElement.value = element
|
||||||
|
if (element) {
|
||||||
|
let { style, rotate: elementRotate } = element
|
||||||
|
lineWidth.value = style.lineWidth
|
||||||
|
fontFamily.value = style.fontFamily
|
||||||
|
fontSize.value = style.fontSize
|
||||||
|
lineDash.value = style.lineDash
|
||||||
|
globalAlpha.value = style.globalAlpha
|
||||||
|
rotate.value = elementRotate
|
||||||
|
element.on('elementRotateChange', onElementRotateChange)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// 元素多选变化
|
||||||
|
app.on('multiSelectChange', elements => {
|
||||||
|
selectedElements.value = elements
|
||||||
|
})
|
||||||
|
// 缩放变化
|
||||||
|
app.on('zoomChange', scale => {
|
||||||
|
currentZoom.value = parseInt(scale * 100)
|
||||||
|
})
|
||||||
|
// 监听前进后退事件
|
||||||
|
app.on('shuttle', (index, length) => {
|
||||||
|
canUndo.value = index > 0
|
||||||
|
canRedo.value = index < length - 1
|
||||||
|
})
|
||||||
|
// 监听数据变化
|
||||||
|
app.on('change', data => {
|
||||||
|
showGrid.value = data.state.showGrid
|
||||||
|
// localStorage.setItem('TINY_WHITEBOARD_DATA', JSON.stringify(data))
|
||||||
|
})
|
||||||
|
// 监听滚动变化
|
||||||
|
app.on('scrollChange', (x, y) => {
|
||||||
|
scroll.y = parseInt(y)
|
||||||
|
scroll.x = parseInt(x)
|
||||||
|
})
|
||||||
|
appInstance.value = app
|
||||||
|
// 窗口尺寸变化
|
||||||
|
let resizeTimer = null
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
clearTimeout(resizeTimer)
|
||||||
|
resizeTimer = setTimeout(() => {
|
||||||
|
app.resize()
|
||||||
|
}, 300)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 暴露方法
|
||||||
|
defineExpose({
|
||||||
|
backToCenter,
|
||||||
|
resetZoom,
|
||||||
|
showFit,
|
||||||
|
getCanvasJson,
|
||||||
|
getCanvasBase64,
|
||||||
|
setCanvasData
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
ul,
|
||||||
|
ol {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-enter-active,
|
||||||
|
.v-leave-active {
|
||||||
|
transition: all 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-enter-from,
|
||||||
|
.v-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-300px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.whiteboart-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
|
||||||
|
.canvasBox {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
position: absolute;
|
||||||
|
left: 20px;
|
||||||
|
top: 50px;
|
||||||
|
width: 250px;
|
||||||
|
background-color: #fff;
|
||||||
|
|
||||||
|
.elementStyle {
|
||||||
|
padding: 10px;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25);
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footerLeft {
|
||||||
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
padding: 0 10px;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.left {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.blockBox {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 5px;
|
||||||
|
|
||||||
|
.zoom {
|
||||||
|
width: 40px;
|
||||||
|
margin: 0 10px;
|
||||||
|
user-select: none;
|
||||||
|
color: #606266;
|
||||||
|
cursor: pointer;
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 0 5px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.exportImageContainer {
|
||||||
|
.imagePreviewBox {
|
||||||
|
height: 400px;
|
||||||
|
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAMUlEQVQ4T2NkYGAQYcAP3uCTZhw1gGGYhAGBZIA/nYDCgBDAm9BGDWAAJyRCgLaBCAAgXwixzAS0pgAAAABJRU5ErkJggg==') 0;
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: scale-down;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.handleBox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 50px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.exportJsonContainer {
|
||||||
|
.jsonPreviewBox {
|
||||||
|
height: 400px;
|
||||||
|
overflow: auto;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #000;
|
||||||
|
|
||||||
|
/deep/ .jsontree_tree {
|
||||||
|
font-family: 'Trebuchet MS', Arial, sans-serif !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.handleBox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 50px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.helpDialogContent {
|
||||||
|
height: 500px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -15,6 +15,7 @@ import App from './App.vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
import log from 'electron-log/renderer' // 渲染进程日志-文件记录
|
import log from 'electron-log/renderer' // 渲染进程日志-文件记录
|
||||||
import customComponent from '@/components/common' // 自定义组件
|
import customComponent from '@/components/common' // 自定义组件
|
||||||
|
import plugins from './plugins' // plugins插件
|
||||||
|
|
||||||
if(process.env.NODE_ENV != 'development') { // 非开发环境,将日志打印到日志文件
|
if(process.env.NODE_ENV != 'development') { // 非开发环境,将日志打印到日志文件
|
||||||
Object.assign(console, log.functions) // 渲染进程日志-控制台替换
|
Object.assign(console, log.functions) // 渲染进程日志-控制台替换
|
||||||
|
@ -40,4 +41,5 @@ app.use(router)
|
||||||
.use(store)
|
.use(store)
|
||||||
.use(ElementPlus, { locale: zhLocale })
|
.use(ElementPlus, { locale: zhLocale })
|
||||||
.use(customComponent) // 自定义组件
|
.use(customComponent) // 自定义组件
|
||||||
|
.use(plugins)
|
||||||
.mount('#app')
|
.mount('#app')
|
|
@ -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
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -82,9 +82,9 @@ export const constantRoutes = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/classTaskAssign',
|
path: '/classTaskAssign',
|
||||||
component: () => import('@/views/classTaskAssign/index.vue'),
|
component: () => import('@/views/classTask/classTaskAssign.vue'),
|
||||||
name: 'classTaskAssign',
|
name: 'classTaskAssign',
|
||||||
meta: {title: '作业设计'},
|
meta: {title: '作业布置'},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/classTask',
|
path: '/classTask',
|
||||||
|
|
|
@ -142,8 +142,9 @@ const getClassWorkList = () => {
|
||||||
edusubject: userStore.edusubject,//学科
|
edusubject: userStore.edusubject,//学科
|
||||||
deaddate: tabActive.value === '进行中'? getTomorrow() : EndDate.value,// 进行中:明天,已结束:选择的日期
|
deaddate: tabActive.value === '进行中'? getTomorrow() : EndDate.value,// 进行中:明天,已结束:选择的日期
|
||||||
status: '1', // 作业状态:1-已发布
|
status: '1', // 作业状态:1-已发布
|
||||||
orderby: 'concat(deaddate,uniquekey) DESC',
|
// orderby: 'concat(deaddate,uniquekey) DESC',
|
||||||
pageSize: 100
|
orderby: 'uniquekey DESC',
|
||||||
|
pageSize: 100,
|
||||||
}).then((response) => {
|
}).then((response) => {
|
||||||
for (var i = 0; i < response.rows.length; i++) {
|
for (var i = 0; i < response.rows.length; i++) {
|
||||||
// 初始化部分新增字段值
|
// 初始化部分新增字段值
|
||||||
|
@ -325,7 +326,7 @@ const escapeHtmlQuotes = (str) => {
|
||||||
// 后端已replace双引号, 故前端不用在处理
|
// 后端已replace双引号, 故前端不用在处理
|
||||||
const regex1 = /\\+/g; // 匹配多个反斜杠
|
const regex1 = /\\+/g; // 匹配多个反斜杠
|
||||||
let result = str.replace(regex1, '\\');
|
let result = str.replace(regex1, '\\');
|
||||||
|
result = str.replace(/(?<!\\)\n/g, '<br />'); //替换\n而不替换\\n 为 \\n
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
const pollingST = ref(null) //轮询定时器标识
|
const pollingST = ref(null) //轮询定时器标识
|
||||||
|
@ -373,7 +374,8 @@ const getStudentVisible = async () => {
|
||||||
edusubject: userStore.edusubject,//学科
|
edusubject: userStore.edusubject,//学科
|
||||||
deaddate: tabActive.value === '进行中'? getTomorrow() : EndDate.value,// 进行中:明天,已结束:选择的日期
|
deaddate: tabActive.value === '进行中'? getTomorrow() : EndDate.value,// 进行中:明天,已结束:选择的日期
|
||||||
status: '1', // 作业状态:1-已发布
|
status: '1', // 作业状态:1-已发布
|
||||||
orderby: 'concat(deaddate,uniquekey) DESC',
|
// orderby: 'concat(deaddate,uniquekey) DESC',
|
||||||
|
orderby: 'uniquekey DESC',
|
||||||
pageSize: 100
|
pageSize: 100
|
||||||
})
|
})
|
||||||
const curWorkList = response.rows
|
const curWorkList = response.rows
|
||||||
|
|
|
@ -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:entpcoursework里的id 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,就是灰色
|
||||||
|
//所有的taskconfig的id都不是0,绿色
|
||||||
|
//只要有一个taskconfig的id是0,就是红色
|
||||||
|
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>
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<el-form ref="classWorkFormScoreRef" :model="classWorkFormScore">
|
<el-form ref="classWorkFormScoreRef" :model="classWorkFormScore">
|
||||||
<!-- <div class="teacher_content" :style="{ height: dialogProps.maxheight + 'px' }"> -->
|
<!-- <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">
|
<div style="font-size: 18px; width: 100%; padding: 5px 10px" class="sticky">
|
||||||
{{ classWorkFormScore.name }} 答题详情
|
{{ classWorkFormScore.name }} 答题详情
|
||||||
</div>
|
</div>
|
||||||
|
@ -88,8 +88,11 @@
|
||||||
<el-col :span="6" style="padding: 10px">
|
<el-col :span="6" style="padding: 10px">
|
||||||
<!-- <span>学生答案:{{ stuItem.feedcontent }}</span> -->
|
<!-- <span>学生答案:{{ stuItem.feedcontent }}</span> -->
|
||||||
<span>学生答案:
|
<span>学生答案:
|
||||||
<span v-if="stuItem.feedcontent !=''" style="background-color: red; color: white; padding: 0 5px; border-radius: 5px;">
|
<span
|
||||||
{{ formatFeedContent(stuItem, quItem) }}
|
v-if="stuItem.feedcontent !=''"
|
||||||
|
style="background-color: red; color: white; padding: 0 5px; border-radius: 5px;"
|
||||||
|
v-html="formatFeedContent(stuItem, quItem)"
|
||||||
|
>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
@ -385,7 +388,7 @@
|
||||||
v-model="fileReadopen"
|
v-model="fileReadopen"
|
||||||
title="文件预览"
|
title="文件预览"
|
||||||
width="80%"
|
width="80%"
|
||||||
:style="{ height: '75vh' }"
|
:style="{ height: '72vh' }"
|
||||||
append-to-body
|
append-to-body
|
||||||
>
|
>
|
||||||
<div class="file-read-dialog">
|
<div class="file-read-dialog">
|
||||||
|
|
|
@ -58,7 +58,7 @@
|
||||||
<!-- 如果当前学习没有试题 :height="mainHeight"-->
|
<!-- 如果当前学习没有试题 :height="mainHeight"-->
|
||||||
<div
|
<div
|
||||||
v-if="classWorkAnalysis.view == 'studentview'"
|
v-if="classWorkAnalysis.view == 'studentview'"
|
||||||
style="width: 100%; height:75vh; "
|
style="width: 100%; height:73vh; "
|
||||||
class="clwk_dialog_view"
|
class="clwk_dialog_view"
|
||||||
>
|
>
|
||||||
<div class="view_table">
|
<div class="view_table">
|
||||||
|
@ -443,6 +443,9 @@ const getStudentClassWorkDataDetail = (row) => {
|
||||||
// <el-table-column label="参考答案" prop="rightanswer"
|
// <el-table-column label="参考答案" prop="rightanswer"
|
||||||
//新增了 复合题、主观题(背景+小题目) 题目标题优化一下
|
//新增了 复合题、主观题(背景+小题目) 题目标题优化一下
|
||||||
wevalres.rows[w].worktitle = wevalres.rows[w].worktitle.replace(/!@#\$%/g, '')
|
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
|
classWorkAnalysis.activeStudentQuizlist = wevalres.rows
|
||||||
|
@ -523,6 +526,7 @@ const escapeHtmlQuotes = (str) => {
|
||||||
// 后端已replace双引号, 故前端不用在处理
|
// 后端已replace双引号, 故前端不用在处理
|
||||||
const regex1 = /\\+/g; // 匹配多个反斜杠
|
const regex1 = /\\+/g; // 匹配多个反斜杠
|
||||||
let result = str.replace(regex1, '\\');
|
let result = str.replace(regex1, '\\');
|
||||||
|
result = str.replace(/(?<!\\)\n/g, '<br />'); //替换\n而不替换\\n 为 \\n
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
@ -654,19 +658,19 @@ defineExpose({
|
||||||
.clwk_dialog_view {
|
.clwk_dialog_view {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-around;
|
justify-content: flex-start;
|
||||||
// align-items: center;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.view_table {
|
.view_table {
|
||||||
flex: 1;
|
flex: 0 0 auto;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-y: auto;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.view_teachrting {
|
.view_teachrting {
|
||||||
flex: 2;
|
flex: 1;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-y: auto;
|
// overflow-y: auto;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,23 +1,23 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="page-resource flex">
|
<div class="page-newcalsetask flex">
|
||||||
<el-menu
|
<el-menu
|
||||||
default-active="1"
|
default-active="1"
|
||||||
class="el-menu-vertical-demo"
|
class="el-menu-vertical-demo"
|
||||||
:collapse="isCollapse"
|
:collapse="isCollapse"
|
||||||
>
|
>
|
||||||
<!--左侧 教材 目录-->
|
<!--左侧 教材 目录-->
|
||||||
<div v-if="!isCollapse">
|
<div v-if="!isCollapse" style="height: 100%;overflow: hidden;">
|
||||||
<ChooseTextbook @change-book="getData" @node-click="getData" />
|
<ChooseTextbook @change-book="getData" @node-click="getData" />
|
||||||
</div>
|
</div>
|
||||||
</el-menu>
|
</el-menu>
|
||||||
|
|
||||||
<div class="page-right" :style="{'margin-left': isCollapse ? '0' : '20px'}">
|
<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;">
|
<el-col :span="12" style="padding-left: 20px; text-align: left;">
|
||||||
<div class="unit-top-left" @click="isCollapse = !isCollapse">
|
<div class="unit-top-left" @click="isCollapse = !isCollapse">
|
||||||
<i v-if="!isCollapse" class="iconfont icon-xiangzuo"></i>
|
<i v-if="!isCollapse" class="iconfont icon-xiangzuo"></i>
|
||||||
<span>作业范围</span>
|
<span>课程目录</span>
|
||||||
<i v-if="isCollapse" class="iconfont icon-xiangyou"></i>
|
<i v-if="isCollapse" class="iconfont icon-xiangyou"></i>
|
||||||
</div>
|
</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
@ -30,33 +30,19 @@
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
<!-- 作业类型:内容 -->
|
<!-- 作业类型:内容 -->
|
||||||
<task-type-view :bookobj="courseObj" />
|
<task-type-view :bookobj="courseObj" :uniquekey="classWorkForm.uniquekey" style="flex: 1; overflow: hidden;"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, ref, toRaw,watch, reactive } from 'vue'
|
import { onMounted, ref, toRaw,watch, reactive } from 'vue'
|
||||||
// import useResoureStore from './store'
|
|
||||||
import ChooseTextbook from '@/components/choose-textbook/index.vue'
|
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 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 { getCurrentTime } from '@/utils/date'
|
||||||
import useUserStore from '@/store/modules/user'
|
import useUserStore from '@/store/modules/user'
|
||||||
const userStore = useUserStore().user
|
const userStore = useUserStore().user
|
||||||
|
|
||||||
// const sourceStore = useResoureStore()
|
|
||||||
const isDialogOpen = ref(false)
|
|
||||||
const toolStore = useToolState()
|
|
||||||
const openDialog = () => {
|
|
||||||
isDialogOpen.value = true
|
|
||||||
}
|
|
||||||
// ---------------------------------------------------
|
// ---------------------------------------------------
|
||||||
const classWorkForm = reactive({
|
const classWorkForm = reactive({
|
||||||
// uniquekey: userStore.edusubject+'-' + getCurrentTime('MMDD')+'-'+(this.taskList.length+1),
|
// uniquekey: userStore.edusubject+'-' + getCurrentTime('MMDD')+'-'+(this.taskList.length+1),
|
||||||
|
@ -72,6 +58,7 @@ const courseObj = reactive({
|
||||||
levelFirstId: '',
|
levelFirstId: '',
|
||||||
levelSecondId: '',
|
levelSecondId: '',
|
||||||
coursetitle:'',
|
coursetitle:'',
|
||||||
|
node: null, // 选择的课程节点
|
||||||
//
|
//
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -95,6 +82,7 @@ const getData = (data) => {
|
||||||
courseObj.levelFirstId = levelFirstId // 单元
|
courseObj.levelFirstId = levelFirstId // 单元
|
||||||
courseObj.levelSecondId = levelSecondId // 章节
|
courseObj.levelSecondId = levelSecondId // 章节
|
||||||
courseObj.coursetitle = node.itemtitle // (单元/章节) 名称
|
courseObj.coursetitle = node.itemtitle // (单元/章节) 名称
|
||||||
|
courseObj.node = node; // 保存当前节点
|
||||||
|
|
||||||
// 头部 教材分析打开外部链接需要当前章节ID
|
// 头部 教材分析打开外部链接需要当前章节ID
|
||||||
localStorage.setItem('unitId', JSON.stringify({ levelFirstId, levelSecondId}))
|
localStorage.setItem('unitId', JSON.stringify({ levelFirstId, levelSecondId}))
|
||||||
|
@ -116,9 +104,10 @@ const init = () => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.page-resource {
|
.page-newcalsetask {
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
.el-menu--collapse {
|
.el-menu--collapse {
|
||||||
width: 0px;
|
width: 0px;
|
||||||
|
|
|
@ -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>
|
|
||||||
<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;
|
|
||||||
|
|
||||||
//打开作业url增加unitId 章节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>
|
|
|
@ -105,6 +105,10 @@ const menuList = [{
|
||||||
icon: 'icon-jiaoxuefansi',
|
icon: 'icon-jiaoxuefansi',
|
||||||
isOuter: true,
|
isOuter: true,
|
||||||
path: '/teaching/classtaskassign?titleName=作业布置&openDialog=newClassTask',
|
path: '/teaching/classtaskassign?titleName=作业布置&openDialog=newClassTask',
|
||||||
|
// path: '/newClassTask'
|
||||||
|
//path: '/classTaskAssign'
|
||||||
|
//isOuter: true,
|
||||||
|
//path: '/teaching/classtaskassign?titleName=作业布置&&openDialog=newClassTask'
|
||||||
id: '2-1'
|
id: '2-1'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -113,6 +117,7 @@ const menuList = [{
|
||||||
isOuter: true,
|
isOuter: true,
|
||||||
path: '/teaching/classtaskassign?titleName=作业布置',
|
path: '/teaching/classtaskassign?titleName=作业布置',
|
||||||
id: '2-2'
|
id: '2-2'
|
||||||
|
// path: '/classTaskAssign'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '作业批改',
|
name: '作业批改',
|
||||||
|
|
|
@ -207,7 +207,6 @@ export default {
|
||||||
},
|
},
|
||||||
openFileWin(items) {
|
openFileWin(items) {
|
||||||
if (items.fileFlag === 'apt') {
|
if (items.fileFlag === 'apt') {
|
||||||
console.log(this.curNode);
|
|
||||||
let curBook = JSON.parse(localStorage.getItem('curBook'))
|
let curBook = JSON.parse(localStorage.getItem('curBook'))
|
||||||
const path="/teaching/aptindex?id="+items.fileId + "&unitId=" + this.curNode.id + "&bookId=" + curBook.id;
|
const path="/teaching/aptindex?id="+items.fileId + "&unitId=" + this.curNode.id + "&bookId=" + curBook.id;
|
||||||
let configObj = outLink().getBaseData()
|
let configObj = outLink().getBaseData()
|
||||||
|
|
Loading…
Reference in New Issue