feat: 拖拽布局-画布归为组件一样处理

flex-api
lichaojun 1 month ago
parent 0defd00fd0
commit 6c58570853

@ -357,3 +357,7 @@ form {
.hidden {
display: none;
}
.layui-anim {
box-sizing: unset;
}

@ -9,18 +9,20 @@
<title>网页布局生成</title>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
href="./layui/css/layui.css"
/>
<link
rel="stylesheet"
href="css/index.css"
href="./css/gridstack.min.css"
/>
<link
rel="stylesheet"
href="./css/gridstack.min.css"
href="./css/index.css"
/>
<script src="./js/gridstack-all.js"></script>
<script src="./js/jquery.min.js"></script>
<script src="./layui/layui.js"></script>
<script src="./js/color.js"></script>
</head>
<body>
<div id="app">
@ -66,8 +68,18 @@
<!-- 画布容器 -->
<div id="canvas-panel">
<div id="canvas-tabs">
<button class="tab-button active" data-tab="welcome-screen">欢迎页</button>
<button class="tab-button" data-tab="main-screen">首页</button>
<button
class="tab-button active"
data-tab="welcome-screen"
>
欢迎页
</button>
<button
class="tab-button"
data-tab="main-screen"
>
首页
</button>
</div>
<!-- <div class="global-save-button-container">
<button class="global-save-button">保存布局</button>
@ -115,10 +127,11 @@
</div>
<div class="form-item">
<label for="background">背景颜色</label>
<input
<!-- <input
type="text"
id="background"
/>
/> -->
<div id="background"></div>
</div>
<div class="form-item">
<label for="fontSize">文字大小</label>

@ -0,0 +1,134 @@
/**
* 将RGBA颜色值转换为HEX格式支持透明度
* @param {string} rgbaStr - RGBA颜色字符串 "rgba(255, 0, 0, 0.5)"
* @returns {string} HEX颜色字符串 "#ff000080"
*/
function rgbaToHex(rgbaStr) {
// 验证输入格式
if (!rgbaStr || typeof rgbaStr !== 'string') {
return '';
}
// 首先检查是否已经是HEX格式
if (isHexColor(rgbaStr)) {
return rgbaStr;
}
// 提取RGBA值
const rgbaMatch = rgbaStr.match(/^rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*([\d.]+))?\s*\)$/i);
if (!rgbaMatch) {
throw new Error('无效的颜色格式请使用RGBA或HEX格式');
}
// 解析RGBA值
let r = parseInt(rgbaMatch[1], 10);
let g = parseInt(rgbaMatch[2], 10);
let b = parseInt(rgbaMatch[3], 10);
let a = rgbaMatch[4] ? parseFloat(rgbaMatch[4]) : 1;
// 验证取值范围
if ([r, g, b].some((value) => value < 0 || value > 255)) {
throw new Error('RGB值必须在0-255范围内');
}
if (a < 0 || a > 1) {
throw new Error('透明度值必须在0-1范围内');
}
// 将透明度转换为0-255的整数
const alphaInt = Math.round(a * 255);
// 辅助函数:将十进制转换为两位十六进制
const toHex = (num) => {
const hex = num.toString(16);
return hex.length === 1 ? '0' + hex : hex;
};
// 转换为HEX格式
const hexColor = `#${toHex(r)}${toHex(g)}${toHex(b)}`;
// 如果有透明度且不等于1则添加透明度值
if (a !== 1) {
return hexColor + toHex(alphaInt);
}
return hexColor;
}
/**
* 检查字符串是否为有效的HEX颜色格式
* @param {string} str - 颜色字符串
* @returns {boolean} 是否为HEX格式
*/
function isHexColor(str) {
// 移除可能存在的空格
const cleanStr = str.trim();
// HEX颜色正则表达式支持以下格式
// #rgb, #rgba, #rrggbb, #rrggbbaa
const hexRegex = /^#([0-9A-F]{3,4}|[0-9A-F]{6}|[0-9A-F]{8})$/i;
return hexRegex.test(cleanStr);
}
// 或者将isHexColor作为内部函数如果只需要在当前函数中使用
function rgbaToHexWithValidation(rgbaStr) {
// 验证输入格式
if (!rgbaStr || typeof rgbaStr !== 'string') {
throw new Error('请输入有效的颜色字符串');
}
// 检查是否已经是HEX格式的内部函数
const isHexColor = (str) => {
const cleanStr = str.trim();
const hexRegex = /^#([0-9A-F]{3,4}|[0-9A-F]{6}|[0-9A-F]{8})$/i;
return hexRegex.test(cleanStr);
};
// 首先检查是否已经是HEX格式
if (isHexColor(rgbaStr)) {
return rgbaStr;
}
// 提取RGBA值
const rgbaMatch = rgbaStr.match(/^rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*([\d.]+))?\s*\)$/i);
if (!rgbaMatch) {
throw new Error(`无效的颜色格式: "${rgbaStr}"请使用RGBA(如 rgba(255,0,0,0.5)) 或 HEX(如 #ff0000) 格式`);
}
// 解析RGBA值
let r = parseInt(rgbaMatch[1], 10);
let g = parseInt(rgbaMatch[2], 10);
let b = parseInt(rgbaMatch[3], 10);
let a = rgbaMatch[4] ? parseFloat(rgbaMatch[4]) : 1;
// 验证取值范围
if ([r, g, b].some((value) => value < 0 || value > 255)) {
throw new Error('RGB值必须在0-255范围内');
}
if (a < 0 || a > 1) {
throw new Error('透明度值必须在0-1范围内');
}
// 将透明度转换为0-255的整数
const alphaInt = Math.round(a * 255);
// 辅助函数:将十进制转换为两位十六进制
const toHex = (num) => {
const hex = num.toString(16);
return hex.length === 1 ? '0' + hex : hex;
};
// 转换为HEX格式
const hexColor = `#${toHex(r)}${toHex(g)}${toHex(b)}`;
// 如果有透明度且不等于1则添加透明度值
if (a !== 1) {
return hexColor + toHex(alphaInt);
}
return hexColor;
}

@ -67,12 +67,17 @@
return timestamp + randomNum;
};
// 判断是画布还是组件
var isScreen = function (type) {
return type === 'screen';
};
/** 初始化 */
var init = function (type, self) {
// debugger;
const initDataStorage = localStorage.getItem(`${type}Data`);
if (!initDataStorage) {
self['initData'] = {
type: 'screen',
id: `grid_${generateUniqueId()}`,
children: [],
version: '1.0.0',
@ -85,6 +90,8 @@
self['initData'] = JSON.parse(initDataStorage);
conputedInitData('init', self);
}
self['initData'].el = document.getElementById(`${type}-screen`);
console.log(self['initData'], `${type}Data`);
// 初始化时应用画布背景
@ -114,7 +121,7 @@
},
`#${type}-screen`
);
console.log(self['grid'], `${type}.grid实例`);
// console.log(self['grid'], `${type}.grid实例`);
// grid.load(initData.children);
@ -143,25 +150,17 @@
component.id = `${component.type}_${generateUniqueId()}`;
}
setComponentView(component);
component.el.addEventListener('click', () => {
console.log('点击组件', component);
component.el.addEventListener('click', (e) => {
e.stopPropagation();
console.log(isScreen(component.type) ? `点击画布` : `点击组件`, component);
if (currentComponent && currentComponent.id === component.id) {
return;
}
// 1. 解绑可能存在的画布属性事件处理器
$('#props-panel form').find('#image, #background').off();
// 2. 重新绑定通用的组件事件处理器
bindComponentEvents();
// 3. 重置属性面板标题并显示所有表单项,为显示组件属性做准备
$('#props-panel .panel-title span').text('组件属性');
$('#props-panel form .form-item').show();
// 清除之前选中组件的获取焦点后的样式
clearOldFocusStyle();
currentComponent = component;
// 设置当前选中组件的获取焦点后的样式
setCurrentFocusStyle();
// 右侧显示组件属性列表
if (!$('#props-panel').find('form').is(':visible')) {
$('#props-panel').find('.wait-box').hide();
@ -169,13 +168,19 @@
}
// 将当前选中组件的属性显示在右侧列表中
setCurrentComponentProps(currentComponent);
// 设置上下左右移动组件ID
setMoveComponentId(currentComponent, self);
// 非画布时设置
if (!isScreen(component.type)) {
// 设置当前选中组件的获取焦点后的样式
setCurrentFocusStyle();
// 设置上下左右移动组件ID
setMoveComponentId(currentComponent, self);
}
});
};
var clearOldFocusStyle = function () {
if (currentComponent) {
if (currentComponent && !isScreen(currentComponent.type)) {
let el = $(currentComponent.el).find('.grid-stack-item-content');
el.css('background', '');
if (currentComponent.image) {
@ -191,7 +196,7 @@
};
var setCurrentFocusStyle = function () {
if (currentComponent) {
if (currentComponent && !isScreen(currentComponent.type)) {
let el = $(currentComponent.el).find('.grid-stack-item-content');
if (currentComponent.focusedStyle_background) {
el.css('background', currentComponent.focusedStyle_background);
@ -238,16 +243,23 @@
var handleRemoveComponent = (component) => {
if (currentComponent && currentComponent.id === component.id) {
currentComponent = null;
// 右侧显示请选择组件
if (!$('#props-panel').find('wait-box').is(':visible')) {
$('#props-panel').find('.wait-box').show();
$('#props-panel').find('form').hide();
}
hidePropsPanel();
}
};
var hidePropsPanel = () => {
// 右侧显示请选择组件
if (!$('#props-panel').find('wait-box').is(':visible')) {
$('#props-panel').find('.wait-box').show();
$('#props-panel').find('form').hide();
}
};
//将当前选中组件的属性显示在右侧列表中
var setCurrentComponentProps = function (component) {
// 设置标题
$('#props-panel .panel-title span').text(isScreen(component.type) ? '画布属性' : '组件属性');
const form = $('#props-panel').find('form');
if (component.hasOwnProperty('childrenType')) {
form.find('#childrenType').val(component.childrenType);
@ -256,7 +268,11 @@
form.find('#childrenType').parent().hide();
}
form.find('#name').val(component.name);
if (!isScreen(component.type)) {
form.find('#name').val(component.name);
} else {
form.find('#name').parent().hide();
}
if (component.hasOwnProperty('image')) {
form.find('#image').val(component.image);
@ -266,7 +282,36 @@
}
if (component.hasOwnProperty('background')) {
form.find('#background').val(component.background);
let options = {
elem: '#background',
alpha: true,
format: 'rgb',
done: function (color) {
const el = $(component.el);
let cptColor = rgbaToHex(color);
component.background = cptColor;
if (isScreen(component.type)) {
component.image = '';
}
if (!cptColor) {
if (isScreen(component.type)) {
el.css('background', 'none');
} else {
el.find('.grid-stack-item-content').css('background', 'none');
}
} else {
if (isScreen(component.type)) {
el.css('background', color);
} else {
el.find('.grid-stack-item-content').css('background', color);
}
}
},
};
if (component.background) {
options.color = component.background;
}
layui.colorpicker.render(options);
form.find('#background').parent().show();
} else {
form.find('#background').parent().hide();
@ -288,33 +333,96 @@
form.find('#fontWeight').parent().hide();
}
form.find('#eventsType').val(component.eventsType);
form.find('#eventsAction').val(component.eventsAction);
form.find('#defaultFocus').val(component.defaultFocus);
form.find('#leftId').val(component.leftId);
form.find('#rightId').val(component.rightId);
form.find('#upId').val(component.upId);
form.find('#downId').val(component.downId);
form.find('#focusedStyle_background').val(component.focusedStyle_background);
form.find('#focusedStyle_border_width').val(component.focusedStyle_border_width);
form.find('#focusedStyle_border_color').val(component.focusedStyle_border_color);
form.find('#focusedStyle_scale').val(component.focusedStyle_scale);
if (!isScreen(component.type)) {
form.find('#eventsType').val(component.eventsType);
form.find('#eventsAction').val(component.eventsAction);
form.find('#defaultFocus').val(component.defaultFocus);
form.find('#leftId').val(component.leftId);
form.find('#rightId').val(component.rightId);
form.find('#upId').val(component.upId);
form.find('#downId').val(component.downId);
form.find('#focusedStyle_background').val(component.focusedStyle_background);
form.find('#focusedStyle_border_width').val(component.focusedStyle_border_width);
form.find('#focusedStyle_border_color').val(component.focusedStyle_border_color);
form.find('#focusedStyle_scale').val(component.focusedStyle_scale);
form.find('#eventsType').parent().show();
form.find('#eventsAction').parent().show();
form.find('#defaultFocus').parent().show();
form.find('#leftId').parent().show();
form.find('#rightId').parent().show();
form.find('#upId').parent().show();
form.find('#downId').parent().show();
form.find('#focusedStyle_background').parent().show();
form.find('#focusedStyle_border_width').parent().show();
form.find('#focusedStyle_border_color').parent().show();
form.find('#focusedStyle_scale').parent().show();
} else {
form.find('#eventsType').parent().hide();
form.find('#eventsAction').parent().hide();
form.find('#defaultFocus').parent().hide();
form.find('#leftId').parent().hide();
form.find('#rightId').parent().hide();
form.find('#upId').parent().hide();
form.find('#downId').parent().hide();
form.find('#focusedStyle_background').parent().hide();
form.find('#focusedStyle_border_width').parent().hide();
form.find('#focusedStyle_border_color').parent().hide();
form.find('#focusedStyle_scale').parent().hide();
}
};
// 定义组件设置配置策略
const componentStrategies = {
background: function (el, value) {
if (!value) {
el.find('.grid-stack-item-content').css('background', 'none');
} else {
el.find('.grid-stack-item-content').css('background', value);
background: function (el, value, component) {
let options = {
elem: '#background',
alpha: true,
format: 'rgb',
done: function (color) {
const el = $(component.el);
let cptColor = rgbaToHex(color);
component.background = cptColor;
if (isScreen(component.type)) {
component.image = '';
}
if (!cptColor) {
if (isScreen(component.type)) {
el.css('background', 'none');
} else {
el.find('.grid-stack-item-content').css('background', 'none');
}
} else {
if (isScreen(component.type)) {
el.css('background', color);
} else {
el.find('.grid-stack-item-content').css('background', color);
}
}
},
};
if (component.background) {
options.color = component.background;
}
layui.colorpicker.render(options);
if (!isScreen(component.type)) {
if (!value) {
el.find('.grid-stack-item-content').css('background', 'none');
} else {
el.find('.grid-stack-item-content').css('background', value);
}
}
},
image: function (el, value, component) {
if (!value) return;
// el.find('.grid-stack-item-content').css('background', `url(${value}) no-repeat center center`).css('background-size', 'cover');
el.find('img').attr('src', value);
if (isScreen(component.type)) {
el.css('background', `url(${value}) no-repeat center center`).css('background-size', 'cover');
component.background = '';
} else {
el.find('img').attr('src', value);
}
},
fontSize: function (el, value) {
if (!value) return;
@ -338,19 +446,19 @@
el.find('.grid-stack-item-content').css('background', value);
}
},
focusedStyle_border_width: function (el, value) {
focusedStyle_border_width: function (el, value, component) {
if (!value) return;
if (currentComponent && currentComponent.id === component.id) {
el.find('.grid-stack-item-content').css('border-width', value + 'px');
}
},
focusedStyle_border_color: function (el, value) {
focusedStyle_border_color: function (el, value, component) {
if (!value) return;
if (currentComponent && currentComponent.id === component.id) {
el.find('.grid-stack-item-content').css('border-color', value);
}
},
focusedStyle_scale: function (el, value) {
focusedStyle_scale: function (el, value, component) {
if (!value) return;
if (currentComponent && currentComponent.id === component.id) {
el.find('.grid-stack-item-content').css('transform', `scale(${value})`);
@ -432,13 +540,35 @@
$('.grid-stack')
.not('#' + tabId)
.hide();
// 处理右侧面板的显示/隐藏
hidePropsPanel();
});
};
// 点击保存按钮处理
var handleSaveClick = function () {
$('.save-container').click(function () {
main.initData.children = main.grid.save();
welcome.initData.children = welcome.grid.save();
console.log(main.initData);
console.log(welcome.initData);
localStorage.setItem('mainData', JSON.stringify(conputedInitData('save', main)));
localStorage.setItem('welcomeData', JSON.stringify(conputedInitData('save', welcome)));
});
};
// 点击画布处理
var handleCanvasClick = function () {
handleAddComponent(welcome.initData, welcome);
handleAddComponent(main.initData, main);
};
// 保存的时候计算x,y,w,h
var conputedInitData = function (type, self) {
if (type === 'save') {
let initDataCopy = JSON.parse(JSON.stringify(self['initData']));
const { el, ...otherInfo } = self['initData'];
let initDataCopy = JSON.parse(JSON.stringify(otherInfo));
initDataCopy.children.forEach((item) => {
item.xCopy = item.x;
item.x = item.x * (1920 / 12);
@ -469,88 +599,15 @@
/** 执行方法 */
$(function () {
init('main', main);
init('welcome', welcome);
init('main', main);
// 调用绑定
bindComponentEvents();
// 处理Tab切换
handleTabSwitch();
$('.save-container').click(function () {
main.initData.children = main.grid.save();
welcome.initData.children = welcome.grid.save();
console.log(main.initData);
console.log(welcome.initData);
localStorage.setItem('mainData', JSON.stringify(conputedInitData('save', main)));
localStorage.setItem('welcomeData', JSON.stringify(conputedInitData('save', welcome)));
});
// 给画布添加点击事件,用于编辑画布属性
$('#main-screen, #welcome-screen').on('click', function (e) {
// 确保点击的是画布背景,而不是某个组件
if (e.target !== this) {
return;
}
console.log('点击了画布背景,编辑画布属性');
// 1. 如果有,则取消当前选中的组件
if (currentComponent) {
clearOldFocusStyle();
currentComponent = null;
}
// 2. 更新右侧面板以显示画布属性
$('#props-panel .panel-title span').text('画布属性');
$('#props-panel .wait-box').hide();
const $form = $('#props-panel form');
$form.show();
// 3. 只显示画布相关的表单项
$form.find('.form-item').hide(); // 首先隐藏所有
$form.find('#image').parent().show(); // 显示背景图片
$form.find('#background').parent().show(); // 显示背景颜色
// 4. 获取当前点击的画布及其数据对象
const canvasId = this.id; // "main-screen" 或 "welcome-screen"
const type = canvasId.split('-')[0]; // "main" 或 "welcome"
const canvasData = type === 'main' ? main : welcome;
// 5. 将当前画布的属性值填充到表单中
$form.find('#image').val(canvasData.initData.image || '');
$form.find('#background').val(canvasData.initData.background || '');
// 6. 为画布属性输入框绑定新的事件
$form.find('#image, #background').off(); // 先解绑旧事件,防止重复绑定
$form.find('#image').on('change', function () {
const imageUrl = $(this).val();
canvasData.initData.image = imageUrl;
if (imageUrl) {
$('#' + canvasId)
.css('background-image', `url(${imageUrl})`)
.css('background-size', 'cover');
// 当设置图片时,清空背景色
$('#' + canvasId).css('background-color', '');
canvasData.initData.background = '';
$form.find('#background').val('');
} else {
// 如果选择的是“请选择”,则只移除背景图片
$('#' + canvasId).css('background-image', 'none');
}
});
$form.find('#background').on('blur', function () {
const color = $(this).val();
canvasData.initData.background = color;
if (color) {
// 当设置颜色时,清空背景图片
$('#' + canvasId).css('background-color', color);
$('#' + canvasId).css('background-image', 'none');
canvasData.initData.image = '';
$form.find('#image').val('');
}
});
});
// 处理保存按钮
handleSaveClick();
// 处理画布点击事件
handleCanvasClick();
});
})();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 326 KiB

File diff suppressed because one or more lines are too long
Loading…
Cancel
Save