qinqing_dev #268
|
@ -1,5 +1,5 @@
|
|||
# 页面标题
|
||||
VITE_APP_TITLE = 文枢课堂
|
||||
VITE_APP_TITLE = AIX智慧课堂
|
||||
|
||||
# 生产环境配置
|
||||
VITE_APP_ENV = 'production'
|
|
@ -1,5 +1,5 @@
|
|||
# 页面标题
|
||||
VITE_APP_TITLE = AIX智慧课堂
|
||||
VITE_APP_TITLE = 文枢课堂
|
||||
|
||||
# 生产环境配置
|
||||
VITE_APP_ENV = 'production'
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
appId: com.electron.app
|
||||
productName: 文枢课堂
|
||||
productName: AIx
|
||||
directories:
|
||||
output: dist
|
||||
buildResources: build
|
||||
win:
|
||||
executableName: 文枢课堂
|
||||
executableName: AIx
|
||||
icon: resources/logo2.ico
|
||||
files:
|
||||
- '!**/.vscode/*'
|
|
@ -1,10 +1,10 @@
|
|||
appId: com.electron.app
|
||||
productName: AIx
|
||||
productName: 文枢课堂
|
||||
directories:
|
||||
output: dist
|
||||
buildResources: build
|
||||
win:
|
||||
executableName: AIx
|
||||
executableName: 文枢课堂
|
||||
icon: resources/logo2.ico
|
||||
files:
|
||||
- '!**/.vscode/*'
|
||||
|
|
17
package.json
17
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "aix-win",
|
||||
"version": "2.0.6",
|
||||
"version": "2.1.1",
|
||||
"description": "",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "example.com",
|
||||
|
@ -26,6 +26,14 @@
|
|||
"@electron/remote": "^2.1.2",
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"@vitejs/plugin-vue-jsx": "^4.0.0",
|
||||
"@antv/x6": "^2.18.1",
|
||||
"@antv/x6-plugin-clipboard": "^2.1.6",
|
||||
"@antv/x6-plugin-dnd": "^2.1.1",
|
||||
"@antv/x6-plugin-export": "^2.1.6",
|
||||
"@antv/x6-plugin-keyboard": "^2.2.3",
|
||||
"@antv/x6-plugin-selection": "^2.2.2",
|
||||
"@antv/x6-plugin-snapline": "^2.1.7",
|
||||
"@antv/x6-plugin-transform": "^2.1.8",
|
||||
"@vue-office/docx": "^1.6.2",
|
||||
"@vue-office/excel": "^1.7.11",
|
||||
"@vue-office/pdf": "^2.0.2",
|
||||
|
@ -39,7 +47,7 @@
|
|||
"electron-store": "8.0.0",
|
||||
"electron-updater": "^6.1.7",
|
||||
"element-china-area-data": "^6.1.0",
|
||||
"element-plus": "^2.7.6",
|
||||
"element-plus": "^2.8.0",
|
||||
"fabric": "^5.3.0",
|
||||
"im_electron_sdk": "^8.0.5904",
|
||||
"js-cookie": "^3.0.5",
|
||||
|
@ -55,7 +63,10 @@
|
|||
"vue-qr": "^4.0.9",
|
||||
"vue-router": "^4.4.0",
|
||||
"xgplayer": "^3.0.19",
|
||||
"xlsx": "^0.18.5"
|
||||
"xlsx": "^0.18.5",
|
||||
"less": "^4.2.0",
|
||||
"less-loader": "^7.3.0",
|
||||
"whiteboard_lyc": "^0.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-toolkit/eslint-config": "^1.0.2",
|
||||
|
|
|
@ -273,9 +273,14 @@ function handleAll() {
|
|||
})
|
||||
// 用于监听-状态管理变化-同步所有窗口
|
||||
ipcMain.handle('pinia-state-change', (e, storeName, jsonStr) => {
|
||||
console.log('pinia-state-change-1', storeName, jsonStr)
|
||||
|
||||
for(const curWin of BrowserWindow.getAllWindows()){
|
||||
const id = curWin.webContents.id
|
||||
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) { // 除了消息发送窗口和销毁的窗口 其他都发送
|
||||
curWin.webContents.send('pinia-state-set', storeName, jsonStr)
|
||||
}
|
||||
|
|
|
@ -62,3 +62,108 @@ export function updateClassworkdata(data) {
|
|||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 修改classwork
|
||||
export function updateClasswork(data) {
|
||||
return request({
|
||||
url: '/education/classwork',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 查询evaluationclue列表
|
||||
export function listEvaluationclue(query) {
|
||||
return request({
|
||||
url: '/education/evaluationclue/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 查询evaluationclue详细
|
||||
export function getEvaluationclue(id) {
|
||||
return request({
|
||||
url: '/education/evaluationclue/' + id,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 新增evaluationclue
|
||||
export function addEvaluationclueReturnId(data) {
|
||||
return request({
|
||||
url: '/education/evaluationclue/addReturnId',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 新增evaluationclue
|
||||
export function addEvaluationclue(data) {
|
||||
return request({
|
||||
url: '/education/evaluationclue',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 修改evaluationclue
|
||||
export function updateEvaluationclue(data) {
|
||||
return request({
|
||||
url: '/education/evaluationclue',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除evaluationclue
|
||||
export function delEvaluationclue(id) {
|
||||
return request({
|
||||
url: '/education/evaluationclue/' + id,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
// 新增evaluationclue,保存base64图片
|
||||
export function saveBase64File(data) {
|
||||
return request({
|
||||
url: '/education/evaluationclue/saveBase64File',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 新增evaluationclue,上传
|
||||
export function saveEvaluationClueUploadFile(data) {
|
||||
return request({
|
||||
url: '/education/evaluationclue/saveUploadFile',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 读取文件内容
|
||||
export function readFile(data) {
|
||||
return fetch(import.meta.env.VITE_APP_RES_FILE_PATH + data.cluelink, {
|
||||
method: "get",
|
||||
headers: {
|
||||
'Content-Type': 'text/plain', // 请求头设置为纯文本
|
||||
'Accept': 'text/plain' // 接受头设置为纯文本
|
||||
},
|
||||
})
|
||||
.then(response => response.text())
|
||||
.then(text => {
|
||||
return Promise.resolve(text);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('读取文件出错:', error);
|
||||
return Promise.reject();
|
||||
});
|
||||
/*return request({
|
||||
url: '/education/evaluationclue/readFile',
|
||||
method: 'post',
|
||||
data: data
|
||||
})*/
|
||||
}
|
||||
|
|
|
@ -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>
|
Before Width: | Height: | Size: 867 B 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 log from 'electron-log/renderer' // 渲染进程日志-文件记录
|
||||
import customComponent from '@/components/common' // 自定义组件
|
||||
import plugins from './plugins' // plugins插件
|
||||
|
||||
if(process.env.NODE_ENV != 'development') { // 非开发环境,将日志打印到日志文件
|
||||
Object.assign(console, log.functions) // 渲染进程日志-控制台替换
|
||||
|
@ -40,4 +41,5 @@ app.use(router)
|
|||
.use(store)
|
||||
.use(ElementPlus, { locale: zhLocale })
|
||||
.use(customComponent) // 自定义组件
|
||||
.use(plugins)
|
||||
.mount('#app')
|
|
@ -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',
|
||||
component: () => import('@/views/classTaskAssign/index.vue'),
|
||||
component: () => import('@/views/classTask/classTaskAssign.vue'),
|
||||
name: 'classTaskAssign',
|
||||
meta: {title: '作业设计'},
|
||||
meta: {title: '作业布置'},
|
||||
},
|
||||
{
|
||||
path: '/classTask',
|
||||
|
|
|
@ -142,8 +142,9 @@ const getClassWorkList = () => {
|
|||
edusubject: userStore.edusubject,//学科
|
||||
deaddate: tabActive.value === '进行中'? getTomorrow() : EndDate.value,// 进行中:明天,已结束:选择的日期
|
||||
status: '1', // 作业状态:1-已发布
|
||||
orderby: 'concat(deaddate,uniquekey) DESC',
|
||||
pageSize: 100
|
||||
// orderby: 'concat(deaddate,uniquekey) DESC',
|
||||
orderby: 'uniquekey DESC',
|
||||
pageSize: 100,
|
||||
}).then((response) => {
|
||||
for (var i = 0; i < response.rows.length; i++) {
|
||||
// 初始化部分新增字段值
|
||||
|
@ -325,7 +326,7 @@ const escapeHtmlQuotes = (str) => {
|
|||
// 后端已replace双引号, 故前端不用在处理
|
||||
const regex1 = /\\+/g; // 匹配多个反斜杠
|
||||
let result = str.replace(regex1, '\\');
|
||||
|
||||
result = str.replace(/(?<!\\)\n/g, '<br />'); //替换\n而不替换\\n 为 \\n
|
||||
return result;
|
||||
}
|
||||
const pollingST = ref(null) //轮询定时器标识
|
||||
|
@ -373,7 +374,8 @@ const getStudentVisible = async () => {
|
|||
edusubject: userStore.edusubject,//学科
|
||||
deaddate: tabActive.value === '进行中'? getTomorrow() : EndDate.value,// 进行中:明天,已结束:选择的日期
|
||||
status: '1', // 作业状态:1-已发布
|
||||
orderby: 'concat(deaddate,uniquekey) DESC',
|
||||
// orderby: 'concat(deaddate,uniquekey) DESC',
|
||||
orderby: 'uniquekey DESC',
|
||||
pageSize: 100
|
||||
})
|
||||
const curWorkList = response.rows
|
||||
|
|
|
@ -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>
|
||||
<el-form ref="classWorkFormScoreRef" :model="classWorkFormScore">
|
||||
<!-- <div class="teacher_content" :style="{ height: dialogProps.maxheight + 'px' }"> -->
|
||||
<div class="teacher_content" :style="{ height: '75vh' }">
|
||||
<div class="teacher_content" :style="{ height: '72vh' }">
|
||||
<div style="font-size: 18px; width: 100%; padding: 5px 10px" class="sticky">
|
||||
{{ classWorkFormScore.name }} 答题详情
|
||||
</div>
|
||||
|
@ -88,8 +88,11 @@
|
|||
<el-col :span="6" style="padding: 10px">
|
||||
<!-- <span>学生答案:{{ stuItem.feedcontent }}</span> -->
|
||||
<span>学生答案:
|
||||
<span v-if="stuItem.feedcontent !=''" style="background-color: red; color: white; padding: 0 5px; border-radius: 5px;">
|
||||
{{ formatFeedContent(stuItem, quItem) }}
|
||||
<span
|
||||
v-if="stuItem.feedcontent !=''"
|
||||
style="background-color: red; color: white; padding: 0 5px; border-radius: 5px;"
|
||||
v-html="formatFeedContent(stuItem, quItem)"
|
||||
>
|
||||
</span>
|
||||
</span>
|
||||
</el-col>
|
||||
|
@ -385,7 +388,7 @@
|
|||
v-model="fileReadopen"
|
||||
title="文件预览"
|
||||
width="80%"
|
||||
:style="{ height: '75vh' }"
|
||||
:style="{ height: '72vh' }"
|
||||
append-to-body
|
||||
>
|
||||
<div class="file-read-dialog">
|
||||
|
|
|
@ -58,7 +58,7 @@
|
|||
<!-- 如果当前学习没有试题 :height="mainHeight"-->
|
||||
<div
|
||||
v-if="classWorkAnalysis.view == 'studentview'"
|
||||
style="width: 100%; height:75vh; "
|
||||
style="width: 100%; height:73vh; "
|
||||
class="clwk_dialog_view"
|
||||
>
|
||||
<div class="view_table">
|
||||
|
@ -443,6 +443,9 @@ const getStudentClassWorkDataDetail = (row) => {
|
|||
// <el-table-column label="参考答案" prop="rightanswer"
|
||||
//新增了 复合题、主观题(背景+小题目) 题目标题优化一下
|
||||
wevalres.rows[w].worktitle = wevalres.rows[w].worktitle.replace(/!@#\$%/g, '')
|
||||
|
||||
// 将feedcontent中的\r替换为<br />
|
||||
wevalres.rows[w].feedcontent = wevalres.rows[w].feedcontent.replace(/(?<!\\)\n/g, '<br />'); //替换\n而不替换\\n 为 \\n
|
||||
}
|
||||
}
|
||||
classWorkAnalysis.activeStudentQuizlist = wevalres.rows
|
||||
|
@ -523,6 +526,7 @@ const escapeHtmlQuotes = (str) => {
|
|||
// 后端已replace双引号, 故前端不用在处理
|
||||
const regex1 = /\\+/g; // 匹配多个反斜杠
|
||||
let result = str.replace(regex1, '\\');
|
||||
result = str.replace(/(?<!\\)\n/g, '<br />'); //替换\n而不替换\\n 为 \\n
|
||||
|
||||
return result;
|
||||
}
|
||||
|
@ -654,19 +658,19 @@ defineExpose({
|
|||
.clwk_dialog_view {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
// align-items: center;
|
||||
justify-content: flex-start;
|
||||
overflow: hidden;
|
||||
}
|
||||
.view_table {
|
||||
flex: 1;
|
||||
flex: 0 0 auto;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
.view_teachrting {
|
||||
flex: 2;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
// overflow-y: auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,23 +1,23 @@
|
|||
<template>
|
||||
<div class="page-resource flex">
|
||||
<div class="page-newcalsetask flex">
|
||||
<el-menu
|
||||
default-active="1"
|
||||
class="el-menu-vertical-demo"
|
||||
:collapse="isCollapse"
|
||||
>
|
||||
<!--左侧 教材 目录-->
|
||||
<div v-if="!isCollapse">
|
||||
<div v-if="!isCollapse" style="height: 100%;overflow: hidden;">
|
||||
<ChooseTextbook @change-book="getData" @node-click="getData" />
|
||||
</div>
|
||||
</el-menu>
|
||||
|
||||
<div class="page-right" :style="{'margin-left': isCollapse ? '0' : '20px'}">
|
||||
<!-- 标题 -->
|
||||
<el-row style="align-items: center; margin-bottom: 0px">
|
||||
<el-row style="align-items: center; margin-bottom: 0px; flex: 0 0 auto">
|
||||
<el-col :span="12" style="padding-left: 20px; text-align: left;">
|
||||
<div class="unit-top-left" @click="isCollapse = !isCollapse">
|
||||
<i v-if="!isCollapse" class="iconfont icon-xiangzuo"></i>
|
||||
<span>作业范围</span>
|
||||
<span>课程目录</span>
|
||||
<i v-if="isCollapse" class="iconfont icon-xiangyou"></i>
|
||||
</div>
|
||||
</el-col>
|
||||
|
@ -30,33 +30,19 @@
|
|||
</el-col>
|
||||
</el-row>
|
||||
<!-- 作业类型:内容 -->
|
||||
<task-type-view :bookobj="courseObj" />
|
||||
<task-type-view :bookobj="courseObj" :uniquekey="classWorkForm.uniquekey" style="flex: 1; overflow: hidden;"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref, toRaw,watch, reactive } from 'vue'
|
||||
// import useResoureStore from './store'
|
||||
import ChooseTextbook from '@/components/choose-textbook/index.vue'
|
||||
import Third from '@/components/choose-textbook/third.vue'
|
||||
// import ResoureSearch from './container/resoure-search.vue'
|
||||
// import ResoureList from './container/resoure-list.vue'
|
||||
// import ThirdList from './container/third-list.vue'
|
||||
import TaskTypeView from '@/views/classTask/container/newTask/taskTypeView.vue'
|
||||
import uploadDialog from '@/components/upload-dialog/index.vue'
|
||||
// import { createWindow } from '@/utils/tool'
|
||||
import { useToolState } from '@/store/modules/tool'
|
||||
import { getCurrentTime } from '@/utils/date'
|
||||
import useUserStore from '@/store/modules/user'
|
||||
const userStore = useUserStore().user
|
||||
|
||||
// const sourceStore = useResoureStore()
|
||||
const isDialogOpen = ref(false)
|
||||
const toolStore = useToolState()
|
||||
const openDialog = () => {
|
||||
isDialogOpen.value = true
|
||||
}
|
||||
// ---------------------------------------------------
|
||||
const classWorkForm = reactive({
|
||||
// uniquekey: userStore.edusubject+'-' + getCurrentTime('MMDD')+'-'+(this.taskList.length+1),
|
||||
|
@ -72,6 +58,7 @@ const courseObj = reactive({
|
|||
levelFirstId: '',
|
||||
levelSecondId: '',
|
||||
coursetitle:'',
|
||||
node: null, // 选择的课程节点
|
||||
//
|
||||
})
|
||||
|
||||
|
@ -95,6 +82,7 @@ const getData = (data) => {
|
|||
courseObj.levelFirstId = levelFirstId // 单元
|
||||
courseObj.levelSecondId = levelSecondId // 章节
|
||||
courseObj.coursetitle = node.itemtitle // (单元/章节) 名称
|
||||
courseObj.node = node; // 保存当前节点
|
||||
|
||||
// 头部 教材分析打开外部链接需要当前章节ID
|
||||
localStorage.setItem('unitId', JSON.stringify({ levelFirstId, levelSecondId}))
|
||||
|
@ -116,9 +104,10 @@ const init = () => {
|
|||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-resource {
|
||||
.page-newcalsetask {
|
||||
padding-top: 10px;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
.el-menu--collapse {
|
||||
width: 0px;
|
||||
|
|
|
@ -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',
|
||||
isOuter: true,
|
||||
path: '/teaching/classtaskassign?titleName=作业布置&openDialog=newClassTask',
|
||||
// path: '/newClassTask'
|
||||
//path: '/classTaskAssign'
|
||||
//isOuter: true,
|
||||
//path: '/teaching/classtaskassign?titleName=作业布置&&openDialog=newClassTask'
|
||||
id: '2-1'
|
||||
},
|
||||
{
|
||||
|
@ -113,6 +117,7 @@ const menuList = [{
|
|||
isOuter: true,
|
||||
path: '/teaching/classtaskassign?titleName=作业布置',
|
||||
id: '2-2'
|
||||
// path: '/classTaskAssign'
|
||||
},
|
||||
{
|
||||
name: '作业批改',
|
||||
|
|
|
@ -207,7 +207,6 @@ export default {
|
|||
},
|
||||
openFileWin(items) {
|
||||
if (items.fileFlag === 'apt') {
|
||||
console.log(this.curNode);
|
||||
let curBook = JSON.parse(localStorage.getItem('curBook'))
|
||||
const path="/teaching/aptindex?id="+items.fileId + "&unitId=" + this.curNode.id + "&bookId=" + curBook.id;
|
||||
let configObj = outLink().getBaseData()
|
||||
|
|
Loading…
Reference in New Issue