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

效果视频

一个简单图片素材网站

登录注册页

效果图

UI

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

初始化

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

 late final  _pageController;
  late final  _tabController;
  final List<String> _tabs = <String>['登录','注册'];

 @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();
  }

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<String>('username');
  String? _password = await SpUtil.getValue<String>('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<T>(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<T> getValue<T>(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<T>(String url, FormData? param, Function(T t) onSuccess, Function(String error) onError) async {
    requestHttp<T>(
      url,
      param: param,
      method: GET,
      onSuccess: onSuccess,
      onError: onError,
    );
  }

Post

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

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

Response

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

 static requestHttp<T>(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<ImageBeanEntity> t) onSuccess, Function(String error) onError){
  DioUtils.instance.get(ApiPath.verticalUrl, null, (data){
    var baseBean = BaseImageEntityEntity.fromJson(data as Map<String, dynamic>);
    var verticalList = VerticalEntityEntity.fromJson(baseBean.res as Map<String, dynamic>);
    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<String, dynamic> json) => $BaseImageEntityEntityFromJson(json);

  Map<String, dynamic> toJson() => $BaseImageEntityEntityToJson(this);

  @override
  String 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});

  @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,
                ),
              );
            })));
  }
}

下载

下载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链接

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

FranzLiszt1847

嘟嘟嘟嘟嘟

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值