flutter开发实战-Webview及dispose关闭背景音

news2024/11/18 11:28:10

flutter开发实战-Webview及dispose关闭背景音

当在使用webview的时候,dispose需要关闭网页的背景音或者音效。
在这里插入图片描述

一、webview的使用

在工程的pubspec.yaml中引入插件

  webview_flutter: ^4.4.2
  webview_cookie_manager: ^2.0.6
    

Webview的使用代码如下

初始化WebViewController

controller = WebViewController()
  ..setJavaScriptMode(JavaScriptMode.unrestricted)
  ..setBackgroundColor(const Color(0x00000000))
  ..setNavigationDelegate(
    NavigationDelegate(
      onProgress: (int progress) {
        // Update loading bar.
      },
      onPageStarted: (String url) {},
      onPageFinished: (String url) {},
      onHttpError: (HttpResponseError error) {},
      onWebResourceError: (WebResourceError error) {},
      onNavigationRequest: (NavigationRequest request) {
        if (request.url.startsWith('https://www.youtube.com/')) {
          return NavigationDecision.prevent;
        }
        return NavigationDecision.navigate;
      },
    ),
  )
  ..loadRequest(Uri.parse('https://flutter.dev'));
    

将WebViewController传递给WebViewWidget

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: const Text('Flutter Simple Example')),
    body: WebViewWidget(controller: controller),
  );
}
    

二、为了方便使用webview,进行封装成一个独立的widget

为了方便使用webview,进行封装成一个独立的widget

WebAppBar

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';

/// 自定义Appbar
class WebAppBar extends StatefulWidget implements PreferredSizeWidget {
  const WebAppBar(
      {Key? key,
      required this.toolbarHeight,
      this.elevation,
      this.backgroundColor,
      this.leadingWidget,
      this.trailingWidget,
      this.centerWidget,
      this.brightness,
      this.backgroundImageName})
      : super(key: key);

  final double toolbarHeight;
  final double? elevation;
  final Color? backgroundColor;
  final Widget? leadingWidget;
  final Widget? trailingWidget;
  final Widget? centerWidget;
  final Brightness? brightness;
  final String? backgroundImageName;

  @override
  // TODO: implement preferredSize
  Size get preferredSize => Size(
      ScreenUtil().screenWidth, toolbarHeight + ScreenUtil().statusBarHeight);

  @override
  State<StatefulWidget> createState() => _WebAppBarState();
}

class _WebAppBarState extends State<WebAppBar> {
  @override
  Widget build(BuildContext context) {
    final SystemUiOverlayStyle overlayStyle =
        widget.brightness == Brightness.dark
            ? SystemUiOverlayStyle.light
            : SystemUiOverlayStyle.dark;

    Widget leadingWidget = (widget.leadingWidget ?? Container());
    Widget centerWidget = (widget.centerWidget ?? Container());
    Widget trailingWidget = (widget.trailingWidget ?? Container());

    return AnnotatedRegion<SystemUiOverlayStyle>(
      //套AnnotatedRegion是为了增加状态栏控制
      value: overlayStyle,
      child: Material(
        color: Colors.transparent,
        //套Material是为了增加elevation
        elevation: widget.elevation ?? 0,
        child: Container(
          padding: EdgeInsets.symmetric(horizontal: 0.0),
          height: widget.toolbarHeight + ScreenUtil().statusBarHeight,
          decoration: BoxDecoration(
            color: widget.backgroundColor,
          ),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.center,
            children: [
              Container(
                height: ScreenUtil().statusBarHeight,
              ),
              Expanded(
                child: Container(
                  height: widget.toolbarHeight,
                  alignment: Alignment.center,
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    crossAxisAlignment: CrossAxisAlignment.center,
                    children: [
                      Container(
                        height: widget.toolbarHeight,
                        child: leadingWidget,
                      ),
                      Expanded(
                        child: Container(
                          alignment: Alignment.center,
                          height: widget.toolbarHeight,
                          child: centerWidget,
                        ),
                      ),
                      Container(
                        height: widget.toolbarHeight,
                        child: trailingWidget,
                      ),
                    ],
                  ),
                ),
              )
            ],
          ),
        ),
      ),
    );
  }
}

    

使用webview的Widget

import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

// #docregion platform_imports
// Import for Android features.
import 'package:webview_flutter_android/webview_flutter_android.dart';

// Import for iOS features.
import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart';
// #enddocregion platform_imports

class WebViewSkeleton extends StatefulWidget {
  const WebViewSkeleton({
    Key? key,
    required this.url,
    required this.onWebProgress,
    required this.onWebResourceError,
    required this.onLoadFinished,
    this.onWebTitleLoaded,
    required this.onWebViewCreated,
    this.appUserAgent,
    this.webViewUserAgent,
  }) : super(key: key);

  final String url;
  final String? appUserAgent;
  final String? webViewUserAgent;
  final Function(int progress) onWebProgress;
  final Function(WebResourceError error) onWebResourceError;
  final Function(String? url) onLoadFinished;
  final Function(String? webTitle)? onWebTitleLoaded;
  final Function(WebViewController controller) onWebViewCreated;

  @override
  State<WebViewSkeleton> createState() => _WebViewSkeletonState();
}

class _WebViewSkeletonState extends State<WebViewSkeleton> {
  // WebViewController
  late final WebViewController _webController;

  // 尝试3次,每次间隔2秒
  int _loadTitleTimes = 0;

  bool _isDisposed = false;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    _isDisposed = false;
    initWebController();
  }

  void initWebController() {
    // #docregion platform_features
    late final PlatformWebViewControllerCreationParams params;
    if (WebViewPlatform.instance is WebKitWebViewPlatform) {
      params = WebKitWebViewControllerCreationParams(
        allowsInlineMediaPlayback: true,
        mediaTypesRequiringUserAction: const <PlaybackMediaTypes>{},
      );
    } else {
      params = const PlatformWebViewControllerCreationParams();
    }

    final WebViewController controller =
        WebViewController.fromPlatformCreationParams(params);
    // #enddocregion platform_features

    controller
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..setBackgroundColor(const Color(0x00000000))
      ..setUserAgent(widget.webViewUserAgent)
      ..setNavigationDelegate(
        NavigationDelegate(
          onProgress: (int progress) {
            debugPrint('WebView is loading (progress : $progress%)');
            widget.onWebProgress(progress);
          },
          onPageStarted: (String url) {
            debugPrint('Page started loading: $url');
            // 网页开始加载
            webPageLoadedStart();
            print('onPageStarted url: $url');
          },
          onPageFinished: (String url) {
            debugPrint('Page finished loading: $url');
            // 网页加载完成
            print('onPageFinished url: $url');

            // 加载完成
            widget.onLoadFinished(url);

            // 获取网页的标题
            getWebPageTitle(url: url);
          },
          onWebResourceError: (WebResourceError error) {
            debugPrint('''
Page resource error:
  code: ${error.errorCode}
  description: ${error.description}
  errorType: ${error.errorType}
  isForMainFrame: ${error.isForMainFrame}
          ''');
            print("onWebResourceError:${error}");
            widget.onWebResourceError(error);
          },
          onNavigationRequest: (NavigationRequest request) {
            String url = Uri.decodeComponent(request.url);
            bool canNavigate = false;
            if (url.startsWith("http")) {
              canNavigate = true;
            }
            // 允许路由替换
            return canNavigate
                ? NavigationDecision.navigate
                : NavigationDecision.prevent;
          },
          onUrlChange: (UrlChange change) {
            debugPrint('url change to ${change.url}');
          },
          // onHttpAuthRequest: (HttpAuthRequest request) {
          //   openDialog(request);
          // },
        ),
      );

    // #docregion platform_features
    if (controller.platform is AndroidWebViewController) {
      AndroidWebViewController.enableDebugging(true);
      (controller.platform as AndroidWebViewController)
          .setMediaPlaybackRequiresUserGesture(false);
    }
    // #enddocregion platform_features

    _webController = controller;
    onWebViewCreated();
  }

  void onWebViewCreated() {
    print("onWebViewCreated");
    // controller.loadUrl(url);此时也可以初始化一个url
    _webController.canGoBack().then((res) {
      // 是否能返回上一级
      print("controller.canGoBack res: $res");
    });
    _webController.currentUrl().then((url) {
      // 返回当前url
      print("controller.currentUrl url: $url");
    });
    _webController.canGoForward().then((res) {
      //是否能前进
      print("controller.canGoForward res: $res");
    });

    String filePre = "file://";
    if (widget.url.startsWith(filePre)) {
      String html = widget.url.substring(filePre.length);
      DefaultAssetBundle.of(context)
          .loadString('assets/htmls/${html}')
          .then((value) => _webController?.loadHtmlString(value));
    } else {
      if (widget.url.startsWith("http://") ||
          widget.url.startsWith("https://")) {
        _webController.loadRequest(Uri.parse(widget.url), headers: {
          'Referer': widget.url,
        });
      }
    }

    widget.onWebViewCreated(_webController);
  }

  @override
  void dispose() {
    // TODO: implement dispose
    print("_WebViewSkeletonState dispose");
    _isDisposed = true;
    webControllerDispose();
    super.dispose();
  }

  Future<void> webControllerDispose() async {
    /// dispose打开空白页面,关闭音频
    String url = "about:blank";
    await _webController?.loadRequest(Uri.parse(url), headers: {

    });
    _webController?.clearCache();
    _webController?.clearLocalStorage();
  }

  void webPageLoadedStart() {
    _loadTitleTimes = 0;
  }

  Future<void> getWebPageTitle({required String url}) async {
    if (_isDisposed) {
      return;
    }

    String? title = await _webController?.getTitle();
    print("getWebPageTitle:${title}");
    if (title != null && title.isNotEmpty) {
      print("webTitle a:${title}");
      setWebPageTitle(title);
    } else {
      try {
        var result = await _webController
            ?.runJavaScriptReturningResult('window.document.title');
        print("webTitle document.url:${result}");
        if (result != null && (result is String) && result.isNotEmpty) {
          setWebPageTitle(result);
        } else {
          result = await _webController?.runJavaScriptReturningResult(
              'window.document.getElementsByTagName("title")[0]');
          print("webTitle document.getElementsByTagName:${result}");
          setWebPageTitle(result);
        }
      } catch (e) {
        print("getWebPageTitle:${e.toString()}");
        // 最多尝试三次
        if (_loadTitleTimes < 3) {
          Future.delayed(Duration(seconds: 2), () {
            _loadTitleTimes++;
            getWebPageTitle(url: url);
          });
        }
      }
    }
  }

  // 设置页面标题
  void setWebPageTitle(data) {
    if (widget.onWebTitleLoaded != null) {
      widget.onWebTitleLoaded!(data);
    }
  }

  // 返回
  void goBack() {
    _webController?.canGoBack().then((res) {
      // 是否能返回上一级
      print("controller.canGoBack res: $res");
      if (true == res) {
        _webController?.goBack();
      }
    });
  }

  // 刷新
  void reload() {
    _webController?.reload();
  }

  @override
  Widget build(BuildContext context) {
    return buildWebView(context);
  }

  Widget buildWebView(BuildContext context) {
    return WebViewWidget(
      controller: _webController,
    );
  }
}

    

使用web view的页面webviewPage

class WebViewPage extends StatefulWidget {
  const WebViewPage({
    Key? key,
    this.arguments,
  }) : super(key: key);

  final Object? arguments;

  @override
  State<WebViewPage> createState() => _WebViewPageState();
}

class _WebViewPageState extends State<WebViewPage> {
  String title = "";
  String? url;

  // WebViewController
  WebViewController? _webViewController;

  double webProgress = 0.0;

  String? webViewUserAgent;
  String? appUserAgent;
  String? webTitle;

  @override
  void initState() {
    // TODO: implement initState
    if (widget.arguments != null && widget.arguments is Map) {
      Map obj = widget.arguments as Map;
      url = obj["url"];
      webViewUserAgent = obj['webViewUserAgent'];
      appUserAgent = obj['appUserAgent'];
      webTitle = obj['webTitle'];
    }

    loggerInfo("_WebViewPageState arguments:${widget.arguments}");

    loggerInfo("_WebViewPageState url:${url}");

    super.initState();
  }

  @override
  void dispose() {
    // TODO: implement dispose
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: WebAppBar(
          toolbarHeight: 44.0,
          backgroundColor: Theme.of(context).primaryColor,
          centerWidget: Text(
            webTitle ?? title,
            textAlign: TextAlign.center,
            overflow: TextOverflow.ellipsis,
            style: TextStyle(
              fontSize: 17,
              color: ColorUtil.hexColor(0xffffff),
              fontWeight: FontWeight.w600,
              fontStyle: FontStyle.normal,
              decoration: TextDecoration.none,
            ),
          ),
          leadingWidget: Row(
            mainAxisAlignment: MainAxisAlignment.start,
            crossAxisAlignment: CrossAxisAlignment.center,
            children: [
              IconButton(
                padding: EdgeInsets.all(0.0),
                onPressed: () {
                  webViewGoBack(context);
                },
                icon: Icon(
                  Icons.arrow_back_ios,
                  color: Colors.white,
                  size: 24.0,
                ),
              ),
              IconButton(
                padding: EdgeInsets.all(0.0),
                onPressed: () {
                  navigatorBack(context);
                },
                icon: Icon(
                  Icons.close_rounded,
                  color: Colors.white,
                  size: 30.0,
                ),
              ),
            ],
          ),
          trailingWidget: Row(
            mainAxisAlignment: MainAxisAlignment.end,
            crossAxisAlignment: CrossAxisAlignment.center,
            children: [
              SizedBox(
                width: 28.0,
              ),
              IconButton(
                padding: EdgeInsets.all(0.0),
                onPressed: () {
                  webViewReload();
                },
                icon: Icon(
                  Icons.refresh_outlined,
                  color: Colors.white,
                  size: 28.0,
                ),
              ),
            ],
          ),
        ),
        body: Stack(
          children: [
            WebViewSkeleton(
              url: url ?? "",
              onWebResourceError: (WebResourceError error) {
                if (mounted) {
                  // TODO onWebResourceError
                }
              },
              onWebProgress: (int progress) {
                if (mounted) {
                  // TODO onWebProgress
                  double precent = progress / 100.0;
                  if (precent > 1.0) {
                    precent = 1.0;
                  }

                  if (precent < 0.0) {
                    precent = 0.0;
                  }

                  setState(() {
                    webProgress = precent;
                    loggerInfo("webProgress:${webProgress}");
                  });
                }
              },
              onLoadFinished: (String? url) {
                if (mounted) {
                  // TODO onLoadFinished
                }
              },
              onWebTitleLoaded: (String? webTitle) {
                if (mounted) {
                  String? aWebTitle;
                  if ('""' != webTitle) {
                    aWebTitle = webTitle;
                  }
                  setState(() {
                    title = aWebTitle ?? "";
                  });
                }
              },
              onWebViewCreated: (WebViewController controller) {
                _webViewController = controller;
              },
            ),
            buildProgressIndicator(context),
          ],
        ),
    );
  }

  Widget buildProgressIndicator(BuildContext context) {
    return (webProgress != 1.0)
        ? LinearProgressIndicator(
            backgroundColor: Colors.transparent,
            valueColor: AlwaysStoppedAnimation(ColorUtil.hexColor(0x3b93ff)),
            value: webProgress,
            minHeight: 2,
          )
        : Container();
  }

  void navigatorBack(BuildContext context) {
    Navigator.of(context).pop();
  }

  void webViewGoBack(BuildContext context) {
    _webViewController?.canGoBack().then((res) {
      // 是否能返回上一级
      loggerInfo("controller.canGoBack res: $res");
      if (true == res) {
        _webViewController?.goBack();
      } else {
        navigatorBack(context);
      }
    });
  }

  void webViewReload() {
    _webViewController?.reload();
  }
}
    

三、解决dispose关闭背景音乐

解决dispose关闭背景音乐的问题,当widget被dispose的时候,我们可以通过加载一个空白页面,来实现这个关闭背景音乐。
加载空白

代码如下

@override
  void dispose() {
    // TODO: implement dispose
    print("_WebViewSkeletonState dispose");
    _isDisposed = true;
    webControllerDispose();
    super.dispose();
  }

  Future<void> webControllerDispose() async {
    /// dispose打开空白页面,关闭音频
    String url = "about:blank";
    await _webController?.loadRequest(Uri.parse(url), headers: {

    });
    _webController?.clearCache();
    _webController?.clearLocalStorage();
  }
    

四、小结

flutter开发实战-Webview及dispose关闭背景音

学习记录,每天不停进步。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1890950.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

UiPath+Appium实现app自动化测试

一、环境准备工作 1.1 完成appium环境的搭建 参考&#xff1a;pythonappiumpytestallure模拟器(MuMu)自动化测试环境搭建_appium mumu模拟器-CSDN博客 1.2 完成uipath的安装 登录官网&#xff0c;完成注册与软件下载安装。 UiPath业务自动化平台&#xff1a;先进的RPA及自动…

Linux操作系统学习:day08

内容来自&#xff1a;Linux介绍 视频推荐&#xff1a;Linux基础入门教程-linux命令-vim-gcc/g -动态库/静态库 -makefile-gdb调试 目录 day0853、命令和编辑模式之间的切换54、命令模式到末行模式的切换与末行模式下的保存退出命令模式到末行模式的切换保存退出 55、末行模式…

大模型训练优化方法

写在前面 在训练模型尤其是大模型的时候&#xff0c;如何加快训练速度以及优化显存利用率是一个很关键的问题。本文主要参考HF上的一篇文章&#xff1a;https://huggingface.co/docs/transformers/perf_train_gpu_one&#xff0c;以及笔者在实际训练中的一些经验&#xff0c;给…

SpringBoot 整合 Minio 实现文件切片极速上传技术

Centos7安装Minio 创建目标文件夹 mkdir minio使用docker查看目标镜像状况 大家需要注意&#xff0c;此处我们首先需要安装docker&#xff0c;对于相关安装教程&#xff0c;大家可以查看我之前的文章&#xff0c;按部就班就可以&#xff0c;此处不再赘述&#xff01;&#x…

【电商指标详解】

前言&#xff1a; &#x1f49e;&#x1f49e;大家好&#xff0c;我是书生♡&#xff0c;本篇文章主要和大家分享一下电商行业中常见指标的详解&#xff01;存在的原因和作用&#xff01;&#xff01;&#xff01;希望对大家有所帮助。 &#x1f49e;&#x1f49e;代码是你的画…

论文学习笔记1:Federated Graph Neural Networks: Overview, Techniques, and Challenges

文章目录 一、introduction二、FedGNN术语与分类2.1主要分类法2.2辅助分类法 三、GNN-ASSISTED FL3.1Centralized FedGNNs3.2Decentralized FedGNNs 四、FL-ASSISTED GNNS4.1horizontal FedGNNs4.1.1Clients Without Missing Edges4.1.1.1Non-i.i.d. problem4.1.1.2Graph embed…

Navicat和MySQL的安装

1、下载 Navicat Navicat 官网&#xff1a;www.navicat.com.cn/ 在产品中可以看到很多的产品&#xff0c;点击免费试用 Navicat Premium 即可&#xff0c;是一套多连数据库开发工具&#xff0c;其他的只能连接单一类型数据库 点击试用 选择系统直接下载 二、安装 Navicat 安…

天诚长租公寓智能门锁管理解决方案

人才是区域创新发展的第一资源&#xff0c;如何解决人才的住房问题&#xff0c;让人才“流进来”、“留下来”、“融进来”&#xff0c;就需要优先安排优质人才公寓、人才优租房和公共租赁住房房源&#xff0c;并为青年人才群体提供智能化、信息化的租住体验及通行服务。 一、…

Hive查询优化 - 面试工作不走弯路

引言&#xff1a;Hive作为一种基于Hadoop的数据仓库工具&#xff0c;广泛应用于大数据分析。然而&#xff0c;由于其依赖于MapReduce框架&#xff0c;查询的性能可能会受到影响。为了确保Hive查询能够高效运行&#xff0c;掌握查询优化技巧至关重要。在日常工作中&#xff0c;高…

实习总结 --- 其他业务

一. 回归测试&#xff1a;回归测试与测新是对应的&#xff0c;当需求准入交付测试的时候首先要进行的就是测新&#xff0c;也就是对新功能对测试&#xff0c;一般是在sim环境下测试的&#xff1b;当测新通过后才会进行回归测试&#xff0c;回归测试的目的是为了保证老功能的正确…

程序算法设计分析

动态规划和分治、贪心相比有什么区别&#xff1f;各自的优缺点&#xff1f; 分治算法特征&#xff1a; 1&#xff09;规模如果很小&#xff0c;则很容易解决。//一般问题都能满足 2&#xff09;大问题可以分为若干规模小的相同问题。//前提 3&#xff09;利用子问题的解&#x…

订单服务-提交订单业务立即购买业务

文章目录 1、提交订单 业务2、在 OrderController 创建 submitOrder 方法3、 在 OrderServiceImpl 中实现 submitOrder 方法4、根据id查询sku详情&#xff08;service-product"&#xff09;5、查询用户地址保存到订单项中&#xff08;service-user&#xff09;6、删除购物…

从.mat文件中导入数据到simulink进行FFT分析

1. 在matlab中准备数据 .mat 文件中包含时间向量和需要分析的数据 load(fcssiabc061302.mat);提取时间和需要分析的数据 time fcssiabc061302.X.Data; % 时间向量 signal fcssiabc061302.Y(1).Data; % A相电流数据 将数据转换为“structure with time”格式…

Redis(十八) 分布式锁

文章目录 前言什么是分布式锁分布式锁的基本实现引入过期时间引入校验 id引入 lua 脚本引入 watch dog&#xff08;看门狗&#xff09;引入 Redlock 算法 前言 在使用 redis 作为中间件的时候&#xff0c;如果使用单机部署的话&#xff0c;如果这个机器故障的话&#xff0c;那…

优先级队列(堆)学的好,头发掉的少(Java版)

本篇会加入个人的所谓鱼式疯言 ❤️❤️❤️鱼式疯言:❤️❤️❤️此疯言非彼疯言 而是理解过并总结出来通俗易懂的大白话, 小编会尽可能的在每个概念后插入鱼式疯言,帮助大家理解的. &#x1f92d;&#x1f92d;&#x1f92d;可能说的不是那么严谨.但小编初心是能让更多人…

取证与数据恢复:冷系统分析,实时系统分析与镜像分析之间的过渡办法

天津鸿萌科贸发展有限公司是 ElcomSoft 系列取证软件的授权代理商。 ElcomSoft 系列取证软件 ElcomSoft 系列取证软件支持从计算机和移动设备进行数据提取、解锁文档、解密压缩文件、破解加密容器、查看和分析证据。 计算机和手机取证的完整集合硬件加速解密最多支持10,000计…

arduino IDE 处于read only editor模式

当我们浏览一些arduino的例子的时候&#xff0c;有时候想修改这些例子。但是这些例子即使另存到自己的文件目录下&#xff0c;仍然不能修改&#xff0c;提示处于read only 模式。 网上有一些什么说法&#xff0c;说要设置什么之类的&#xff0c;当我们点开之后&#xff0c;好像…

13-4 GPT-5:博士级AI,人工智能的新时代

图片来源&#xff1a;AI Disruptive 人工智能世界正在迅速发展&#xff0c;新的创新和突破层出不穷。在本文中&#xff0c;我们将深入探讨最新的进展&#xff0c;从即将推出的 GPT-5 模型到 Apple 和 Meta 之间可能的合作。 GPT-5&#xff1a;博士级别的人工智能 虽然尚未正…

Windows Server 2008近源应急OS-1

前景需要&#xff1a;小王从某安全大厂被优化掉后&#xff0c;来到了某私立小学当起了计算机老师。某一天上课的时候&#xff0c;发现鼠标在自己动弹&#xff0c;又发现除了某台电脑&#xff0c;其他电脑连不上网络。感觉肯定有学生捣乱&#xff0c;于是开启了应急。 我们需要…

微信小程序 typescript 开发日历界面

1.界面代码 <view class"o-calendar"><view class"o-calendar-container" ><view class"o-calendar-titlebar"><view class"o-left_arrow" bind:tap"prevMonth">《</view>{{year}}年{{month…