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 ...
随机推荐
- ios真机弱网测试
一.开启苹果手机开发者选项 首先你需要将iPhone和一台Mac电脑相连接,然后在Mac上打开Xcode开发工具,此时你iPhone的设置里就会出现"开发者"这个选项了. 二.进入 ...
- Keycloak 团队宣布他们正在弃用大多数 Keycloak 适配器,包括Spring Security和Spring Boot
2月14日,Keycloak 团队宣布他们正在弃用大多数 Keycloak 适配器. 其中包括Spring Security和Spring Boot的适配器,这意味着今后Keycloak团队将不再提供 ...
- pytest(8)-参数化
前言 什么是参数化,通俗点理解就是,定义一个测试类或测试函数,可以传入不同测试用例对应的参数,从而执行多个测试用例. 例如对登录接口进行测试,假设有3条用例:正确账号正确密码登录.正确账号错误密码登录 ...
- mysql中generated always as的使用
-- generated always as的使用 CREATE TABLE t1( c1 INT, c2 DOUBLE(20,19) , c3 INT GENERATED ALWAYS AS(c1* ...
- suse 12 编译部署Keepalived + nginx 为 kube-apiserver 提供高可用
文章目录 编译部署nginx 下载nginx源码包 编译nginx 配置nginx.conf 配置nginx为systemctl管理 分发nginx二进制文件和配置文件 启动kube-nginx服务 ...
- [LeetCode]1450. 在既定时间做作业的学生人数
给你两个整数数组 startTime(开始时间)和 endTime(结束时间),并指定一个整数 queryTime 作为查询时间. 已知,第 i 名学生在 startTime[i] 时开始写作业并于 ...
- Spring Boot 学习-基础
一.Spring Boot 概述 SpringBoot 定义 Spring Boot 并不是用来替代 Spring 的新框架,而是和 Spring 框架紧密结合用于提升 Spring 开发者体验的工具 ...
- ensp上防火墙上配置nat
博文大纲:一.华为防火墙NAT的六个分类:二.解决NAT转换时的环路及无效ARP:三.server-map表的作用:四.NAT对报文的处理流程:五.各种常用NAT的配置方法: 一.华为防火墙NAT的六 ...
- 企业必读:BI数据可视化工具选型
伴随着大数据时代的到来,企业对数据的需求从"IT主导的报表模式"转向"业务主导的自助分析模式",可视化BI工具也随之应运而生.面对如此众多的可视化BI工具,我们 ...
- 企业为什么需要BI报表工具?
企业一旦成规模运作起来,数据量是数以万计的,无论数据还是报表都需要协同管理不断更新.面对各大OA\ERP\CRM系统的数据,传统用Excel处理数据效率是很低下的.要想快速出报表,快速取数,快速做业务 ...