react中使用截图组件Cropper组件
--最近项目用react,学习react并使用cropper组件裁剪图片。
(这里开发组件不够统一有用tsx(TypeScript + xml/html)写的组件,有用jsx(javascript+xml/html)写的组件
前言:cropper组件引入到项目中的手顺直接看官方文档;github:https://github.com/fengyuanchen/cropperjs#methods 在线演示url: https://fengyuanchen.github.io/cropper/
1.cropper组件以及各种操作的简单封装。
react-cropper.js文件
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Cropper from 'cropperjs';
const optionProps = [
'dragMode',
'aspectRatio',
'data',
'crop',
// unchangeable props start from here
'viewMode',
'preview',
'responsive',
'restore',
'checkCrossOrigin',
'checkOrientation',
'modal',
'guides',
'center',
'highlight',
'background',
'autoCrop',
'autoCropArea',
'movable',
'rotatable',
'scalable',
'zoomable',
'zoomOnTouch',
'zoomOnWheel',
'wheelZoomRatio',
'cropBoxMovable',
'cropBoxResizable',
'toggleDragModeOnDblclick',
'minContainerWidth',
'minContainerHeight',
'minCanvasWidth',
'minCanvasHeight',
'minCropBoxWidth',
'minCropBoxHeight',
'ready',
'cropstart',
'cropmove',
'cropend',
'zoom',
];
const unchangeableProps = optionProps;
class ReactCropper extends Component {
componentDidMount() {
const options = Object.keys(this.props)
.filter(propKey => optionProps.indexOf(propKey) !== -1)
.reduce((prevOptions, propKey) =>
Object.assign({}, prevOptions, { [propKey]: this.props[propKey] }), {});
this.cropper = new Cropper(this.img, options);
}
UNSAFE_componentWillReceiveProps(nextProps) {
if (nextProps.src !== this.props.src) {
this.cropper.reset().clear().replace(nextProps.src);
}
if (nextProps.aspectRatio !== this.props.aspectRatio) {
this.setAspectRatio(nextProps.aspectRatio);
}
if (nextProps.data !== this.props.data) {
this.setData(nextProps.data);
}
if (nextProps.dragMode !== this.props.dragMode) {
this.setDragMode(nextProps.dragMode);
}
if (nextProps.cropBoxData !== this.props.cropBoxData) {
this.setCropBoxData(nextProps.cropBoxData);
}
if (nextProps.canvasData !== this.props.canvasData) {
this.setCanvasData(nextProps.canvasData);
}
if (nextProps.moveTo !== this.props.moveTo) {
if (nextProps.moveTo.length > 1) {
this.moveTo(nextProps.moveTo[0], nextProps.moveTo[1]);
} else {
this.moveTo(nextProps.moveTo[0]);
}
}
if (nextProps.zoomTo !== this.props.zoomTo) {
this.zoomTo(nextProps.zoomTo);
}
if (nextProps.rotateTo !== this.props.rotateTo) {
this.rotateTo(nextProps.rotateTo);
}
if (nextProps.scaleX !== this.props.scaleX) {
this.scaleX(nextProps.scaleX);
}
if (nextProps.scaleY !== this.props.scaleY) {
this.scaleY(nextProps.scaleY);
}
if (nextProps.enable !== this.props.enable) {
if (nextProps.enable) {
this.enable();
} else {
this.disable();
}
}
Object.keys(nextProps).forEach((propKey) => {
let isDifferentVal = nextProps[propKey] !== this.props[propKey];
const isUnchangeableProps = unchangeableProps.indexOf(propKey) !== -1;
if (typeof nextProps[propKey] === 'function' && typeof this.props[propKey] === 'function') {
isDifferentVal = nextProps[propKey].toString() !== this.props[propKey].toString();
}
if (isDifferentVal && isUnchangeableProps) {
throw new Error(`prop: ${propKey} can't be change after componentDidMount`);
}
});
}
componentWillUnmount() {
if (this.img) {
// Destroy the cropper, this makes sure events such as resize are cleaned up and do not leak
this.cropper.destroy();
delete this.img;
delete this.cropper;
}
}
setDragMode(mode) {
return this.cropper.setDragMode(mode);
}
setAspectRatio(aspectRatio) {
return this.cropper.setAspectRatio(aspectRatio);
}
getCroppedCanvas(options) {
return this.cropper.getCroppedCanvas(options);
}
setCropBoxData(data) {
return this.cropper.setCropBoxData(data);
}
getCropBoxData() {
return this.cropper.getCropBoxData();
}
setCanvasData(data) {
return this.cropper.setCanvasData(data);
}
getCanvasData() {
return this.cropper.getCanvasData();
}
getImageData() {
return this.cropper.getImageData();
}
getContainerData() {
return this.cropper.getContainerData();
}
setData(data) {
return this.cropper.setData(data);
}
getData(rounded) {
return this.cropper.getData(rounded);
}
crop() {
return this.cropper.crop();
}
move(offsetX, offsetY) {
return this.cropper.move(offsetX, offsetY);
}
moveTo(x, y) {
return this.cropper.moveTo(x, y);
}
zoom(ratio) {
return this.cropper.zoom(ratio);
}
zoomTo(ratio) {
return this.cropper.zoomTo(ratio);
}
rotate(degree) {
return this.cropper.rotate(degree);
}
rotateTo(degree) {
return this.cropper.rotateTo(degree);
}
enable() {
return this.cropper.enable();
}
disable() {
return this.cropper.disable();
}
reset() {
return this.cropper.reset();
}
clear() {
return this.cropper.clear();
}
replace(url, onlyColorChanged) {
return this.cropper.replace(url, onlyColorChanged);
}
scale(scaleX, scaleY) {
return this.cropper.scale(scaleX, scaleY);
}
scaleX(scaleX) {
return this.cropper.scaleX(scaleX);
}
scaleY(scaleY) {
return this.cropper.scaleY(scaleY);
}
render() {
const {
src,
alt,
crossOrigin,
style,
className,
} = this.props;
return (
<div
style={style}
className={className}
>
<img
crossOrigin={crossOrigin}
ref={(img) => { this.img = img; }}
src={src}
alt={alt === undefined ? 'picture' : alt}
style={{ opacity: 0 }}
/>
</div>
);
}
}
ReactCropper.propTypes = {
style: PropTypes.object, // eslint-disable-line react/forbid-prop-types
className: PropTypes.string,
// react cropper options
crossOrigin: PropTypes.string,
src: PropTypes.string,
alt: PropTypes.string,
// props of option can be changed after componentDidmount
aspectRatio: PropTypes.number,
dragMode: PropTypes.oneOf(['crop', 'move', 'none']),
data: PropTypes.shape({
x: PropTypes.number,
y: PropTypes.number,
width: PropTypes.number,
height: PropTypes.number,
rotate: PropTypes.number,
scaleX: PropTypes.number,
scaleY: PropTypes.number,
}),
scaleX: PropTypes.number,
scaleY: PropTypes.number,
enable: PropTypes.bool,
cropBoxData: PropTypes.shape({
left: PropTypes.number,
top: PropTypes.number,
width: PropTypes.number,
height: PropTypes.number,
}),
canvasData: PropTypes.shape({
left: PropTypes.number,
top: PropTypes.number,
width: PropTypes.number,
height: PropTypes.number,
}),
zoomTo: PropTypes.number,
moveTo: PropTypes.arrayOf(PropTypes.number),
rotateTo: PropTypes.number,
// cropperjs options
// https://github.com/fengyuanchen/cropperjs#options
// aspectRatio, dragMode, data
viewMode: PropTypes.oneOf([0, 1, 2, 3]),
preview: PropTypes.string,
responsive: PropTypes.bool,
restore: PropTypes.bool,
checkCrossOrigin: PropTypes.bool,
checkOrientation: PropTypes.bool,
modal: PropTypes.bool,
guides: PropTypes.bool,
center: PropTypes.bool,
highlight: PropTypes.bool,
background: PropTypes.bool,
autoCrop: PropTypes.bool,
autoCropArea: PropTypes.number,
movable: PropTypes.bool,
rotatable: PropTypes.bool,
scalable: PropTypes.bool,
zoomable: PropTypes.bool,
zoomOnTouch: PropTypes.bool,
zoomOnWheel: PropTypes.bool,
wheelZoomRatio: PropTypes.number,
cropBoxMovable: PropTypes.bool,
cropBoxResizable: PropTypes.bool,
toggleDragModeOnDblclick: PropTypes.bool,
minContainerWidth: PropTypes.number,
minContainerHeight: PropTypes.number,
minCanvasWidth: PropTypes.number,
minCanvasHeight: PropTypes.number,
minCropBoxWidth: PropTypes.number,
minCropBoxHeight: PropTypes.number,
ready: PropTypes.func,
cropstart: PropTypes.func,
cropmove: PropTypes.func,
cropend: PropTypes.func,
crop: PropTypes.func,
zoom: PropTypes.func,
};
ReactCropper.defaultProps = {
src: null,
dragMode: 'crop',
data: null,
scaleX: 1,
scaleY: 1,
enable: true,
zoomTo: 1,
rotateTo: 0,
};
export default ReactCropper;
2.cropper组件调用的简单封装
CropperView.jsx文件
import React, { Component, useEffect } from 'react';
import $ from "jquery";
import Cropper from './cropper/react-cropper'
import 'cropperjs/dist/cropper.css'
/* global FileReader */
var showCropArea = true;
export default class CropperView extends Component {
constructor(props) {
super(props);
this.state = {
cropResult: null,
};
// this.cropper = this;
this.onChange = this.onChange.bind(this);
this.src = props.src;
}
// componentDidMount(){
// useEffect(() => {
// alert("cropZone" + this.cropper);
// // if (typeof this.cropper.getCroppedCanvas() === 'undefined') {
// // return;
// // }
// alert("left:" + this.cropper.getCropBoxData().left
// + "top:" + this.cropper.getCropBoxData().top
// + "width:" + this.cropper.getCropBoxData().width
// + "height:" + this.cropper.getCropBoxData().height);
// }, [this.props.save]);
// }
onChange(e) {
e.preventDefault();
let files;
if (e.dataTransfer) {
files = e.dataTransfer.files;
} else if (e.target) {
files = e.target.files;
}
const reader = new FileReader();
reader.onload = () => {
this.setState({ src: reader.result });
};
reader.readAsDataURL(files[0]);
}
cropZone() {
if (typeof this.cropper.getCroppedCanvas() === 'undefined') {
return;
}
alert("left:" + this.cropper.getCropBoxData().left
+ "top:" + this.cropper.getCropBoxData().top
+ "width:" + this.cropper.getCropBoxData().width
+ "height:" + this.cropper.getCropBoxData().height);
$(".cropper-crop-box").hide();
$(".cropper-drag-box").hide();
$(".cropper-wrap-box").append(
"<div style=\"width:" + this.cropper.getCropBoxData().width + ";"
+ "height:" + this.cropper.getCropBoxData().height + ";"
+ "background:#0000FF; opacity:0.3"
+ "position:absolute; left:" + this.cropper.getCropBoxData().left +";"
+ "top:" + this.cropper.getCropBoxData().top + ";\">");
}
showCropZone() {
if (showCropArea) {
$(".cropper-crop-box").css("display", "block");
$(".cropper-drag-box").css("display", "block");
} else {
$(".cropper-crop-box").hide();
$(".cropper-drag-box").hide();
}
}
creatCrop(){
this.cropper.crop();
}
clearCrop(){
this.cropper.clear();
}
resetCrop(){
this.cropper.reset();
}
moveLeft(){
this.cropper.move(-5, 0);
}
moveRight(){
this.cropper.move(5, 0);
}
moveUp(){
console.log("====moveUp===");
try {
this.cropper.move(0, -5);
}catch (err){
console.log(err);
}
}
moveDown(){
this.cropper.move(0, 5);
}
enlarge(){
try {
// 放大
// this.cropper.zoom(0.1);
var allCanvasDate = this.cropper.getCanvasData();
var newCanvasDate = {left:allCanvasDate.left, top: allCanvasDate.top,
width: allCanvasDate.width*2, height: allCanvasDate.height*2}
this.cropper.setCanvasData(newCanvasDate);
}catch (err){
console.log(err);
}
}
shrink(){
try {
// 缩小
// this.cropper.zoom(-0.1)
var allCanvasDate = this.cropper.getCanvasData();
var newCanvasDate = {left:allCanvasDate.left, top: allCanvasDate.top,
width: allCanvasDate.width*0.5, height: allCanvasDate.height*0.5}
this.cropper.setCanvasData(newCanvasDate);
}catch (err){
console.log(err);
}
}
test(){
try {
var allDate = this.cropper.getData(true);
alert(allDate.toString());
var par = {x:allDate.x, y:allDate.y, width:allDate.width*2, height:allDate.height*2,
rotate:allDate.rotate, scaleX:allDate.scaleX, scaleY:allDate.scaleY}
this.cropper.setData(par);
}catch (err){
console.log(err);
}
}
moveCrop(){
//this.cropper.movecrop();
console.log("===move===crop===");
}
reduceCrop(){
var par = {left:this.cropper.getCropBoxData().left, top:this.cropper.getCropBoxData().top,
width:this.cropper.getCropBoxData().width*0.8, height:this.cropper.getCropBoxData().height*0.8}
this.cropper.setCropBoxData(par);
}
raiseCrop(){
var par = {left:this.cropper.getCropBoxData().left, top:this.cropper.getCropBoxData().top,
width:this.cropper.getCropBoxData().width*1.2, height:this.cropper.getCropBoxData().height*1.2}
this.cropper.setCropBoxData(par);
}
CropLeft(){
var par = {left:this.cropper.getCropBoxData().left - 10, top:this.cropper.getCropBoxData().top,
width:this.cropper.getCropBoxData().width, height:this.cropper.getCropBoxData().height}
this.cropper.setCropBoxData(par);
}
CropRight(){
var par = {left:this.cropper.getCropBoxData().left + 10, top:this.cropper.getCropBoxData().top,
width:this.cropper.getCropBoxData().width, height:this.cropper.getCropBoxData().height}
this.cropper.setCropBoxData(par);
}
CropUp(){
var par = {left:this.cropper.getCropBoxData().left, top:this.cropper.getCropBoxData().top - 10,
width:this.cropper.getCropBoxData().width, height:this.cropper.getCropBoxData().height}
this.cropper.setCropBoxData(par);
}
CropDown(){
var par = {left:this.cropper.getCropBoxData().left, top:this.cropper.getCropBoxData().top + 10,
width:this.cropper.getCropBoxData().width, height:this.cropper.getCropBoxData().height}
this.cropper.setCropBoxData(par);
}
render() {
return (
<div className="r-view" style={{ position:'absolute', left:'185px', top:"83px" }}>
<Cropper
style={{ height:'100%', width:'auto' }}
aspectRatio={16 / 9}
preview=".img-preview"
guides={false}
src={this.props.src}
viewMode={2}
minContainerWidth={585}
minContainerHeight={430}
ref={cropper => { this.cropper = cropper; }}
zoomable={true}
zoomOnTouch={true}
/>
</div>
);
}
}
3.cropper组件与各种按钮操作的绑定(原因:设备上不会支持手指在选择区域的操作以及图片的放大缩小操作),页面整合组件。
CropperScreen.tsx文件
import * as React from 'react';
import styled from "styled-components";
import useTranslate from "../../hooks/useTranslate";
import CropView from "./CropperView";
import 'cropperjs/dist/cropper.css' import FormView from "./FormView" const AppStyle = styled.div`
background: #CCC;
`; var showCropArea = false; export default function CropperScreen() {
const t = useTranslate(); const handleBackClicked = () => {
$("#viewable").show();
$("#scan_settings_id").css("display","none");
}; const doCropClicked = () => {
// showCropArea = !showCropArea;
// if (showCropArea) {
// alert("disabled false");
// $("#btn_save_crop").removeAttr("disabled");
// } else {
// alert("disabled true");
// $("#btn_save_crop").attr("disabled", "true");
// }
// cropUser.showCropZone(showCropArea); cropUser.clearCrop(); }; const doSaveCropClicked = () => {
cropUser.cropZone();
}; const handReset = () => {
cropUser.resetCrop();
cropUser.creatCrop();
} const handMoveLeft = () => {
cropUser.moveLeft();
} const handMoveRight = () => {
cropUser.moveRight();
} const handMoveUp = () => {
console.log("====handMoveUp===");
try {
cropUser.moveUp();
}catch (err){
console.log(err);
}
} const handMoveDown = () => {
cropUser.moveDown();
} const handMoveCrop = () => {
alert("unknow");
cropUser.moveCrop();
} const handReduceCrop = () => {
cropUser.reduceCrop();
} const handRaiseCrop = () => {
cropUser.raiseCrop()
} const handEnlarge = () => {
cropUser.enlarge();
} const handShrink = () => {
cropUser.shrink();
} const handCropLeft = () => {
cropUser.CropLeft()
} const handCropRight = () => {
cropUser.CropRight()
} const handCropUp = () => {
cropUser.CropUp()
} const handCropDown = () => {
cropUser.CropDown()
} const handTest = () => {
cropUser.test()
} const handleSubmit = () => {
alert("====handleSubmit====");
console.log(formUser); console.log(formUser.state);
} let maxItem = 2;
let cropUser
let formUser
var initValue = {name:'tom',job: '12'} return (
<AppStyle id="scan_settings_id" className="r-view" style={{display:"none", height:"470px"}}>
<header className="r-titlebar">
<a href="#" className="r-titlebar__back" onClick={handleBackClicked} />
<h1 className="r-titlebar__title">Preview</h1>
</header>
<div style={{position:"absolute", left:35}}>
</div>
<CropView ref={cropView => {cropUser = cropView}} src={require('../smartsdk-css/img/test.png')}/>
<div className="r-floating-island">
<div>
<button style={{width:'70px', height:'35px', marginLeft:'5px'}} onClick={doCropClicked}>Crop</button>
<button style={{width:'70px', height:'35px', marginLeft:'5px'}} onClick={doSaveCropClicked}>Save</button>
</div>
<div>
<button style={{width:'70px', height:'35px', marginLeft:'5px'}} onClick={handReset}>reset</button>
<button style={{width:'70px', height:'35px', marginLeft:'5px'}} onClick={handMoveCrop}>mcrop</button>
</div>
<div>
<button style={{width:'70px', height:'35px', marginLeft:'5px'}} onClick={handMoveLeft}>left</button>
<button style={{width:'70px', height:'35px', marginLeft:'5px'}} onClick={handMoveRight}>right</button>
</div>
<div>
<button style={{width:'70px', height:'35px', marginLeft:'5px'}} onClick={handMoveUp}>up</button>
<button style={{width:'70px', height:'35px', marginLeft:'5px'}} onClick={handMoveDown}>down</button>
</div>
<div>
<button style={{width:'70px', height:'35px', marginLeft:'5px'}} onClick={ handEnlarge}>+</button>
<button style={{width:'70px', height:'35px', marginLeft:'5px'}} onClick={ handShrink}>-</button>
</div>
<div>
<button style={{width:'70px', height:'35px', marginLeft:'5px'}} onClick={ handReduceCrop}>reduce</button>
<button style={{width:'70px', height:'35px', marginLeft:'5px'}} onClick={ handRaiseCrop}>raise</button>
</div> <div>
<button style={{width:'70px', height:'35px', marginLeft:'5px'}} onClick={ handCropLeft}>cropr</button>
<button style={{width:'70px', height:'35px', marginLeft:'5px'}} onClick={ handCropRight}>cropl</button>
</div> <div>
<button style={{width:'70px', height:'35px', marginLeft:'5px'}} onClick={ handCropUp}>cropu</button>
<button style={{width:'70px', height:'35px', marginLeft:'5px'}} onClick={ handCropDown}>cropd</button>
</div>
<div>
<button style={{width:'70px', height:'35px', marginLeft:'5px'}} onClick={ handTest}>test</button>
</div> {/* <FormView handleSubmit={handleSubmit} ref={formView => {formUser = formView}} props = {initValue}/> */} {/* <button className="r-start-button">
{t("dapi:cba.common.start")}
</button> */}
</div>
</AppStyle >
);
}
4.最后调用整体的组件,页面展示

页面

react中使用截图组件Cropper组件的更多相关文章
- React中的Context——从父组件传递数据
简介:在React中,数据可以以流的形式自上而下的传递,每当你使用一个组件的时候,你可以看到组件的props属性会自上而下的传递.但是,在某些情况下,我们不想通过父组件的props属性一级一级的往下传 ...
- react 中的无状态函数式组件
无状态函数式组件,顾名思义,无状态,也就是你无法使用State,也无法使用组件的生命周期方法,这就决定了函数组件都是展示性组件,接收Props,渲染DOM,而不关注其他逻辑. 其实无状态函数式组件也是 ...
- 基于 React 实现一个 Transition 过渡动画组件
过渡动画使 UI 更富有表现力并且易于使用.如何使用 React 快速的实现一个 Transition 过渡动画组件? 基本实现 实现一个基础的 CSS 过渡动画组件,通过切换 CSS 样式实现简单的 ...
- 理解React中es6方法创建组件的this
首发于:https://mingjiezhang.github.io/(转载请说明此出处). 在JavaScript中,this对象是运行时基于函数的执行环境(也就是上下文)绑定的. 从react中的 ...
- React中父组件与子组件之间的数据传递和标准化的思考
React中父组件与子组件之间的数据传递的的实现大家都可以轻易做到,但对比很多人的实现方法,总是会有或多或少的差异.在一个团队中,这种实现的差异体现了每个人各自的理解的不同,但是反过来思考,一个团队用 ...
- React 深入系列1:React 中的元素、组件、实例和节点
文:徐超,<React进阶之路>作者 授权发布,转载请注明作者及出处 React 深入系列,深入讲解了React中的重点概念.特性和模式等,旨在帮助大家加深对React的理解,以及在项目中 ...
- [转] React 中组件间通信的几种方式
在使用 React 的过程中,不可避免的需要组件间进行消息传递(通信),组件间通信大体有下面几种情况: 父组件向子组件通信 子组件向父组件通信 跨级组件之间通信 非嵌套组件间通信 下面依次说下这几种通 ...
- react中直接调用子组件的方法(非props方式)
我们都知道在 react中,若要在父组件调用子组件的方法,通常我们会采用在父组件定义一个方法,作为props转给子组件,然后执行该方法,可以获取到子组件传回的参数以得到我们的目的. 显而易见,这个执行 ...
- React 中的 Component、PureComponent、无状态组件 之间的比较
React 中的 Component.PureComponent.无状态组件之间的比较 table th:first-of-type { width: 150px; } 组件类型 说明 React.c ...
随机推荐
- 虫师Selenium2+Python_8、自动化测试高级应用
P205--HTML测试报告 P213--自动发邮件功能 P221--Page Object 设计模式
- Oracle - Trunc() 函数截取日期&截取数值
Oracle TRUNC函数可以截取数字和日期类型:截取日期:select to_char(sysdate,'yyyy-mm-dd hh24:mi:ss') from dual; --显示当前时间 s ...
- Solution -「LOCAL」舟游
\(\mathcal{Description}\) \(n\) 中卡牌,每种三张.对于一次 \(m\) 连抽,前 \(m-1\) 次抽到第 \(i\) 种的概率是 \(p_i\),第 \(m\) ...
- Hive之同比环比的计算
Hive系列文章 Hive表的基本操作 Hive中的集合数据类型 Hive动态分区详解 hive中orc格式表的数据导入 Java通过jdbc连接hive 通过HiveServer2访问Hive Sp ...
- tip1:学习使用mybatis中使用mysql数据库的基本操作
1.查看mysql服务是否启动: 2.root用户链接数据库:mysql -u root -p,随后输入正确的密码即可. 3.root用户创建数据库: 4.查看已建数据库:show databases ...
- python2批量改密码
客户端代码: 如果注释最下面三行socket发送,可以直接在本机修改密码不发送数据到服务端. # -*- coding: utf-8 -*- #author:Guoyabin import rando ...
- ShaderLab实现Vignette过场动画效果
实现Vignette过场动画效果 postprocessing中有渐晕效果(Vignette),镜头可以由边缘往中间慢慢变黑: 但是我打包WebGL的时候提示我postprocessing,GPU不支 ...
- oracle11g在windows下安装
oracle下载 https://www.oracle.com/database/technologies/oracle-database-software-downloads.html 下载11g ...
- KALI搭建Docker+Vulhub漏洞复现环境
在学习网络安全的过程中,少不了的就是做漏洞复现,而漏洞复现一般比较常用的方式就是使用docker-vulhub进行环境搭建,我近期也遇到了这个问题,但是网上的教程特别混乱,根本起不到帮助作用,即使有可 ...
- 绘制地图其实并不难!如何绘制地图?看看Smartbi的制作方法
数据地图是一种图形化的数据资产管理工具,它提供了多层次的图形化展现,并具备各种力度的控制能力,满足业务使用.数据管理.开发运维不同应用场景的图形查询和辅助分析需求.长期以来,很多职场人士都希望能在自己 ...