<script lang="ts" setup>
|
import { inject, nextTick, onMounted, reactive, toRefs } from 'vue';
|
|
import { useMessage, useRedo } from '@jnpf/hooks';
|
import { ScrollContainer } from '@jnpf/ui';
|
import { useModal } from '@jnpf/ui/modal';
|
import { isUrl } from '@jnpf/utils';
|
|
import { CopyOutlined, DeleteOutlined } from '@ant-design/icons-vue';
|
import { cloneDeep } from 'lodash-es';
|
import draggable from 'vuedraggable';
|
|
import { $t } from '#/locales';
|
|
import HAppCommon from '../../Portal/HAppCommon/index.vue';
|
import { basicComponents, chartComponents, layoutComponents, systemComponents } from '../helper/componentMap';
|
import { chartData, mapChartData, needDefaultList, needJumpSetList, noNeedMaskList, rankList, tableList, timeAxisList } from '../helper/dataMap';
|
import Parser from './Parser.vue';
|
import PreviewModal from './Preview.vue';
|
import RightPanel from './RightPanel.vue';
|
|
interface State {
|
componentsList: any[];
|
layout: any[];
|
refresh: any;
|
config: any;
|
copyDrawingList: string;
|
activeId: string;
|
activeData: any;
|
}
|
|
const props = defineProps(['conf', 'showType']);
|
const state = reactive<State>({
|
componentsList: [
|
{ title: '布局控件', list: layoutComponents },
|
{ title: '基础控件', list: basicComponents },
|
{ title: '系统控件', list: systemComponents },
|
{ title: '图表控件', list: chartComponents },
|
],
|
layout: [],
|
refresh: {},
|
config: {},
|
copyDrawingList: '',
|
activeId: '',
|
activeData: null,
|
});
|
const { componentsList, refresh, activeId, activeData, layout } = toRefs(state);
|
const defaultConf = {
|
layoutId: 100,
|
layout: [],
|
refresh: {
|
autoRefresh: false,
|
autoRefreshTime: '5',
|
},
|
};
|
defineExpose({ getData, handleData });
|
const emitter: any = inject('emitter');
|
const { createConfirm } = useMessage();
|
const [registerPreview, { openModal: openPreviewModal }] = useModal();
|
const { initRedo, addRecord, handleUndo, handleRedo, getCanUndo, getCanRedo } = useRedo();
|
|
function init() {
|
if (typeof props.conf === 'object' && props.conf !== null) {
|
state.config = { ...defaultConf, ...props.conf };
|
} else {
|
state.config = cloneDeep(defaultConf);
|
state.config.layoutId = 100;
|
}
|
state.layout = state.config.layout || [];
|
state.refresh = state.config.refresh || {};
|
initRedo();
|
setTimeout(() => {
|
addLocalRecord(state.layout);
|
setActiveData();
|
}, 50);
|
emitter.on('addComponent', (data) => {
|
handleAddComponent(data.val, '', data.item);
|
});
|
emitter.on('handlerActive', (val) => {
|
handleClick(val);
|
});
|
}
|
function handleAddComponent(item, type?, currentItem?) {
|
const clone = cloneDeep(item);
|
const i = state.config.layoutId;
|
const x = 0;
|
let y = 0;
|
if (state.layout.length) {
|
let maxYItem = { y: -1, h: 0 };
|
for (let i = 0; i < state.layout.length; i++) {
|
if (state.layout[i].y > maxYItem.y || (state.layout[i].y == maxYItem.y && state.layout[i].h > maxYItem.h)) maxYItem = state.layout[i];
|
}
|
y = maxYItem.y + maxYItem.h;
|
}
|
const row = { ...clone, i, x, y };
|
if (needDefaultList.includes(row.jnpfKey) && type != 'copy') row.option.defaultValue = getDefaultValue(row);
|
if (row.jnpfKey == 'customEcharts' && type != 'copy') {
|
row.option = getDefaultValue(row);
|
row.appOption = getAppDefaultValue(row);
|
}
|
if (currentItem) {
|
if (currentItem.jnpfKey == 'card') currentItem.children = [row];
|
if (currentItem.jnpfKey == 'tab') {
|
for (let i = 0; i < currentItem.children.length; i++) {
|
const element = currentItem.children[i];
|
if (element.name == currentItem.active) currentItem.children[i].children = [row];
|
}
|
}
|
} else {
|
state.layout.push(row);
|
}
|
state.activeId = state.config.layoutId;
|
state.activeData = row;
|
state.config.layoutId++;
|
addLocalRecord(state.layout);
|
}
|
function getDefaultValue(row) {
|
const jnpfKey = row.jnpfKey;
|
if (jnpfKey == 'text') return 'JNPF快速开发平台';
|
if (jnpfKey == 'image') return 'https://app.cdn.jnpfsoft.com/image/3.2/banner1.png';
|
if (jnpfKey == 'video') return 'https://cdn.jnpfsoft.com/2022/video/index_video.mp4';
|
if (jnpfKey == 'barChart' || jnpfKey == 'lineChart' || jnpfKey == 'pieChart') return chartData.baseBarData;
|
if (jnpfKey == 'radarChart') return chartData.radarData;
|
if (jnpfKey == 'mapChart') return mapChartData;
|
if (jnpfKey == 'rankList') return rankList;
|
if (jnpfKey == 'timeAxis') return timeAxisList;
|
if (jnpfKey == 'tableList') return tableList;
|
if (jnpfKey == 'customEcharts') return chartData.customEchartsData;
|
}
|
function getAppDefaultValue(row) {
|
const jnpfKey = row.jnpfKey;
|
if (jnpfKey == 'customEcharts') return chartData.customAppEchartsData;
|
}
|
function addLocalRecord(val) {
|
if (JSON.stringify(val) != state.copyDrawingList) {
|
state.copyDrawingList = JSON.stringify(val);
|
addRecord(val);
|
}
|
}
|
function setActiveData(i = 0) {
|
state.activeId = '';
|
state.activeData = {};
|
if (state.layout.length) {
|
state.activeData = state.layout[i];
|
state.activeId = state.layout[i].i;
|
}
|
}
|
function onRelationChange() {}
|
function resizedEvent(i, item) {
|
emitter.emit(`eChart${i}`);
|
const loop = (data) => {
|
if (data.children && item.children.length) {
|
data.children.map((ele) => {
|
if (ele.jnpfKey) emitter.emit(`eChart${ele.i}`);
|
if (ele.children && ele.children.length) loop(ele);
|
return ele;
|
});
|
}
|
};
|
loop(item);
|
addLocalRecord(state.layout);
|
}
|
function movedEvent() {
|
addLocalRecord(state.layout);
|
}
|
function handleClick(item) {
|
state.activeId = item.i;
|
state.activeData = item;
|
}
|
function handleRemoveItem(i) {
|
state.layout = state.layout.filter((item) => item.i !== i);
|
state.activeId = '';
|
state.activeData = {};
|
addLocalRecord(state.layout);
|
nextTick(() => {
|
const len = state.layout.length;
|
if (len) setActiveData(len - 1);
|
});
|
}
|
function getData() {
|
return new Promise((resolve, reject) => {
|
const loop = (list) => {
|
for (const e of list) {
|
const option = e.option || {};
|
const card = e.card || {};
|
if (card.cardRightBtn) {
|
if (card.linkType === '1' && !card.urlAddress) {
|
reject(new Error(`${e.label}控件“菜单名称”属性不能为空`));
|
break;
|
}
|
if (card.linkType === '2') {
|
if (!card.urlAddress) {
|
reject(new Error(`${e.label}控件“链接地址”属性不能为空`));
|
break;
|
}
|
if (!isUrl(card.urlAddress)) {
|
reject(new Error('请输入正确的链接地址'));
|
break;
|
}
|
}
|
if (card.appLinkType === '1' && !card.appUrlAddress) {
|
reject(new Error(`${e.label}控件“菜单名称”属性不能为空`));
|
break;
|
}
|
if (card.appLinkType === '2') {
|
if (!card.appUrlAddress) {
|
reject(new Error(`${e.label}控件“链接地址”属性不能为空`));
|
break;
|
}
|
if (!isUrl(card.appUrlAddress)) {
|
reject(new Error('请输入正确的链接地址'));
|
break;
|
}
|
}
|
}
|
if (option.linkType === '1' && !option.urlAddress) {
|
reject(new Error(`${e.label}控件“菜单名称”属性不能为空`));
|
break;
|
}
|
if (option.linkType === '2') {
|
if (!option.urlAddress) {
|
reject(new Error(`${e.label}控件“链接地址”属性不能为空`));
|
break;
|
}
|
if (!isUrl(option.urlAddress)) {
|
reject(new Error('请输入正确的链接地址'));
|
break;
|
}
|
}
|
if (option.appLinkType === '1' && !option.appUrlAddress) {
|
reject(new Error(`${e.label}控件“菜单名称”属性不能为空`));
|
break;
|
}
|
if (option.appLinkType === '2') {
|
if (!option.appUrlAddress) {
|
reject(new Error(`${e.label}控件“链接地址”属性不能为空`));
|
break;
|
}
|
if (!isUrl(option.appUrlAddress)) {
|
reject(new Error('请输入正确的链接地址'));
|
break;
|
}
|
}
|
if ((e.jnpfKey == 'video' || e.jnpfKey == 'image') && option.styleType == 2) {
|
const val = e.jnpfKey == 'video' ? '视频' : '图片';
|
if (!option.defaultValue) {
|
reject(new Error(`${e.label}控件“${val}地址”属性不能为空`));
|
break;
|
}
|
if (!isUrl(option.defaultValue)) {
|
reject(new Error(`请输入正确的${val}地址`));
|
break;
|
}
|
}
|
if (e.jnpfKey == 'iframe') {
|
if (!option.defaultValue) {
|
reject(new Error(`${e.label}控件“链接地址”属性不能为空`));
|
break;
|
}
|
if (!isUrl(option.defaultValue)) {
|
reject(new Error('请输入正确的链接地址'));
|
break;
|
}
|
}
|
if (needJumpSetList.includes(e.jnpfKey) && option.urlAddress && !isUrl(option.urlAddress)) {
|
reject(new Error('请输入正确的跳转地址'));
|
break;
|
}
|
if (e.children && Array.isArray(e.children)) loop(e.children);
|
}
|
};
|
loop(state.layout);
|
state.config.layout = state.layout;
|
resolve({ formData: state.config, target: 1 });
|
});
|
}
|
function handleEmpty() {
|
createConfirm({
|
iconType: 'warning',
|
title: $t('common.tipTitle'),
|
content: $t('formGenerator.cleanComponentTip'),
|
onOk: () => {
|
state.layout = [];
|
state.config.layoutId = 100;
|
state.activeId = '';
|
state.activeData = {};
|
addLocalRecord(state.layout);
|
},
|
});
|
}
|
function handleData(data) {
|
state.layout = cloneDeep(data);
|
state.copyDrawingList = JSON.stringify(state.layout);
|
let boo = false;
|
const loop = (list) => {
|
for (const e of list) {
|
if (e.i === state.activeId) {
|
state.activeData = e;
|
state.activeId = e.i;
|
boo = true;
|
}
|
if (e.children && Array.isArray(e.children)) loop(e.children);
|
}
|
};
|
loop(state.layout);
|
if (!boo) {
|
state.activeData = {};
|
state.activeId = '';
|
}
|
}
|
function handlePreview() {
|
openPreviewModal(true, { layout: state.layout });
|
}
|
function handleRedoAndUndo(type) {
|
const method = type == 1 ? handleUndo : handleRedo;
|
method(handleData);
|
}
|
|
onMounted(() => init());
|
</script>
|
<template>
|
<div class="portal-design-box">
|
<div class="center-box">
|
<div class="action-bar">
|
<div class="action-bar-left">
|
<div v-for="(item, listIndex) in componentsList" :key="listIndex" class="components-part">
|
<a-dropdown placement="bottom">
|
<div class="components-title">{{ item.title }}<i class="icon-ym icon-ym-arrow-down"></i></div>
|
<template #overlay>
|
<a-menu class="components-menu">
|
<a-menu-item class="components-item" v-for="(child, index) in item.list" :key="index" @click="handleAddComponent(child)">
|
<i :class="child.icon"></i>
|
{{ child.label }}
|
</a-menu-item>
|
</a-menu>
|
</template>
|
</a-dropdown>
|
</div>
|
</div>
|
<div class="action-bar-right">
|
<a-tooltip :title="$t('common.undoText')">
|
<a-button type="text" class="action-bar-btn" :disabled="!getCanUndo" @click="handleRedoAndUndo(1)">
|
<i class="icon-ym icon-ym-undo"></i>
|
</a-button>
|
</a-tooltip>
|
<a-tooltip :title="$t('common.redoText')">
|
<a-button type="text" class="action-bar-btn" :disabled="!getCanRedo" @click="handleRedoAndUndo(2)">
|
<i class="icon-ym icon-ym-redo"></i>
|
</a-button>
|
</a-tooltip>
|
<a-divider type="vertical" class="action-bar-divider" />
|
<a-tooltip :title="$t('common.cleanText')">
|
<a-button type="text" class="action-bar-btn" @click="handleEmpty">
|
<i class="icon-ym icon-ym-clean"></i>
|
</a-button>
|
</a-tooltip>
|
<a-tooltip :title="$t('common.previewText')">
|
<a-button type="text" class="action-bar-btn" @click="handlePreview">
|
<i class="icon-ym icon-ym-video-play"></i>
|
</a-button>
|
</a-tooltip>
|
</div>
|
</div>
|
<ScrollContainer class="layout-area">
|
<div v-if="showType == 'pc'">
|
<grid-layout v-model:layout="layout" :row-height="40" v-if="layout.length">
|
<grid-item
|
v-for="item in layout"
|
:x="item.x"
|
:y="item.y"
|
:w="item.w"
|
:h="item.h"
|
:i="item.i"
|
:key="item.i"
|
:max-h="item.maxH"
|
:min-h="item.minH"
|
:min-w="item.minW"
|
:max-w="item.maxW"
|
:class="{ 'active-item': item.i === activeId }"
|
@resized="resizedEvent(item.i, item)"
|
@moved="movedEvent"
|
@click="handleClick(item)">
|
<Parser :item="item" :active-id="activeId" />
|
<div class="mask" v-if="!noNeedMaskList.includes(item.jnpfKey)"></div>
|
<div class="drawing-item-action">
|
<span :title="$t('common.copyText')" class="drawing-item-action-item drawing-item-copy" @click="handleAddComponent(item, 'copy')">
|
<CopyOutlined />
|
</span>
|
<a-popconfirm
|
:title="$t('formGenerator.delComponentTip')"
|
class="drawing-item-action-item drawing-item-delete"
|
@confirm="handleRemoveItem(item.i)">
|
<span :title="$t('common.delText')">
|
<DeleteOutlined />
|
</span>
|
</a-popconfirm>
|
</div>
|
</grid-item>
|
</grid-layout>
|
<div v-show="!layout.length" class="empty-info">
|
<img src="@/assets/images/portal/empty.png" class="empty-img" />
|
</div>
|
</div>
|
<div id="ipad" v-else>
|
<div class="outer-ipad">
|
<div class="ipad-body">
|
<ScrollContainer class="center-scrollbar">
|
<draggable v-if="layout.length" class="drawing-board" :list="layout" :animation="340" group="componentsGroup" item-key="i">
|
<template #item="{ element }">
|
<div class="item-box portal-tabs-box m-[10px]" @click="handleClick(element)" :class="{ 'active-item': element.i === activeId }">
|
<HAppCommon :active-data="element" :active-id="activeId" />
|
<div class="mask" v-if="!noNeedMaskList.includes(element.jnpfKey)"></div>
|
<div class="drawing-item-action">
|
<span :title="$t('common.copyText')" class="drawing-item-action-item drawing-item-copy" @click="handleAddComponent(element)">
|
<CopyOutlined />
|
</span>
|
<a-popconfirm
|
:title="$t('formGenerator.delComponentTip')"
|
class="drawing-item-action-item drawing-item-delete"
|
@confirm="handleRemoveItem(element.i)">
|
<span :title="$t('common.delText')">
|
<DeleteOutlined />
|
</span>
|
</a-popconfirm>
|
</div>
|
</div>
|
</template>
|
</draggable>
|
<div v-show="!layout.length" class="empty-info app-empty-info">
|
<img src="@/assets/images/portal/empty.png" class="empty-img" />
|
</div>
|
</ScrollContainer>
|
</div>
|
</div>
|
</div>
|
</ScrollContainer>
|
</div>
|
<RightPanel :active-data="activeData" :refresh="refresh" :show-type="showType" @relation-change="onRelationChange" />
|
<PreviewModal @register="registerPreview" />
|
</div>
|
</template>
|