Flutter For Web——一个简单的图片素材网站
创始人
2024-02-10 04:10:43
0

一个简单的图片素材网站

  • 效果视频
  • 登录注册页
    • 效果图
    • UI
      • 初始化
      • TabBar
      • PageView
      • 组合
      • 登录
        • 账号输入
        • 按键处理
      • SharedPreferences封装
        • 保存数据
        • 取出数据
        • 清除缓冲内容
  • 搜索栏
    • 效果图
    • UI
  • 首页
    • 效果图
    • UI
  • Dio网络请求
    • Dio单例封装
    • 构造Dio对象
    • Get
    • Post
    • Response
    • 使用
    • 解析Json
  • 图片阅览
    • UI
    • Dialog
  • 下载
    • UI
    • 调用浏览器进行下载
  • Git

效果视频

一个简单图片素材网站

登录注册页

效果图

UI

登录和注册页滑动切换使用的是TabBar+PageView完成

初始化

首先初始化TabBar和PageView控制器,并为其添加切换监听事件

 late final  _pageController;late final  _tabController;final List _tabs = ['登录','注册'];@overridevoid initState() {_pageController = PageController();_tabController = TabController(length: _tabs.length, vsync: this);super.initState();}void _changeTab(int index) {_pageController.animateToPage(index, duration: const Duration(milliseconds: 300), curve: Curves.ease);}void _onPageChanged(int index) {_tabController.animateTo(index, duration: const Duration(milliseconds: 300));}@overridevoid dispose() {_pageController.dispose();_tabController.dispose();super.dispose();}

TabBar

TabBar的使用方法如下,重点就是点击事件和控制器的绑定

Widget navBar = TabBar(//选中的颜色labelColor: Colors.white,labelStyle: const TextStyle(color: Colors.white, fontSize: 16),//未选中的颜色unselectedLabelColor: Colors.black,unselectedLabelStyle: const TextStyle(color: Colors.black, fontSize: 16),//去掉下划线indicator: const BoxDecoration(),controller: _tabController,onTap: _changeTab,tabs: _tabs.map((e) => Tab(text: e)).toList(),);

PageView

PageView的使用方法如下,重点就是页面切换事件和控制器的绑定,它的子组件就由需要滑动的页面组成,这里这样登录和注册两个

Widget navViews = SizedBox(width: 500.0,height: 320.0,child: PageView(controller: _pageController,onPageChanged: _onPageChanged,children: [ConstrainedBox(constraints: const BoxConstraints.expand(),child: const LoginPage()),ConstrainedBox(constraints: const BoxConstraints.expand(),child: const RegisterPage())],),);

组合

最外层插背景图片,并铺满全屏,下面就控制登录、注册界面在屏幕左方

return Scaffold(body: Container(decoration: bg,width:  double.infinity,height: double.infinity,child:Align(alignment: Alignment.centerLeft,child: Wrap(children:[Container(margin: const EdgeInsets.only(left: 100.0),child: Column(children: [Container(width: 200.0,decoration: gradient,child: navBar,),const SizedBox(height: 10.0),navViews],),)]),),));

登录

登录与注册一致,此处以登录为例子,一个简单的表单和缓存记录比对,通过shared_preferences这个库对注册数据进行缓存,然后登录进行读取,从而进行判断

账号输入

账号和密码差不多,以账号为例子;同样绑定控制器和焦点节点,在尾部添加一个清空文本按钮,当内容不为空时出现,反之,隐藏;validator里面为不满足你所设置的条件,则下方弹出一行提示(内容自定义),基本Material风格都这样设计的

///用户名Widget username_input = TextFormField(maxLines: 1,controller: _usernameController,focusNode: _focusNodeUserName,decoration: InputDecoration(icon:const Icon(Icons.people_alt_outlined),labelText: '账号',suffixIcon: (_isShowClear)? IconButton(icon: const Icon(Icons.clear),onPressed: () {// 清空文本框的内容_usernameController.clear();}): null),validator: (value) {if(value == null || value.isEmpty){return '用户名不能为空';}else{return null;}},onSaved: (String? data) {_username = data.toString();},autovalidateMode: AutovalidateMode.onUserInteraction,);

按键处理

重点在于点击事件那里,可以加一个表单验证,就是加入你输入的内容为空时,不满足上述validator所设置的条件,就可以不执行方法体的内容,因为dart判空机制,所以前面需要加一个

 ///登录按钮Widget loginButton = Container(width: 150.0,height: 40.0,decoration: gradient,child:ElevatedButton(style: ButtonStyle(//去除阴影elevation: MaterialStateProperty.all(0),//将按钮背景设置为透明backgroundColor: MaterialStateProperty.all(Colors.transparent),),onPressed: () {if (_formKey.currentState!.validate()) {_formKey.currentState!.save();login(_username, _password,context);//testDio();}},child: const Text('登录')));

通过按钮被按下后,通过键值对获取SharedPreferences的缓存内容,然后与输入的进行判断,并通过Toast进行提示

void login(String username,String password,BuildContext context) async{String? _username = await SpUtil.getValue('username');String? _password = await SpUtil.getValue('password');if (_username == username && _password == password) {print('[成功信息]:登录成功');showSuccessToast('登录成功!');Navigator.of(context).push(MaterialPageRoute(builder: (context) => const HomePage()));} else {print('[错误信息]:登录失败');showFailedToast('登录失败!');}
}

SharedPreferences封装

首先导入依赖

shared_preferences: ^2.0.15

保存数据

  static setValue(String key, T value) async {SharedPreferences prefs = await SharedPreferences.getInstance();switch (T) {case String:prefs.setString(key, value as String);break;case int:prefs.setInt(key, value as int);break;case bool:prefs.setBool(key, value as bool);break;case double:prefs.setDouble(key, value as double);break;}}

取出数据

  static Future getValue(String key) async {SharedPreferences prefs = await SharedPreferences.getInstance();late T res;switch (T) {case String:res = prefs.getString(key) as T;break;case int:res = prefs.getInt(key) as T;break;case bool:res = prefs.getBool(key) as T;break;case double:res = prefs.getDouble(key) as T;break;}return res;}

清除缓冲内容

static void removeCache(String key) async{SharedPreferences sp = await SharedPreferences.getInstance();sp.remove(key);}static void removeAllCache() async{SharedPreferences sp = await SharedPreferences.getInstance();sp.clear();}

搜索栏

因为我没有找到好的图片素材接口,那个接口没有通过关机键搜索然后返回内容的接口,所以此处搜索栏没有使用通过搜索内容跳转相关内容的页面,无论输入什么都跳转至全览页

效果图

UI

一个背景图加一个Colum布局
搜索条通过InputDecoration包含了一个尾部跳转按钮,border: InputBorder.none此句可以去除输入框下划线

var inputStyle = InputDecoration(suffixIcon: IconButton(onPressed: (){Navigator.of(context).push(MaterialPageRoute(builder: (context) => const AllImage()));},icon: const Icon(Icons.g_mobiledata_outlined)),icon:const Icon(Icons.search),hintText: 'Search for all image',border: InputBorder.none);

然后使用Card布局,在添加一点间距,圆角角度加大一点,就完成了一个搜索条

/// 搜索框Widget searchBar = Container(height: 60.0,width: 800.0,//padding: const EdgeInsets.all(20.0),child: Card(shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(30.0))),color: Colors.white,child:  Container(alignment: Alignment.center,margin: const EdgeInsets.only(left: 20.0),child:TextField(decoration: inputStyle,maxLines: 1,))));

上图效果最后由如下代码组装

 Widget topList = Column(mainAxisAlignment: MainAxisAlignment.center,crossAxisAlignment: CrossAxisAlignment.start,children: [Text(first_line_text,style: getTextStyle(28.0, FontWeight.bold, Colors.white)),const SizedBox(height: 20.0,),Text(second_line_text,style: getTextStyle(14.0, FontWeight.bold, Colors.white)),const SizedBox(height: 20.0,),searchBar,const SizedBox(height: 20.0,),Text(third_line_text,style: getTextStyle(14.0, FontWeight.bold, Colors.white))],);

首页

效果图

UI

此处只是简单使用GridView进行图片内容展示

  Widget bottomArea = Container(height: 750,margin: const EdgeInsets.only(left: 100.0,right: 100.0),child: GridView.count(physics: const NeverScrollableScrollPhysics(),crossAxisCount: 6,mainAxisSpacing: 20.0,crossAxisSpacing: 20.0,childAspectRatio: 0.7,children: List.generate(imageList.length, (index) => getImageChile(imageList[index],context)),),);

将获取的网络图片通过Image.network进行展示,并设置未显示时,显示loading样式的progress占位,并使用GestureDetector为图片添加点击事件

GestureDetector getImageChile(ImageBeanEntity entity,BuildContext context){return GestureDetector(onTap: (){DialogUtil.showImageDialog(context, entity.img);},child: Image.network(entity.img,errorBuilder: (context,error,stackTrace){return const CircularProgressIndicator();},loadingBuilder: (context,child,progress){if(progress == null)return child;return Container(alignment: Alignment.center,child: CircularProgressIndicator(value: progress.expectedTotalBytes != null ?progress.cumulativeBytesLoaded / progress.expectedTotalBytes! : null,),);}),);}

Dio网络请求

本例通过Dio库进行网络请求访问,添加如下依赖

dio: ^4.0.0

Dio单例封装

通过懒汉单例构造Dio封装

  static var dio;static var dioUtils;static DioUtils get instance => getInstance();static DioUtils getInstance() {return dioUtils ??= DioUtils();}

构造Dio对象

其中baseUrl为接口前缀,例如http://172.0.0.1/?limit=12这个示例接口,其中http://172.0.0.1就为baseUrl,此处只做为示例,具体根据开发需求和自己喜好,也可动态配置

  // 创建 dio 实例对象static Dio createInstance() {if (dio == null) {/// 全局属性:请求前缀、连接超时时间、响应超时时间var options = BaseOptions(// responseType: ResponseType.json,baseUrl: ApiPath.baseUrl,connectTimeout: _connectTimeout,receiveTimeout: _receiveTimeout,sendTimeout: _sendTimeout,);dio = Dio(options);}return dio;}// 清空 dio 对象static clear() {dio = null;}

Get

通过传入接口和参数,然后将请求结果通过回调函数进行回调,此处指定为Get方式请求

get(String url, FormData? param, Function(T t) onSuccess, Function(String error) onError) async {requestHttp(url,param: param,method: GET,onSuccess: onSuccess,onError: onError,);}

Post

与Get方法一样,此处不在阐述

post(String url, FormData param, Function(T t) onSuccess, Function(String error) onError) async {requestHttp(url,param: param,method: POST,onSuccess: onSuccess,onError: onError,);}

Response

此处建立dio对象,然后进行网络请求,最后将response.dataJson字符串进行回调,若是失败则走失败回调

 static requestHttp(String url, {param, method, required Function(T map) onSuccess, required Function(String error) onError,}) async {dio = createInstance();try {Response response = await dio.request(url,data: param,options: Options(method: method));if (response.statusCode == 200) {onSuccess(response.data);} else {onError("【statusCode】${response.statusCode}");}} on DioError catch (e) {/// 打印请求失败相关信息print("【请求出错1】${e.toString()}");onError(e.toString());}}

使用

此处处理网络请求返回回来的数据

void getImageData(Function(List t) onSuccess, Function(String error) onError){DioUtils.instance.get(ApiPath.verticalUrl, null, (data){var baseBean = BaseImageEntityEntity.fromJson(data as Map);var verticalList = VerticalEntityEntity.fromJson(baseBean.res as Map);onSuccess(verticalList.vertical);},(error){print("【请求失败】${error.toString()}");showFailedToast('failed!');onError(error);});
}

解析Json

使用的是一个JsonToDartBeanAction插件进行解析,只需要通过输入需要解析的Json串,他会自动生成bean类和转换类
以此类为例,我传入的JSON串如下

{"msg":"success","res":Object{...},"code":0
}

然后他自动生成Bean类以及JSON解析和转换类

@JsonSerializable()
class BaseImageEntityEntity {late String msg;dynamic res;late int code;BaseImageEntityEntity();factory BaseImageEntityEntity.fromJson(Map json) => $BaseImageEntityEntityFromJson(json);Map toJson() => $BaseImageEntityEntityToJson(this);@overrideString toString() {return jsonEncode(this);}
}

这些都是它自动生成的,你每创建Bean类,它就会多一个对应的解析类,然后将添加到convert文件中

图片阅览

UI

Dialog

通过继承Dialog组件实现自定义,通过通过GestureDetector组件为图片添加点击事件,点击区域外部可取消Dialog,使用 Navigator.pop(context);也可以取消当前Dialog

class ImageDialog extends Dialog {final String imageUrl;const ImageDialog(this.imageUrl, {super.key});@overrideWidget build(BuildContext context) {return Container(width: double.infinity,height: double.infinity,margin: const EdgeInsets.all(100.0),padding: const EdgeInsets.all(50.0),decoration: const BoxDecoration(color: Color(0x66000000),borderRadius: BorderRadius.all(Radius.circular(15.0))),child: GestureDetector(onTap: () {// downloadImage();DialogUtil.showDownloadDialog(context, imageUrl);},child: Image.network(imageUrl,errorBuilder: (context, error, stackTrace) {return const CircularProgressIndicator();}, loadingBuilder: (context, child, progress) {if (progress == null) return child;return Container(alignment: Alignment.center,child: CircularProgressIndicator(value: progress.expectedTotalBytes != null? progress.cumulativeBytesLoaded /progress.expectedTotalBytes!: null,),);})));}
}

下载

下载Dialog与上述无异,此处滤过

UI

调用浏览器进行下载

此处下载功能通过调用原生htmla标签进行下载,但是需要引入一个库,前者是使用html的库,后者是使用http的库

universal_html: ^1.2.1
http: ^0.13.1

引入依赖

import 'package:universal_html/html.dart' as html;
import 'package:http/http.dart' as http;

首先访问图片URL,然后将其进行编码,最后使用html.AnchorElement创建html标签,然后以当前时间为下载完成图片的名字

 void downloadImage() async {try {final http.Response response = await http.get(Uri.parse(imageUrl));final data = response.bodyBytes;final base64data = base64Encode(data);final a = html.AnchorElement(href: 'data:image/jpeg;base64,$base64data');String imageName = 'download_in_${DateTime.now().toString()}.png';a.download = imageName;a.click();a.remove();} catch (e) {print(e.toString());}}

Git

Git链接

相关内容

热门资讯

喜欢穿一身黑的男生性格(喜欢穿... 今天百科达人给各位分享喜欢穿一身黑的男生性格的知识,其中也会对喜欢穿一身黑衣服的男人人好相处吗进行解...
发春是什么意思(思春和发春是什... 本篇文章极速百科给大家谈谈发春是什么意思,以及思春和发春是什么意思对应的知识点,希望对各位有所帮助,...
网络用语zl是什么意思(zl是... 今天给各位分享网络用语zl是什么意思的知识,其中也会对zl是啥意思是什么网络用语进行解释,如果能碰巧...
为什么酷狗音乐自己唱的歌不能下... 本篇文章极速百科小编给大家谈谈为什么酷狗音乐自己唱的歌不能下载到本地?,以及为什么酷狗下载的歌曲不是...
华为下载未安装的文件去哪找(华... 今天百科达人给各位分享华为下载未安装的文件去哪找的知识,其中也会对华为下载未安装的文件去哪找到进行解...
家里可以做假山养金鱼吗(假山能... 今天百科达人给各位分享家里可以做假山养金鱼吗的知识,其中也会对假山能放鱼缸里吗进行解释,如果能碰巧解...
四分五裂是什么生肖什么动物(四... 本篇文章极速百科小编给大家谈谈四分五裂是什么生肖什么动物,以及四分五裂打一生肖是什么对应的知识点,希...
怎么往应用助手里添加应用(应用... 今天百科达人给各位分享怎么往应用助手里添加应用的知识,其中也会对应用助手怎么添加微信进行解释,如果能...
客厅放八骏马摆件可以吗(家里摆... 今天给各位分享客厅放八骏马摆件可以吗的知识,其中也会对家里摆八骏马摆件好吗进行解释,如果能碰巧解决你...
美团联名卡审核成功待激活(美团... 今天百科达人给各位分享美团联名卡审核成功待激活的知识,其中也会对美团联名卡审核未通过进行解释,如果能...