JeecgBoot仪表盘版本发布,重磅新功能!支持在线拖拽设计大屏和门户

pull/581/head
zhangdaiscott 2023-06-06 21:14:34 +08:00
parent 9d61822479
commit 7f03885154
12 changed files with 2506 additions and 281 deletions

View File

@ -34,6 +34,7 @@
},
"dependencies": {
"@jeecg/online": "3.4.4-RC",
"@qiaoqiaoyun/drag-free": "^1.0.2",
"@iconify/iconify": "^2.2.1",
"@ant-design/colors": "^6.0.0",
"@ant-design/icons-vue": "^6.1.0",

File diff suppressed because it is too large Load Diff

View File

@ -2,6 +2,8 @@ import type { App } from 'vue';
import { Icon } from './Icon';
import AIcon from '/@/components/jeecg/AIcon.vue';
import { Button, JUploadButton } from './Button';
//敲敲云—仪表盘设计器(拖拽设计)
import DragEngine from '@qiaoqiaoyun/drag-free';
import {
// Need
Button as AntButton,
@ -107,6 +109,7 @@ export function registerGlobComp(app: App) {
.use(InputNumber)
.use(Carousel)
.use(Popconfirm)
.use(DragEngine)
.use(Skeleton)
.use(Cascader)
.use(Rate);

View File

@ -0,0 +1,473 @@
<template>
<div class="p-2">
<div class="bg-white mb-2 p-4">
<BasicForm @register="registerForm" />
</div>
<a-spin :spinning="spinning">
<div class="bg-white p-2">
<slot name="cardTitle"></slot>
<List :grid="{ gutter: 5, xs: 1, sm: 2, md: 3, lg: 3, xl: 4, xxl: grid }" size="small" :data-source="data" :pagination="paginationProp">
<template #header>
<div class="flex justify-start space-x-2">
<slot name="header"></slot>
<Tooltip>
<template #title>
<div class="w-50">每行显示数量</div>
<Slider id="slider" v-bind="sliderProp" v-model:value="grid" @change="sliderChange" />
</template>
<a-button type="primary" style="min-width: 80px">
<TableOutlined />
</a-button>
</Tooltip>
<Tooltip>
<template #title>刷新</template>
<a-button @click="fetch">
<RedoOutlined />
</a-button>
</Tooltip>
</div>
</template>
<template #renderItem="{ item, index }">
<ListItem style="margin-top: 10px">
<Card
class="cardItem"
size="small"
:headStyle="{ textAlign: 'left', fontWeight: '500', background: '#efefef', minHeight: '20px' }"
:bodyStyle="{ display: 'none' }"
>
<template #title>
<em class="aui-tag"><div class="aui-tag-re"></div><div class="aui-tag-ye"></div><div class="aui-tag-bl"></div></em>
<span class="lock-to-right" v-if="item.protectionCode">
<Icon icon="ant-design:lock-filled" :size="15" style="margin: 5px;"/>
</span>
</template>
<!--<template #extra>-->
<!--<Dropdown :trigger="['hover']" :dropMenuList="getDropDownAction(item)" popconfirm>-->
<!--<SettingOutlined />-->
<!--</Dropdown>-->
<!--</template>-->
<template #cover>
<div class="title-div ellipsis">{{ item.name }}</div>
<div class="image-div" @click="handleDesign(item)">
<img :src="getCover(item.coverUrl)" />
<div class="image-mask">
<Icon icon="ant-design:eye-outlined" v-if="params.izTemplate === '1' && !hasAuth()" :size="60" />
<Icon icon="ri:drag-drop-fill" v-else :size="60" />
</div>
</div>
</template>
<template class="ant-card-actions" #actions>
<Tooltip>
<template #title>预览</template>
<EyeOutlined key="view" style="font-size: 20px" @click="handleView(item.id)" />
</Tooltip>
<Dropdown :trigger="['hover']" :dropMenuList="getDropDownAction(item)" popconfirm>
<SettingOutlined key="set" style="font-size: 20px" />
</Dropdown>
</template>
</Card>
</ListItem>
</template>
</List>
</div>
</a-spin>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, ref, unref } from 'vue';
import { EyeOutlined, DeleteOutlined, EditOutlined, RedoOutlined, TableOutlined, EllipsisOutlined, SettingOutlined } from '@ant-design/icons-vue';
import { Dropdown } from '/@/components/Dropdown';
import { List, Card, Image, Tooltip, Slider, Popconfirm } from 'ant-design-vue';
import { BasicForm, useForm } from '/@/components/Form';
import { propTypes } from '/@/utils/propTypes';
import { isFunction } from '/@/utils/is';
import { useSlider } from '/@/components/CardList/src/data';
import { useCopyToClipboard } from '/@/hooks/web/useCopyToClipboard';
import { useMessage } from '/@/hooks/web/useMessage';
import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
import { usePermission } from '/@/hooks/web/usePermission';
const { hasPermission } = usePermission();
const ListItem = List.Item;
const defCover = 'https://jeecgdev.oss-cn-beijing.aliyuncs.com/temp/designCover_1655434422024.png';
const { createMessage } = useMessage();
const { clipboardRef, copiedRef } = useCopyToClipboard();
const spinning = ref<boolean>(false);
const grid = ref(5);
// slider
const sliderProp = computed(() => useSlider(4, 7));
//
const props = defineProps({
// API
params: propTypes.object.def({}),
//api
api: propTypes.func,
searchFormSchema: propTypes.object.def([]),
});
//
const emit = defineEmits(['getMethod', 'delete', 'edit', 'view', 'design', 'add', 'copy']);
//
const data = ref([]);
//
// cover
//pageSize
const height = computed(() => {
return `${200 - grid.value * 8}px`;
});
const width = computed(() => {
let rowNum = grid.value;
let width = rowNum==4?"360px":rowNum==6?"230px":rowNum==7?"190px":"280px";
return width;
});
//
const [registerForm, { validate }] = useForm({
schemas: props.searchFormSchema,
labelWidth: 80,
//
baseColProps: { span: 6 },
actionColOptions: {
xs: 24,
sm: 12,
md: 12,
lg: 8,
xl: 8,
xxl: 6,
style: { textAlign: 'left' },
},
autoSubmitOnEnter: true,
submitFunc: handleSubmit,
resetFunc: handleReset,
});
//
async function handleSubmit() {
const data = await validate();
//
pageNo.value = 1;
await fetch(data);
}
//
async function handleReset() {
await fetch();
}
async function sliderChange(n) {
pageSize.value = n * 3;
const data = await validate();
fetch(data);
}
//
onMounted(() => {
fetch();
emit('getMethod', fetch);
});
async function fetch(p = {}) {
spinning.value = true;
const { api, params } = props;
if (api && isFunction(api)) {
const res = await api({ ...params, pageNo: pageNo.value, pageSize: pageSize.value, ...p });
data.value = res.records;
total.value = res.total;
}
spinning.value = false;
}
//
const pageNo = ref(1);
const pageSize = ref(15);
const total = ref(0);
const paginationProp = ref({
showSizeChanger: false,
showQuickJumper: true,
pageSize,
pageNo,
total,
showTotal: (total) => `${total}`,
onChange: pageChange,
onShowSizeChange: pageSizeChange,
});
async function pageChange(p, pz) {
pageNo.value = p;
pageSize.value = pz;
const data = await validate();
fetch(data);
}
async function pageSizeChange(current, size) {
pageSize.value = size;
const data = await validate();
fetch(data);
}
/**
* 获取封面图
*/
function getCover(url) {
return url ? getFileAccessHttpUrl(url) : defCover;
}
/**
* 下拉操作栏
*/
function getDropDownAction(record) {
//1.
if(props.params.izTemplate === '1'){
let commonAction = [];
//1.1
if(hasAuth()){
commonAction = [
{
text: '编辑',
event: '1',
onClick: handleEdit.bind(null, record),
},
{
text: '复制面板',
event: '2',
onClick: handleCopy.bind(null, record.id),
},{
text: '取消模板',
event: '6',
onClick: handleTemplate.bind(null, record,'0'),
}
];
}else{
///1.2
commonAction = [
{
text: '复制面板',
event: '2',
onClick: handleCopy.bind(null, record.id),
}
];
}
return commonAction;
}else{
//tab
let commonAction = [
{
text: '编辑',
event: '1',
onClick: handleEdit.bind(null, record),
},
{
text: '复制面板',
event: '2',
onClick: handleCopy.bind(null, record.id),
},
{
text: '复制路由',
event: '3',
onClick: handleCopyUrl.bind(null, record.path),
},
];
//
if(hasAuth()){
if(record.izTemplate == '1'){
commonAction.push({
text: '取消模板',
event: '6',
onClick: handleTemplate.bind(null, record,'0'),
})
}else{
commonAction.push({
text: '收藏模板',
event: '5',
onClick: handleTemplate.bind(null, record,'1'),
})
}
}
//
if(!hasPassword(record)){
commonAction.push({
text: '删除',
event: '4',
popConfirm: {
title: '是否确认删除',
confirm: handleDelete.bind(null, record),
}
})
}else {
commonAction.push({
text: '删除',
event: '4',
onClick: handleDelete.bind(null, record),
})
}
return commonAction;
}
}
/**
* 判断是否有模板操作权限
*/
function hasAuth(){
return hasPermission('drag:template:edit')
}
/**
* 是否包含保护码
**/
function hasPassword(record){
return record.protectionCode && record.protectionCode.length > 0;
}
/**
* 复制面板
* @param id
*/
function handleCopy(id) {
emit('copy', id);
}
/**
* 复制路由地址
* @param value
*/
async function handleCopyUrl(value) {
if (value) {
clipboardRef.value = value;
if (unref(copiedRef)) {
createMessage.success('复制成功!');
}
} else {
createMessage.warning('复制失败,请检查路径!');
}
}
async function handleCreate() {
emit('add');
}
async function handleEdit(record) {
emit('edit', record);
}
async function handleDelete(record) {
emit('delete', record);
}
async function handleView(id) {
emit('view', id);
}
async function handleDesign(record) {
if(props.params.izTemplate === '1' && !hasAuth()){
//tab,
emit('view', record.id);
}else{
emit('design', record);
}
}
async function handleTemplate(record,izTemplate) {
emit('template', record.id,izTemplate);
}
</script>
<style scoped lang="less">
.addItem {
height: 250px;
display: flex;
justify-content: center;
align-items: center;
}
.cardItem {
width: v-bind('width');
text-align: center;
border-radius: 3px;
box-shadow: 0 4px 5px rgb(0 0 0 / 20%);
&:hover {
box-shadow: 0 0 20px 0 #616161;
}
.title-div {
height: 30px;
line-height: 30px;
background: #fff;
font-weight: 500;
font-size: 14px;
border-bottom: 1px solid #efefef;
}
.ellipsis {
display:block;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.image-div {
img {
width: 100%;
//object-fit: cover;
height: v-bind('height');
}
&:hover {
.image-mask {
opacity: 1;
cursor: default;
}
}
.image-mask {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
width: 100%;
height: v-bind('height');
bottom: 50px;
background: rgba(101, 101, 101, 0.6);
color: #fff;
opacity: 0;
}
}
.aui-tag {
display: flex;
div {
width: 6px;
height: 6px;
background: #333;
border-radius: 100px;
margin: 0 2px;
}
.aui-tag-re {
background: #ff5a52;
}
.aui-tag-ye {
background: #e6c02a;
}
.aui-tag-bl {
background: #53c22c;
}
}
}
/*右上角锁标记*/
.lock-to-right{
z-index: 0;
color: #fff;
position: absolute;
right: 0;
top: 0;
}
.lock-to-right::after {
content: '';
position: absolute;
top: 0;
right: 0;
border-style: solid;
border-width: 0 50px 40px 0;
border-color: transparent #db2828 transparent transparent;
z-index: -1
}
</style>

View File

@ -0,0 +1,187 @@
<template>
<BasicModal
v-bind="$attrs"
:footer="null"
wrapClassName="drag-design-process-modal"
:style="{ top: '0', padding: '0' }"
@register="registerModal"
:bodyStyle="bodyStyle"
:canFullscreen="false"
:closable="false"
defaultFullscreen
destroyOnClose
>
<div id="dragEngineBox" style="height:100vh;overflow-y: auto">
<DragEngine
v-if="refresh"
:dragData="dragData"
:pageId="pageId"
:token="getToken()"
:tenantId="getTenantId()"
:lowAppId="lowAppId"
:isLowApp="isLowApp"
@save="handleSave"
@close="handleClose"
@scroll="handleScroll"
@openWindow="openWindow"
>
</DragEngine>
</div>
</BasicModal>
<PasswordModal ref="passwordRef" @closed="closeModal"></PasswordModal>
</template>
<script lang="ts" setup>
import { ref, unref, reactive, nextTick,computed } from 'vue';
import { getToken,getTenantId } from '/@/utils/auth';
import { queryById } from '../page.api';
import { BasicModal, useModalInner } from '/src/components/Modal';
import { getCacheByDynKey } from '/@/utils/auth';
import { JDragConfigEnum } from '/@/enums/jeecgEnum';
import PasswordModal from './PasswordModal.vue';
// Emits
const emit = defineEmits(['success', 'register', 'close']);
const bodyStyle = {
padding: '0',
height: window.innerHeight + 'px',
};
//
const props = defineProps({
lowAppId: { type: String },
// 使
isLowApp: { type: Boolean, default: true }
});
//Id
const pageId = ref('');
const title = ref('');
const refresh = ref(true);
const passwordRef = ref();
//
const dragData = ref({
componentData: [],
name: '',
coverUrl: '',
backgroundColor: '',
backgroundImage: '',
designType: 100,
theme: 'default',
style: 'default',
updateCount: null,
});
//
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
setModalProps({ showCancelBtn: false, showOkBtn: false });
refresh.value = false;
pageId.value = data.record.id;
//
checkCode(data.record);
title.value = `页面设计 [${data.record.name}]`;
const res = await queryById({ id: unref(pageId) });
if (res.success) {
dragData.value.name = res.result.name;
dragData.value.coverUrl = res.result.coverUrl;
let template = res.result.template ? JSON.parse(res.result.template) : [];
dragData.value.componentData = template;
dragData.value.backgroundColor = res.result.backgroundColor;
dragData.value.backgroundImage = res.result.backgroundImage;
dragData.value.designType = res.result.designType;
dragData.value.style = res.result.style || 'default';
dragData.value.theme = res.result.theme || 'default';
//
dragData.value.updateCount = res.result.updateCount;
}
setTimeout(() => {
nextTick(() => {
refresh.value = true;
});
}, 300);
});
/**
* 检验保护码
*/
function checkCode(result) {
const password = result.protectionCode;
let passIsExit = getCacheByDynKey(JDragConfigEnum.DRAG_CACHE_PREFIX + unref(pageId));
let hasPassword = password && password.length > 0;
if (hasPassword && !passIsExit) {
passwordRef.value.showModal('design', result);
passwordRef.value.extraMsg = '';
}
}
/**
* 关闭事件
*/
function handleClose() {
closeModal();
emit('success');
emit('close')
}
/**
* 保存布局后的回调事件
* @param data
*/
function handleSave(data) {
//modal
//closeModal()
emit('success');
}
/**
* 新增组件后的滚动事件
* @param data
*/
function handleScroll(scrollHeight) {
let dom = document.getElementById("dragEngineBox");
scrollIntoView(dom,scrollHeight)
}
/**
* 模拟滚动效果
* @param element 滚动元素
* @param scrollHeight 滚动高度
*/
function scrollIntoView(element,scrollHeight) {
//
let scrollTop = element.scrollTop;
// step
const step = () =>{
//
let distance = scrollHeight - scrollTop;
//
scrollTop = scrollTop + distance / 10;
if (Math.abs(distance) < 1) {
element.scrollTo(0, scrollHeight);
} else {
element.scrollTo(0, scrollTop);
setTimeout(step, 20);
}
};
step();
}
/**
* 打开分享
* @param url
*/
function openWindow(url){
window.open(url, '_blank');
}
</script>
<style lang="less">
@import '@qiaoqiaoyun/drag-free/lib/index.css';
.drag-design-process-modal {
.ant-modal-header {
padding: 0 !important;
}
.ant-modal-body > .scrollbar {
padding-top: 0;
}
.jeecg-modal-content > .scroll-container {
padding: 0 !important;
}
}
</style>

View File

@ -0,0 +1,71 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" okText="保存" :title="getTitle" @ok="handleSubmit" :width="700" destroyOnClose>
<BasicForm @register="registerForm" />
<template #appendFooter>
<a-button type="primary" @click="handleSubmit(1)"></a-button>
</template>
</BasicModal>
</template>
<script lang="ts" setup>
import { ref, computed, unref } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { BasicForm, useForm } from '/@/components/Form/index';
import { formSchema } from '../page.data';
import { saveOrUpdate } from '../page.api';
import { removeCacheByDynKey } from '/@/utils/auth';
import { JDragConfigEnum } from '/@/enums/jeecgEnum';
import { encryptByBase64,decodeByBase64 } from '/@/utils/cipher.ts';
// Emits
const emit = defineEmits(['success', 'register']);
const isUpdate = ref(true);
//
const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
schemas: formSchema,
showActionButtonGroup: false,
});
//
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
//
await resetFields();
setModalProps({ confirmLoading: false });
isUpdate.value = !!data?.isUpdate;
if (unref(isUpdate)) {
let obj = {...data.record}
//
if(obj.protectionCode && obj.protectionCode.length>0){
obj.protectionCode = decodeByBase64( obj.protectionCode)
}
//
await setFieldsValue({
...obj,
});
}
});
//
const getTitle = computed(() => (!unref(isUpdate) ? '新增' : '编辑'));
/**
* 表单提交事件
* @param flag 标识0:保存/1:保存并设计
*/
async function handleSubmit(flag = 0) {
try {
const values = await validate();
setModalProps({ confirmLoading: true });
//
if(values.protectionCode){
values.protectionCode = encryptByBase64(values.protectionCode)
}
//
const res = await saveOrUpdate(values, isUpdate.value);
//
values.id && removeCacheByDynKey(JDragConfigEnum.DRAG_CACHE_PREFIX + values.id);
//
closeModal();
//
emit('success', flag === 1 ? res : null);
} finally {
setModalProps({ confirmLoading: false });
}
}
</script>

View File

@ -0,0 +1,105 @@
<!--面板保护密码弹窗-->
<template>
<a-modal
v-model:visible="visible"
okText="确认"
:bodyStyle="{ padding: '24px 0 0 0' }"
:closable="false"
:maskClosable="false"
v-bind="$attrs"
centered
destroyOnClose
>
<a-form ref="formRef" :model="formState" :rules="rules" :label-col="labelCol" :wrapper-col="wrapperCol">
<a-form-item :extra="extraMsg" label="保护码" name="password">
<a-input v-model:value="formState.password" type="password" />
</a-form-item>
</a-form>
<template #footer>
<a-button @click="handleClosed"></a-button>
<a-button key="submit" type="primary" @click="handleOk"></a-button>
</template>
</a-modal>
</template>
<script lang="ts">
import { defineComponent, reactive, ref, toRaw, unref } from 'vue';
import { RuleObject, ValidateErrorEntity } from 'ant-design-vue/es/form/interface';
import { encryptByBase64 } from '/@/utils/cipher.ts';
import { setCacheByDynKey } from '/@/utils/auth';
import { JDragConfigEnum } from '/@/enums/jeecgEnum';
export default defineComponent({
name: 'PasswordModal',
emits: ['success', 'closed'],
setup(props, { emit }) {
//modal
const formRef = ref();
//
const formModal = ref();
//
const extraMsg = ref('面板被保护中,编辑前请先输入保护码');
//
const operatorType = ref('');
//
const visible = ref(false);
//
const formState = reactive({ password: '' });
//modal
function showModal(type,record) {
formModal.value = { ...record };
formState.password = '';
operatorType.value = type;
visible.value = true;
}
//
function handleClosed() {
visible.value = false;
emit('closed');
}
//
let validatePass = async (rule: RuleObject, value: string) => {
if (value === '') {
return Promise.reject('密码不能为空');
} else {
let password = formModal.value.protectionCode;
if (password !== encryptByBase64(value)) {
return Promise.reject('密码不正确');
}
return Promise.resolve();
}
};
//
const rules = { password: [{ validator: validatePass, trigger: 'change' }] };
//
const handleOk = () => {
formRef.value
.validate()
.then(async () => {
let values = toRaw(unref(formModal));
//
emit('success', unref(operatorType),values);
//
let isEdit = unref(extraMsg) && unref(extraMsg).length > 0;
//
!isEdit && setCacheByDynKey(JDragConfigEnum.DRAG_CACHE_PREFIX+values.id, values.protectionCode);
visible.value = false;
})
.catch((error) => {
console.log('error', error);
});
};
return {
visible,
showModal,
handleClosed,
extraMsg,
handleOk,
formRef,
formState,
rules,
labelCol: { span: 4 },
wrapperCol: { span: 14 },
};
},
});
</script>

View File

@ -0,0 +1,74 @@
import { defHttp } from '/@/utils/http/axios';
import { Modal } from 'ant-design-vue';
enum Api {
list = '/drag/page/list',
queryById = '/drag/page/queryById',
queryPageById = '/drag/page/queryPageById',
save = '/drag/page/add',
edit = '/drag/page/edit',
copyPage = '/drag/page/copyPage',
deleteOne = '/drag/page/delete',
deleteBatch = '/drag/page/deleteBatch',
}
/**
*
* @param params
*/
export const list = (params) => defHttp.get({ url: Api.list, params });
/**
* id
* @param params
*/
export const queryById = (params) => defHttp.get({ url: Api.queryById, params }, { isTransformResponse: false });
/**
* id()
* @param params
*/
export const queryPageById = (params) => defHttp.get({ url: Api.queryPageById, params }, { isTransformResponse: false });
/**
*
* @param params
*/
export const saveOrUpdate = (params, isUpdate) => {
let url = isUpdate ? Api.edit : Api.save;
return defHttp.post({ url: url, params });
};
/**
*
*/
export const deleteOne = (params, handleSuccess) => {
return defHttp.delete({ url: Api.deleteOne, params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
};
/**
*
* @param params
*/
export const batchDelete = (params, handleSuccess) => {
Modal.confirm({
title: '确认删除',
content: '是否删除选中数据',
okText: '确认',
cancelText: '取消',
onOk: () => {
return defHttp.delete({ url: Api.deleteBatch, data: params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
},
});
};
/**
*
*/
export const copyPage = (params, handleSuccess) => {
return defHttp.get({ url: Api.copyPage, params }, { isTransformResponse: false }).then(() => {
handleSuccess();
});
};

View File

@ -0,0 +1,66 @@
import { BasicColumn, FormSchema } from '/@/components/Table';
export const columns: BasicColumn[] = [
{
title: '名称',
align: 'center',
dataIndex: 'name',
},
];
export const searchFormSchema: FormSchema[] = [
{
label: '名称',
field: 'name',
component: 'Input',
colProps: { span: 6 },
},
];
export const formSchema: FormSchema[] = [
{
label: '',
field: 'id',
component: 'Input',
show: false,
},
{
label: '名称',
field: 'name',
component: 'Input',
required: true,
},
{
label: '封面图',
field: 'coverUrl',
component: 'JImageUpload',
componentProps: {
fileMax: 1,
},
},
{
label: '分类',
field: 'type',
component: 'Select',
defaultValue: '1',
required: true,
componentProps: {
options: [
{
label: '仪表盘设计',
value: '1',
key: '1',
},
{
label: '门户设计器',
value: '2',
key: '2',
}
]
}
},
{
label: '保护码',
field: 'protectionCode',
component: 'StrengthMeter'
}
];

View File

@ -0,0 +1,176 @@
<template>
<CardList
:searchFormSchema="searchFormSchema"
:params="params"
:api="list"
@getMethod="getMethod"
@delete="handleDelete"
@view="handleView"
@design="handleDesign"
@edit="handleEdit"
@copy="handleCopy"
@template="handleTemplate"
>
<template #header>
<a-button preIcon="ant-design:plus-outlined" type="primary" @click="handleCreate"></a-button>
</template>
<template #cardTitle>
<a-tabs defaultActiveKey="1" @change="tabChange" size="small">
<a-tab-pane key="1">
<template #tab>
<span>
<Icon icon="ant-design:bar-chart-outlined" :size="20"></Icon>
仪表盘设计
</span>
</template>
</a-tab-pane>
<a-tab-pane key="3">
<template #tab>
<span>
<Icon icon="ant-design:star-outlined" :size="20"></Icon>
模板
</span>
</template>
</a-tab-pane>
</a-tabs>
</template>
</CardList>
<!--编辑弹窗-->
<PageModal @register="registerModal" @success="handleOk" />
<!--保护密码弹窗-->
<PasswordModal ref="passwordRef" @success="checkPassOk"/>
<!--页面配置弹窗-->
<DragPageModal @register="registerDragModal" @success="success" :isLowApp="false"/>
</template>
<script lang="ts" setup name="drag-page">
import CardList from './components/CardList.vue';
import PageModal from './components/PageModal.vue';
import PasswordModal from './components/PasswordModal.vue';
import DragPageModal from './components/DragPageModal.vue';
import { ref,reactive } from 'vue';
import { useModal } from '/@/components/Modal';
import { router } from '/@/router';
import { list, deleteOne, copyPage ,saveOrUpdate} from './page.api';
import { searchFormSchema } from './page.data';
const [registerModal, { openModal }] = useModal();
const [registerDragModal, { openModal: openDragModal }] = useModal();
const passwordRef = ref()
let reload = (params?) => {};
//
const params = reactive({type:"1",izTemplate:'0'});
// fetch;
function getMethod(m: any) {
reload = m;
}
/**
* 提交后的回调
*/
function handleOk(record) {
reload();
record && handleDesign(record);
}
/**
* 新增事件
*/
function handleCreate() {
openModal(true, {
isUpdate: false,
});
}
/**
* 编辑
*/
function handleEdit(record: Recordable) {
//
let hasPassword = record.protectionCode && record.protectionCode.length > 0;
if (hasPassword) {
passwordRef.value.showModal('edit', record);
//passwordRef.value.extraMsg = ',';
} else {
openModal(true, {
record,
isUpdate: true,
});
}
}
/**
* 密码校验成功
*/
async function checkPassOk(type, record) {
if (type == 'edit') {
openModal(true, {
record,
isUpdate: true,
});
} else if (type == 'delete') {
await deleteOne({ id: record.id }, reload);
}
}
/**
* 删除事件
*/
async function handleDelete(record) {
//
let hasPassword = record.protectionCode && record.protectionCode.length > 0;
if (hasPassword) {
passwordRef.value.showModal('delete', record);
passwordRef.value.extraMsg = '面板被保护中,删除前请先输入保护码';
} else {
await deleteOne({ id: record.id }, reload);
}
}
/**
* 页面配置
*/
function handleDesign(record) {
openDragModal(true, {
record,
isUpdate: true,
});
}
/**
* 页面预览
*/
function handleView(id) {
router.push({ name: 'drag-page-view-@id'!, params: { id } });
}
/**
* 面板复制
*/
async function handleCopy(id) {
await copyPage({ id }, reload);
}
/**
* 收藏模板
*/
async function handleTemplate(id,template) {
let params = {id, izTemplate:template};
const res = await saveOrUpdate(params,true);
console.log('handleTemplate-------------->res:',res)
reload();
}
//
function tabChange(key) {
if(key!=='3'){
params.type = key;
params.izTemplate = '0';
}else{
params.type = '';
params.izTemplate = '1';
}
reload({pageNo:1});
}
/**
* 成功回调刷新列表
*/
function success() {
reload();
}
</script>

View File

@ -0,0 +1,65 @@
import {ref} from 'vue'
import html2canvas from 'html2canvas';
/**
* image
*/
export function useExportImage() {
const exportRef = ref();
/**
*
* @param fileName
*/
function onExportImage(fileName) {
let ele = exportRef.value;
if(!ele){
console.error('没有导出对象')
return;
}
const size = {
width: ele.offsetWidth,
height: ele.offsetHeight
}
html2canvas(ele, { useCORS: true, logging: true }).then(async (canvas) => {
const dataURL = canvas.toDataURL('image/png');
await download(dataURL, size, fileName);
});
}
async function download(imgUrl, size, fileName) {
const dataUrl = await getBase64(imgUrl, size);
const link:any = document.createElement('a');
link.href = dataUrl;
link.download = `${fileName}.png`;
link.click();
}
function getBase64(url, size){
return new Promise((resolve) => {
let canvas:any = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const img = new Image();
// 允许跨域
img.crossOrigin = 'Anonymous';
img.src = url;
img.onload = () => {
// eslint-disable-next-line prefer-destructuring
canvas.height = size.height;
// eslint-disable-next-line prefer-destructuring
canvas.width = size.width;
ctx!.drawImage(img, 0, 0, size.width, size.height);
const dataURL = canvas.toDataURL('image/png');
canvas = null;
resolve(dataURL);
};
});
}
return {
exportRef,
onExportImage
}
}

View File

@ -0,0 +1,163 @@
<template>
<div ref="exportRef">
<ViewEngine :dragData="dragData" :token="getToken()" @go="compRouter" @btnClick="btnClick"></ViewEngine>
</div>
<DemoModal @register="registerModal"/>
<DesformViewModal @register="registerRecordModal" :showComment="false" :showFiles="false" :showDataLog="false"/>
</template>
<script lang="ts" name="drag-page-view" setup>
import { ref, unref, reactive,computed, watch } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import {queryById} from './page.api';
import { useTabs } from '/@/hooks/web/useTabs';
import { useModal } from '/@/components/Modal';
import { openWindow } from '/@/utils';
import { getToken } from '/@/utils/auth';
import { useUserStore } from '/@/store/modules/user';
import DemoModal from '/@/views/system/examples/demo/DemoModal.vue';
import {router} from "/@/router";
import {useExportImage} from './useExportImage'
const { setTitle } = useTabs();
const userStore = useUserStore();
//id
const currentId: any = ref('');
//
const { currentRoute, push, resolve: pathResolve } = useRouter();
//
const dragData = ref({
name: '',
coverUrl: '',
backgroundColor: '',
theme: 'default',
style: 'default',
designType: 100,
backgroundImage: '',
componentData: [],
});
//
async function initData() {
const { params, path } = unref(currentRoute);
let id = params.id ? params.id : path.substr(path.lastIndexOf('/') + 1);
currentId.value = id;
const res = await queryById({ id });
if (res.success) {
console.info(123, res);
await setTitle(res.result.name);
let template = res.result.template ? JSON.parse(res.result.template) : [];
dragData.value.componentData = template;
dragData.value.name = res.result.name;
dragData.value.coverUrl = res.result.coverUrl;
dragData.value.backgroundColor = res.result.backgroundColor;
dragData.value.backgroundImage = res.result.backgroundImage;
dragData.value.designType = res.result.designType;
dragData.value.theme = res.result.theme || 'default';
dragData.value.style = res.result.style || 'default';
}
}
//
function compRouter(url: any, params: any) {
if (url && url.indexOf('http') > -1) {
openWindow(url);
} else {
push({ path: url, query: params });
}
}
//********************begin*****************************************
const [registerModal, { openModal }] = useModal();
//
const [registerRecordModal, { openModal: openRecordModal }] = useModal();
const route = useRoute();
/**
* 跳转路由页面
* @param path
* @param openMode
*/
function goPage(nextRoute, params) {
if(params.openMode==='2'){
//
let winUrl = pathResolve(nextRoute);
window.open(winUrl.href, '_blank')
}else{
//
push(nextRoute)
}
}
function btnClick(params) {
console.log("btnClick---->params",params);
let operationType = params.operationType;
if(operationType=='1'){
let modalData = {
mode: 'add',
desformCode: params.worksheet.value,
dataId: null,
isOnline: false,
viewId: '',
lowAppId: route.params.appId
}
console.log('创建记录 打开modal的参数', modalData)
openRecordModal(true, modalData);
}else if(operationType=='2'){
let appId = route.params.appId;
let designFormCode = params.worksheet.value;
let nextRoute = {
path: `/myapp/${appId}/desform/${designFormCode}`,
};
if(params.view){
nextRoute['query']={
view: params.view
}
}
console.log('打开视图 路由', nextRoute)
goPage(nextRoute, params)
//update-end-author:taoyan date:2023-2-23 for: QQYUN-3674model
}else if(operationType=='3'){
//update-begin-author:taoyan date:2023-3-1 for: QQYUN-4420
let appId = route.params.appId;
let dragId = params.customPage.value;
let nextRoute = {
path: `/myapp/${appId}/drag/${dragId}`,
};
goPage(nextRoute, params);
//update-end-author:taoyan date:2023-3-1 for: QQYUN-4420
}else if(operationType=='4'){
//
}
}
//********************end*****************************************
//update-begin-author:taoyan date:2023-2-24 for: QQYUN-3663
const emit = defineEmits(['loadOk']);
const props = defineProps({
routeInfo: {
type: Object,
default: ()=>{}
},
});
const { exportRef, onExportImage } = useExportImage();
watch(()=>props.routeInfo, (info)=>{
if(info){
if(info.exportImage){
console.log('导出图片》》》');
let name = dragData.value.name;
onExportImage(name);
}
}
}, {deep: true, immediate: true});
//update-end-author:taoyan date:2023-2-24 for: QQYUN-3663
initData();
</script>
<style lang="less" scoped>
@import '@qiaoqiaoyun/drag-free/lib/index.css';
</style>