Compare commits

...

2 Commits

Author SHA1 Message Date
userName 836b33ad64 添加表单模块 2025-04-15 16:53:01 +08:00
userName 33d6c81a55 在线开发 2025-04-14 08:40:15 +08:00
172 changed files with 33644 additions and 445 deletions

7
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,7 @@
{
"i18n-ally.localesPaths": [
"src/locales",
"src/locales/lang",
"public/resource/tinymce/langs"
]
}

View File

@ -0,0 +1,7 @@
{
"folders": [
{
"path": "."
}
]
}

View File

@ -81,11 +81,15 @@
"@vueuse/core": "^10.7.1",
"@zxcvbn-ts/core": "^3.0.4",
"ant-design-vue": "^4.0.8",
"bpmn-js": "^17.0.2",
"bpmn-js-properties-panel": "^5.13.0",
"bpmn-js-token-simulation": "^0.33.1",
"axios": "^1.6.4",
"codemirror": "^5.65.16",
"cropperjs": "^1.6.1",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.10",
"diagram-js": "^14.1.0",
"driver.js": "^1.3.1",
"echarts": "^5.4.3",
"element-plus": "^2.6.0",

View File

@ -0,0 +1,229 @@
const customDrawStyles = [
{
id: 'gl-draw-polygon-fill-inactive',
type: 'fill',
filter: [
'all',
['==', 'active', 'false'],
['==', '$type', 'Polygon'],
['!=', 'mode', 'static'],
],
paint: {
'fill-color': '#3bb2d0',
'fill-outline-color': '#6495ed',
'fill-opacity': 0.5,
},
},
{
id: 'gl-draw-polygon-fill-active',
type: 'fill',
filter: ['all', ['==', 'active', 'true'], ['==', '$type', 'Polygon']],
paint: {
'fill-color': '#fbb03b',
'fill-outline-color': '#fbb03b',
'fill-opacity': 0.5,
},
},
{
id: 'gl-draw-polygon-midpoint',
type: 'circle',
filter: ['all', ['==', '$type', 'Point'], ['==', 'meta', 'midpoint']],
paint: {
'circle-radius': 3,
'circle-color': '#fbb03b',
},
},
{
id: 'gl-draw-polygon-stroke-inactive',
type: 'line',
filter: [
'all',
['==', 'active', 'false'],
['==', '$type', 'Polygon'],
['!=', 'mode', 'static'],
],
layout: {
'line-cap': 'round',
'line-join': 'round',
},
paint: {
'line-color': '#3bb2d0',
'line-width': 2,
},
},
{
id: 'gl-draw-polygon-stroke-active',
type: 'line',
filter: ['all', ['==', 'active', 'true'], ['==', '$type', 'Polygon']],
layout: {
'line-cap': 'round',
'line-join': 'round',
},
paint: {
'line-color': '#fbb03b',
'line-dasharray': [0.2, 2],
'line-width': 2,
},
},
{
id: 'gl-draw-line-inactive',
type: 'line',
filter: [
'all',
['==', 'active', 'false'],
['==', '$type', 'LineString'],
['!=', 'mode', 'static'],
['!=', 'user_isSnapGuide', 'true'],
],
layout: {
'line-cap': 'round',
'line-join': 'round',
},
paint: {
'line-color': '#3bb2d0',
'line-width': 2,
},
},
{
id: 'gl-draw-line-active',
type: 'line',
filter: ['all', ['==', '$type', 'LineString'], ['==', 'active', 'true']],
layout: {
'line-cap': 'round',
'line-join': 'round',
},
paint: {
'line-color': '#fbb03b',
'line-dasharray': [0.2, 2],
'line-width': 2,
},
},
{
id: 'gl-draw-polygon-and-line-vertex-stroke-inactive',
type: 'circle',
filter: ['all', ['==', 'meta', 'vertex'], ['==', '$type', 'Point'], ['!=', 'mode', 'static']],
paint: {
'circle-radius': 5,
'circle-color': '#fff',
},
},
{
id: 'gl-draw-polygon-and-line-vertex-inactive',
type: 'circle',
filter: ['all', ['==', 'meta', 'vertex'], ['==', '$type', 'Point'], ['!=', 'mode', 'static']],
paint: {
'circle-radius': 3,
'circle-color': '#fbb03b',
},
},
{
id: 'gl-draw-point-point-stroke-inactive',
type: 'circle',
filter: [
'all',
['==', 'active', 'false'],
['==', '$type', 'Point'],
['==', 'meta', 'feature'],
['!=', 'mode', 'static'],
],
paint: {
'circle-radius': 5,
'circle-opacity': 1,
'circle-color': '#fff',
},
},
{
id: 'gl-draw-point-inactive',
type: 'circle',
filter: [
'all',
['==', 'active', 'false'],
['==', '$type', 'Point'],
['==', 'meta', 'feature'],
['!=', 'mode', 'static'],
],
paint: {
'circle-radius': 3,
'circle-color': '#3bb2d0',
},
},
{
id: 'gl-draw-point-stroke-active',
type: 'circle',
filter: ['all', ['==', '$type', 'Point'], ['==', 'active', 'true'], ['!=', 'meta', 'midpoint']],
paint: {
'circle-radius': 7,
'circle-color': '#fff',
},
},
{
id: 'gl-draw-point-active',
type: 'circle',
filter: ['all', ['==', '$type', 'Point'], ['!=', 'meta', 'midpoint'], ['==', 'active', 'true']],
paint: {
'circle-radius': 5,
'circle-color': '#fbb03b',
},
},
{
id: 'gl-draw-polygon-fill-static',
type: 'fill',
filter: ['all', ['==', 'mode', 'static'], ['==', '$type', 'Polygon']],
paint: {
'fill-color': '#404040',
'fill-outline-color': '#404040',
'fill-opacity': 0.1,
},
},
{
id: 'gl-draw-polygon-stroke-static',
type: 'line',
filter: ['all', ['==', 'mode', 'static'], ['==', '$type', 'Polygon']],
layout: {
'line-cap': 'round',
'line-join': 'round',
},
paint: {
'line-color': '#404040',
'line-width': 2,
},
},
{
id: 'gl-draw-line-static',
type: 'line',
filter: ['all', ['==', 'mode', 'static'], ['==', '$type', 'LineString']],
layout: {
'line-cap': 'round',
'line-join': 'round',
},
paint: {
'line-color': '#404040',
'line-width': 2,
},
},
{
id: 'gl-draw-point-static',
type: 'circle',
filter: ['all', ['==', 'mode', 'static'], ['==', '$type', 'Point']],
paint: {
'circle-radius': 5,
'circle-color': '#404040',
},
},
{
id: 'guide',
type: 'line',
filter: ['all', ['==', '$type', 'LineString'], ['==', 'user_isSnapGuide', 'true']],
layout: {
'line-cap': 'round',
'line-join': 'round',
},
paint: {
'line-color': '#c00c00',
'line-width': 1,
'line-dasharray': [5, 5],
},
},
];
export { customDrawStyles };

View File

@ -0,0 +1,652 @@
<template>
<div class="map-container">
<div :id="mapContainerName" class="map-box"></div>
<a-input-search class="inputbox" v-model:value="address" enter-button placeholder="请输入地址" @search="searchAddress" />
</div>
</template>
<script lang="ts" setup>
import {
onMounted,
onUnmounted,
defineProps,
defineEmits,
reactive,
ref,
defineExpose,
watch,
inject,
} from 'vue';
import mapboxgl, { Map, Popup } from 'mapbox-gl';
//
import MapboxDraw from '@mapbox/mapbox-gl-draw';
import { generateUUID, getGeometryCenter } from '../src/tool';
import 'mapbox-extensions/dist/index.css';
import U from 'mapbox-gl-utils';
import 'mapbox-gl/dist/mapbox-gl.css';
import * as turf from '@turf/turf';
import '../src/index.less';
import { MapboxConfig, MapboxDefaultStyle, MapControlConfig } from '../src/config';
import { MP } from '../src/MP';
import {
SnapPolygonMode,
SnapPointMode,
SnapLineMode,
SnapModeDrawStyles,
SnapDirectSelect,
} from 'mapbox-gl-draw-snap-mode';
import { customDrawStyles } from '../Styles/Styles';
import { WktToGeojson, GeojsonToWkt } from '../src/WktGeojsonTransform';
import { getPolygonCenter } from '@/api/tiankongdi/index'
import { message } from 'ant-design-vue';
import axios from 'axios';
const mapContainerName = ref<String>();
mapContainerName.value = 'mapContainer-' + generateUUID();
const props = defineProps(['geoms','id','isRead']);
const emit = defineEmits(['handlerDrawComplete'])
const address = ref()
const searchAddress = ()=>{
console.log('213',address.value)
axios({
method: 'get',
url: `https://restapi.amap.com/v3/geocode/geo?key=4f992c089f9496201f6e4ea39ff3ab60&address=`+address.value,
}).then((res) => {
if(res.data){
let location = res.data.geocodes[0].location.split(',')
map.flyTo({
center: location,
zoom: 13,
bearing: 0,
speed: 1, //
curve: 2, // 线
essential: true,
easing(t) {
//
return t;
},
});
}
});
}
//
let map: Map;
let drawTool: any;
let clickPoisition: Array<number> = [];
let mp: any = null;
let geojson = reactive({
geojson: {},
});
let drawing = ref(false);
let geoms = ref<any>([])
let hasPolygon = false;
onMounted(() => {
mapboxgl.accessToken = MapboxConfig.ACCESS_TOKEN;
map = initMap();
map.on('load', () => {
//mapbox-gl-utils
U.init(map);
mp = new MP(map)
//
handlerInitDrawTool();
map.on('click', (e) => {
clickPoisition = e.lngLat;
});
//
map.on('draw.create', function (e) {
if (hasPolygon) {
drawTool.delete(e.features[0].id);
message.warning("只能绘制一个图斑,请先删除已有的!");
return;
}
hasPolygon = true;
handlerDealFeature(e.features[0]);
});
map.on('draw.update', function (e) {
handlerDealFeature(e.features[0]);
});
map.on('draw.delete', function (e) {
hasPolygon = false;
handlerDeleteFeature(e.features[0]);
});
map.on("draw.selectionchange", (e) => {
e.features.forEach(feature => {
if (feature.properties.user_static) {
drawTool.changeMode("simple_select", { featureIds: [] }); //
}
});
});
let filter = '"RelationId"=\'' + props.id + "'";
// getPolygonCenter({ tablename: 'idle_shp', filter: filter }).then(res => {
// if(res.length > 0){
// try {
// let geojson = WktToGeojson(res[0].centroid_point);
// map.flyTo({
// center: geojson.coordinates,
// zoom: 17.2,
// bearing: 0,
// speed: 1, //
// curve: 2, // 线
// essential: true,
// easing(t) {
// //
// return t;
// },
// });
// } catch (e) {
// console.log(e)
// }
// }
// });
});
});
//
onUnmounted(() => {
map ? map.remove() : null;
});
//
const initMap = () => {
return new mapboxgl.Map({
container: mapContainerName.value,
language: 'zh-cmn',
projection: 'equirectangular', // wgs84
style: MapboxDefaultStyle,
minZoom: 1,
maxZoom: 24,
zoom: 10,
pitch: 0,
center: [118.340253, 35.092481],
});
};
//
const handlerInitDrawTool = (feature, bool) => {
geojson.geojson = feature;
if (drawTool) {
drawTool.deleteAll();
if (feature.features) {
drawTool.set(geojson.geojson);
}
} else {
drawTool = new MapboxDraw({
displayControlsDefault: false,
controls: {
point: props.isRead? false: true,
polygon: props.isRead? false: true, //
trash: props.isRead? false: true //
},
modes: {
...MapboxDraw.modes,
draw_point: SnapPointMode,
draw_polygon: SnapPolygonMode,
draw_line_string: SnapLineMode,
direct_select: SnapDirectSelect,
},
styles: customDrawStyles,
userProperties: true,
snap: true,
snapOptions: {
snapPx: 12, // defaults to 15
snapToMidPoints: true, // defaults to false
snapVertexPriorityDistance: 0.0025, // defaults to 1.25
},
guides: false,
});
map.addControl(drawTool, 'top-right');
setTimeout(() => {
document.querySelector(".mapbox-gl-draw_polygon")?.setAttribute("title", "绘制图斑");
document.querySelector(".mapbox-gl-draw_trash")?.setAttribute("title", "删除图斑");
}, 500);
// let featureList:any = []
if(props.geoms){
props.geoms.forEach(item => {
const geojsonPolygon = WktToGeojson(item.geom)
const featureId = `polygon-${generateUUID()}`;
let properties = {}
if(props.isRead){
properties = { user_static: true}
}
const feature = {
id: featureId,
type: "Feature",
properties,
geometry: geojsonPolygon
};
geoms.value.push(feature)
// featureList.push(feature)
hasPolygon = true
drawTool.add(feature);
})
}
// drawTool.add(featureList);
}
drawing.value = true;
};
//
const handlerDealFeature = (feature) => {
if(map.getLayer('area-label')){
map.removeLayer('area-label');
map.removeSource('area-label');
}
let area = turf.area(feature);
const centroid = turf.centroid(feature);
let labelText = `${area.toFixed(2)}`;
map.addLayer({
id: `area-label`, // ID
type: "symbol",
source: {
type: "geojson",
data: centroid
},
// layout: {
// "text-field": labelText, //
// "text-size": 14,
// "text-anchor": "center"
// },
paint: {
"text-color": "#ff0000"
}
});
let existFeature = geoms.value.find((item, index) => {
return item.id == feature.id;
});
if (existFeature) {
//
for (let i = 0; i < geoms.value.length; i++) {
if (geoms.value[i].id == feature.id) {
geoms.value[i] = feature;
}
}
} else {
//
geoms.value.push(feature);
}
//
handlerDrawComplete();
};
//
const handlerDeleteFeature = (feature) => {
if(map.getLayer('area-label')){
map.removeLayer('area-label');
map.removeSource('area-label');
}
for (let i = 0; i < geoms.value.length; i++) {
if (geoms.value[i].id == feature.id) {
geoms.value.splice(i, 1);
}
}
handlerDrawComplete();
};
const handlerDrawComplete = () => {
let arr = [];
geoms.value.forEach((item, index) => {
let wktStr = GeojsonToWkt(item.geometry);
let area = turf.area(item).toFixed(2)
let center = turf.center(item)
let obj = {
center: center.geometry.coordinates,
geom: wktStr,
};
arr.push(obj);
});
emit('handlerDrawComplete', arr);
};
</script>
<style scoped>
.cloud-query-div {
position: absolute;
top: 50px;
left: 10px;
width: 66px;
height: 66px;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
padding: 5px;
border-radius: 5px;
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1);
user-select: none;
cursor: pointer;
}
.cloud-query-icon {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 8px;
}
.map-container {
width: 100%;
height: 100%;
}
.map-box {
width: 100%;
height: 100%;
}
.layer-control-center {
position: absolute;
top: 15px;
left: 15px;
background: #fff;
border-radius: 8px;
}
.layer-control-center p {
margin: 0px;
}
.layer-control-center .ant-checkbox-wrapper {
}
.draw-control-center {
position: absolute;
padding: 8px;
top: 15px;
right: 15px;
background: #ffffff;
border-radius: 12px;
}
.draw-control-center .draw-btn {
float: left;
margin: 0px 7px;
padding: 5px;
border-radius: 5px;
}
.draw-control-center .draw-btn:hover {
background-color: rgb(0 0 0/5%);
cursor: pointer;
}
.mapboxgl-ctrl-group:not(:empty) {
box-shadow: none;
}
.mapboxgl-ctrl-group {
padding: 6px;
border-radius: 12px;
top: 5px;
right: 0px;
}
.mapbox-gl-draw_ctrl-draw-btn {
width: 20px !important;
height: 20px !important;
float: left;
}
.mapboxgl-ctrl-top-right {
width: 360px;
}
.mapboxgl-ctrl-group button + button {
border: 0px;
margin: 0px 6px;
}
.mapbox-gl-draw_ctrl-draw-btn:hover {
transform: scale(1.2);
}
.mapbox-gl-draw_polygon {
background-image: url(/polygon.png);
background-size: 100% 100%;
width: 100px;
height: 100px;
}
.mapbox-gl-draw_point {
background-image: url(/point.png);
background-size: 100% 100%;
width: 100px;
height: 100px;
}
.mapbox-gl-draw_line {
background-image: url(/line.png);
background-size: 100% 100%;
width: 100px;
height: 100px;
margin: 0px 6px;
}
.mapbox-gl-draw_trash {
background-image: url(/del.png);
background-size: 100% 100%;
width: 100px;
height: 100px;
}
.mapbox-gl-draw_combine {
background-image: url(/combine.png);
background-size: 100% 100%;
width: 100px;
height: 100px;
}
.mapbox-gl-draw_uncombine {
background-image: url(/uncombine.png);
background-size: 100% 100%;
width: 100px;
height: 100px;
}
.jas-ctrl-measure {
position: relative;
top: 6px;
right: 10px;
}
.jas-ctrl-measure-item {
height: 22px;
color: rgb(255, 255, 255);
}
.layer-item {
padding: 8px 16px;
}
.layer-item:hover {
background: #c7dcf580;
}
::v-deep .ant-collapse-content-box {
padding: 0px !important;
}
::v-deep .jas-ctrl-extend-desktop-container {
width: 320px !important;
}
.position-by-lnglat {
height: 29px;
background: #fff;
position: absolute;
top: 10px;
right: 131px;
border-radius: 3px;
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1);
.to-location {
width: 29px;
height: 29px;
float: left;
background: url(/map/location.png);
background-size: 20px 20px;
background-repeat: no-repeat;
background-position: 4px 5px;
&:hover {
cursor: pointer;
}
}
.picture-azimuth {
width: 29px;
height: 29px;
float: left;
background: url(/map/is_show_picture.png);
background-size: 20px 20px;
background-repeat: no-repeat;
background-position: 4px 5px;
&:hover {
cursor: pointer;
}
}
.picture-azimuth-active {
width: 29px;
height: 29px;
float: left;
background: url(/map/not_show_picture.png);
background-size: 20px 20px;
background-repeat: no-repeat;
background-position: 4px 5px;
&:hover {
cursor: pointer;
}
}
.draw-polygon {
width: 29px;
height: 29px;
float: left;
background: url(/map/draw_polygon.png);
background-size: 20px 20px;
background-repeat: no-repeat;
background-position: 4px 5px;
&:hover {
cursor: pointer;
}
}
.split-line {
width: 29px;
height: 29px;
float: left;
background: url(/map/split_polygon.png);
background-size: 20px 20px;
background-repeat: no-repeat;
background-position: 4px 5px;
&:hover {
cursor: pointer;
}
}
.split-polygon {
width: 29px;
height: 29px;
float: left;
background: url(/map/split_polygon_polygon.png);
background-size: 20px 20px;
background-repeat: no-repeat;
background-position: 4px 5px;
&:hover {
cursor: pointer;
}
}
}
.to-location-input {
padding: 16px;
padding-right: 4px;
width: 418px;
min-height: 60px;
background: #fff;
position: absolute;
top: 48px;
right: 10px;
z-index: 999999;
border-radius: 5px;
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
.location-operation {
width: 100%;
height: 40px;
border-bottom: 1px solid #f1f1f1;
margin-bottom: 12px;
}
.location-item-list-coantienr {
width: 100%;
max-height: 400px;
overflow-y: auto;
.location-item {
line-height: 20px;
margin-bottom: 6px;
}
}
}
.split-panel-item:hover {
cursor: pointer;
color: #999;
}
.cloudqueryNotice {
background: rgba(0, 0, 0, 0.53);
padding: 0px 14px;
border-radius: 6px;
position: fixed;
top: 20px;
right: 5vw;
width: 700px;
color: #fff;
z-index: 10;
.cloudquery-title {
height: 40px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 16px;
}
.cloudquery-left {
display: flex;
align-items: center;
}
.cloudquery-right {
display: flex;
align-items: center;
justify-content: space-around;
width: 130px;
}
img {
width: 34px;
height: 29px;
}
.cloudquery-btn {
display: flex;
justify-content: flex-end;
}
.line {
background: #ededed;
width: 1px;
height: 20px;
}
.anticon.anticon-close {
height: 30px;
}
button {
width: 70px;
height: 26px;
background: linear-gradient(-74deg, #086dec, #0b4bdd);
box-shadow: 3px 4px 5px 1px rgba(13, 13, 13, 0.05);
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
}
.title-box {
margin-left: 10px;
font-size: 14px;
}
}
.inputbox{
position: absolute;
z-index: 999;
left: 20px;
top: 10px;
width: 300px;
}
</style>

View File

@ -0,0 +1,4 @@
import { withInstall } from '@/utils';
import getAddressDetails from './index.vue'
export const AddressDetails = withInstall(getAddressDetails);

View File

@ -0,0 +1,84 @@
<template>
<div>
<a-input v-model:value="address" placeholder="" @click="showMaps" />
<a-row class="containerbox" v-if="lngLat.length>0">
<a-col :span="12" class="flex">
<div class="labelbox">经度</div>
<a-input class="latbox" v-model:value="lngLat[0]" disabled />
</a-col>
<a-col :span="12" class="flex">
<div class="labelbox">维度</div>
<a-input class="latbox" v-model:value="lngLat[1]" disabled />
</a-col>
</a-row>
</div>
<a-modal v-model:visible="mapsVisible" title="位置选择" @cancel="handleCancel" @ok="handleOk" width="800px" height="500px" >
<div class="modalbox">
<MapboxMap @handlerDrawComplete="handlerDrawComplete"></MapboxMap>
</div>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, watch, defineAsyncComponent } from 'vue';
import MapboxMap from './components/map.vue';
import axios from 'axios';
const address = ref('')
const mapsVisible = ref(false)
const lngLat = ref([])
const paramsData = ref()
const showMaps = ()=>{
console.log('asdddd')
mapsVisible.value = true
}
const handleCancel = ()=>{
paramsData.value = null
}
const handleOk = ()=>{
if(paramsData.value){
lngLat.value = paramsData.value.center
axios({
method: 'get',
url: `https://restapi.amap.com/v3/geocode/regeo?output=json&location=`+paramsData.value.center+`&key=4f992c089f9496201f6e4ea39ff3ab60&radius=1000&extensions=base`,
}).then((res) => {
if(res.data){
address.value = res.data.regeocode.formatted_address
}
});
}else{
lngLat.value = []
address.value = ''
}
mapsVisible.value = false
}
const handlerDrawComplete = (e)=>{
paramsData.value = e[0]
}
</script>
<style lang="less">
.modalbox{
width: 100%;
height: 500px;
}
.flex{
display: flex;
}
.containerbox{
margin-left: -100px;
margin-top: 20px;
}
.labelbox{
width: 110px;
text-align: end;
white-space: nowrap;
font-size: 14px;
line-height: 32px;
}
.latbox{
margin-left: 10px;
}
</style>

View File

@ -0,0 +1,554 @@
import U from 'mapbox-gl-utils';
import * as turf from '@turf/turf';
type EventType = 'Point' | 'LineString' | 'Polygon';
interface GenerateGeoJSONDataInterface {
lng: number;
lat: number;
}
interface FeatureCollection {
type: string;
features: Array<any>;
}
const CIRCLE_STYLE = {
'circle-color': '#6495ED',
'circle-radius': 8,
'circle-stroke-width': 2,
'circle-stroke-color': '#ffffff',
};
const LINE_STYLE = {
'line-color': '#6495ED',
'line-width': 3,
};
const LINE_IS_DRAW_STYLE = {
'line-dasharray': [2, 2],
'line-color': '#00FA9A',
'line-width': 3,
};
const POLYGON_STYLE = {
'fill-color': '#FAFAD2',
'fill-opacity': 0.6,
};
function typeOf(obj: any): any {
const toString: any = Object.prototype.toString;
const map: any = {
'[object Boolean]': 'boolean',
'[object Number]': 'number',
'[object String]': 'string',
'[object Function]': 'function',
'[object Array]': 'array',
'[object Date]': 'date',
'[object RegExp]': 'regExp',
'[object Undefined]': 'undefined',
'[object Null]': 'null',
'[object Object]': 'object',
};
return map[toString.call(obj)];
}
function deepClone(data: any): any {
// 获取传入拷贝函数的数据类型
const type = typeOf(data);
// 定义一个返回any类型的数据
let reData: any;
// 递归遍历一个array类型数据
if (type === 'array') {
reData = [];
for (let i = 0; i < data.length; i++) {
reData.push(deepClone(data[i]));
}
} else if (type === 'object') {
//递归遍历一个object类型数据
reData = {};
for (const i in data) {
reData[i] = deepClone(data[i]);
}
} else {
// 返回基本数据类型
return data;
}
// 将any类型的数据return出去作为deepClone的结果
return reData;
}
class BaseMP {
map: any;
listeners: {};
constructor(map: any) {
this.map = map;
// 初始化mapbox工具类
U.init(this.map);
this.listeners = {};
}
//监听
on(event: EventType, callback: void) {
console.log('on', event);
this.listeners[event] = callback;
}
off(event: EventType) {
console.log('off', event);
if (this.listeners[event]) {
delete this.listeners[event];
}
}
emit(event: EventType, data: any) {
if (this.listeners[event]) {
this.listeners[event](data);
}
}
//防抖函数
debounce = (func: Function, delay: number) => {
let timer: any = null;
return (...args: any) => {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, args);
}, delay);
};
};
//切换鼠标样式
_changeMouseCursor = (cursor: string = 'pointer') => {
this.map.getCanvas().style.cursor = cursor;
};
//生成geoJson
_generateGeoJSON(data: GenerateGeoJSONDataInterface[], geometryType: EventType) {
/**
* // 生成点类型的GeoJSON
* var data = [{lng: -122.4194, lat: 37.7749}, {lng: -122.408, lat: 37.791}, {lng: -122.431, lat: 37.769}];
* var geoJSON = generateGeoJSON(data, "Point");
*
* // 生成线类型的GeoJSON
* var data = [{lng: -122.4194, lat: 37.7749}, {lng: -122.408, lat: 37.791}, {lng: -122.431, lat: 37.769}];
* var geoJSON = generateGeoJSON(data, "LineString");
*
* // 生成面类型的GeoJSON
* var data = [{lng: -122.4194, lat: 37.7749}, {lng: -122.408, lat: 37.791}, {lng: -122.431, lat: 37.769}];
* var geoJSON = generateGeoJSON(data, "Polygon");
* **/
const featureCollection: FeatureCollection = {
type: 'FeatureCollection',
features: [],
};
switch (geometryType) {
case 'Point':
for (let i = 0; i < data.length; i++) {
const feature = {
type: 'Feature',
geometry: {
type: geometryType,
coordinates: [data[i].lng, data[i].lat],
},
properties: {},
};
featureCollection.features.push(feature);
}
break;
case 'LineString':
const lineFeature = {
type: 'Feature',
geometry: {
type: geometryType,
coordinates: [],
},
properties: {},
};
for (let i = 0; i < data.length; i++) {
lineFeature['geometry']['coordinates'].push([data[i].lng, data[i].lat]);
}
featureCollection.features.push(lineFeature);
break;
case 'Polygon':
const polygonFeature = {
type: 'Feature',
geometry: {
type: geometryType,
coordinates: [[]],
},
properties: {},
};
for (let i = 0; i < data.length; i++) {
polygonFeature['geometry']['coordinates'][0].push([data[i].lng, data[i].lat]);
}
featureCollection.features.push(polygonFeature);
break;
}
return featureCollection;
}
}
export class MP extends BaseMP {
drawModelChoose: { LineString: string; Point: string; Polygon: string; DEFAULT: string };
drawModel: string;
drawLocal: never[];
drawCurrentId: {
point: string; //点
line: string; //线
lastLine: string; //鼠标位置的点
polygon: string;
};
currentMouseLocation: null;
correctionMouseLocation: any;
clickCount: number;
clickTimeout: any;
constructor(map) {
super(map);
this.drawModelChoose = {
LineString: 'LineString',
Point: 'Point',
Polygon: 'Polygon',
DEFAULT: 'default',
};
this.drawModel = this.drawModelChoose.DEFAULT;
this.drawLocal = [];
this.drawCurrentId = {
point: 'current-draw-point', //点
line: 'current-draw-line', //线
lastLine: 'current-draw-last-line', //鼠标位置的点
polygon: 'current-draw-polygon',
};
this.currentMouseLocation = null; //当前鼠标位置
this.correctionMouseLocation = null; //校正后的鼠标位置
this.clickCount = 0; // 定义一个计数器
this.clickTimeout = null; // 定义一个超时变量
}
//添加或更新source
_addOrUpdateSource = (gS, id) => {
if (this.map.getSource(id)) {
this.map.getSource(id).setData(gS);
} else {
this.map.U.addGeoJSON(id, gS);
}
};
// 添加layer有则不操作
_addLayer = (layerId, sourceId, type, style) => {
// 如果图层不存在,添加图层。如果存在,不做操作
if (!this.map.getLayer(sourceId)) {
switch (type) {
case this.drawModelChoose.Point:
//addCircleLayer
this.map.U.addCircleLayer(layerId, sourceId, style);
break;
case this.drawModelChoose.LineString:
//addLineLayer
this.map.U.addLineLayer(layerId, sourceId, style);
break;
case this.drawModelChoose.Polygon:
//addPolygonLayer
this.map.U.addFillLayer(layerId, sourceId, style);
break;
default:
console.log('layer类型错误');
}
}
};
_currentDrawSource = () => {
// currentGSP 点 currentGSL 线 currentGSPL 面
const currentGSP = this._generateGeoJSON(this.drawLocal, 'Point');
this._addOrUpdateSource(currentGSP, this.drawCurrentId.point);
if (
this.drawModel === this.drawModelChoose.LineString ||
this.drawModel === this.drawModelChoose.Polygon
) {
const currentGSL = this._generateGeoJSON(this.drawLocal, 'LineString');
this._addOrUpdateSource(currentGSL, this.drawCurrentId.line);
}
if (this.drawModel === this.drawModelChoose.Polygon) {
const lastGSPL = this._generateGeoJSON(this.drawLocal, 'Polygon');
this._addOrUpdateSource(lastGSPL, this.drawCurrentId.polygon);
}
};
//绘制当前的layer
_currentDrawLayer = () => {
// 如果是点,直接绘制点
this._addLayer(this.drawCurrentId.point, this.drawCurrentId.point, 'Point', CIRCLE_STYLE);
// 如果是线,在点的基础上,加上线
// 如果是面,在线的基础上,加上面
if (
this.drawModel === this.drawModelChoose.LineString ||
this.drawModel === this.drawModelChoose.Polygon
) {
this._addLayer(this.drawCurrentId.line, this.drawCurrentId.line, 'LineString', LINE_STYLE);
}
if (this.drawModel === this.drawModelChoose.Polygon) {
this._addLayer(
this.drawCurrentId.polygon,
this.drawCurrentId.polygon,
'Polygon',
POLYGON_STYLE,
);
}
};
//最后动态的一笔
_currentDrawLastLine = () => {
if (
this.drawModel === this.drawModelChoose.LineString ||
this.drawModel === this.drawModelChoose.Polygon
) {
if (!this.drawLocal.length) {
this.map.U.removeLayer(this.drawCurrentId.lastLine);
this.map.U.removeSource(this.drawCurrentId.lastLine);
return false;
}
const startPoint = this.drawLocal[this.drawLocal.length - 1];
const endPoint = this.getDrawEndPoint();
// 添加动态线
const lastGSL = this._generateGeoJSON([startPoint, endPoint], 'LineString');
this.crossesLine(this.drawLocal, [startPoint, endPoint]);
this._addOrUpdateSource(lastGSL, this.drawCurrentId.lastLine);
this._addLayer(
this.drawCurrentId.lastLine,
this.drawCurrentId.lastLine,
'LineString',
LINE_IS_DRAW_STYLE,
);
}
if (this.drawModel === this.drawModelChoose.Polygon) {
this._currentDrawLastPolygon();
}
};
//获取矫正的最后点
getDrawEndPoint = () => {
let endPoint = null;
if (this.nearlyClientDistant()) {
endPoint = this.correctionMouseLocation;
} else {
endPoint = this.currentMouseLocation;
}
return endPoint;
};
//最后动态的一笔,跟随面生成
_currentDrawLastPolygon = () => {
// 添加动态面
const endPoint = this.getDrawEndPoint();
const drawLocalCopy = deepClone(this.drawLocal);
drawLocalCopy.push(endPoint);
drawLocalCopy.push(drawLocalCopy[0]);
const lastGSPL = this._generateGeoJSON(drawLocalCopy, 'Polygon');
this._addOrUpdateSource(lastGSPL, this.drawCurrentId.polygon);
this._addLayer(
this.drawCurrentId.polygon,
this.drawCurrentId.polygon,
'Polygon',
POLYGON_STYLE,
);
};
//鼠标点击
clickHandler = (e) => {
const _this = this;
this.clickCount++; // 每次单击事件计数器加1
if (this.clickCount === 1) {
// 如果是第一次单击
this.clickTimeout = setTimeout(function () {
// 启动超时变量
// 单击事件
_this.clickCount = 0; // 计数器清零
if (_this.nearlyClientDistant()) {
_this.drawLocal.push(_this.correctionMouseLocation);
// 如果是第一个点,那么就结束绘制
// todo 画面第一点后结束
if (
_this.drawModel === _this.drawModelChoose.Polygon &&
_this.correctionMouseLocation === _this.drawLocal[0]
) {
_this._currentDrawSource();
_this._currentDrawLayer();
_this.emit(_this.drawModel, _this.drawLocal);
_this.finishDraw();
return false;
} else {
_this.drawLocal.pop();
}
} else {
_this.drawLocal.push(e.lngLat);
}
_this._currentDrawSource();
_this._currentDrawLayer();
if (_this.drawModel === _this.drawModelChoose.Point) {
_this.drawIsFinish();
}
}, 200); // 设置超时时间为300毫秒
} else {
// 如果不是第一次单击
clearTimeout(this.clickTimeout); // 清除超时变量
this.clickCount = 0; // 计数器清零
// 在这里编写双击事件的操作
// 鼠标双击事件
if (this.drawModel === this.drawModelChoose.LineString) {
_this.drawLocal.push(e.lngLat);
_this._currentDrawSource();
_this._currentDrawLayer();
this.drawIsFinish();
}
if (this.drawModel === this.drawModelChoose.Polygon) {
_this.drawLocal.push(e.lngLat);
_this._currentDrawSource();
_this._currentDrawLayer();
this.drawIsFinish();
}
}
};
//右键点击
contextmenuHandler = (e) => {
if (this.drawLocal.length < 1) {
return false;
}
this.drawLocal.pop();
this._currentDrawSource();
this._currentDrawLayer();
this._currentDrawLastLine();
};
// 鼠标移动
mousemoveHandler = this.debounce((e) => {
this.currentMouseLocation = e.lngLat;
this._currentDrawLastLine();
}, 5);
// 判断是否吸附距离12像素
nearlyClientDistant = () => {
// 判断屏幕距离,鼠标吸附
let _this = this;
let mousePoint = [this.currentMouseLocation.lng, this.currentMouseLocation.lat];
let closestFeature = null;
let closestDistance = Infinity;
let threshold = 10; // 阈值,单位为像素
// 遍历所有特征,找到距离最近的特征
let clientPosition = _this.map.project(mousePoint);
this.drawLocal.forEach(function (feature) {
_this.map.project([feature.lng, feature.lat]);
let featurePosition = _this.map.project([feature.lng, feature.lat]);
// 计算两个点在屏幕上的像素距离
let distance = Math.sqrt(
Math.pow(clientPosition.x - featurePosition.x, 2) +
Math.pow(clientPosition.y - featurePosition.y, 2),
);
if (distance < closestDistance && distance < threshold) {
closestFeature = feature;
closestDistance = distance;
}
});
// 如果距离小于阈值,则将标注吸附到特征上
if (closestFeature) {
this._changeMouseCursor('pointer');
this.correctionMouseLocation = closestFeature;
return true;
} else {
// console.log('范围外')
this._changeMouseCursor('crosshair');
return false;
}
};
// 判断是否相交 true 相交 false 不相交
crossesLine = (line1, line2) => {
let _this = this;
if (this.drawLocal.length >= 3) {
let _line1 = line1.map((e) => {
return [e.lng, e.lat];
});
_line1.pop();
let _line2 = line2.map((e) => {
return [e.lng, e.lat];
});
//判断是否有交叉
let intersect = turf.lineIntersect(turf.lineString(_line1), turf.lineString(_line2));
if (intersect.features.length > 0) {
let coordinates = intersect.features[0].geometry.coordinates;
//判断是否交叉在点上
const isExist = _line1.some(
(item) => item[0] === coordinates[0] && item[1] === coordinates[1],
);
if (isExist) {
// console.log('no cross,穿过了交点')
_this.map.U.setProperty(this.drawCurrentId.lastLine, 'line-color', '#00FA9A');
_this.map.on('click', this.clickHandler);
} else {
// console.log('cross')
_this.map.U.setProperty(this.drawCurrentId.lastLine, 'line-color', '#DC143C');
_this._changeMouseCursor('no-drop');
_this.map.off('click', this.clickHandler);
}
} else {
// console.log('no cross,不沾边')
_this.map.U.setProperty(this.drawCurrentId.lastLine, 'line-color', '#00FA9A');
_this.map.on('click', this.clickHandler);
}
} else {
return false;
}
};
drawStart = () => {
//每次绘制都要初始化数据
this.drawLocal = [];
this.deleteDraw();
//禁用鼠标双击放大事件
this.map.doubleClickZoom.disable();
this._changeMouseCursor('crosshair');
this.map.on('click', this.clickHandler);
this.map.on('contextmenu', this.contextmenuHandler);
this.map.on('mousemove', this.mousemoveHandler);
};
draw = (shape) => {
if(this.drawModelChoose[shape]) {
this.drawModel = this.drawModelChoose[shape];
this.drawStart();
} else {
console.log(`暂无${shape}类型`);
}
};
drawIsFinish = () => {
let _this = this;
this.emit(this.drawModel, this.drawLocal);
this.finishDraw();
//恢复双击放大功能
setTimeout(() => {
_this.map.doubleClickZoom.enable();
}, 10);
};
// 结束绘制线和面
finishDraw = () => {
this.map.U.removeSource(this.drawCurrentId.lastLine);
this.map.U.removeLayer(this.drawCurrentId.lastLine);
this.drawModel = this.drawModelChoose.DEFAULT;
this.unDraw();
};
unDraw = () => {
// console.log('推出绘制模式');
this.map.off('click', this.clickHandler);
this.map.off('contextmenu', this.contextmenuHandler);
this.map.off('mousemove', this.mousemoveHandler);
this._changeMouseCursor('pointer');
this.drawLocal = [];
};
//删除绘制的内容
deleteDraw = () => {
this.map.U.removeSource([
this.drawCurrentId.line,
this.drawCurrentId.point,
this.drawCurrentId.lastLine,
this.drawCurrentId.polygon,
]);
this.map.U.removeLayer([
this.drawCurrentId.line,
this.drawCurrentId.point,
this.drawCurrentId.lastLine,
this.drawCurrentId.polygon,
]);
this.unDraw();
};
}

View File

@ -0,0 +1,47 @@
import WKT from "terraformer-wkt-parser"
import { wktToGeoJSON,geojsonToWKT } from "@terraformer/wkt"
const WktToGeojson = (wktData)=> {
// return WKT.parse(wktData)
return wktToGeoJSON(wktData);
}
const GeojsonToWkt = (geojsonData)=> {
// return WKT.convert(geojsonData)
console.log("geojsonData",geojsonData)
return geojsonToWKT(geojsonData)
}
const removeZM = (geoJSON) => {
function removeZMFromCoords(coords) {
return coords.map((coord) => [coord[0], coord[1]]); // 只保留 X 和 Y
}
switch (geoJSON.type) {
case 'Point':
geoJSON.coordinates = removeZMFromCoords([geoJSON.coordinates]).flat();
break;
case 'LineString':
case 'MultiPoint':
geoJSON.coordinates = removeZMFromCoords(geoJSON.coordinates);
break;
case 'Polygon':
case 'MultiLineString':
geoJSON.coordinates = geoJSON.coordinates.map((ring) => removeZMFromCoords(ring));
break;
case 'MultiPolygon':
geoJSON.coordinates = geoJSON.coordinates.map((polygon) =>
polygon.map((ring) => removeZMFromCoords(ring))
);
break;
default:
throw new Error(`Unsupported geometry type: ${geoJSON.type}`);
}
return geoJSON;
}
export {WktToGeojson,GeojsonToWkt,removeZM}

View File

@ -0,0 +1,80 @@
export enum MapboxConfig {
ACCESS_TOKEN = 'pk.eyJ1IjoiemhhbmcxMjM4ODk5OSIsImEiOiJja3N5Ync1cXcyMTR2Mm9xempmbGE4MnBtIn0.R-j78CRvbs6JZG-MDSoh8Q',
// ACCESS_TOKEN = "1234",
TDT_TOKEN = 'b6585bc41ee16251dbe6b1af64f375d9',
// add more config options here
}
export const MapboxDefaultStyle = {
glyphs: 'mapbox://fonts/mapbox/{fontstack}/{range}.pbf',
version: 8,
sources: {
'dianzi': {
type: 'raster',
tiles: [
`https://t0.tianditu.gov.cn/DataServer?T=cva_w&x={x}&y={y}&l={z}&tk=${MapboxConfig.TDT_TOKEN}`,
],
tileSize: 256,
minzoom:18,
maxzoom:24,
},
'dianzi-biaozhu': {
type: 'raster',
tiles: [
`https://t0.tianditu.gov.cn/DataServer?T=vec_w&x={x}&y={y}&l={z}&tk=${MapboxConfig.TDT_TOKEN}`,
],
tileSize: 256,
},
'raster-tiles': {
type: 'raster',
tiles: [
`https://t0.tianditu.gov.cn/DataServer?T=img_w&x={x}&y={y}&l={z}&tk=${MapboxConfig.TDT_TOKEN}`,
],
tileSize: 256,
minzoom:0,
maxzoom:18,
},
},
layers: [
{
id: 'dianzi-biaozhu',
type: 'raster',
source: 'dianzi-biaozhu',
layout: {
visibility: 'visible',
},
},{
id: 'tdt-img-tiles',
type: 'raster',
source: 'raster-tiles',
minzoom: 0,
maxzoom: 18,
},{
id: 'dianzi',
type: 'raster',
source: 'dianzi',
layout: {
visibility: 'visible',
},
minzoom: 18,
maxzoom: 24,
}
],
};
export const MapControlConfig = {
DrawPoint: {
handler: 'handlerDrawPoint',
icon: '/point.png',
title: '绘制点',
},
DrawLineString: {
handler: 'handlerDrawLineString',
icon: '/line.png',
title: '绘制线',
},
DrawPolygon: {
handler: 'handlerDrawPolygon',
icon: '/polygon.png',
title: '绘制面',
},
};

View File

@ -0,0 +1,22 @@
.mapboxgl-ctrl-logo {
display: none !important;
}
.map-container {
position: relative;
}
.map-box,
.map-container {
width: 100%;
height: 100%;
}
.map-control {
position: absolute;
right: 10px;
top: 10px;
display: flex;
}
.map-control img {
width: 40px;
height: 40px;
cursor: pointer;
}

View File

@ -0,0 +1,27 @@
.mapboxgl-ctrl-logo {
display: none !important;
}
.map-container{
position: relative;
}
.map-box,
.map-container {
width: 100%;
height: 100%;
}
.map-control {
position: absolute;
right: 10px;
top: 10px;
display: flex;
img {
width: 40px;
height: 40px;
cursor: pointer;
}
img:hover {
scale: 1.2;
}
}

View File

@ -0,0 +1,62 @@
import { ControlOutlined } from '@ant-design/icons-vue';
import * as turf from '@turf/turf'
// js生成UUID
const generateUUID = ()=>{
var d = new Date().getTime(); //Timestamp
var d2 = (performance && performance.now && (performance.now()*1000)) || 0;
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random() * 16;
if(d > 0) {
r = (d + r)%16 | 0;
d = Math.floor(d/16);
} else {
r = (d2 + r)%16 | 0;
d2 = Math.floor(d2/16);
}
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
}
// js 针对后端post接口参数拼接到url的情况 将json参数转换为 ?key1=value1&key2=value2
const ObjectToUrl = (obj)=>{
let params = "?";
for(let item in obj){
params+=item+"="+obj[item]+"&"
}
return params;
}
// turf获取几何图形的中心
const getGeometryCenter = (geometry)=>{
let coordinates = [];
switch(geometry.geometry.type.toUpperCase()){
case "POINT":
break;
case "MULTIPOINT":
break;
case "LINESTRING":
break;
case "MULTILINESTRING":
break;
case "POLYGON":
coordinates = geometry.geometry.coordinates
break;
case "MULTIPOLYGON":
coordinates = geometry.geometry.coordinates[0]
break;
}
// let polygon = turf.polygon(coordinates);
// let center = turf.centerOfMass(polygon);
return [coordinates[0][0][0],coordinates[0][0][1]];
// return [117.732878836452,35.1320944773393]
}
export { generateUUID,ObjectToUrl,getGeometryCenter }

View File

@ -19,7 +19,6 @@
>
{{ btnText ? btnText : t('component.cropper.selectImage') }}
</a-button>
<CropperModal
@register="register"
@upload-success="handleUploadSuccess"

View File

@ -24,7 +24,12 @@
<div :class="`${prefixCls}-toolbar`">
<Upload :fileList="[]" accept="image/*" :beforeUpload="handleBeforeUpload">
<Tooltip :title="t('component.cropper.selectImage')" placement="bottom">
<a-button size="small" preIcon="ant-design:upload-outlined" type="primary" />
<a-button
size="small"
preIcon="ant-design:upload-outlined"
:icon="h(UploadOutlined)"
type="primary"
/>
</Tooltip>
</Upload>
<Space>
@ -32,6 +37,7 @@
<a-button
type="primary"
preIcon="ant-design:reload-outlined"
:icon="h(ReloadOutlined)"
size="small"
:disabled="!src"
@click="handlerToolbar('reset')"
@ -41,6 +47,7 @@
<a-button
type="primary"
preIcon="ant-design:rotate-left-outlined"
:icon="h(RotateLeftOutlined)"
size="small"
:disabled="!src"
@click="handlerToolbar('rotate', -45)"
@ -50,6 +57,7 @@
<a-button
type="primary"
preIcon="ant-design:rotate-right-outlined"
:icon="h(RotateRightOutlined)"
size="small"
:disabled="!src"
@click="handlerToolbar('rotate', 45)"
@ -59,6 +67,7 @@
<a-button
type="primary"
preIcon="vaadin:arrows-long-h"
:icon="h(ColumnWidthOutlined)"
size="small"
:disabled="!src"
@click="handlerToolbar('scaleX')"
@ -68,6 +77,7 @@
<a-button
type="primary"
preIcon="vaadin:arrows-long-v"
:icon="h(ColumnHeightOutlined)"
size="small"
:disabled="!src"
@click="handlerToolbar('scaleY')"
@ -77,6 +87,7 @@
<a-button
type="primary"
preIcon="ant-design:zoom-in-outlined"
:icon="h(ZoomInOutlined)"
size="small"
:disabled="!src"
@click="handlerToolbar('zoom', 0.1)"
@ -86,6 +97,7 @@
<a-button
type="primary"
preIcon="ant-design:zoom-out-outlined"
:icon="h(ZoomOutOutlined)"
size="small"
:disabled="!src"
@click="handlerToolbar('zoom', -0.1)"
@ -113,7 +125,7 @@
<script lang="ts" setup>
import type { CropendResult, Cropper } from './typing';
import { ref, PropType } from 'vue';
import { ref, PropType, h, watch } from 'vue';
import CropperImage from './Cropper.vue';
import { Space, Upload, Avatar, Tooltip } from 'ant-design-vue';
import { useDesign } from '@/hooks/web/useDesign';
@ -121,6 +133,16 @@
import { dataURLtoBlob } from '@/utils/file/base64Conver';
import { isFunction } from '@/utils/is';
import { useI18n } from '@/hooks/web/useI18n';
import {
UploadOutlined,
ReloadOutlined,
RotateLeftOutlined,
ZoomOutOutlined,
ZoomInOutlined,
RotateRightOutlined,
ColumnHeightOutlined,
ColumnWidthOutlined,
} from '@ant-design/icons-vue';
type apiFunParams = { file: Blob; name: string; filename: string };
@ -134,11 +156,17 @@
src: { type: String },
size: { type: Number },
});
console.log(props.src);
const src = ref(props.src || '');
watch(
() => props.src,
(v: string) => {
src.value = v;
},
);
const emit = defineEmits(['uploadSuccess', 'uploadError', 'register']);
let filename = '';
const src = ref(props.src || '');
const previewSource = ref('');
const cropper = ref<Cropper>();
let scaleX = 1;

View File

@ -29,6 +29,8 @@ import ApiTreeSelect from './components/ApiTreeSelect.vue';
import ApiCascader from './components/ApiCascader.vue';
import ApiTransfer from './components/ApiTransfer.vue';
import { BasicUpload, ImageUpload,VideoUpload,FileUpload } from '@/components/Upload';
import {Location } from '@/components/Map'
import {AddressDetails} from '@/components/AddressDetails'
import { StrengthMeter } from '@/components/StrengthMeter';
import { IconPicker } from '@/components/Icon';
import { CountdownInput } from '@/components/CountDown';
@ -47,6 +49,8 @@ componentMap.set('AutoComplete', AutoComplete);
componentMap.set('ImageUpload', ImageUpload);
componentMap.set("VideoUpload",VideoUpload);
componentMap.set("FileUpload",FileUpload);
componentMap.set("Location",Location);
componentMap.set("AddressDetails",AddressDetails);
componentMap.set('Select', Select);
componentMap.set('ApiSelect', ApiSelect);
componentMap.set('ApiTree', ApiTree);

View File

@ -0,0 +1,279 @@
<template>
<div v-if="tableData.ifShow">
<div style="display: flex; margin-bottom: 10px">
<div style="margin-left: 10px">
<a-radio-group
:disabled="tableData.componentProps.disabled"
v-model:value="noTitleKey"
:options="tableData.componentProps.options"
@change="onTabChange($event, tableData.field)"
/>
</div>
</div>
<!-- <a-card
style="width: 100%"
v-for="(item,index) in tableData.componentProps.options"
:key="index"
v-show="noTitleKey === item.value"
:title="item.label"
> -->
<a-card style="width: 100%">
<BasicForm ref="cardRef" @register="registerForm" @click="changeForm" @change="changeForm">
<template #CardGroup>
<template v-if="Object.keys(childItem).length > 0">
<CardGourp
:data="childItem"
:parentValue="props.parentValue"
:formData="props.formData"
v-if="childItem"
:callModal="props.callModal"
/>
</template>
</template>
</BasicForm>
</a-card>
</div>
</template>
<script lang="ts" setup>
import { BasicForm, useForm } from '@/components/Form';
import { CardGourp } from './index';
import { ref, watch, nextTick } from 'vue';
import { subTableStore } from '@/store/modules/subTable';
import dayjs from 'dayjs';
const emits = defineEmits(['changeRadioVal']);
const subTableDataStore = subTableStore();
const props = defineProps({
data: {
type: Object,
default: () => ({}),
},
parentValue: {
type: String,
default: () => '',
},
formData: {
type: Object,
default: () => ({}),
},
callModal: {
type: Boolean,
default: () => false,
},
});
const noTitleKey = ref(0);
const formColumns = ref([]);
const tabListNoTitle: any = ref([]);
const tableData = props.data;
const childGourp = ref([]);
const childItem = ref({});
let cardFormData = props.formData;
const nowTime = ref(dayjs().format('YYYY-MM-DD HH:mm:ss'));
const userName = localStorage.getItem('fireUserLoginName');
const cardRef = ref<any>();
if (props.callModal) {
tableData.ifShow = true;
}
const [
registerForm,
{ getFieldsValue, setFieldsValue, updateSchema, resetFields, validate, clearValidate },
] = useForm({
labelWidth: 100,
schemas: formColumns.value,
showActionButtonGroup: false,
baseColProps: { lg: 24, md: 24 },
});
watch(
() => subTableDataStore.getToSetGroupData,
() => {
if (subTableDataStore.getToSetGroupData) {
if (Object.keys(subTableDataStore.getGroupData).includes(tableData.field)) {
noTitleKey.value = subTableDataStore.getGroupData[tableData.field];
if (tableData.ifShow) {
onTabChange({ target: { value: noTitleKey.value } }, tableData.field);
}
} else {
noTitleKey.value = tableData.componentProps.options[0].value;
if (tableData.ifShow) {
onTabChange({ target: { value: noTitleKey.value } }, tableData.field);
}
}
}
},
);
watch(
() => props.formData,
(newVal) => {
cardFormData = {...newVal};
let groupData = subTableDataStore.getGroupData;
Object.keys(groupData).forEach((item) => {
if(groupData[item]){
cardFormData[item] = groupData[item];
}
})
formColumns.value.forEach((element) => {
for (const key in cardFormData) {
if (element.field == key) {
var obj = {};
obj[key] = cardFormData[key];
subTableDataStore.setGroupData(obj);
}
}
});
setTimeout(() => {
setFieldsValue(subTableDataStore.getGroupData);
}, 10);
},
{ deep: true },
);
const onTabChange = (event, field) => {
clearValidate();
let clearGroupDataKey = []
let value = event.target.value;
subTableDataStore.setOneGroupData(field, value);
noTitleKey.value = value;
var currentIndex = (childGourp.value || []).findIndex((element) => element.index === value);
if (currentIndex == -1) {
childItem.value = {};
} else {
childItem.value = childGourp.value[currentIndex];
}
formColumns.value.forEach((element) => {
if (element.component === 'CardGroup') {
element.slot = 'CardGroup';
}
element.show = false;
updateSchema([{ field: element.field, show: false }]);
if (element.index == value) {
element.show = true;
if (element.requiredString) {
element.itemProps.required = true;
updateSchema([{ field: element.field, itemProps: { required: true } }]);
}
updateSchema([{ field: element.field, show: true }]);
} else {
clearGroupDataKey.push(element.field)
element.itemProps.required = false;
// delete element.itemProps.required;
clearValidate(element.field);
updateSchema([{ field: element.field, itemProps: { required: false } }]);
// updateSchema([{ field: element.field, itemProps: element.itemProps }]);
}
});
subTableDataStore.clearGroupDataKeyList(clearGroupDataKey)
setTimeout(() => {
resetFields();
setFieldsValue(subTableDataStore.getGroupData);
}, 10);
emits('changeRadioVal');
};
if (tableData.componentProps) {
tableData.componentProps.options.forEach((element, index) => {
tabListNoTitle.value.push({
key: index,
tab: element.label,
...element,
index: index,
});
element.children.forEach((childElement) => {
formColumns.value.push({
parentValue: element.field,
...childElement,
show: index == 0 ? true : false,
index: element.value,
requiredString: childElement.itemProps.required,
});
if (childElement.component == 'CardGroup') {
childGourp.value.push({
...childElement,
index: element.value,
});
}
if (['createuser', 'modifyuser'].includes(childElement.type)) {
var obj = {};
obj[childElement.field] = userName;
subTableDataStore.setGroupData(obj);
}
if (['createtime', 'modifytime'].includes(childElement.type)) {
var obj = {};
obj[childElement.field] = nowTime.value;
subTableDataStore.setGroupData(obj);
}
});
});
nextTick(() => {
if (Object.keys(subTableDataStore.getGroupData).includes(tableData.field)) {
noTitleKey.value = subTableDataStore.getGroupData[tableData.field];
if (tableData.ifShow) {
onTabChange({ target: { value: noTitleKey.value } }, tableData.field);
}
} else {
noTitleKey.value = tableData.componentProps.options[0].value;
if (tableData.ifShow) {
onTabChange({ target: { value: noTitleKey.value } }, tableData.field);
}
}
});
}
if (tableData.ifShow) {
setTimeout(() => {
resetFields();
}, 10);
}
function changeForm() {
setTimeout(() => {
let data = getFieldsValue();
console.log(data,'changeForm');
let result = {};
Object.keys(data).forEach((key) => {
if (key.indexOf('card_group') == -1) {
result[key] = data[key];
}
});
subTableDataStore.setGroupData(result);
}, 500);
}
// pinia
// file_upload
function submitChangeFrom() {
setTimeout(() => {
let data = getFieldsValue();
let oldDefaultGroupData = subTableDataStore.getOldDefaultGroupData;
let result = {};
Object.keys(data).forEach((key) => {
if (key.indexOf('_select') !== -1 || key.indexOf('_upload') !== -1) {
if(oldDefaultGroupData[key] != data[key]){
result[key] = data[key];
}
}
});
console.log(result,'submitChangeFrom')
subTableDataStore.setGroupData(result);
}, 100)
}
async function verify() {
const data = await validate()
.then(async (data) => {
return true;
})
.catch(async (err) => {
console.log(err);
if (err.errorFields.length > 0) {
return false;
} else {
return true;
}
});
return data;
}
defineExpose({
verify,
changeForm,
submitChangeFrom,
});
</script>

View File

@ -0,0 +1,3 @@
export { default as FormViewer } from './index.vue';
export { default as SubTable } from './subTable.vue';
export { default as CardGourp } from './cardGourp.vue';

View File

@ -0,0 +1,629 @@
<template>
<div class="my-form-viewer">
<div v-show="tabsColumns.length > 1">
<a-tabs v-model:activeKey="activeTabsKey" style="width: 100%" @change="tabsChange">
<a-tab-pane v-for="(colItem, index) in tabsColumns" :tab="colItem.label" :key="index">
<BasicForm :ref="`tabsFormRef${index}`" @register="registerForm" :key="index">
<template #CardGroup>
<CardGourp
v-if="cardGroupData.length > 0 && cardGroupData[index]"
:data="cardGroupData[index]"
:formData="cardGourpFormData"
:parentValue="cardGroupData[index].field"
ref="groupRef"
@changeRadioVal="radioVal"
/>
</template>
</BasicForm>
<subTable ref="subTableRef" :data="subTableColumns[index]" :tabsKey="tabsKey" />
<template v-for="(item, useIndex) in createOrModifyList[index]" :key="useIndex">
<CreateOrModifyComponent :data="item" />
</template>
</a-tab-pane>
</a-tabs>
</div>
<BasicForm
ref="formRef"
@register="registerForm"
v-if="formModalVisible && tabsColumns.length < 1"
>
<template #CardGroup>
<CardGourp
v-if="cardGroupData.length > 0"
:data="cardGroupData[0]"
:formData="cardGourpFormData"
:parentValue="cardGroupData[0].field"
ref="groupRef"
@changeRadioVal="radioVal"
/>
</template>
</BasicForm>
<!-- 设计子表 -->
<subTable v-if="formModalVisible && tabsColumns.length < 1" :data="subTableColumns[0]" />
<!-- 卡片 -->
<template v-for="(item, index) in cardLayout" :key="index">
<a-row style="width: 100%">
<a-col :span="item?.colProps?.span || 24" style="padding: 10px">
<a-card
:title="item.label"
:class="
item.shadow === 'always' ? 'card-always' : item.shadow === 'hover' ? 'card-hover' : ''
"
>
<template v-for="(childItem, childIndex) in item.columns[0].children" :key="childIndex">
<a-row style="width: 100%; margin-bottom: 10px">
<a-col :span="childItem?.colProps?.span || 24">
<CallModalCardFormItem
:data="childItem"
:dataKey="item.field"
:values="cardValues"
/>
</a-col>
</a-row>
</template>
</a-card>
</a-col>
</a-row>
</template>
<!-- todo 创建/修改 /时间 -->
<template v-if="tabsColumns.length < 1">
<template v-for="(item, index) in createOrModifyList[0]" :key="index">
<CreateOrModifyComponent :data="item" />
</template>
</template>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref, defineProps, defineEmits, getCurrentInstance, nextTick } from 'vue';
import { BasicForm, useForm } from '@/components/Form';
import { functionGetFormDataFormScheme, LoadFormScheme } from '@/api/demo/formScheme';
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons-vue';
import CallModalCardFormItem from '@/views/demo/onlineform/formCall/CallModalFormItem/index.vue';
import CreateOrModifyComponent from '@/views/demo/onlineform/formCall/CreateOrModifyComponent/index.vue';
import { SubTable, CardGourp } from './index';
import { v4 as uuidv4 } from 'uuid';
import { cardNestStructure } from '@/views/demo/onlineform/util.ts';
import { subTableStore } from '@/store/modules/subTable';
import dayjs from 'dayjs';
const { proxy } = getCurrentInstance();
const emit = defineEmits(['getFormSuccess']);
const subTableDataStore = subTableStore();
const tabsFormRef0 = ref();
const tabsFormRef1 = ref();
const tabsFormRef2 = ref();
const tabsFormRef3 = ref();
const tabsFormRef4 = ref();
const formRef = ref();
const activeTabsKey = ref();
let formColumns: FormSchema[] = [];
const props = defineProps({
formConfig: Object,
processId: String,
formVerison: String,
formRelationId: String,
flowFormData: Object,
instanceInfo: Object,
issueId: String,
isDetail: Boolean,
});
const subTableId = ref(null);
const subTableColumns: any = ref([]);
const subTableDB = ref([]);
const cardLayout = ref([]);
const cardValues = ref({});
const createOrModifyList = ref([]);
const formModalVisible = ref(false);
const tabsColumns: any = ref([]);
const infoUseSubTableData = ref();
const infoUseMainTableData = ref({});
const tabsKey = ref(0);
const keyValue = ref('');
const FieldsValue = ref({});
const subTableRef = ref<any>();
const cardGroupData = ref([]);
const cardGourpFormData = ref({});
subTableDataStore.clearGoupData();
const nowTime = ref(dayjs().format('YYYY-MM-DD HH:mm:ss'));
const userName = localStorage.getItem('fireUserLoginName');
const getCardLayoutKey = (dataList, key) => {
dataList.forEach((item) => {
if (item.component === 'Card') {
getCardLayoutKey(item.columns[0].children, key);
} else if (item.type === 'subTable') {
cardValues.value[key][item.field] = [];
} else {
cardValues.value[key][item.field] = '';
}
});
};
const [
registerForm,
{ getFieldsValue, setFieldsValue, updateSchema, resetFields, validate, clearValidate },
] = useForm({
labelWidth: 100,
schemas: formColumns,
showActionButtonGroup: false,
actionColOptions: {
span: 24,
},
});
async function getFormHistory() {
const data = await LoadFormScheme({
schemeId: props.formVerison,
});
if (data) {
const scheme = JSON.parse(data.scheme);
scheme.formInfo.tabList.forEach((tabElement, index) => {
createOrModifyList.value.push([]);
tabElement.schemas.forEach((element) => {
//
props.formConfig.forEach((configElement) => {
if (configElement.field == element.field) {
element.componentProps.disabled = !configElement.disabled;
element.ifShow = configElement.ifShow;
if (configElement.required) {
element.itemProps.required = configElement.required;
}
}
if (
props.isDetail &&
['createuser', 'createtime', 'modifyuser', 'modifytime'].includes(element.type)
) {
element.ifShow = true;
element.component = 'Input';
element.componentProps.disabled = true;
}
if (element.columns) {
element.columns.forEach((child) => {
child.children.forEach((t) => {
if (configElement.field == t.field) {
t.componentProps.disabled = !configElement.disabled;
t.ifShow = configElement.ifShow;
if (configElement.required) {
t.itemProps.required = configElement.required;
}
}
if (
props.isDetail &&
['createuser', 'createtime', 'modifyuser', 'modifytime'].includes(t.type)
) {
t.ifShow = true;
t.component = 'Input';
t.componentProps.disabled = true;
}
});
});
}
});
});
});
subTableColumns.value = [];
// card
scheme.formInfo.tabList = cardNestStructure(scheme.formInfo.tabList);
subTableDB.value = scheme.db;
let disDetail = false;
let tableColumns = [];
scheme.formInfo.tabList.forEach((tabElement, index) => {
subTableColumns.value.push({
indexValue: index,
parentFileId: '',
child: [],
multiterm: '',
});
tabElement.schemas.forEach((element) => {
if (element.field == props.formRelationId) {
keyValue.value = element.componentProps.fieldName;
getFormDetail(scheme.formInfo.tabList);
disDetail = true;
}
if (element.component === 'InputGuid') {
element.ifShow = false;
}
//
if (element.rules !== undefined) {
let myString = element.rules[0].pattern;
const lastCharacter = myString.charAt(myString.length - 1);
if (lastCharacter === 'i' || lastCharacter === 's') {
element.rules[0].pattern = new RegExp(
element.rules[0].pattern.slice(1, -2),
lastCharacter,
);
} else {
element.rules[0].pattern = new RegExp(element.rules[0].pattern.slice(1, -1));
}
}
//
if (element.component === 'Card') {
cardLayout.value.push(element);
cardValues.value[element.field] = {};
getCardLayoutKey(element.columns[0].children, element.field);
}
if (['createuser', 'createtime', 'modifyuser', 'modifytime'].includes(element.type)) {
createOrModifyList.value[index].push(element);
}
//
if (element.type === 'subTable') {
subTableId.value = element.field;
subTableColumns.value[index].parentFileId = element.field;
(subTableColumns.value[index].multiterm = element.componentProps.multiterm),
tableColumns.push({
parentFileId: element.field,
child: [],
dataTable: element.componentProps.dataTable,
});
element.columns.forEach((itemColumn) => {
itemColumn.children.forEach((itemColumnChild) => {
subTableColumns.value[index].child.push({
key: itemColumnChild.field,
title: itemColumnChild.label,
dataIndex: itemColumnChild.field,
...itemColumnChild,
width: 120,
});
});
});
}
//
if (element.component === 'CardGroup') {
element.slot = 'CardGroup';
if (cardGroupData.value.length !== index) {
cardGroupData.value.push(null);
}
cardGroupData.value.push(element);
// cardGroupData.value.push({
// ...element,
// });
}
//
if (element.component === 'Grid' && element.label === '栅格布局') {
element.columns.forEach((itemColumn) => {
itemColumn.children.forEach((itemColumnChild) => {
itemColumnChild.colProps.span = itemColumn.span;
formColumns.push({
parentValue: index,
...itemColumnChild,
show: index == 0 ? true : false,
});
});
});
}
formColumns.push({
parentValue: index,
...element,
show: index == 0 ? true : false,
});
});
//
if (tabElement.text != '') {
tabsColumns.value.push({
label: tabElement.text,
value: index,
children: tabElement.schemas,
});
}
});
formModalVisible.value = true;
setTimeout(() => {
subTableDataStore.setTableData(tableColumns);
resetFields();
if (!disDetail) {
if (props.flowFormData) {
if (props.flowFormData.mapGeom) {
props.flowFormData.MapGeom = props.flowFormData.mapGeom;
}
console.log('flowFormDataAfter', props.flowFormData);
setFieldsValue(props.flowFormData);
}
}
}, 10);
}
}
function tabsChange(e) {
tabsKey.value = e;
formColumns.forEach((element) => {
element.show = false;
if (element.parentValue == e) {
element.show = true;
}
});
//
// const values = await validate();
const values = getFieldsValue();
if (Object.keys(FieldsValue.value).length == 0) {
FieldsValue.value = values;
}
for (const key in values) {
for (const fieKey in FieldsValue.value) {
if (key == fieKey) {
if (values[key] != undefined) {
FieldsValue.value[key] = values[key];
}
}
}
}
setTimeout(() => {
setFieldsValue(FieldsValue.value);
clearValidate();
}, 10);
}
async function getFormDetail(element) {
var instance = props.instanceInfo;
const querys = {
id: props.formVerison,
key: keyValue.value,
keyValue: instance.pkeyValue,
};
const data = await functionGetFormDataFormScheme(querys);
let obj = new Object();
for (var i in data) {
subTableDB.value.forEach((element) => {
if (element.type == 'chlid') {
subTableDataStore.getTableData.forEach((element) => {
if (element.dataTable == i) {
subTableDataStore.setSingleData(element.parentFileId, data[i]);
}
});
} else {
for (var j in data[i]) {
Object.assign(obj, data[i][j]);
}
}
});
}
cardGourpFormData.value = obj;
let cardGroupKeys = [];
element.forEach((tabItem) => {
tabItem.schemas.forEach((elItem) => {
if (elItem.component == 'CardGroup' && elItem.field.indexOf('guid') == -1) {
cardGroupKeys = cardGroupGetKey(elItem, cardGroupKeys);
}
});
});
let setGroupDataObj = {};
Object.keys(obj).forEach((itemKey) => {
if (cardGroupKeys.includes(itemKey)) {
setGroupDataObj[itemKey] = obj[itemKey];
}
});
subTableDataStore.setGroupData(setGroupDataObj);
subTableDataStore.setOldDefaultGroupData(setGroupDataObj);
subTableDataStore.setToSetGroupData();
FieldsValue.value = obj;
setFieldsValue({
...obj,
});
if (subTableDB.value.length > 1) {
let childTableName = subTableDB.value.find((item) => item.type === 'chlid').name;
let mainTableName = subTableDB.value.find((item) => item.type === 'main').name;
infoUseSubTableData.value = data[childTableName].map((item) => {
return {
...item,
key: uuidv4(),
};
});
data[mainTableName].forEach((item) => {
infoUseMainTableData.value = { ...infoUseMainTableData.value, ...item };
});
}
if (Object.keys(cardValues.value).length > 0) {
Object.keys(cardValues.value).forEach((cardItem) => {
let cardItemKeyList = Object.keys(cardValues.value[cardItem]);
Object.keys(infoUseMainTableData.value).forEach((item) => {
if (cardItemKeyList.includes(item)) {
cardValues.value[cardItem][item] = infoUseMainTableData.value[item];
}
});
// todo
cardItemKeyList.forEach((item) => {
if (item.indexOf('grid') !== -1) {
cardValues.value[cardItem][item] = infoUseSubTableData.value;
}
});
});
}
let mainTable = subTableDB.value.find((item) => item.type === 'main').name;
emit('getFormSuccess', data[mainTable][0]);
}
const groupRef = ref();
const cardGroupGetKey = (element, resultList) => {
resultList.push(element.field);
element.componentProps.options.forEach((groupItem) => {
groupItem.children.forEach((childItem) => {
resultList.push(childItem.field);
if (childItem.component == 'CardGroup' && childItem.field.indexOf('guid') == -1) {
resultList = cardGroupGetKey(childItem, resultList);
}
});
});
return resultList;
};
async function getForm() {
try {
//
if (groupRef.value) {
if (groupRef.value.length > 0) {
for (let index = 0; index < groupRef.value.length; index++) {
await groupRef.value[index].submitChangeFrom();
if (!(await groupRef.value[index].verify())) {
return false;
}
}
} else {
await groupRef.value.submitChangeFrom();
if (!(await groupRef.value.verify())) {
return false;
}
}
}
let resultObj = {};
let values;
switch (activeTabsKey.value) {
case 0:
resultObj = proxy.$refs.tabsFormRef0[0].getFieldsValue();
proxy.$refs.tabsFormRef0[0].setFieldsValue({
...resultObj,
...subTableDataStore.getGroupData,
});
await proxy.$refs.tabsFormRef0[0].validate();
values = proxy.$refs.tabsFormRef0[0].getFieldsValue();
break;
case 1:
resultObj = proxy.$refs.tabsFormRef1[0].getFieldsValue();
proxy.$refs.tabsFormRef1[0].setFieldsValue({
...resultObj,
...subTableDataStore.getGroupData,
});
await proxy.$refs.tabsFormRef1[0].validate();
values = proxy.$refs.tabsFormRef1[0].getFieldsValue();
console.log('resultValue111', JSON.parse(JSON.stringify(values)));
break;
case 2:
resultObj = proxy.$refs.tabsFormRef2[0].getFieldsValue();
proxy.$refs.tabsFormRef2[0].setFieldsValue({
...resultObj,
...subTableDataStore.getGroupData,
});
await proxy.$refs.tabsFormRef2[0].validate();
values = proxy.$refs.tabsFormRef2[0].getFieldsValue();
break;
case 3:
resultObj = proxy.$refs.tabsFormRef3[0].getFieldsValue();
proxy.$refs.tabsFormRef3[0].setFieldsValue({
...resultObj,
...subTableDataStore.getGroupData,
});
await proxy.$refs.tabsFormRef3[0].validate();
values = proxy.$refs.tabsFormRef3[0].getFieldsValue();
break;
case 4:
resultObj = proxy.$refs.tabsFormRef4[0].getFieldsValue();
proxy.$refs.tabsFormRef4[0].setFieldsValue({
...resultObj,
...subTableDataStore.getGroupData,
});
await proxy.$refs.tabsFormRef4[0].validate();
values = proxy.$refs.tabsFormRef4[0].getFieldsValue();
break;
case 5:
resultObj = proxy.$refs.tabsFormRef5[0].getFieldsValue();
proxy.$refs.tabsFormRef5[0].setFieldsValue({
...resultObj,
...subTableDataStore.getGroupData,
});
await proxy.$refs.tabsFormRef5[0].validate();
values = proxy.$refs.tabsFormRef5[0].getFieldsValue();
break;
case 6:
resultObj = proxy.$refs.tabsFormRef6[0].getFieldsValue();
proxy.$refs.tabsFormRef6[0].setFieldsValue({
...resultObj,
...subTableDataStore.getGroupData,
});
await proxy.$refs.tabsFormRef6[0].validate();
values = proxy.$refs.tabsFormRef6[0].getFieldsValue();
break;
default:
resultObj = getFieldsValue();
setFieldsValue({
...resultObj,
...subTableDataStore.getGroupData,
});
values = await validate();
break;
}
for (const key in values) {
for (const fieKey in FieldsValue.value) {
if (key == fieKey) {
if (values[key] == null || values[key] == undefined) {
values[key] = FieldsValue.value[key];
}
}
}
}
let cardValueList = Object.values(cardValues.value);
cardValueList.forEach((item) => {
values = { ...values, ...item };
});
let query = values;
//
if (subTableDataStore.getTableData.length > 0) {
subTableDataStore.getTableData.forEach((item) => {
query[item.parentFileId] = JSON.stringify(item.child);
});
}
//
setTimeout(() => {
if (Object.keys(subTableDataStore.getGroupData).length > 0) {
for (const key in subTableDataStore.getGroupData) {
query[key] = subTableDataStore.getGroupData[key];
}
}
}, 100);
//
if (createOrModifyList.value.length > 0) {
createOrModifyList.value.forEach((childTab) => {
childTab.forEach((item) => {
if (!item.componentProps.disabled) {
if (item.type == 'createuser' || item.type == 'modifyuser') {
query[item.field] = userName;
} else if (item.type == 'createtime' || item.type == 'modifytime') {
query[item.field] = nowTime.value;
}
}
});
});
}
console.log(query);
return query;
} catch (error) {
console.error(error);
return false;
}
}
defineExpose({
getForm,
});
onMounted(() => {
if (props.flowFormData) {
FieldsValue.value = props.flowFormData;
}
if (props.formVerison) {
getFormHistory();
}
// tabsFormRef.value && tabsFormRef.value.clearValidate();
// formRef.value && formRef.value.clearValidate();
});
function radioVal() {
console.log('radioVal');
clearValidate();
//form
setFieldsValue({
...subTableDataStore.getGroupData,
});
}
</script>
<style lang="less" scoped>
.my-process-designer {
width: 100%;
}
.my-form-viewer {
overflow: auto;
width: 100%;
height: calc(100vh - 350px);
}
::v-deep .ant-tabs .ant-tabs-nav-wrap {
position: fixed;
width: 100%;
z-index: 1;
background-color: @component-background;
border-bottom: 1px solid rgba(5, 5, 5, 0.06);
}
::v-deep .ant-tabs-content-holder {
margin-top: 40px;
}
</style>

View File

@ -0,0 +1,133 @@
<template>
<div v-if="columns.length > 1">
<a-table
class="sub-table"
:columns="columns"
:data-source="tableData"
:pagination="false"
:scroll="scrollValue"
>
<!-- v-if="props.data.multiterm" -->
<template #headerCell="{ column, record }">
<template v-if="column.key === 'setting'">
<PlusOutlined class="icon-button" @click="addListItem" v-if="!isDetail" />
</template>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'setting'">
<DeleteOutlined
class="icon-button"
@click="delListItem(column, record)"
v-if="!isDetail"
/>
</template>
<template v-else>
<FormItem :data="column" :record="record" />
</template>
</template>
</a-table>
<!-- <BasicForm ref="subTableRef" @register="registerForm" @change="changeData" v-else /> -->
</div>
</template>
<script lang="ts" setup>
import { v4 as uuidv4 } from 'uuid';
import { onMounted, ref, watch } from 'vue';
import FormItem from '@/views/demo/onlineform/formCall/ShowFormModal/FormItem/index.vue';
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons-vue';
import { subTableStore } from '@/store/modules/subTable';
import { useMessage } from '@/hooks/web/useMessage';
import { BasicForm, useForm } from '@/components/Form';
import dayjs from 'dayjs';
const { createMessage } = useMessage();
const subTableDataStore = subTableStore();
const scrollValue = ref();
const columns: any = ref([
{
dataIndex: 'setting',
key: 'setting',
fixed: 'left',
width: 60,
},
]);
const tableData: any = ref([]);
const props = defineProps({
data: {
type: Object,
default: () => {},
},
tabsKey: {
type: String,
default: '',
},
});
const nowTime = ref(dayjs().format('YYYY-MM-DD HH:mm:ss'));
const userName = localStorage.getItem('fireUserLoginName');
const [registerForm, { getFieldsValue, setFieldsValue, updateSchema, resetFields, validate }] =
useForm({
labelWidth: 100,
schemas: columns,
showActionButtonGroup: false,
baseColProps: { lg: 24, md: 24 },
});
onMounted(() => {
subTableDataStore.getTableData.forEach((element) => {
if (element.parentFileId == props.data.parentFileId) {
tableData.value = element.child;
}
});
});
props.data.child.forEach((element) => {
if (
!['createuser', 'modifyuser', 'createtime', 'modifytime'].includes(element.type) &&
element.component != 'InputGuid'
) {
columns.value.push({
...element,
});
}
});
watch(
() => tableData.value,
(newVal) => {
subTableDataStore.setSingleData(props.data.parentFileId, newVal);
},
{ deep: true },
);
scrollValue.value = { x: (columns.value.length - 1) * 140, y: 300 };
const addListItem = () => {
if (!props.data.multiterm && tableData.value.length >= 1) {
createMessage.error('单行模式,只允许一条数据!');
return;
}
let keyValue = uuidv4();
let emptyItem = { key: keyValue };
props.data.child.map((item) => {
if (item.component == 'InputGuid') {
emptyItem[item.field] = keyValue;
} else if (item.type == 'createuser' || item.type == 'modifyuser') {
emptyItem[item.field] = userName;
} else if (item.type == 'createtime' || item.type == 'modifytime') {
emptyItem[item.field] = nowTime.value;
} else {
emptyItem[item.field] = '';
}
});
tableData.value.push(emptyItem);
};
const delListItem = (column, record) => {
tableData.value = tableData.value.filter((item) => item.key != record.key);
};
function getData() {
return tableData.value;
}
defineExpose({
getData,
});
function changeData() {
console.log(tableData.value);
}
</script>

View File

@ -0,0 +1,4 @@
import { withInstall } from '@/utils';
import getLocation from './src/components/Location.vue'
export const Location = withInstall(getLocation);

View File

@ -0,0 +1,286 @@
<template>
<div>
<div class="mapContainer" :id="'mapContainer' + mapRandom">
<div class="refresh-button">
<ReloadOutlined @click="onrefresh" />
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import mapboxgl, { Map, Popup } from 'mapbox-gl';
import { ref, toRefs, watch, onMounted } from 'vue';
import { ReloadOutlined } from '@ant-design/icons-vue';
import type { UploadFile, UploadProps } from 'ant-design-vue';
import { Modal, Upload } from 'ant-design-vue';
import { on } from '@/utils/domUtils';
import { UploadRequestOption } from 'ant-design-vue/lib/vc-upload/interface';
import { useMessage } from '@/hooks/web/useMessage';
import { isArray, isFunction, isObject, isString } from '@/utils/is';
import { warn } from '@/utils/log';
import { useI18n } from '@/hooks/web/useI18n';
import { useUploadType } from '../hooks/useUpload';
import { uploadContainerProps } from '../props';
import { isImgTypeByName } from '../helper';
import { UploadResultStatus } from '@/components/Upload/src/types/typing';
import { parse } from 'path';
defineOptions({ name: 'ImageUpload' });
const mapRandom = ref(parseInt(Math.random() * 100000).toString());
const emit = defineEmits(['change', 'update:value', 'delete']);
const props = defineProps({
...uploadContainerProps,
});
const { t } = useI18n();
const { createMessage } = useMessage();
const { accept, helpText, maxNumber, maxSize } = toRefs(props);
const isInnerOperate = ref<boolean>(false);
const { getStringAccept } = useUploadType({
acceptRef: accept,
helpTextRef: helpText,
maxNumberRef: maxNumber,
maxSizeRef: maxSize,
});
const previewOpen = ref<boolean>(false);
const previewImage = ref<string>('');
const previewTitle = ref<string>('');
const fileList = ref<UploadProps['fileList']>([]);
const isLtMsg = ref<boolean>(true);
const isActMsg = ref<boolean>(true);
let map: Map;
onMounted(() => {
mapboxgl.accessToken = "pk.eyJ1IjoieHVqaW5nbGlhbmciLCJhIjoiY2w3bzFzZnZqMjdieTN1cG92N2I1d2huOSJ9.aQqMz4S-cTziUYizIH_gNg"
map = initMap();
map.on('load', function () {
refreshLocation();
});
});
watch(
() => props.value,
(v) => {
if (isInnerOperate.value) {
isInnerOperate.value = false;
return;
}
if (v) {
let value: string[] = [];
if (isArray(v)) {
value = v;
} else {
value.push(v);
}
fileList.value = value.map((item, i) => {
if (item && isString(item)) {
return {
uid: -i + '',
name: item.substring(item.lastIndexOf('/') + 1),
status: 'done',
url: item,
};
} else if (item && isObject(item)) {
return item;
} else {
return;
}
}) as UploadProps['fileList'];
}
},
);
const initMap = () => {
return new mapboxgl.Map({
container: 'mapContainer' + mapRandom.value,
language: 'zh-cmn',
projection: 'equirectangular', // wgs84
style: {
glyphs: 'mapbox://fonts/mapbox/{fontstack}/{range}.pbf',
version: 8,
sources: {
'raster-tiles': {
type: 'raster',
tiles: [
`https://t0.tianditu.gov.cn/DataServer?T=img_w&x={x}&y={y}&l={z}&tk=b6585bc41ee16251dbe6b1af64f375d9`,
],
tileSize: 256,
},
},
layers: [
{
id: 'tdt-img-tiles',
type: 'raster',
source: 'raster-tiles',
minzoom: 0,
maxzoom: 18,
},
],
},
maxZoom: 22,
minZoom: 6,
zoom: 15,
center: [118.298906, 35.135013],
});
};
const refreshLocation = () => {
map.addSource('points', {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
properties: {},
geometry: {
type: 'Point',
coordinates: [118.298906, 35.135013],
},
},
],
},
});
// Add a circle layer
map.addLayer({
id: 'circle',
type: 'circle',
source: 'points',
paint: {
'circle-color': '#409EFF',
'circle-radius': 6,
'circle-stroke-width': 3,
'circle-stroke-color': '#ffffff',
},
});
};
const onrefresh = () => {
createMessage.success(t('component.map.refreshSuccess'));
};
function getBase64<T extends string | ArrayBuffer | null>(file: File) {
return new Promise<T>((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
resolve(reader.result as T);
};
reader.onerror = (error) => reject(error);
});
}
const handlePreview = async (file: UploadFile) => {
console.log('fileEEEEE', file);
if (!file.url && !file.preview) {
file.preview = await getBase64<string>(file.originFileObj!);
}
previewImage.value = file.url || file.preview || '';
previewOpen.value = true;
previewTitle.value =
file.name || previewImage.value.substring(previewImage.value.lastIndexOf('/') + 1);
};
const handleRemove = async (file: UploadFile) => {
if (fileList.value) {
const index = fileList.value.findIndex((item) => item.uid === file.uid);
index !== -1 && fileList.value.splice(index, 1);
const value = getValue();
isInnerOperate.value = true;
emit('change', value);
emit('delete', file);
}
};
const handleCancel = () => {
previewOpen.value = false;
previewTitle.value = '';
};
const beforeUpload = (file: File) => {
const { maxSize, accept } = props;
const { name } = file;
const isAct = isImgTypeByName(name);
if (!isAct) {
createMessage.error(t('component.upload.acceptUpload', [accept]));
isActMsg.value = false;
//
setTimeout(() => (isActMsg.value = true), 1000);
}
const isLt = file.size / 1024 / 1024 > maxSize;
if (isLt) {
createMessage.error(t('component.upload.maxSizeMultiple', [maxSize]));
isLtMsg.value = false;
//
setTimeout(() => (isLtMsg.value = true), 1000);
}
return (isAct && !isLt) || Upload.LIST_IGNORE;
};
async function customRequest(info: UploadRequestOption<any>) {
const { api } = props;
if (!api || !isFunction(api)) {
return warn('upload api must exist and be a function');
}
try {
const res = await props.api?.({
data: {
...(props.uploadParams || {}),
},
file: info.file,
name: props.name,
filename: props.filename,
});
info.onSuccess!(res.data);
const value = getValue();
isInnerOperate.value = true;
emit('change', value);
} catch (e: any) {
console.log(e);
info.onError!(e);
}
}
function getValue() {
const list = (fileList.value || [])
.filter((item) => item?.status === UploadResultStatus.DONE)
.map((item: any) => {
return item?.url || item?.response?.url;
});
return props.multiple ? list : list.length > 0 ? list[0] : '';
}
</script>
<style lang="less">
.ant-upload-select-picture-card i {
color: #999;
font-size: 32px;
}
.ant-upload-select-picture-card .ant-upload-text {
margin-top: 8px;
color: #666;
}
.mapContainer {
width: 100%;
height: 280px;
border-radius: 6px;
}
.refresh-button {
position: absolute;
top: 6px;
right: 6px;
width: 30px;
height: 30px;
border-radius: 4px;
background: #fff;
line-height: 30px;
text-align: center;
z-index: 999;
}
</style>

View File

@ -0,0 +1,139 @@
import type { BasicColumn, ActionItem } from '@/components/Table';
import { FileBasicColumn, FileItem, PreviewFileItem, UploadResultStatus } from '../types/typing';
import { isImgTypeByName } from '../helper';
import { Progress, Tag } from 'ant-design-vue';
import TableAction from '@/components/Table/src/components/TableAction.vue';
import ThumbUrl from './ThumbUrl.vue';
import { useI18n } from '@/hooks/web/useI18n';
const { t } = useI18n();
// 文件上传列表
export function createTableColumns(): FileBasicColumn[] {
return [
{
dataIndex: 'thumbUrl',
title: t('component.upload.legend'),
width: 100,
customRender: ({ record }) => {
const { thumbUrl } = (record as FileItem) || {};
return thumbUrl && <ThumbUrl fileUrl={thumbUrl} />;
},
},
{
dataIndex: 'name',
title: t('component.upload.fileName'),
align: 'left',
customRender: ({ text, record }) => {
const { percent, status: uploadStatus } = (record as FileItem) || {};
let status: 'normal' | 'exception' | 'active' | 'success' = 'normal';
if (uploadStatus === UploadResultStatus.ERROR) {
status = 'exception';
} else if (uploadStatus === UploadResultStatus.UPLOADING) {
status = 'active';
} else if (uploadStatus === UploadResultStatus.SUCCESS) {
status = 'success';
}
return (
<div>
<p class="truncate mb-1 max-w-[280px]" title={text}>
{text}
</p>
<Progress percent={percent} size="small" status={status} />
</div>
);
},
},
{
dataIndex: 'size',
title: t('component.upload.fileSize'),
width: 100,
customRender: ({ text = 0 }) => {
return text && (text / 1024).toFixed(2) + 'KB';
},
},
{
dataIndex: 'status',
title: t('component.upload.fileStatue'),
width: 100,
customRender: ({ text }) => {
if (text === UploadResultStatus.SUCCESS) {
return <Tag color="green">{() => t('component.upload.uploadSuccess')}</Tag>;
} else if (text === UploadResultStatus.ERROR) {
return <Tag color="red">{() => t('component.upload.uploadError')}</Tag>;
} else if (text === UploadResultStatus.UPLOADING) {
return <Tag color="blue">{() => t('component.upload.uploading')}</Tag>;
}
return text || t('component.upload.pending');
},
},
];
}
export function createActionColumn(handleRemove: Function): FileBasicColumn {
return {
width: 120,
title: t('component.upload.operating'),
dataIndex: 'action',
fixed: false,
customRender: ({ record }) => {
const actions: ActionItem[] = [
{
label: t('component.upload.del'),
color: 'error',
onClick: handleRemove.bind(null, record),
},
];
return <TableAction actions={actions} outside={true} />;
},
};
}
// 文件预览列表
export function createPreviewColumns(): BasicColumn[] {
return [
{
dataIndex: 'url',
title: t('component.upload.legend'),
width: 100,
customRender: ({ record }) => {
const { url } = (record as PreviewFileItem) || {};
return isImgTypeByName(url) && <ThumbUrl fileUrl={url} />;
},
},
{
dataIndex: 'name',
title: t('component.upload.fileName'),
align: 'left',
},
];
}
export function createPreviewActionColumn({
handleRemove,
handleDownload,
}: {
handleRemove: Fn;
handleDownload: Fn;
}): BasicColumn {
return {
width: 160,
title: t('component.upload.operating'),
dataIndex: 'action',
fixed: false,
customRender: ({ record }) => {
const actions: ActionItem[] = [
{
label: t('component.upload.del'),
color: 'error',
onClick: handleRemove.bind(null, record),
},
{
label: t('component.upload.download'),
onClick: handleDownload.bind(null, record),
},
];
return <TableAction actions={actions} outside={true} />;
},
};
}

View File

@ -0,0 +1,32 @@
export function checkFileType(file: File, accepts: string[]) {
const newTypes = accepts.join('|');
// const reg = /\.(jpg|jpeg|png|gif|txt|doc|docx|xls|xlsx|xml)$/i;
const reg = new RegExp('\\.(' + newTypes + ')$', 'i');
return reg.test(file.name);
}
export function checkImgType(file: File) {
return isImgTypeByName(file.name);
}
export function isImgTypeByName(name: string) {
return /\.(jpg|jpeg|png|gif|webp)$/i.test(name);
}
export function isVideoTypeByName(name: string) {
return /\.(mp4|mov|avi)$/i.test(name);
}
export function getBase64WithFile(file: File) {
return new Promise<{
result: string;
file: File;
}>((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve({ result: reader.result as string, file });
reader.onerror = (error) => reject(error);
});
}

View File

@ -0,0 +1,61 @@
import { Ref, unref, computed } from 'vue';
import { useI18n } from '@/hooks/web/useI18n';
const { t } = useI18n();
export function useUploadType({
acceptRef,
helpTextRef,
maxNumberRef,
maxSizeRef,
}: {
acceptRef: Ref<string[]>;
helpTextRef: Ref<string>;
maxNumberRef: Ref<number>;
maxSizeRef: Ref<number>;
}) {
// 文件类型限制
const getAccept = computed(() => {
const accept = unref(acceptRef);
if (accept && accept.length > 0) {
return accept;
}
return [];
});
const getStringAccept = computed(() => {
return unref(getAccept)
.map((item) => {
if (item.indexOf('/') > 0 || item.startsWith('.')) {
return item;
} else {
return `.${item}`;
}
})
.join(',');
});
// 支持jpg、jpeg、png格式不超过2M最多可选择10张图片
const getHelpText = computed(() => {
const helpText = unref(helpTextRef);
if (helpText) {
return helpText;
}
const helpTexts: string[] = [];
const accept = unref(acceptRef);
if (accept.length > 0) {
helpTexts.push(t('component.upload.accept', [accept.join(',')]));
}
const maxSize = unref(maxSizeRef);
if (maxSize) {
helpTexts.push(t('component.upload.maxSize', [maxSize]));
}
const maxNumber = unref(maxNumberRef);
if (maxNumber && maxNumber !== Infinity) {
helpTexts.push(t('component.upload.maxNumber', [maxNumber]));
}
return helpTexts.join('');
});
return { getAccept, getStringAccept, getHelpText };
}

View File

@ -0,0 +1,118 @@
import type { PropType } from 'vue';
import { FileBasicColumn } from './types/typing';
import type { Options } from 'sortablejs';
import { Merge } from '@/utils/types';
type SortableOptions = Merge<
Omit<Options, 'onEnd'>,
{
onAfterEnd?: <T = any, R = any>(params: T) => R;
// ...可扩展
}
>;
type ListType = 'text' | 'picture' | 'picture-card';
export const basicProps = {
listType: {
type: String as PropType<ListType>,
default: 'picture-card',
},
helpText: {
type: String as PropType<string>,
default: '',
},
// 文件最大多少MB
maxSize: {
type: Number as PropType<number>,
default: 2,
},
// 最大数量的文件Infinity不限制
maxNumber: {
type: Number as PropType<number>,
default: 1,
},
// 根据后缀,或者其他
accept: {
type: Array as PropType<string[]>,
default: () => [],
},
multiple: {
type: Boolean as PropType<boolean>,
default: false,
},
uploadParams: {
type: Object as PropType<any>,
default: () => ({}),
},
api: {
type: Function as PropType<PromiseFn>,
default: null,
required: true,
},
name: {
type: String as PropType<string>,
default: 'file',
},
filename: {
type: String as PropType<string>,
default: null,
},
fileListOpenDrag: {
type: Boolean,
default: true,
},
fileListDragOptions: {
type: Object as PropType<SortableOptions>,
default: () => ({}),
},
};
export const uploadContainerProps = {
value: {
type: Array as PropType<string[]>,
default: () => [],
},
...basicProps,
showPreviewNumber: {
type: Boolean as PropType<boolean>,
default: true,
},
emptyHidePreview: {
type: Boolean as PropType<boolean>,
default: false,
},
};
export const previewProps = {
value: {
type: Array as PropType<string[]>,
default: () => [],
},
};
export const fileListProps = {
columns: {
type: Array as PropType<FileBasicColumn[]>,
default: null,
},
actionColumn: {
type: Object as PropType<FileBasicColumn>,
default: null,
},
dataSource: {
type: Array as PropType<any[]>,
default: null,
},
openDrag: {
type: Boolean,
default: false,
},
dragOptions: {
type: Object as PropType<SortableOptions>,
default: () => ({}),
},
};

View File

@ -0,0 +1,46 @@
import { BasicColumn } from '@/components/Table';
import { UploadApiResult } from '@/api/sys/model/uploadModel';
export enum UploadResultStatus {
DONE = 'done',
SUCCESS = 'success',
ERROR = 'error',
UPLOADING = 'uploading',
}
export interface FileItem {
thumbUrl?: string;
name: string;
size: string | number;
type?: string;
percent: number;
file: File;
status?: UploadResultStatus;
response?: UploadApiResult;
uuid: string;
}
export interface PreviewFileItem {
url: string;
name: string;
type: string;
}
export interface FileBasicColumn extends Omit<BasicColumn, 'customRender'> {
/**
* Renderer of the table cell. The return value should be a VNode, or an object for colSpan/rowSpan config
* @type Function | ScopedSlot
*/
customRender?: Function;
/**
* Title of this column
* @type any (string | slot)
*/
title: string;
/**
* Display field of the data record, could be set like a.b.c
* @type string
*/
dataIndex: string;
}

View File

@ -147,6 +147,8 @@ import { message } from 'ant-design-vue';
drawTool = new MapboxDraw({
displayControlsDefault: false,
controls: {
point: props.isRead? false: true,
line: props.isRead? false: true,
polygon: props.isRead? false: true, //
trash: props.isRead? false: true //
},

View File

@ -1,65 +1,32 @@
<template>
<div class="map-container">
<div id="mapContainer" class="map-box"></div>
<!-- <div class="map-control">
<img
v-for="(item, index) in nextMapControl"
:key="index"
:src="item.icon"
:title="item.title"
@click="handlerMapControlClick(item.handler)"
/>
<img v-show="nextMapControl.length > 0" @click="handlerUnDraw" src="/del.png" title="清除" />
</div> -->
<!-- layer list -->
<div class="layer-workspace" :style="{'left':showLayerList?'0px':'-356px'}">
<LayerComponent
@changeOpenModal="changeOpenModal"
@changeOpenInsertShpModal="changeOpenInsertShpModal"
:layerList="layerList"
@clearLayerList="handlerClearLayerList"
@handlerLayerShowChange="handlerLayerControler"
@handlerGetTableList="handlerGetTableList"
/>
<div class="close-layer-workspace" @click="changeLayerList">
<MenuFoldOutlined v-if="showLayerList" />
<MenuUnfoldOutlined v-else />
</div>
</div>
<LayerControl @draw="handlerDrawPolygon" :style="{'left':showLayerList?'386px':'20px'}" />
<UseModal v-model:openModal="openModal" @changeOpenModal="changeOpenModal" @handlerAddToLayerList="handlerAddToLayerList" />
<InsertShp v-model:openModal="insertShpModal" />
<!-- <AddLayer v-model:openModal="insertShpModal" /> -->
<!-- table data list -->
<DataListComponent v-if="DataTableList" />
<!-- attribute -->
<RightShowInfo :openModal="openRightInfo" />
<div :id="mapContainerName" class="map-box"></div>
</div>
</template>
<script lang="ts" setup>
import { onMounted, onUnmounted, defineProps, reactive, ref, defineExpose } from 'vue';
import { useMessage } from '@/hooks/web/useMessage';
import { Map, Popup } from 'mapbox-gl';
import heatGeoJson from './lib/data.json'
import {
onMounted,
onUnmounted,
defineProps,
defineEmits,
reactive,
ref,
defineExpose,
watch,
inject,
} from 'vue';
import mapboxgl, { Map, Popup } from 'mapbox-gl';
//
import MapboxDraw from '@mapbox/mapbox-gl-draw';
import { generateUUID, getGeometryCenter } from './src/tool';
import 'mapbox-extensions/dist/index.css';
import U from 'mapbox-gl-utils';
import 'mapbox-gl/dist/mapbox-gl.css';
import * as turf from '@turf/turf';
import './src/index.less';
import { MapboxConfig, MapboxDefaultStyle, MapControlConfig } from './src/config';
import { MP } from './src/MP';
import { DrawingType } from '@/enums/mapEnum';
import {MenuFoldOutlined,MenuUnfoldOutlined} from '@ant-design/icons-vue'
import {
SnapPolygonMode,
SnapPointMode,
@ -67,404 +34,291 @@
SnapModeDrawStyles,
SnapDirectSelect,
} from 'mapbox-gl-draw-snap-mode';
import LayerComponent from './LayerComponent/index.vue';
import LayerControl from './LayerControl/index.vue';
import UseModal from './Modal/index.vue';
import InsertShp from './InsertShp/index.vue';
import AddLayer from './AddLayer/index.vue';
import DataListComponent from './DataListComponent/index.vue';
import RightShowInfo from './RightShowInfo/index.vue';
import { customDrawStyles } from './Styles/Styles';
import { WktToGeojson, GeojsonToWkt } from './src/WktGeojsonTransform';
import { getPolygonCenter } from '@/api/tiankongdi/index'
import { message } from 'ant-design-vue';
const mapContainerName = ref<String>();
mapContainerName.value = 'mapContainer-' + generateUUID();
const props = defineProps(['geoms','id','isRead']);
const emit = defineEmits(['handlerDrawComplete'])
import proj4 from 'proj4';
// let fromProjection = "+proj=tmerc +lat_0=0 +lon_0=117 +k=1 +x_0=500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +type=crs";
let fromProjection = "+proj=tmerc +lat_0=0 +lon_0=117 +k=1 +x_0=39500000 +y_0=0 +ellps=GRS80 +units=m +no_defs +type=crs"
// let toProjection = "+proj=longlat +ellps=GRS80 +no_defs +type=crs"
let toProjection = "+proj=longlat +datum=WGS84 +no_defs +type=crs";
// let coordinates = [39591762.54875996,3908085.3169043385];
let coordinates = [39592654.90645946,3909001.5344353705]
const converted = proj4(fromProjection, toProjection, coordinates);
// alert(converted)
const openModal = ref(false);
const insertShpModal = ref(false);
const openRightInfo = ref(false);
const changeOpenModal = (value) => {
openModal.value = value;
};
const changeOpenInsertShpModal = (value) => {
insertShpModal.value = value;
};
// layer list
const showLayerList = ref<boolean>(false);
const changeLayerList = ()=>{
showLayerList.value = !showLayerList.value;
}
const layerList = ref<[]>([]);
const handlerAddToLayerList = (value) => {
layerList.value.push(value);
console.log("layerList.value",layerList.value);
}
const handlerClearLayerList = () => {
layerList.value = [];
}
// map
interface MapboxOptionsInterface {
mapOptions: mapboxgl.MapboxOptions;
control: DrawingType[];
}
const props = defineProps<MapboxOptionsInterface>();
let nextMapControl: Array<any> = reactive([]);
nextMapControl = props.control
? props.control.map((item) => {
console.log('item::: ', item);
return MapControlConfig[item];
})
: [];
console.log('nextMapControl::: ', nextMapControl);
//
let map: Map;
let popup: Popup;
let drawTool: any;
let clickPoisition: Array<number> = [];
let selectFeature: Object = {};
let mp: any = null;
const { createConfirm, createMessage } = useMessage();
// emit
//
const emit = defineEmits(['mapOnLoad', 'mapDraw']);
let geojson = reactive({
geojson: {},
});
let drawing = ref(false);
let geoms = ref<any>([])
let hasPolygon = false;
onMounted(() => {
mapboxgl.accessToken = MapboxConfig.ACCESS_TOKEN;
map = initMap();
map.on('load', () => {
// map.addLayer({
// 'id': 'wms-test-layer',
// 'type': 'raster',
// 'source': {
// 'type': 'raster',
// 'tiles': [
// // "http://175.27.168.120:8080/geoserver/feixian/wms?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&FORMAT=image%2Fjpeg&TRANSPARENT=true&LAYERS=feixian%3Ayingxiang_17&exceptions=application%2Fvnd.ogc.se_inimage&SRS=EPSG%3A4548&STYLES=&WIDTH=728&HEIGHT=768&BBOX=586801.5388788335%2C3908183.1422136417%2C590270.7583843566%2C3911848.0123377703",
// // "http://175.27.168.120:8080/geoserver/feixian/wms?service=WMS&version=1.1.0&request=GetMap&layers=feixian:yingxiang_17&styles=&bbox={bbox-epsg-3857}&width=256&height=256&srs=EPSG:3857&format=image/png&TRANSPARENT=TRUE"
// "http://60.213.14.14:8060/geoserver/feixian/wms?service=WMS&version=1.1.0&request=GetMap&layers=feixian:yingxiang&styles=&bbox={bbox-epsg-3857}&width=256&height=256&srs=EPSG:3857&format=image/png&TRANSPARENT=TRUE"
// ],
// 'tileSize': 256
// },
// 'paint': {}
// });
map.addSource('radar', {
'type': 'image',
// 'url': 'http://175.27.168.120:8080/geoserver/feixian/wms?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&FORMAT=image%2Fjpeg&TRANSPARENT=true&LAYERS=feixian%3Ayingxiang_17&exceptions=application%2Fvnd.ogc.se_inimage&SRS=EPSG%3A4548&STYLES=&WIDTH=728&HEIGHT=768&BBOX=586801.5388788335%2C3908183.1422136417%2C590270.7583843566%2C3911848.0123377703',
// 'url':"http://175.27.168.120:8080/geoserver/feixian/wms?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&FORMAT=image%2Fjpeg&TRANSPARENT=true&LAYERS=feixian%3Ayingxiang_17&exceptions=application%2Fvnd.ogc.se_inimage&SRS=EPSG%3A4548&STYLES=&WIDTH=727&HEIGHT=768&BBOX=591035.9133569683%2C3908525.08342436%2C591469.2675472646%2C3908982.893941982",
'url':"http://60.213.14.14:8060/geoserver/feixian/wms?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&FORMAT=image%2Fjpeg&TRANSPARENT=true&LAYERS=feixian%3Ayingxiang&exceptions=application%2Fvnd.ogc.se_inimage&SRS=EPSG%3A4527&STYLES=&WIDTH=1024&HEIGHT=1024&BBOX=39591762.54875996%2C3908085.3169043385%2C39592654.90645946%2C3909001.5344353705",
'coordinates': [
// EPSG:4548
// [118.00090749819051,35.30579563806707],
// [118.0057224793592,35.30579563806707],
// [118.0057224793592,35.30170937770571],
// [118.00090749819051,35.30170937770571],
// EPSG:4527
// [118.00884630303432,35.3058545162185],
// [118.01875909202998,35.3058545162185],
// [118.01875909202998,35.29767983003392],
// [118.00884630303432,35.29767983003392],
[118.00884630303432,35.30585451621851],
[118.01875909202998,35.30585451621851],
[118.01875909202998,35.297679830033914],
[118.00884630303432,35.297679830033914],
// [39591762.54875996,3909001.5344353705],
// [39592654.90645946,3909001.5344353705],
// [39592654.90645946,3908085.3169043385],
// [39591762.54875996,3908085.3169043385],
]
});
map.addLayer({
id: 'radar-layer',
'type': 'raster',
'source': 'radar',
'paint': {
'raster-fade-duration': 0
}
});
map.addSource('earthquakes', {
'type': 'geojson',
'data': heatGeoJson
});
map.addLayer(
{
'id': 'earthquakes-heat',
'type': 'heatmap',
'source': 'earthquakes',
// 'maxzoom': 18,
'minzoom':11,
'paint': {
// Increase the heatmap weight based on frequency and property magnitude
'heatmap-weight': [
'interpolate',
['linear'],
['get', 'mag'],
0,
1,
1,
0
],
// Increase the heatmap color weight weight by zoom level
// heatmap-intensity is a multiplier on top of heatmap-weight
'heatmap-intensity': [
'interpolate',
['linear'],
['zoom'],
0,
1,
9,
3
],
// Color ramp for heatmap. Domain is 0 (low) to 1 (high).
// Begin color ramp at 0-stop with a 0-transparancy color
// to create a blur-like effect.
'heatmap-color': [
'interpolate',
['linear'],
['heatmap-density'],
0,
'rgba(33,102,172,0)',
0.2,
'rgb(103,169,207)',
0.4,
'rgb(209,229,240)',
0.6,
'rgb(253,219,199)',
0.8,
'rgb(239,138,98)',
1,
'rgb(178,24,43)'
],
// Adjust the heatmap radius by zoom level
'heatmap-radius': [
'interpolate',['linear'],['zoom'],
3,
6,
9,
20
],
// Transition from heatmap to circle layer by zoom level
'heatmap-opacity': [
'interpolate',
['linear'],
['zoom'],
7,
1,
8,
1
]
}
}
);
//mapbox-gl-utils
// U.init(map);
mp = new MP(map);
emit('mapOnLoad', map);
U.init(map);
mp = new MP(map)
//
handlerInitDrawTool();
map.on('click', (e) => {
console.log('click',e)
clickPoisition = e.lngLat;
});
map.on('draw.selectionchange', (e) => {
handlerCopyToTargetLayer(e);
//
map.on('draw.create', function (e) {
if (hasPolygon) {
drawTool.delete(e.features[0].id);
message.warning("只能绘制一个图斑,请先删除已有的!");
return;
}
hasPolygon = true;
handlerDealFeature(e.features[0]);
});
map.on('draw.update', function (e) {
handlerDealFeature(e.features[0]);
});
map.on('draw.delete', function (e) {
hasPolygon = false;
handlerDeleteFeature(e.features[0]);
});
map.on("draw.selectionchange", (e) => {
e.features.forEach(feature => {
if (feature.properties.user_static) {
drawTool.changeMode("simple_select", { featureIds: [] }); //
}
});
});
let filter = '"RelationId"=\'' + props.id + "'";
getPolygonCenter({ tablename: 'idle_shp', filter: filter }).then(res => {
if(res.length > 0){
try {
let geojson = WktToGeojson(res[0].centroid_point);
map.flyTo({
center: geojson.coordinates,
zoom: 17.2,
bearing: 0,
speed: 1, //
curve: 2, // 线
essential: true,
easing(t) {
//
return t;
},
});
} catch (e) {
console.log(e)
}
}
});
window.handlerCopyFeature = handlerCopyFeature;
});
});
//
//
onUnmounted(() => {
map ? map.remove() : null;
});
//
//
//
const initMap = () => {
return new mapboxgl.Map({
container: 'mapContainer',
container: mapContainerName.value,
language: 'zh-cmn',
projection: 'equirectangular', // wgs84
style: MapboxDefaultStyle,
maxZoom: 22,
minZoom: 1,
zoom:10,
// ...props.mapOptions,
center:[117.984425,35.270654],
maxZoom: 24,
zoom: 10,
pitch: 0,
center: [118.340253, 35.092481],
});
};
//
const handlerInitDrawTool = (feature, bool) => {
geojson.geojson = feature;
const handlerMapControlClick = (handler: string) => {
handler === 'handlerDrawPoint' && handlerDrawPoint();
handler === 'handlerDrawLineString' && handlerDrawLineString();
handler === 'handlerDrawPolygon' && handlerDrawPolygon();
};
//
const handlerDrawPoint = () => {
mp.draw('Point');
mp.on('Point', function (e) {
emit('mapDraw', 'Point', e);
});
};
//线
const handlerDrawLineString = () => {
mp.draw('LineString');
mp.on('LineString', function (e) {
emit('mapDraw', 'LineString', e);
});
};
//
const handlerDrawPolygon = () => {
mp.draw('Polygon');
mp.on('Polygon', function (e) {
emit('mapDraw', 'Polygon', e);
});
};
//
const handlerUnDraw = () => {
mp.deleteDraw();
emit('mapDraw', 'cancel');
};
//
const handlerInitDrawTool = () => {
let drawTool = new MapboxDraw({
modes: {
...MapboxDraw.modes,
draw_point: SnapPointMode,
draw_polygon: SnapPolygonMode,
draw_line_string: SnapLineMode,
direct_select: SnapDirectSelect,
},
// Styling guides
styles: SnapModeDrawStyles,
userProperties: true,
// Config snapping features
snap: true,
snapOptions: {
snapPx: 15, // defaults to 15
snapToMidPoints: true, // defaults to false
snapVertexPriorityDistance: 0.0025, // defaults to 1.25
},
guides: true,
});
// map.addControl(drawTool, 'top-right');
};
//
const handlerCopyToTargetLayer = (e) => {
if (e.features.length > 0) {
if (popup) {
popup.remove();
popup = null;
}
selectFeature = e.features[0];
popup = new mapboxgl.Popup({
closeButton: false,
closeOnClick: false,
});
// popup
popup
.setLngLat(clickPoisition)
.setHTML(
`
<div style="color:#333;padding:3px 12px;cursor:pointer;" type="primary" icon="el-icon-search" onclick="handlerCopyFeature();">复制当前图斑</div>`,
)
.addTo(map);
} else {
popup.remove();
}
};
const handlerCopyFeature = () => {
console.log(selectFeature);
popup.remove();
createMessage.success('复制成功!');
};
const handlerRenderLayer = (layer) => {
console.log('layerInfo', layer);
};
defineExpose({
handlerRenderLayer,
});
const handlerLayerControler = (layerInfo) => {
layerInfo.layer = layerInfo.layer? layerInfo.layer : JSON.parse(layerInfo.style);
if (map.getSource(layerInfo.layer.id)) {
if (layerInfo.checked) {
map.setLayoutProperty(layerInfo.layer.id, 'visibility', 'visible');
} else {
map.setLayoutProperty(layerInfo.layer.id, 'visibility', 'none');
if (drawTool) {
drawTool.deleteAll();
if (feature.features) {
drawTool.set(geojson.geojson);
}
} else {
map.addLayer(layerInfo.layer);
map.on('click', layerInfo.layer.id, function (e) {
handlerPreviewFeatureInfo(e);
drawTool = new MapboxDraw({
displayControlsDefault: false,
controls: {
polygon: props.isRead? false: true, //
trash: props.isRead? false: true //
},
modes: {
...MapboxDraw.modes,
draw_point: SnapPointMode,
draw_polygon: SnapPolygonMode,
draw_line_string: SnapLineMode,
direct_select: SnapDirectSelect,
},
styles: customDrawStyles,
userProperties: true,
snap: true,
snapOptions: {
snapPx: 12, // defaults to 15
snapToMidPoints: true, // defaults to false
snapVertexPriorityDistance: 0.0025, // defaults to 1.25
},
guides: false,
});
map.addControl(drawTool, 'top-right');
setTimeout(() => {
document.querySelector(".mapbox-gl-draw_polygon")?.setAttribute("title", "绘制图斑");
document.querySelector(".mapbox-gl-draw_trash")?.setAttribute("title", "删除图斑");
}, 500);
// let featureList:any = []
props.geoms.forEach(item => {
const geojsonPolygon = WktToGeojson(item.geom)
console.log('geojsonPolygon',geojsonPolygon)
const featureId = `polygon-${generateUUID()}`;
let properties = {}
if(props.isRead){
properties = { user_static: true}
}
const feature = {
id: featureId,
type: "Feature",
properties,
geometry: geojsonPolygon
};
geoms.value.push(feature)
// featureList.push(feature)
hasPolygon = true
drawTool.add(feature);
})
// console.log('featureList',featureList)
// drawTool.add(featureList);
}
drawing.value = true;
};
//
const handlerPreviewFeatureInfo = (e) => {
if (e.features) {
openRightInfo.value = true;
//
const handlerDealFeature = (feature) => {
if(map.getLayer('area-label')){
map.removeLayer('area-label');
map.removeSource('area-label');
}
let area = turf.area(feature);
const centroid = turf.centroid(feature);
let labelText = `${area.toFixed(2)}`;
map.addLayer({
id: `area-label`, // ID
type: "symbol",
source: {
type: "geojson",
data: centroid
},
layout: {
"text-field": labelText, //
"text-size": 14,
"text-anchor": "center"
},
paint: {
"text-color": "#ff0000"
}
});
let existFeature = geoms.value.find((item, index) => {
return item.id == feature.id;
});
if (existFeature) {
//
for (let i = 0; i < geoms.value.length; i++) {
if (geoms.value[i].id == feature.id) {
geoms.value[i] = feature;
}
}
} else {
//
geoms.value.push(feature);
}
//
handlerDrawComplete();
};
//
const handlerDeleteFeature = (feature) => {
if(map.getLayer('area-label')){
map.removeLayer('area-label');
map.removeSource('area-label');
}
for (let i = 0; i < geoms.value.length; i++) {
if (geoms.value[i].id == feature.id) {
geoms.value.splice(i, 1);
}
}
handlerDrawComplete();
};
const handlerDrawComplete = () => {
let arr = [];
geoms.value.forEach((item, index) => {
let wktStr = GeojsonToWkt(item.geometry);
let area = turf.area(item).toFixed(2)
let obj = {
area: area,
geom: wktStr,
};
arr.push(obj);
});
emit('handlerDrawComplete', arr);
};
const DataTableList = ref<Boolean>(false);
const handlerGetTableList = ()=>{
DataTableList.value = true;
}
</script>
<style scoped>
.cloud-query-div {
position: absolute;
top: 50px;
left: 10px;
width: 66px;
height: 66px;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
padding: 5px;
border-radius: 5px;
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1);
user-select: none;
cursor: pointer;
}
.cloud-query-icon {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 8px;
}
.map-container {
width: 100%;
height: 100%;
}
.map-box {
width: 100%;
height: 100%;
}
.layer-control-center {
position: absolute;
top: 15px;
left: 15px;
background: #fff;
border-radius: 8px;
}
.layer-control-center p {
margin: 0px;
}
.layer-control-center .ant-checkbox-wrapper {
}
.draw-control-center {
position: absolute;
padding: 7px;
padding: 8px;
top: 15px;
right: 15px;
background: #ffffff;
@ -473,7 +327,7 @@
.draw-control-center .draw-btn {
float: left;
margin: 0px 6px;
margin: 0px 7px;
padding: 5px;
border-radius: 5px;
}
@ -488,11 +342,10 @@
}
.mapboxgl-ctrl-group {
padding: 10px;
padding: 6px;
border-radius: 12px;
position: relative;
right: 140px;
top: 5px;
right: 0px;
}
.mapbox-gl-draw_ctrl-draw-btn {
width: 20px !important;
@ -501,6 +354,7 @@
}
.mapboxgl-ctrl-top-right {
width: 360px;
}
.mapboxgl-ctrl-group button + button {
@ -552,24 +406,211 @@
width: 100px;
height: 100px;
}
.layer-workspace{
position:absolute;
top:0px;
left:0px;
height:100%;
.jas-ctrl-measure {
position: relative;
top: 6px;
right: 10px;
}
.close-layer-workspace{
.jas-ctrl-measure-item {
height: 22px;
color: rgb(255, 255, 255);
}
.layer-item {
padding: 8px 16px;
}
.layer-item:hover {
background: #c7dcf580;
}
::v-deep .ant-collapse-content-box {
padding: 0px !important;
}
::v-deep .jas-ctrl-extend-desktop-container {
width: 320px !important;
}
.position-by-lnglat {
height: 29px;
background: #fff;
position: absolute;
bottom:20px;
left:355px;
width:30px;
height:30px;
background:#e0e5ed;
z-index:9999;
line-height: 30px;
text-align: center;
border-top-right-radius:4px;
border-bottom-right-radius:4px;
cursor:pointer;
top: 10px;
right: 131px;
border-radius: 3px;
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1);
.to-location {
width: 29px;
height: 29px;
float: left;
background: url(/map/location.png);
background-size: 20px 20px;
background-repeat: no-repeat;
background-position: 4px 5px;
&:hover {
cursor: pointer;
}
}
.picture-azimuth {
width: 29px;
height: 29px;
float: left;
background: url(/map/is_show_picture.png);
background-size: 20px 20px;
background-repeat: no-repeat;
background-position: 4px 5px;
&:hover {
cursor: pointer;
}
}
.picture-azimuth-active {
width: 29px;
height: 29px;
float: left;
background: url(/map/not_show_picture.png);
background-size: 20px 20px;
background-repeat: no-repeat;
background-position: 4px 5px;
&:hover {
cursor: pointer;
}
}
.draw-polygon {
width: 29px;
height: 29px;
float: left;
background: url(/map/draw_polygon.png);
background-size: 20px 20px;
background-repeat: no-repeat;
background-position: 4px 5px;
&:hover {
cursor: pointer;
}
}
.split-line {
width: 29px;
height: 29px;
float: left;
background: url(/map/split_polygon.png);
background-size: 20px 20px;
background-repeat: no-repeat;
background-position: 4px 5px;
&:hover {
cursor: pointer;
}
}
.split-polygon {
width: 29px;
height: 29px;
float: left;
background: url(/map/split_polygon_polygon.png);
background-size: 20px 20px;
background-repeat: no-repeat;
background-position: 4px 5px;
&:hover {
cursor: pointer;
}
}
}
.to-location-input {
padding: 16px;
padding-right: 4px;
width: 418px;
min-height: 60px;
background: #fff;
position: absolute;
top: 48px;
right: 10px;
z-index: 999999;
border-radius: 5px;
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
.location-operation {
width: 100%;
height: 40px;
border-bottom: 1px solid #f1f1f1;
margin-bottom: 12px;
}
.location-item-list-coantienr {
width: 100%;
max-height: 400px;
overflow-y: auto;
.location-item {
line-height: 20px;
margin-bottom: 6px;
}
}
}
.split-panel-item:hover {
cursor: pointer;
color: #999;
}
.cloudqueryNotice {
background: rgba(0, 0, 0, 0.53);
padding: 0px 14px;
border-radius: 6px;
position: fixed;
top: 20px;
right: 5vw;
width: 700px;
color: #fff;
z-index: 10;
.cloudquery-title {
height: 40px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 16px;
}
.cloudquery-left {
display: flex;
align-items: center;
}
.cloudquery-right {
display: flex;
align-items: center;
justify-content: space-around;
width: 130px;
}
img {
width: 34px;
height: 29px;
}
.cloudquery-btn {
display: flex;
justify-content: flex-end;
}
.line {
background: #ededed;
width: 1px;
height: 20px;
}
.anticon.anticon-close {
height: 30px;
}
button {
width: 70px;
height: 26px;
background: linear-gradient(-74deg, #086dec, #0b4bdd);
box-shadow: 3px 4px 5px 1px rgba(13, 13, 13, 0.05);
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
}
.title-box {
margin-left: 10px;
font-size: 14px;
}
}
</style>

View File

@ -0,0 +1,194 @@
<template>
<div class="process-design" :style="'display: flex; height:' + data.height">
<bpmn-process-designer
v-model="data.xmlString"
v-bind="data.controlForm"
keyboard
ref="processDesigner"
:events="[
'element.click',
'connection.added',
'connection.removed',
'shape.removed',
'selection.changed',
]"
@connection-added="connectionAdded"
@connection-removed="connectionRemoved"
@shape-removed="shapeRemoved"
@element-click="elementClick"
@init-finished="initModeler"
@save="onSaveProcess"
:schemeCode="data.schemeCode"
:pageFlow="data.pageFlow"
:pageType="data.pageType"
/>
<!-- 属性面板 -->
<bmpn-process-penal
:bpmn-modeler="data.modeler"
:prefix="data.controlForm.prefix"
class="process-panel"
ref="processPanel"
:schemeCode="data.schemeCode"
:pageView="data.pageView"
:pageType="data.pageType"
/>
</div>
</template>
<script lang="ts" setup>
import { reactive, defineProps, defineEmits, ref, watch } from 'vue';
import './package/theme/index.scss';
//
import { BpmnProcessDesigner, BmpnProcessPenal } from './package/index';
import 'highlight.js/styles/atom-one-dark-reasonable.css';
import { flowStore } from '@/store/modules/flow';
const flowWfDataStore = flowStore();
const emit = defineEmits(['save']);
// Vue.use(vuePlugin);
const props = defineProps({
// code使
schemeCode: {
type: String,
},
// wfData
pageView: String,
// detial
pageType: String,
//
pageFlow: String,
bpmnXml: String,
});
const data = reactive({
height: document.documentElement.clientHeight - 144.5 + 'px;',
xmlString: props.bpmnXml,
modeler: null,
controlForm: {
prefix: 'flowable',
},
element: null,
schemeCode: props.schemeCode,
pageView: props.pageView,
pageType: props.pageType,
pageFlow: props.pageFlow,
});
watch(
() => props.pageView,
(newVal) => {
data.pageView = newVal;
},
);
watch(
() => props.pageFlow,
(newVal) => {
data.pageFlow = newVal;
},
);
if (props.pageType == 'detail') {
data.height = document.documentElement.clientHeight - 294.5 + 'px;';
}
const processPanel = ref<any>();
const processDesigner = ref<any>();
async function getFlow() {
let panel = await processPanel.value.getPanel();
let flow = await processDesigner.value.getFlow();
panel.scheme.flowContent = flow;
return panel;
// return {
// panel:panel,
// flowContent:flow
// };
}
async function validateFlow() {
let res = await processPanel.value.validatePanel();
return res;
}
defineExpose({
getFlow,
validateFlow,
});
function connectionAdded(node) {
console.log('connectionAdded');
data.element = node;
if (node.type == 'bpmn:SequenceFlow') {
const element = {
id: node.id,
type: node.type,
lineConditions: '',
from: node.source.id,
isInit: true,
to: node.target.id,
};
flowWfDataStore.setWfDataNode(element);
}
}
function connectionRemoved(node) {
flowWfDataStore.deleteWfData(node);
}
function shapeRemoved(node) {
flowWfDataStore.deleteWfData(node);
}
function elementClick(element) {
data.element = element;
}
function initModeler(modeler) {
setTimeout(() => {
data.modeler = modeler;
}, 10);
}
function onSaveProcess(saveData) {
emit('save', saveData);
}
</script>
<style lang="scss" scoped>
body {
overflow: auto !important;
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;
}
}
.process-panel {
height: 100%;
}
</style>

View File

@ -0,0 +1,378 @@
<template>
<div class="my-process-designer">
<div class="my-process-designer__header">
<slot name="control-header"></slot>
<template v-if="!$slots['control-header']">
<div slot="content">
<!-- <a-button :type="headerButtonType" :icon="h(SaveOutlined)" @click="onSave" class="ml-2">保存流程 </a-button> -->
<!-- <a-button :type="headerButtonType" @click="previewProcessXML" class="ml-2">预览XML</a-button> -->
<!-- <a-button :type="headerButtonType" @click="previewProcessJson" class="ml-2">预览JSON</a-button> -->
<!-- <a-button :type="headerButtonType" @click="getFlow" class="ml-2">获取json</a-button> -->
<a-space>
<a-tooltip placement="bottom" class="ml-2" title="缩小视图">
<a-button
:disabled="process.defaultZoom <= 0.3"
:icon="h(ZoomOutOutlined)"
@click="processZoomOut()"
/>
</a-tooltip>
<a-button>{{ Math.floor(process.defaultZoom * 10 * 10) + '%' }}</a-button>
<a-tooltip placement="bottom" title="放大视图">
<a-button
:disabled="process.defaultZoom >= 3.9"
:icon="h(ZoomInOutlined)"
@click="processZoomIn()"
/>
</a-tooltip>
<a-tooltip placement="bottom" title="撤销" class="ml-2">
<a-button :icon="h(RotateLeftOutlined)" @click="processUndo()" />
</a-tooltip>
<a-tooltip placement="bottom" title="恢复">
<a-button :icon="h(RotateRightOutlined)" @click="processRedo()" />
</a-tooltip>
<a-tooltip placement="bottom" title="重新绘制">
<a-button :icon="h(ClearOutlined)" @click="processRestart()" />
</a-tooltip>
</a-space>
</div>
</template>
<div> </div>
</div>
<div class="my-process-designer__container">
<div class="my-process-designer__canvas" ref="bpmn-canvas" id="bpmn-canvas"></div>
</div>
<a-modal v-model:open="process.previewModelVisible" width="60%" title="预览">
<highlightjs
:language="process.previewType"
:code="process.previewResult"
style="height: 60vh"
/>
</a-modal>
</div>
</template>
<script lang="ts" setup>
import { h, provide, reactive, onMounted, defineProps, defineEmits, watch, nextTick } from 'vue';
import {
ZoomOutOutlined,
ZoomInOutlined,
RotateLeftOutlined,
RotateRightOutlined,
ClearOutlined,
} from '@ant-design/icons-vue';
//
import BpmnModeler from 'bpmn-js/lib/Modeler';
import DefaultEmptyXML from './plugins/defaultEmpty';
// ()
import customTranslate from './plugins/translate/customTranslate';
import { getDetail } from '@/api/sys/WFSchemeInfo';
import { flowStore } from '@/store/modules/flow';
// json
import convert from 'xml-js';
const flowWfDataStore = flowStore();
const emit = defineEmits([
'init-finished',
'event',
'commandStack-changed',
'input',
'change',
'canvas-viewbox-changed',
'destroy',
'save',
'element-click',
'connection-added',
'connection-removed',
'connection-changed',
]);
interface processType {
defaultZoom: number;
previewModelVisible: boolean;
simulationStatus: boolean;
previewResult: string;
previewType: string;
recoverable: boolean;
revocable: boolean;
bpmnModeler: any;
}
const process: processType = reactive({
defaultZoom: 1,
previewModelVisible: false,
simulationStatus: false,
previewResult: '',
previewType: 'xml',
recoverable: false,
revocable: false,
bpmnModeler: null,
});
const props = defineProps({
processId: {
type: String,
default: '',
},
processName: {
type: String,
default: '',
},
value: String, // xml
prefix: {
type: String,
default: 'flowable',
},
events: {
type: Array,
default: () => ['element.click'],
},
headerButtonSize: {
type: String,
default: 'small',
validator: (value: any) => ['default', 'medium', 'small', 'mini'].indexOf(value) !== -1,
},
headerButtonType: {
type: String,
default: 'primary',
validator: (value: any) =>
['default', 'primary', 'success', 'warning', 'danger', 'info'].indexOf(value) !== -1,
},
schemeCode: String,
pageFlow: String,
pageType: String,
});
watch(
() => props.pageFlow,
(newVal) => {
createNewDiagram(newVal);
},
);
onMounted(() => {
initBpmnModeler();
createNewDiagram(props.value);
if (props.schemeCode) {
getDetailInfo(1);
}
if (props.pageType == 'detail') {
createNewDiagram(props.pageFlow);
}
});
async function getDetailInfo(a) {
let data = await getDetail({ code: props.schemeCode });
if (a == 1) {
createNewDiagram(data.scheme.flowContent);
}
}
// function onSave() {
// return new Promise((resolve, reject) => {
// if (process.bpmnModeler == null) {
// reject();
// }
// process.bpmnModeler.saveXML({ format: true }).then(({ xml }) => {
// emit('save', xml);
// resolve(xml);
// });
// });
// }
function initBpmnModeler() {
if (process.bpmnModeler) return;
process.bpmnModeler = new BpmnModeler({
container: '#bpmn-canvas',
additionalModules: [
{
translate: ['value', customTranslate],
},
],
});
emit('init-finished', process.bpmnModeler);
initModelListeners();
}
function initModelListeners() {
const EventBus = process.bpmnModeler.get('eventBus');
// , . - ,
props.events.forEach((event) => {
EventBus.on(event, function (eventObj) {
provide('wfdesign', eventObj);
let eventName = event.replace('.', '-');
let element = eventObj ? eventObj.element : null;
emit(eventName, element, eventObj);
emit('event', eventName, element, eventObj);
//
// if (
// eventObj.newSelection &&
// eventObj.newSelection.length > 0 &&
// eventObj.newSelection[0].type === 'bpmn:ScriptTask'
// ) {
// let name = eventObj.newSelection[0].di.bpmnElement.name
// ? eventObj.newSelection[0].di.bpmnElement.name
// : '';
// name = '';
// nextTick(function () {
// const modeling = process.bpmnModeler.get('modeling');
// if (eventObj.newSelection[0].type == 'label') return;
// //tasklabel
// //label
// modeling.updateProperties(eventObj.newSelection[0], { name });
// flowWfDataStore.setWfDataName(eventObj.newSelection[0].id, name);
// });
// }
});
});
process.bpmnModeler.on('element.click', ({ element }) => {
if (element) {
flowWfDataStore.setWfDataName(element.di.bpmnElement.id, element.di.bpmnElement.name);
}
});
// xml
EventBus.on('commandStack.changed', async (event) => {
console.log('commandStack.changed');
console.log(event);
await process.bpmnModeler.saveXML({ format: true }).then(({ xml }) => {
const json = JSON.parse(convert.xml2json(xml, { spaces: 2 }));
const flowElements = json.elements[0].elements[0].elements;
console.log(flowElements);
for (var j = 0; j < flowElements.length; j++) {
if (flowElements[j].attributes.name) {
flowWfDataStore.setWfDataName(
flowElements[j].attributes.id,
flowElements[j].attributes.name,
);
}
if (flowElements[j].name == 'bpmn2:sequenceFlow') {
const currentNode = flowWfDataStore.getWfDataNode(flowElements[j].attributes.id);
if (currentNode) {
flowWfDataStore.updataWfDataNode(
flowElements[j].attributes.id,
'from',
flowElements[j].attributes.sourceRef,
);
flowWfDataStore.updataWfDataNode(
flowElements[j].attributes.id,
'to',
flowElements[j].attributes.targetRef,
);
}
}
}
});
// try {
// process.recoverable = process.bpmnModeler.get('commandStack').canRedo();
// process.revocable = process.bpmnModeler.get('commandStack').canUndo();
// let { xml } = await process.bpmnModeler.saveXML({ format: true });
// emit('commandStack-changed', event);
// emit('input', xml);
// emit('change', xml);
// } catch (e) {
// console.error(`[Process Designer Warn]: ${e.message || e}`);
// }
});
//
process.bpmnModeler.on('canvas.viewbox.changed', ({ viewbox }) => {
emit('canvas-viewbox-changed', { viewbox });
const { scale } = viewbox;
process.defaultZoom = Math.floor(scale * 100) / 100;
});
process.bpmnModeler.on('shape.added', (e) => {
let addElement = e.element;
let name = addElement.di.bpmnElement.name ? addElement.di.bpmnElement.name : '';
if (name == '') {
console.log(addElement.type);
switch (addElement.type) {
case 'bpmn:Task':
name = '任务节点';
nextTick(function () {
const modeling = process.bpmnModeler.get('modeling');
if (addElement.type == 'label') return;
//tasklabel
//label
modeling.updateProperties(addElement, { name });
flowWfDataStore.setWfDataName(addElement.id, name);
});
break;
default:
break;
}
}
});
}
/* 创建新的流程图 */
async function createNewDiagram(xml) {
//
let newId = props.processId || `Process_${new Date().getTime()}`;
let newName = props.processName || `业务流程_${new Date().getTime()}`;
let xmlString = xml || DefaultEmptyXML(newId, newName, props.prefix);
try {
let { warnings } = await process.bpmnModeler.importXML(xmlString);
if (warnings && warnings.length) {
warnings.forEach((warn) => console.warn(warn));
}
} catch (e) {
console.error(`[Process Designer Warn]: ${e.message || e}`);
}
}
function processRedo() {
process.bpmnModeler.get('commandStack').redo();
}
function processUndo() {
process.bpmnModeler.get('commandStack').undo();
}
function processZoomIn(zoomStep = 0.1) {
let newZoom = Math.floor(process.defaultZoom * 100 + zoomStep * 100) / 100;
if (newZoom > 4) {
throw new Error('[Process Designer Warn ]: The zoom ratio cannot be greater than 4');
}
process.defaultZoom = newZoom;
process.bpmnModeler.get('canvas').zoom(process.defaultZoom);
}
function processZoomOut(zoomStep = 0.1) {
let newZoom = Math.floor(process.defaultZoom * 100 - zoomStep * 100) / 100;
if (newZoom < 0.2) {
throw new Error('[Process Designer Warn ]: The zoom ratio cannot be less than 0.2');
}
process.defaultZoom = newZoom;
process.bpmnModeler.get('canvas').zoom(process.defaultZoom);
}
function processRestart() {
process.recoverable = false;
process.revocable = false;
createNewDiagram(null).then(() => process.bpmnModeler.get('canvas').zoom(1, 'auto'));
flowWfDataStore.setWfDataAll([]);
}
/*----------------------------- 方法结束 ---------------------------------*/
// function previewProcessXML() {
// // process.bpmnModeler.get('elementRegistry').get(id)
// // idshape
// process.bpmnModeler.saveXML({ format: true }).then(({ xml }) => {
// console.log(xml);
// process.previewResult = xml;
// process.previewType = 'xml';
// process.previewModelVisible = true;
// });
// }
// function previewProcessJson() {
// process.bpmnModeler.saveXML({ format: true }).then(({ xml }) => {
// process.previewResult = convert.xml2json(xml, { spaces: 2 });
// console.log(process.previewResult);
// process.previewType = 'json';
// process.previewModelVisible = true;
// });
// }
async function getFlow() {
let flowContent;
let flowElements;
await process.bpmnModeler.saveXML({ format: true }).then(({ xml }) => {
flowContent = xml;
const json = JSON.parse(convert.xml2json(xml, { spaces: 2 }));
flowElements = json.elements[0].elements[0].elements;
flowWfDataStore.setElments(flowElements);
});
return flowContent;
}
defineExpose({
getFlow,
});
</script>
<style scoped>
::v-deep .bjs-container a {
visibility: hidden;
}
</style>

View File

@ -0,0 +1,7 @@
import BpmnProcessDesigner from './ProcessDesigner.vue';
BpmnProcessDesigner.install = function (Vue) {
Vue.component(BpmnProcessDesigner.name, BpmnProcessDesigner);
};
export default BpmnProcessDesigner;

View File

@ -0,0 +1,27 @@
export default (key, name, type) => {
if (!type) type = "camunda";
const TYPE_TARGET = {
activiti: "http://activiti.org/bpmn",
camunda: "http://bpmn.io/schema/bpmn",
flowable: "http://flowable.org/bpmn"
};
return `
<?xml version="1.0" encoding="UTF-8"?>
<bpmn2:definitions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:bpmn2="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
xmlns:dc="http://www.omg.org/spec/DD/20100524/DC"
xmlns:di="http://www.omg.org/spec/DD/20100524/DI"
xsi:schemaLocation="http://www.omg.org/spec/BPMN/20100524/MODEL BPMN20.xsd"
id="diagram_${key}"
targetNamespace="${TYPE_TARGET[type]}">
<bpmn2:process id="${key}" name="${name}" isExecutable="true">
</bpmn2:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="${key}">
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn2:definitions>
`;
};

View File

@ -0,0 +1,9 @@
import translations from './zh.ts'
export default function customTranslate(template, replacements) {
replacements = replacements || {}
template = translations[template] || template
return template.replace(/{([^}]+)}/g, function (_, key) {
return replacements[key] || '{' + key + '}'
})
}

View File

@ -0,0 +1,251 @@
export default {
'Activate hand tool': '激活抓手工具',
'Activate lasso tool': '激活套索工具',
'Activate create/remove space tool': '激活创建/删除空间工具',
'Activate global connect tool': '激活全局连接工具',
'Create start event': '创建开始事件',
'Create end event': '创建结束事件',
'Create task': '创建任务',
'Create user task': '创建用户任务',
'Create gateway': '创建网关',
'Create data object reference': '创建数据对象',
'Create data store reference': '创建数据存储',
'Create group': '创建分组',
'Create intermediate/boundary event': '创建中间/边界事件',
'Create expanded sub-process': '创建扩展子过程',
'Create pool/participant': '创建池/参与者',
'Change element': '修改类型',
Delete: '移除',
'Append end event': '追加结束事件',
'Append gateway': '追加网关',
'Append task': '追加任务',
'Append intermediate/boundary event': '追加中间抛出事件/边界事件',
'Add text annotation': '添加 text annotation',
'Connect using association': '使用关联连接',
'Connect to other element': '消息关联',
'Start event': '开始事件',
'End event': '结束事件',
'Message intermediate catch event': '消息中间捕获事件',
'Message intermediate throw event': '消息中间抛出事件',
'Timer intermediate catch event': '定时中间捕获事件',
'Escalation intermediate throw event': '升级中间抛出事件',
'Conditional intermediate catch event': '条件中间捕获事件',
'Link intermediate catch event': '链接中间捕获事件',
'Link intermediate throw event': '链接中间抛出事件',
'Compensation intermediate throw event': '补偿中间抛出事件',
'Signal intermediate catch event': '信号中间捕获事件',
'Signal intermediate throw event': '信号中间抛出事件',
Task: '任务',
'User task': '用户任务',
'Service task': '服务任务',
'Send task': '发送任务',
'Receive task': '、接收任务',
'Manual task': '手动任务',
'Business rule task': '业务规则任务',
'Script task': '脚本任务',
'Call activity': '调用活动',
'Sub-process (collapsed)': '子流程(已折叠)',
'Sub-process (expanded)': '子流程(扩大)',
'Intermediate throw event': '中间抛出事件',
'Message start event': '消息开始事件',
'Timer start event': '定时开始事件',
'Conditional start event': '条件开始事件',
'Signal start event': '信号开始事件',
'Exclusive gateway': '排他网关',
'Parallel gateway': '并行网关',
'Inclusive gateway': '包容网关',
'Complex gateway': '复杂网关',
'Event-based gateway': '事件网关',
'Message end event': '消息结束事件',
'Escalation end event': '升级结束事件',
'Error end event': '错误结束事件',
'Compensation end event': '补偿结束事件',
'Signal end event': '信号结束事件',
'Terminate end event': '终止结束事件',
Process: '业务流程',
'Append EndEvent': '追加结束事件',
'Append Gateway': '追加网关',
'Append Task': '追加任务',
'Append Intermediate/Boundary Event': '追加中间抛出事件/边界事件',
'Activate the global connect tool': '激活全局连接工具',
'Append {type}': '添加 {type}',
'Add Lane above': '在上面添加道',
'Divide into two Lanes': '分割成两个道',
'Divide into three Lanes': '分割成三个道',
'Add Lane below': '在下面添加道',
'Append compensation activity': '追加补偿活动',
'Change type': '修改类型',
'Connect using Association': '使用关联连接',
'Connect using Sequence/MessageFlow or Association': '使用顺序/消息流或者关联连接',
'Connect using DataInputAssociation': '使用数据输入关联连接',
Remove: '移除',
'Activate the hand tool': '激活抓手工具',
'Activate the lasso tool': '激活套索工具',
'Activate the create/remove space tool': '激活创建/删除空间工具',
'Create expanded SubProcess': '创建扩展子过程',
'Create IntermediateThrowEvent/BoundaryEvent': '创建中间抛出事件/边界事件',
'Create Pool/Participant': '创建池/参与者',
'Parallel Multi Instance': '并行多重事件',
'Sequential Multi Instance': '时序多重事件',
DataObjectReference: '数据对象参考',
DataStoreReference: '数据存储参考',
Loop: '循环',
'Ad-hoc': '即席',
'Create {type}': '创建 {type}',
'Create StartEvent': '创建开始事件',
'Create EndEvent': '创建结束事件',
'Create Task': '创建任务',
'Create User Task': '创建用户任务',
'Create Gateway': '创建网关',
'Create DataObjectReference': '创建数据对象',
'Create DataStoreReference': '创建数据存储',
'Create Group': '创建分组',
'Create Intermediate/Boundary Event': '创建中间/边界事件',
'Message Start Event': '消息开始事件',
'Timer Start Event': '定时开始事件',
'Conditional Start Event': '条件开始事件',
'Signal Start Event': '信号开始事件',
'Error Start Event': '错误开始事件',
'Escalation Start Event': '升级开始事件',
'Compensation Start Event': '补偿开始事件',
'Message Start Event (non-interrupting)': '消息开始事件(非中断)',
'Timer Start Event (non-interrupting)': '定时开始事件(非中断)',
'Conditional Start Event (non-interrupting)': '条件开始事件(非中断)',
'Signal Start Event (non-interrupting)': '信号开始事件(非中断)',
'Escalation Start Event (non-interrupting)': '升级开始事件(非中断)',
'Message Intermediate Catch Event': '消息中间捕获事件',
'Message Intermediate Throw Event': '消息中间抛出事件',
'Timer Intermediate Catch Event': '定时中间捕获事件',
'Escalation Intermediate Throw Event': '升级中间抛出事件',
'Conditional Intermediate Catch Event': '条件中间捕获事件',
'Link Intermediate Catch Event': '链接中间捕获事件',
'Link Intermediate Throw Event': '链接中间抛出事件',
'Compensation Intermediate Throw Event': '补偿中间抛出事件',
'Signal Intermediate Catch Event': '信号中间捕获事件',
'Signal Intermediate Throw Event': '信号中间抛出事件',
'Collapsed Pool': '折叠池',
'Expanded Pool': '展开池',
'no parent for {element} in {parent}': '在{parent}里,{element}没有父类',
'no shape type specified': '没有指定的形状类型',
'flow elements must be children of pools/participants': '流元素必须是池/参与者的子类',
'out of bounds release': 'out of bounds release',
'more than {count} child lanes': '子道大于{count} ',
'element required': '元素不能为空',
'diagram not part of bpmn:Definitions': '流程图不符合bpmn规范',
'no diagram to display': '没有可展示的流程图',
'no process or collaboration to display': '没有可展示的流程/协作',
'element {element} referenced by {referenced}#{property} not yet drawn':
'由{referenced}#{property}引用的{element}元素仍未绘制',
'already rendered {element}': '{element} 已被渲染',
'failed to import {element}': '导入{element}失败',
Id: '编号',
Name: '名称',
General: '常规',
Details: '详情',
'Message Name': '消息名称',
Message: '消息',
Initiator: '创建者',
'Asynchronous Continuations': '持续异步',
'Asynchronous Before': '异步前',
'Asynchronous After': '异步后',
'Job Configuration': '工作配置',
Exclusive: '排除',
'Job Priority': '工作优先级',
'Retry Time Cycle': '重试时间周期',
Documentation: '文档',
'Element Documentation': '元素文档',
'History Configuration': '历史配置',
'History Time To Live': '历史的生存时间',
Forms: '表单',
'Form Key': '表单key',
'Form Fields': '表单字段',
'Business Key': '业务key',
'Form Field': '表单字段',
ID: '编号',
Type: '类型',
Label: '名称',
'Default Value': '默认值',
'Default Flow': '默认流转路径',
'Conditional Flow': '条件流转路径',
'Sequence Flow': '普通流转路径',
Validation: '校验',
'Add Constraint': '添加约束',
Config: '配置',
Properties: '属性',
'Add Property': '添加属性',
Value: '值',
Listeners: '监听器',
'Execution Listener': '执行监听',
'Event Type': '事件类型',
'Listener Type': '监听器类型',
'Java Class': 'Java类',
Expression: '表达式',
'Must provide a value': '必须提供一个值',
'Delegate Expression': '代理表达式',
Script: '脚本',
'Script Format': '脚本格式',
'Script Type': '脚本类型',
'Inline Script': '内联脚本',
'External Script': '外部脚本',
Resource: '资源',
'Field Injection': '字段注入',
Extensions: '扩展',
'Input/Output': '输入/输出',
'Input Parameters': '输入参数',
'Output Parameters': '输出参数',
Parameters: '参数',
'Output Parameter': '输出参数',
'Timer Definition Type': '定时器定义类型',
'Timer Definition': '定时器定义',
Date: '日期',
Duration: '持续',
Cycle: '循环',
Signal: '信号',
'Signal Name': '信号名称',
Escalation: '升级',
Error: '错误',
'Link Name': '链接名称',
Condition: '条件名称',
'Variable Name': '变量名称',
'Variable Event': '变量事件',
'Specify more than one variable change event as a comma separated list.':
'多个变量事件以逗号隔开',
'Wait for Completion': '等待完成',
'Activity Ref': '活动参考',
'Version Tag': '版本标签',
Executable: '可执行文件',
'External Task Configuration': '扩展任务配置',
'Task Priority': '任务优先级',
External: '外部',
Connector: '连接器',
'Must configure Connector': '必须配置连接器',
'Connector Id': '连接器编号',
Implementation: '实现方式',
'Field Injections': '字段注入',
Fields: '字段',
'Result Variable': '结果变量',
Topic: '主题',
'Configure Connector': '配置连接器',
'Input Parameter': '输入参数',
Assignee: '代理人',
'Candidate Users': '候选用户',
'Candidate Groups': '候选组',
'Due Date': '到期时间',
'Follow Up Date': '跟踪日期',
Priority: '优先级',
'The follow up date as an EL expression (e.g. ${someDate} or an ISO date (e.g. 2015-06-26T09:54:00)':
'跟踪日期必须符合EL表达式 ${someDate} ,或者一个ISO标准日期2015-06-26T09:54:00',
'The due date as an EL expression (e.g. ${someDate} or an ISO date (e.g. 2015-06-26T09:54:00)':
'跟踪日期必须符合EL表达式 ${someDate} ,或者一个ISO标准日期2015-06-26T09:54:00',
Variables: '变量',
'Candidate Starter Configuration': '候选人起动器配置',
'Candidate Starter Groups': '候选人起动器组',
'This maps to the process definition key.': '这映射到流程定义键。',
'Candidate Starter Users': '候选人起动器的用户',
'Specify more than one user as a comma separated list.': '指定多个用户作为逗号分隔的列表。',
'Tasklist Configuration': 'Tasklist配置',
Startable: '启动',
'Specify more than one group as a comma separated list.': '指定多个组作为逗号分隔的列表。',
'Execution listeners': '执行监听器',
};

View File

@ -0,0 +1,5 @@
const hljs = require("highlight.js/lib/core");
hljs.registerLanguage("xml", require("highlight.js/lib/languages/xml"));
hljs.registerLanguage("json", require("highlight.js/lib/languages/json"));
export default hljs;

View File

@ -0,0 +1,7 @@
import BpmnProcessDesigner from "./designer/index.ts";
import BmpnProcessPenal from "./penal";
export {
BpmnProcessDesigner,
BmpnProcessPenal
}

View File

@ -0,0 +1,591 @@
<template>
<div :class="prefixCls" style="width: 30%">
<a-tabs v-model:activeKey="configActiveName">
<a-tab-pane :tab="data.wfNodeName" key="1">
<!-- 开始节点 -->
<start-event-option
ref="startRef"
:element="data.currentWfNode"
:pageType="props.pageType"
:schemeCode="props.schemeCode"
:class="data.currentWfNode.type == 'bpmn:StartEvent' ? '' : 'hidden'"
/>
<!-- 审核节点 -->
<user-task-option
ref="taskRef"
:element="data.currentWfNode"
:pageType="props.pageType"
:class="data.currentWfNode.type == 'bpmn:Task' ? '' : 'hidden'"
/>
<!-- 结束节点 -->
<end-event-option
ref="endRef"
:element="data.currentWfNode"
:pageType="props.pageType"
:class="data.currentWfNode.type == 'bpmn:EndEvent' ? '' : 'hidden'"
/>
<!-- 子流程 -->
<subprocess-option
ref="subprocessRef"
:element="data.currentWfNode"
:pageType="props.pageType"
:class="data.currentWfNode.type == 'bpmn:SubProcess' ? '' : 'hidden'"
/>
<!-- 排他网关 -->
<exclusive-gateway-option
ref="exclusiveGatewayRef"
:element="data.currentWfNode"
:pageType="props.pageType"
:class="data.currentWfNode.type == 'bpmn:ExclusiveGateway' ? '' : 'hidden'"
/>
<!-- 并行网关 -->
<parallel-gateway-option
ref="parallelGatewayRef"
:element="data.currentWfNode"
:pageType="props.pageType"
:class="data.currentWfNode.type == 'bpmn:ParallelGateway' ? '' : 'hidden'"
/>
<!-- 包容网关 -->
<!-- "bpmn:InclusiveGateway" -->
<inclusive-gateway-option
ref="inclusiveGatewayRef"
:element="data.currentWfNode"
:pageType="props.pageType"
:class="data.currentWfNode.type == 'bpmn:InclusiveGateway' ? '' : 'hidden'"
/>
<!-- 线条 -->
<myline-option
ref="mylineRef"
:element="data.currentWfNode"
:pageType="props.pageType"
:class="data.currentWfNode.type == 'bpmn:SequenceFlow' ? '' : 'hidden'"
/>
<!-- 脚本节点 -->
<script-option
ref="scriptRef"
:element="data.currentWfNode"
:pageType="props.pageType"
:class="data.currentWfNode.type == 'bpmn:ScriptTask' ? '' : 'hidden'"
/>
</a-tab-pane>
<a-tab-pane tab="流程属性" key="2">
<shcemeinfo-config ref="shcemeinfoRef" :disabled="true" :pageType="props.pageType" />
</a-tab-pane>
<a-tab-pane key="3" tab="发起权限" force-render v-if="props.pageType != 'detail'">
<auth-config ref="authRef" />
</a-tab-pane>
</a-tabs>
</div>
</template>
<script lang="ts" setup>
import { reactive, onMounted, defineProps, watch, ref } from 'vue';
import { useDesign } from '@/hooks/web/useDesign';
import { lr_AESEncrypt } from '../utils';
import { useMessage } from '@/hooks/web/useMessage';
import { getDetail } from '@/api/sys/WFSchemeInfo';
//
//
//
//
//
//
//
//
//
// 线
import {
shcemeinfoConfig,
authConfig,
StartEventOption,
userTaskOption,
endEventOption,
parallelGatewayOption,
exclusiveGatewayOption,
inclusiveGatewayOption,
subprocessOption,
mylineOption,
scriptOption,
} from './page';
import { flowStore } from '@/store/modules/flow';
const flowWfDataStore = flowStore();
const { prefixCls } = useDesign('process-property');
const { createMessage } = useMessage();
const props = defineProps({
bpmnModeler: Object,
prefix: {
type: String,
default: 'camunda',
},
width: {
type: Number,
default: 520,
},
element: Object,
schemeCode: String,
pageType: String,
pageView: String,
});
watch(
() => props.pageView,
(newVal) => {
var content = JSON.parse(newVal);
shcemeinfoRef.value.setForm(content);
flowWfDataStore.setWfDataAll(content.wfData);
},
);
const startRef = ref<any>();
const shcemeinfoRef = ref<any>();
const authRef = ref<any>();
const taskRef = ref<any>();
const endRef = ref<any>();
const subprocessRef = ref<any>();
const exclusiveGatewayRef = ref<any>();
const parallelGatewayRef = ref<any>();
const inclusiveGatewayRef = ref<any>();
const configActiveName = ref('2');
interface dataType {
currentWfNode: any;
wfNodeName: string;
wfData: any;
activeTab: string;
elementId: string;
elementType: string;
elementBusinessObject: any; // businessObject 使
conditionFormVisible: boolean; //
formVisible: boolean; //
}
const data: dataType = reactive({
currentWfNode: undefined,
wfNodeName: '',
wfData: [],
activeTab: 'base',
elementId: '',
elementType: '',
elementBusinessObject: {}, // businessObject 使
conditionFormVisible: false, //
formVisible: false, //
});
function initModels() {
setTimeout(() => {
getActiveElement();
}, 10);
}
function getActiveElement() {
// bpmn:Process
initFormOnChanged(null);
props.bpmnModeler.on('import.done', (e) => {
initFormOnChanged(null);
});
//
props.bpmnModeler.on('selection.changed', ({ newSelection }) => {
initFormOnChanged(newSelection[0] || null);
});
props.bpmnModeler.on('element.changed', ({ element }) => {
// ""
if (element && element.id === data.elementId) {
initFormOnChanged(element);
}
});
}
//
function initFormOnChanged(element) {
if (element == null) {
configActiveName.value = '2';
data.wfNodeName = '';
data.currentWfNode = {
type: '',
};
return;
}
if (element.type == 'bpmn:Process') {
data.currentWfNode = {
type: element.type,
};
configActiveName.value = '2';
} else {
data.currentWfNode = element;
configActiveName.value = '1';
setNodeData(element.type, element);
}
}
//
function setNodeData(name, element) {
switch (name) {
case 'bpmn:StartEvent':
data.wfNodeName = '开始节点';
data.currentWfNode = {
id: element.id,
type: element.type,
isNextAuditor: false,
isCustmerTitle: false,
formRelations: [],
formType: '1',
formCode: '',
formVerison: undefined,
formRelationId: undefined,
formUrl: '',
formAppUrl: '',
authFields: [],
messageType: '',
isInit: true,
formTitle: '',
issueCode: '',
mapConfig:{},
};
break;
case 'bpmn:EndEvent':
data.wfNodeName = '结束节点';
data.currentWfNode = {
id: element.id,
type: element.type,
};
break;
case 'bpmn:Task':
data.wfNodeName = '审核节点';
data.currentWfNode = {
id: element.id,
type: element.type,
isAddSign: false,
isTransfer: false,
isBatchAudit: false,
IsSingleTask: false,
autoAgree: '',
noAuditor: '1',
rejectType: '1',
messageType: '',
auditUsers: [],
lookUsers: [],
formRelations: [],
formType: '1',
formCode: '',
formTitle: '',
formVerison: undefined,
formRelationId: undefined,
formUrl: '',
formAppUrl: '',
authFields: [],
name: '审核节点',
btnlist: [
{
code: 'agree',
name: '同意',
hidden: false,
isNextAuditor: false,
isSign: false,
isSys: true,
},
{
code: 'disagree',
name: '驳回',
hidden: false,
isNextAuditor: false,
isSign: false,
isSys: true,
},
],
isCountersign: false,
isCountersignAll: false,
countersignAgian: '1',
countersignType: '1',
countersignAllType: 100,
isOvertimeMessage: false,
overtimeMessageType: '',
overtimeMessageStart: 1,
overtimeMessageInterval: 1,
overtimeGo: 12,
isInherit: true,
isInit: true,
issueCode: '',
};
break;
case 'bpmn:SubProcess':
data.wfNodeName = '子流程';
data.currentWfNode = {
id: element.id,
type: element.type,
isAsync: false,
wfschemeId: '',
wfVersionId: '',
isInit: true,
};
break;
case 'bpmn:ExclusiveGateway':
data.wfNodeName = '排他网关';
data.currentWfNode = {
id: element.id,
type: element.type,
conditions: [],
isInit: true,
};
break;
case 'bpmn:ParallelGateway':
data.wfNodeName = '并行网关';
data.currentWfNode = {
id: element.id,
type: element.type,
isInit: true,
};
break;
case 'bpmn:InclusiveGateway':
data.wfNodeName = '包含网关';
data.currentWfNode = {
id: element.id,
type: element.type,
conditions: [],
isInit: true,
};
break;
case 'bpmn:SequenceFlow':
data.wfNodeName = '线条';
data.currentWfNode = {
id: element.id,
type: element.type,
lineConditions: '',
from: element.businessObject.sourceRef.id,
isInit: true,
to: element.businessObject.targetRef.id,
};
break;
case 'bpmn:ScriptTask':
data.wfNodeName = '脚本节点';
data.currentWfNode = {
id: element.id,
type: element.type,
isInit: true,
executeType: '1',
sqlDb: '',
sqlStr: '',
sqlStrRevoke: '',
apiUrl: '',
apiUrlRevoke: '',
ioc: '',
iocRevoke: '',
};
break;
default:
break;
}
// flowStore
flowWfDataStore.setWfDataNode(data.currentWfNode);
}
async function getDetailInfo() {
let data = await getDetail({ code: props.schemeCode });
let scheme = JSON.parse(data.scheme.content);
let baseinfo = {
id: data.schemeinfo.id,
code: data.schemeinfo.code,
name: data.schemeinfo.name,
category: data.schemeinfo.category,
enabledMark: data.schemeinfo.enabledMark,
mark: data.schemeinfo.mark,
isInApp: data.schemeinfo.isInApp,
description: data.schemeinfo.description,
icon: data.schemeinfo.icon,
color: data.schemeinfo.color,
schemeId: data.schemeinfo.schemeId,
undoType: scheme.undoType,
undoDbCode: scheme.undoDbCode,
undoDbSQL: scheme.undoDbSQL,
undoIOCName: scheme.undoIOCName,
undoUrl: scheme.undoUrl,
deleteType: scheme.deleteType,
deleteDbCode: scheme.deleteDbCode,
deleteDbSQL: scheme.deleteDbSQL,
deleteIOCName: scheme.deleteIOCName,
deleteUrl: scheme.deleteUrl,
deleteDraftType: scheme.deleteDraftType,
deleteDraftDbCode: scheme.deleteDraftDbCode,
deleteDraftDbSQL: scheme.deleteDraftDbSQL,
deleteDraftIOCName: scheme.deleteDraftIOCName,
deleteDraftUrl: scheme.deleteDraftUrl,
};
let wfData = scheme.wfData;
flowWfDataStore.setWfDataAll(wfData);
let auth = {
authType: data.schemeinfo.authType,
authData: data.schemeAuthList.map((t) => {
return {
id: t.objId,
name: t.objName,
type: t.objType,
};
}),
};
shcemeinfoRef.value.setForm(baseinfo);
authRef.value.setForm(auth);
}
async function validatePanel() {
let res = await shcemeinfoRef.value.validateForm();
if (!res) {
configActiveName.value = '2';
}
return res;
}
async function getPanel() {
// 1.
let baseinfo = await shcemeinfoRef.value.getForm();
// 2.
let auth = await authRef.value.getForm();
let wfData = flowWfDataStore.getWfData;
var startIndex = (wfData || []).findIndex(
(element: { type?: string }) => element.type == 'bpmn:StartEvent',
);
if (startIndex == -1) {
return createMessage.warn('请设置开始节点');
}
var endIndex = (wfData || []).findIndex(
(element: { type?: string }) => element.type == 'bpmn:EndEvent',
);
if (endIndex == -1) {
return createMessage.warn('请设置结束节点');
}
wfData.forEach(
(node: {
type?: string;
lineConditions?: string;
from?: string;
messageType?: any;
overtimeMessageType?: any;
}) => {
if (node.type == 'bpmn:SequenceFlow') {
if (node.lineConditions == '') {
const fromNode:
| {
type?: string;
btnlist?: any;
}
| any = wfData.find((t: { id: string }) => t.id === node.from);
if (fromNode.type == 'bpmn:Task') {
if (fromNode.btnlist.findIndex((t) => t.code == 'agree' && !t.hidden) != -1) {
node.lineConditions = 'agree'; //
}
}
}
}
if (node.type == 'bpmn:StartEvent') {
if (node.messageType instanceof Array) {
if (node.messageType.length == 1) {
node.messageType = node.messageType[0];
} else {
node.messageType = node.messageType.join(',');
}
}
}
if (node.type == 'bpmn:Task') {
if (node.overtimeMessageType instanceof Array) {
if (node.overtimeMessageType.length == 1) {
node.overtimeMessageType = node.overtimeMessageType[0];
} else {
node.overtimeMessageType = node.overtimeMessageType.join(',');
}
}
if (node.messageType instanceof Array) {
if (node.messageType.length == 1) {
node.messageType = node.messageType[0];
} else {
node.messageType = node.messageType.join(',');
}
}
}
},
);
let scheme = {
wfData: wfData,
undoType: baseinfo.undoType,
undoDbCode: baseinfo.undoDbCode,
undoDbSQL: baseinfo.undoDbSQL,
undoIOCName: baseinfo.undoIOCName,
undoUrl: baseinfo.undoUrl,
deleteType: baseinfo.deleteType,
deleteDbCode: baseinfo.deleteDbCode,
deleteDbSQL: baseinfo.deleteDbSQL,
deleteIOCName: baseinfo.deleteIOCName,
deleteUrl: baseinfo.deleteUrl,
deleteDraftType: baseinfo.deleteDraftType,
deleteDraftDbCode: baseinfo.deleteDraftDbCode,
deleteDraftDbSQL: baseinfo.deleteDraftDbSQL,
deleteDraftIOCName: baseinfo.deleteDraftIOCName,
deleteDraftUrl: baseinfo.deleteDraftUrl,
};
let dto = {
schemeinfo: {
id: baseinfo.id,
code: baseinfo.code,
name: baseinfo.name,
category: baseinfo.category,
enabledMark: baseinfo.enabledMark,
mark: baseinfo.mark,
isInApp: baseinfo.isInApp,
authType: auth.authType,
description: baseinfo.description,
icon: baseinfo.icon,
color: baseinfo.color,
schemeId: baseinfo.schemeId,
},
schemeAuthList:
auth.authType == 1
? []
: auth.authData.map((t) => {
return {
objId: t.id,
objName: t.name,
objType: t.type,
};
}),
scheme: {
content: lr_AESEncrypt(JSON.stringify(scheme), 'hc'),
},
// scheme:scheme
};
console.log(dto);
return dto;
}
defineExpose({
getPanel,
validatePanel,
});
onMounted(() => {
initModels();
if (props.schemeCode) {
getDetailInfo();
}
if (props.pageType == 'detail') {
var content = JSON.parse(props.pageView);
shcemeinfoRef.value.setForm(content);
flowWfDataStore.setWfDataAll(content.wfData);
}
});
</script>
<style lang="less" scoped>
@prefix-cls: ~'@{namespace}-process-property';
.@{prefix-cls} {
background-color: @component-background;
}
::v-deep .ant-tabs {
height: 100%;
width: 100%;
padding: 0 20px;
}
::v-deep .ant-tabs-content-holder {
overflow-y: auto;
padding-right: 10px;
}
.hidden {
display: none;
}
</style>

View File

@ -0,0 +1,228 @@
<template>
<div class="auth-config">
<a-tabs v-model:activeKey="authType" type="card" size="small">
<a-tab-pane key="1" tab="所有成员">
<a-alert
message="权限说明"
description="所有人员指不限制流程模版的发起人员,表示每个人都能发起该流程模版。"
type="info"
show-icon
/>
</a-tab-pane>
<a-tab-pane key="2" tab="指定成员">
<a-space>
<a-radio-group>
<a-radio-button value="1" @click="handlePostClick"></a-radio-button>
<a-radio-button value="2" @click="handleRoleClick"></a-radio-button>
<a-radio-button value="3" @click="handleAccountClick"></a-radio-button>
</a-radio-group>
<a-button danger @click="clearAll"></a-button>
</a-space>
<a-table
size="small"
:columns="columns"
:data-source="dataSource"
bordered
:pagination="false"
>
<template #bodyCell="{ column, text, record }">
<template v-if="['name'].includes(column.dataIndex)">
<div>
{{ text }}
</div>
</template>
<template v-else-if="['type'].includes(column.dataIndex)">
<div>
{{ typeFormat(text) }}
</div>
</template>
<template v-else-if="column.dataIndex === 'operation'">
<a-popconfirm
v-if="dataSource.length"
title="确定要删除吗?"
@confirm="onDelete(record.name)"
>
<delete-outlined two-tone-color="#eb2f96" />
</a-popconfirm>
</template>
</template>
</a-table>
</a-tab-pane>
</a-tabs>
<a-modal
width="60%"
v-model:open="data.postOpen"
wrap-class-name="full-modal-children"
title="添加岗位"
@ok="postHandleOk"
:destroyOnClose="true"
>
<SelectPos ref="posRef" />
</a-modal>
<a-modal
width="60%"
v-model:open="data.roleOpen"
wrap-class-name="full-modal-children"
title="添加角色"
@ok="roleHandleOk"
:destroyOnClose="true"
>
<SelectRole ref="roleRef" />
</a-modal>
<a-modal
width="60%"
v-model:open="data.accountOpen"
wrap-class-name="full-modal-children"
title="添加角色"
@ok="accountHandleOk"
:destroyOnClose="true"
>
<SelectAccount ref="accountRef" />
</a-modal>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { DeleteOutlined } from '@ant-design/icons-vue';
import { SelectPos } from '@/components/SelectPos/index';
import { SelectRole } from '@/components/SelectRole/index';
import { SelectAccount } from '@/components/SelectAccount/index';
let authType = ref('1');
const columns = [
{
title: '名称',
dataIndex: 'name',
},
{
title: '类型',
dataIndex: 'type',
},
{
title: '操作',
dataIndex: 'operation',
},
];
let dataSource = ref([]);
const data = reactive({
postOpen: false,
roleOpen: false,
accountOpen: false,
});
function typeFormat(type) {
type = type.toString();
switch (type) {
case '1':
return '岗位';
case '2':
return '角色';
case '3':
return '用户';
case '4':
return '上下级';
case '5':
return '节点';
case '6':
return '表字段';
}
}
function clearAll() {
dataSource.value = [];
}
function addTableData(selectData) {
let addData = selectData.filter(
(t: { id: string }) =>
dataSource.value.findIndex((t2: { id: string }) => t2.id == t.id) == -1,
);
dataSource.value = dataSource.value.concat(addData);
}
//
const posRef = ref<any>();
function handlePostClick() {
data.postOpen = true;
}
function postHandleOk() {
let selectData = posRef.value.getRow().map((t) => {
return { type: '1', id: t.id, name: t.name };
});
addTableData(selectData);
data.postOpen = false;
}
//
const roleRef = ref<any>();
function handleRoleClick() {
data.roleOpen = true;
}
function roleHandleOk() {
let selectData = roleRef.value.getRow().map((t) => {
return { type: '2', id: t.id, name: t.name, condition: '' };
});
addTableData(selectData);
data.roleOpen = false;
}
//
const accountRef = ref<any>();
function handleAccountClick() {
data.accountOpen = true;
}
function accountHandleOk() {
let selectData = accountRef.value.getRow().map((t) => {
return { type: '3', id: t.id, name: t.name };
});
addTableData(selectData);
data.accountOpen = false;
}
function onDelete(key) {
dataSource.value = dataSource.value.filter((item: { name: string }) => item.name !== key);
}
function setForm(data) {
authType.value = data.authType.toString();
dataSource.value = data.authData;
}
function getForm() {
return {
authType: authType.value,
authData: dataSource.value,
};
}
defineExpose({
getForm,
setForm,
});
</script>
<style lang="less" scoped>
.site-space-compact-wrapper {
width: 100%;
.ant-select {
width: 100%;
}
}
::v-deep .ant-tabs {
padding: 0 !important;
}
::v-deep .ant-table-content {
margin-top: 10px;
}
</style>
<style lang="less">
.full-modal-children {
.ant-modal {
max-width: 100%;
}
.ant-modal-content {
height: calc(80vh);
}
.ant-modal-body {
height: 80%;
}
}
</style>

View File

@ -0,0 +1,212 @@
<!-- 开始节点配置 -->
<template>
<l-layout :top="180" >
<template #top>
<el-form
class="l-form-config"
label-width="88px"
label-position="left"
size="mini"
>
<el-form-item label="节点标识">
<el-input v-model="node.id" readonly ></el-input>
</el-form-item>
<div style="padding:0 16px;">
<el-alert
title="排他网关说明"
type="info"
description="排他网关不会等待所有分支汇入才往下执行只要有分支汇入就会往下执行出口分支只会执行一条条件为true如果多条出口分支条件为true也执行一条"
show-icon
:closable="false"
>
</el-alert>
</div>
</el-form>
</template>
<l-layout :top="40">
<template #top v-if="!disabled">
<div style="padding-left:8px;float:left;" >
<el-button-group>
<el-button size="mini" icon="el-icon-plus" @click="handleFormulaClick">{{$t('')}}</el-button>
<el-button size="mini" icon="el-icon-plus" @click="handleSQlClick">{{$t('sql')}}</el-button>
</el-button-group>
</div>
<div style="padding-right:8px;float:right;">
<el-button size="mini" type="danger" icon="el-icon-delete" @click="handleClearClick">{{$t('')}}</el-button>
</div>
</template>
<l-table :columns="columns" :dataSource="node.conditions" >
<template v-slot:type="scope" >
{{typeFormat(scope.row.type)}}
</template>
<l-table-btns v-if="!disabled" :btns="tableBtns" @click="handleTableBtnClick" ></l-table-btns>
</l-table>
</l-layout>
<l-dialog
:title="$t('添加公式条件')"
:visible.sync="formulaVisible"
:height="480"
@ok="handleFormulaOk"
@closed="handleFormulaClosed"
@opened="handleFormulaOpened"
>
<condition-formula ref="conditionFormula" ></condition-formula>
</l-dialog>
<l-dialog
:title="$t('添加sql条件')"
:visible.sync="sqlVisible"
:height="480"
@ok="handleSqlOk"
@closed="handleSqlClosed"
@opened="handleSqlOpened"
>
<condition-sql ref="conditionSql" ></condition-sql>
</l-dialog>
</l-layout>
</template>
<script>
import conditionFormula from './conditionFormula.vue'
import conditionSql from './conditionSql.vue'
export default {
name:'gateway-xor-option',
props:{
disabled:{
type:Boolean,
default:false
}
},
components: {
conditionFormula,
conditionSql
},
data () {
return {
tableBtns:[
{prop:'Edit',label:'编辑'},
{prop:'Delete',label:'删除'}
],
columns:[
{label:'类型',prop:'type',width:'80', align: 'center'},
{label:'名称',prop:'name',minWidth:'100'},
],
tableData:[],
formulaVisible:false,
sqlVisible:false,
editRow:null,
isEdit:false,
rowIndex:0,
}
},
computed: {
node(){
return this.wfdesign.currentWfNode;
}
},
inject: ["wfdesign"],
methods:{
typeFormat(type){
switch(type){
case '1':
return '表达式'
case '2':
return 'sql语句'
}
},
handleTableBtnClick(btn){
switch(btn.type){
case 'Edit':
this.isEdit = true;
this.editRow = btn.row;
this.rowIndex = btn.rowIndex;
if(this.editRow.type == '1'){
this.formulaVisible = true;
}
else{
this.sqlVisible = true;
}
break;
case 'Delete':
this.node.conditions.splice(btn.rowIndex,1);
break;
}
console.log(btn);
//this.tableData.splice(btn.rowIndex,1);
},
handleFormulaClick(){
this.isEdit = false;
this.formulaVisible = true;
},
handleSQlClick(){
this.isEdit = false;
this.sqlVisible = true;
},
handleClearClick(){
this.node.conditions = [];
},
handleFormulaOk(){
this.$refs.conditionFormula.validateForm(()=>{
let formData = this.$refs.conditionFormula.getForm();
formData.type = '1';
if(this.isEdit){
this.node.conditions[this.rowIndex] = formData;
}
else{
formData.code = this.$uuid();
this.node.conditions.push(formData);
}
this.formulaVisible = false;
})
},
handleFormulaOpened(){
if(this.isEdit){
this.$refs.conditionFormula.setForm(this.editRow);
}
},
handleFormulaClosed(){
this.$refs.conditionFormula.resetForm();
},
handleSqlOk(){
this.$refs.conditionSql.validateForm(()=>{
let formData = this.$refs.conditionSql.getForm();
formData.type = '2';
if(this.isEdit){
this.node.conditions[this.rowIndex] = formData;
}
else{
formData.code = this.$uuid();
this.node.conditions.push(formData);
}
this.sqlVisible = false;
})
},
handleSqlOpened(){
if(this.isEdit){
this.$refs.conditionSql.setForm(this.editRow);
}
},
handleSqlClosed(){
this.$refs.conditionSql.resetForm();
},
}
}
</script>
<style>
</style>

View File

@ -0,0 +1,124 @@
<!-- 开始节点配置 -->
<template>
<el-form
class="l-form-config"
label-width="88px"
label-position="left"
size="mini">
<el-form-item label="节点标识">
<el-input v-model="node.id" readonly ></el-input>
</el-form-item>
<el-divider>执行操作</el-divider>
<div style="text-align: center;margin-bottom:16px;" >
<el-radio-group v-model="node.executeType" size="mini" :disabled="disabled">
<el-radio-button label="1">执行SQL</el-radio-button>
<el-radio-button label="2">.NET方法</el-radio-button>
<el-radio-button label="3">第三方接口</el-radio-button>
</el-radio-group>
</div>
<template v-if="node.executeType == '1'" >
<div style="padding:0 0 16px 0;">
<el-alert
title="sql参数说明"
type="info"
description="参数有 @processId流程进程主键 @code上一步执行码 @userId流程发起人Id @userAccount流程发起人账号 @companyId流程发起人公司 @departmentId流程发起人部门;
@userId2上一步审核人Id @userAccount2上一步审核人账号 @companyId2上一步审核人公司 @departmentId2上一步审核人部门;
"
show-icon
:closable="false"
>
</el-alert>
</div>
<el-form-item label-width="0">
<el-select v-model="node.sqlDb" placeholder="请选择执行SQL数据库" :disabled="disabled">
<el-option-group
v-for="group in lr_dblinkTree"
:key="group.id"
:label="group.label">
<el-option
v-for="item in group.children"
:key="item.id"
:label="item.label"
:value="item.id">
</el-option>
</el-option-group>
</el-select>
</el-form-item>
<el-form-item label-width="0">
<el-input :readonly="disabled" type="textarea" v-model="node.sqlStr" rows="8" placeholder="请填写SQL语句" ></el-input>
</el-form-item>
<el-form-item label-width="0">
<el-input :readonly="disabled" type="textarea" v-model="node.sqlStrRevoke" rows="8" placeholder="请填写SQL语句撤销" ></el-input>
</el-form-item>
</template>
<template v-if="node.executeType == '2'" >
<div style="padding:0 0 16px 0;">
<el-alert
title="IOC说明"
type="info"
description="注意编写一个继承IWorkFlowMethod的类
"
show-icon
:closable="false"
>
</el-alert>
</div>
<el-form-item label-width="0">
<el-input :readonly="disabled" type="textarea" v-model="node.ioc" rows="4" placeholder="ioc名称" ></el-input>
</el-form-item>
<el-form-item label-width="0">
<el-input :readonly="disabled" type="textarea" v-model="node.iocRevoke" rows="4" placeholder="ioc名称撤销" ></el-input>
</el-form-item>
</template>
<template v-if="node.executeType == '3'" >
<div style="padding:0 0 16px 0;">
<el-alert
title="接口参数说明"
type="info"
description="注意配置支持Post方法的接口,json数据格式{
processId:'流程发起实例主键',userId:'流程发起人Id',userAccount:'流程发起人账号',companyId:'流程发起人公司',departmentId:'流程发起人部门',code:'上一步执行码',
userId2:'上一步审核人Id',userAccount2:'上一步审核人账号',companyId2:'上一步审核人公司',departmentId2:'上一步审核人部门'
}
"
show-icon
:closable="false"
>
</el-alert>
</div>
<el-form-item label-width="0">
<el-input :readonly="disabled" type="textarea" v-model="node.apiUrl" rows="4" placeholder="接口地址" ></el-input>
</el-form-item>
<el-form-item label-width="0">
<el-input :readonly="disabled" type="textarea" v-model="node.apiUrlRevoke" rows="4" placeholder="接口地址(撤销)" ></el-input>
</el-form-item>
</template>
</el-form>
</template>
<script>
export default {
name:'script-task-option',
props:{
disabled:{
type:Boolean,
default:false
}
},
data () {
return {
}
},
computed: {
node(){
return this.wfdesign.currentWfNode;
}
},
inject: ["wfdesign"]
}
</script>
<style>
</style>

View File

@ -0,0 +1,70 @@
<!-- 开始节点配置 -->
<template>
<div class="end-event">
<a-form
ref="formRef"
:model="node"
labelAlign="left"
:disabled="data.componentDisabled"
:label-col="labelCol"
:wrapper-col="wrapperCol"
>
<a-form-item label="节点标识">
<a-input v-model:value="node.id" placeholder="请输入" readonly />
</a-form-item>
</a-form>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref, watch, defineProps, onMounted } from 'vue';
import { flowStore } from '@/store/modules/flow';
const flowWfDataStore = flowStore();
const node: any = ref({});
const props = defineProps({
element: {
type: Object,
default: () => {
return {};
},
},
pageType: String,
});
watch(
() => props.element,
(newVal) => {
if (newVal.type == 'bpmn:EndEvent') {
const currentNode = flowWfDataStore.getWfDataNode(newVal.id);
if (currentNode) {
node.value = currentNode;
} else {
node.value = newVal;
}
}
},
);
onMounted(() => {
if (props.element.type == 'bpmn:EndEvent') {
const currentNode = flowWfDataStore.getWfDataNode(props.element.id);
if (currentNode) {
node.value = currentNode;
} else {
node.value = props.element;
}
}
});
const labelCol = { span: 7 };
const wrapperCol = { span: 17 };
const data = reactive({
componentDisabled: props.pageType == 'detail' ? true : false,
});
function getForm() {
return node;
}
defineExpose({
getForm,
});
</script>
<style></style>

View File

@ -0,0 +1,2 @@
export { default as conditionFormula } from './src/conditionFormula.vue';
export { default as conditionSql } from './src/conditionSql.vue';

View File

@ -0,0 +1,236 @@
<template>
<div class="user-task">
<a-form
ref="formRef"
:model="node"
labelAlign="left"
:label-col="labelCol"
:wrapper-col="wrapperCol"
:disabled="data.componentDisabled"
>
<a-form-item label="节点标识">
<a-input v-model:value="node.id" placeholder="请输入" readonly />
</a-form-item>
<a-alert
message="排他网关说明"
description="排他网关不会等待所有分支汇入才往下执行只要有分支汇入就会往下执行出口分支只会执行一条条件为true如果多条出口分支条件为true也执行一条"
type="info"
show-icon
/>
<a-space>
<a-radio-group>
<a-radio-button value="1" @click="handleFormulaClick"></a-radio-button>
<a-radio-button value="2" @click="handleSQlClick">sql</a-radio-button>
</a-radio-group>
<a-button danger @click="handleClearClick"></a-button>
</a-space>
<a-table :columns="data.columns" :data-source="node.conditions" bordered :pagination="false">
<template #bodyCell="{ column, text, record }">
<template v-if="['name'].includes(column.dataIndex)">
<div>
{{ text }}
</div>
</template>
<template v-else-if="['type'].includes(column.dataIndex)">
<div>
{{ typeFormat(text) }}
</div>
</template>
<template v-else-if="column.dataIndex === 'operation'">
<a-button @click="onEdit(record)" size="small">编辑</a-button>
<a-button danger @click="onDelete(record.id)" size="small" class="ml-2">删除</a-button>
</template>
</template>
</a-table>
</a-form>
<a-modal
width="40%"
v-model:open="data.formulaVisible"
title="添加公式条件"
@ok="handleFormulaOk"
:destroyOnClose="true"
>
<conditionFormula ref="conditionFormulaRef" :propsData="propsData" />
</a-modal>
<a-modal
width="40%"
v-model:open="data.sqlVisible"
title="添加sql条件"
@ok="handleSqlOk"
:destroyOnClose="true"
>
<conditionSql ref="conditionSqlRef" :propsData="propsData" />
</a-modal>
</div>
</template>
<script lang="ts" setup>
import { conditionFormula, conditionSql } from './index';
import { reactive, ref, onMounted, watch, defineProps } from 'vue';
import { buildGUID } from '@/utils/uuid';
import { flowStore } from '@/store/modules/flow';
const flowWfDataStore = flowStore();
const labelCol = { span: 7 };
const wrapperCol = { span: 17 };
const conditionFormulaRef = ref<any>();
const conditionSqlRef = ref<any>();
const props = defineProps({
element: {
type: Object,
default: () => {
return {};
},
},
pageType: String,
});
const node: any = ref({
conditions: [],
});
const propsData = ref();
watch(
() => props.element,
(newVal) => {
if (newVal.type == 'bpmn:ExclusiveGateway') {
const currentNode = flowWfDataStore.getWfDataNode(newVal.id);
if (currentNode) {
node.value = currentNode;
} else {
node.value = newVal;
}
}
},
);
const data = reactive({
columns: [
{ title: '类型', dataIndex: 'type' },
{ title: '名称', dataIndex: 'name', width: '40%' },
{ title: '操作', dataIndex: 'operation' },
],
tableData: [],
formulaVisible: false,
sqlVisible: false,
editRow: null,
isEdit: false,
rowIndex: 0,
componentDisabled: props.pageType == 'detail' ? true : false,
});
onMounted(() => {
if (props.element.type == 'bpmn:ExclusiveGateway') {
const currentNode = flowWfDataStore.getWfDataNode(props.element.id);
if (currentNode) {
node.value = currentNode;
} else {
node.value = props.element;
}
}
});
function typeFormat(type) {
switch (type) {
case '1':
return '表达式';
case '2':
return 'sql语句';
}
}
function handleFormulaClick() {
data.formulaVisible = true;
data.isEdit = false;
propsData.value = {
dbCode: '',
table: '',
columns: [],
rfield: '',
cfield: '',
compareType: '',
value: '',
name: '',
};
}
function handleSQlClick() {
data.sqlVisible = true;
data.isEdit = false;
propsData.value = {
name: '',
dbCode: '',
sql: '',
};
}
function handleClearClick() {
node.value.conditions = [];
}
async function handleFormulaOk() {
conditionFormulaRef.value.validateForm();
let obj = conditionFormulaRef.value.getForm();
obj.type = '1';
if (
obj.name == '' ||
obj.table == '' ||
obj.value == '' ||
obj.rfield == '' ||
obj.cfield == '' ||
obj.compareType == ''
) {
data.formulaVisible = true;
} else {
if (data.isEdit) {
var currentIndex = node.value.conditions.findIndex(
(element) => element.code === propsData.value.code,
);
node.value.conditions[currentIndex] = obj;
} else {
obj.code = buildGUID();
node.value.conditions.push(obj);
}
data.formulaVisible = false;
}
}
function handleSqlOk() {
conditionSqlRef.value.validateForm();
let obj = conditionSqlRef.value.getForm();
obj.type = '2';
if (obj.name == '' || obj.dbCode == '' || obj.sql == '') {
data.sqlVisible = true;
} else {
if (data.isEdit) {
var currentIndex = node.value.conditions.findIndex(
(element) => element.code === propsData.value.code,
);
node.value.conditions[currentIndex] = obj;
} else {
obj.code = buildGUID();
node.value.conditions.push(obj);
}
data.sqlVisible = false;
}
}
function onEdit(record) {
data.isEdit = true;
propsData.value = record;
if (record.type == 1) {
data.formulaVisible = true;
} else {
data.sqlVisible = true;
}
}
function onDelete(id) {
let list = node.value.conditions;
var currentIndex = (list || []).findIndex((element) => element.id == id);
node.value.conditions.splice(currentIndex, 1);
}
defineExpose({});
</script>
<style scoped>
::v-deep .ant-alert-info {
margin-bottom: 20px;
}
::v-deep .ant-table-wrapper {
margin-top: 20px;
}
</style>

View File

@ -0,0 +1,213 @@
<template>
<div class="l-from-body">
<a-form
ref="formRef"
:rules="data.rules"
:model="formData"
labelAlign="left"
:label-col="labelCol"
:wrapper-col="wrapperCol"
>
<a-form-item label="条件名称" name="name">
<a-input v-model:value="formData.name" placeholder="请输入" />
</a-form-item>
<a-form-item label="数据库" name="dbCode">
<a-select
v-model:value="formData.dbCode"
placeholder="请选择"
:options="sqlOptions"
:field-names="{ label: 'text', value: 'id', options: 'childNodes' }"
/>
</a-form-item>
<a-form-item label="数据表" name="table">
<a-space>
<a-input v-model:value="formData.table" placeholder="请输入" readonly />
<a-button @click="handleTableClick"></a-button>
</a-space>
</a-form-item>
<a-form-item label="关联流程字段" name="rfield">
<a-select
v-model:value="formData.rfield"
placeholder="请选择"
:options="formData.columns"
:field-names="{ label: 'name', value: 'dbColumnName' }"
/>
</a-form-item>
<a-form-item label="比较字段" name="cfield">
<a-select
v-model:value="formData.cfield"
placeholder="请选择"
:options="formData.columns"
:field-names="{ label: 'name', value: 'dbColumnName' }"
/>
</a-form-item>
<a-form-item label="比较类型" name="compareType">
<a-select
v-model:value="formData.compareType"
placeholder="请选择"
:options="data.options"
/>
</a-form-item>
<a-form-item label="数据值" name="value">
<a-input v-model:value="formData.value" placeholder="请输入" />
</a-form-item>
</a-form>
<a-modal
width="60%"
wrap-class-name="full-modal-children"
v-model:open="data.tableOpen"
title="选择数据表"
@ok="tableHandleOk"
:destroyOnClose="true"
>
<SelectTable ref="tableRef" :dbCode="formData.dbCode" />
</a-modal>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref, onMounted } from 'vue';
import { SelectTable } from '@/components/SelectTable/index';
import { getTableForms } from '@/api/sys/flowPenal';
import { getLoadDataBaseLinkTree } from '@/api/demo/system';
import { useMessage } from '@/hooks/web/useMessage';
const { createMessage } = useMessage();
const labelCol = { span: 6 };
const wrapperCol = { span: 17 };
const formRef = ref<any>();
const tableRef = ref<any>();
const sqlOptions = ref([]);
let formData: any = ref({});
const props = defineProps({
propsData: {
type: Object,
default: () => {
return {};
},
},
});
formData.value = props.propsData;
const data = reactive({
rules: {
dbCode: [{ required: true, message: '请选择数据库' }],
table: [{ required: true, message: '请选择数据表' }],
rfield: [{ required: true, message: '请选择关联字段' }],
cfield: [{ required: true, message: '请选择比较字段' }],
compareType: [{ required: true, message: '请选择比较类型' }],
value: [{ required: true, message: '请填写值' }],
name: [{ required: true, message: '请填写条件名称' }],
},
options: [
{ value: '1', label: '等于' },
{ value: '2', label: '不等于' },
{ value: '3', label: '大于' },
{ value: '4', label: '大于等于' },
{ value: '5', label: '小于' },
{ value: '6', label: '小于等于' },
{ value: '7', label: '包含' },
{ value: '8', label: '不包含' },
{ value: '9', label: '包含于' },
{ value: '10', label: '不包含于' },
],
tableOpen: false,
});
async function getSQL() {
const data = await getLoadDataBaseLinkTree();
let res: any = [];
data.forEach((element) => {
if (element.childNodes == null) {
element.childNodes = [];
}
let point = {};
if (element.id == 'hcsystemdb') {
point = {
id: '1',
text: '常用数据库',
childNodes: [
{
id: element.id,
text: element.text,
},
],
};
res.push(point);
} else {
res.push(element);
}
});
sqlOptions.value = res;
}
function handleTableClick() {
if (formData.value.dbCode == '') {
return createMessage.warning('请先选择数据库');
}
data.tableOpen = true;
}
function tableHandleOk() {
console.log(tableRef.value.getRow());
const obj = tableRef.value.getRow();
formData.value.table = obj[0].tableName;
handleTableChange(obj[0].tableName);
data.tableOpen = false;
}
//
async function handleTableChange(table) {
var querys = {
dbCode: formData.value.dbCode,
tableNames: table,
};
const obj = await getTableForms(querys);
let columnsData = obj[0].db_codecolumnsList;
columnsData.forEach((element) => {
element.name = element.dbColumnName + '(' + element.description + ')';
});
formData.value.columns = columnsData;
}
function setForm(data) {
formData.value = data;
}
//
function validateForm() {
formRef.value
.validate()
.then(async () => {
return true;
})
.catch(async () => {
return false;
});
}
function getForm() {
return formData.value;
}
defineExpose({
validateForm,
getForm,
setForm,
});
onMounted(() => {
getSQL();
});
</script>
<style scoped>
.l-from-body {
padding: 10px 30px;
}
</style>
<style lang="less">
.full-modal-children {
.ant-modal {
max-width: 100%;
}
.ant-modal-content {
height: calc(80vh);
}
.ant-modal-body {
height: 80%;
}
}
</style>

View File

@ -0,0 +1,109 @@
<template>
<div class="l-from-body">
<a-form
ref="formRef"
:rules="data.rules"
:model="formData"
labelAlign="left"
:label-col="labelCol"
:wrapper-col="wrapperCol"
>
<a-form-item label="条件名称" name="name">
<a-input v-model:value="formData.name" placeholder="请输入" />
</a-form-item>
<a-form-item label="数据库" name="dbCode">
<a-select
v-model:value="formData.dbCode"
placeholder="请选择"
:options="sqlOptions"
:field-names="{ label: 'text', value: 'id', options: 'childNodes' }"
/>
</a-form-item>
<a-form-item label="SQL语句" name="sql">
<a-textarea v-model:value="formData.sql" placeholder="请输入" :rows="4" />
</a-form-item>
</a-form>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref, onMounted } from 'vue';
import { getLoadDataBaseLinkTree } from '@/api/demo/system';
const labelCol = { span: 5 };
const wrapperCol = { span: 17 };
const formRef = ref<any>();
let formData: any = ref({});
const props = defineProps({
propsData: {
type: Object,
default: () => {
return {};
},
},
});
formData.value = props.propsData;
const data = reactive({
rules: {
dbCode: [{ required: true, message: '请选择数据库' }],
sql: [{ required: true, message: '请填写sql语句' }],
name: [{ required: true, message: '请填写条件名称' }],
},
});
const sqlOptions = ref([]);
async function getSQL() {
const data = await getLoadDataBaseLinkTree();
let res: any = [];
data.forEach((element) => {
if (element.childNodes == null) {
element.childNodes = [];
}
let point = {};
if (element.id == 'hcsystemdb') {
point = {
id: '1',
text: '常用数据库',
childNodes: [
{
id: element.id,
text: element.text,
},
],
};
res.push(point);
} else {
res.push(element);
}
});
sqlOptions.value = res;
}
function setForm(data) {
formData.value = data;
}
//
function validateForm() {
formRef.value
.validate()
.then(async () => {
return true;
})
.catch(async () => {
return false;
});
}
function getForm() {
return formData.value;
}
defineExpose({
getForm,
validateForm,
setForm,
});
onMounted(() => {
getSQL();
});
</script>
<style scoped>
.l-from-body {
padding: 10px 30px;
}
</style>

View File

@ -0,0 +1,233 @@
<template>
<div class="user-task">
<a-form
ref="formRef"
:model="node"
labelAlign="left"
:label-col="labelCol"
:wrapper-col="wrapperCol"
:disabled="data.componentDisabled"
>
<a-form-item label="节点标识">
<a-input v-model:value="node.id" placeholder="请输入" readonly />
</a-form-item>
<a-alert
message="包容网关说明"
description="包容网关会等待所有分支汇入才往下执行出口分支能执行多条条件为true"
type="info"
show-icon
/>
<a-space>
<a-radio-group>
<a-radio-button value="1" @click="handleFormulaClick"></a-radio-button>
<a-radio-button value="2" @click="handleSQlClick">sql</a-radio-button>
</a-radio-group>
<a-button danger @click="handleClearClick"></a-button>
</a-space>
<a-table :columns="data.columns" :data-source="node.conditions" bordered :pagination="false">
<template #bodyCell="{ column, text, record }">
<template v-if="['name'].includes(column.dataIndex)">
<div>
{{ text }}
</div>
</template>
<template v-else-if="['type'].includes(column.dataIndex)">
<div>
{{ typeFormat(text) }}
</div>
</template>
<template v-else-if="column.dataIndex === 'operation'">
<a-button @click="onEdit(record)" size="small">编辑</a-button>
<a-button danger @click="onDelete(record.id)" size="small" class="ml-2">删除</a-button>
</template>
</template>
</a-table>
</a-form>
<a-modal
width="40%"
v-model:open="data.formulaVisible"
title="添加公式条件"
@ok="handleFormulaOk"
:destroyOnClose="true"
>
<conditionFormula ref="conditionFormulaRef" :propsData="propsData" />
</a-modal>
<a-modal
width="40%"
v-model:open="data.sqlVisible"
title="添加sql条件"
@ok="handleSqlOk"
:destroyOnClose="true"
>
<conditionSql ref="conditionSqlRef" :propsData="propsData" />
</a-modal>
</div>
</template>
<script lang="ts" setup>
import { conditionFormula, conditionSql } from '../exclusiveGateway/index';
import { reactive, ref, onMounted, watch, defineProps } from 'vue';
import { buildGUID } from '@/utils/uuid';
import { flowStore } from '@/store/modules/flow';
const flowWfDataStore = flowStore();
const labelCol = { span: 7 };
const wrapperCol = { span: 17 };
const conditionFormulaRef = ref<any>();
const conditionSqlRef = ref<any>();
const props = defineProps({
element: {
type: Object,
default: () => {
return {};
},
},
schemeCode: String,
pageType: String,
pageView: String,
});
const propsData = ref();
const node: any = ref({
conditions: [],
id: '',
});
watch(
() => props.element,
(newVal) => {
if (newVal.type == 'bpmn:InclusiveGateway') {
const currentNode = flowWfDataStore.getWfDataNode(newVal.id);
if (currentNode) {
node.value = currentNode;
} else {
node.value = newVal;
}
}
},
);
const data = reactive({
columns: [
{ title: '类型', dataIndex: 'type' },
{ title: '名称', dataIndex: 'name', width: '40%' },
{ title: '操作', dataIndex: 'operation' },
],
tableData: [],
formulaVisible: false,
sqlVisible: false,
editRow: null,
rowIndex: 0,
isEdit: false,
componentDisabled: props.pageType == 'detail' ? true : false,
});
onMounted(() => {
if (props.element.type == 'bpmn:InclusiveGateway') {
const currentNode = flowWfDataStore.getWfDataNode(props.element.id);
if (currentNode) {
node.value = currentNode;
} else {
node.value = props.element;
}
}
});
function typeFormat(type) {
switch (type) {
case '1':
return '表达式';
case '2':
return 'sql语句';
}
}
function handleFormulaClick() {
data.formulaVisible = true;
data.isEdit = false;
propsData.value = {
dbCode: '',
table: '',
columns: [],
rfield: '',
cfield: '',
compareType: '',
value: '',
name: '',
};
}
function handleSQlClick() {
data.sqlVisible = true;
data.isEdit = false;
propsData.value = {
name: '',
dbCode: '',
sql: '',
};
}
function handleClearClick() {
node.conditions = [];
}
async function handleFormulaOk() {
conditionFormulaRef.value.validateForm();
let obj = conditionFormulaRef.value.getForm();
obj.type = '1';
if (
obj.name == '' ||
obj.table == '' ||
obj.value == '' ||
obj.rfield == '' ||
obj.cfield == '' ||
obj.compareType == ''
) {
data.formulaVisible = true;
} else {
if (data.isEdit) {
node.value.conditions[data.rowIndex] = obj;
} else {
obj.code = buildGUID();
node.value.conditions.push(obj);
}
data.formulaVisible = false;
}
}
function handleSqlOk() {
conditionSqlRef.value.validateForm();
let obj = conditionSqlRef.value.getForm();
obj.type = '2';
if (obj.name == '' || obj.dbCode == '' || obj.sql == '') {
data.sqlVisible = true;
} else {
if (data.isEdit) {
node.value.conditions[data.rowIndex] = obj;
} else {
obj.code = buildGUID();
node.value.conditions.push(obj);
}
data.sqlVisible = false;
}
}
function onEdit(record) {
data.isEdit = true;
propsData.value = record;
if (record.type == 1) {
data.formulaVisible = true;
} else {
data.sqlVisible = true;
}
}
function onDelete(id) {
let list = node.value.conditions;
var currentIndex = (list || []).findIndex((element) => element.id == id);
node.value.conditions.splice(currentIndex, 1);
}
defineExpose({});
</script>
<style scoped>
::v-deep .ant-alert-info {
margin-bottom: 20px;
}
::v-deep .ant-table-wrapper {
margin-top: 20px;
}
</style>

View File

@ -0,0 +1,7 @@
import BpmnPropertiesPanel from "./PropertiesPanel.vue";
BpmnPropertiesPanel.install = function(Vue) {
Vue.component(BpmnPropertiesPanel.name, BpmnPropertiesPanel);
};
export default BpmnPropertiesPanel;

View File

@ -0,0 +1,98 @@
<!-- 开始节点配置 -->
<template>
<div class="start-event">
<a-form
ref="formRef"
:model="node"
labelAlign="left"
:label-col="labelCol"
:wrapper-col="wrapperCol"
:disabled="data.componentDisabled"
>
<a-form-item label="节点标识">
<a-input v-model:value="node.id" placeholder="请输入" readonly />
</a-form-item>
<a-form-item
v-if="data.conditionsOptions && data.conditionsOptions.length > 0"
label="流转条件"
>
<a-select
v-model:value="node.lineConditions"
placeholder="请选择"
:options="data.conditionsOptions"
:field-names="{ label: 'name', value: 'code' }"
/>
</a-form-item>
</a-form>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, defineProps, onMounted, watch } from 'vue';
import { flowStore } from '@/store/modules/flow';
const flowWfDataStore = flowStore();
const props = defineProps({
element: {
type: Object,
default: () => {
return {};
},
},
pageType: String,
});
const labelCol = { span: 7 };
const wrapperCol = { span: 17 };
let node: any = ref({});
const data = reactive({
conditionsOptions: [],
componentDisabled: props.pageType == 'detail' ? true : false,
});
watch(
() => props.element,
(newVal) => {
if (newVal.type == 'bpmn:SequenceFlow') {
const currentNode = flowWfDataStore.getWfDataNode(newVal.id);
if (currentNode) {
node.value = currentNode;
node.value.from = newVal.from;
node.value.to = newVal.to;
} else {
node.value = newVal;
}
getConditions();
}
},
);
onMounted(() => {
if (props.element.type == 'bpmn:SequenceFlow') {
const currentNode = flowWfDataStore.getWfDataNode(props.element.id);
if (currentNode) {
node.value = currentNode;
} else {
node.value = props.element;
}
}
});
function getConditions() {
/**
ExclusiveGateway:'排他网关',
Task:'审核节点',
*/
let wfdata = flowWfDataStore.getWfData;
wfdata.forEach((element: { id: string; conditions: any; btnlist: any; type: string }) => {
if (element.id == node.value.from) {
if (element.type == 'bpmn:ExclusiveGateway') {
data.conditionsOptions = element.conditions;
} else if (element.type == 'bpmn:Task') {
data.conditionsOptions = element.btnlist.filter((t) => !t.hidden);
} else {
data.conditionsOptions = [];
}
}
});
}
</script>
<style></style>

View File

@ -0,0 +1,11 @@
export { default as shcemeinfoConfig } from './shcemeInfo/index.vue';
export { default as authConfig } from './auth/index.vue';
export { default as StartEventOption } from './startEvent/index.vue';
export { default as userTaskOption } from './userTask/index.vue';
export { default as endEventOption } from './endEvent/index.vue';
export { default as parallelGatewayOption } from './parallelGateway/index.vue';
export { default as exclusiveGatewayOption } from './exclusiveGateway/index.vue';
export { default as inclusiveGatewayOption } from './inclusiveGateway/index.vue';
export { default as subprocessOption } from './subprocess/index.vue';
export { default as mylineOption } from './myline/index.vue';
export { default as scriptOption } from './script/index.vue';

View File

@ -0,0 +1,84 @@
<!-- 开始节点配置 -->
<template>
<div class="user-task">
<a-form
ref="formRef"
:model="node"
labelAlign="left"
:label-col="labelCol"
:wrapper-col="wrapperCol"
:disabled="data.componentDisabled"
>
<a-form-item label="节点标识">
<a-input v-model:value="node.id" placeholder="请输入" readonly />
</a-form-item>
<a-alert
message="并行网关说明"
description="并行网关会等待所有分支汇入才往下执行,所有出口分支都会被执行"
type="info"
show-icon
/>
</a-form>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref, onMounted, watch, defineProps } from 'vue';
import { flowStore } from '@/store/modules/flow';
const flowWfDataStore = flowStore();
const labelCol = { span: 7 };
const wrapperCol = { span: 17 };
const props = defineProps({
element: {
type: Object,
default: () => {
return {};
},
},
schemeCode: String,
pageType: String,
pageView: String,
});
const node: any = ref({});
watch(
() => props.element,
(newVal) => {
if (newVal.type == 'bpmn:ParallelGateway') {
const currentNode = flowWfDataStore.getWfDataNode(newVal.id);
if (currentNode) {
node.value = currentNode;
} else {
node.value = newVal;
}
}
},
);
const data = reactive({
componentDisabled: props.pageType == 'detail' ? true : false,
});
onMounted(() => {
if (props.element.type == 'bpmn:ParallelGateway') {
const currentNode = flowWfDataStore.getWfDataNode(props.element.id);
if (currentNode) {
node.value = currentNode;
} else {
node.value = props.element;
}
}
});
defineExpose({});
</script>
<style scoped>
::v-deep .ant-alert-info {
margin-bottom: 20px;
}
::v-deep .ant-table-wrapper {
margin-top: 20px;
}
</style>

View File

@ -0,0 +1,277 @@
<template>
<div class="start-event">
<a-form
ref="formRef"
:model="node"
labelAlign="left"
:label-col="labelCol"
:wrapper-col="wrapperCol"
:disabled="data.componentDisabled"
>
<a-form-item label="节点标识">
<a-input v-model:value="node.id" placeholder="请输入" readonly />
</a-form-item>
<a-divider plain="true">执行操作</a-divider>
<a-tabs v-model:activeKey="node.executeType" type="card" size="small" centered="true">
<a-tab-pane key="1" tab="执行SQL">
<a-space direction="vertical" size="middle" class="site-space-compact-wrapper">
<a-alert
message="sql参数说明"
description="参数有 @processId流程进程主键 @code上一步执行码 @userId流程发起人Id @userAccount流程发起人账号 @companyId流程发起人公司 @departmentId流程发起人部门;
@userId2上一步审核人Id @userAccount2上一步审核人账号 @companyId2上一步审核人公司 @departmentId2上一步审核人部门;
"
type="info"
show-icon
/>
<a-space-compact block>
<a-select
v-model:value="node.sqlDb"
placeholder="请选择执行SQL数据库"
:options="sqlOptions"
:field-names="{ label: 'text', value: 'id', options: 'childNodes' }"
/>
</a-space-compact>
<a-space-compact block>
<a-textarea
v-model:value="node.sqlStr"
placeholder="请填写SQL语句"
:auto-size="{ minRows: 5, maxRows: 8 }"
/>
</a-space-compact>
<a-space-compact block>
<a-textarea
v-model:value="node.sqlStrRevoke"
placeholder="请填写SQL语句(撤销)"
:auto-size="{ minRows: 5, maxRows: 8 }"
/>
</a-space-compact>
</a-space>
</a-tab-pane>
<a-tab-pane key="2" tab=".NET方法">
<a-space direction="vertical" size="middle" class="site-space-compact-wrapper">
<a-alert
message="IOC说明"
description="注意编写一个继承IWorkFlowMethod的类
"
type="info"
show-icon
/>
<a-space-compact block>
<a-textarea
v-model:value="node.ioc"
placeholder="IOC名称"
:auto-size="{ minRows: 5, maxRows: 8 }"
/>
</a-space-compact>
<a-space-compact block>
<a-textarea
v-model:value="node.iocRevoke"
placeholder="IOC名称(撤销)"
:auto-size="{ minRows: 5, maxRows: 8 }"
/>
</a-space-compact>
</a-space>
</a-tab-pane>
<a-tab-pane key="3" tab="第三方接口">
<a-space direction="vertical" size="middle" class="site-space-compact-wrapper">
<a-alert
message="接口参数说明"
description="注意配置支持Post方法的接口,json数据格式{
processId:'流程发起实例主键',userId:'流程发起人Id',userAccount:'流程发起人账号',companyId:'流程发起人公司',departmentId:'流程发起人部门',code:'上一步执行码',
userId2:'上一步审核人Id',userAccount2:'上一步审核人账号',companyId2:'上一步审核人公司',departmentId2:'上一步审核人部门'
}
"
type="info"
show-icon
/>
<a-space-compact block>
<a-textarea
v-model:value="node.apiUrl"
placeholder="接口地址"
:auto-size="{ minRows: 5, maxRows: 8 }"
/>
</a-space-compact>
<a-space-compact block>
<a-textarea
v-model:value="node.apiUrlRevoke"
placeholder="接口地址(撤销)"
:auto-size="{ minRows: 5, maxRows: 8 }"
/>
</a-space-compact>
</a-space>
</a-tab-pane>
</a-tabs>
</a-form>
</div>
</template>
<script lang="ts" setup>
import { reactive, defineProps, ref, watch, onMounted } from 'vue';
import { flowStore } from '@/store/modules/flow';
import { getLoadDataBaseLinkTree } from '@/api/demo/system';
const flowWfDataStore = flowStore();
const labelCol = { span: 7 };
const wrapperCol = { span: 17 };
//
const props = defineProps({
element: {
type: Object,
default: () => {
return {};
},
},
pageType: String,
});
interface dataType {
columns: any;
formRelations: any;
elementData: any;
componentDisabled: boolean;
formOpen: boolean;
formVerisons: any;
formName: string;
}
const data: dataType = reactive({
columns: [
{
title: '名称',
dataIndex: 'label',
},
{
title: '字段',
dataIndex: 'field',
},
{
title: '必填',
dataIndex: 'required',
},
{
title: '编辑',
dataIndex: 'disabled',
},
{
title: '查看',
dataIndex: 'ifShow',
},
],
formRelations: [],
elementData: props.element,
componentDisabled: props.pageType == 'detail' ? true : false,
formOpen: false,
formVerisons: [],
formName: '',
});
let node: any = ref({});
watch(
() => props.element,
(newVal) => {
if (newVal.type == 'bpmn:ScriptTask') {
const currentNode = flowWfDataStore.getWfDataNode(newVal.id);
if (currentNode) {
node.value = currentNode;
} else {
node.value = newVal;
}
}
},
);
const sqlOptions = ref([]);
async function getSQL() {
const data = await getLoadDataBaseLinkTree();
let res: any = [];
data.forEach((element) => {
if (element.childNodes == null) {
element.childNodes = [];
}
let point = {};
if (element.id == 'hcsystemdb') {
point = {
id: '1',
text: '常用数据库',
childNodes: [
{
id: element.id,
text: element.text,
},
],
};
res.push(point);
} else {
res.push(element);
}
});
sqlOptions.value = res;
}
onMounted(() => {
getSQL();
if (props.element.type == 'bpmn:ScriptTask') {
const currentNode = flowWfDataStore.getWfDataNode(props.element.id);
if (currentNode) {
node.value = currentNode;
} else {
node.value = props.element;
}
}
});
</script>
<style lang="less" scoped>
.site-space-compact-wrapper {
width: 100%;
.ant-select {
width: 100%;
}
}
::v-deep .ant-tabs {
padding: 0 !important;
}
::v-deep .ant-space {
width: 90%;
margin-left: 5%;
.ant-space-item {
width: 100%;
button {
width: 20%;
}
.ant-btn-dangerous {
width: 100%;
}
}
}
.l-rblock {
width: 100%;
height: 100%;
background: #f0f2f5;
}
.l-page-pane {
box-sizing: border-box;
position: relative;
width: 100%;
height: 100%;
max-width: 794px;
overflow: hidden auto;
background: #fff;
border-radius: 4px;
margin: auto;
padding: 24px;
}
.addDataBaseTableBox {
border: 1px dashed #f0f0f0;
text-align: center;
cursor: pointer;
margin-top: -20px;
&:hover {
border-color: #409eff;
}
}
.connectTableTitle {
padding-top: 20px;
}
.formLine {
height: 50px;
}
</style>

View File

@ -0,0 +1,340 @@
<template>
<div class="shceme-info">
<a-form
ref="formRef"
:rules="rules"
:model="formState"
labelAlign="left"
:label-col="labelCol"
:wrapper-col="wrapperCol"
:disabled="data.componentDisabled"
>
<template v-if="!data.componentDisabled">
<a-form-item label="模板编号" name="code">
<a-input v-model:value="formState.code" placeholder="请输入" />
</a-form-item>
<a-form-item label="模板名称" name="name">
<a-input v-model:value="formState.name" placeholder="请输入" />
</a-form-item>
<a-form-item label="模板图标" name="icon">
<IconPicker v-model:value="formState.icon" />
</a-form-item>
<a-form-item label="图标颜色" name="color">
<a-input
type="color"
:key="formState.color"
:default-value="formState.color"
v-model="formState.color"
placeholder="请输入"
@change="(e) => colorChange(e.target.value)"
/>
</a-form-item>
<a-form-item label="模板分类" name="category">
<a-select
v-model:value="formState.category"
placeholder="请选择"
:options="data.optionsType"
:field-names="{ label: 'itemName', value: 'itemName' }"
/>
</a-form-item>
<a-form-item label="我的任务创建">
<a-radio-group v-model:value="formState.mark" name="radioGroup">
<a-radio
v-for="(item, index) in data.optionsNotOrOk"
:key="index"
:value="item.value"
>{{ item.label }}</a-radio
>
</a-radio-group>
</a-form-item>
<a-form-item label="移动端创建">
<a-radio-group v-model:value="formState.isInApp" name="radioGroup">
<a-radio
v-for="(item, index) in data.optionsNotOrOk"
:key="index"
:value="item.value"
>{{ item.label }}</a-radio
>
</a-radio-group>
</a-form-item>
<a-textarea
v-model:value="formState.description"
placeholder="请填写备注"
:auto-size="{ minRows: 5, maxRows: 8 }"
/>
</template>
<a-divider plain="true">撤销操作</a-divider>
<a-tabs v-model:activeKey="formState.undoType" type="card" size="small" centered="true">
<a-tab-pane key="1" tab="执行SQL">
<a-space direction="vertical" size="middle" class="site-space-compact-wrapper">
<a-space-compact block>
<a-select
v-model:value="formState.undoDbCode"
placeholder="请选择执行SQL数据库"
:options="sqlOptions"
:field-names="{ label: 'text', value: 'id', options: 'childNodes' }"
/>
</a-space-compact>
<a-space-compact block>
<a-textarea
v-model:value="formState.undoDbSQL"
placeholder="请填写SQL语句参数有 @processId流程进程主键 @userId用户Id @userAccount用户账号 @companyId用户公司 @departmentId用户部门"
:auto-size="{ minRows: 5, maxRows: 8 }"
/>
</a-space-compact>
</a-space>
</a-tab-pane>
<a-tab-pane key="2" tab=".NET方法">
<a-space direction="vertical" size="middle" class="site-space-compact-wrapper">
<a-space-compact block>
<a-textarea
v-model:value="formState.undoIOCName"
placeholder="请填写.NET方法IOC别名"
:auto-size="{ minRows: 5, maxRows: 8 }"
/>
</a-space-compact>
</a-space>
</a-tab-pane>
<a-tab-pane key="3" tab="第三方接口">
<a-space direction="vertical" size="middle" class="site-space-compact-wrapper">
<a-space-compact block>
<a-textarea
v-model:value="formState.undoUrl"
placeholder="请填写第三方接口地址(POST),JSON 格式,参数有 userId用户Id,userAccount用户账号,companyId用户公司,departmentId用户部门"
:auto-size="{ minRows: 5, maxRows: 8 }"
/>
</a-space-compact>
</a-space>
</a-tab-pane>
</a-tabs>
<a-divider plain="true">作废操作</a-divider>
<a-tabs v-model:activeKey="formState.deleteType" type="card" size="small" centered="true">
<a-tab-pane key="1" tab="执行SQL">
<a-space direction="vertical" size="middle" class="site-space-compact-wrapper">
<a-space-compact block>
<a-select
v-model:value="formState.deleteDbCode"
placeholder="请选择执行SQL数据库"
:options="sqlOptions"
:field-names="{ label: 'text', value: 'id', options: 'childNodes' }"
/>
</a-space-compact>
<a-space-compact block>
<a-textarea
v-model:value="formState.deleteDbSQL"
placeholder="请填写SQL语句参数有 @processId流程进程主键 @userId用户Id @userAccount用户账号 @companyId用户公司 @departmentId用户部门"
:auto-size="{ minRows: 5, maxRows: 8 }"
/>
</a-space-compact>
</a-space>
</a-tab-pane>
<a-tab-pane key="2" tab=".NET方法">
<a-space direction="vertical" size="middle" class="site-space-compact-wrapper">
<a-space-compact block>
<a-textarea
v-model:value="formState.deleteIOCName"
placeholder="请填写.NET方法IOC别名"
:auto-size="{ minRows: 5, maxRows: 8 }"
/>
</a-space-compact>
</a-space>
</a-tab-pane>
<a-tab-pane key="3" tab="第三方接口">
<a-space direction="vertical" size="middle" class="site-space-compact-wrapper">
<a-space-compact block>
<a-textarea
v-model:value="formState.undoUrl"
placeholder="请填写第三方接口地址(POST),JSON 格式,参数有 userId用户Id,userAccount用户账号,companyId用户公司,departmentId用户部门"
:auto-size="{ minRows: 5, maxRows: 8 }"
/>
</a-space-compact>
</a-space>
</a-tab-pane>
</a-tabs>
<a-divider plain="true">删除草稿</a-divider>
<a-tabs
v-model:activeKey="formState.deleteDraftType"
type="card"
size="small"
centered="true"
>
<a-tab-pane key="1" tab="执行SQL">
<a-space direction="vertical" size="middle" class="site-space-compact-wrapper">
<a-space-compact block>
<a-select
v-model:value="formState.deleteDraftDbCode"
placeholder="请选择执行SQL数据库"
:options="sqlOptions"
:field-names="{ label: 'text', value: 'id', options: 'childNodes' }"
/>
</a-space-compact>
<a-space-compact block>
<a-textarea
v-model:value="formState.deleteDraftDbSQL"
placeholder="请填写SQL语句参数有 @processId流程进程主键 @userId用户Id @userAccount用户账号 @companyId用户公司 @departmentId用户部门"
:auto-size="{ minRows: 5, maxRows: 8 }"
/>
</a-space-compact>
</a-space>
</a-tab-pane>
<a-tab-pane key="2" tab=".NET方法">
<a-space direction="vertical" size="middle" class="site-space-compact-wrapper">
<a-space-compact block>
<a-textarea
v-model:value="formState.deleteDraftIOCName"
placeholder="请填写.NET方法IOC别名"
:auto-size="{ minRows: 5, maxRows: 8 }"
/>
</a-space-compact>
</a-space>
</a-tab-pane>
<a-tab-pane key="3" tab="第三方接口">
<a-space direction="vertical" size="middle" class="site-space-compact-wrapper">
<a-space-compact block>
<a-textarea
v-model:value="formState.undoUrl"
placeholder="请填写第三方接口地址(POST),JSON 格式,参数有 userId用户Id,userAccount用户账号,companyId用户公司,departmentId用户部门"
:auto-size="{ minRows: 5, maxRows: 8 }"
/>
</a-space-compact>
</a-space>
</a-tab-pane>
</a-tabs>
</a-form>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref, onMounted, defineProps } from 'vue';
import type { Rule } from 'ant-design-vue/es/form';
import { IconPicker } from '@/components/Icon/index';
import { getLoad } from '@/api/sys/sysDataItemDetail';
import { getLoadDataBaseLinkTree } from '@/api/demo/system';
const formRef = ref();
const labelCol = { span: 7 };
const wrapperCol = { span: 17 };
const props = defineProps({
pageType: String,
});
const data = reactive({
optionsNotOrOk: [
{ label: '允许', value: 1 },
{ label: '不允许', value: 2 },
],
optionsType: [],
componentDisabled: props.pageType == 'detail' ? true : false,
});
const sqlOptions = ref([]);
let formState = ref({
code: '',
name: '',
category: '',
enabledMark: 1,
mark: 1,
isInApp: 2,
description: '',
titleRules: '',
undoType: '1',
undoDbCode: '',
undoDbSQL: '',
undoIOCName: '',
undoUrl: '',
deleteType: '1',
deleteDbCode: '',
deleteDbSQL: '',
deleteIOCName: '',
deleteUrl: '',
deleteDraftType: '1',
deleteDraftDbCode: '',
deleteDraftDbSQL: '',
deleteDraftIOCName: '',
deleteDraftUrl: '',
icon: 'ant-design:appstore-outlined',
color: '#409EFF',
});
const rules: Record<string, Rule[]> = {
code: [
{ required: true, message: '请输入模板编号' },
// { validator: lr_existDbFiled, keyValue: () => { return data.formData.f_Id }, tableName: 'lr_wf_schemeinfo', keyName: 'f_Id', trigger: 'blur' }
],
name: [{ required: true, message: '请输入模板名称' }],
category: [{ required: true, message: '请选择模板分类' }],
icon: [{ required: true, message: '请选择图标' }],
color: [{ required: true, message: '请选择颜色' }],
};
function colorChange(color) {
formState.value.color = color;
}
async function getSQL() {
const data = await getLoadDataBaseLinkTree();
let res: any = [];
data.forEach((element) => {
if (element.childNodes == null) {
element.childNodes = [];
}
let point = {};
if (element.id == 'hcsystemdb') {
point = {
id: '1',
text: '常用数据库',
childNodes: [
{
id: element.id,
text: element.text,
},
],
};
res.push(point);
} else {
res.push(element);
}
});
sqlOptions.value = res;
}
async function fetch() {
let list = await getLoad({ code: 'FlowSort' });
data.optionsType = list;
}
async function validateForm() {
let res = await formRef.value
.validate()
.then(() => {
return true;
})
.catch(() => {
return false;
});
return res;
}
function setForm(data) {
formState.value = data;
}
function getForm() {
return formState.value;
}
defineExpose({
setForm,
getForm,
validateForm,
});
onMounted(() => {
fetch();
getSQL();
});
</script>
<style lang="less" scoped>
.site-space-compact-wrapper {
width: 100%;
.ant-select {
width: 100%;
}
}
::v-deep .ant-tabs {
padding: 0 !important;
}
</style>

View File

@ -0,0 +1,523 @@
<!-- 开始节点配置 -->
<template>
<div class="start-event">
<a-form
ref="formRef"
:model="node"
labelAlign="left"
:label-col="labelCol"
:wrapper-col="wrapperCol"
:disabled="data.componentDisabled"
>
<a-form-item label="节点标识">
<a-input v-model:value="node.id" placeholder="请输入" readonly />
</a-form-item>
<a-form-item label="下一审核人">
<a-switch v-model:checked="node.isNextAuditor" @change="updateWfData('isNextAuditor')" />
</a-form-item>
<a-form-item label="自定义标题">
<a-switch v-model:checked="node.isCustmerTitle" @change="updateWfData('isCustmerTitle')" />
</a-form-item>
<a-form-item label="通知策略" name="f_Color">
<a-checkbox-group
v-model:value="node.messageType"
name="checkboxgroup"
@change="updateWfData('messageType')"
:options="[
{ value: '1', label: '短信' },
{ value: '2', label: '邮箱' },
{ value: '3', label: '微信' },
{ value: '4', label: '站内消息' },
]"
/>
</a-form-item>
<a-divider plain="true">表单添加</a-divider>
<a-tabs
v-model:activeKey="node.formType"
type="card"
size="small"
centered="true"
@change="tabsChange"
>
<a-tab-pane key="1" tab="自定义表单">
<a-space direction="vertical" size="middle" class="site-space-compact-wrapper">
<a-space-compact block>
<a-input v-model:value="data.formName" placeholder="请选择表单" readonly />
<a-button @click="handleShow"></a-button>
</a-space-compact>
<a-space-compact block>
<a-select
v-model:value="node.formVerison"
@change="custmerformVerisonChange"
:options="data.formVerisons"
:fieldNames="{ value: 'id', label: 'createDate' }"
placeholder="请选择表单版本"
/>
</a-space-compact>
<a-space-compact block>
<a-select
v-model:value="node.formRelationId"
placeholder="请选择流程关联字段"
@change="updateWfData('formRelationId')"
:options="data.formRelations"
/>
</a-space-compact>
<a-space-compact block>
<a-input v-model:value="node.formTitle" placeholder="请输入表单标题" />
</a-space-compact>
<a-space-compact block>
<a-input v-model:value="data.issueName" placeholder="请选择发布表单" readonly />
<a-button @click="handleIssueShow"></a-button>
</a-space-compact>
</a-space>
</a-tab-pane>
<a-tab-pane key="2" tab="系统表单">
<a-space direction="vertical" size="middle" class="site-space-compact-wrapper">
<a-space-compact block>
<a-input
v-model:value="node.formUrl"
placeholder="请输入PC端表单地址"
@change="updateWfData('formUrl')"
/>
</a-space-compact>
<a-space-compact block>
<a-input v-model:value="node.formTitle" placeholder="请输入PC端表单标题" />
</a-space-compact>
<a-space-compact block>
<a-input
v-model:value="node.formAppUrl"
placeholder="请输入APP端表单地址"
@change="updateWfData('formAppUrl')"
/>
</a-space-compact>
</a-space>
</a-tab-pane>
</a-tabs>
<a-divider plain="true">表单字段权限</a-divider>
<a-table
:columns="data.columns"
:data-source="node.authFields"
bordered
:pagination="false"
v-if="node.formType == 1"
>
<template #headerCell="{ column }">
<template v-if="column.dataIndex === 'required'">
<a-checkbox v-model:checked="requiredCheck" @change="handleChangeCheck('required',$event)">{{column.title}}</a-checkbox>
</template>
<template v-if="column.dataIndex === 'disabled'">
<a-checkbox v-model:checked="disabledCheck" @change="handleChangeCheck('disabled',$event)">{{column.title}}</a-checkbox>
</template>
<template v-if="column.dataIndex === 'ifShow'">
<a-checkbox v-model:checked="ifShowCheck" @change="handleChangeCheck('ifShow',$event)">{{column.title}}</a-checkbox>
</template>
</template>
<template #bodyCell="{ column, text, record }">
<template v-if="['label', 'field'].includes(column.dataIndex)">
<div>
{{ text }}
</div>
</template>
<template v-else-if="['required', 'disabled', 'ifShow'].includes(column.dataIndex)">
<div>
<a-switch v-model:checked="record[column.dataIndex]" size="small" />
</div>
</template>
</template>
</a-table>
<a-table
:columns="data.columns"
:data-source="node.authFields"
bordered
:pagination="false"
v-else
>
<template #bodyCell="{ column, record }">
<template v-if="['label', 'fieldName'].includes(column.dataIndex)">
<div>
<a-input v-model:value="record[column.dataIndex]" placeholder="请输入" />
</div>
</template>
<template v-else-if="['required', 'disabled', 'ifShow'].includes(column.dataIndex)">
<div>
<a-switch v-model:checked="record[column.dataIndex]" size="small" />
</div>
</template>
<template v-else-if="column.dataIndex === 'operation'">
<delete-outlined
two-tone-color="#eb2f96"
@click="handleDeleteAuthField(record.field)"
/>
</template>
</template>
</a-table>
<a-divider plain="true" />
<a-space v-if="node.formType != 1">
<a-button
@click="handleAddAuthField"
width="100%"
type="dashed"
danger
:icon="h(PlusOutlined)"
>添加字段</a-button
>
</a-space>
</a-form>
<a-modal
width="60%"
wrap-class-name="full-modal"
v-model:open="data.formOpen"
title="选择表单"
@ok="formHandleOk"
:destroyOnClose="true"
>
<SelectForm ref="formRef" />
</a-modal>
<a-modal
width="60%"
wrap-class-name="full-modal"
v-model:open="data.issueOpen"
title="选择发布表单"
@ok="issueHandleOk"
:destroyOnClose="true"
>
<SelectIssueForm ref="issueRef" />
</a-modal>
</div>
</template>
<script lang="ts" setup>
import { reactive, defineProps, ref, watch, h, onMounted } from 'vue';
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons-vue';
import { flowStore } from '@/store/modules/flow';
import { SelectForm } from '@/components/SelectForm/index';
import { SelectIssueForm } from '@/components/SelectIssueForm/index';
import { functionGetSchemePageList, functionLoadFormPage } from '@/api/demo/formScheme';
import { cardNestStructure } from '@/views/demo/onlineform/util.ts';
import { fun_GetPageList } from '@/api/demo/formModule';
import { getRFields } from '@/views/demo/workflow/scheme/util.ts'
const flowWfDataStore = flowStore();
const labelCol = { span: 7 };
const wrapperCol = { span: 17 };
const requiredCheck = ref(false)
const disabledCheck = ref(false)
const ifShowCheck = ref(false)
//
const props = defineProps({
disabled: {
type: Boolean,
default: false,
},
element: {
type: Object,
default: () => {
return {};
},
},
schemeCode: String,
pageType: String,
pageView: String,
});
interface dataType {
columns: any;
formRelations: any;
elementData: any;
componentDisabled: boolean;
formOpen: boolean;
formVerisons: any;
formName: string;
issueOpen: boolean;
issueName: string;
mapConfig:string;
}
const data: dataType = reactive({
columns: [
{
title: '名称',
dataIndex: 'label',
width: 100,
},
{
title: '字段',
dataIndex: 'fieldName',
ellipsis: true,
},
{
title: '必填',
dataIndex: 'required',
width: 85,
},
{
title: '编辑',
dataIndex: 'disabled',
width: 85,
},
{
title: '查看',
dataIndex: 'ifShow',
width: 85,
},
],
formRelations: [],
elementData: props.element,
componentDisabled: props.pageType == 'detail' ? true : false,
formOpen: false,
formVerisons: [],
formName: '',
issueName: '',
issueOpen: false,
});
let node: any = ref({});
watch(
() => node.value.formCode,
(newVal) => {
if (newVal) {
getFormList();
getIssueFormList();
getVersions(false);
}
},
);
watch(
() => props.element,
(newVal) => {
if (newVal.type == 'bpmn:StartEvent') {
const currentNode = flowWfDataStore.getWfDataNode(newVal.id);
if (currentNode) {
node.value = currentNode;
} else {
node.value = newVal;
}
}
},
);
watch(
() => node.value.formType,
(newVal) => {
if (newVal == 1) {
data.columns = data.columns.filter((item) => item.dataIndex !== 'operation');
} else {
data.columns.push({
title: '',
dataIndex: 'operation',
});
}
},
);
onMounted(() => {
if (props.element.type == 'bpmn:StartEvent') {
const currentNode = flowWfDataStore.getWfDataNode(props.element.id);
if (currentNode) {
node.value = currentNode;
} else {
node.value = props.element;
}
}
});
//
async function getFormList() {
const list = await functionLoadFormPage({
page: 1,
limit: 1000,
});
list.items.forEach((element) => {
if (element.id == node.value.formCode) {
data.formName = element.name;
}
});
}
//
function tabsChange() {
data.formName = '';
node.value.formCode = '';
node.value.formTitle = '';
node.value.formVerison = undefined;
node.value.formRelationId = undefined;
node.value.formUrl = '';
node.value.formAppUrl = '';
node.value.authFields = [];
node.value.formRelations = [];
}
//
function handleShow() {
data.formOpen = true;
}
//
const formRef = ref<any>();
async function formHandleOk() {
let obj = formRef.value.getRow();
node.value.formCode = obj[0].id;
data.formName = obj[0].name;
node.value.formVerison = undefined;
node.value.formRelationId = undefined;
node.value.formUrl = '';
node.value.formAppUrl = '';
node.value.authFields = [];
node.value.formRelations = [];
getVersions(true);
data.formOpen = false;
}
//
function handleIssueShow() {
data.issueOpen = true;
}
const issueRef = ref<any>();
async function issueHandleOk() {
let obj = issueRef.value.getRow();
let formPublishInfo = JSON.parse(obj[0].scheme)
let mapConfig = formPublishInfo.table.maps;
node.value.issueId = obj[0].id;
node.value.mapConfig = mapConfig
data.issueName = obj[0].name;
data.issueOpen = false;
data.mapConfig = mapConfig;
}
//
async function getIssueFormList() {
const list = await fun_GetPageList({
page: 1,
limit: 1000,
});
list.items.forEach((element) => {
if (element.id == node.value.issueId) {
data.issueName = element.name;
}
});
}
async function getVersions(isChange) {
const list = await functionGetSchemePageList({
schemeInfoId: node.value.formCode,
});
data.formVerisons = list.items;
if (node.value.formVerison) {
custmerformVerisonChange(node.value.formVerison, isChange);
}
}
//
async function custmerformVerisonChange(val, isChange) {
let obj;
data.formVerisons.forEach((element) => {
if (element.id == val) {
obj = element;
}
});
loadFormScheme(obj.scheme, isChange);
}
function loadFormScheme(strScheme, isChange) {
const scheme = JSON.parse(strScheme);
let fields: any[] = [];
let rfields: {
label?: string;
value?: string;
}[] = [];
scheme.formInfo.tabList = cardNestStructure(scheme.formInfo.tabList);
scheme.formInfo.tabList.forEach((tabElement) => {
const {rFieldList, fieldList} = getRFields(tabElement.schemas,rfields,fields)
rfields = rFieldList
fields = fieldList
});
data.formRelations = rfields;
if (isChange) {
node.value.authFields = fields;
}
}
function updateWfData(key) {
// flowWfDataStore.updataWfDataNode(node.value.id, key, node.value[key]);
}
function handleAddAuthField() {
node.value.authFields.push({
field: '',
label: '',
required: true,
disabled: true,
ifShow: true,
});
}
function handleDeleteAuthField(key) {
node.value.authFields = node.value.authFields.filter((item) => item.field !== key);
}
const handleChangeCheck = (type: string, data) => {
let checked = data.target.checked
node.value.authFields.forEach(item => {
switch (type) {
case 'required':
item.required = checked
break
case 'disabled':
item.disabled = checked
break
case 'ifShow':
item.ifShow = checked
break
}
})
}
</script>
<style lang="less" scoped>
.site-space-compact-wrapper {
width: 100%;
.ant-select {
width: 100%;
}
}
::v-deep .ant-tabs {
padding: 0 !important;
}
::v-deep .ant-space {
width: 90%;
margin-left: 5%;
.ant-space-item {
width: 100%;
button {
width: 20%;
}
.ant-btn-dangerous {
width: 100%;
}
}
}
:deep(.ant-checkbox+span){
padding-inline-end: 0px;
}
.l-rblock {
width: 100%;
height: 100%;
background: #f0f2f5;
}
.l-page-pane {
box-sizing: border-box;
position: relative;
width: 100%;
height: 100%;
max-width: 794px;
overflow: hidden auto;
background: #fff;
border-radius: 4px;
margin: auto;
padding: 24px;
}
.addDataBaseTableBox {
border: 1px dashed #f0f0f0;
text-align: center;
cursor: pointer;
margin-top: -20px;
&:hover {
border-color: #409eff;
}
}
.connectTableTitle {
padding-top: 20px;
}
.formLine {
height: 50px;
}
</style>

View File

@ -0,0 +1,100 @@
<!-- 开始节点配置 -->
<template>
<div class="subprocess">
<a-form
ref="formRef"
:model="node"
labelAlign="left"
:label-col="labelCol"
:wrapper-col="wrapperCol"
:disabled="data.componentDisabled"
>
<a-form-item label="节点标识">
<a-input v-model:value="node.id" placeholder="请输入" readonly />
</a-form-item>
<a-form-item label="是否异步">
<a-switch v-model:checked="node.isAsync" />
</a-form-item>
<a-form-item label="流程模版">
<a-select
v-model:value="node.wfschemeId"
:options="list"
@change="changeScheme"
:fieldNames="{ value: 'id', label: 'name' }"
/>
</a-form-item>
<a-form-item label="流程版本">
<a-select
v-model:value="node.wfVersionId"
:options="verisons"
:fieldNames="{ value: 'id', label: 'createDate' }"
/>
</a-form-item>
</a-form>
</div>
</template>
<script lang="ts" setup>
import { reactive, defineProps, ref, watch, onMounted } from 'vue';
import { getLoad, getVerisonsLoad } from '@/api/sys/WFSchemeInfo';
import { flowStore } from '@/store/modules/flow';
const flowWfDataStore = flowStore();
const labelCol = { span: 7 };
const wrapperCol = { span: 17 };
const props = defineProps({
element: {
type: Object,
default: () => {
return {};
},
},
schemeCode: String,
pageType: String,
pageView: String,
});
let node: any = ref({});
const data = reactive({
componentDisabled: props.pageType == 'detail' ? true : false,
});
const list: any = ref([]);
const verisons: any = ref([]);
watch(
() => props.element,
(newVal) => {
if (newVal.type == 'bpmn:SubProcess') {
const currentNode = flowWfDataStore.getWfDataNode(newVal.id);
if (currentNode) {
node.value = currentNode;
} else {
node.value = newVal;
}
}
},
);
async function getSchemeList() {
const data = await getLoad();
list.value = data;
}
async function changeScheme() {
const data = await getVerisonsLoad({ id: node.value.wfschemeId });
verisons.value = data;
}
onMounted(() => {
getSchemeList();
if (props.element.type == 'bpmn:SubProcess') {
const currentNode = flowWfDataStore.getWfDataNode(props.element.id);
if (currentNode) {
node.value = currentNode;
} else {
node.value = props.element;
}
}
});
defineExpose({});
</script>
<style></style>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,4 @@
export { default as AuditorLevel } from './src/auditorLevel.vue';
export { default as AuditorSql } from './src/auditorSql.vue';
export { default as AuditorNode } from './src/auditorNode.vue';
export { default as ExecuteSQL } from './src/executeSQL.vue';

View File

@ -0,0 +1,76 @@
<template>
<div class="l-from-body">
<a-form
ref="formRef"
:rules="data.rules"
:model="data.formData"
labelAlign="left"
:label-col="labelCol"
:wrapper-col="wrapperCol"
>
<a-form-item label="上下级" name="id">
<a-select
v-model:value="data.formData.id"
placeholder="请选择上下级"
:options="data.options"
@change="handleChange"
/>
</a-form-item>
</a-form>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
const formRef = ref<any>();
const labelCol = { span: 7 };
const wrapperCol = { span: 17 };
const data = reactive({
formData: {
name: '',
type: '4',
id: '',
},
rules: {
id: [{ required: true, message: '请选择上下级' }],
},
options: [
{ value: '1', label: '上一级' },
{ value: '2', label: '上二级' },
{ value: '3', label: '上三级' },
{ value: '4', label: '上四级' },
{ value: '5', label: '上五级' },
{ value: '6', label: '下一级' },
{ value: '7', label: '下二级' },
{ value: '8', label: '下三级' },
{ value: '9', label: '下四级' },
{ value: '10', label: '下五级' },
],
});
function handleChange(val, option) {
data.formData.name = option.label;
}
function validateForm() {
formRef.value
.validate()
.then(async () => {
return true;
})
.catch(async () => {
return false;
});
}
function getForm() {
let rows = data.formData;
return rows;
}
defineExpose({
getForm,
validateForm,
});
</script>
<style scoped>
.l-from-body {
padding: 10px;
}
</style>

View File

@ -0,0 +1,94 @@
<template>
<div class="l-from-body">
<a-form
ref="formRef"
:rules="data.rules"
:model="data.formData"
labelAlign="left"
:label-col="labelCol"
:wrapper-col="wrapperCol"
>
<a-form-item label="节点" name="id">
<a-select
v-model:value="data.formData.id"
placeholder="请选择节点"
:options="data.options"
:field-names="{ label: 'name', value: 'id' }"
@change="handleChange"
/>
</a-form-item>
</a-form>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref, defineProps, watch, onMounted } from 'vue';
import { flowStore } from '@/store/modules/flow';
const flowWfDataStore = flowStore();
const labelCol = { span: 7 };
const wrapperCol = { span: 17 };
const props = defineProps({
id: String,
});
watch(
() => props.id,
(newVal) => {
getOptions(newVal);
},
);
const formRef = ref<any>();
const data = reactive({
formData: {
name: '',
type: '5',
id: '',
},
rules: {
id: [{ required: true, message: '请选择节点' }],
},
options: [],
});
onMounted(() => {
getOptions(props.id);
});
function getOptions(nodeId) {
/**
ExclusiveGateway:'排他网关',
Task:'审核节点',
*/
let wfdata = flowWfDataStore.getWfData;
var arr: any = [];
wfdata.forEach((element: { type: string; id: string }) => {
if (element.type == 'bpmn:Task' && element.id != nodeId) {
arr.push(element);
}
});
data.options = arr;
}
function handleChange(val, option) {
data.formData.name = option.name;
}
function validateForm() {
formRef.value
.validate()
.then(async () => {
return true;
})
.catch(async () => {
return false;
});
}
function getForm() {
let rows = data.formData;
return rows;
}
defineExpose({
getForm,
validateForm,
});
</script>
<style scoped>
.l-from-body {
padding: 10px;
}
</style>

View File

@ -0,0 +1,179 @@
<template>
<div class="l-from-body">
<a-form
ref="formRef"
:rules="data.rules"
:model="formData"
labelAlign="left"
:label-col="labelCol"
:wrapper-col="wrapperCol"
>
<a-form-item label="数据库" name="dbCode">
<a-select
v-model:value="formData.dbCode"
placeholder="请选择"
:options="sqlOptions"
:field-names="{ label: 'text', value: 'id', options: 'childNodes' }"
/>
</a-form-item>
<a-form-item label="数据表" name="table">
<a-space>
<a-input v-model:value="formData.table" placeholder="请输入" readonly />
<a-button @click="handleTableClick"></a-button>
</a-space>
</a-form-item>
<a-form-item label="关联流程字段" name="rfield">
<a-select
v-model:value="formData.rfield"
placeholder="请选择"
:options="data.columns"
:field-names="{ label: 'name', value: 'dbColumnName' }"
/>
</a-form-item>
<a-form-item label="审核人字段" name="auditorField">
<a-select
v-model:value="formData.auditorField"
placeholder="请选择"
:options="data.columns"
:field-names="{ label: 'name', value: 'dbColumnName' }"
/>
</a-form-item>
</a-form>
<a-modal
width="60%"
wrap-class-name="full-modal-children"
v-model:open="data.tableOpen"
title="选择数据表"
@ok="tableHandleOk"
:destroyOnClose="true"
>
<SelectTable ref="tableRef" />
</a-modal>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref, onMounted } from 'vue';
import { SelectTable } from '@/components/SelectTable/index';
import { getTableForms } from '@/api/sys/flowPenal';
import { getLoadDataBaseLinkTree } from '@/api/demo/system';
import { useMessage } from '@/hooks/web/useMessage';
const { createMessage } = useMessage();
const labelCol = { span: 6 };
const wrapperCol = { span: 17 };
const formRef = ref<any>();
const tableRef = ref<any>();
const sqlOptions = ref([]);
const data = reactive({
rules: {
dbCode: [{ required: true, message: '请选择数据库' }],
table: [{ required: true, message: '请选择数据表' }],
rfield: [{ required: true, message: '请选择关联字段' }],
auditorField: [{ required: true, message: '请选择审核人字段' }],
},
columns: [],
tableOpen: false,
});
let formData = ref({
dbCode: '',
table: '',
rfield: '',
auditorField: '',
type: '6',
});
async function getSQL() {
const data = await getLoadDataBaseLinkTree();
let res: any = [];
data.forEach((element) => {
if (element.childNodes == null) {
element.childNodes = [];
}
let point = {};
if (element.id == 'hcsystemdb') {
point = {
id: '1',
text: '常用数据库',
childNodes: [
{
id: element.id,
text: element.text,
},
],
};
res.push(point);
} else {
res.push(element);
}
});
sqlOptions.value = res;
}
function handleTableClick() {
if (formData.value.dbCode == '') {
return createMessage.warning('请先选择数据库');
}
data.tableOpen = true;
}
function tableHandleOk() {
const obj = tableRef.value.getRow();
formData.value.table = obj[0].name;
handleTableChange(obj[0].name);
data.tableOpen = false;
}
//
async function handleTableChange(table) {
var querys = {
dbCode: formData.value.dbCode,
tableNames: table,
};
const obj: Recordable = await getTableForms(querys);
let columnsData = obj[0].db_codecolumnsList;
columnsData.forEach((element) => {
element.name = element.dbColumnName + '(' + element.description + ')';
});
data.columns = columnsData;
}
//
function validateForm() {
formRef.value
.validate()
.then(async () => {
return true;
})
.catch(async () => {
return false;
});
}
function getForm() {
return formData.value;
}
defineExpose({
validateForm,
getForm,
});
onMounted(() => {
getSQL();
});
</script>
<style scoped>
.l-from-body {
padding: 10px 30px;
}
</style>
<style lang="less">
.full-modal-children {
.ant-modal {
max-width: 100%;
}
.ant-modal-content {
height: calc(80vh);
}
.ant-modal-body {
height: 80%;
}
}
</style>

View File

@ -0,0 +1,56 @@
<template>
<div class="l-from-body">
<a-form
ref="formRef"
:rules="data.rules"
:model="data.formData"
labelAlign="left"
:label-col="labelCol"
:wrapper-col="wrapperCol"
>
<a-form-item label="SQL语句" name="Sql">
<a-textarea v-model:value="data.formData.Sql" placeholder="SQL语句" :rows="4" />
</a-form-item>
</a-form>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
const formRef = ref<any>();
const labelCol = { span: 7 };
const wrapperCol = { span: 17 };
const data = reactive({
formData: {
Sql: '',
type: '7',
},
rules: {
Sql: [{ required: true, message: '请输入SQL语句' }],
},
});
function validateForm() {
formRef.value
.validate()
.then(async () => {
return true;
})
.catch(async () => {
return false;
});
}
function getForm() {
let rows = data.formData;
rows.name = data.formData.Sql;
return rows;
}
defineExpose({
getForm,
validateForm,
});
</script>
<style scoped>
.l-from-body {
padding: 10px;
}
</style>

View File

@ -0,0 +1,63 @@
/* 改变主题色变量 */
// $--color-primary: #1890ff;
// $--color-danger: #ff4d4f;
/* 改变 icon 字体路径变量,必需 */
.process-drawer .el-drawer__header {
padding: 16px 16px 8px 16px;
margin: 0;
line-height: 24px;
font-size: 18px;
color: #303133;
box-sizing: border-box;
border-bottom: 1px solid #e8e8e8;
}
div[class^="el-drawer"]:focus,
span:focus {
outline: none;
}
.process-drawer .el-drawer__body {
box-sizing: border-box;
padding: 16px;
width: 100%;
overflow-y: auto;
}
.process-design {
.el-table td,
.el-table th {
color: #333;
}
.el-dialog__header {
padding: 16px 16px 8px 16px;
box-sizing: border-box;
border-bottom: 1px solid #e8e8e8;
}
.el-dialog__body {
padding: 16px;
max-height: 80vh;
box-sizing: border-box;
overflow-y: auto;
}
.el-dialog__footer {
padding: 16px;
box-sizing: border-box;
border-top: 1px solid #e8e8e8;
}
.el-dialog__close {
font-weight: 600;
}
.el-select {
width: 100%;
}
.el-divider:not(.el-divider--horizontal) {
margin: 0 8px ;
}
.el-divider.el-divider--horizontal {
margin: 16px 0;
}
}

View File

@ -0,0 +1,171 @@
@import "./flow-element-variables.scss";
@import "bpmn-js-token-simulation/assets/css/bpmn-js-token-simulation.css";
// @import "bpmn-js-token-simulation/assets/css/font-awesome.min.css";
// @import "bpmn-js-token-simulation/assets/css/normalize.css";
@import "bpmn-js/dist/assets/diagram-js.css";
@import "bpmn-js/dist/assets/bpmn-font/css/bpmn.css";
@import "bpmn-js/dist/assets/bpmn-font/css/bpmn-codes.css";
@import "./process-designer.scss";
@import "./process-panel.scss";
$success-color: #4eb819;
$primary-color: #409EFF;
$warning-color: #E6A23C;
$danger-color: #F56C6C;
$cancel-color: #909399;
.process-viewer {
position: relative;
border: 1px solid #EFEFEF;
background: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PHBhdHRlcm4gaWQ9ImEiIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgcGF0dGVyblVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHBhdGggZD0iTTAgMTBoNDBNMTAgMHY0ME0wIDIwaDQwTTIwIDB2NDBNMCAzMGg0ME0zMCAwdjQwIiBmaWxsPSJub25lIiBzdHJva2U9IiNlMGUwZTAiIG9wYWNpdHk9Ii4yIi8+PHBhdGggZD0iTTQwIDBIMHY0MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZTBlMGUwIi8+PC9wYXR0ZXJuPjwvZGVmcz48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSJ1cmwoI2EpIi8+PC9zdmc+') repeat!important;
.success-arrow {
fill: $success-color;
stroke: $success-color;
}
.success-conditional {
fill: white;
stroke: $success-color;
}
.fail-arrow {
fill: $warning-color;
stroke: $warning-color;
}
.fail-conditional {
fill: white;
stroke: $warning-color;
}
.success.djs-connection {
.djs-visual path {
stroke: $success-color!important;
marker-end: url(#sequenceflow-end-white-success)!important;
}
}
.success.djs-connection.condition-expression {
.djs-visual path {
marker-start: url(#conditional-flow-marker-white-success)!important;
}
}
.success.djs-shape {
.djs-visual rect {
stroke: $success-color!important;
fill: $success-color!important;
fill-opacity: 0.15!important;
}
.djs-visual polygon {
stroke: $success-color!important;
}
.djs-visual path:nth-child(2) {
stroke: $success-color!important;
fill: $success-color!important;
}
.djs-visual circle {
stroke: $success-color!important;
fill: $success-color!important;
fill-opacity: 0.15!important;
}
}
.primary.djs-shape {
.djs-visual rect {
stroke: $primary-color!important;
fill: $primary-color!important;
fill-opacity: 0.15!important;
}
.djs-visual polygon {
stroke: $primary-color!important;
}
.djs-visual circle {
stroke: $primary-color!important;
fill: $primary-color!important;
fill-opacity: 0.15!important;
}
}
.warning.djs-connection {
.djs-visual path {
stroke: $warning-color!important;
marker-end: url(#sequenceflow-end-white-fail)!important;
}
}
.warning.djs-connection.condition-expression {
.djs-visual path {
marker-start: url(#conditional-flow-marker-white-fail)!important;
}
}
.warning.djs-shape {
.djs-visual rect {
stroke: $warning-color!important;
fill: $warning-color!important;
fill-opacity: 0.15!important;
}
.djs-visual polygon {
stroke: $warning-color!important;
}
.djs-visual path:nth-child(2) {
stroke: $warning-color!important;
fill: $warning-color!important;
}
.djs-visual circle {
stroke: $warning-color!important;
fill: $warning-color!important;
fill-opacity: 0.15!important;
}
}
.danger.djs-shape {
.djs-visual rect {
stroke: $danger-color!important;
fill: $danger-color!important;
fill-opacity: 0.15!important;
}
.djs-visual polygon {
stroke: $danger-color!important;
}
.djs-visual circle {
stroke: $danger-color!important;
fill: $danger-color!important;
fill-opacity: 0.15!important;
}
}
.cancel.djs-shape {
.djs-visual rect {
stroke: $cancel-color!important;
fill: $cancel-color!important;
fill-opacity: 0.15!important;
}
.djs-visual polygon {
stroke: $cancel-color!important;
}
.djs-visual circle {
stroke: $cancel-color!important;
fill: $cancel-color!important;
fill-opacity: 0.15!important;
}
}
}
.process-viewer .djs-tooltip-container, .process-viewer .djs-overlay-container, .process-viewer .djs-palette {
display: none;
}

View File

@ -0,0 +1,153 @@
// token-simulation
.djs-palette {
background: var(--palette-background-color);
border: solid 1px var(--palette-border-color) !important;
border-radius: 2px;
}
.my-process-designer {
padding: 5px 0 10px 10px;
display: flex;
flex-direction: column;
width: 70%;
height: 100%;
box-sizing: border-box;
.my-process-designer__header {
width: 100%;
min-height: 36px;
.el-button {
text-align: center;
}
.el-button-group {
margin: 4px;
}
.el-tooltip__popper {
.el-button {
width: 100%;
text-align: left;
padding-left: 8px;
padding-right: 8px;
}
.el-button:hover {
background: rgba(64, 158, 255, 0.8);
color: #ffffff;
}
}
.align {
position: relative;
i {
&:after {
content: "|";
position: absolute;
transform: rotate(90deg) translate(200%, 60%);
}
}
}
.align.align-left i {
transform: rotate(90deg);
}
.align.align-right i {
transform: rotate(-90deg);
}
.align.align-top i {
transform: rotate(180deg);
}
.align.align-bottom i {
transform: rotate(0deg);
}
.align.align-center i {
transform: rotate(90deg);
&:after {
transform: rotate(90deg) translate(0, 60%);
}
}
.align.align-middle i {
transform: rotate(0deg);
&:after {
transform: rotate(90deg) translate(0, 60%);
}
}
}
.my-process-designer__container {
display: inline-flex;
width: 100%;
flex: 1;
.my-process-designer__canvas {
flex: 1;
height: 100%;
position: relative;
background: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PHBhdHRlcm4gaWQ9ImEiIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgcGF0dGVyblVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHBhdGggZD0iTTAgMTBoNDBNMTAgMHY0ME0wIDIwaDQwTTIwIDB2NDBNMCAzMGg0ME0zMCAwdjQwIiBmaWxsPSJub25lIiBzdHJva2U9IiNlMGUwZTAiIG9wYWNpdHk9Ii4yIi8+PHBhdGggZD0iTTQwIDBIMHY0MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZTBlMGUwIi8+PC9wYXR0ZXJuPjwvZGVmcz48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSJ1cmwoI2EpIi8+PC9zdmc+")
repeat !important;
div.toggle-mode {
display: none;
}
}
.my-process-designer__property-panel {
height: 100%;
overflow: scroll;
overflow-y: auto;
z-index: 10;
* {
box-sizing: border-box;
}
}
svg {
width: 100%;
height: 100%;
min-height: 100%;
overflow: hidden;
}
}
}
//
.djs-palette.open {
.djs-palette-entries {
div[class^="bpmn-icon-"]:before,
div[class*="bpmn-icon-"]:before {
line-height: unset;
}
div.entry {
position: relative;
}
div.entry:hover {
&::after {
width: max-content;
content: attr(title);
vertical-align: text-bottom;
position: absolute;
right: -10px;
top: 0;
bottom: 0;
overflow: hidden;
transform: translateX(100%);
font-size: 0.5em;
display: inline-block;
text-decoration: inherit;
font-variant: normal;
text-transform: none;
background: #fafafa;
box-shadow: 0 0 6px #eeeeee;
border: 1px solid #cccccc;
box-sizing: border-box;
padding: 0 16px;
border-radius: 4px;
z-index: 100;
}
}
}
}
pre {
margin: 0;
height: 100%;
overflow: hidden;
max-height: calc(80vh - 32px);
overflow-y: auto;
}
.hljs {
word-break: break-word;
white-space: pre-wrap;
}
.hljs * {
font-family: Consolas, Monaco, monospace;
}

View File

@ -0,0 +1,110 @@
.process-design {
.process-panel__container {
box-sizing: border-box;
padding: 0 8px;
border-left: 1px solid #eeeeee;
box-shadow: 0 0 8px #cccccc;
max-height: 100%;
overflow-y: scroll;
}
.panel-tab__title {
font-weight: 600;
padding: 0 8px;
font-size: 1.1em;
line-height: 1.2em;
i {
margin-right: 8px;
font-size: 1.2em;
}
}
.panel-tab__content {
width: 100%;
box-sizing: border-box;
border-top: 1px solid #eeeeee;
padding: 8px 16px;
.panel-tab__content--title {
display: flex;
justify-content: space-between;
padding-bottom: 8px;
span {
flex: 1;
text-align: left;
}
}
}
.element-property {
width: 100%;
display: flex;
align-items: flex-start;
margin: 8px 0;
.element-property__label {
display: block;
width: 90px;
text-align: right;
overflow: hidden;
padding-right: 12px;
line-height: 32px;
font-size: 14px;
box-sizing: border-box;
}
.element-property__value {
flex: 1;
line-height: 32px;
}
.el-form-item {
width: 100%;
margin-bottom: 0;
padding-bottom: 18px;
}
}
.list-property {
flex-direction: column;
.element-listener-item {
width: 100%;
display: inline-grid;
grid-template-columns: 16px auto 32px 32px;
grid-column-gap: 8px;
}
.element-listener-item + .element-listener-item {
margin-top: 8px;
}
}
.listener-filed__title {
display: inline-flex;
width: 100%;
justify-content: space-between;
align-items: center;
margin-top: 0;
span {
width: 200px;
text-align: left;
font-size: 14px;
}
i {
margin-right: 8px;
}
}
.element-drawer__button {
margin-top: 8px;
width: 100%;
display: inline-flex;
justify-content: space-around;
}
.element-drawer__button > .el-button {
width: 100%;
}
.el-collapse-item__content {
padding-bottom: 0;
}
.el-input.is-disabled .el-input__inner {
color: #999999;
}
.el-form-item.el-form-item--mini {
margin-bottom: 0;
& + .el-form-item {
margin-top: 16px;
}
}
}

View File

@ -0,0 +1,95 @@
import CryptoJS from 'crypto-js/crypto-js';
// 创建监听器实例
export function createListenerObject(options, isTask, prefix) {
const listenerObj = Object.create(null);
listenerObj.event = options.event;
// isTask && (listenerObj.id = options.id); // 任务监听器特有的 id 字段
switch (options.listenerType) {
case "scriptListener":
listenerObj.script = createScriptObject(options, prefix);
break;
case "expressionListener":
listenerObj.expression = options.expression;
break;
case "delegateExpressionListener":
listenerObj.delegateExpression = options.delegateExpression;
break;
default:
listenerObj.class = options.class;
}
// 注入字段
if (options.fields) {
listenerObj.fields = options.fields.map(field => {
return createFieldObject(field, prefix);
});
}
// 任务监听器的 定时器 设置
if (isTask && options.event === "timeout" && !!options.eventDefinitionType) {
const timeDefinition = window.bpmnInstances.moddle.create("bpmn:FormalExpression", { body: options.eventTimeDefinitions });
const TimerEventDefinition = window.bpmnInstances.moddle.create("bpmn:TimerEventDefinition", {
id: `TimerEventDefinition_${uuid(8)}`,
[`time${options.eventDefinitionType.replace(/^\S/, s => s.toUpperCase())}`]: timeDefinition
});
listenerObj.eventDefinitions = [TimerEventDefinition];
}
return window.bpmnInstances.moddle.create(`${prefix}:${isTask ? "TaskListener" : "ExecutionListener"}`, listenerObj);
}
// 创建 监听器的注入字段 实例
export function createFieldObject(option, prefix) {
const { name, fieldType, string, expression } = option;
const fieldConfig = fieldType === "string" ? { name, string } : { name, expression };
return window.bpmnInstances.moddle.create(`${prefix}:Field`, fieldConfig);
}
// 创建脚本实例
export function createScriptObject(options, prefix) {
const { scriptType, scriptFormat, value, resource } = options;
const scriptConfig = scriptType === "inlineScript" ? { scriptFormat, value } : { scriptFormat, resource };
return window.bpmnInstances.moddle.create(`${prefix}:Script`, scriptConfig);
}
// 更新元素扩展属性
export function updateElementExtensions(element, extensionList) {
const extensions = window.bpmnInstances.moddle.create("bpmn:ExtensionElements", {
values: extensionList
});
window.bpmnInstances.modeling.updateProperties(element, {
extensionElements: extensions
});
}
// 创建一个id
export function uuid(length = 8, chars) {
let result = "";
let charsString = chars || "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
for (let i = length; i > 0; --i) {
result += charsString[Math.floor(Math.random() * charsString.length)];
}
return result;
}
export function lr_AESKey(key){
const length = key.length
if(length <32){
for(let i=0,len =32 - length;i<len;i++){
key += "0"
}
return key
}
else{
return key.substring(0, 32)
}
}
// AES加解密算法
export function lr_AESEncrypt(source, key) {
key = CryptoJS.enc.Utf8.parse(lr_AESKey(key))//32位
let iv = CryptoJS.enc.Utf8.parse("1234567890000000")//16位
//let srcs = CryptoJS.enc.Utf8.parse(source)
let encrypted = CryptoJS.AES.encrypt(source, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
})
return encrypted.toString()
}

View File

@ -0,0 +1,3 @@
export { default as ProcessDesigner } from './package/designer/ProcessDesigner.vue';
export { default as ProcessDesignerPage } from './index.vue';

View File

@ -0,0 +1,2 @@
export { default as ProcessViewer } from './index.vue';

View File

@ -0,0 +1,225 @@
<template>
<div class="my-process-designer">
<div class="my-process-designer__header">
<slot name="control-header"></slot>
<template v-if="!$slots['control-header']">
<div slot="content">
<a-space>
<a-tooltip placement="bottom" class="ml-2" title="缩小视图">
<a-button
:disabled="data.defaultZoom <= 0.3"
:icon="h(ZoomOutOutlined)"
@click="processZoomOut()"
/>
</a-tooltip>
<a-button>{{ Math.floor(data.defaultZoom * 10 * 10) + '%' }}</a-button>
<a-tooltip placement="bottom" title="放大视图">
<a-button
:disabled="data.defaultZoom >= 3.9"
:icon="h(ZoomInOutlined)"
@click="processZoomIn()"
/>
</a-tooltip>
<div class="ml-2 tag-box">
<a-tag color="processing">正在审核</a-tag>
<a-tag color="success">已审核</a-tag>
</div>
</a-space>
</div>
</template>
<div> </div>
</div>
<div class="my-process-designer__container">
<div class="my-process-designer__canvas" ref="bpmn-canvas" id="view"></div>
</div>
</div>
</template>
<script lang="ts" setup>
import '@/components/ProcessDesigner/package/theme/index.scss';
import { h, reactive, onMounted, defineProps, defineEmits, watch } from 'vue';
import { ZoomOutOutlined, ZoomInOutlined } from '@ant-design/icons-vue';
import BpmnViewer from 'bpmn-js/lib/Viewer';
import MoveCanvasModule from 'diagram-js/lib/navigation/movecanvas';
const emit = defineEmits(['event', 'element-click']);
const data = reactive({
bpmnModeler: null,
defaultZoom: 1,
});
const props = defineProps({
xml: String,
flowViewer: {
type: Object,
default: () => ({
finishedTaskSet: [],
finishedSequenceFlowSet: [],
unfinishedTaskSet: [],
rejectedTaskSet: [],
}),
},
events: {
type: Array,
default: () => ['element.click'],
},
});
watch(
() => props.xml,
(newVal) => {
createDiagram(newVal);
},
);
onMounted(() => {
initBpmnModeler();
createDiagram(props.xml);
});
function initBpmnModeler() {
if (data.bpmnModerler) return;
const containerEl = document.getElementById('view');
data.bpmnModerler && data.bpmnModerler.destroy(); //
data.bpmnModerler = new BpmnViewer({
container: containerEl,
additionalModules: [
//
MoveCanvasModule,
],
});
initModelListeners();
}
function initModelListeners() {
const EventBus = data.bpmnModerler.get('eventBus');
// , . - ,
props.events.forEach((event) => {
EventBus.on(event, function (eventObj) {
let eventName = event.replace('.', '-');
let element = eventObj ? eventObj.element : null;
emit(eventName, element, eventObj);
emit('event', eventName, element, eventObj);
});
});
}
async function createDiagram(xml) {
const viewer = data.bpmnModerler;
if (xml != null && xml !== '') {
try {
const result = await data.bpmnModerler.importXML(xml);
const { warnings } = result;
if (warnings && warnings.length) {
warnings.forEach((warn) => console.warn(warn));
}
const canvas = viewer.get('canvas');
canvas.zoom('fit-viewport');
addCustomDefs();
} catch (e) {
} finally {
setNodeColor();
}
}
}
//
function addCustomDefs() {
const canvas = data.bpmnModerler.get('canvas');
const svg = canvas._svg;
const customSuccessDefs = this.$refs.customSuccessDefs;
const customFailDefs = this.$refs.customFailDefs;
svg.appendChild(customSuccessDefs);
svg.appendChild(customFailDefs);
}
//
function setNodeColor() {
const elementRegistry = data.bpmnModerler.get('elementRegistry');
console.log(elementRegistry);
let { finishedTaskSet, rejectedTaskSet, unfinishedTaskSet, finishedSequenceFlowSet } =
props.flowViewer;
if (Array.isArray(unfinishedTaskSet)) {
unfinishedTaskSet.forEach((item) => {
if (elementRegistry._elements[item]) {
const element = elementRegistry._elements[item].gfx;
element.classList.add('nodeProcing');
}
});
}
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');
// }
// }
});
}
if (Array.isArray(finishedSequenceFlowSet)) {
finishedSequenceFlowSet.forEach((item) => {
if (elementRegistry._elements[item]) {
const element = elementRegistry._elements[item].gfx;
element.classList.add('nodeSuccess');
}
});
}
if (Array.isArray(finishedTaskSet)) {
finishedTaskSet.forEach((item) => {
if (elementRegistry._elements[item]) {
const element = elementRegistry._elements[item].gfx;
element.classList.add('nodeSuccess');
}
});
}
}
function processZoomIn(zoomStep = 0.1) {
let newZoom = Math.floor(data.defaultZoom * 100 + zoomStep * 100) / 100;
if (newZoom > 4) {
throw new Error('[Process Designer Warn ]: The zoom ratio cannot be greater than 4');
}
data.defaultZoom = newZoom;
data.bpmnModerler.get('canvas').zoom(data.defaultZoom);
}
function processZoomOut(zoomStep = 0.1) {
let newZoom = Math.floor(data.defaultZoom * 100 - zoomStep * 100) / 100;
if (newZoom < 0.2) {
throw new Error('[Process Designer Warn ]: The zoom ratio cannot be less than 0.2');
}
data.defaultZoom = newZoom;
data.bpmnModerler.get('canvas').zoom(data.defaultZoom);
}
function getOperationTagType(type) {
return 'success';
}
</script>
<style lang="less" scoped>
.my-process-designer {
width: 100%;
}
.tag-box {
float: right;
}
::v-deep .ant-tag-success {
padding: 5px 11px;
}
::v-deep .ant-tag-processing {
padding: 5px 11px;
}
::v-deep .bjs-container a {
visibility: hidden;
}
.canvas {
width: 100%;
height: 100%;
:global(.nodeSuccess .djs-visual > :nth-child(1)) {
stroke: #52c41a !important;
stroke-width: 3px;
}
:global(.nodeProcing .djs-visual > :nth-child(1)) {
stroke: #1890ff !important;
stroke-width: 3px;
}
}
</style>

View File

@ -1,27 +1,28 @@
<template>
<!-- <PageWrapper dense contentFullHeight fixedHeight contentClass="flex"> -->
<div class="select-account">
<DeptTree class="w-1/4 xl:w-1/5" @select="handleSelect" />
<BasicTable @register="registerTable" class="w-3/4 xl:w-4/5" :searchInfo="searchInfo"> </BasicTable>
</div>
<div class="select-account">
<DeptTree class="w-1/4 xl:w-1/5" @select="handleSelect" />
<BasicTable @register="registerTable" class="w-3/4 xl:w-4/5" :searchInfo="searchInfo" />
</div>
<!-- </PageWrapper> -->
</template>
<script lang="ts" setup>
import { reactive } from 'vue';
import { reactive, watch, defineProps } from 'vue';
import { BasicTable, useTable, TableAction } from '@/components/Table';
import { getAccountList, deleteAccount } from '@/api/demo/system';
import { PageWrapper } from '@/components/Page';
import DeptTree from './DeptTree.vue';
import { useMessage } from '/@/hooks/web/useMessage';
import { columns, searchFormSchema } from './account.data';
defineOptions({ name: 'AccountManagement' });
const searchInfo = reactive < Recordable > ({});
const [registerTable, { reload, updateTableDataRecord, getSelectRows, clearSelectedRowKeys }] = useTable({
const searchInfo = reactive<Recordable>({});
const [
registerTable,
{ reload, updateTableDataRecord, getSelectRows, clearSelectedRowKeys, setSelectedRowKeys },
] = useTable({
title: '账号列表',
api: getAccountList,
rowKey: 'id',
@ -31,7 +32,8 @@
schemas: searchFormSchema,
autoSubmitOnEnter: true,
},
rowSelection: {//
rowSelection: {
//
type: 'checkbox',
},
useSearchForm: true,
@ -44,6 +46,20 @@
return info;
},
});
const props = defineProps({
selectListValue: {
type: Object,
default: () => {
return [];
},
},
});
watch(
() => props.selectListValue,
(newVal: any) => {
setSelectedRowKeys(newVal);
},
);
function handleSelect(orgId = '') {
searchInfo.orgId = orgId;
reload();
@ -51,16 +67,15 @@
function getRow() {
let rows = getSelectRows();
console.log(rows)
return rows
return rows;
}
defineExpose({
getRow
})
getRow,
});
</script>
<style scoped>
.select-account{
.select-account {
display: flex;
height: 100%;
}
</style>
</style>

View File

@ -1,5 +1,5 @@
@use './common';
@use './variable';
@use './toolbar';
@use './component';
@import './common';
@import './variable';
@import './toolbar';
@import './component';
// @import 'vxe-table/styles/index';

View File

@ -0,0 +1,7 @@
import lrLayout from './src/lrLayout.vue'
lrLayout.install = function(Vue) {
Vue.component(lrLayout.name, lrLayout)
}
export default lrLayout

View File

@ -0,0 +1,208 @@
<template>
<div class="l-layout" :style="{'padding-left':leftWidth}">
<div class="l-layout--left" :style="{'width':leftWidth}" >
<div class="l-layout--wrapper" ><slot name="left"></slot></div>
<div v-if="leftMove" class="l-layout--move" @mousedown="onMousedown('left',$event)" ></div>
</div>
<div class="l-layout--container" :style="{'padding-right':rightWidth}" >
<div class="l-layout--right" :style="{ 'width':rightWidth}">
<div class="l-layout--wrapper" ><slot name="right"></slot></div>
<div v-if="rightMove" class="l-layout--move" @mousedown="onMousedown('right',$event)" ></div>
</div>
<div class="l-layout--container" :style="{'padding-bottom':bottomHight}" >
<div class="l-layout--bottom" :style="{'height':bottomHight}" >
<div class="l-layout--wrapper" > <slot name="bottom"></slot></div>
<div v-if="bottomMove" class="l-layout--move" @mousedown="onMousedown('bottom',$event)" ></div>
</div>
<div class="l-layout--container" :style="{'padding-top':topHight}" >
<div class="l-layout--top" :style="{'height':topHight}" >
<div class="l-layout--wrapper" ><slot name="top"></slot></div>
<div v-if="topMove" class="l-layout--move" @mousedown="onMousedown('top',$event)" ></div>
</div>
<div class="l-layout--wrapper" ref="mid">
<slot></slot>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name:'l-layout',
props: {
left: {
type: Number,
default: 200
},
leftMove: {
type: Boolean,
default: true
},
right: {
type: Number,
default: 200
},
rightMove:{
type: Boolean,
default: true
},
top: {
type: Number,
default: 60
},
topMove:{
type: Boolean,
default: true
},
bottom:{
type: Number,
default: 60
},
bottomMove:{
type: Boolean,
default: true
},
},
data () {
return {
mleft:this.left,
mright:this.right,
mtop:this.top,
mbottom:this.bottom,
move:{
type:'',
isMove:false,
pageX:0,
pageY:0,
size:0,
h:0,
w:0,
}
};
},
mounted () {
},
watch:{
left(val){
this.mleft = val;
},
right(val){
this.mright = val;
},
top(val){
this.mtop = val;
},
bottom(val){
this.mbottom = val;
}
},
computed:{
leftWidth:function(){
if(this.$slots.left){
return this.mleft + 'px'
}
else{
return '0'
}
},
rightWidth:function(){
if(this.$slots.right){
return this.mright + 'px'
}
else{
return '0'
}
},
topHight:function(){
if(this.$slots.top){
return this.mtop + 'px'
}
else{
return '0'
}
},
bottomHight:function(){
if(this.$slots.bottom){
return this.mbottom + 'px'
}
else{
return '0'
}
}
},
methods:{
onMousedown:function(type,e){
this.move.type = type;
this.move.isMove = true;
this.move.pageX = e.pageX;
this.move.pageY = e.pageY;
this.move.size = this["m"+type];
this.move.h = this.$refs.mid.clientHeight;
this.move.w = this.$refs.mid.clientWidth;
document.onmouseup = this.onMouseup;
document.onmousemove = this.onMousemove;
},
onMousemove:function(e){
if(this.move.isMove){
switch(this.move.type){
case 'left':
var x1 = e.pageX - this.move.pageX;
var left = this.move.size + x1;
if(left < 0){
left = 4;
}
else if(left > this.move.size + this.move.w){
left = this.move.size + this.move.w
}
this.mleft = left;
break;
case 'right':
var x2 = e.pageX - this.move.pageX;
var right = this.move.size - x2;
if(right < 0){
right = 4;
}
else if(right > this.move.size + this.move.w){
right = this.move.size + this.move.w
}
this.mright = right;
break;
case 'top':
var y = e.pageY - this.move.pageY;
var top = this.move.size + y;
if(top < 0){
top = 4;
}
else if(top > this.move.size + this.move.h){
top = this.move.size + this.move.h
}
this.mtop = top;
break;
case 'bottom':
var y2 = e.pageY - this.move.pageY;
var bottom = this.move.size - y2;
if(bottom < 0){
bottom = 4;
}
else if(bottom > this.move.size + this.move.h){
bottom = this.move.size + this.move.h
}
this.mbottom = bottom;
break;
}
}
},
onMouseup:function(){
this.move.isMove = false;
document.onmousemove = document.onmouseup = null;
}
}
}
</script>
<style lang="less">
// @import './index.less';
</style>

View File

@ -0,0 +1,7 @@
import lrPanel from './src/lrPanel.vue'
lrPanel.install = function(Vue) {
Vue.component(lrPanel.name, lrPanel)
}
export default lrPanel

View File

@ -0,0 +1,68 @@
<template>
<div class="l-panel">
<div class="l-panel--warpper" :style="{'padding-top':paddingTop}" >
<div v-if="title || $slots.title" class="l-panel--title" >
<slot name="title">{{ title }}</slot>
</div>
<div v-if="$slots.toolLeft || $slots.toolRight" class="l-panel--tool" :style="{'top':toolTop}" >
<div class="l-panel--tool-left">
<slot name="toolLeft" ></slot>
</div>
<div class="l-panel--tool-right">
<slot name="toolRight" ></slot>
</div>
</div>
<div class="l-panel--body" >
<slot></slot>
</div>
</div>
</div>
</template>
<script>
export default {
name:'l-panel',
props: {
title:String,
loading:{
type:Boolean,
default:false
}
},
data () {
return {
};
},
mounted () {
},
computed:{
paddingTop:function(){
var ptop = 0;
if(this.title || this.$slots.title){
ptop += 40;
}
if(this.$slots.toolLeft || this.$slots.toolRight){
ptop += 40;
}
return ptop + 'px';
},
toolTop:function(){
if(this.title || this.$slots.title){
return '40px'
}
else{
return '0'
}
}
},
methods:{
}
}
</script>
<style lang="less">
// @import './index.less';
.l-panel--warpper {
background-color: @component-background;
}
</style>

View File

@ -41,11 +41,33 @@ export const LoginRoute: AppRouteRecordRaw = {
title: t('routes.basic.login'),
},
};
export const FORMCALLPAGE_ROUTE: AppRouteRecordRaw = {
path: '/formCallPageParent',
component: LAYOUT,
name: 'formCallPageParent',
meta: {
title: '表单',
hideBreadcrumb: true,
hideMenu: true,
},
children: [
{
path: '/formCallPage',
name: 'formCallPage',
component: () => import('@/views/demo/onlineform/formCall/index.vue'),
meta: {
title: '表单调用',
hideBreadcrumb: true,
},
},
],
};
// Basic routing without permission
// 未经许可的基本路由
export const basicRoutes = [
LoginRoute,
RootRoute,
FORMCALLPAGE_ROUTE,
...mainOutRoutes,
REDIRECT_ROUTE,
PAGE_NOT_FOUND_ROUTE,

View File

@ -0,0 +1,278 @@
<template>
<template v-if="['Grid'].includes(schema.component) && schema.type == 'subTable'">
<a-table
class="sub-table"
:columns="subTableColumns"
:data-source="subTableList"
:pagination="false"
style="width: 100%"
v-if="subTableId"
:scroll="scrollValue"
>
<template #headerCell="{ column, record }">
<template v-if="column.key === 'setting'">
<PlusOutlined class="icon-button" @click="addListItem" />
</template>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'setting'">
<DeleteOutlined class="icon-button" @click="delListItem(column, record)" />
</template>
<template v-else>
<FormItem :data="column" :record="record" />
</template>
</template>
</a-table>
</template>
<template v-else-if="['Grid'].includes(schema.component)">
<Row class="grid-row">
<Col
class="grid-col"
v-for="(colItem, index) in schema.columns"
:key="index"
:span="colItem.span"
>
<FormRender
v-for="(item, k) in colItem.children"
:key="k"
:schema="item"
:formData="formData"
:formConfig="formConfig"
:setFormModel="setFormModel"
/>
</Col>
</Row>
</template>
<template v-else-if="['Card'].includes(schema.component)">
<a-row class="grid-row" style="width: 100%; padding: 10px">
<a-col class="grid-col" :span="schema.colProps.span">
<a-card
:title="schema.label"
:class="
schema.shadow === 'always'
? 'card-always'
: schema.shadow === 'hover'
? 'card-hover'
: ''
"
>
<a-row class="grid-row">
<a-col
class="grid-col"
v-for="(colItem, index) in schema.columns"
:key="index"
:span="colItem.span"
>
<template v-for="(item, k) in colItem.children" :key="k">
<FormRender
:schema="item"
:formData="formData"
:formConfig="formConfig"
:setFormModel="setFormModel"
/>
</template>
</a-col>
</a-row>
</a-card>
</a-col>
</a-row>
</template>
<template v-else-if="['Tabs'].includes(schema.component)">
<a-tabs style="width: 100%">
<a-tab-pane
v-for="(colItem, index) in schema.componentProps.options"
:tab="colItem.label"
:key="index"
>
<FormRender
v-for="(item, k) in colItem.children"
:key="k"
:schema="item"
:formData="formData"
:formConfig="formConfig"
:setFormModel="setFormModel"
/>
</a-tab-pane>
</a-tabs>
</template>
<template v-else-if="['CardGroup'].includes(schema.component)">
<div style="width: 100%">
<div style="display: flex">
{{ schema.label }}
<div style="margin-left: 10px">
<a-radio-group
v-model:value="noTitleKey"
:options="schema.componentProps.options"
/>
</div>
</div>
<template v-for="(item, index) in schema.componentProps.options" :key="index">
<a-card style="width: 100%" v-show="noTitleKey === item.value">
<FormRender
v-for="(childItem, k) in item.children"
:key="k"
:schema="childItem"
:formData="formData"
:formConfig="formConfig"
:setFormModel="setFormModel"
/>
</a-card>
</template>
</div>
</template>
<template v-else>
<VFormItem
v-if="(isCreateOrModifyComponent && schema.display) || !isCreateOrModifyComponent"
:formConfig="formConfig"
:schema="schema"
:formData="formData"
:setFormModel="setFormModel"
@change="emit('change', { schema: schema, value: $event, subTableList: subTableList })"
@submit="emit('submit', schema)"
@reset="emit('reset')"
>
<template
v-if="schema.componentProps && schema.componentProps.slotName"
#[schema.componentProps!.slotName]
>
<slot :name="schema.componentProps!.slotName"></slot>
</template>
</VFormItem>
</template>
</template>
<script setup lang="ts">
import { ref, PropType } from 'vue';
import { IVFormComponent, IFormConfig } from '../../../typings/v-form-component';
import VFormItem from '../../VFormItem/index.vue';
import { Row, Col } from 'ant-design-vue';
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons-vue';
import { v4 as uuidv4 } from 'uuid';
import FormItem from '@/views/demo/onlineform/formCall/ShowFormModal/FormItem/index.vue';
const props = defineProps({
formData: {
type: Object,
default: () => ({}),
},
schema: {
type: Object as PropType<IVFormComponent>,
default: () => ({}),
},
formConfig: {
type: Object as PropType<IFormConfig>,
default: () => [] as IFormConfig[],
},
setFormModel: {
type: Function as PropType<(key: string, value: any) => void>,
default: null,
},
});
console.log('itemitem', props.formConfig)
const emit = defineEmits(['change', 'submit', 'reset']);
const scrollValue = ref();
const isCreateOrModifyComponent = [
'createuser',
'createtime',
'modifyuser',
'modifytime',
].includes(props.schema.type);
let subTableColumns = ref([
{
dataIndex: 'setting',
key: 'setting',
fixed: 'left',
width: 60,
},
]);
let subTableId = ref(null);
let subTableData = ref([]);
let subTableList = ref([]);
const noTitleKey = ref('0')
if(props.schema.component === 'CardGroup'){
noTitleKey.value = props.schema.componentProps.options[0].value
}
// if (props.formConfig.schemas) {
// props.formConfig.schemas.forEach((item) => {
// if (item.type === 'subTable') {
// subTableId.value = item.field;
// let tableData = [];
// item.columns.forEach((itemColumn) => {
// itemColumn.children.forEach((itemColumnChild) => {
// tableData.push(itemColumnChild);
// subTableColumns.value.push({
// key: itemColumnChild.field,
// title: itemColumnChild.label,
// dataIndex: itemColumnChild.field,
// width: 120,
// ...itemColumnChild,
// });
// });
// });
// scrollValue.value = { x: (subTableColumns.value.length - 1) * 140, y: 300 };
// subTableData.value = tableData;
// }else{
// }
// });
// }
console.log("props.schema",props.schema)
if (props.schema) {
if(props.schema.type === "subTable"){
subTableId.value = props.schema.field
let tableData = [];
props.schema.columns.forEach((itemColumn) => {
itemColumn.children.forEach((itemColumnChild) => {
tableData.push(itemColumnChild);
subTableColumns.value.push({
key: itemColumnChild.field,
title: itemColumnChild.label,
dataIndex: itemColumnChild.field,
width: 120,
...itemColumnChild,
});
});
});
scrollValue.value = { x: (subTableColumns.value.length - 1) * 140, y: 300 };
subTableData.value = tableData;
}
}
const addListItem = () => {
let keyValue = uuidv4();
let emptyItem = { key: keyValue };
subTableData.value.map((item) => {
if (item.component == 'InputGuid') {
emptyItem[item.field] = keyValue;
} else {
emptyItem[item.field] = '';
}
});
subTableList.value.push(emptyItem);
};
const delListItem = (column, record) => {
subTableList.value = subTableList.value.filter((item) => item.key != record.key);
};
</script>
<style>
.v-form-render-item {
overflow: hidden;
}
/* .card-always{
border-color: transparent;
box-shadow: 0 1px 2px -2px rgba(0, 0, 0, 0.16), 0 3px 6px 0 rgba(0, 0, 0, 0.12), 0 5px 12px 4px rgba(0, 0, 0, 0.09);
}
.card-hover{
transition: box-shadow 0.2s, border-color 0.2s;
}
.card-hover:hover{
border-color: transparent;
box-shadow: 0 1px 2px -2px rgba(0, 0, 0, 0.16), 0 3px 6px 0 rgba(0, 0, 0, 0.12), 0 5px 12px 4px rgba(0, 0, 0, 0.09);
} */
</style>

View File

@ -0,0 +1,156 @@
<!--
* @Description: 表单渲染器根据json生成表单
-->
<template>
<div class="v-form-container">
<Form class="v-form-model" ref="eFormModel" :model="formModel" v-bind="formModelProps">
<Row>
<FormRender
v-for="(schema, index) of noHiddenList"
:key="index"
:schema="schema"
:formConfig="formConfig"
:formData="formModelNew"
@change="handleChange"
:setFormModel="setFormModel"
@submit="handleSubmit"
@reset="resetFields"
>
<template v-if="schema && schema.componentProps" #[`schema.componentProps!.slotName`]>
<slot
:name="schema.componentProps!.slotName"
v-bind="{ formModel: formModel, field: schema.field, schema }"
></slot>
</template>
</FormRender>
</Row>
</Form>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, PropType, provide, ref, unref } from 'vue';
import FormRender from './components/FormRender.vue';
import { IFormConfig, AForm } from '../../typings/v-form-component';
import { Form, Row, Col } from 'ant-design-vue';
import { useFormInstanceMethods } from '../../hooks/useFormInstanceMethods';
import { IProps, IVFormMethods, useVFormMethods } from '../../hooks/useVFormMethods';
import { useVModel } from '@vueuse/core';
import { omit } from 'lodash-es';
export default defineComponent({
name: 'VFormCreate',
components: {
FormRender,
Form,
Row,
},
props: {
fApi: {
type: Object,
},
formModel: {
type: Object,
default: () => ({}),
},
formConfig: {
type: Object as PropType<IFormConfig>,
required: true,
},
},
emits: ['submit', 'change', 'update:fApi', 'update:formModel'],
setup(props, context) {
const wrapperComp = props.formConfig.layout == 'vertical' ? Col : Row;
const { emit } = context;
const eFormModel = ref<AForm | null>(null);
const formModelNew = computed({
get: () => props.formModel,
set: (value) => emit('update:formModel', value),
});
console.log('props.formConfig', props.formConfig)
const noHiddenList = computed(() => {
return (
props.formConfig.schemas &&
props.formConfig.schemas.filter((item) => item.hidden !== true)
);
});
console.log('noHiddenList', noHiddenList)
const fApi = useVModel(props, 'fApi', emit);
const { submit, validate, clearValidate, resetFields, validateField } =
useFormInstanceMethods<['submit', 'change', 'update:fApi', 'update:formModel']>(
props,
formModelNew,
context,
eFormModel,
);
const { linkOn, ...methods } = useVFormMethods<
['submit', 'change', 'update:fApi', 'update:formModel']
>(
{ formConfig: props.formConfig, formData: props.formModel } as unknown as IProps,
context,
eFormModel,
{
submit,
validate,
validateField,
resetFields,
clearValidate,
},
);
fApi.value = methods;
const handleChange = (_event) => {
const { schema, value } = _event;
const { field } = unref(schema);
linkOn[field!]?.forEach((formItem) => {
formItem.update?.(value, formItem, fApi.value as IVFormMethods);
});
};
/**
* 获取表单属性
*/
const formModelProps = computed(
() => omit(props.formConfig, ['disabled', 'labelWidth', 'schemas']) as Recordable,
);
const handleSubmit = () => {
submit();
};
provide('formModel', formModelNew);
const setFormModel = (key, value) => {
formModelNew.value[key] = value;
};
provide<(key: String, value: any) => void>('setFormModelMethod', setFormModel);
// inject
return {
eFormModel,
submit,
validate,
validateField,
resetFields,
clearValidate,
handleChange,
formModelProps,
handleSubmit,
setFormModel,
formModelNew,
wrapperComp,
noHiddenList,
};
},
});
</script>
<style lang="less" scoped>
.v-form-model {
overflow: hidden;
width: 96%;
margin-left: 2%;
}
</style>

View File

@ -0,0 +1,83 @@
<!--
* @Description: 渲染代码
-->
<template>
<Modal
title="代码"
:footer="null"
:open="visible"
@cancel="visible = false"
wrapClassName="v-code-modal"
style="top: 20px"
width="850px"
:destroyOnClose="true"
>
<PreviewCode :editorJson="editorVueJson" fileFormat="vue" />
</Modal>
</template>
<script lang="ts">
import { computed, defineComponent, reactive, toRefs } from 'vue';
import { formatRules, removeAttrs } from '../../../utils';
import PreviewCode from './PreviewCode.vue';
import { IFormConfig } from '../../../typings/v-form-component';
import { Modal } from 'ant-design-vue';
const codeVueFront = `<template>
<div>
<v-form-create
:formConfig="formConfig"
:formData="formData"
v-model="fApi"
/>
<a-button @click="submit"></a-button>
</div>
</template>
<script>
export default {
name: 'Demo',
data () {
return {
fApi:{},
formData:{},
formConfig: `;
/* eslint-disable */
let codeVueLast = `
}
},
methods: {
async submit() {
const data = await this.fApi.submit()
console.log(data)
}
}
}
<\/script>`;
//
export default defineComponent({
name: 'CodeModal',
components: { PreviewCode, Modal },
setup() {
const state = reactive({
visible: false,
jsonData: {} as IFormConfig,
});
const showModal = (formConfig: IFormConfig) => {
formConfig.schemas && formatRules(formConfig.schemas);
state.visible = true;
state.jsonData = formConfig as any;
};
const editorVueJson = computed(() => {
return (
codeVueFront +
JSON.stringify(removeAttrs(state.jsonData as any), null, '\t') +
codeVueLast
);
});
return { ...toRefs(state), editorVueJson, showModal };
},
});
</script>

View File

@ -0,0 +1,592 @@
<!--
* @Description: 组件属性控件
-->
<template>
<div class="properties-content">
<div class="properties-body" v-if="formConfig.currentItem">
<Empty class="hint-box" v-if="!formConfig.currentItem.key" description="未选择组件" />
<Form
label-align="left"
layout="vertical"
v-if="
['Grid'].includes(formConfig.currentItem.component) &&
formConfig.currentItem.type == 'subTable'
"
>
<FormItem v-for="item in subTableOptions" :key="item.name" :label="item.label">
<!-- 处理数组属性placeholder -->
<div v-if="item.children">
<template v-for="(child, index) of item.children" :key="index">
<component
v-if="child.component"
v-bind="child.componentProps"
v-model:value="formConfig.currentItem.componentProps[item.name][index]"
:is="child.component"
/>
</template>
</div>
<!-- 如果不是数组则正常处理属性值 -->
<component
v-else-if="item.component"
class="component-prop"
v-bind="item.componentProps"
:is="item.component"
@change="handleFieldTableChange"
v-model:value="formConfig.currentItem.componentProps[item.name]"
/>
</FormItem>
<FormItem label="控制属性">
<Col v-for="item in SubControllOptions" :key="item.name">
<Checkbox
v-if="showControlAttrs(item.includes)"
v-bind="item.componentProps"
v-model:checked="formConfig.currentItem.componentProps[item.name]"
>
{{ item.label }}
</Checkbox>
</Col>
</FormItem>
</Form>
<Form label-align="left" layout="vertical" v-else>
<!-- 循环遍历渲染组件属性 -->
<div v-if="formConfig.currentItem && formConfig.currentItem.componentProps">
<FormItem v-for="item in inputOptions" :key="item.name" :label="item.label">
<!-- 处理数组属性placeholder -->
<div v-if="item.children">
<template v-for="(child, index) of item.children" :key="index">
<component
v-if="child.component"
v-bind="child.componentProps"
v-model:value="formConfig.currentItem.componentProps[item.name][index]"
:is="child.component"
/>
</template>
</div>
<!-- 如果不是数组则正常处理属性值 -->
<component
v-else-if="item.component"
class="component-prop"
v-bind="item.componentProps"
:is="item.component"
@change="handleFieldTableChange"
v-model:value="formConfig.currentItem.componentProps[item.name]"
/>
</FormItem>
<FormItem label="控制属性">
<Col v-for="item in controlOptions" :key="item.name">
<Checkbox
v-if="showControlAttrs(item.includes)"
v-bind="item.componentProps"
v-model:checked="formConfig.currentItem.componentProps[item.name]"
>
{{ item.label }}
</Checkbox>
</Col>
</FormItem>
<FormItem label="标题集合" v-if="['Transfer'].includes(formConfig.currentItem.component)">
<Input v-model:value="formConfig.currentItem.componentProps.titles[0]" />
<Input v-model:value="formConfig.currentItem.componentProps.titles[1]" />
</FormItem>
</div>
<FormItem label="关联字段">
<Select
mode="multiple"
v-model:value="formConfig.currentItem['link']"
:options="linkOptions"
/>
</FormItem>
<FormItem label="图标" v-if="['Button'].includes(formConfig.currentItem.component)">
<IconPicker v-model:value="formConfig.currentItem.componentProps.icon" />
</FormItem>
<FormItem label="脚本" v-if="['Button'].includes(formConfig.currentItem.component)">
<a-button size="mini" @click="handleButtonClick"> </a-button>
</FormItem>
<FormItem
label="选项"
v-if="
[
'CheckboxGroup',
'RadioGroup',
'TreeSelect',
'Cascader',
'Transfer',
'AutoComplete',
'Tabs',
'CardGroup',
].includes(formConfig.currentItem.component)
"
>
<FormOptions />
</FormItem>
<FormItem label="选项" v-if="['Select'].includes(formConfig.currentItem.component)">
<div class="select-radio">
<a-radio-group v-model:value="formConfig.currentItem.dataType" button-style="solid">
<a-radio-button value="1">静态数据</a-radio-button>
<a-radio-button value="2">数据字典</a-radio-button>
</a-radio-group>
</div>
<template v-if="formConfig.currentItem.dataType === '1'">
<FormOptions />
</template>
<template v-else-if="formConfig.currentItem.dataType === '2'">
<a-select
v-model:value="formConfig.currentItem.dataCode"
show-search
style="width: 100%"
placeholder="请选择字典类型"
:default-active-first-option="false"
:show-arrow="false"
:filter-option="false"
:not-found-content="null"
:options="selectDictionaryData"
@search="handleSearch"
@change="handleChange"
@focus="handleSelectFocus"
></a-select>
</template>
</FormItem>
<FormItem
label="栅格"
v-if="
['Grid'].includes(formConfig.currentItem.component) &&
formConfig.currentItem.label == '栅格布局'
"
>
<FormOptions />
</FormItem>
</Form>
</div>
</div>
<BasicModal
@register="buttonScriptModal"
title="按钮点击脚本"
:height="500"
:width="1000"
:useWrapper="false"
@ok="handleSubmit"
>
<div class="alertModal">
<div class="alertModal_content">
<a-textarea
v-model:value="formContent"
autosize
:auto-size="{ minRows: 22, maxRows: 22 }"
/>
</div>
<div class="alertModal_content">
<a-textarea
class="alertModal_content-textarea"
v-model:value="formItemPropsScript"
autosize
readOnly
:auto-size="{ minRows: 22, maxRows: 22 }"
/>
</div>
</div>
</BasicModal>
</template>
<script lang="ts">
import {
Empty,
Input,
Form,
FormItem,
Switch,
Checkbox,
Select,
InputNumber,
RadioGroup,
Col,
Row,
message,
} from 'ant-design-vue';
import { IconPicker } from '@/components/Icon';
import RadioButtonGroup from '@/components/Form/src/components/RadioButtonGroup.vue';
import { computed, defineComponent, ref, watch, inject } from 'vue';
import { useFormDesignState } from '../../../hooks/useFormDesignState';
import {
baseComponentControlAttrs,
baseComponentAttrs,
baseComponentCommonAttrs,
componentPropsFuncs,
designSubTableAttrs,
designSubControlAttrs,
} from '../../VFormDesign/config/componentPropsConfig';
import FormOptions from './FormOptions.vue';
import { formItemsForEach, remove } from '../../../utils';
import { IBaseFormAttrs } from '../config/formItemPropsConfig';
import { getOutKeyList } from '@/api/formdesign/index';
import { BasicModal, useModal } from '@/components/Modal';
import { formItemPropsScript } from '../../VFormDesign/config/formItemPropsScript';
import { getDictionaryType, getDictionary } from '@/api/sys/categories.ts';
import { useOnlineFormDesignStore } from '@/store/modules/onlineFormDesign';
export default defineComponent({
name: 'ComponentProps',
components: {
FormOptions,
Empty,
Input,
Form,
FormItem,
Switch,
Checkbox,
Select,
InputNumber,
RadioGroup,
RadioButtonGroup,
Col,
Row,
message,
BasicModal,
useModal,
IconPicker,
},
setup() {
const onlineFormDesignStore = useOnlineFormDesignStore();
let fieldTableValue = ref();
let FieldTableOptions = ref();
let ChlidTableOptions = ref(); //
let receivedData = ref();
const selectRadio = ref(0);
const selectDictionary = ref();
const selectDictionaryData = computed(() => onlineFormDesignStore.getSelectDictionaryData());
const timeout = ref();
const handleNextStepsData = inject('handleNextStepsData');
let currentIndex = ref(); //
watch(
() => handleNextStepsData, //
(newVal) => {
FieldTableOptions.value = [];
ChlidTableOptions.value = [];
if (newVal && newVal.value && newVal.value.scheme && newVal.value.scheme.scheme) {
receivedData.value = JSON.parse(newVal.value.scheme.scheme);
receivedData.value.db.forEach((item) => {
FieldTableOptions.value.push({
value: item.name,
label: item.name,
});
if (item.type == 'chlid') {
ChlidTableOptions.value.push({
value: item.name,
label: item.name,
});
}
});
}
},
{ deep: true, immediate: true },
);
const handleSearch = (val: string) => {
onlineFormDesignStore.getSelectData(val);
};
const handleSelectFocus = () => {
onlineFormDesignStore.getSelectData('');
};
const handleChange = (code: string) => {
onlineFormDesignStore.setSelectOptions(formConfig.value.currentItem.field, code);
};
let FieldNamesOptions = ref<SelectProps['options']>([]);
const fetch = () => {
if (!fieldTableValue.value || typeof fieldTableValue.value !== 'string') {
return;
}
getOutKeyList({
tableNames: fieldTableValue.value,
dbCode: receivedData.value.dbCode,
}).then((data: Recordable) => {
let arr: any[] = [];
if (data && data[0]) {
data[0].db_codecolumnsList.forEach((item) => {
arr.push({
label: item.dbColumnName + '(' + item.description + ')',
value: item.dbColumnName,
csType: item.csType,
});
});
FieldNamesOptions.value = arr;
}
});
};
const handleFieldTableChange = (e) => {
fieldTableValue.value = e;
fetch();
inputOptions.value.forEach((item) => {
if (item.name == 'fieldName') {
item.componentProps.options = FieldNamesOptions;
}
});
};
// compuated
const allOptions = ref([] as Omit<IBaseFormAttrs, 'tag'>[]);
const showControlAttrs = (includes: string[] | undefined) => {
if (!includes) return true;
return includes.includes(formConfig.value.currentItem!.component);
};
const { formConfig } = useFormDesignState();
if (formConfig.value.currentItem) {
formConfig.value.currentItem.componentProps =
formConfig.value.currentItem.componentProps || {};
}
watch(
() => formConfig.value.currentItem?.field,
(_newValue, oldValue) => {
// currentIndex.value = formConfig.value.schemas.findIndex(
// (element) => element.field === formConfig.value.currentItem.field,
// );
formConfig.value.schemas &&
formItemsForEach(formConfig.value.schemas, (item) => {
if (item.link) {
const index = item.link.findIndex((linkItem) => linkItem === oldValue);
index !== -1 && remove(item.link, index);
}
});
},
);
const thisFormType: any = inject('thisFormType');
const formType_design = ref();
watch(
() => thisFormType,
() => {
formType_design.value = thisFormType.value?.info?.formType | 0;
},
{ deep: true, immediate: true },
);
watch(
() => formConfig.value.currentItem && formConfig.value.currentItem.component,
() => {
// console.log(formConfig.value);
// currentIndex.value = formConfig.value.schemas.findIndex(
// (element) => element.field === formConfig.value.currentItem.field,
// );
allOptions.value = [];
baseComponentControlAttrs.forEach((item) => {
item.category = 'control';
if (!item.includes) {
// include
allOptions.value.push(item);
} else if (item.includes.includes(formConfig.value.currentItem!.component)) {
// include
allOptions.value.push(item);
}
});
baseComponentCommonAttrs.forEach((item) => {
if (
(formType_design.value != 2 &&
!['Divider', 'Button'].includes(formConfig.value.currentItem?.component)) ||
!['dataTable', 'fieldName'].includes(item.name)
) {
item.category = 'input';
if (item.includes) {
if (item.includes.includes(formConfig.value.currentItem!.component)) {
allOptions.value.push(item);
}
} else if (item.exclude) {
if (!item.exclude.includes(formConfig.value.currentItem!.component)) {
allOptions.value.push(item);
}
} else {
allOptions.value.push(item);
}
}
});
baseComponentAttrs[formConfig.value.currentItem!.component] &&
baseComponentAttrs[formConfig.value.currentItem!.component].forEach(async (item) => {
if (item.component) {
if (['Switch', 'Checkbox', 'Radio'].includes(item.component as string)) {
item.category = 'control';
allOptions.value.push(item);
} else {
item.category = 'input';
allOptions.value.push(item);
}
}
});
},
{
immediate: true,
},
);
//
const controlOptions = computed(() => {
return allOptions.value.filter((item) => {
return item.category == 'control';
});
});
//
const SubControllOptions = computed(() => {
return designSubControlAttrs;
});
//
const inputOptions = computed(() => {
fetch();
let arr = allOptions.value.filter((item) => {
return item.category == 'input';
});
console.log(arr);
console.log(currentIndex.value);
arr.forEach((item) => {
if (item.name == 'dataTable') {
if (currentIndex.value == -1) {
item.componentProps.options = ChlidTableOptions;
} else {
item.componentProps.options = FieldTableOptions;
}
}
if (item.name == 'fieldName') {
item.componentProps.options = FieldNamesOptions;
}
});
return arr;
});
const subTableOptions = computed(() => {
let arr = designSubTableAttrs;
arr.forEach((item) => {
if (item.name == 'dataTable') {
item.componentProps.options = ChlidTableOptions;
}
if (item.name == 'fieldName') {
item.componentProps.options = FieldNamesOptions;
}
});
return arr;
});
watch(
() => formConfig.value.currentItem!.componentProps,
() => {
const func = componentPropsFuncs[formConfig.value.currentItem!.component];
if (func) {
func(formConfig.value.currentItem!.componentProps, allOptions.value);
}
fieldTableValue.value = formConfig.value?.currentItem?.componentProps?.dataTable;
handleFieldTableChange(formConfig.value?.currentItem?.componentProps?.dataTable);
},
{
immediate: true,
deep: true,
},
);
const linkOptions = computed(() => {
return (
formConfig.value.schemas &&
formConfig.value.schemas
.filter((item) => item.key !== formConfig.value.currentItem!.key)
.map(({ label, field }) => ({ label: label + '/' + field, value: field }))
);
});
//
const formContent: any = ref('');
//
const [buttonScriptModal, { openModal, closeModal }] = useModal();
//
function handleButtonClick() {
formContent.value = formConfig.value.currentItem.componentProps?.clickCode;
openModal();
}
//
function handleSubmit() {
if (!checkChinese(formContent.value)) {
message.warning('脚本的代码部分不能含有中文字符!');
return;
}
formConfig.value.currentItem.componentProps.clickCode = formContent.value;
closeModal();
}
//
function checkChinese(str) {
//
const lines = str.split('\n');
let flag = true;
//
for (const line of lines) {
// console.log
const consoleIndex = line.indexOf('console.log');
let partToCheck = line;
if (consoleIndex !== -1) {
partToCheck = line.substring(0, consoleIndex).trim(); // console.log
}
// '//'
const commentIndex = partToCheck.indexOf('//');
// '//''//'
partToCheck =
commentIndex !== -1 ? partToCheck.substring(0, commentIndex).trim() : partToCheck;
//
console.log(partToCheck);
if (/[\u4e00-\u9fa5]/.test(partToCheck)) {
flag = false;
}
}
return flag;
}
return {
formConfig,
showControlAttrs,
linkOptions,
controlOptions,
SubControllOptions,
inputOptions,
subTableOptions,
fieldTableValue,
FieldTableOptions,
ChlidTableOptions,
handleFieldTableChange,
fetch,
BasicModal,
formContent,
formItemPropsScript,
buttonScriptModal,
openModal,
closeModal,
handleButtonClick,
handleSubmit,
selectRadio,
selectDictionary,
selectDictionaryData,
handleSearch,
handleChange,
handleSelectFocus,
};
},
});
</script>
<style lang="less" scoped>
.select-radio {
display: flex;
justify-content: center;
margin-bottom: 15px;
}
.alertModal {
display: flex;
background-color: @border-color-base;
&_content {
width: 50%;
padding: 1px;
&-textarea {
background-color: @component-onlineform-formdesign-alert-background-color;
}
}
}
</style>

View File

@ -0,0 +1,64 @@
<!--
* @Description: 表单项属性
-->
<template>
<div class="properties-content">
<div class="properties-body" v-if="formConfig.currentItem">
<Empty class="hint-box" v-if="!formConfig.currentItem.key" description="未选择控件" />
<Form v-else label-align="left" layout="vertical">
<div v-for="item of baseItemColumnProps" :key="item.name">
<FormItem :label="item.label" v-if="showProps(item.exclude)">
<component
v-if="formConfig.currentItem.colProps && item.component"
class="component-props"
v-bind="item.componentProps"
:is="item.component"
v-model:value="formConfig.currentItem.colProps[item.name]"
/>
</FormItem>
</div>
</Form>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { baseItemColumnProps } from '../config/formItemPropsConfig';
import { Empty, Input, Form, FormItem, Switch, Checkbox, Select, Slider } from 'ant-design-vue';
import RuleProps from './RuleProps.vue';
import { useFormDesignState } from '../../../hooks/useFormDesignState';
import { isArray } from 'lodash-es';
export default defineComponent({
name: 'FormItemProps',
components: {
RuleProps,
Empty,
Input,
Form,
FormItem,
Switch,
Checkbox,
Select,
Slider,
},
// props: {} as PropsOptions,
setup() {
const { formConfig } = useFormDesignState();
const showProps = (exclude: string[] | undefined) => {
if (!exclude) {
return true;
}
return isArray(exclude) ? !exclude.includes(formConfig.value.currentItem!.component) : true;
};
return {
baseItemColumnProps,
formConfig,
showProps,
};
},
});
</script>

View File

@ -0,0 +1,230 @@
<!--
* @Description: 表单项属性控件属性面板
-->
<template>
<div class="properties-content">
<div class="properties-body" v-if="formConfig.currentItem?.itemProps">
<Empty class="hint-box" v-if="!formConfig.currentItem.key" description="未选择控件" />
<Form
v-else-if="formConfig.currentItem.component == 'MapGeom'"
label-align="left"
layout="vertical"
description="图斑控件"
>
<FormItem label="选择空间数据表">
<a-select
v-model:value="formConfig.currentItem.mapSetData.chooseLayer"
:options="shpLayerSourceOptions"
size="middle"
placeholder="请选择空间数据表"
/>
</FormItem>
<FormItem label="是否允许编辑图斑">
<Switch v-model:checked="formConfig.currentItem.mapSetData.isAllowEditPolygon" />
</FormItem>
<FormItem label="是否开启位置跳转">
<Switch v-model:checked="formConfig.currentItem.mapSetData.isEnablePostionJump" />
</FormItem>
</Form>
<Form
label-align="left"
layout="vertical"
v-else-if="
formConfig.currentItem.component == 'Grid' && formConfig.currentItem.type == 'subTable'
"
>
<FormItem label="标签">
<a-input v-model:value="formConfig.currentItem.label" placeholder="请输入" />
</FormItem>
<FormItem label="字段标识">
<a-input v-model:value="formConfig.currentItem.field" placeholder="请输入" />
</FormItem>
</Form>
<Form
label-align="left"
layout="vertical"
v-else-if="formConfig.currentItem.component == 'Card'"
>
<FormItem label="标签">
<a-input v-model:value="formConfig.currentItem.label" placeholder="请输入" />
</FormItem>
<FormItem label="字段标识">
<a-input v-model:value="formConfig.currentItem.field" placeholder="请输入" />
</FormItem>
<FormItem label="显示阴影">
<a-select ref="select" v-model:value="formConfig.currentItem.shadow" style="width: 100%">
<a-select-option value="always">总是</a-select-option>
<a-select-option value="hover">悬浮显示</a-select-option>
<a-select-option value="never">不显示</a-select-option>
</a-select>
</FormItem>
<FormItem label="自定义类">
<a-input v-model:value="formConfig.currentItem.myclass" placeholder="请输入" />
</FormItem>
</Form>
<Form
label-align="left"
layout="vertical"
v-else-if="
['CreateUser', 'ModifyUser', 'CreateTime', 'ModifyTime'].includes(
formConfig.currentItem.component,
)
"
>
<FormItem label="标签">
<a-input v-model:value="formConfig.currentItem.label" placeholder="请输入" />
</FormItem>
<FormItem label="字段标识">
<a-input v-model:value="formConfig.currentItem.field" placeholder="请输入" />
</FormItem>
<FormItem label="是否可见">
<a-switch v-model:checked="formConfig.currentItem.display" />
</FormItem>
</Form>
<Form v-else label-align="left" layout="vertical">
<div v-for="item of baseFormItemProps" :key="item.name">
<FormItem :label="item.label" v-if="showProps(item.exclude)">
<component
v-if="item.component"
class="component-props"
v-bind="item.componentProps"
:is="item.component"
v-model:value="formConfig.currentItem[item.name]"
/>
</FormItem>
</div>
<div v-for="item of advanceFormItemProps" :key="item.name">
<FormItem :label="item.label" v-if="showProps(item.exclude)">
<component
v-if="item.component"
class="component-props"
v-bind="item.componentProps"
:is="item.component"
v-model:value="formConfig.currentItem.itemProps[item.name]"
/>
</FormItem> </div
><div v-for="item of advanceFormItemColProps" :key="item.name">
<FormItem :label="item.label" v-if="showProps(item.exclude)">
<component
v-if="item.component"
class="component-props"
v-bind="item.componentProps"
:is="item.component"
v-model:value="formConfig.currentItem.itemProps[item.name]['span']"
/>
</FormItem>
</div>
<FormItem label="控制属性" v-if="controlPropsList.length">
<Col v-for="item of controlPropsList" :key="item.name">
<Checkbox v-model:checked="formConfig.currentItem.itemProps[item.name]">
{{ item.label }}
</Checkbox>
</Col>
</FormItem>
<FormItem
label="是否必选"
v-if="!['Grid', 'Tabs'].includes(formConfig.currentItem.component)"
>
<Switch v-model:checked="formConfig.currentItem.itemProps['required']" />
<Input
v-if="formConfig.currentItem.itemProps['required']"
v-model:value="formConfig.currentItem.itemProps['message']"
placeholder="请输入必选提示"
/>
</FormItem>
<FormItem
v-if="!['Grid', 'Tabs'].includes(formConfig.currentItem.component)"
label="校验规则"
:class="{ 'form-rule-props': !!formConfig.currentItem.itemProps['rules'] }"
>
<RuleProps />
</FormItem>
</Form>
</div>
</div>
</template>
<script lang="ts" setup name="FormItemProps">
import { ref, computed, watch, inject } from 'vue';
import { Empty, Input, Form, FormItem, Switch, Checkbox, Col } from 'ant-design-vue';
import {
baseFormItemControlAttrs,
baseFormItemProps,
advanceFormItemProps,
advanceFormItemColProps,
} from '../../VFormDesign/config/formItemPropsConfig';
import { PercentageOutlined } from '@ant-design/icons-vue';
import RuleProps from './RuleProps.vue';
import { useFormDesignState } from '../../../hooks/useFormDesignState';
import { isArray } from 'lodash-es';
// api
// import { loadTableRecordInfo } from '@/api/database/index';
import { ShpLayerSourceLoadPage } from '@/api/demo/formScheme';
const { formConfig } = useFormDesignState();
let shpLayerSourceOptions: any = ref([]);
let dbcode = ref();
const handleNextStepsData = inject('handleNextStepsData');
watch(
() => handleNextStepsData,
(newVal: any) => {
dbcode.value = newVal?.value?.info?.DbCode;
},
{ deep: true, immediate: true },
);
watch(
() => formConfig.value,
() => {
// console.log('formConfig', formConfig.value);
if (formConfig.value.currentItem) {
formConfig.value.currentItem.itemProps = formConfig.value.currentItem.itemProps || {};
formConfig.value.currentItem.itemProps.labelCol =
formConfig.value.currentItem.itemProps.labelCol || {};
formConfig.value.currentItem.itemProps.wrapperCol =
formConfig.value.currentItem.itemProps.wrapperCol || {};
if (formConfig.value.currentItem.component === 'MapGeom') {
//
formConfig.value.currentItem.mapSetData = formConfig.value.currentItem.mapSetData
? formConfig.value.currentItem.mapSetData
: {
chooseLayer: '',
isAllowEditPolygon: false,
isEnablePostionJump: false,
};
//
if (shpLayerSourceOptions.value.length == 0) {
getShpLayerSourceOptions();
}
}
}
},
{ deep: true, immediate: true },
);
const showProps = (exclude: string[] | undefined) => {
if (!exclude) {
return true;
}
return isArray(exclude) ? !exclude.includes(formConfig.value.currentItem!.component) : true;
};
const controlPropsList = computed(() => {
return baseFormItemControlAttrs.filter((item) => {
return showProps(item.exclude);
});
});
//
async function getShpLayerSourceOptions() {
// let options: any = await loadTableRecordInfo({});
let options: any = await ShpLayerSourceLoadPage();
shpLayerSourceOptions.value = [];
options.items.forEach((e) => {
// shpLayerSourceOptions.value.push({ label: e.tableName, value: e.tableName });
shpLayerSourceOptions.value.push({ label: e.name, value: e.relationTable });
});
}
</script>

View File

@ -0,0 +1,58 @@
<!--
* @Description: 拖拽节点控件
-->
<template>
<div
class="drag-move-box component-border"
@click.stop="handleSelectItem"
:class="{ active: schema.key === formConfig.currentItem?.key }"
>
<div class="form-item-box">
<VFormItem :formConfig="formConfig" :schema="schema" />
</div>
<div class="show-key-box">
{{ schema.label + (schema.field ? '/' + schema.field : '') }}
</div>
<FormNodeOperate :schema="schema" :currentItem="formConfig.currentItem" />
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs, PropType } from 'vue';
import { IVFormComponent } from '../../../typings/v-form-component';
import FormNodeOperate from './FormNodeOperate.vue';
import { useFormDesignState } from '../../../hooks/useFormDesignState';
import VFormItem from '../../VFormItem/index.vue';
// import VFormItem from '../../VFormItem/vFormItem.vue';
export default defineComponent({
name: 'FormNode',
components: {
VFormItem,
FormNodeOperate,
},
props: {
schema: {
type: Object as PropType<IVFormComponent>,
required: true,
},
},
setup(props) {
const { formConfig, formDesignMethods } = useFormDesignState();
const state = reactive({});
// formDesignMethods
const handleSelectItem = () => {
// formDesignMethods
formDesignMethods.handleSetSelectItem(props.schema);
};
return {
...toRefs(state),
handleSelectItem,
formConfig,
};
},
});
</script>
<style lang="scss" scoped>
.component-border{
border: 1px solid #d9d9d9
}
</style>

View File

@ -0,0 +1,87 @@
<!--
* @Description: 节点操作复制删除控件
-->
<template>
<div class="copy-delete-box">
<a class="copy" :class="activeClass" @click.stop="handleCopy">
<Icon icon="ant-design:copy-outlined" />
</a>
<a class="delete" :class="activeClass" @click.stop="handleDelete">
<Icon icon="ant-design:delete-outlined" />
</a>
</div>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue';
import { IVFormComponent } from '../../../typings/v-form-component';
import { remove } from '../../../utils';
import { useFormDesignState } from '../../../hooks/useFormDesignState';
import Icon from '@/components/Icon/Icon.vue';
export default defineComponent({
name: 'FormNodeOperate',
components: {
Icon,
},
props: {
schema: {
type: Object,
default: () => ({}),
},
currentItem: {
type: Object,
default: () => ({}),
},
},
setup(props) {
const { formConfig, formDesignMethods } = useFormDesignState();
const activeClass = computed(() => {
return props.schema.key === props.currentItem.key ? 'active' : 'unactivated';
});
/**
* 删除当前项
*/
const handleDelete = () => {
const traverse = (schemas: IVFormComponent[]) => {
schemas.some((formItem, index) => {
const { component, key } = formItem;
//
if(['Grid', 'Tabs','Card', 'CardGroup'].includes(component)){
switch(component){
case 'Grid':
case 'Card':
formItem.columns?.forEach((item) => traverse(item.children))
break;
case 'Tabs':
case 'CardGroup':
console.log(formItem,'formItem')
formItem?.componentProps?.options?.forEach(optionItem => {
traverse(optionItem.children)
})
break;
}
}
if (key === props.currentItem.key) {
let params: IVFormComponent =
schemas.length === 1
? { component: '' }
: schemas.length - 1 > index
? schemas[index + 1]
: schemas[index - 1];
formDesignMethods.handleSetSelectItem(params);
remove(schemas, index);
return true;
}
});
};
traverse(formConfig.value!.schemas);
};
const handleCopy = () => {
formDesignMethods.handleCopy();
};
return { activeClass, handleDelete, handleCopy };
},
});
</script>

View File

@ -0,0 +1,466 @@
<template>
<div>
<div v-if="['Grid'].includes(formConfig.currentItem!.component)">
<div v-for="(item, index) of formConfig.currentItem!['columns']" :key="index">
<div class="options-box">
<Input v-model:value="item.span" class="options-value" />
<a class="options-delete" @click="deleteGridOptions(index)">
<Icon icon="ant-design:delete-outlined" />
</a>
</div>
</div>
<a @click="addGridOptions">
<Icon icon="ant-design:file-add-outlined" />
添加栅格
</a>
</div>
<div v-else-if="['TreeSelect', 'Cascader'].includes(formConfig.currentItem!.component)">
<BasicTree
ref="treeDataRef"
:treeData="treeDataAndOptions"
:fieldNames="{ key: 'value', title: 'label' }"
:clickRowToExpand="false"
:defaultExpandAll="true"
>
<template #title="{ label, value }">
<div class="options-box">
<Input
:value="label"
@input="updateLabelOrValue('label', label, value, $event.target.value)"
/>
<Input
:value="value"
@blur="updateLabelOrValue('value', label, value, $event.target.value)"
class="options-value"
/>
<Tooltip title="添加子级选项" placement="top">
<a class="options-delete" @click="addTreeNode(value)">
<Icon icon="ant-design:folder-add-outlined" />
</a>
</Tooltip>
<a class="options-delete" @click="deleteTreeNode(value)">
<Icon icon="ant-design:delete-outlined" />
</a>
</div>
</template>
</BasicTree>
<a @click="addTreeNode(null)">
<Icon icon="ant-design:file-add-outlined" />
添加父级选项
</a>
<a @click="showModal('import')">
<Icon icon="ant-design:import-outlined" />
导入数据
</a>
<!-- <a @click="showModal('export')">
<Icon icon="ant-design:export-outlined" />
导出数据
</a> -->
</div>
<div v-else-if="['Transfer'].includes(formConfig.currentItem!.component)">
<div v-for="(item, index) of formConfig.currentItem!.componentProps![key]" :key="index">
<div class="options-box">
<Input v-model:value="item.title" />
<Input v-model:value="item.description" class="options-value" />
<a
class="options-delete"
@click="updateTransferDisabled(index, false)"
v-if="item.disabled"
>
<Icon icon="bi:unlock" />
</a>
<a
class="options-delete"
@click="updateTransferDisabled(index, true)"
v-if="!item.disabled"
>
<Icon icon="bi:lock" />
</a>
<a class="options-delete" @click="deleteOptions(index)">
<Icon icon="ant-design:delete-outlined" />
</a>
</div>
</div>
<a @click="addOptions">
<Icon icon="ant-design:file-add-outlined" />
添加标题
</a>
</div>
<div v-else>
<div v-for="(item, index) of formConfig.currentItem!.componentProps![key]" :key="index">
<div class="options-box">
<Input v-model:value="item.label" />
<Input v-model:value="item.value" class="options-value" />
<a class="options-delete" @click="deleteOptions(index)">
<Icon icon="ant-design:delete-outlined" />
</a>
</div>
</div>
<a @click="addOptions">
<Icon icon="ant-design:file-add-outlined" />
添加选项
</a>
</div>
</div>
<Modal
:title="title"
:open="visible"
@ok="handleImportJson"
@cancel="handleCancel"
cancelText="关闭"
:destroyOnClose="true"
wrapClassName="v-code-modal"
style="top: 20px"
:width="850"
>
<p class="hint-box">{{ tip }}</p>
<div class="v-json-box">
<CodeEditor v-model:value="json" ref="myEditor" :mode="MODE.JSON" />
</div>
<template #footer>
<div v-if="type == 'import'">
<a-button @click="handleCancel"></a-button>
<Upload
class="upload-button"
:beforeUpload="beforeUpload"
:showUploadList="false"
accept="application/json"
>
<a-button type="primary">导入json文件</a-button>
</Upload>
<a-button type="primary" @click="handleImportJson"></a-button>
</div>
<div v-if="type == 'export'">
<a-button @click="handleCancel"></a-button>
<a-button
type="primary"
class="copy-btn"
data-clipboard-action="copy"
:data-clipboard-text="json"
@click="handleCopyJson"
>
复制数据
</a-button>
<a-button @click="handleExportJson" type="primary">导出代码</a-button>
</div>
</template>
</Modal>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs, ref, unref, watch } from 'vue';
import { useFormDesignState } from '../../../hooks/useFormDesignState';
import { BasicTree, TreeItem, TreeActionType } from '@/components/Tree';
import { remove, formItemsForEach, generateKey } from '../../../utils';
import { CodeEditor, MODE } from '@/components/CodeEditor';
import { Upload, Modal, Input, Tooltip } from 'ant-design-vue';
import { copyText } from '@/utils/copyTextToClipboard';
import { options_json, treeData_json } from '../../VFormDesign/config/formItemPropsScript';
import { v4 as uuidv4 } from 'uuid';
import { cloneDeep } from 'lodash-es';
import Icon from '@/components/Icon/Icon.vue';
import message from '../../../utils/message';
export default defineComponent({
name: 'FormOptions',
components: { Input, Icon, BasicTree, CodeEditor, Upload, Modal, Tooltip },
// props: {},
setup() {
const { formConfig } = useFormDesignState();
let key: string = '';
//
const state = reactive({
title: '',
visible: false,
json: {} as any,
type: '',
tip: '',
});
//
const treeDataAndOptions = ref<TreeItem[]>([]);
watch(
() => formConfig.value.currentItem.component,
() => {
if (formConfig.value.currentItem?.component == 'TreeSelect') {
//
treeDataAndOptions.value = formConfig.value.currentItem?.componentProps?.treeData;
key = 'treeData';
} else if (formConfig.value.currentItem?.component == 'Cascader') {
//
treeDataAndOptions.value = formConfig.value.currentItem?.componentProps?.options;
key = 'options';
} else if (formConfig.value.currentItem?.component == 'Transfer') {
// 穿
key = 'dataSource';
} else {
// --
key = 'options';
}
},
{ deep: true, immediate: true },
);
//
const treeDataRef = ref<Nullable<TreeActionType>>(null);
const getTree = (): any => {
return unref(treeDataRef);
};
//
const addTreeNode = (value) => {
let length = getLength(1, getTree().getTreeData());
getTree().insertNodeByKey({
parentKey: value,
node: {
label: `选项${length}`,
value: `${length}`,
},
//
push: 'push',
});
refresh();
};
//
const deleteTreeNode = (value) => {
getTree().deleteNodeByKey(value);
refresh();
};
//
const getLength = (length, treeDataOrOptions) => {
treeDataOrOptions?.forEach((to) => {
if (to.children) {
length = getLength(length, to.children);
}
if (length == to.value) {
length++;
length = getLength(length, treeDataOrOptions);
}
});
return length;
};
// labelvalue
const updateLabelOrValue = (type, label, value, newLabelOrValue) => {
if (type == 'label') {
getTree().updateNodeByKey(value, { label: newLabelOrValue, value: value });
}
if (type == 'value') {
if (checkRepeat(true, getTree().getTreeData(), newLabelOrValue)) {
getTree().updateNodeByKey(value, { label: label, value: newLabelOrValue });
} else {
message.warning('不能赋给重复的值');
}
}
refresh();
};
// labelvalue-
const checkRepeat = (flag, treeData, newValue) => {
treeData.forEach((tree) => {
if (tree.value == newValue) {
flag = false;
} else if (tree.children) {
return checkRepeat(flag, tree.children, newValue);
}
});
return flag;
};
//
const refresh = () => {
getTree().expandAll(true);
treeDataAndOptions.value = getTree().getTreeData();
formConfig.value.currentItem.componentProps[key] = getTree().getTreeData();
};
//
const handleCancel = () => {
state.visible = false;
};
//
const showModal = (type) => {
state.visible = true;
state.type = type;
if (type == 'import') {
state.title = '树状结构数据导入';
state.tip = '导入格式如下:';
if (key == 'treeData') {
state.json = treeData_json;
} else {
state.json = options_json;
}
} else if (type == 'export') {
state.title = '树状结构数据导出';
state.tip = '导出代码如下:';
state.json = getTree().getTreeData();
if (key == 'treeData') {
state.json = { treeData: getTree().getTreeData() };
} else {
state.json = { options: getTree().getTreeData() };
}
}
};
// JSON
const handleImportJson = () => {
try {
const editorJsonData = JSON.parse(state.json);
editorJsonData[key] &&
formItemsForEach(editorJsonData[key], (formItem) => {
generateKey(formItem);
});
//
cloneDeep(getTree().getTreeData())?.forEach((item) => {
deleteTreeNode(item.value);
});
//
editorJsonData[key]?.forEach((item) => {
getTree().insertNodeByKey({
parentKey: null,
node: item,
push: 'push',
});
});
refresh();
handleCancel();
message.success('导入成功');
} catch {
message.error('导入失败,数据格式不对');
}
};
// json
const beforeUpload = (e: File) => {
const reader = new FileReader();
reader.readAsText(e);
reader.onload = function () {
state.json = this.result as string;
handleImportJson();
};
return false;
};
// json
const handleExportJson = () => {
let content = 'data:text/csv;charset=utf-8,';
content += `${JSON.stringify(state.json, null, 2)}`;
const encodedUri = encodeURI(content);
const actions = document.createElement('a');
actions.setAttribute('href', encodedUri);
actions.setAttribute('download', 'file.json');
actions.click();
};
//
const handleCopyJson = () => {
const value = `${JSON.stringify(state.json, null, 2)}`;
if (!value) {
message.warning('代码为空');
return;
}
copyText(value);
};
const addOptions = () => {
if (!formConfig.value.currentItem?.componentProps?.[key])
formConfig.value.currentItem!.componentProps![key] = [];
const len = formConfig.value.currentItem?.componentProps?.[key].length + 1;
if (['Tabs'].includes(formConfig.value.currentItem!.component)) {
formConfig.value.currentItem!.componentProps![key].push({
label: `选项卡${len}`,
value: '' + len,
children: [],
});
} else if (['CardGroup'].includes(formConfig.value.currentItem!.component)) {
formConfig.value.currentItem!.componentProps![key].push({
label: `卡片${len}`,
value: '' + len,
field: `use_card_${uuidv4()}`,
children: [],
});
} else if (['Transfer'].includes(formConfig.value.currentItem!.component)) {
formConfig.value.currentItem!.componentProps![key].push({
key: `key-${len}`,
title: `标题${len}`,
description: `描述${len}`,
disabled: false,
});
} else {
formConfig.value.currentItem!.componentProps![key].push({
label: `选项${len}`,
value: '' + len,
});
}
};
const deleteOptions = (index: number) => {
remove(formConfig.value.currentItem?.componentProps?.[key], index);
};
const addGridOptions = () => {
formConfig.value.currentItem?.['columns']?.push({
span: 12,
children: [],
});
};
const deleteGridOptions = (index: number) => {
if (index === 0) return message.warning('请至少保留一个栅格');
remove(formConfig.value.currentItem!['columns']!, index);
};
const updateTransferDisabled = (index: number, flag: boolean) => {
formConfig.value.currentItem.componentProps[key][index].disabled = flag;
};
return {
...toRefs(state),
formConfig,
addOptions,
deleteOptions,
key,
deleteGridOptions,
addGridOptions,
treeDataRef,
treeDataAndOptions,
addTreeNode,
deleteTreeNode,
updateLabelOrValue,
updateTransferDisabled,
handleImportJson,
handleExportJson,
handleCopyJson,
beforeUpload,
handleCancel,
showModal,
MODE,
};
},
});
</script>
<style lang="less" scoped>
.options-box {
display: flex;
align-items: center;
margin-bottom: 5px;
.options-value {
margin: 0 8px;
}
.options-delete {
flex-shrink: 0;
width: 30px;
height: 30px;
border-radius: 50%;
background: #f5f5f5;
color: #666;
line-height: 30px;
text-align: center;
&:hover {
background: #ff4d4f;
}
}
}
.upload-button {
margin: 0 10px;
}
.copy-btn {
margin: 0 5px;
}
</style>

View File

@ -0,0 +1,273 @@
<!--
* @Description: 右侧属性面板控件 表单属性面板
-->
<template>
<div class="properties-content">
<Form class="properties-body" label-align="left" layout="vertical">
<!-- <e-upload v-model="fileList"></e-upload>-->
<FormItem label="表单布局">
<RadioGroup button-style="solid" v-model:value="formConfig.layout">
<RadioButton value="horizontal">水平</RadioButton>
<RadioButton value="vertical" :disabled="formConfig.labelLayout === 'Grid'">
垂直
</RadioButton>
<RadioButton value="inline" :disabled="formConfig.labelLayout === 'Grid'">
行内
</RadioButton>
</RadioGroup>
</FormItem>
<!-- <Row> -->
<FormItem label="标签布局">
<RadioGroup
buttonStyle="solid"
v-model:value="formConfig.labelLayout"
@change="lableLayoutChange"
>
<RadioButton value="flex">固定</RadioButton>
<RadioButton value="Grid" :disabled="formConfig.layout !== 'horizontal'">
栅格
</RadioButton>
</RadioGroup>
</FormItem>
<!-- </Row> -->
<FormItem label="标签宽度px" v-show="formConfig.labelLayout === 'flex'">
<InputNumber
:style="{ width: '100%' }"
v-model:value="formConfig.labelWidth"
:min="0"
:step="1"
/>
</FormItem>
<div v-if="formConfig.labelLayout === 'Grid'">
<FormItem label="labelCol">
<Slider v-model:value="sliderSpan" :max="24" />
</FormItem>
<FormItem label="wrapperCol">
<Slider v-model:value="sliderSpan" :max="24" />
</FormItem>
<FormItem label="标签对齐">
<RadioGroup button-style="solid" v-model:value="formConfig.labelAlign">
<RadioButton value="left">靠左</RadioButton>
<RadioButton value="right">靠右</RadioButton>
</RadioGroup>
</FormItem>
<FormItem label="控件大小">
<RadioGroup button-style="solid" v-model:value="formConfig.size">
<RadioButton value="default">默认</RadioButton>
<RadioButton value="small"></RadioButton>
<RadioButton value="large"></RadioButton>
</RadioGroup>
</FormItem>
</div>
<FormItem label="表单属性">
<Col
><Checkbox v-model:checked="formConfig.colon" v-if="formConfig.layout == 'horizontal'"
>label后面显示冒号</Checkbox
></Col
>
<Col><Checkbox v-model:checked="formConfig.disabled">禁用</Checkbox></Col>
<Col><Checkbox v-model:checked="formConfig.hideRequiredMark">隐藏必选标记</Checkbox></Col>
</FormItem>
<FormItem label="脚本">
<a-space direction="vertical">
<a-button
v-model:value="formConfig.beforeSetData"
size="mini"
@click="handleBtnClick('beforeSetData')"
>
添加赋值前脚本
</a-button>
<a-button
v-model:value="formConfig.afterValidateForm"
size="mini"
@click="handleBtnClick('afterValidateForm')"
>
添加校验后脚本
</a-button>
<a-button
v-model:value="formConfig.afterSaveEvent"
size="mini"
@click="handleBtnClick('afterSaveEvent')"
>
添加保存后脚本
</a-button>
<a-button
v-model:value="formConfig.changeDataEvent"
size="mini"
@click="handleBtnClick('changeDataEvent')"
>
添加数据改变脚本
</a-button>
</a-space>
</FormItem>
</Form>
</div>
<BasicModal
@register="propsScriptModal"
:title="formTitle"
:height="500"
:width="1000"
@ok="handleSubmit"
>
<div class="alertModal">
<div class="alertModal_content">
<a-textarea
v-model:value="formContent"
autosize
:auto-size="{ minRows: 22, maxRows: 22 }"
/>
</div>
<div class="alertModal_content">
<a-textarea
class="alertModal_content-textarea"
v-model:value="formItemPropsScript"
autosize
readOnly
:auto-size="{ minRows: 22, maxRows: 22 }"
/>
</div>
</div>
</BasicModal>
</template>
<script lang="ts" setup name="FormProps">
import { ref, computed } from 'vue';
import { BasicModal, useModal } from '@/components/Modal';
import { formItemPropsScript } from '../../VFormDesign/config/formItemPropsScript';
import { useFormDesignState } from '../../../hooks/useFormDesignState';
import {
InputNumber,
Slider,
Checkbox,
Col,
RadioChangeEvent,
Form,
FormItem,
RadioButton,
RadioGroup,
message,
} from 'ant-design-vue';
const { formConfig } = useFormDesignState();
formConfig.value = formConfig.value || {
labelCol: { span: 24 },
wrapperCol: { span: 24 },
};
const lableLayoutChange = (e: RadioChangeEvent) => {
if (e.target.value === 'Grid') {
formConfig.value.layout = 'horizontal';
}
};
const sliderSpan = computed(() => {
if (formConfig.value.labelLayout) {
return Number(formConfig.value.labelCol!.span);
}
return 0;
});
//
let formTitle: any = ref('');
let btnClickEvent_now = '';
let formContent: any = ref('');
//
const [propsScriptModal, { openModal, closeModal }] = useModal();
//
function handleBtnClick(btnClickEvent: string) {
switch (btnClickEvent) {
case 'beforeSetData':
formTitle.value = '添加赋值前脚本';
btnClickEvent_now = 'beforeSetData';
formContent.value = formConfig.value?.beforeSetData;
break;
case 'afterValidateForm':
formTitle.value = '添加校验后脚本';
btnClickEvent_now = 'afterValidateForm';
formContent.value = formConfig.value?.afterValidateForm;
break;
case 'afterSaveEvent':
formTitle.value = '添加保存后脚本';
btnClickEvent_now = 'afterSaveEvent';
formContent.value = formConfig.value?.afterSaveEvent;
break;
case 'changeDataEvent':
formTitle.value = '添加数据改变脚本';
btnClickEvent_now = 'changeDataEvent';
formContent.value = formConfig.value?.changeDataEvent;
break;
default:
formTitle.value = '';
break;
}
// console.log(formConfig.value);
// console.log(description);
openModal();
}
//
function handleSubmit() {
if (!checkChinese(formContent.value)) {
message.warning('脚本的代码部分不能含有中文字符!');
return;
}
if (btnClickEvent_now) {
if (btnClickEvent_now == 'beforeSetData') {
formConfig.value.beforeSetData = formContent.value;
} else if (btnClickEvent_now == 'afterValidateForm') {
formConfig.value.afterValidateForm = formContent.value;
} else if (btnClickEvent_now == 'afterSaveEvent') {
formConfig.value.afterSaveEvent = formContent.value;
} else if (btnClickEvent_now == 'changeDataEvent') {
formConfig.value.changeDataEvent = formContent.value;
}
}
closeModal();
}
//
function checkChinese(str) {
//
const lines = str.split('\n');
let flag = true;
//
for (const line of lines) {
// console.log
const consoleIndex = line.indexOf('console.log');
let partToCheck = line;
if (consoleIndex !== -1) {
partToCheck = line.substring(0, consoleIndex).trim(); // console.log
}
// '//'
const commentIndex = partToCheck.indexOf('//');
// '//''//'
partToCheck =
commentIndex !== -1 ? partToCheck.substring(0, commentIndex).trim() : partToCheck;
//
if (/[\u4e00-\u9fa5]/.test(partToCheck)) {
flag = false;
}
}
return flag;
}
</script>
<style lang="less" scoped>
.alertModal {
display: flex;
background-color: @border-color-base;
&_content {
width: 50%;
padding: 1px;
&-textarea {
background-color: @component-onlineform-formdesign-alert-background-color;
}
}
}
</style>

View File

@ -0,0 +1,136 @@
<!--
* @Description: 导入JSON模板
-->
<template>
<Modal
title="JSON数据"
:open="visible"
@ok="handleImportJson"
@cancel="handleCancel"
cancelText="关闭"
:destroyOnClose="true"
wrapClassName="v-code-modal"
style="top: 20px"
:width="850"
>
<p class="hint-box">导入格式如下:</p>
<div class="v-json-box">
<CodeEditor v-model:value="json" ref="myEditor" :mode="MODE.JSON" />
</div>
<template #footer>
<a-button @click="handleCancel"></a-button>
<Upload
class="upload-button"
:beforeUpload="beforeUpload"
:showUploadList="false"
accept="application/json"
>
<a-button type="primary">导入json文件</a-button>
</Upload>
<a-button type="primary" @click="handleImportJson"></a-button>
</template>
</Modal>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs } from 'vue';
// import message from '../../../utils/message';
import { useFormDesignState } from '../../../hooks/useFormDesignState';
// import { codemirror } from 'vue-codemirror-lite';
import { IFormConfig } from '../../../typings/v-form-component';
import { formItemsForEach, generateKey } from '../../../utils';
import { CodeEditor, MODE } from '@/components/CodeEditor';
import { useMessage } from '@/hooks/web/useMessage';
import { Upload, Modal } from 'ant-design-vue';
export default defineComponent({
name: 'ImportJsonModal',
components: {
CodeEditor,
Upload,
Modal,
},
setup() {
const { createMessage } = useMessage();
const state = reactive({
visible: false,
json: `{
"schemas": [
{
"component": "input",
"label": "输入框",
"field": "input_2",
"span": 24,
"props": {
"type": "text"
}
}
],
"layout": "horizontal",
"labelLayout": "flex",
"labelWidth": 100,
"labelCol": {},
"wrapperCol": {}
}`,
jsonData: {
schemas: {},
config: {},
},
handleSetSelectItem: null,
});
const { formDesignMethods } = useFormDesignState();
const handleCancel = () => {
state.visible = false;
};
const showModal = () => {
state.visible = true;
};
const handleImportJson = () => {
// JSON
try {
const editorJsonData = JSON.parse(state.json) as IFormConfig;
console.log('editorJsonData', editorJsonData);
editorJsonData.schemas &&
formItemsForEach(editorJsonData.schemas, (formItem) => {
generateKey(formItem);
});
formDesignMethods.setFormConfig({
...editorJsonData,
activeKey: 1,
currentItem: { component: '' },
});
handleCancel();
createMessage.success('导入成功');
} catch {
createMessage.error('导入失败,数据格式不对');
}
};
const beforeUpload = (e: File) => {
// json
const reader = new FileReader();
reader.readAsText(e);
reader.onload = function () {
state.json = this.result as string;
handleImportJson();
};
return false;
};
return {
handleImportJson,
beforeUpload,
handleCancel,
showModal,
...toRefs(state),
MODE,
};
},
});
</script>
<style lang="less" scoped>
.upload-button {
margin: 0 10px;
}
</style>

View File

@ -0,0 +1,65 @@
<!--
* @Description: 渲染JSON数据
-->
<template>
<Modal
title="JSON数据"
:footer="null"
:open="visible"
@cancel="handleCancel"
:destroyOnClose="true"
wrapClassName="v-code-modal"
style="top: 20px"
width="850px"
>
<PreviewCode :editorJson="editorJson" />
</Modal>
</template>
<script lang="ts">
import { computed, defineComponent, reactive, toRefs } from 'vue';
import PreviewCode from './PreviewCode.vue';
import { IFormConfig } from '../../../typings/v-form-component';
import { formatRules, removeAttrs } from '../../../utils';
import { Modal } from 'ant-design-vue';
export default defineComponent({
name: 'JsonModal',
components: {
PreviewCode,
Modal,
},
emits: ['cancel'],
setup(_props, { emit }) {
const state = reactive<{
visible: boolean;
jsonData: IFormConfig;
}>({
visible: false, // json
jsonData: {} as IFormConfig, // json
});
/**
* 显示Json数据弹框
* @param jsonData
*/
const showModal = (jsonData: IFormConfig) => {
formatRules(jsonData.schemas);
state.jsonData = jsonData as any;
state.visible = true;
};
// json
const editorJson = computed(() => {
// @ts-ignore
return JSON.stringify(removeAttrs(state.jsonData), null, '\t');
});
//
const handleCancel = () => {
state.visible = false;
emit('cancel');
};
return { ...toRefs(state), editorJson, handleCancel, showModal };
},
});
</script>

View File

@ -0,0 +1,221 @@
<!--
* @Description: 表单项布局控件
* 千万不要在template下面的第一行加注释因为这里拖动的第一个元素
-->
<template>
<Col v-bind="colPropsComputed">
<template v-if="['Grid','Card'].includes(schema.component)">
<div
class="grid-box component-border"
:class="{ active: schema.key === currentItem.key }"
@click.stop="handleSetSelectItem(schema)"
>
{{ schema.label }}
<Row class="grid-row" v-bind="schema.componentProps">
<Col
class="grid-col"
v-for="(colItem, index) in schema.columns"
:key="index"
:span="colItem.span"
>
<draggable
class="list-main draggable-box"
:component-data="{ name: 'list', tag: 'div', type: 'transition-group' }"
v-bind="{
group: 'form-draggable',
ghostClass: 'moving',
animation: 180,
handle: '.drag-move',
}"
item-key="key"
v-model="colItem.children"
@start="$emit('dragStart', $event, colItem.children)"
@add="$emit('handleColAdd', $event, colItem.children)"
>
<template #item="{ element }">
<LayoutItem
class="drag-move"
:schema="element"
:current-item="currentItem"
@handle-copy="$emit('handle-copy')"
@handle-delete="$emit('handle-delete')"
/>
</template>
</draggable>
</Col>
</Row>
<FormNodeOperate :schema="schema" :currentItem="currentItem" />
</div>
</template>
<template v-else-if="['Tabs'].includes(schema.component)">
<div
class="grid-box component-border"
:class="{ active: schema.key === currentItem.key }"
@click.stop="handleSetSelectItem(schema)"
>
<a-tabs>
<a-tab-pane v-for="(colItem, index) in schema.componentProps.options" :tab="colItem.label" :key="index">
<draggable
class="list-main draggable-box"
:component-data="{ name: 'list', tag: 'div', type: 'transition-group' }"
v-bind="{
group: 'form-draggable',
ghostClass: 'moving',
animation: 180,
handle: '.drag-move',
}"
item-key="key"
v-model="colItem.children"
@start="$emit('dragStart', $event, colItem.children)"
@add="$emit('handleColAdd', $event, colItem.children)"
>
<template #item="{ element }">
<LayoutItem
class="drag-move"
:schema="element"
:current-item="currentItem"
@handle-copy="$emit('handle-copy')"
@handle-delete="$emit('handle-delete')"
/>
</template>
</draggable>
</a-tab-pane>
</a-tabs>
<FormNodeOperate :schema="schema" :currentItem="currentItem" />
</div>
</template>
<template v-else-if="['CardGroup'].includes(schema.component)">
<div
class="grid-box component-border"
:class="{ active: schema.key === currentItem.key }"
@click.stop="handleSetSelectItem(schema)"
>
{{ schema.label }}
<a-radio-group v-model:value="cardGroupRadio" :options="schema.componentProps.options" />
<div class="list-main draggable-box" v-for="(colItem, index) in schema.componentProps.options" :tab="colItem.label" :key="index">
<draggable
v-show="cardGroupRadio == colItem.value"
class="list-main draggable-box"
style="min-height: 50px"
:component-data="{ name: 'list', tag: 'div', type: 'transition-group' }"
v-bind="{
group: 'form-draggable',
ghostClass: 'moving',
animation: 180,
handle: '.drag-move',
}"
item-key="key"
v-model="colItem.children"
@start="$emit('dragStart', $event, colItem.children)"
@add="$emit('handleColAdd', $event, colItem.children)"
>
<template #item="{ element }">
<LayoutItem
class="drag-move"
:schema="element"
:current-item="currentItem"
@handle-copy="$emit('handle-copy')"
@handle-delete="$emit('handle-delete')"
/>
</template>
</draggable>
</div>
<FormNodeOperate :schema="schema" :currentItem="currentItem" />
</div>
</template>
<FormNode
v-else
:key="schema.key"
:schema="schema"
:current-item="currentItem"
@handle-copy="$emit('handle-copy')"
@handle-delete="$emit('handle-delete')"
/>
</Col>
</template>
<script lang="ts">
import { computed, defineComponent, PropType, reactive, toRefs, ref } from 'vue';
import draggable from 'vuedraggable';
import FormNode from './FormNode.vue';
import FormNodeOperate from './FormNodeOperate.vue';
import { useFormDesignState } from '../../../hooks/useFormDesignState';
import { IVFormComponent } from '../../../typings/v-form-component';
import { Row, Col } from 'ant-design-vue';
import { v4 as uuidv4 } from 'uuid';
export default defineComponent({
name: 'LayoutItem',
components: {
FormNode,
FormNodeOperate,
draggable,
Row,
Col,
},
props: {
schema: {
type: Object as PropType<IVFormComponent>,
required: true,
},
currentItem: {
type: Object,
required: true,
},
},
emits: ['dragStart', 'handleColAdd', 'handle-copy', 'handle-delete'],
setup(props) {
const {
formDesignMethods: { handleSetSelectItem },
formConfig,
} = useFormDesignState();
const state = reactive({});
const colPropsComputed = computed(() => {
const { colProps = {} } = props.schema;
return colProps;
});
const list1 = computed(() => props.schema.columns);
// AColdiv
const layoutTag = computed(() => {
return formConfig.value.layout === 'horizontal' ? 'Col' : 'div';
});
//
const cardGroupRadio = ref()
if(props.schema.component === 'CardGroup'){
// field
props.schema.componentProps.options = props.schema.componentProps.options?.map(item => {
return {...item,field:`use_card_${uuidv4()}`}
})
if(props.schema.componentProps.options.length > 0){
cardGroupRadio.value = props.schema.componentProps.options[0].value
}
}
return {
...toRefs(state),
colPropsComputed,
handleSetSelectItem,
layoutTag,
list1,
cardGroupRadio,
};
},
});
</script>
<style lang="less">
@import url('../styles/variable.less');
.layout-width {
width: 100%;
}
.hidden-item {
background-color: rgb(240 191 195);
}
</style>
<style lang="scss" scoped>
.component-border{
border: 1px solid #d9d9d9
}
</style>

View File

@ -0,0 +1,95 @@
<template>
<div>
<div class="v-json-box">
<CodeEditor :value="editorJson" ref="myEditor" :mode="MODE.JSON" />
</div>
<div class="copy-btn-box">
<a-button
@click="handleCopyJson"
type="primary"
class="copy-btn"
data-clipboard-action="copy"
:data-clipboard-text="editorJson"
>
复制数据
</a-button>
<a-button @click="handleExportJson" type="primary">导出代码</a-button>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs } from 'vue';
import { CodeEditor, MODE } from '@/components/CodeEditor';
import { copyText } from '@/utils/copyTextToClipboard';
import { useMessage } from '@/hooks/web/useMessage';
export default defineComponent({
name: 'PreviewCode',
components: {
CodeEditor,
},
props: {
fileFormat: {
type: String,
default: 'json',
},
editorJson: {
type: String,
default: '',
},
},
setup(props) {
const state = reactive({
visible: false,
});
const exportData = (data: string, fileName = `file.${props.fileFormat}`) => {
let content = 'data:text/csv;charset=utf-8,';
content += data;
const encodedUri = encodeURI(content);
const actions = document.createElement('a');
actions.setAttribute('href', encodedUri);
actions.setAttribute('download', fileName);
actions.click();
};
const handleExportJson = () => {
exportData(props.editorJson);
};
const { createMessage } = useMessage();
const handleCopyJson = () => {
//
const value = props.editorJson;
if (!value) {
createMessage.warning('代码为空!');
return;
}
copyText(value);
};
return {
...toRefs(state),
exportData,
handleCopyJson,
handleExportJson,
MODE,
};
},
});
</script>
<style lang="less" scoped>
// modal
.copy-btn-box {
padding-top: 8px;
text-align: center;
.copy-btn {
margin-right: 8px;
}
}
</style>

View File

@ -0,0 +1,293 @@
<!--
* @Description: 正则校验选项组件
-->
<template>
<div class="rule-props-content">
<Form v-if="formConfig.currentItem && formConfig.currentItem['rules']">
<div
v-for="(item, index) of formConfig.currentItem['rules']"
:key="index"
class="rule-props-item"
>
<Icon
icon="ant-design:close-circle-filled"
class="rule-props-item-close"
@click="removeRule(index)"
/>
<FormItem label="正则" :labelCol="{ span: 6 }" :wrapperCol="{ span: 16 }">
<AutoComplete
v-model:value="item.pattern"
placeholder="请输入正则表达式"
:dataSource="patternDataSource"
/>
</FormItem>
<FormItem label="文案" :labelCol="{ span: 6 }" :wrapperCol="{ span: 16 }">
<Input v-model:value="item.message" placeholder="请输入提示文案" />
</FormItem>
</div>
</Form>
<a @click="addRules">
<Icon icon="ant-design:file-add-outlined" />
添加正则
</a>
</div>
</template>
<script lang="ts">
import { ref, defineComponent } from 'vue';
import { remove } from '../../../utils';
import { useFormDesignState } from '../../../hooks/useFormDesignState';
import { isArray } from 'lodash-es';
import { Form, FormItem, AutoComplete, Input } from 'ant-design-vue';
import Icon from '@/components/Icon/Icon.vue';
export default defineComponent({
name: 'RuleProps',
components: {
Form,
FormItem,
AutoComplete,
Input,
Icon,
},
setup() {
//
const { formConfig } = useFormDesignState();
// currentItem
/**
* 添加正则校验判断当前组件的rules是不是数组如果不是数组使用set方法重置成数组然后添加正则校验
*/
const addRules = () => {
if (!isArray(formConfig.value.currentItem!.rules))
formConfig.value.currentItem!['rules'] = [];
formConfig.value.currentItem!.rules?.push({ pattern: '', message: '' });
};
/**
* 删除正则校验当正则规则为0时删除rules属性
* @param index {number} 需要删除的规则下标
*/
const removeRule = (index: number) => {
remove(formConfig.value.currentItem!.rules as Array<any>, index);
if (formConfig.value.currentItem!.rules?.length === 0)
delete formConfig.value.currentItem!['rules'];
};
const patternDataSource = ref([
{
value: '/^(?:(?:\\+|00)86)?1[3-9]\\d{9}$/',
text: '手机号码',
},
{
value: '/^((ht|f)tps?:\\/\\/)?[\\w-]+(\\.[\\w-]+)+:\\d{1,5}\\/?$/',
text: '网址带端口号',
},
{
value:
'/^(((ht|f)tps?):\\/\\/)?[\\w-]+(\\.[\\w-]+)+([\\w.,@?^=%&:/~+#-\\(\\)]*[\\w@?^=%&/~+#-\\(\\)])?$/',
text: '网址带参数',
},
{
value: '/^[0-9A-HJ-NPQRTUWXY]{2}\\d{6}[0-9A-HJ-NPQRTUWXY]{10}$/',
text: '统一社会信用代码',
},
{
value: '/^(s[hz]|S[HZ])(000[\\d]{3}|002[\\d]{3}|300[\\d]{3}|600[\\d]{3}|60[\\d]{4})$/',
text: '股票代码',
},
{
value: '/^([a-f\\d]{32}|[A-F\\d]{32})$/',
text: 'md5格式32位',
},
{
value: '/^[a-f\\d]{4}(?:[a-f\\d]{4}-){4}[a-f\\d]{12}$/i',
text: 'GUID/UUID',
},
{
value: '/^\\d+(?:\\.\\d+){2}$/',
text: '版本号x.y.z格式',
},
{
value:
'/^https?:\\/\\/(.+\\/)+.+(\\.(swf|avi|flv|mpg|rm|mov|wav|asf|3gp|mkv|rmvb|mp4))$/i',
text: '视频链接地址',
},
{
value: '/^https?:\\/\\/(.+\\/)+.+(\\.(gif|png|jpg|jpeg|webp|svg|psd|bmp|tif))$/i',
text: '图片链接地址',
},
{
value: '/^-?\\d+(,\\d{3})*(\\.\\d{1,2})?$/',
text: '数字/货币金额(支持负数、千分位分隔符)',
},
{
value:
'/(?:^[1-9]([0-9]+)?(?:\\.[0-9]{1,2})?$)|(?:^(?:0)$)|(?:^[0-9]\\.[0-9](?:[0-9])?$)/',
text: '数字/货币金额',
},
{
value: '/^[1-9]\\d{9,29}$/',
text: '银行卡号',
},
{
value: '/^(?:[\u4e00-\u9fa5·]{2,16})$/',
text: '中文姓名',
},
{
value: '/(^[a-zA-Z][a-zA-Z\\s]{0,20}[a-zA-Z]$)/',
text: '英文姓名',
},
{
value:
'/^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-HJ-NP-Z](?:((\\d{5}[A-HJK])|([A-HJK][A-HJ-NP-Z0-9][0-9]{4}))|[A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9挂学警港澳])$/',
text: '车牌号(新能源)',
},
{
value:
'/^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-HJ-NP-Z][A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9挂学警港澳]$/',
text: '车牌号(非新能源)',
},
{
value:
'/^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-HJ-NP-Z][A-HJ-NP-Z0-9]{4,5}[A-HJ-NP-Z0-9挂学警港澳]$/',
text: '车牌号(新能源+非新能源)',
},
{
value:
'/^(([^<>()[\\]\\\\.,;:\\s@"]+(\\.[^<>()[\\]\\\\.,;:\\s@"]+)*)|(".+"))@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\])|(([a-zA-Z\\-0-9]+\\.)+[a-zA-Z]{2,}))$/',
text: 'email(邮箱)',
},
{
value: '/^(?:(?:\\d{3}-)?\\d{8}|^(?:\\d{4}-)?\\d{7,8})(?:-\\d+)?$/',
text: '座机',
},
{
value:
'/^[1-9]\\d{5}(?:18|19|20)\\d{2}(?:0[1-9]|10|11|12)(?:0[1-9]|[1-2]\\d|30|31)\\d{3}[\\dXx]$/',
text: '身份证号',
},
{
value:
'/(^[EeKkGgDdSsPpHh]\\d{8}$)|(^(([Ee][a-fA-F])|([DdSsPp][Ee])|([Kk][Jj])|([Mm][Aa])|(1[45]))\\d{7}$)/',
text: '护照',
},
{
value:
'/^(?:[\u3400-\u4DB5\u4E00-\u9FEA\uFA0E\uFA0F\uFA11\uFA13\uFA14\uFA1F\uFA21\uFA23\uFA24\uFA27-\uFA29]|[\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0])+$/',
text: '中文汉字',
},
{
value: '/^\\d+\\.\\d+$/',
text: '小数',
},
{
value: '/^\\d{1,}$/',
text: '数字',
},
{
value: '/^[1-9][0-9]{4,10}$/',
text: 'qq号',
},
{
value: '/^[A-Za-z0-9]+$/',
text: '数字字母组合',
},
{
value: '/^[a-zA-Z]+$/',
text: '英文字母',
},
{
value: '/^[a-z]+$/',
text: '小写英文字母',
},
{
value: '/^[A-Z]+$/',
text: '大写英文字母',
},
{
value: '/^[a-zA-Z0-9_-]{4,16}$/',
text: '用户名校验4到16位字母数字下划线减号',
},
{
value: '/^#?([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/',
text: '16进制颜色',
},
{
value: '/^[a-zA-Z][-_a-zA-Z0-9]{5,19}$/',
text: '微信号',
},
{
value: '/^(0[1-7]|1[0-356]|2[0-7]|3[0-6]|4[0-7]|5[1-7]|6[1-7]|7[0-5]|8[013-6])\\d{4}$/',
text: '邮政编码(中国)',
},
{
value: '/^[^A-Za-z]*$/',
text: '不能包含字母',
},
{
value: '/^\\+?[1-9]\\d*$/',
text: '正整数不包含0',
},
{
value: '/^-[1-9]\\d*$/',
text: '负整数不包含0',
},
{
value: '/^-?[0-9]\\d*$/',
text: '整数',
},
{
value: '/^(-?\\d+)(\\.\\d+)?$/',
text: '浮点数',
},
{
value: '/^[A-Za-z0-9\u4e00-\u9fa5]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$/',
text: 'email(支持中文邮箱)',
},
]);
return { addRules, removeRule, formConfig, patternDataSource };
},
});
</script>
<style lang="less" scoped>
:deep(.icon) {
width: 1em;
height: 1em;
overflow: hidden;
fill: currentcolor;
vertical-align: -0.15em;
}
.rule-props-content {
:deep(.ant-form-item) {
margin-bottom: 0;
}
.rule-props-item {
position: relative;
margin-bottom: 5px;
padding: 3px 2px;
border-radius: 5px;
background-color: #f0eded;
:deep(.ant-form-item) {
border: 0 !important;
}
&-close {
position: absolute;
z-index: 999;
top: -5px;
right: -5px;
border-radius: 7px;
background-color: #a3a0a0;
color: #ccc;
cursor: pointer;
&:hover {
color: #00c;
}
}
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,365 @@
import { IAnyObject } from '../../../typings/base-type';
import { baseComponents, commonComponents } from '../../../core/formItemConfig';
import { Input, Select, RadioGroup, Slider } from 'ant-design-vue';
import { Component } from 'vue';
export const globalConfigState: { span: number } = {
span: 24,
};
export interface IBaseFormAttrs {
name: string; // 字段名
label: string; // 字段标签
component?: string | Component; // 属性控件
componentProps?: IAnyObject; // 传递给控件的属性
exclude?: string[]; // 需要排除的控件
includes?: string[]; // 符合条件的组件
on?: IAnyObject;
children?: IBaseFormAttrs[];
category?: 'control' | 'input';
}
export interface IBaseFormItemControlAttrs extends IBaseFormAttrs {
target?: 'props' | 'options'; // 绑定到对象下的某个目标key中
}
export const baseItemColumnProps: IBaseFormAttrs[] = [
{
name: 'span',
label: '栅格数',
component: 'Slider',
on: {
change(value: number) {
globalConfigState.span = value;
},
},
componentProps: {
max: 24,
min: 0,
marks: { 12: '' },
},
},
{
name: 'offset',
label: '栅格左侧的间隔格数',
component: 'Slider',
componentProps: {
max: 24,
min: 0,
marks: { 12: '' },
},
},
{
name: 'order',
label: '栅格顺序,flex 布局模式下有效',
component: 'Slider',
componentProps: {
max: 24,
min: 0,
marks: { 12: '' },
},
},
{
name: 'pull',
label: '栅格向左移动格数',
component: 'Slider',
componentProps: {
max: 24,
min: 0,
marks: { 12: '' },
},
},
{
name: 'push',
label: '栅格向右移动格数',
component: 'Slider',
componentProps: {
max: 24,
min: 0,
marks: { 12: '' },
},
},
{
name: 'xs',
label: '<576px 响应式栅格',
component: 'Slider',
componentProps: {
max: 24,
min: 0,
marks: { 12: '' },
},
},
{
name: 'sm',
label: '≥576px 响应式栅格',
component: 'Slider',
componentProps: {
max: 24,
min: 0,
marks: { 12: '' },
},
},
{
name: 'md',
label: '≥768p 响应式栅格',
component: 'Slider',
componentProps: {
max: 24,
min: 0,
marks: { 12: '' },
},
},
{
name: 'lg',
label: '≥992px 响应式栅格',
component: 'Slider',
componentProps: {
max: 24,
min: 0,
marks: { 12: '' },
},
},
{
name: 'xl',
label: '≥1200px 响应式栅格',
component: 'Slider',
componentProps: {
max: 24,
min: 0,
marks: { 12: '' },
},
},
{
name: 'xxl',
label: '≥1600px 响应式栅格',
component: 'Slider',
componentProps: {
max: 24,
min: 0,
marks: { 12: '' },
},
},
{
name: '≥2000px',
label: '≥1600px 响应式栅格',
component: 'Slider',
componentProps: {
max: 24,
min: 0,
marks: { 12: '' },
},
},
];
// 控件属性面板的配置项
export const advanceFormItemColProps: IBaseFormAttrs[] = [
{
name: 'labelCol',
label: '标签col',
component: Slider,
componentProps: {
max: 24,
min: 0,
marks: { 12: '' },
},
exclude: ['Grid', 'Tabs'],
},
{
name: 'wrapperCol',
label: '控件-span',
component: Slider,
componentProps: {
max: 24,
min: 0,
marks: { 12: '' },
},
exclude: ['Grid', 'Tabs'],
},
];
// 控件属性面板的配置项
export const baseFormItemProps: IBaseFormAttrs[] = [
{
// 动态的切换控件的类型
name: 'component',
label: '控件-FormItem',
component: Select,
componentProps: {
options: commonComponents.concat(baseComponents).map((item) => ({
value: item.component,
label: item.label,
key: item.component + '===' + item.label,
})),
},
exclude: ['Grid', 'Tabs'],
},
{
name: 'label',
label: '标签',
component: Input,
componentProps: {
type: 'Input',
placeholder: '请输入标签',
},
exclude: ['Grid', 'Tabs'],
},
{
name: 'field',
label: '字段标识',
component: Input,
componentProps: {
type: 'InputTextArea',
placeholder: '请输入字段标识',
},
exclude: ['Grid', 'Tabs'],
},
{
name: 'defaultValue',
label: '默认值',
component: Input,
componentProps: {
placeholder: '请输入默认值',
},
exclude: ['Grid', 'Tabs'],
},
{
name: 'helpMessage',
label: '提示信息',
component: Input,
componentProps: {
placeholder: '请输入提示信息',
},
exclude: ['Grid', 'Tabs'],
},
];
// 控件属性面板的配置项
export const advanceFormItemProps: IBaseFormAttrs[] = [
{
name: 'labelAlign',
label: '标签对齐',
component: RadioGroup,
componentProps: {
options: [
{
label: '靠左',
value: 'left',
},
{
label: '靠右',
value: 'right',
},
],
},
exclude: ['Grid', 'Tabs'],
},
{
name: 'help',
label: '提示',
component: Input,
componentProps: {
placeholder: '请输入提示信息',
},
exclude: ['Grid', 'Tabs'],
},
{
name: 'extra',
label: '额外消息',
component: Input,
componentProps: {
type: 'InputTextArea',
placeholder: '请输入额外消息',
},
exclude: ['Grid', 'Tabs'],
},
{
name: 'validateTrigger',
label: '检验触发',
component: Input,
componentProps: {
type: 'InputTextArea',
placeholder: '请输入',
},
exclude: ['Grid', 'Tabs'],
},
{
name: 'validateStatus',
label: '校验状态',
component: RadioGroup,
componentProps: {
options: [
{
label: '默认',
value: '',
},
{
label: '成功',
value: 'success',
},
{
label: '警告',
value: 'warning',
},
{
label: '错误',
value: 'error',
},
{
label: '校验中',
value: 'validating',
},
],
},
exclude: ['Grid', 'Tabs'],
},
];
export const baseFormItemControlAttrs: IBaseFormItemControlAttrs[] = [
{
name: 'required',
label: '必填项',
component: 'Checkbox',
exclude: ['alert'],
},
{
name: 'hidden',
label: '隐藏',
component: 'Checkbox',
exclude: ['alert'],
},
{
name: 'hiddenLabel',
component: 'Checkbox',
exclude: ['Grid', 'Tabs'],
label: '隐藏标签',
},
{
name: 'colon',
label: 'label后面显示冒号',
component: 'Checkbox',
componentProps: {},
exclude: ['Grid', 'Tabs'],
},
{
name: 'hasFeedback',
label: '输入反馈',
component: 'Checkbox',
componentProps: {},
includes: ['Input'],
},
{
name: 'autoLink',
label: '自动关联',
component: 'Checkbox',
componentProps: {},
includes: ['Input'],
},
{
name: 'validateFirst',
label: '检验证错误停止',
component: 'Checkbox',
componentProps: {},
includes: ['Input'],
},
];

View File

@ -0,0 +1,99 @@
export const formItemPropsScript = `脚本参数说明
// 获取表单是新增还是编辑
let update = utils.isUpdate();
// 组件变更数据
let data = utils.data();
// 数据设置
// 获取主表数据
let mainValue = utils.getValue('组件的字段标识');
// 设置主表数据
setFieldsValue(utils.setValue('组件的字段标识', '设置的值'));
// 获取子表数据
let childValue = getChildValue('组件的字段标识');
// 添加子表数据
subTableList.value = addChildValue(data);
data[{1: "value1", {2: "value2"}]{1: "value1"}
// 修改子表数据
subTableList.value = addChildValue('组件的字段标识', '旧value', '新value');
// 删除子表数据
subTableList.value = deleteChildValue('组件的字段标识', 'value');
// 组件设置
// 设置组件为隐藏
formColumns.value = utils.setHide('组件的字段标识', false);
// 取消组件隐藏
formColumns.value = utils.setHide('组件的字段标识', true);
// 设置组件为禁用
formColumns.value = utils.setDisabled('组件的字段标识', true);
// 取消组件禁用
formColumns.value = utils.setDisabled('组件的字段标识', false);
// 设置组件为必填
formColumns.value = utils.setRequired('组件的字段标识', true);
// 设置组件不为必填
formColumns.value = utils.setRequired('组件的字段标识', false);
// 功能设置
// 提示消息
utils.message('提示信息', '提示类型');
successerrorwarninfo
// 获取登录者信息
var loginUser = utils.loginUser();
loginUser{ account: , name: }
// 回调方法
// get请求
// 需要用到回调方法
// 例子let resGet = await utils.httpGet('/api/FormScheme/LoadFormPage', { page: 1, limit: 10 });
let resGet = await utils.httpGet(url, params);
url:api, params:
// post请求
let resPost = await utils.httpPost(url, params);
url:api, params:
// put请求
// 需要用到回调方法
let resPut = await utils.httpPut(url, params);
url:api, params:
`;
export const options_json = `{
"options": [
{
"label": "选项1",
"value": "1",
"children": [
{
"label": "选项3",
"value": "3"
}
]
},
{
"label": "选项2",
"value": "2"
}
]
}`;
export const treeData_json = `{
"treeData": [
{
"label": "选项1",
"value": "1",
"children": [
{
"label": "选项3",
"value": "3"
}
]
},
{
"label": "选项2",
"value": "2"
}
]
}`;

View File

@ -0,0 +1,476 @@
<template>
<Layout>
<LayoutSider
:class="`left ${prefixCls}-sider`"
collapsible
collapsedWidth="0"
width="345"
:zeroWidthTriggerStyle="{
'margin-top': '-70px',
'background-color': 'white',
width: '35px',
height: '35px',
'background-image': `url(/iocn/text-indent-right.svg)`,
'background-position': 'center',
'background-repeat': 'no-repeat',
'border-radius:': '15px',
}"
breakpoint="md"
>
<div class="collapseItem-box">
<CollapseContainer title="常用控件" isTitleBold="true">
<CollapseItem
:list="commonComponents"
:handleListPush="handleListPushDrag"
@add-attrs="handleAddAttrs"
@handle-list-push="handleListPush"
/>
</CollapseContainer>
<CollapseContainer title="基础控件" isTitleBold="true">
<CollapseItem
:list="baseComponents"
:handleListPush="handleListPushDrag"
@add-attrs="handleAddAttrs"
@handle-list-push="handleListPush"
/>
</CollapseContainer>
<CollapseContainer title="自定义控件" isTitleBold="true">
<CollapseItem
:list="customComponents"
@add-attrs="handleAddAttrs"
:handleListPush="handleListPushDrag"
@handle-list-push="handleListPush"
/>
</CollapseContainer>
<CollapseContainer title="布局控件" isTitleBold="true">
<CollapseItem
:list="layoutComponents"
:handleListPush="handleListPushDrag"
@add-attrs="handleAddAttrs"
@handle-list-push="handleListPush"
/>
</CollapseContainer>
</div>
</LayoutSider>
<LayoutContent>
<Toolbar
@handle-open-json-modal="handleOpenModal(jsonModal!)"
@handle-open-import-json-modal="handleOpenModal(importJsonModal!)"
@handle-preview="handleOpenModal(eFormPreview!)"
@handle-open-code-modal="handleOpenModal(codeModal!)"
@handle-clear-form-items="handleClearFormItems"
/>
<FormComponentPanel
:current-item="formConfig.currentItem"
:data="formConfig"
@handle-set-select-item="handleSetSelectItem"
/>
</LayoutContent>
<LayoutSider
:class="`right ${prefixCls}-sider`"
collapsible
:reverseArrow="true"
collapsedWidth="0"
width="320"
:zeroWidthTriggerStyle="{
'margin-top': '-70px',
'background-color': 'white',
width: '35px',
height: '35px',
'background-image': `url(/iocn/text-indent-left.svg)`,
'background-position': 'center',
'background-repeat': 'no-repeat',
'border-radius:': '15px',
}"
breakpoint="lg"
>
<PropsPanel ref="propsPanel" :activeKey="formConfig.activeKey">
<template v-for="item of formConfig.schemas" #[`${item.component}Props`]="data">
<slot
:name="`${item.component}Props`"
v-bind="{ formItem: data, props: data.componentProps }"
></slot>
</template>
</PropsPanel>
</LayoutSider>
</Layout>
<JsonModal ref="jsonModal" />
<CodeModal ref="codeModal" />
<ImportJsonModal ref="importJsonModal" />
<VFormPreview ref="eFormPreview" :formConfig="formConfig" />
<!-- <VFormPreview2 ref="eFormPreview2" :formConfig="formConfig" /> -->
</template>
<script lang="ts" setup>
import CollapseItem from './modules/CollapseItem.vue';
import FormComponentPanel from './modules/FormComponentPanel.vue';
import JsonModal from './components/JsonModal.vue';
import VFormPreview from '../VFormPreview/index.vue';
// import VFormPreview2 from '../VFormPreview/useForm.vue';
import Toolbar from './modules/Toolbar.vue';
import PropsPanel from './modules/PropsPanel.vue';
import ImportJsonModal from './components/ImportJsonModal.vue';
import CodeModal from './components/CodeModal.vue';
import 'codemirror/mode/javascript/javascript';
import { ref, provide, Ref, inject, watch, computed, defineProps } from 'vue';
import { Layout, LayoutContent, LayoutSider } from 'ant-design-vue';
import { IVFormComponent, IFormConfig, PropsTabKey } from '../../typings/v-form-component';
import { formItemsForEach, generateKey, removeAttrs } from '../../utils';
import { cloneDeep } from 'lodash-es';
import {
commonComponents,
baseComponents,
customComponents,
layoutComponents,
} from '../../core/formItemConfig';
import { useRefHistory, UseRefHistoryReturn } from '@vueuse/core';
import { globalConfigState } from './config/formItemPropsConfig';
import { IFormDesignMethods, IPropsPanel, IToolbarMethods } from '../../typings/form-type';
import { useDesign } from '@/hooks/web/useDesign';
import { CollapseContainer } from '@/components/Container';
import { useOnlineFormDesignStore } from '@/store/modules/onlineFormDesign'
// defineProps({
// title: {
// type: String,
// default: 'v-form-antd',
// }
// });
const onlineFormDesignStore = useOnlineFormDesignStore()
const props = defineProps({
title: {
type: String,
default: 'v-form-antd表单设计器',
},
saveClick: {
default: false,
type: Boolean,
},
});
const { prefixCls } = useDesign('form-design');
//
const propsPanel = ref<null | IPropsPanel>(null);
const jsonModal = ref<null | IToolbarMethods>(null);
const importJsonModal = ref<null | IToolbarMethods>(null);
const eFormPreview = ref<null | IToolbarMethods>(null);
// const eFormPreview2 = ref<null | IToolbarMethods>(null);
const codeModal = ref<null | IToolbarMethods>(null);
const formModel = ref({});
// endregion
const formConfig = ref<IFormConfig>({
//
schemas: [],
layout: 'horizontal',
labelLayout: 'flex',
labelWidth: 100,
labelCol: {},
wrapperCol: {},
currentItem: {
component: '',
componentProps: {},
},
activeKey: 1,
});
const designSendGrandson = inject('designSendGrandson');
watch(
() => props.saveClick,
(newValue, oldValue) => {
if (newValue) {
const config = cloneDeep(formConfig.value);
let formJson = JSON.stringify(removeAttrs(config), null, '\t');
designSendGrandson(formJson);
}
},
{ immediate: true, deep: true },
);
let receivedData = ref();
const handleNextStepsData = inject('handleNextStepsData');
watch(
() => handleNextStepsData, //
(newVal) => {
if (newVal.value && newVal.value.scheme && newVal.value.scheme.scheme) {
receivedData.value = JSON.parse(newVal.value.scheme.scheme);
if (receivedData.value.formInfo.tabList && receivedData.value.formInfo.tabList.length > 1) {
const arr: any = [];
receivedData.value.formInfo.tabList.forEach((item, index) => {
arr.push({
label: item.text,
value: index + 1,
children: item.schemas,
});
});
receivedData.value.formInfo.schemas = [
{
component: 'Tabs',
label: '选项卡',
colProps: {
span: 24,
},
field: 'Tabs',
componentProps: {
options: arr,
},
itemProps: {
labelCol: {},
wrapperCol: {},
},
},
];
}
if (
receivedData.value.formInfo.tabList &&
receivedData.value.formInfo.tabList.length == 1
) {
receivedData.value.formInfo.schemas = receivedData.value.formInfo.tabList[0].schemas;
}
delete receivedData.value.formInfo.tabList;
//
const editorJsonData = receivedData.value.formInfo as IFormConfig;
editorJsonData.schemas &&
formItemsForEach(editorJsonData.schemas, (formItem) => {
generateKey(formItem);
});
setFormConfig({
...editorJsonData,
activeKey: 1,
currentItem: { component: '' },
});
}
},
{ deep: true, immediate: true },
);
const setFormConfig = (config: IFormConfig) => {
//
config.schemas = config.schemas || [];
config.schemas.forEach((item) => {
item.colProps = item.colProps || { span: 24 };
item.componentProps = item.componentProps || {};
item.itemProps = item.itemProps || {};
});
formConfig.value = config as any;
};
//
const historyReturn = useRefHistory(formConfig as any, {
deep: true,
capacity: 20,
parse: (val: IFormConfig) => {
// 使lodash.cloneDeepcurrentItem
const formConfig = cloneDeep(val);
const { currentItem, schemas } = formConfig;
// formItems
const item = schemas && schemas.find((item) => item.key === currentItem?.key);
//
if (item) {
formConfig.currentItem = item;
}
return formConfig;
},
});
/**
* 选中表单项
* @param schema 当前选中的表单项
*/
const handleSetSelectItem = (schema: IVFormComponent) => {
formConfig.value.currentItem = schema as any;
handleChangePropsTabs(
schema.key ? (formConfig.value.activeKey! === 1 ? 2 : formConfig.value.activeKey!) : 1,
);
if(schema.component === 'Select' && schema.dataType === '2' && onlineFormDesignStore.getSelectDictionaryData().length === 0){
onlineFormDesignStore.getSelectData("")
onlineFormDesignStore.setSelectOptions(schema.field,schema.dataCode)
}
};
const setGlobalConfigState = (formItem: IVFormComponent) => {
formItem.colProps = formItem.colProps || {};
formItem.colProps.span = globalConfigState.span;
// console.log('setGlobalConfigState', formItem);
};
/**
* 添加属性
* @param schemas
* @param index
*/
const handleAddAttrs = (_formItems: IVFormComponent[], _index: number) => {};
const handleListPushDrag = (item: IVFormComponent) => {
const formItem = cloneDeep(item);
setGlobalConfigState(formItem);
generateKey(formItem);
return formItem;
};
/**
* 单击控件时添加到面板中
* @param item {IVFormComponent} 当前点击的组件
*/
const handleListPush = (item: IVFormComponent) => {
// console.log('handleListPush', item);
const formItem = cloneDeep(item);
setGlobalConfigState(formItem);
generateKey(formItem);
if (!formConfig.value.currentItem?.key) {
handleSetSelectItem(formItem);
formConfig.value.schemas && formConfig.value.schemas.push(formItem as any);
return;
}
handleCopy(formItem, false);
};
/**
* 复制表单项如果表单项为栅格布局则遍历所有自表单项重新生成key
* @param {IVFormComponent} formItem
* @return {IVFormComponent}
*/
const copyFormItem = (formItem: IVFormComponent) => {
const newFormItem = cloneDeep(formItem);
if (newFormItem.component === 'Grid') {
formItemsForEach([formItem], (item) => {
generateKey(item);
});
}
return newFormItem;
};
/**
* 复制或者添加表单isCopy为true时则复制表单
* @param item {IVFormComponent} 当前点击的组件
* @param isCopy {boolean} 是否复制
*/
const handleCopy = (
item: IVFormComponent = formConfig.value.currentItem as IVFormComponent,
isCopy = true,
) => {
const key = formConfig.value.currentItem?.key;
/**
* 遍历当表单项配置如果是复制则复制一份表单项如果不是复制则直接添加到表单项中
* @param schemas
*/
const traverse = (schemas: IVFormComponent[]) => {
// 使some
schemas.some((formItem: IVFormComponent, index: number) => {
if (formItem.key === key) {
//
isCopy
? schemas.splice(index, 0, copyFormItem(formItem))
: schemas.splice(index + 1, 0, item);
const event = {
newIndex: index + 1,
};
//
handleBeforeColAdd(event, schemas, isCopy);
return true;
}
if (['Grid', 'Tabs'].includes(formItem.component)) {
//
formItem.columns?.forEach((item) => {
traverse(item.children);
});
}
});
};
if (formConfig.value.schemas) {
traverse(formConfig.value.schemas as any);
}
};
/**
* 添加到表单中
* @param newIndex {object} 事件对象
* @param schemas {IVFormComponent[]} 表单项列表
* @param isCopy {boolean} 是否复制
*/
const handleBeforeColAdd = ({ newIndex }: any, schemas: IVFormComponent[], isCopy = false) => {
const item = schemas[newIndex];
isCopy && generateKey(item);
handleSetSelectItem(item);
};
/**
* 打开模态框
* @param Modal {IToolbarMethods}
*/
const handleOpenModal = (Modal: IToolbarMethods) => {
const config = cloneDeep(formConfig.value);
Modal?.showModal(config);
};
/**
* 切换属性面板
* @param key
*/
const handleChangePropsTabs = (key: PropsTabKey) => {
formConfig.value.activeKey = key;
};
/**
* 清空表单项列表
*/
const handleClearFormItems = () => {
formConfig.value.schemas = [];
handleSetSelectItem({ component: '' });
};
const setFormModel = (key, value) => (formModel.value[key] = value);
provide('formModel', formModel);
// inject
provide<(key: String, value: any) => void>('setFormModelMethod', setFormModel);
// region
// provide('currentItem', formConfig.value.currentItem)
// inject
provide<Ref<IFormConfig>>('formConfig', formConfig as any);
//
provide<UseRefHistoryReturn<any, any>>('historyReturn', historyReturn);
// inject
provide<IFormDesignMethods>('formDesignMethods', {
handleBeforeColAdd,
handleCopy,
handleListPush,
handleSetSelectItem,
handleAddAttrs,
setFormConfig,
});
// endregion
</script>
<style lang="less" scoped>
@prefix-cls: ~'@{namespace}-form-design';
[data-theme='dark'] {
.@{prefix-cls}-sider {
background-color: #1f1f1f;
}
}
[data-theme='light'] {
.@{prefix-cls}-sider {
background-color: #fff;
}
}
.collapseItem-box {
height: calc(100vh - 55px);
overflow: auto;
}
.vben-basic-title {
font-weight: bold;
}
</style>

View File

@ -0,0 +1,112 @@
<template>
<div :class="prefixCls">
<draggable
tag="ul"
:model-value="list"
v-bind="{
group: { name: 'form-draggable', pull: 'clone', put: false },
sort: false,
clone: cloneItem,
animation: 180,
ghostClass: 'moving',
}"
item-key="type"
@start="handleStart($event, list)"
@add="handleAdd"
>
<template #item="{ element, index }">
<li
class="bs-box text-ellipsis"
@dragstart="$emit('add-attrs', list, index)"
@click="$emit('handle-list-push', element)"
>
<!-- <svg v-if="element.icon.indexOf('icon-') > -1" class="icon" aria-hidden="true">
<use :xlink:href="`#${element.icon}`" />
</svg> -->
<Icon :icon="element.icon" />
{{ element.label }}</li
></template
>
</draggable>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, PropType } from 'vue';
import { IVFormComponent } from '../../../typings/v-form-component';
import draggable from 'vuedraggable';
import Icon from '@/components/Icon/Icon.vue';
import { useDesign } from '@/hooks/web/useDesign';
export default defineComponent({
name: 'CollapseItem',
components: { draggable, Icon },
props: {
list: {
type: [Array] as PropType<IVFormComponent[]>,
default: () => [],
},
handleListPush: {
type: Function,
default: null,
},
},
setup(props, { emit }) {
const { prefixCls } = useDesign('form-design-collapse-item');
const state = reactive({});
const handleStart = (e: any, list1: IVFormComponent[]) => {
emit('start', list1[e.oldIndex].component);
};
const handleAdd = (e: any) => {
console.log(e);
};
// https://github.com/SortableJS/vue.draggable.next
// https://github.com/SortableJS/vue.draggable.next/blob/master/example/components/custom-clone.vue
const cloneItem = (one) => {
return props.handleListPush(one);
};
return { prefixCls, state, handleStart, handleAdd, cloneItem };
},
});
</script>
<style lang="less" scoped>
@prefix-cls: ~'@{namespace}-form-design-collapse-item';
@import url('../styles/variable.less');
.@{prefix-cls} {
ul {
display: flex;
flex-wrap: wrap;
margin-bottom: 0;
padding: 5px;
list-style: none;
// background: #efefef;
li {
width: calc(50% - 6px);
height: 36px;
margin: 2.7px;
padding: 8px 12px;
transition: all 0.3s;
border: 1px solid @border-color;
border-radius: 3px;
line-height: 20px;
cursor: move;
&:hover {
position: relative;
border: 1px solid @primary-color;
// z-index: 1;
box-shadow: 0 2px 6px @primary-color;
color: @primary-color;
}
}
}
svg {
display: inline !important;
}
}
</style>

View File

@ -0,0 +1,193 @@
<!--
* @Description: 中间表单布局面板
* https://github.com/SortableJS/vue.draggable.next/issues/138
-->
<template>
<div class="form-panel v-form-container">
<ImagePreview v-if="isShowImagePreview" :globalImagePreviewUrl="globalImagePreviewUrl" @closeImagePreview="closeImagePreview"></ImagePreview>
<Empty
class="empty-text"
v-show="formConfig.schemas.length === 0"
description="从左侧选择控件添加"
/>
<Form v-bind="formConfig">
<div class="image-preview" v-if="false">
<ImagePreview ></ImagePreview>
</div>
<div class="draggable-box">
<draggable
class="list-main ant-row"
group="form-draggable"
:component-data="{ name: 'list', tag: 'div', type: 'transition-group' }"
ghostClass="moving"
:animation="180"
handle=".drag-move"
v-model="formConfig.schemas"
item-key="key"
@add="addItem"
@start="handleDragStart"
>
<template #item="{ element }">
<LayoutItem
class="drag-move"
:schema="element"
:data="formConfig"
:current-item="formConfig.currentItem || {}"
/>
</template>
</draggable>
</div>
</Form>
</div>
</template>
<script lang="ts">
import draggable from 'vuedraggable';
import LayoutItem from '../components/LayoutItem.vue';
import { defineComponent, computed,ref,inject,watch } from 'vue';
import { storeToRefs } from 'pinia';
import { cloneDeep } from 'lodash-es';
import { useFormDesignState } from '../../../hooks/useFormDesignState';
import { Form, Empty } from 'ant-design-vue';
import ImagePreview from "@/components/Upload/src/components/image_preview.vue";
import { userFormFileStore } from '@/store/modules/formFileUrl';
export default defineComponent({
name: 'FormComponentPanel',
components: {
LayoutItem,
draggable,
Form,
Empty,
ImagePreview
},
emits: ['handleSetSelectItem'],
setup(_, { emit }) {
const { formConfig } = useFormDesignState();
/**
* 拖拽完成事件
* @param newIndex
*/
const addItem = ({ newIndex }: any) => {
formConfig.value.schemas = formConfig.value.schemas || [];
const schemas = formConfig.value.schemas;
schemas[newIndex] = cloneDeep(schemas[newIndex]);
emit('handleSetSelectItem', schemas[newIndex]);
};
/**
* 拖拽开始事件
* @param e {Object} 事件对象
*/
const handleDragStart = (e: any) => {
emit('handleSetSelectItem', formConfig.value.schemas[e.oldIndex]);
};
// currentItem
// AColdiv
const layoutTag = computed(() => {
return formConfig.value.layout === 'horizontal' ? 'Col' : 'div';
});
//
const isShowImagePreview = ref<Boolean>(false);
const closeImagePreview = ()=>{
isShowImagePreview.value = false;
}
const globalImagePreviewUrl=ref<String>();
const formFileStore = userFormFileStore();
const formFileState = storeToRefs(formFileStore);
watch(formFileState.url, (newValue, oldValue) => {
// isShowImagePreview.value = true;
// globalImagePreviewUrl.value = newValue;
});
return {
addItem,
handleDragStart,
formConfig,
layoutTag,
closeImagePreview,
isShowImagePreview,
globalImagePreviewUrl
};
},
});
</script>
<style lang="less" scoped>
@import url('../styles/variable.less');
@import url('../styles/drag.less');
.v-form-container {
//
.ant-form-inline {
.list-main {
display: flex;
flex-wrap: wrap;
place-content: flex-start flex-start;
.layout-width {
width: 100%;
}
}
.ant-form-item-control-wrapper {
min-width: 175px !important;
}
}
}
.form-panel {
position: relative;
height: 100%;
.empty-text {
position: absolute;
z-index: 100;
inset: -10% 0 0;
height: 150px;
margin: auto;
color: #aaa;
}
.draggable-box {
height: calc(100vh - 120px);
// width: 100%;
overflow: auto;
.drag-move {
min-height: 62px;
cursor: move;
}
.list-main {
height: 100%;
overflow-y: scroll;
//
.list-enter-active {
transition: all 0.5s;
}
.list-leave-active {
transition: all 0.3s;
}
.list-enter,
.list-leave-to {
transform: translateX(-100px);
opacity: 0;
}
.list-enter {
height: 30px;
}
}
}
}
</style>

View File

@ -0,0 +1,98 @@
<!--
* @Description: 右侧属性配置面板
-->
<template>
<div>
<Tabs v-model:activeKey="formConfig.activeKey" :tabBarStyle="{ margin: 0 }">
<TabPane :key="1" tab="表单">
<FormProps />
</TabPane>
<TabPane :key="2" tab="控件">
<FormItemProps />
</TabPane>
<TabPane :key="3" tab="栅格">
<ComponentColumnProps />
</TabPane>
<TabPane :key="4" tab="组件">
<slot v-if="slotProps" :name="slotProps.component + 'Props'"></slot>
<ComponentProps v-else />
</TabPane>
</Tabs>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, ref, watch, inject } from 'vue';
import FormProps from '../components/FormProps.vue';
import FormItemProps from '../components/FormItemProps.vue';
import ComponentProps from '../components/ComponentProps.vue';
import ComponentColumnProps from '../components/FormItemColumnProps.vue';
import { useFormDesignState } from '../../../hooks/useFormDesignState';
import { customComponents } from '../../../core/formItemConfig';
import { TabPane, Tabs } from 'ant-design-vue';
type ChangeTabKey = 1 | 2;
export interface IPropsPanel {
changeTab: (key: ChangeTabKey) => void;
}
export default defineComponent({
name: 'PropsPanel',
components: {
FormProps,
FormItemProps,
ComponentProps,
ComponentColumnProps,
Tabs,
TabPane,
},
setup() {
const { formConfig } = useFormDesignState();
const slotProps = computed(() => {
// return customComponents.find(
// (item) => item.component === formConfig.value.currentItem?.component,
// );
});
// return { formConfig, customComponents, slotProps };
return { formConfig, slotProps };
},
});
</script>
<style lang="less" scoped>
@import url('../styles/variable.less');
:deep(.ant-tabs) {
box-sizing: border-box;
form {
width: 100%;
height: 85vh;
margin-right: 10px;
overflow: hidden auto;
}
.hint-box {
margin-top: 200px;
}
.ant-form-item,
.ant-slider-with-marks {
margin-right: 20px;
margin-bottom: 0;
margin-left: 10px;
}
.ant-form-item {
// width: 100%;
margin-bottom: 0;
.ant-form-item-label {
line-height: 2;
vertical-align: text-top;
}
}
.ant-input-number {
width: 100%;
}
}
</style>

View File

@ -0,0 +1,139 @@
<!--
* @Description: 工具栏
-->
<template>
<div class="operating-area">
<!-- 头部操作按钮区域 start -->
<!-- 操作左侧区域 start -->
<div class="left-btn-box" style="text-indent: 20px">
<Tooltip v-for="item in toolbarsConfigs" :title="item.title" :key="item.icon">
<a-button @click="$emit(item.event)" id="button">
<template #icon>
<Icon :icon="item.icon" :style="{ fontSize: '16px' }" />
</template>
</a-button>
</Tooltip>
<Tooltip title="撤销">
<a-button :class="{ disabled: !canUndo }" :disabled="!canUndo" @click="undo" id="button">
<template #icon>
<Icon icon="ant-design:undo-outlined" :style="{ fontSize: '16px' }" />
</template>
</a-button>
</Tooltip>
<Tooltip title="重做">
<a-button :class="{ disabled: !canRedo }" @click="redo" id="button">
<template #icon>
<Icon icon="ant-design:redo-outlined" :style="{ fontSize: '16px' }" />
</template>
</a-button>
</Tooltip>
</div>
</div>
<!-- 操作区域 start -->
</template>
<script lang="ts">
import { defineComponent, inject, reactive, toRefs } from 'vue';
import { UseRefHistoryReturn } from '@vueuse/core';
import { IFormConfig } from '../../../typings/v-form-component';
import { Tooltip } from 'ant-design-vue';
import Icon from '@/components/Icon/Icon.vue';
interface IToolbarsConfig {
type: string;
title: string;
icon: string;
event: string;
}
export default defineComponent({
name: 'OperatingArea',
components: {
Tooltip,
Icon,
},
setup() {
const state = reactive<{
toolbarsConfigs: IToolbarsConfig[];
}>({
toolbarsConfigs: [
{
title: '预览-支持布局',
type: 'preview',
event: 'handlePreview',
icon: 'bi:life-preserver',
},
// {
// title: '-',
// type: 'preview',
// event: 'handlePreview2',
// icon: 'ant-design:chrome-filled',
// },
{
title: '导入JSON',
type: 'importJson',
event: 'handleOpenImportJsonModal',
icon: 'ant-design:import-outlined',
},
{
title: '生成JSON',
type: 'exportJson',
event: 'handleOpenJsonModal',
icon: 'ant-design:export-outlined',
},
{
title: '生成代码',
type: 'exportCode',
event: 'handleOpenCodeModal',
icon: 'bi:code-slash',
},
{
title: '清空',
type: 'reset',
event: 'handleClearFormItems',
icon: 'ant-design:clear-outlined',
},
],
});
const historyRef = inject('historyReturn') as UseRefHistoryReturn<IFormConfig, IFormConfig>;
const { undo, redo, canUndo, canRedo } = historyRef;
return { ...toRefs(state), undo, redo, canUndo, canRedo };
},
});
</script>
<style lang="less" scoped>
//noinspection CssUnknownTarget
@import url('../styles/variable.less');
.operating-area {
display: flex;
place-content: center space-between;
height: @operating-area-height;
padding: 0 12px;
padding-left: 30px;
border-bottom: 2px solid @border-color;
font-size: 16px;
line-height: @operating-area-height;
text-align: left;
#button {
margin: 0 5px;
width: 32px;
height: 32px;
&.disabled,
&.disabled:hover {
color: #ccc;
}
&:hover {
color: @primary-color;
}
> span {
font-size: 14px;
}
}
}
</style>

View File

@ -0,0 +1,241 @@
.draggable-box {
height: 100%;
overflow: auto;
background-color: @component-onlineform-formdesign-background-color;
:deep(.list-main) {
position: relative;
padding: 5px;
margin: 3px;
overflow: hidden;
border-radius: 4px;
.moving {
position: relative;
box-sizing: border-box;
// 拖放移动中;
min-height: 35px;
padding: 0 !important;
overflow: hidden;
&::before {
content: '';
position: absolute;
top: 0;
right: 0;
width: 100%;
height: 3px;
background-color: @primary-color;
}
}
.drag-move-box {
position: relative;
box-sizing: border-box;
min-height: 60px;
padding: 8px;
margin: 3px;
overflow: hidden;
transition: all 0.3s;
border-radius: 3px;
&:hover {
background-color: @primary-hover-bg-color;
}
// 选择时 start
&::before {
content: '';
position: absolute;
top: 0;
right: -100%;
width: 0%;
height: 3px;
transition: all 0.3s;
outline: 3px solid @primary-color;
background-color: @primary-color;
}
&.active {
outline: 3px solid @primary-color;
// outline-offset: 0;
background-color: @primary-hover-bg-color;
&::before {
right: 0;
}
}
// 选择时 end
.form-item-box {
position: relative;
box-sizing: border-box;
word-wrap: break-word;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.ant-form-item {
// 修改ant form-item的margin为padding
margin: 0;
padding-bottom: 6px;
}
}
.show-key-box {
// 显示key
position: absolute;
right: 3px;
bottom: 2px;
// z-index: 999;
color: @primary-color;
font-size: 14px;
}
.copy,
.delete {
position: absolute;
top: 0;
width: 30px;
height: 30px;
// z-index: 989;
transition: all 0.3s;
color: #fff;
line-height: 30px;
text-align: center;
&.unactivated {
opacity: 0 !important;
pointer-events: none;
}
&.active {
opacity: 1 !important;
}
}
.copy {
right: 30px;
border-radius: 0 0 0 8px;
background-color: @primary-color;
}
.delete {
right: 0;
background-color: @primary-color;
}
}
.grid-box {
position: relative;
box-sizing: border-box;
width: 100%;
padding: 5px;
margin: 3px;
overflow: hidden;
transition: all 0.3s;
background-color: @layout-background-color;
border-radius: 3px;
// 鼠标划过
&:hover {
background-color: @layout-hover-bg-color;
}
.form-item-box {
position: relative;
box-sizing: border-box;
.ant-form-item {
// 修改ant form-item的margin为padding
margin: 0;
padding-bottom: 15px;
}
}
.grid-row {
background-color: @layout-background-color;
.grid-col {
.draggable-box {
min-width: 50px;
min-height: 80px;
border: 1px #ccc dashed;
// background: #fff;
.list-main {
position: relative;
min-height: 83px;
border: 1px #ccc dashed;
}
}
}
}
// 选择时 start
&::before {
content: '';
position: absolute;
top: 0;
right: -100%;
width: 0%;
height: 3px;
transition: all 0.3s;
background: transparent;
}
&.active {
outline: 3px solid @layout-color;
// outline-offset: 0;
background-color: @layout-hover-bg-color;
&::before {
right: 0;
background-color: @layout-color;
}
}
// 选择时 end
> .copy-delete-box {
> .copy,
> .delete {
position: absolute;
top: 0;
width: 30px;
height: 30px;
// z-index: 989;
transition: all 0.3s;
color: #fff;
line-height: 30px;
text-align: center;
&.unactivated {
opacity: 0 !important;
pointer-events: none;
}
&.active {
opacity: 1 !important;
}
}
> .copy {
right: 30px;
border-radius: 0 0 0 8px;
background-color: @layout-color;
}
> .delete {
right: 0;
background-color: @layout-color;
}
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More