Commit e4ce8b08 authored by fanxuehui's avatar fanxuehui

feat: drop&drag

基础拖拽
parent ebe4589e
......@@ -26,7 +26,15 @@ module.exports = {
test: /\.(css|less)$/,
use: [
{ loader: 'style-loader', options: {} },
{ loader: 'css-loader', options: { modules: true, sourceMap: true } },
{
loader: 'css-loader',
options: {
modules: {
localIdentName: '[name]__[local]___[hash:base64:5]',
},
sourceMap: true,
},
},
{ loader: 'postcss-loader', options: { sourceMap: true } },
{
loader: 'less-loader',
......@@ -57,6 +65,8 @@ module.exports = {
'@shared': resolve('../src/jimu-editor/shared'),
'@hooks': resolve('../src/jimu-editor/hooks'),
'@hoc': resolve('../src/jimu-editor/hoc'),
'@config': resolve('../src/jimu-editor/config'),
'@utils': resolve('../src/jimu-editor/utils'),
},
},
externals: {
......
import React from 'react';
import { toJS } from 'mobx';
/*
* todo
* 和数据流解耦
*/
function Canvas({ stageStore }) {
return (
<div>
<p>extended Canvas</p>
{stageStore.data.widgetList.map((wrappedWidget) => (
<wrappedWidget.widget.layer
key={wrappedWidget.id}
setFocus={() => stageStore.setFocus(wrappedWidget.id)}
style={toJS(wrappedWidget.style)}
></wrappedWidget.widget.layer>
))}
</div>
);
}
export default Canvas;
......@@ -2,28 +2,11 @@ import React from 'react';
import { render } from 'react-dom';
import JimuEditor from '../src/jimu-editor/index';
import { observer } from 'mobx-react';
import {toJS} from 'mobx';
import * as Demo from './widgets/demo';
/*
* todo
* 和数据流解耦
*/
function Canvas({ stageStore }) {
return (
<div>
<p>extended Canvas</p>
{stageStore.data.widgetList.map((wrappedWidget) => (
<wrappedWidget.widget.layer
key={wrappedWidget.id}
setFocus={() => stageStore.setFocus(wrappedWidget.id)}
style={toJS(wrappedWidget.style)}
></wrappedWidget.widget.layer>
))}
</div>
);
}
import ExtendCanvas from './extendCanvas';
render(
<JimuEditor controls={[]} canvas={observer(Canvas)} widgets={[Demo]} />,
<JimuEditor controls={[]} canvas={observer(ExtendCanvas)} widgets={[Demo]} />,
document.getElementById('app')
);
......@@ -3,6 +3,7 @@ function Icon({ addSelf }) {
const handleClick = () => {
addSelf();
};
return <button onClick={handleClick}>demo icon</button>;
}
......
module.exports = {
url: {
type: 'text',
name: '表单链接',
value:
'https://shengdianhuadg.com/frontend_service/common?appid=3&funid=27&activity_id=xym&theme=1&from_id=2&channel=86660&activity_id=xym'
},
bg: {
type: 'image',
value: '//yun.duiba.com.cn/h5-mami/webgame/activity/jiugongge/bg02.jpg'
},
box: {
type: 'image',
value: '//yun.duiba.com.cn/h5-mami/webgame/activity/jiugongge/box.png'
},
btn: {
type: 'image',
value: '//yun.duiba.com.cn/h5-mami/webgame/activity/jiugongge/btn.png'
},
bottom: {
type: 'image',
value: '//yun.duiba.com.cn/h5-mami/webgame/activity/jiugongge/bottom.png'
},
item01: {
type: 'image',
value: '//yun.duiba.com.cn/h5-mami/webgame/activity/jiugongge/item01.png'
},
item02: {
type: 'image',
value: '//yun.duiba.com.cn/h5-mami/webgame/activity/jiugongge/item02.png'
},
item03: {
type: 'image',
value: '//yun.duiba.com.cn/h5-mami/webgame/activity/jiugongge/item03.png'
},
item04: {
type: 'image',
value: '//yun.duiba.com.cn/h5-mami/webgame/activity/jiugongge/item04.png'
},
item05: {
type: 'image',
value: '//yun.duiba.com.cn/h5-mami/webgame/activity/jiugongge/item05.png'
},
item06: {
type: 'image',
value: '//yun.duiba.com.cn/h5-mami/webgame/activity/jiugongge/item06.png'
},
item07: {
type: 'image',
value: '//yun.duiba.com.cn/h5-mami/webgame/activity/jiugongge/item07.png'
},
item08: {
type: 'image',
value: '//yun.duiba.com.cn/h5-mami/webgame/activity/jiugongge/item08.png'
}
}
{
"skinName": "jiugongge-40e87f8e85952d6f82d8",
"script": "//yun.tuisnake.com/jimu-web/render/skin/dist/jiugongge-40e87f8e85952d6f82d8.js",
"config": {
"url": {
"type": "text",
"name": "表单链接",
"value": "https://shengdianhuadg.com/frontend_service/common?appid=3&funid=27&activity_id=xym&theme=1&from_id=2&channel=86660&activity_id=xym"
},
"bg": {
"type": "image",
"value": "//yun.duiba.com.cn/h5-mami/webgame/activity/jiugongge/bg02.jpg",
"width": 750,
"height": 1503
},
"box": {
"type": "image",
"value": "//yun.duiba.com.cn/h5-mami/webgame/activity/jiugongge/box.png",
"width": 600,
"height": 600
},
"btn": {
"type": "image",
"value": "//yun.duiba.com.cn/h5-mami/webgame/activity/jiugongge/btn.png",
"width": 606,
"height": 137
},
"bottom": {
"type": "image",
"value": "//yun.duiba.com.cn/h5-mami/webgame/activity/jiugongge/bottom.png",
"width": 567,
"height": 274
},
"item01": {
"type": "image",
"value": "//yun.duiba.com.cn/h5-mami/webgame/activity/jiugongge/item01.png",
"width": 178,
"height": 179
},
"item02": {
"type": "image",
"value": "//yun.duiba.com.cn/h5-mami/webgame/activity/jiugongge/item02.png",
"width": 178,
"height": 179
},
"item03": {
"type": "image",
"value": "//yun.duiba.com.cn/h5-mami/webgame/activity/jiugongge/item03.png",
"width": 178,
"height": 179
},
"item04": {
"type": "image",
"value": "//yun.duiba.com.cn/h5-mami/webgame/activity/jiugongge/item04.png",
"width": 178,
"height": 179
},
"item05": {
"type": "image",
"value": "//yun.duiba.com.cn/h5-mami/webgame/activity/jiugongge/item05.png",
"width": 178,
"height": 179
},
"item06": {
"type": "image",
"value": "//yun.duiba.com.cn/h5-mami/webgame/activity/jiugongge/item06.png",
"width": 178,
"height": 179
},
"item07": {
"type": "image",
"value": "//yun.duiba.com.cn/h5-mami/webgame/activity/jiugongge/item07.png",
"width": 178,
"height": 179
},
"item08": {
"type": "image",
"value": "//yun.duiba.com.cn/h5-mami/webgame/activity/jiugongge/item08.png",
"width": 178,
"height": 179
}
}
}
@import '../../styles/mixin.less';
.bgi() {
background-repeat: no-repeat;
background-size: 100% 100%;
}
.lottery {
.wd(750px, 1503px);
.bgi();
}
.box {
.wd(600px, 600px);
.bgi();
.ps(240px, 75px);
.item {
.wd(178px, 178px);
.bgi();
position: absolute;
opacity: 0.5;
&.checked {
opacity: 1;
}
&:nth-child(1) {
left: 36px;
top: 36px;
}
&:nth-child(2) {
left: 210px;
top: 36px;
}
&:nth-child(3) {
left: 386px;
top: 36px;
}
&:nth-child(8) {
left: 36px;
top: 214px;
}
&:nth-child(4) {
left: 386px;
top: 214px;
}
&:nth-child(7) {
left: 36px;
top: 392px;
}
&:nth-child(6) {
left: 210px;
top: 392px;
}
&:nth-child(5) {
left: 386px;
top: 392px;
}
}
}
.phone_input {
.wd(600px, 100px);
.ps(880px, 75px);
border-radius: 20px;
font-size: 30px;
&::placeholder {
text-align: center;
font-size: 30px;
}
}
.start_button {
.wd(606px, 137px);
.bgi();
.ps(890px, 80px);
}
.prize {
position: fixed;
top: 0px;
left: 0;
z-index: 11;
transition: all .5s;
transform: scale(.1);
visibility: hidden;
&.show {
opacity: 1;
visibility: visible;
transform: scale(1);
}
.mask {
.wd(750px, 100vh);
.bgi();
position: absolute;
background-color: rgba(0, 0, 0, 0.9);
}
.prize_body {
position: absolute;
z-index: 11;
}
.card {
.wd(649px, 455px);
.bgi();
position: absolute;
top: 120px;
left: 50px;
}
.btn {
.wd(497px, 193px);
.bgi();
.ps(640px, 130px);
}
.bottom {
.wd(567px, 274px);
.bgi();
.ps(850px, 100px);
}
}
import React, { Component } from 'react';
import cn from 'classnames';
// import toast from '../../libs/toast'
import { createBgStyles } from './utils';
import styles from './index.less';
class App extends Component<{ config: any }> {
state = {
start: false,
delay: 200,
slowDown: 10,
checkedIndex: 1,
};
start() {
const { delay, slowDown, checkedIndex } = this.state;
setTimeout(() => {
if (delay > 300 && checkedIndex === 5) {
console.log('jump');
}
if (checkedIndex > 7) {
this.setState({ checkedIndex: 1 });
} else {
this.setState({ checkedIndex: checkedIndex + 1 });
}
if (slowDown < 0) {
this.setState({ delay: delay + 30 });
} else {
this.setState({ delay: delay < 80 ? 80 : delay - 20 });
}
console.log(delay, slowDown);
if (delay <= 80) this.setState({ slowDown: slowDown - 1 });
this.start();
}, delay);
}
render() {
const { config } = this.props;
const { start, checkedIndex } = this.state;
const bgStyle = createBgStyles(config);
return (
<div>
<div className={styles.lottery} style={bgStyle.bg}>
<div className={styles.box} style={bgStyle.box}>
{new Array(9).fill(1).map((v, index) => (
<div
className={cn({
[styles.item]: true,
[styles.checked]: !start || checkedIndex === index + 1,
})}
key={index}
style={bgStyle[`item0${index + 1}`]}
/>
))}
</div>
<div
className={styles.start_button}
style={bgStyle.btn}
onClick={() => {
this.setState({ start: true });
this.start();
}}
/>
</div>
</div>
);
}
}
export default App;
/**
* 产生背景图样式
* @param {Object} config 背景图配置项
*/
export function createBgStyles(config): { [key: string]: any } {
const styles = {};
for (let i in config) {
if (config[i].type === 'image') {
styles[i] = { backgroundImage: `url(${config[i].value})` };
}
}
return styles;
}
import React from 'react';
import { useDrop } from 'react-dnd';
import { useStore } from '@hooks/use-store';
import DefaultCanvas from './default-canvas';
import { CanvasWrapperUniqueId } from '@config';
import styles from './index.less';
function CanvasHost() {
const { scopeStore, stageStore } = useStore();
const { canvas } = scopeStore;
const [collectedProps, drop] = useDrop({
accept: ['demo'],
drop(item, monitor) {
const DropContainerRect = document
.getElementById(CanvasWrapperUniqueId)
.getBoundingClientRect();
// 拖拽end时pointer坐标
const pointerCoords = monitor.getClientOffset();
const coordRelScene = {
x: pointerCoords.x - DropContainerRect.x,
y: pointerCoords.y - DropContainerRect.y,
};
return {
coordRelScene,
};
},
});
const CumCanvas = canvas ? canvas : DefaultCanvas;
return (
<div
className={styles.canvas_wrapper}
ref={drop}
id={CanvasWrapperUniqueId}
>
<CumCanvas stageStore={stageStore}></CumCanvas>
</div>
);
}
export default CanvasHost;
......@@ -4,26 +4,20 @@ import React from 'react';
import { useStore } from '@hooks/use-store';
import { observer } from 'mobx-react';
import withCardItem from '@hoc/card';
import { uuidGen } from '@utils/uuid';
import styles from './index.less';
let id = 0;
function IconHost() {
const { scopeStore, stageStore } = useStore();
const { widgets } = scopeStore;
return (
<div className={styles.icon_host}>
{widgets.map((widget, i) => (
<widget.icon
key={i}
addSelf={() => {
stageStore.addWidget({
id: id++,
style:{},
widget: widget,
});
}}
></widget.icon>
))}
{widgets.map((widget, i) => {
const WrappedCard = withCardItem(widget, 'demo');
return <WrappedCard key={i}></WrappedCard>;
})}
</div>
);
}
......
......@@ -5,6 +5,3 @@
position: relative;
text-align: center;
}
.canvas_wrapper {
flex-grow: 1;
}
......@@ -2,22 +2,19 @@ import React, { ComponentType } from 'react';
import { observer } from 'mobx-react';
import { useStore } from '../../hooks/use-store';
import styles from './index.less';
import Canvas from '../canvas';
import CanvasHost from '../canvas-host';
import EditorHost from '../editor-host';
import ExtendedConcrols from '../extended-controls';
import IconHost from '../icon-host';
import CardHost from '../card-host';
function Stage() {
const { scopeStore, stageStore } = useStore();
const { canvas } = scopeStore;
const handleClick = () => {};
const CumCanvas = canvas ? canvas : Canvas;
return (
<div onClick={handleClick} className={styles.stage}>
<IconHost></IconHost>
<div className={styles.canvas_wrapper}>
<CumCanvas stageStore={stageStore}></CumCanvas>
</div>
<CardHost></CardHost>
<CanvasHost></CanvasHost>
<ExtendedConcrols></ExtendedConcrols>
<EditorHost></EditorHost>
</div>
......
export const CanvasWrapperUniqueId = 'jimu____canvas____id';
import React from 'react';
import { useDrag } from 'react-dnd';
import { uuidGen } from '@utils/uuid';
import { getNumber } from '@utils/helper';
import { useStore } from '@hooks/use-store';
import { CanvasWrapperUniqueId } from '@config';
import { IWidget } from '@shared/interfaces';
function withCardItem(
Widget: IWidget,
type,
defaultAttrs = { style: { width: '100px' } }
) {
function wrapped() {
const { scopeStore, stageStore } = useStore();
const [collectedProps, drag] = useDrag({
item: { id: '', type },
begin(monitor) {
return {
id: uuidGen(),
type,
};
},
end(item, monitor) {
if (!monitor.didDrop()) {
// 未拖拽至舞台中
return false;
} else {
// 获取场景inner的Rect
const SceneInnerRect = document
.getElementById(CanvasWrapperUniqueId)
.getBoundingClientRect();
let attrs = { style: { width: '100px', height: '100px' }, id: '' };
// 拖拽至舞台中
// merge拖拽结果与默认配置融合后存入store
// width, height, left, top属性%转px
Object.keys(attrs.style)
.filter(
(key) => ['width', 'height', 'left', 'top'].indexOf(key) !== 1
)
.forEach((key) => {
if (
typeof attrs.style[key] === 'string' &&
attrs.style[key].indexOf('%') !== -1
) {
switch (key) {
case 'width':
case 'left':
attrs.style[key] =
(getNumber(SceneInnerRect.width) *
getNumber(attrs.style[key])) /
100 +
'px';
break;
case 'height':
case 'top':
attrs.style[key] =
(getNumber(SceneInnerRect.height) *
getNumber(attrs.style[key])) /
100 +
'px';
break;
default:
break;
}
}
});
// 此处坐标为鼠标落点相对于舞台的坐标
attrs.style = Object.assign({}, attrs.style, {
left:
monitor.getDropResult().coordRelScene.x -
getNumber(attrs.style.width) / 2 +
'px',
top:
monitor.getDropResult().coordRelScene.y -
getNumber(attrs.style.height) / 2 +
'px',
});
const { id } = monitor.getItem();
attrs.id = id;
// 将部件添加至场景
stageStore.addWidget({
id,
...defaultAttrs,
...attrs,
widget: Widget,
});
}
},
});
return (
<div ref={drag}>
<Widget.icon
addSelf={(attrs) => {
stageStore.addWidget({
id: uuidGen(),
...defaultAttrs,
...attrs,
widget: Widget,
});
}}
></Widget.icon>
</div>
);
}
return wrapped;
}
export default withCardItem;
import React from 'react';
// https://github.com/mobxjs/mobx-react-lite/#observer-batching
import 'mobx-react-lite/batchingForReactDom';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { JimuEditorProps } from './shared/interfaces';
import withCardItem from '@hoc/card';
import App from './app';
import { StoreProvider } from './store';
function JimuEditor(props: JimuEditorProps) {
return (
<StoreProvider>
<App {...props}></App>
<DndProvider backend={HTML5Backend}>
<App {...props}></App>
</DndProvider>
</StoreProvider>
);
}
......@@ -16,3 +20,4 @@ function JimuEditor(props: JimuEditorProps) {
export { JimuEditorProps, WidgetDataTypes } from './shared/interfaces';
export default JimuEditor;
export { withCardItem };
import {
CSSProperties,
ComponentType,
ReactElement,
ComponentProps,
} from 'react';
import { CSSProperties, ComponentType } from 'react';
// 积木编辑器props
export interface JimuEditorProps {
......@@ -12,7 +7,7 @@ export interface JimuEditorProps {
widgets?: IWidget[];
}
interface ICanvasProps {
stageStore:object;
stageStore: object;
}
// 控件类型
export interface IControls {
......@@ -30,19 +25,8 @@ export enum IControlsTypes {
}
// 组件物料类型
export interface IWrappedWidget {
id: string;
style: CSSProperties;
actionAttrs: {
actionHide: IAction;
actionShow: IAction;
[propName: string]: IAction;
};
actions: IAction[];
eventAttrs: {
[propName: string]: IEvent;
};
events: IEvent[];
export interface IWrappedWidget extends IWidgetAttrs {
widget: IWidget;
}
export interface IWidget {
editor: ComponentType<WidgetEditorProps>;
......@@ -50,11 +34,19 @@ export interface IWidget {
icon: ComponentType<WidgetIconProps>;
meta: IMeta;
}
export interface WrappedWidget {
type: string;
style: CSSProperties;
export interface IWidgetAttrs {
id: string;
widget: IWidget;
style: CSSProperties;
actionAttrs?: {
actionHide: IAction;
actionShow: IAction;
[propName: string]: IAction;
};
actions?: IAction[];
eventAttrs?: {
[propName: string]: IEvent;
};
events?: IEvent[];
}
export interface IMeta {
script: string;
......@@ -84,11 +76,11 @@ export interface IEvent {
pub?: string;
}
export interface WidgetEditorProps {
target: WrappedWidget;
changeTargetProps: (style:CSSProperties) => void;
target: IWrappedWidget;
changeTargetProps: (style: CSSProperties) => void;
}
interface WidgetIconProps {
addSelf: () => void;
addSelf: (attrs: IWidgetAttrs) => void;
}
interface WidgetLayerProps {
setFocus: () => void;
......@@ -97,7 +89,7 @@ interface WidgetLayerProps {
// 产出JSON格式
export interface IPage {
attr: IPageAttr;
widgetList: WrappedWidget[];
widgetList: IWrappedWidget[];
}
export interface IPageAttr {
style: CSSProperties;
......
// 编辑器扩展和外部参数管理
// 编辑器扩展和外部参数
// 物料市场配置
import { observable, action, computed } from 'mobx';
import { ComponentType, Component } from 'react';
import { IControls, IWidget } from '../shared/interfaces';
......
// 舞台数据
import { observable, action, computed } from 'mobx';
import { IPage, IWidget } from '../shared/interfaces';
import { IPage, IWidget, IWrappedWidget } from '../shared/interfaces';
import { CSSProperties } from 'react';
export class StageStore {
@observable
......@@ -12,6 +12,8 @@ export class StageStore {
},
widgetList: [],
};
// 当前编辑的组件
@observable
targetWidgetId: string = '';
@computed
......@@ -21,6 +23,7 @@ export class StageStore {
);
}
// 修改当前组件属性
@action
changeTargetProps = (style: CSSProperties) => {
this.data.widgetList.find(
......@@ -30,12 +33,14 @@ export class StageStore {
this.data.widgetList.find((widget) => widget.id === this.targetWidgetId)
);
};
// 添加组件
@action
addWidget(wrappedWidget) {
addWidget(wrappedWidget: IWrappedWidget): void {
this.data.widgetList.push(wrappedWidget);
}
// 设置焦点
@action
setFocus(id) {
setFocus(id: string): void {
this.targetWidgetId = id;
}
}
// 获取字符串中连续的数字串
export const getNumber = function getNumber(str) {
if (typeof str === 'string') {
const arr = str.match(/[-|0-9][0-9]*/);
return arr && arr[0] ? parseInt(arr[0], 10) : 0;
} else {
return str;
}
};
import { v4 as uuidv4 } from 'uuid';
const defaultOptions = {};
export const uuidGen = (opt?: object) => uuidv4({ ...defaultOptions, ...opt });
......@@ -15,9 +15,11 @@
"declaration": true,
"baseUrl": ".",
"paths": {
"@config": ["src/jimu-editor/config"],
"@hoc/*": ["src/jimu-editor/hoc/*"],
"@hooks/*": ["src/jimu-editor/hooks/*"],
"@shared/*": ["src/jimu-editor/shared/*"],
"@utils/*": ["src/jimu-editor/utils/*"],
"@/*": ["src/*"]
},
"outDir": "dist",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment