baigl #261

Merged
baigl merged 14 commits from baigl into main 2024-09-24 16:34:39 +08:00
8 changed files with 1545 additions and 100 deletions
Showing only changes of commit 54a24ed077 - Show all commits

View File

@ -26,6 +26,14 @@
"@electron/remote": "^2.1.2", "@electron/remote": "^2.1.2",
"@element-plus/icons-vue": "^2.3.1", "@element-plus/icons-vue": "^2.3.1",
"@vitejs/plugin-vue-jsx": "^4.0.0", "@vitejs/plugin-vue-jsx": "^4.0.0",
"@antv/x6": "^2.18.1",
"@antv/x6-plugin-clipboard": "^2.1.6",
"@antv/x6-plugin-dnd": "^2.1.1",
"@antv/x6-plugin-export": "^2.1.6",
"@antv/x6-plugin-keyboard": "^2.2.3",
"@antv/x6-plugin-selection": "^2.2.2",
"@antv/x6-plugin-snapline": "^2.1.7",
"@antv/x6-plugin-transform": "^2.1.8",
"@vue-office/docx": "^1.6.2", "@vue-office/docx": "^1.6.2",
"@vue-office/excel": "^1.7.11", "@vue-office/excel": "^1.7.11",
"@vue-office/pdf": "^2.0.2", "@vue-office/pdf": "^2.0.2",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,6 +19,7 @@
<el-row class="middle"> <el-row class="middle">
<el-col :span="24" style="height: 100%; overflow: hidden;"> <el-col :span="24" style="height: 100%; overflow: hidden;">
<el-form-item label="作业资源:" class="el-form-work-list"> <el-form-item label="作业资源:" class="el-form-work-list">
<!-- 左侧作业资源 -->
<el-col :span="15" class="work-left"> <el-col :span="15" class="work-left">
<!-- 习题训练 --> <!-- 习题训练 -->
<div v-if="classWorkForm.worktype=='习题训练'" style="height: 100%; display: flex; flex-direction: column;"> <div v-if="classWorkForm.worktype=='习题训练'" style="height: 100%; display: flex; flex-direction: column;">
@ -114,7 +115,8 @@
@pagination="getPaginationList" /> @pagination="getPaginationList" />
</div> --> </div> -->
</div> </div>
<!-- <div v-if="classWorkForm.worktype!='习题训练'"> <!-- 非习题训练常规作业 -->
<div v-if="classWorkForm.worktype!='习题训练'">
<div :style="{ 'overflow': 'auto'}"> <div :style="{ 'overflow': 'auto'}">
<template v-if="classWorkForm.worktype!='常规作业'"> <template v-if="classWorkForm.worktype!='常规作业'">
<template v-for="(item, index) in workResource.teachResourceList" :key="item"> <template v-for="(item, index) in workResource.teachResourceList" :key="item">
@ -132,19 +134,19 @@
</div> </div>
</div> </div>
<div style="display: flex;align-items: center; justify-content: space-around; margin-left: 15px; margin-right: 15px;"> <div style="display: flex;align-items: center; justify-content: space-around; margin-left: 15px; margin-right: 15px;">
<el-button @click="prevRead(item)" icon="Search">预览</el-button> <el-button icon="Search" @click="prevRead(item)">预览</el-button>
<el-button @click="handleClassWorkAddOfResource(item)" icon="FolderAdd">添加到作业</el-button> <el-button icon="FolderAdd" @click="handleClassWorkAddOfResource(item)">添加到作业</el-button>
</div> </div>
</div> </div>
</template> </template>
</template> </template>
<template v-if="classWorkForm.worktype =='常规作业'"> <!-- <template v-if="classWorkForm.worktype =='常规作业'">
<div class="upload-homework" v-loading="fileLoading"> <div class="upload-homework" v-loading="fileLoading">
<FileUpload v-model="fileHomeworkList" :fileSize="800" :fileType="['mp3','mp4','doc','docx','xlsx','xls','pdf','ppt','pptx','jpg','jpeg','gif','png','txt']"/> <FileUpload v-model="fileHomeworkList" :fileSize="800" :fileType="['mp3','mp4','doc','docx','xlsx','xls','pdf','ppt','pptx','jpg','jpeg','gif','png','txt']"/>
</div> </div>
</template> </template> -->
</div>
</div> </div>
</div> -->
<!-- <div v-if="classWorkForm.activeIndex==3"> <!-- <div v-if="classWorkForm.activeIndex==3">
<el-row> <el-row>
<el-col :span="20"> <el-col :span="20">
@ -196,6 +198,8 @@
</div> </div>
</div> --> </div> -->
</el-col> </el-col>
<!-- 右侧选中的作业资源 -->
<el-col :span="9" class="work-right"> <el-col :span="9" class="work-right">
<div v-if="classWorkForm.worktype=='习题训练'" :style="{height: '100%', 'overflow': 'auto', 'border':'1px dotted blue','border-radius':'5px', 'background-color': '#f7f7f7'}"> <div v-if="classWorkForm.worktype=='习题训练'" :style="{height: '100%', 'overflow': 'auto', 'border':'1px dotted blue','border-radius':'5px', 'background-color': '#f7f7f7'}">
<template v-for="(item,index) in classWorkForm.quizlist" :key="item.id"> <template v-for="(item,index) in classWorkForm.quizlist" :key="item.id">
@ -214,18 +218,20 @@
<div v-if="classWorkForm.worktype!='习题训练'" :style="{'overflow': 'auto', 'border':'1px dotted blue','border-radius':'5px', 'background-color': '#f7f7f7'}"> <div v-if="classWorkForm.worktype!='习题训练'" :style="{'overflow': 'auto', 'border':'1px dotted blue','border-radius':'5px', 'background-color': '#f7f7f7'}">
<div style="margin: 5px; background-color: white"> <div style="margin: 5px; background-color: white">
<template v-for="(item,index) in chooseWorkLists" :key="item.id"> <template v-for="(item) in chooseWorkLists" :key="item.id">
<div v-if="item.worktype==classWorkForm.worktype"> <div v-if="item.worktype==classWorkForm.worktype">
<div style="margin-bottom: 5px; padding-left: 15px;display: flex;flex-direction: row;align-items: center;"> <div class="choose-work">
<div style="font-size: 1.2em; font-weight: bold;margin-right: 5px">{{ item.worktype }}</div> <div class="choose-work-title">{{ item.worktype }}</div>
<div style="display: flex;align-items: center; justify-content: space-around; margin-left: 15px; margin-right: 15px;flex: 1;"> <div class="choose-work-content">
<div style="color: silver; display: flex;align-items: center;flex: 1;"> <div style="color: silver; display: flex;align-items: center;flex: 1;">
<el-form-item label="分值"> <el-form-item label="分值">
<el-input-number v-model="item.score" :min="1" :max="100" size="small"></el-input-number > <el-input-number v-model="item.score" :min="1" :max="100" size="small"></el-input-number >
</el-form-item> </el-form-item>
<div style="display: flex;align-items: center;flex: 1;justify-content: flex-end;">
<el-button @click="prevRead(item)">预览</el-button>
<el-button type="danger" @click="deleteClassWorkAddOfResource(item)">删除</el-button>
</div>
</div> </div>
<el-button @click="prevRead(item)" icon="Search">预览</el-button>
<el-button @click="deleteClassWorkAddOfResource(item)" type="danger" icon="Delete">删除</el-button>
</div> </div>
</div> </div>
</div> </div>
@ -252,6 +258,26 @@
</el-row> </el-row>
</div> </div>
</el-form> </el-form>
<!-- 预览框 -->
<el-dialog v-if="prevReadMsg.visible" v-model="prevReadMsg.visible" class="prev-read-zy-wrap" width="90%" style="height: 80vh" append-to-body>
<!-- <div v-if="prevReadMsg.type=='课标研读'" style="height: 100%;">
<standard book-type="课标研读" :show-cata="true" :show-tools="false" :course-obj="courseObj" :bookdatahtml="versionObj.bookdata" :teachResObj="activeTeachResOfStandard"></standard>
</div>
<div v-if="prevReadMsg.type=='目标设定'" style="height: 100%;display: flex;">
<degreeevolution :courseObj="courseObj" :show-class="true" :teachResObj="activeTeachResOfStandard" :attainmentList="attainmentList" :courseQualityList="courseQualityList"/>
</div>
<div v-if="prevReadMsg.type=='教材研读'" style="height: 100%;">
<standard book-type="教材研读" :course-obj="courseObj" :show-tools="false" :bookdatahtml="versionObj.bookdata" :teachResObj="activeTeachResOfStandard"></standard>
</div> -->
<div v-if="prevReadMsg.type=='框架梳理'" style="height: 100%;">
<FlowChart ref="flowref" :flowHeight="mainHeight" :dataSource="flowData"/>
</div>
<!-- <div v-if="prevReadMsg.type=='学科定位'" style="height: 100%;">
<teachJsMind :course-obj="courseObj" :teachResObj="activeTeachResOfStandard"></teachJsMind>
</div> -->
<!-- <div v-if="prevReadMsg.type=='习题训练'">习题训练</div> -->
</el-dialog>
</div> </div>
</template> </template>
@ -259,12 +285,17 @@
import { onMounted, ref, nextTick, watch, reactive, getCurrentInstance } from 'vue' import { onMounted, ref, nextTick, watch, reactive, getCurrentInstance } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { useToolState } from '@/store/modules/tool' import { useToolState } from '@/store/modules/tool'
import { getCurrentTime } from '@/utils/date'
import { processList } from '@/hooks/useProcessList'
import {listEntpcoursework, listEntpcourseworkNew, getEntpcoursework} from '@/api/education/entpCourseWork' import {listEntpcoursework, listEntpcourseworkNew, getEntpcoursework} from '@/api/education/entpCourseWork'
import { updateClasswork } from '@/api/classTask'
import { useGetHomework } from '@/hooks/useGetHomework'
import { addClassworkReturnId } from '@/api/teaching/classwork' import { addClassworkReturnId } from '@/api/teaching/classwork'
import { updateClasswork, listEvaluationclue,readFile } from '@/api/classTask'
import { listEvaluation } from '@/api/subject'
import { listEntpcoursefile } from '@/api/education/entpcoursefile'
import { useGetHomework } from '@/hooks/useGetHomework'
import { processList } from '@/hooks/useProcessList'
import { getCurrentTime } from '@/utils/date'
import FlowChart from "@/components/Flowchart/index.vue";
import useUserStore from '@/store/modules/user' import useUserStore from '@/store/modules/user'
@ -364,9 +395,16 @@ let classWorkForm = reactive({
title: '',// title: '',//
quizlist: [], // quizlist: [], //
}); // }); //
const chooseWorkLists = ref([]); // ? list const chooseWorkLists = ref([]); //
const whiteboardObj = ref(''); // - const whiteboardObj = ref(''); // -
//
const boardLoading = ref(false);
const question = ref(''); //
const prevReadMsg = reactive({
visible: false,
type: ""
});// msg
const flowData = ref({})//
/*** /***
* 作业类型切换 * 作业类型切换
@ -403,7 +441,7 @@ const queryForm = reactive({
/** /**
* @desc: 新查询试题 * @desc: 1习题训练 - 新查询试题
* @return: {*} * @return: {*}
* @param {*} queryType * @param {*} queryType
* 0 - 标准查询 * 0 - 标准查询
@ -451,6 +489,41 @@ const handleQueryFromEntpCourseWork= (queryType) => {
}) })
} }
//
/**
* 2框架设计教学资源从课标分析教材分析里来
*/
const getQueryFromEvaluationclue = () => {
// props.bookobj.levelSecondId, //userStore.evalid, // // ID
listEvaluationclue({ cluegroup: 'teachresource', evalid: props.bookobj.levelSecondId, pageSize: 1000 }).then((clueres) => {
for (var i=0; i<clueres.rows.length; i++) {
if (clueres.rows[i].cluetag == 'standardview') {
clueres.rows[i].worktype = '课标研读';
} else if (clueres.rows[i].cluetag == 'targetview') {
clueres.rows[i].worktype = '目标设定';
} else if (clueres.rows[i].cluetag == 'contentview') {
clueres.rows[i].worktype = '教材研读';
} else if (clueres.rows[i].cluetag == 'frameview') {
clueres.rows[i].worktype = '框架梳理';
} else if (clueres.rows[i].cluetag == 'mapview') {
clueres.rows[i].worktype = '学科定位';
}
if (clueres.rows[i].childlist != '') {
clueres.rows[i].childArray = JSON.parse('['+clueres.rows[i].childlist+']');
for (var j=0; j<clueres.rows[i].childArray.length; j++) {
clueres.rows[i].childArray[j].title = clueres.rows[i].childArray[j].title.replace(/(<([^>]+)>)/ig, '');
}
} else {
clueres.rows[i].childArray = {};
}
}
console.log("框架梳理、课标研读、目标设定、教材研读、学科定位的资源",clueres.rows);
workResource.teachResourceList = clueres.rows;
})
}
/** /**
* 添加资源 * 添加资源
* @param fromsrc - 试题来源 * @param fromsrc - 试题来源
@ -481,6 +554,103 @@ const handleClassWorkQuizAdd = (fromsrc, entpcourseworkid) => {
ElMessage('试题已经存在') ElMessage('试题已经存在')
} }
}; };
/**
* 预览资源
*/
const prevRead = async (item) => {
prevReadMsg.visible = true;
prevReadMsg.type = item.worktype;
if (item.worktype==='课标研读'){
//
listEvaluation({itemkey: 'subject', edusubject: userStore.edusubject, edustage: userStore.edustage}).then(res => {
// TODO -
console.log("课标研读-还未接入",res);
// this.versionObj = res.rows[0];
// //
// if (this.versionObj.fileurl.length > 0) {
// readFile({cluelink: this.versionObj.fileurl}).then(fileres => {
// this.versionObj.bookdata = fileres;
// this.activeTeachResOfStandard = item;
// })
// }
})
}
if (item.worktype==='目标设定'){
// TODO -
// this.activeTeachResOfStandard = item;
}
if (item.worktype==='教材研读'){
// TODO -
// getEvaluation(this.courseObj.evalrootid).then(bookres => {
// this.versionObj = bookres.data;
// if (this.versionObj.fileurl.length > 0) {
// readFile({cluelink: this.versionObj.fileurl}).then(fileres => {
// this.versionObj.bookdata = fileres;
// this.activeTeachResOfStandard = item;
// })
// }
// })
}
if (item.worktype==='框架梳理'){
flowData.value = {};
const { chapterId } = await useGetHomework(props.bookobj.node)
// this.entpcourseid = chapterId
let queryParams = {
entpcourseid: chapterId,
ppttype: '教材分析',
parentid: item.id,
title: '逻辑框架建构',
filetype: 'draw'
}
listEntpcoursefile(queryParams).then(response=>{
if (response.rows.length == 0) {
return;
}
flowData.value = JSON.parse(response.rows[0].datacontent)
})
}
if (item.worktype==='学科定位'){
// TODO -
// this.activeTeachResOfStandard = item;
}
};
/**
* 添加到作业
*/
//
const handleClassWorkAddOfResource = (work) => {
//
let arrSole = chooseWorkLists.value.filter((item) => {
return item.worktype === classWorkForm.worktype
})
//
if(arrSole.length > 0) {
ElMessage('该资源已存在,请勿重复添加')
return;
}
//
let arr = chooseWorkLists.value.filter((item) => {
return item.id === work.id
})
if (arr.length===0) {
work.score = 1;
chooseWorkLists.value.push(work);
}
};
/**
* 删除作业
*/
const deleteClassWorkAddOfResource = (work) => {
chooseWorkLists.value.filter((item, index) => {
if (item.id === work.id) {
chooseWorkLists.value.splice(index, 1);
return;
}
})
}
/** /**
* 作业设计-提交 * 作业设计-提交
@ -656,47 +826,66 @@ const handleClassWorkSave = async () => {
const { chapterId } = await useGetHomework(props.bookobj.node) const { chapterId } = await useGetHomework(props.bookobj.node)
// this.entpcourseid = chapterId // this.entpcourseid = chapterId
const cform = {
id: 0,
workdate: classWorkForm.workdate, // //web
deaddate: '', //
entpid: userStore.deptId, //
level: 1,
parentid: 0,
worktype: classWorkForm.worktype, //
workkey: '',
worktag: '',
uniquekey: classWorkForm.uniquekey,//
classid: 0,
classcourseid: 0,
entpcourseid: chapterId, //
slideid: 0,
title: classWorkForm.title, //
workcodes: JSON.stringify(classWorkForm.workcodes), //
edusubject: userStore.edusubject, //
evalid: props.bookobj.levelSecondId, //userStore.evalid, // // ID
edustage: userStore.edustage, // ,,
status: '10', //2024-09-11
edituserid: userStore.userId, // id
entpcourseworklist: '', // list
};
if (classWorkForm.worktype === "课堂展示") { if (classWorkForm.worktype === "课堂展示") {
this.boardLoading = true boardLoading.value = true
let canvasJson = this.$refs.boardref.getCanvasJson() let canvasJson = this.$refs.boardref.getCanvasJson()
let canvasBase64 = await this.$refs.boardref.getCanvasBase64() let canvasBase64 = await this.$refs.boardref.getCanvasBase64()
var formObj = {}; //
formObj.id = 0; formObj.worktag = question.value;
formObj.workdate = this.classWorkForm.workdate;
formObj.deaddate = '';
formObj.entpid = this.userStore.deptId;
formObj.level = 1;
formObj.parentid = 0;
formObj.worktype = this.classWorkForm.worktype;
formObj.workkey = '';
formObj.worktag = this.question;
formObj.uniquekey = this.classWorkForm.uniquekey;
formObj.classid = 0;
formObj.classcourseid = 0;
formObj.entpcourseid = chapterId;//
formObj.slideid = 0;
formObj.title = this.classWorkForm.title;
formObj.workcodes = JSON.stringify({json: canvasJson, base64: canvasBase64}); formObj.workcodes = JSON.stringify({json: canvasJson, base64: canvasBase64});
formObj.edusubject = this.courseObj.edusubject;
formObj.evalid = this.courseObj.evalid;
formObj.edustage = this.userStore.edustage;
formObj.status = '10'; //2024-09-11
formObj.edituserid = this.userStore.id;
formObj.entpcourseworklist = JSON.stringify([{'id':-1, 'score': '10'}]); formObj.entpcourseworklist = JSON.stringify([{'id':-1, 'score': '10'}]);
try { try {
addClassworkReturnId(formObj).then(() => { addClassworkReturnId(formObj).then(() => {
this.classWorkForm.worktype = "习题训练"; ElMessage({ type: 'success', message: '作业设计成功!'});
this.chooseWorkLists = []; //
this.whiteboardObj = ''; classWorkForm.worktype = "习题训练";
classWorkForm.uniquekey = props.uniquekey, //
classWorkForm.title = "";
classWorkForm.quizlist = [], //
//
chooseWorkLists.value = [];
whiteboardObj.value = ''; // ? //
// refresh the list // refresh the list
// //
// this.getClassWorkAllList(); // this.getClassWorkAllList();
this.newWorkSpace = false;
this.newWorkSpaceEdit = false; // TODO
this.workEdit = false;
//TODO 3
// this.newWorkSpace = false;
// this.newWorkSpaceEdit = false;
// this.workEdit = false;
boardLoading.value = false
}) })
} finally { } finally {
this.boardLoading = false boardLoading.value = false
} }
} }
else if(classWorkForm.worktype === "常规作业"){ else if(classWorkForm.worktype === "常规作业"){
@ -750,47 +939,22 @@ const handleClassWorkSave = async () => {
// //
ll.push({'id': classWorkForm.quizlist[i].id, 'score': classWorkForm.quizlist[i].score}); ll.push({'id': classWorkForm.quizlist[i].id, 'score': classWorkForm.quizlist[i].score});
} }
}else { }else if( classWorkForm.worktype === "框架梳理") {
// TODO ------------ chooseWorkLists.value.filter((item) => {
// this.chooseWorkLists.filter((item, index) => { if (item.worktype === classWorkForm.worktype) {
// if (item.worktype === this.classWorkForm.worktype) { ll.push({'id':item.id, 'score': item.score});
// ll.push({'id':item.id, 'score': item.score});
// }
// })
} }
console.log(userStore,'userStoreuserStoreuserStore') })
const cform = { }
id: 0, // list
workdate: classWorkForm.workdate, // //web
deaddate: '', //
entpid: userStore.deptId, //
level: 1,
parentid: 0,
worktype: classWorkForm.worktype, //
workkey: '',
worktag: '',
uniquekey: classWorkForm.uniquekey,//
classid: 0,
classcourseid: 0,
entpcourseid: chapterId, //
slideid: 0,
title: classWorkForm.title, //
workcodes: JSON.stringify(classWorkForm.workcodes), //
edusubject: userStore.edusubject, //
evalid: props.bookobj.levelSecondId, //userStore.evalid, // // ID
edustage: userStore.edustage, // ,,
status: '10', //2024-09-11
edituserid: userStore.userId, // id
};
if (ll.length > 0) { if (ll.length > 0) {
cform.entpcourseworklist = JSON.stringify(ll); cform.entpcourseworklist = JSON.stringify(ll);
} else { } else {
cform.entpcourseworklist = ''; cform.entpcourseworklist = '';
} }
if(cform.entpcourseworklist == '') return ElMessage({ type: 'warning', message: '请先添加作业资源!'});
console.log(cform,'提交的数据'); console.log(cform,'提交的数据');
if(cform.entpcourseworklist == '') return ElMessage({ type: 'warning', message: '请先添加作业资源!'});
addClassworkReturnId(cform).then(workres => { addClassworkReturnId(cform).then(workres => {
ElMessage({ type: 'success', message: '作业设计成功!'}); ElMessage({ type: 'success', message: '作业设计成功!'});
// //
@ -827,7 +991,10 @@ watch(() => props.bookobj.levelSecondId, (newVal) => {
console.log(props.bookobj,'课程选择') console.log(props.bookobj,'课程选择')
queryForm.eid = props.bookobj.levelSecondId, queryForm.eid = props.bookobj.levelSecondId,
queryForm.sectionName = props.bookobj.coursetitle, queryForm.sectionName = props.bookobj.coursetitle,
//
handleQueryFromEntpCourseWork(0); handleQueryFromEntpCourseWork(0);
//
getQueryFromEvaluationclue();
}) })
</script> </script>
@ -879,7 +1046,31 @@ watch(() => props.bookobj.levelSecondId, (newVal) => {
line-height: 26px; line-height: 26px;
overflow: hidden; overflow: hidden;
} }
}
.choose-work{
height: 90px;
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 5px;
.choose-work-title{
font-size: 1.2em;
font-weight: bold;
margin-right: 5px
}
.choose-work-content{
width: 100%;
display: flex;
justify-content: space-around;
flex-direction: column;
flex: 1;
:deep(.el-form-item__label){
width: 50px !important;
}
}
} }