• Flutter For Web——一个简单的图片素材网站


    效果视频

    一个简单图片素材网站

    登录注册

    效果图

    UI

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

    初始化

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

     late final  _pageController;
      late final  _tabController;
      final List _tabs = ['登录','注册'];
    
     @override
      void 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));
      }
    
      @override
      void dispose() {
        _pageController.dispose();
        _tabController.dispose();
        super.dispose();
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    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(),
        );
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    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()
              )
            ],
          ),
        );
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    组合

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

    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
                            ],
                          ),
                        )
                      ]
                  ),
            ),
        )
        );
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    登录

    登录与注册一致,此处以登录为例子,一个简单的表单和缓存记录比对,通过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,
        );
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    按键处理

    重点在于点击事件那里,可以加一个表单验证,就是加入你输入的内容为空时,不满足上述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('登录')
            )
        );
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    通过按钮被按下后,通过键值对获取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('登录失败!');
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    SharedPreferences封装

    首先导入依赖

    shared_preferences: ^2.0.15
    
    • 1
    保存数据
      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;
        }
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    取出数据
      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;
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    清除缓冲内容
    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();
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    搜索栏

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

    效果图

    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);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    然后使用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,))
          )
        );
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

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

     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))
          ],
        );
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    首页

    效果图

    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)),
          ),
        );
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    将获取的网络图片通过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,
                  ),
                );
              }
          ),
        );
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    Dio网络请求

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

    dio: ^4.0.0
    
    • 1

    Dio单例封装

    通过懒汉单例构造Dio封装

      static var dio;
      static var dioUtils;
    
      static DioUtils get instance => getInstance();
    
      static DioUtils getInstance() {
        return dioUtils ??= DioUtils();
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    构造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;
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    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,
        );
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    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,
        );
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    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());
        }
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    使用

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

    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);
      });
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    解析Json

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

    {
        "msg":"success",
        "res":Object{...},
        "code":0
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    然后他自动生成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);
    
      @override
      String toString() {
        return jsonEncode(this);
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

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

    图片阅览

    UI

    Dialog

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

    class ImageDialog extends Dialog {
      final String imageUrl;
    
      const ImageDialog(this.imageUrl, {super.key});
    
      @override
      Widget 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,
                    ),
                  );
                })));
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    下载

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

    UI

    调用浏览器进行下载

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

    universal_html: ^1.2.1
    http: ^0.13.1
    
    • 1
    • 2

    引入依赖

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

    首先访问图片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());
        }
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    Git

    Git链接

  • 相关阅读:
    django接入djangorestframework-simplejwt步骤
    集合的练习
    差值结构表达的吸引能
    ECC(SM2) 简介及 C# 和 js 实现【加密知多少系列】
    全局解决SpringBoot框架中的application.properties/yml注解中文乱码问题(一劳永逸)
    在Linux环境下VScode中配置ROS、PCL和OpenCV开发环境记录
    钟汉良日记:出门在外靠什么?
    Teams Tab App 代码深入浅出 - 配置页面
    SeaTunnel 换maven源,解决插件下载慢
    pytorch的mask-rcnn的模型参数解释
  • 原文地址:https://blog.csdn.net/News53231323/article/details/128005652