form generator
This commit is contained in:
177
src/components/Editor/index.vue
Normal file
177
src/components/Editor/index.vue
Normal file
@ -0,0 +1,177 @@
|
||||
<template>
|
||||
<div
|
||||
:style="`border: 1px solid #ccc; width: ${width}px`"
|
||||
:class="{
|
||||
disabled: readOnly,
|
||||
}"
|
||||
>
|
||||
<Toolbar
|
||||
style="border-bottom: 1px solid #ccc"
|
||||
:editor="editorRef"
|
||||
:defaultConfig="toolbarConfig"
|
||||
:mode="mode"
|
||||
/>
|
||||
<Editor
|
||||
:style="`min-height: ${minHeight}px; height: ${height}px;
|
||||
overflow-y: hidden`"
|
||||
v-model="valueHtml"
|
||||
:defaultConfig="editorConfig"
|
||||
:mode="mode"
|
||||
@onCreated="handleCreated"
|
||||
@onChange="handleChange"
|
||||
@onBlur="emitBlur"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import "@wangeditor/editor/dist/css/style.css"; // 引入 css
|
||||
import { getToken } from "@/utils/auth";
|
||||
import {
|
||||
onBeforeUnmount,
|
||||
ref,
|
||||
shallowRef,
|
||||
onMounted,
|
||||
toRefs,
|
||||
nextTick,
|
||||
watch,
|
||||
computed,
|
||||
} from "vue";
|
||||
import { Editor, Toolbar } from "@wangeditor/editor-for-vue";
|
||||
const baseUrl = import.meta.env.VITE_APP_BASE_API;
|
||||
export default {
|
||||
components: { Editor, Toolbar },
|
||||
props: {
|
||||
readOnly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: "请输入内容",
|
||||
},
|
||||
minHeight: {
|
||||
type: [String, Number],
|
||||
default: 300,
|
||||
},
|
||||
height: {
|
||||
type: [String, Number],
|
||||
default: 300,
|
||||
},
|
||||
width: {
|
||||
type: [String, Number],
|
||||
default: 820,
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: "default", // or 'simple'
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
// 编辑器实例,必须用 shallowRef
|
||||
const editorRef = shallowRef();
|
||||
// editorRef.value.on("blur", () => {
|
||||
// console.log("blur");
|
||||
// });
|
||||
|
||||
// 内容 HTML
|
||||
const valueHtml = ref("");
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
valueHtml.value = val;
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
const { height } = toRefs(props);
|
||||
|
||||
const toolbarConfig = {
|
||||
excludeKeys: [],
|
||||
};
|
||||
// onBlur: (editor) => {
|
||||
// console.log("onBlur");
|
||||
// },
|
||||
const editorConfig = {
|
||||
placeholder: props.placeholder,
|
||||
readOnly: props.readOnly,
|
||||
MENU_CONF: {
|
||||
uploadImage: {
|
||||
server: `${baseUrl}/common/upload`,
|
||||
// 自定义增加 http header
|
||||
fieldName: "file",
|
||||
headers: {
|
||||
Authorization: `Bearer ${getToken()}`,
|
||||
},
|
||||
customInsert(res, insertFn) {
|
||||
// res 即服务端的返回结果
|
||||
// console.log(res);
|
||||
// 从 res 中找到 url alt href ,然后插图图片
|
||||
insertFn(res.url, null, null);
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const isEmpty = () => {
|
||||
return editorRef.value.isEmpty();
|
||||
};
|
||||
|
||||
// 组件销毁时,也及时销毁编辑器
|
||||
onBeforeUnmount(() => {
|
||||
const editor = editorRef.value;
|
||||
if (editor == null) return;
|
||||
editor.destroy();
|
||||
});
|
||||
|
||||
const handleCreated = (editor) => {
|
||||
editorRef.value = editor; // 记录 editor 实例,重要!
|
||||
};
|
||||
const handleChange = (editor) => {
|
||||
if (editor.isEmpty()) {
|
||||
context.emit("update:modelValue", "");
|
||||
} else {
|
||||
context.emit("update:modelValue", editor.getHtml());
|
||||
}
|
||||
};
|
||||
context.expose({ isEmpty });
|
||||
|
||||
const emitBlur = () => {
|
||||
context.emit("blur", editorRef.value);
|
||||
// editorRef.value.emit("blur");
|
||||
};
|
||||
|
||||
return {
|
||||
editorRef,
|
||||
valueHtml,
|
||||
mode: "default", // 或 'simple'
|
||||
toolbarConfig,
|
||||
editorConfig,
|
||||
height,
|
||||
handleCreated,
|
||||
handleChange,
|
||||
// isEmpty,
|
||||
emitBlur,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.disabled {
|
||||
background-color: #f5f7fa;
|
||||
cursor: not-allowed;
|
||||
:deep(.w-e-text-container) {
|
||||
background-color: inherit;
|
||||
// color: green;
|
||||
opacity: 0.5;
|
||||
}
|
||||
:deep(.w-e-toolbar) {
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
background-color: inherit;
|
||||
}
|
||||
}
|
||||
</style>
|
138
src/components/ProcessDesigner/index.vue
Normal file
138
src/components/ProcessDesigner/index.vue
Normal file
@ -0,0 +1,138 @@
|
||||
<template>
|
||||
<div class="process-design" :style="'display: flex; height:' + height">
|
||||
<!-- <bpmn-process-designer
|
||||
v-model="xmlString"
|
||||
v-bind="controlForm"
|
||||
keyboard
|
||||
ref="processDesigner"
|
||||
:events="[
|
||||
'element.click',
|
||||
'connection.added',
|
||||
'connection.removed',
|
||||
'connection.changed',
|
||||
]"
|
||||
@element-click="elementClick"
|
||||
@init-finished="initModeler"
|
||||
@event="handlerEvent"
|
||||
@save="onSaveProcess"
|
||||
/>
|
||||
<bmpn-process-penal
|
||||
:bpmn-modeler="modeler"
|
||||
:prefix="controlForm.prefix"
|
||||
class="process-panel"
|
||||
/> -->
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import "@/plugins/package/theme/index.scss";
|
||||
import { ref } from "vue";
|
||||
const props = defineProps({
|
||||
bpmnXml: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
designerForm: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
const { bpmnXml, designerForm } = toRefs(props);
|
||||
|
||||
const height = ref(document.documentElement.clientHeight - 94.5 + "px;");
|
||||
const xmlString = ref(bpmnXml.value);
|
||||
</script>
|
||||
<!--
|
||||
|
||||
<script>
|
||||
// import Vue from 'vue';
|
||||
import { BpmnProcessDesigner, BmpnProcessPenal } from "@/plugins/package/index";
|
||||
// 自定义元素选中时的弹出菜单(修改 默认任务 为 用户任务)
|
||||
import CustomContentPadProvider from "@/plugins/package/designer/plugins/content-pad";
|
||||
// 自定义左侧菜单(修改 默认任务 为 用户任务)
|
||||
import CustomPaletteProvider from "@/plugins/package/designer/plugins/palette";
|
||||
import vuePlugin from "@/plugins/package/highlight";
|
||||
import "highlight.js/styles/atom-one-dark-reasonable.css";
|
||||
// Vue.use(vuePlugin);
|
||||
|
||||
export default {
|
||||
name: "ProcessDesigner",
|
||||
|
||||
components: {
|
||||
BpmnProcessDesigner,
|
||||
BmpnProcessPenal,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
modeler: null,
|
||||
controlForm: {
|
||||
processId: this.designerForm.processKey || "",
|
||||
processName: this.designerForm.processName || "",
|
||||
simulation: false,
|
||||
labelEditing: false,
|
||||
labelVisible: false,
|
||||
prefix: "flowable",
|
||||
headerButtonSize: "small",
|
||||
additionalModel: [CustomContentPadProvider, CustomPaletteProvider],
|
||||
},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
elementClick(element) {
|
||||
this.element = element;
|
||||
},
|
||||
initModeler(modeler) {
|
||||
setTimeout(() => {
|
||||
this.modeler = modeler;
|
||||
}, 10);
|
||||
},
|
||||
handlerEvent(eventName, element) {},
|
||||
onSaveProcess(saveData) {
|
||||
this.$emit("save", saveData);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
-->
|
||||
|
||||
<style lang="scss">
|
||||
body {
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body,
|
||||
body * {
|
||||
/* 滚动条 */
|
||||
&::-webkit-scrollbar-track-piece {
|
||||
background-color: #fff; /*滚动条的背景颜色*/
|
||||
-webkit-border-radius: 0; /*滚动条的圆角宽度*/
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 10px; /*滚动条的宽度*/
|
||||
height: 8px; /*滚动条的高度*/
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:vertical {
|
||||
/*垂直滚动条的样式*/
|
||||
height: 50px;
|
||||
background-color: rgba(153, 153, 153, 0.5);
|
||||
-webkit-border-radius: 4px;
|
||||
outline: 2px solid #fff;
|
||||
outline-offset: -2px;
|
||||
border: 2px solid #fff;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
/*滚动条的hover样式*/
|
||||
background-color: rgba(159, 159, 159, 0.3);
|
||||
-webkit-border-radius: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
/*滚动条的hover样式*/
|
||||
background-color: rgba(159, 159, 159, 0.5);
|
||||
-webkit-border-radius: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
259
src/components/ProcessViewer/index.vue
Normal file
259
src/components/ProcessViewer/index.vue
Normal file
@ -0,0 +1,259 @@
|
||||
<template>
|
||||
<div class="process-viewer">
|
||||
<div class="process-canvas" style="height: 100%;" ref="processCanvas" v-show="!isLoading" />
|
||||
<!-- 自定义箭头样式,用于成功状态下流程连线箭头 -->
|
||||
<defs ref="customSuccessDefs">
|
||||
<marker id="sequenceflow-end-white-success" viewBox="0 0 20 20" refX="11" refY="10" markerWidth="10" markerHeight="10" orient="auto">
|
||||
<path class="success-arrow" d="M 1 5 L 11 10 L 1 15 Z" style="stroke-width: 1px; stroke-linecap: round; stroke-dasharray: 10000, 1;"></path>
|
||||
</marker>
|
||||
<marker id="conditional-flow-marker-white-success" viewBox="0 0 20 20" refX="-1" refY="10" markerWidth="10" markerHeight="10" orient="auto">
|
||||
<path class="success-conditional" d="M 0 10 L 8 6 L 16 10 L 8 14 Z" style="stroke-width: 1px; stroke-linecap: round; stroke-dasharray: 10000, 1;"></path>
|
||||
</marker>
|
||||
</defs>
|
||||
<!-- 自定义箭头样式,用于失败状态下流程连线箭头 -->
|
||||
<defs ref="customFailDefs">
|
||||
<marker id="sequenceflow-end-white-fail" viewBox="0 0 20 20" refX="11" refY="10" markerWidth="10" markerHeight="10" orient="auto">
|
||||
<path class="fail-arrow" d="M 1 5 L 11 10 L 1 15 Z" style="stroke-width: 1px; stroke-linecap: round; stroke-dasharray: 10000, 1;"></path>
|
||||
</marker>
|
||||
<marker id="conditional-flow-marker-white-fail" viewBox="0 0 20 20" refX="-1" refY="10" markerWidth="10" markerHeight="10" orient="auto">
|
||||
<path class="fail-conditional" d="M 0 10 L 8 6 L 16 10 L 8 14 Z" style="stroke-width: 1px; stroke-linecap: round; stroke-dasharray: 10000, 1;"></path>
|
||||
</marker>
|
||||
</defs>
|
||||
<!-- 已完成节点悬浮弹窗 -->
|
||||
<el-dialog class="comment-dialog" :title="dlgTitle || '审批记录'" :visible.sync="dialogVisible">
|
||||
<el-row>
|
||||
<el-table :data="taskCommentList" size="mini" border header-cell-class-name="table-header-gray">
|
||||
<el-table-column label="序号" header-align="center" align="center" type="index" width="55px" />
|
||||
<el-table-column label="候选办理" prop="candidate" width="150px" align="center"/>
|
||||
<el-table-column label="实际办理" prop="assigneeName" width="100px" align="center"/>
|
||||
<el-table-column label="处理时间" prop="createTime" width="140px" align="center"/>
|
||||
<el-table-column label="办结时间" prop="finishTime" width="140px" align="center" />
|
||||
<el-table-column label="耗时" prop="duration" width="100px" align="center"/>
|
||||
<el-table-column label="审批意见" align="center">
|
||||
<template slot-scope="scope">
|
||||
{{scope.row.commentList&&scope.row.commentList[0]?scope.row.commentList[0].fullMessage:''}}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-row>
|
||||
</el-dialog>
|
||||
<div style="position: absolute; top: 0px; left: 0px; width: 100%;">
|
||||
<el-row type="flex" justify="end">
|
||||
<el-button-group key="scale-control" size="medium">
|
||||
<el-button size="medium" type="default" :plain="true" :disabled="defaultZoom <= 0.3" icon="el-icon-zoom-out" @click="processZoomOut()" />
|
||||
<el-button size="medium" type="default" style="width: 90px;">{{ Math.floor(this.defaultZoom * 10 * 10) + "%" }}</el-button>
|
||||
<el-button size="medium" type="default" :plain="true" :disabled="defaultZoom >= 3.9" icon="el-icon-zoom-in" @click="processZoomIn()" />
|
||||
<el-button size="medium" type="default" icon="el-icon-c-scale-to-original" @click="processReZoom()" />
|
||||
<slot />
|
||||
</el-button-group>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import '@/plugins/package/theme/index.scss';
|
||||
import BpmnViewer from 'bpmn-js/lib/Viewer';
|
||||
import MoveCanvasModule from 'diagram-js/lib/navigation/movecanvas';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
xml: {
|
||||
type: String
|
||||
},
|
||||
finishedInfo: {
|
||||
type: Object
|
||||
},
|
||||
// 所有节点审批记录
|
||||
allCommentList: {
|
||||
type: Array
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
dialogVisible: false,
|
||||
dlgTitle: undefined,
|
||||
defaultZoom: 1,
|
||||
// 是否正在加载流程图
|
||||
isLoading: false,
|
||||
bpmnViewer: undefined,
|
||||
// 已完成流程元素
|
||||
processNodeInfo: undefined,
|
||||
// 当前任务id
|
||||
selectTaskId: undefined,
|
||||
// 任务节点审批记录
|
||||
taskCommentList: [],
|
||||
// 已完成任务悬浮延迟Timer
|
||||
hoverTimer: null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
xml: {
|
||||
handler(newXml) {
|
||||
this.importXML(newXml);
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
finishedInfo: {
|
||||
handler(newInfo) {
|
||||
this.setProcessStatus(newInfo);
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.$nextTick(() => {
|
||||
this.importXML(this.xml)
|
||||
this.setProcessStatus(this.finishedInfo);
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
processReZoom() {
|
||||
this.defaultZoom = 1;
|
||||
this.bpmnViewer.get('canvas').zoom('fit-viewport', 'auto');
|
||||
},
|
||||
processZoomIn(zoomStep = 0.1) {
|
||||
let newZoom = Math.floor(this.defaultZoom * 100 + zoomStep * 100) / 100;
|
||||
if (newZoom > 4) {
|
||||
throw new Error('[Process Designer Warn ]: The zoom ratio cannot be greater than 4');
|
||||
}
|
||||
this.defaultZoom = newZoom;
|
||||
this.bpmnViewer.get('canvas').zoom(this.defaultZoom);
|
||||
},
|
||||
processZoomOut(zoomStep = 0.1) {
|
||||
let newZoom = Math.floor(this.defaultZoom * 100 - zoomStep * 100) / 100;
|
||||
if (newZoom < 0.2) {
|
||||
throw new Error('[Process Designer Warn ]: The zoom ratio cannot be less than 0.2');
|
||||
}
|
||||
this.defaultZoom = newZoom;
|
||||
this.bpmnViewer.get('canvas').zoom(this.defaultZoom);
|
||||
},
|
||||
getOperationTagType(type) {
|
||||
return 'success';
|
||||
// switch (type) {
|
||||
// case this.SysFlowTaskOperationType.AGREE:
|
||||
// case this.SysFlowTaskOperationType.MULTI_AGREE:
|
||||
// return 'success';
|
||||
// case this.SysFlowTaskOperationType.REFUSE:
|
||||
// case this.SysFlowTaskOperationType.PARALLEL_REFUSE:
|
||||
// case this.SysFlowTaskOperationType.MULTI_REFUSE:
|
||||
// return 'warning';
|
||||
// case this.SysFlowTaskOperationType.STOP:
|
||||
// return 'danger'
|
||||
// default:
|
||||
// return 'primary';
|
||||
// }
|
||||
},
|
||||
// 流程图预览清空
|
||||
clearViewer() {
|
||||
if (this.$refs.processCanvas) {
|
||||
this.$refs.processCanvas.innerHTML = '';
|
||||
}
|
||||
if (this.bpmnViewer) {
|
||||
this.bpmnViewer.destroy();
|
||||
}
|
||||
this.bpmnViewer = null;
|
||||
},
|
||||
// 添加自定义箭头
|
||||
addCustomDefs() {
|
||||
const canvas = this.bpmnViewer.get('canvas');
|
||||
const svg = canvas._svg;
|
||||
const customSuccessDefs = this.$refs.customSuccessDefs;
|
||||
const customFailDefs = this.$refs.customFailDefs;
|
||||
svg.appendChild(customSuccessDefs);
|
||||
svg.appendChild(customFailDefs);
|
||||
},
|
||||
// 任务悬浮弹窗
|
||||
onSelectElement(element) {
|
||||
this.selectTaskId = undefined;
|
||||
this.dlgTitle = undefined;
|
||||
|
||||
if (this.processNodeInfo == null || this.processNodeInfo.finishedTaskSet == null) return;
|
||||
|
||||
if (element == null || this.processNodeInfo.finishedTaskSet.indexOf(element.id) === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectTaskId = element.id;
|
||||
this.dlgTitle = element.businessObject ? element.businessObject.name : undefined;
|
||||
// 计算当前悬浮任务审批记录,如果记录为空不显示弹窗
|
||||
this.taskCommentList = (this.allCommentList || []).filter(item => {
|
||||
return item.activityId === this.selectTaskId;
|
||||
});
|
||||
this.dialogVisible = true;
|
||||
},
|
||||
// 显示流程图
|
||||
async importXML(xml) {
|
||||
this.clearViewer();
|
||||
if (xml != null && xml !== '') {
|
||||
try {
|
||||
this.bpmnViewer = new BpmnViewer({
|
||||
additionalModules: [
|
||||
// 移动整个画布
|
||||
MoveCanvasModule
|
||||
],
|
||||
container: this.$refs.processCanvas,
|
||||
});
|
||||
// 任务节点悬浮事件
|
||||
this.bpmnViewer.on('element.click', ({ element }) => {
|
||||
this.onSelectElement(element);
|
||||
});
|
||||
|
||||
this.isLoading = true;
|
||||
await this.bpmnViewer.importXML(xml);
|
||||
this.addCustomDefs();
|
||||
} catch (e) {
|
||||
this.clearViewer();
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
this.setProcessStatus(this.processNodeInfo);
|
||||
}
|
||||
}
|
||||
},
|
||||
// 设置流程图元素状态
|
||||
setProcessStatus (processNodeInfo) {
|
||||
this.processNodeInfo = processNodeInfo;
|
||||
if (this.isLoading || this.processNodeInfo == null || this.bpmnViewer == null) return;
|
||||
let { finishedTaskSet, rejectedTaskSet, unfinishedTaskSet, finishedSequenceFlowSet } = this.processNodeInfo;
|
||||
const canvas = this.bpmnViewer.get('canvas');
|
||||
const elementRegistry = this.bpmnViewer.get('elementRegistry');
|
||||
if (Array.isArray(finishedSequenceFlowSet)) {
|
||||
finishedSequenceFlowSet.forEach(item => {
|
||||
if (item != null) {
|
||||
canvas.addMarker(item, 'success');
|
||||
let element = elementRegistry.get(item);
|
||||
const conditionExpression = element.businessObject.conditionExpression;
|
||||
if (conditionExpression) {
|
||||
canvas.addMarker(item, 'condition-expression');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
if (Array.isArray(finishedTaskSet)) {
|
||||
finishedTaskSet.forEach(item => canvas.addMarker(item, 'success'));
|
||||
}
|
||||
if (Array.isArray(unfinishedTaskSet)) {
|
||||
unfinishedTaskSet.forEach(item => canvas.addMarker(item, 'primary'));
|
||||
}
|
||||
if (Array.isArray(rejectedTaskSet)) {
|
||||
rejectedTaskSet.forEach(item => {
|
||||
if (item != null) {
|
||||
let element = elementRegistry.get(item);
|
||||
if (element.type.includes('Task')) {
|
||||
canvas.addMarker(item, 'danger');
|
||||
} else {
|
||||
canvas.addMarker(item, 'warning');
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
destroyed() {
|
||||
this.clearViewer();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
Reference in New Issue
Block a user