react canvas画图,支持拖拽(ctrl + 左键拖拽)

import React, {Component} from 'react';
import { Button, Modal, Menu, Dropdown, Form, Row, Col, Input, Radio } from 'antd';
import './config.css';

class WorkstationConfig extends Component {
    constructor(props) {
        super(props);
        this.state = {
            loading: false,
			visible: false,
			canvas: {
            	width: 0,
				height: 0
			},
			text: 'text',
			font: '',
			selectKey: '',
			cursor: 'default'
        };

        this.canvas = null;
        this.ctx = null;
        this.clientRect = null; //保存canvas距离浏览器上下左右边距
		this.nodeArray = {}; //所有未初始渲染前所有节点数据
        this.dataNode = {}; //保存渲染后的所有实际节点数据
		this.selectNodeId = ''; //当前选中节点ID
		this.selectNode = {}; //当前选中节点
		this.dragging = false; //是否选中节点,
		this.workstationSpace = 3; //座位的左右间距
		this.seatR = 7; //座位圆半径
		this.dragHoldX = null;
		this.dragHoldY = null;
		this.clientX = null; //鼠标右击的X坐标
		this.clientY = null; //鼠标右击的Y坐标
    }

    componentDidMount() {
        this.init();
    }

    //页面初始化
    init = () =>{
		//设置canvas宽 高
		let workstationConfig = this.workstationConfig;
		!this.canvas && (this.canvas = this.workstationCanvas); //获取canvas对象
		this.clientRect = this.workstationCanvas.getBoundingClientRect(); //canvas距离浏览器上下左右边距
		!this.ctx && (this.ctx = this.canvas.getContext("2d")); //获取画布对象
		this.setState({
			canvas: {
				width: workstationConfig.clientWidth - 20,
				height: workstationConfig.clientHeight - 20
			}
		});
		window.addEventListener('mouseup', this.mouseUpListener);
	};

    //选择添加菜单
	menuClick = ({ key }) => {
		this.setState({
			visible: true,
			selectKey: key
		});
	};

	/**
	 * 获取鼠标右击时的坐标
	 * @param evt
	 * @returns {boolean}
	 */
	canvasContextMenu = ( evt ) => {
		this.clientX = evt.clientX;
		this.clientY = evt.clientY;
		return false;
	};

	/**
	 * 获取UUID
	 * @param pKey
	 * @returns {string}
	 */
	getUUID =  ( pKey ) => {
		let uid = 'xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx'.replace(/[xy]/g,function(c){
			let r=Math.random()*16|0,v=c=='x' ? r:(r&0x3|0x8);
			return v.toString(16).toLocaleUpperCase();
		});
		if( (pKey && this.dataNode[pKey + "_" + uid]) || this.nodeArray[uid] ){
			console.log("--- uid存在,重新生成 ---");
			this.getUUID();
		}
		return uid;
	};

	/**
	 * 获取鼠标在canvas上位置
	 */
	mouseSite = () =>{
		let mouseX = this.clientX - this.clientRect.left,
			mouseY = this.clientY - this.clientRect.top;
		return {
			mouseX: mouseX,
			mouseY: mouseY
		}
	};

	/**
	 * 绘制数据节点
	 */
	drawScreen = () =>{
		//清除画布
		this.ctx.clearRect(0, 0, this.state.canvas.width, this.state.canvas.height);
		//在canvas上渲染节点
		for(let key in this.nodeArray){
			if(this.nodeArray.hasOwnProperty(key)){
				let item = this.nodeArray[key];
				this[item['type'] + 'Draw'](item);
			}
		}
	};

	/**
	 * 绘制房间节点
	 * @param data
	 */
	roomDraw = ( data ) => {
		let pKey = data['key'];
		if(!data['textX'] || !data['textY']){
			data = this.calculateCoord(data);
		}
		data['pKey'] = pKey;
		this.dataNode[pKey + "_" + pKey] = data;
		!data['text'] && (data['text'] = '房间号');
		this.drawBase( data );
	};

	/**
	 * 绘制工位节点
	 * @param data
	 */
	workstationDraw = ( data ) => {
		!data['text'] && (data['text'] = '座位号');
		if(!data['textX'] || !data['textY']){
			data = this.calculateCoord(data);
		}
		let seat = data['seat'] || 1, //每排座位数
			row = data['row'] || 1, // 单双排
			pKey = data['key'];

		for(let j = 0;j < row;j++){ //计算单双排
			let item = {...data},
				y = data['y'],
				seatData = {}, //座位圆坐标
				width = data['width'],
				height = data['height'];
			y = height * j + y + (j * this.workstationSpace); //计算每排座位的 Y 坐标
			item['y'] = y;
			if((row == 2  && j == 0) || (row == 1 && data['direction'] == 'top')){
				seatData['y'] = y - this.seatR - 5;
			}else if( row == 2 && j == 1  || (row == 1 && data['direction'] == 'bottom')){
				seatData['y'] = +height + y + this.seatR + 5;
			}else{
				throw "参数错误: row = " + row;
			}
			for(let i = 0;i < seat;i++){ //计算座位数
				let x = data['x'], key = this.getUUID( pKey );
				x = width * i + x + (i * this.workstationSpace); //计算每个座位的 X 坐标
				item['x'] = x;
				item['pKey'] = pKey;
				item['key'] = key;
				this.dataNode[pKey + "_" + key] = item;
				this.drawBase( item );
				seatData['x'] = width / 2 + x;
				this.drawSeat(seatData);
			}
		}
	};

	/**
	 * 绘制座位
	 * @param data
	 */
	drawSeat = ( data ) =>{
		this.ctx.beginPath();
		this.ctx.arc(data['x'], data['y'], this.seatR, 0, 2*Math.PI);
		this.ctx.stroke();
	};

	/**
	 * 绘制节点基础方法
	 * @param data
	 */
	drawBase = ( data ) =>{
		this.ctx.fillStyle = '#000000';
		this.ctx.strokeRect(data['x'], data['y'], data.width, data.height);
		this.ctx.font = data['font'];
		this.ctx.fillText(data['text'], +data['x'] + +data['textX'], +data['y'] + +data['textY'] - 3);
	};

	/**
	 * 计算文字在节点中的居中位置
	 * @param data
	 * @returns {*}
	 */
	calculateCoord = ( data ) =>{
		//计算文本在节点中的相对位置(居中位置)
		//计算文本居中位置后,缓存坐标,避免重复计算导致浏览器重复重拍,影响性能
		if(this.textRef){
			let textWidthH = this.textRef.getBoundingClientRect();
			data['textX'] = ((data.width - textWidthH['width']) / 2).toFixed(2);
			data['textY'] = (+textWidthH['height'] + ((data.height - textWidthH['height']) / 2)).toFixed(2);
		}
		return data;
	};

	/**
	 * 判断是否选中节点对象
	 * @param shape
	 * @param mx
	 * @param my
	 * @returns {boolean}
	 */
	hitTest = () => {
		let mouseSite = this.mouseSite();
		for (let key in this.nodeArray) {
			if( this.nodeArray.hasOwnProperty(key)) {
				let shape =  this.nodeArray[key];
				//判断点击的节点
				if (mouseSite.mouseX > shape.x && mouseSite.mouseX < (shape.x + +shape.width) && mouseSite.mouseY > shape.y && mouseSite.mouseY < (shape.y + +shape.height)) {
					this.dragging = true;
					//判断鼠标是否保持在节点上
					this.dragHoldX = mouseSite.mouseX - shape.x;
					this.dragHoldY = mouseSite.mouseY - shape.y;
					return key;
				}
			}
		}
		return false;
	};

	/**
	 * 鼠标按下事件
	 * @param evt
	 */
	canvasMouseDown = ( evt ) => {
		if(evt.ctrlKey){
			this.clientX = evt['clientX'];
			this.clientY = evt['clientY'];
			if( this.selectNodeId = this.hitTest() ){
				this.selectNode = this.nodeArray[this.selectNodeId];
				this.setState({
					cursor: 'move'
				});
			}
		}
	};

	/**
	 * 鼠标释放事件
	 * @param evt
	 */
	mouseUpListener = (evt) => {
		if (this.dragging) {
			this.dragging = false;
			this.setState({
				cursor: 'default'
			});
		}
		return false;
	};

	/**
	 * 鼠标移动事件
	 * @param evt
	 */
	mouseMoveListener = (evt) => {
		if(this.dragging && evt.ctrlKey){
			this.clientX = evt['clientX'];
			this.clientY = evt['clientY'];
			let posX, posY, minX = 0,
				maxX = this.state.canvas.width - this.selectNode.width,
				minY = 0,
				maxY = this.state.canvas.height - this.selectNode.height,
				mouseSite = this.mouseSite();//获取鼠标点击坐标
			//防止拖动到画布外面
			posX = mouseSite.mouseX - this.dragHoldX;
			posX = (posX < minX) ? minX : ((posX > maxX) ? maxX : posX);
			posY = mouseSite.mouseY - this.dragHoldY;
			posY = (posY < minY) ? minY : ((posY > maxY) ? maxY : posY);
			this.nodeArray[this.selectNodeId].x = posX;
			this.nodeArray[this.selectNodeId].y = posY;
			this.drawScreen();
		}else{
			this.dragging = false;
			this.setState({
				cursor: 'default'
			});
		}
		return false;
	};


	handleOk = e => {
		this.props.form.validateFields((err, fieldsValue) => {
			if (!err) {
				let key = this.getUUID(),
					mouseSite = this.mouseSite(),
					x = mouseSite.mouseX - fieldsValue['width'] / 2,
					y = mouseSite.mouseY - fieldsValue['height'] / 2;
				fieldsValue['fontSize'] = fieldsValue['fontSize'] || (this.state.selectKey == 'room' ? 16 : 12);
				fieldsValue['font'] = fieldsValue['fontSize'] + "px Arial";
				this.nodeArray[key] = {...{
						type: this.state.selectKey,
						key: key,
						x: x,
						y: y
					}, ...fieldsValue};

				this.setState({
					text: fieldsValue['text'] || (this.state.selectKey == 'room' ? "房间号" : "座位号"),
					font: fieldsValue['font']
				}, () => {
					this.drawScreen();
					this.setState({
						visible: false
					});
				});
			}
		});
	};

	handleCancel = e => {
		this.setState({
			visible: false
		});
	};

	render() {
		const menu = (
			<Menu onClick={this.menuClick}>
				<Menu.Item key="workstation">添加工位</Menu.Item>
				<Menu.Item key="room">添加房间</Menu.Item>
			</Menu>
		);

		let { getFieldDecorator } = this.props.form;
		const formItemLayout = {
			labelCol: {
				xs: { span: 24 },
				sm: { span: 8 }
			},
			wrapperCol: {
				xs: { span: 24 },
				sm: { span: 16 }
			}
		};

		let style = {color: '#ffffff'};
		this.state.font && (style['font'] = this.state.font);
        return (
			<div className={'workstation-config'} onContextMenu={(e) => this.canvasContextMenu(e)} ref={ref => this.workstationConfig = ref} selectstart="return false;">
				<Dropdown overlay={menu} trigger={['contextMenu']}>
					<canvas
						onMouseDown={e => this.canvasMouseDown(e)}
						onMouseMove={e => this.mouseMoveListener(e)}
						width={this.state.canvas.width}
						height={this.state.canvas.height}
						ref={ref => (this.workstationCanvas = ref)}
						className={'workstation-canvas'}
						style={{cursor: this.state.cursor}}
					></canvas>
				</Dropdown>
				<Modal
					title="参数配置"
					maskClosable={false}
					destroyOnClose={true}
					visible={this.state.visible}
					onOk={this.handleOk}
					onCancel={this.handleCancel}
				>
					{this.state.selectKey == 'workstation' && <Form {...formItemLayout}>
						<Row span={24} gutter={24}>
							<Col span={12}>
								<Form.Item label="座位数">
									{getFieldDecorator('seat', { initialValue: 3 })(
										<Input type="text" />
									)}
								</Form.Item>
							</Col>
							<Col span={12}>
								<Form.Item label="字体大小">
									{getFieldDecorator('fontSize', { initialValue: 12 })(
										<Input type="text" />
									)}
								</Form.Item>
							</Col>
						</Row>
						<Row span={24} gutter={24}>
							<Col span={12}>
								<Form.Item label="宽" >
									{getFieldDecorator('width', { initialValue: 40 })(
										<Input type="text" />
									)}
								</Form.Item>
							</Col>
							<Col span={12}>
								<Form.Item label="高" >
									{getFieldDecorator('height', { initialValue: 25 })(
										<Input type="text" />
									)}
								</Form.Item>
							</Col>
						</Row>
						<Row span={24} gutter={24}>
							<Col span={12}>
								<Form.Item label="方向" >
									{getFieldDecorator('position', { initialValue: 1 })(
										<Radio.Group  value={1}>
											<Radio value={1}></Radio>
											<Radio value={2}></Radio>
										</Radio.Group>
									)}
								</Form.Item>
							</Col>
							<Col span={12}>
								<Form.Item label="单双排" >
									{getFieldDecorator('row', { initialValue: 2 })(
										<Radio.Group value={1}>
											<Radio value={1}>单排</Radio>
											<Radio value={2}>双排</Radio>
										</Radio.Group>
									)}
								</Form.Item>
							</Col>
						</Row>
						<Row>
							<Col>
								<label style={style} ref={ref => this.textRef = ref}>{this.state.text}</label>
							</Col>
						</Row>
					</Form>}
					{this.state.selectKey == 'room' && <Form {...formItemLayout}>
						<Row span={24} gutter={24}>
							<Col span={12}>
								<Form.Item label="房间号">
									{getFieldDecorator('text', { initialValue: '房间号' })(
										<Input type="text" />
									)}
								</Form.Item>
							</Col>
							<Col span={12}>
								<Form.Item label="字体大小">
									{getFieldDecorator('fontSize', { initialValue: 16 })(
										<Input type="text" />
									)}
								</Form.Item>
							</Col>
						</Row>
						<Row span={24} gutter={24}>
							<Col span={12}>
								<Form.Item label="宽" >
									{getFieldDecorator('width', { initialValue: 150 })(
										<Input type="text" />
									)}
								</Form.Item>
							</Col>
							<Col span={12}>
								<Form.Item label="高" >
									{getFieldDecorator('height', { initialValue: 50 })(
										<Input type="text" />
									)}
								</Form.Item>
							</Col>
						</Row>
						<Row>
							<Col>
								<label style={style} ref={ref => this.textRef = ref}>{this.state.text}</label>
							</Col>
						</Row>
					</Form>}
				</Modal>
			</div>
        );
    }
}

const WarrpWorkstationConfig = Form.create({ name: 'WorkstationConfig' })(WorkstationConfig);
export default WarrpWorkstationConfig;

config.css

.workstation-config {
    width: 100%;
    height: calc(100vh - 64px);
}

.workstation-canvas {
    background-color: #ffffff;
    margin: 10px;
}
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值