【后台管理系统】—— Ant Design Pro组件使用(一)
一、搜索Search
- 搜索框
<Search placeholder="请输入关键字"
defaultValue={kw && kw != 'null' ? kw : ''}
className={styles.search}
onChange={() => this.handleKwChange()}
onSearch={(e) => this.handleSearch(e)}
/> 引入工具方法:去掉收尾空格
import {trimStr} from '@/utils/utils'; // utils.js
export function trimStr(str){
return str.replace(/(^\s*)|(\s*$)/g,"");
}搜索按钮触发搜索方法,输入内容改变自动搜索
handleSearch = e => {
const { dispatch } = this.props;
const { currentPage } = this.state;
let kw = trimStr(e);
this.setState({ keyword : kw });
dispatch({
type: 'newMallOrder/fetch',
payload: {
currentPage,
e: {
keyword: kw
},
showCount: 10
},
});
}; handleKwChange = () => {
const { dispatch } = this.props;
const { currentPage } = this.state;
if(event && event.target && event.target.value){
let value = event.target.value;
this.handleSearch(value)
}else{
dispatch({
type: 'newMallOrder/fetch',
payload: {
currentPage,
e: {
keyword: null
},
showCount: 10
},
});
}
}
二、选择器Select & TreeSelect
- 表单中嵌入Select选择器
<FormItem>
{getFieldDecorator('tempTypeId',{
initialValue: 0
})(
<Select placeholder="请选择" style={{ width: '100%' }}
onChange={this.handleTempType}>
<Option value={0}>H5在线编辑</Option>
<Option value={1}>贺卡</Option>
<Option value={2}>海报</Option>
<Option value={3}>壁纸</Option>
<Option value={4}>全部</Option>
</Select>
)}
</FormItem>选择方法:
handleTempType = value => {
const { dispatch } = this.props;
const { keyword } = this.state; this.setState({
tempType: tempTypeMap[value]
})
dispatch({
type: 'temp/fetch',
payload: {
currentPage: 1,
e: {
keyword: keyword,
subjectClass: tempTypeMap[value]
},
showCount: 2
}
});
dispatch({
type: 'temp/fetchType',
payload: {
ofClass: tempTypeMap[value]
},
callback: (res) => {
if(res.code == 200){
let typeList = res.data; // 获取联动选择框的数据 typeList.forEach((typeItem, index) => {
dispatch({
type: 'temp/fetchThirdType',
payload: typeItem.id,
callback: (res) => {
if(res.code == 200 && res.data.length){
typeList[index].list = res.data;
}
}
})
})
setTimeout(() => this.setState({ typeList }), 0)
}
}
});
} - 联动选择的第一个选择框的父级数据
let ParentTypeData = [
{
title: 'H5在线编辑',
value: 0,
key: 0,
},
{
title: '贺卡',
value: 1,
key: 1,
},
{
title: '海报',
value: 2,
key: 2,
},
{
title: '壁纸',
value: 3,
key: 3,
},
{
title: '全部',
value: 4,
key: 4,
},
]; - 处理获取到的联动选择第二个选择框的数据为TreeSelect需要的数据格式
let typeData = [];
const typeTree = (typeList, typeData) => {
if(typeList.length) {
for(let i=0; i<typeList.length; i++){
typeData[i] = {
title: typeList[i].kind,
value: typeList[i].id,
key: typeList[i].id
}
//二级分类
if(typeList[i].list){
typeData[i].children = [];
typeTree(typeList[i].list, typeData[i].children)
}
}
}
}
typeTree(typeList, typeData); 表单中嵌入TreeSelect选择器
<FormItem label="主题类别" {...formLayout}>
<TreeSelect
defaultValue={tempType == null ? 4 : tempTypeMap.indexOf(tempType)}
value={parentTypeId}
style={{display: `${editDisable ? 'none' : 'inline-block'}`, width: '47%', marginRight: '6%'}}
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
treeData={ParentTypeData}
placeholder="请选择"
onChange={handleParentType}
/>
{form.getFieldDecorator('typeIds', {
rules: [{ type:"array", required: true, message: '请选择主题类别'}],
initialValue: detail.types && detail.types.length
? detail.types.map((type) => type.id)
: []
})(
<TreeSelect
multiple // 多选
style={{width: `${editDisable ? '100%' : '47%'}`}}
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
treeData={typeData}
placeholder="请选择"
disabled={editDisable}
onChange={handleTypeChange}
/>
)}
</FormItem>
三、图片视频音频上传Upload
- 弹框表单中上传一张图片、一个音频
- 引入upload.js中封装的handImageUpload文件上传七牛云的方法
import { handleImageUpload } from '@/utils/upload'; // 预览文件时url前面需要加七牛云服务器前缀
// eg: 'http://fileserver.liuliu123.cn/'
import { setFileHost } from '@/utils/utils';upload.js
var qiniu = require('qiniu-js')
import axios from 'axios';
import configs from '@/utils/env'
import { message } from 'antd'; //七牛云上传,input, onchange事件
export function handleImageUpload(file, type, resName) {
// console.log(file,'handleImageUpload')
let suffix = file.type.split('/')[1]; return new Promise(function(resolve, reject){ if(!file) {
reject('file is undefined')
} function dataURItoBlob(base64Data) {
var byteString;
if(base64Data.split(',')[0].indexOf('base64') >= 0)
byteString = atob(base64Data.split(',')[1]);
else
byteString = unescape(base64Data.split(',')[1]);
var mimeString = base64Data.split(',')[0].split(':')[1].split(';')[0];
var ia = new Uint8Array(byteString.length);
for(var i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
return new Blob([ia], {
type: mimeString
});
} function randomString(len) {
len = len || 32;
var $chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678';
var maxPos = $chars.length;
var pwd = '';
for (let i = 0; i < len; i++) {
pwd += $chars.charAt(Math.floor(Math.random() * maxPos));
}
return pwd;
} var reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = function(e) { var fileBlob = dataURItoBlob(e.target.result);
var key;
switch(type){
case 'audio':
key = 'sys/file/music/' + resName + '.' + suffix;
break;
case 'video':
key = 'liveWallPaper/' + resName + '.' + suffix;
break;
case 'tutorial': //教程文件
key = 'sys/tutorial/' + new Date().getTime() + randomString(5) + '.' + suffix;
break;
case 'tutorialVideo': //针对IOS富文本视频显示问题单独处理教程视频
key = 'yihezo/' + new Date().getTime() +randomString(5) + '.' + suffix;
break;
default:
key = 'user/h5/' + new Date().getTime() +randomString(5) + '.' + suffix;
} var putExtra = {
fname: file.name,
params: {},
mimeType: ["image/png", "image/jpeg", "image/jpg", "image/gif", "image/webp", "image/apng", "image/svg",
"audio/mp3", "audio/mp4", "audio/ogg", "audio/mpeg",
"video/mp4", "video/ogg", "video/webm"]
}; var config = {
useCdnDomain: true,
}; if(type == 'tutorialVideo' ){ //针对IOS富文本视频显示问题单独处理教程视频
axios.post(configs[process.env.API_ENV]['BG_SERVER']+'/file/qiniu/token/video', {
key: 'yihezo/' + new Date().getTime() +randomString(5) //入参:教程视频的key
}, {
headers: {
AuthorizationToken: localStorage.getItem('login_token')
}
}).then(res =>{
let {data} = res;
if(data.code == 200) {
let token = data.data.token;
let observable = qiniu.upload(fileBlob, key, token, putExtra, config) let subscription = observable.subscribe({next(res){
// console.log(res, 'loading')
}, error(res){
message.error('上传失败');
}, complete(res) {
resolve(res.key)
}})
//subscription.unsubscribe() // 上传取消 } else {
message.error('获取七牛云token失败');
}
}).catch(error => {
console.error(error)
reject(error)
}) }else{
axios.post(configs[process.env.API_ENV]['BG_SERVER']+'/file/qiniu/token', {}, {
headers: {
AuthorizationToken: localStorage.getItem('login_token')
}
}).then(res =>{
let {data} = res;
if(data.code == 200) {
let token = data.data.token;
let observable = qiniu.upload(fileBlob, key, token, putExtra, config) let subscription = observable.subscribe({next(res){
// console.log(res, 'loading')
}, error(res){
message.error('上传失败');
}, complete(res) {
resolve(res.key)
}})
//subscription.unsubscribe() // 上传取消 } else {
message.error('获取七牛云token失败');
}
}).catch(error => {
console.error(error)
reject(error)
})
} }
}) } - state中定义初始值
fileThumb: null, // 存储上传七牛云后返回的图片url
fileUri: null, // 存储上传七牛云后返回的文件url
fileVisible: false, // 控制预览文件的弹框是否visible
previewVisible: false,// 控制预览图片的弹框是否visible
previewImage: '', //预览要上传的图片和上传后的图片的url
previewFile: '' //预览要上传的图片和上传后的图片的url 弹框表单中Upload组件
// 上传按钮
const ImgUpButton = (
<div>
<Icon type="plus" />
<div className="ant-upload-text">Upload</div>
</div>
); const FileUpButton = (
<Button>
<Icon type="upload" /> Upload
</Button>
)<FormItem label="资源图片" {...this.formLayout}>
{
getFieldDecorator('thumb', {
rules: [{ required: true, message: '请上传图片' }],
initialValue: current.thumb ? [{ // 默认必须是数组
uid: '-1',
status: 'done',
name: current.resName,
url: `${setFileHost()+current.thumb}`,
thumbUrl: `${setFileHost()+current.thumb}`
}] : "" // 无值时必须是空串
})(
<div>
<Upload
accept="image/*" // 限制上传的文件类型
action={(file) => handleImageUpload(file, 'image').then(res => {
const newFileThumb = [];
newFileThumb.push(res);
this.setState({
fileThumb: newFileThumb,
})
})} // 上传七牛云后存储url
listType="picture-card"
fileList={imgList} // 显示的图片数组
onRemove={this.handleImgRemove}
onPreview={this.handleImgPreview}
onChange={this.handleImgChange}
>
{imgList.length >= 1 ? null : ImgUpButton}
</Upload>
<Modal visible={previewVisible} footer={null} onCancel={this.handleImgCancel}>
<img alt="资源图片" style={{ width: '100%' }} src={previewImage} />
</Modal>
</div>
)}
</FormItem>
<FormItem label="资源文件" extra={resNameError || resName == null ? <span style={{color:'#1890FF'}}>请先输入资源名称</span> : ''} {...this.formLayout}>
{getFieldDecorator('uri', {
rules: [{ required: true, message: '请上传文件' }],
initialValue: current.uri ? [{
uid: '-1',
status: 'done',
name: current.uri,
url: `${setFileHost()+current.uri}`
}] : ""
})(
<div>
<Upload
accept="audio/mp3, audio/mp4, audio/ogg, audio/mpeg"
disabled={ resNameError || resName == null ? true : false }
action={(file) => handleImageUpload(file, 'audio', resName).then(res => {
const newFileUri = [];
newFileUri.push(res);
this.setState({
fileUri: newFileUri
})
})}
fileList={fileList}
onRemove={this.handleFileRemove}
onPreview={this.handleFilePreview}
onChange={this.handleFileChange}
>
{fileList.length >= 1 ? null : FileUpButton}
</Upload>
<Modal visible={fileVisible} footer={null} onCancel={this.handleFileCancel} style={{textAlign: 'center'}}>
<audio src={previewFile} style={{ width: '80%' }} controls="controls" autoPlay="autoplay">
您的浏览器不支持 audio 标签。
</audio>
</Modal>
</div>
)}
</FormItem>// 上传图片 使用的方法
// 删除、预览弹框关闭、预览图片url和预览弹框打开,存储改变的图片url
handleImgRemove = () => {
this.setState({
imgList: [],
fileThumb: [''],
})
return true
} handleImgCancel = () => this.setState({ previewVisible: false }) handleImgPreview = (file) => {
this.setState({
previewImage: file.url || file.thumbUrl,
previewVisible: true,
});
} handleImgChange = ({ fileList }) => this.setState({ imgList: fileList }) // 上传文件 使用的方法
// 删除、预览弹框关闭、预览文件url和预览弹框打开,存储文件的图片url
handleFileRemove = () => {
this.setState({
fileList: [],
fileUri: [''],
})
return true
} handleFileCancel = () => this.setState({ fileVisible: false }) handleFilePreview = (file) => {
file.url ?
this.setState({
previewFile: file.url,
fileVisible: true,
}) :
message.error('请先保存');
} handleFileChange = ({ fileList }) => this.setState({ fileList })
弹框表单中上传多张图片
引入upload.js中封装的handImageUpload文件上传七牛云的方法
import { handleImageUpload } from '@/utils/upload';
import {setFileHost} from '@/utils/utils';- state中定义初始数据
// 上传多张轮播图(可上传视频)
imgList: [],
fileThumbs: [],
previewVisible: false,
previewImage: '', // 上传一张图片
introImgList: [],
introFileThumb: '',
introPreviewVisible: false,
introPreviewImage: '', showModal显示弹框的方法中: 处理获得的图片数组存入state
showEditModal = item => {
const { dispatch } = this.props; dispatch({
type: 'project/fetchDetail',
payload: {
goodsId: item.id
},
callback: (res) => {
if(res){
this.setState({
detail: res.data,
imgList: res.data.rotationChart && res.data.rotationChart.length
? this.initImgList(res.data) : "", // Upload显示图片本地存储的图片url数组
fileThumbs: res.data.rotationChart && res.data.rotationChart.length
? this.initFileThumbs(res.data) : "", // 上传七牛云后存储的用于传给后端的图片url数组
introImgList: res.data.introPic
? [{
uid: '-1',
status: 'done',
name: res.data.introPic,
url: `${setFileHost()+res.data.introPic}`,
thumbUrl: `${setFileHost()+res.data.introPic}`
}] : '',
introFileThumb: res.data.introPic ? res.data.introPic : '',
current: item,
addSubmit: false
}, () => {
this.setState({
visible: true
})
});
}
}
})
};弹框表单中嵌入Upload组件
const ImgUpButton = ( // 上传图片的按钮
<div>
<Icon type="plus" />
<div className="ant-upload-text">Upload</div>
</div>
);<FormItem label="产品图片" {...formLayout}>
{ getFieldDecorator('rotationChart', {
rules: [{ required: true, message: '请上传1-7张图片'}],
// 默认显示是图片数组,无值也是空数组
initialValue: current && detail && detail.rotationChart && detail.rotationChart.length
? initImgList(detail) : []
})(
<div>
<Upload
accept="image/*"
// action={(file) => handleImageUpload(file, 'image').then(res => {
// handleFileThumb(res, file, imgList)
// })}
listType="picture-card"
fileList={imgList}
onPreview={handleImgPreview}
onRemove={handleImgRemove}
// beforeUpload上传前的处理函数: 嵌套handleImageUpload方法 (上传一张图片或一个文件时,如果需要上传前判断文件类型、文件大小也是这么做)
// 1.包含handleFileThumb方法,代替action实现上传七牛云服务器后存储state;2.同时将新的图片数组imgArray存入本地imgList改变Upload组件显示的图片
beforeUpload={beforeUpload}
// onChange={handleImgChange}
>
{imgList.length >= 7 ? null : ImgUpButton}
</Upload>
<Modal visible={previewVisible} footer={null} onCancel={handleImgCancel} style={{textAlign: 'center'}}>
{ previewType == 'liveWallPaper' ?
<video src={previewImage} style={{ width: '50%' }} controls="controls" autoPlay="autoplay">
您的浏览器不支持 video 标签。
</video>
: <img alt="产品图片" style={{ width: '100%' }} src={previewImage} />}
</Modal>
</div>
)}
</FormItem>使用到的方法
initImgList = (item) => { // 处理Upload默认显示的数据
let defaultImgList = [];
item.rotationChart.forEach((imgListItem, index) => {
defaultImgList.push ({
uid: `${-1-index}`,
status: 'done',
name: item.name,
url: imgListItem.img ? `${setFileHost()+imgListItem.img}` : '',
thumbUrl: imgListItem.thumb ? `${setFileHost()+imgListItem.thumb}` : ''
})
})
return defaultImgList
}initFileThumbs = (item) => { // 不更改不上传新的图片时默认向后端传的图片url数组
let defaultFileThumbs = [];
item.rotationChart.forEach((fileThumb, index) => {
defaultFileThumbs[index] = fileThumb;
})
return defaultFileThumbs
}handleFileThumb = (res, file, imgList) => { // 更改fileThumbs数组
let { fileThumbs } = this.state;
fileThumbs[imgList.length-1] = {
img: res,
index: imgList.length-1,
type: file.type.split('/')[0],
thumb: res
};
this.setState({
fileThumbs
})
}// 关闭预览弹框
handleImgCancel = () => this.setState({ previewVisible: false }) // 显示预览弹框
handleImgPreview = (file) => {
this.setState({
previewImage: file.url || file.thumbUrl,
previewVisible: true,
});
} // 删除预览弹框
handleImgRemove = (file) => {
const { fileThumbs, imgList } = this.state;
let newList = [...imgList];
let newFileThumbs = [...fileThumbs];
newList.forEach((imgItem, index) => {
if(imgItem.uid == file.uid){
newList.splice(index, 1)
newFileThumbs.splice(index, 1)
}
})
this.setState({
imgList: newList,
fileThumbs: newFileThumbs
}, () => {
return true
})
}beforeUpload = (file) => {
let type = file.type.split('/')[0];
let name = file.name.split('.')[0]; // 判断文件类型 -- 如果是视频url直接存入imgList,存入fileThumb
if(type == 'video') {
let imgArray = [...this.state.imgList];
imgArray.push(file); handleImageUpload(file, 'video', name).then(res => {
this.setState({
imgList: imgArray
})
this.handleFileThumb(res, file, imgArray)
})
}else{
// 如果是图片,使用react-cropper插件相关设置进行裁剪处理
// 当打开同一张图片的时候清除上一次的缓存
if (this.refs.cropper) {
this.refs.cropper.reset();
} var reader = new FileReader();
const image = new Image();
//因为读取文件需要时间,所以要在回调函数中使用读取的结果
reader.readAsDataURL(file); //开始读取文件 reader.onload = (e) => {
image.src = reader.result;
image.onload = () => {
this.setState({
srcCropper: e.target.result, //cropper的图片路径
selectImgName: file.name, //文件名称
selectImgSize: (file.size / 1024 / 1024), //文件大小
selectImgSuffix: file.type.split("/")[1], //文件类型
editImageModalVisible: true, //打开控制裁剪弹窗的变量,为true即弹窗
})
if (this.refs.cropper) {
this.refs.cropper.replace(e.target.result);
}
}
}
return false;
}
}
不需要裁剪的使用beforeUpload判断文件大小的上传一张图片
<FormItem label="人物介绍图片" {...formLayout}>
{getFieldDecorator('introPic', {
initialValue: current && detail && detail.introPic
? [{
uid: '-1',
status: 'done',
name: detail.introPic,
url: `${setFileHost()+detail.introPic}`,
thumbUrl: `${setFileHost()+detail.introPic}`
}] : ''
})(
<div>
<Upload
accept="image/*"
// action={(file) => handleImageUpload(file, 'image').then(res => {
// handleIntroFileThumb(res)
// })}
listType="picture-card"
fileList={introImgList}
onPreview={handleIntroImgPreview}
onRemove={handleIntroImgRemove}
beforeUpload={beforeIntroUpload}
// onChange={handleIntroImgChange}
>
{introImgList.length >= 1 ? null : ImgUpButton}
</Upload>
<Modal visible={introPreviewVisible} footer={null} onCancel={handleIntroImgCancel} style={{textAlign: 'center'}}>
<img alt="人物介绍图片" style={{ width: '100%' }} src={introPreviewImage} />
</Modal>
</div>
)}
</FormItem>beforeIntroUpload = (file) => {
const isLt3M = file.size / 1024 / 1024 < 3;
if (!isLt3M) { //添加文件限制
message.error('文件大小不能超过3M');
return false;
}
// console.log('file', file) handleImageUpload(file, 'image').then(res => {
this.setState({ // 存入introImgList
introImgList: [{
uid: file.uid,
status: 'done',
name: file.name,
url: `${setFileHost()+res}`,
thumbUrl: `${setFileHost()+res}`
}]
})
this.handleIntroFileThumb(res) // 存入introFileThumbs
}) return true
}
转载请注明出处
【后台管理系统】—— Ant Design Pro组件使用(一)的更多相关文章
- 【后台管理系统】—— Ant Design Pro组件使用(二)
一.关联表单项 - 动态增删输入框Input 封装子组件 class ParamsInputArray extends React.Component{ constructor(prop ...
- Ant Design Pro路由传值
Ant Design Pro 路由传值 了解Ant Design Pro组件间通讯原理的小伙伴肯定都知道,两个页面之间可以通过Models进行传值,在以往的传值过程中,我都是直接将需要的值直接一股脑的 ...
- 【后台管理系统】—— Ant Design Pro入门学习&项目实践笔记(三)
前言:前一篇记录了[后台管理系统]目前进展开发中遇到的一些应用点,这一篇会梳理一些自己学习Ant Design Pro源码的功能点.附:Ant Design Pro 在线预览地址. Dashboard ...
- Ant Design Pro (中后台系统)教程
一.概念:https://pro.ant.design/docs/getting-started-cn(官方网站) 1.Ant Design Pro 是什么: https://www.cnblogs ...
- ant design pro 当中改变ant design 组件的样式和 数据管理
ant design pro 简介 官网简介 链接 https://pro.ant.design/docs/getting-started-cn 项目结构 https://github.com/ant ...
- Ant Design Pro 学习笔记:数据流向
在讲这个问题之前,有一个问题应当讲一下: Ant Design Pro / umi / dva 是什么关系? 首先是 umi / dva 的关系. umi 是一个基于路由的 react 开发框架. d ...
- 阿里开源项目之Ant Design Pro
本篇文章主要包含的内容有三个方面. 第一.Ant Design Pro简介; 第二.Ant Design Pro能做什么; 第三.初步使用; 我相信通过这三个方面的讲解能让你大概知道Ant Desig ...
- ant design pro(一)安装、目录结构、项目加载启动【原始、以及idea开发】
一.概述 1.1.脚手架概念 编程领域中的“脚手架(Scaffolding)”指的是能够快速搭建项目“骨架”的一类工具.例如大多数的React项目都有src,public,webpack配置文件等等, ...
- Ant Design Pro快速入门
在上一篇文章中,我们介绍了如何构建一个Ant Design Pro的环境. 同时讲解了如何启动服务并查看前端页面功能. 在本文中,我们将简单讲解如何在Ant Design Pro框架下实现自己的业务功 ...
随机推荐
- The Frog's Games
The Frog's Games Problem Description The annual Games in frogs' kingdom started again. The most famo ...
- 洛谷P2507 [SCOI2008]配对 题解(dp+贪心)
洛谷P2507 [SCOI2008]配对 题解(dp+贪心) 标签:题解 阅读体验:https://zybuluo.com/Junlier/note/1299251 链接题目地址:洛谷P2507 [S ...
- Burp Suite详细使用教程-Intruder模块详3
Burp Suite使用详细教程连载的第三章.0×02 Intruder—内置有效负荷测试使用技巧内置有效负荷测试选择项如下图: 今天的小技巧使用的是 numbers,给大伙科普下:Numbers 数 ...
- 2019全国大学生数学建模竞赛(高教社杯)A题题解
文件下载:https://www.lanzous.com/i6x5iif 问题一 整体过程: 0x01. 首先,需要确定燃油进入和喷出的间歇性工作过程的时间关系.考虑使用决策变量对一段时间内燃油进入和 ...
- wordpress中page页添加非插件留言板功能
把下面的代码插入到page页面中即可 <!-- 留言板 --> <div class="wrap"> <div id="primary&qu ...
- JS 页面跳转,参数的传递
当我们通过location.replace()进行页面的跳转时,我们想进行参数的传递,当时学习的时候,以前在网上找过获取方法,已经忘记出处在哪里了.获取方法大概是这样的: 1.将参数通过拼接的方式拼接 ...
- MyBatis源码浅析
什么是MyBatis MyBatis是支持定制化SQL.存储过程以及高级映射的优秀的持久层框架.MyBatis 避免了几乎所有的 JDBC 代码和手工设置参数以及抽取结果集.MyBatis 使用简单的 ...
- UVa10426
GCD Extreme(II) Input: Standard Input Output: Standard Output Given the value of N, you will have to ...
- 用doxygen+graphviz生成函数调用流程图
https://www.jianshu.com/p/fe4b6b95dca5 注意点:由于使用到了Graphviz,所以要设置Dot选项,勾选HAVE_DOT,并设置DOT_PATH为Graphviz ...
- Flutter SDK安装(windows)
Flutter集成了Dart,因此不需要单独安装dart-sdk.Flutter的SDK可以从官网下载:https://flutter.io/sdk-archive/#windows 在Flutter ...