Flutter入门二: 熟悉Widget、文字样式、ListView

Flutter入门 学习大纲

上一节,我们完成了Flutter的环境搭建。本节,我们开始搭建项目,简单了解Flutter及其基础组件

  1. 项目创建
    1.1 命令行创建
    1.2 Android Studio创建
  2. 熟悉工程
    2.1 简单实现
    2.2 熟悉widget
    2.3 Text
    2.4 MaterialApp
    2.5 ListView 列表视图
  3. 常用基础组件
    3.1 基础文本
    3.2 富文本
    3.3 基础容器Container

1. 项目创建

1.1 命令行创建

以前不支持 驼峰写法,需要通过小写字母+下划线_命名,但是现在支持

flutter create flutter_demo
  • 创建成功
image.png
  • 按照指令,到指定文件夹运行项目
cd FlutterDemo
flutter run

注意:

  • 如果此时未打开模拟器,会提示需要选择一个模拟器
  • 如果此时打开了多个模拟器,也会提示您选中一个模拟器运行
    image.png
  • 选中模拟器后,flutter会自动使用Xcode工具进行编译
    image.png
  • 如果需要真机调试,我们需要手动打开项目工程,去Xcode配置证书

终端运行Flutter命令键

Flutter run key commands.
r Hot reload. 🔥🔥🔥 热重载(比对被修改部分,更新被修改代码)
R Hot restart.      热重启(所有资源重新加载)
h Repeat this help message.
d Detach (terminate "flutter run" but leave application running).
c Clear the screen
q Quit (terminate the application on the device).
s Save a screenshot to flutter.png.
b Toggle the platform brightness setting (dark and light mode).                   (debugBrightnessOverride)
w Dump widget hierarchy to the console.                                                      (debugDumpApp)
t Dump rendering tree to the console.                                                 (debugDumpRenderTree)
L Dump layer tree to the console.                                                      (debugDumpLayerTree)
S Dump accessibility tree in traversal order.                                          (debugDumpSemantics)
U Dump accessibility tree in inverse hit test order.                                   (debugDumpSemantics)
i Toggle widget inspector.                                         (WidgetsApp.showWidgetInspectorOverride)
I Toggle oversized image inversion 🖼️.                                        (debugInvertOversizedImages)
p Toggle the display of construction lines.                                         (debugPaintSizeEnabled)
o Simulate different operating systems.                                             (defaultTargetPlatform)
z Toggle elevation checker.
g Run source code generators.
M Write SkSL shaders to a unique file in the project directory.
v Launch DevTools.
P Toggle performance overlay.                                           (WidgetsApp.showPerformanceOverlay)
a Toggle timeline events for all widget build methods.
  • 如果使用安卓模拟器,可以在顶部控制栏配置模拟器
    image.png
  • 选中模拟器,运行:


    image.png
  • 由于个人习惯,我更喜欢Xcode开发,在终端使用命令运行。执行flutter run时,会让我们选择想使用的模拟器(后续我主要使用iPhone模拟器)。

1.2 Android Studio创建

  • 除了使用flutter create命令创建项目,我们还可使用Android Studio创建,也可以使用VSCode创建,它们都有flutter插件
  • 啥? 你问Xcode是否可以创建?
    想想苹果谷歌的竞争者关系,就知道苹果不可能做这样的支持插件的。😂

1.2.1 使用Android Studio创建

image.png
  • 四种创建方式:
image.png
  • 项目基本信息

    image.png

  • 项目唯一标识支持平台

    image.png

  • Finish后,会进行网络请求拉取资源,创建成功。
    (如果没配置镜像,拉取资源非常缓慢上一节有介绍如何配置镜像

    image.png

  • 坑点
    如果Android Studio 正在运行项目,我们command + Q强制退出。下次打开Android Studio时,会回到当时的缓存。如果缓存成功找回,会运行正常。如果缓存找不到,会导致无法运行,而且新建工程无法运行

  • 处理方式:
    删除flutter目录下cache缓存文件夹中的lockfile文件,再运行项目即可。(相当于XCodeClean操作)

# pwd请替换为自己flutter的文件目录
rm /pwd/flutter/bin/cache/lockfile

2. 熟悉工程

  • 创建项目后,可以看到main.dart很多代码。我们最快熟悉的方式是: 全部删除手动实现分析
    image.png

2.1 简单实现

Flutter中,我们使用的开发语言Dart,现在我们先体验,完整的Dart语法,我们可以去官网了解

  • 手动实现:
// 以下代码,均使用Dart语言编写

// 资源包  (可以理解为我们iOS的UIKit)
import 'package:flutter/material.dart';

// main入口函数  (就像iOS main.m中的main函数)
void main() {
  // runApp(app); // runApp就像iOS的 UIApplication, 而app就像我们的根控制器
  // flutter中没有控制器和视图的概念,都是widget组件。
  runApp(
    Center(  //我们用Center部件,自动居中展示。这是Center的构造函数
      // child就像subView
      // Text是一种文本框样式,设置默认值和显示方向(ltf: left to right 从左到右)
      child: Text('Hello', textDirection: TextDirection.ltr,),
    )
  );
}
  • flutter运行,文本框成功展示
    image.png

2.2 熟悉widget

Widget: 作用类似于OC中的UIView,是小部件。分为两大类:

  • StatelessWidget:无状态组件。快捷键stless
    创建后决定了样式,状态更改需要手动创建新widget

  • StatefulWidget: 有状态组件。快捷键stful
    本质上也是无状态组件渲染无状态,因为UI本身无状态的。但它会保留组件状态,记录组件状态的属性改变时,会自动重新渲染。直到该组件完全销毁,才会释放记录的属性

2.2.1 无状态组件(StatelessWidget)
  • 我们将main入口的Center组件改成自定义MyWidget无状态组件。
import 'package:flutter/material.dart';

void main() {
  runApp(
    MyWidget(), //()是构造函数,类似C++。
  );
}

// 无状态Widget快捷键是 stless
// 一个Widget,就是一个class类
class MyWidget extends StatelessWidget {
  @override
  // build:会将小部件放到渲染树中去
  // 渲染树: 会从main入口的第一个widget开始渲染,然后以树结构依次向下渲染组件
  // 所有widget都必须有build方法,build返回的是什么,MyWidget在外界渲染的就是什么
  Widget build(BuildContext context) {
    return Center(
      child: Text(
        'Hello Flutter2',
        textDirection: TextDirection.ltr,
      ),
    );
  }
}
  • 选择模拟器debug运行,模拟器启动后,可以修改文本内容,点击热重载按钮,感受热重载的强大
image.png
  • reload热重载功能,是通过比较新旧代码变化,来更新被改动部分代码的。 但是有些场景热重载失败的,只能restart重启才可以。

具体场景,参考Flutter官网介绍

Dart语言简写:

  • Dart语言中,如果函数只有一行内容时,可以使用=>缩写:
// 改动前
//void main() {
//  runApp(MyWidget());
//}
// 简写:
void main() => runApp(MyWidget()); 

2.3 Text

  • Text是一个文本组件,是StatelessWidget不可变组件。
2.3.1 构造函数与参数
  • Text为例,构造函数是Text(),包含必选参数可选值,可选值可以赋默认值
    image.png
2.3.2 final和const修饰符

finalconst都类似于Swiftlet,是不可变的。

  • final可以不赋初始值,运行时赋值
  • const必须在创建时就赋值

比如Text使用频率最高styletextAlign,都是final声明,因为Text本身是StatelessWidget不可变的组件。

image.png

  • Text加入样式,使用变量创建TextStyle_下划线表示私有变量
import 'package:flutter/material.dart';

// 入口,展示MyWidget组件
void main() => runApp(MyWidget());

class MyWidget extends StatelessWidget {
  @override
  // build 确定组件返回的内容
  Widget build(BuildContext context) {

    // final创建一个_textStyle不可变变量,_开头的属性,表示私有属性
    final _textStyle = TextStyle(color: Colors.red, fontSize: 40.0, fontWeight: FontWeight.bold);

    return Center(
      child: Text(
        'Hello Flutter',
        textDirection: TextDirection.ltr,
        style: _textStyle, // 直接使用变量
      ),
    );
  }
}

TextTextStyle相关属性参数,可以Command + 鼠标左键查看

image.png

2.4 MaterialApp

  • MaterialApp:Flutter推荐方式,提供快速构建APP的方式(包括导航栏内容主题等)
  • home: 类似于根控制器,需要指定一个widget
    Scaffold: 类似于导航控制器NavigationController,包含导航栏相关属性。是一个可变组件statefulWidget
    appBar:导航栏
    body: 内容
  • theme主题,可控制主题色
import 'package:flutter/material.dart';

// 入口,展示MyWidget组件
void main() => runApp(App());

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // MaterialApp:Flutter推荐方式,提供快速构建APP的方式(包括导航栏、内容、主题等)
    return MaterialApp(
      // home: 类似于根控制器,也是需要指定一个widget
      // Scaffold: 类似于导航控制器NavigationController,包含导航栏相关属性。是一个可变组件statefulWidget
      home: Scaffold(
        // appBar:导航栏
        appBar: AppBar(
              title: Text('Flutter Demo'),
            ),
        // body: 内容
        body: MyWidget(),
      ),
      // theme:主题,可控制主题色
      theme: ThemeData(
        primaryColor: Colors.yellow
      ),
    );
  }
}

class MyWidget extends StatelessWidget {
  @override
  // build 确定组件返回的内容
  Widget build(BuildContext context) {

    // final创建一个_textStyle不可变变量,_开头的属性,表示私有属性
    final _textStyle = TextStyle(color: Colors.red, fontSize: 40.0, fontWeight: FontWeight.bold);

    return Center(
      child: Text(
        'Hello Flutter',
        textDirection: TextDirection.ltr,
        style: _textStyle, // 直接使用变量
      ),
    );
  }
}
image.png

2.5 ListView 列表视图

  • 列表视图:类似iOS的UITableView,但是没有Section的概念。
2.5.1 准备Model

在布局之前,先准备数据

  • 新建Model文件夹,新建car.dart文件:
    image.png
// 不需要导入material.dart,因为Car直接继承Object

// Car 模型
class Car {
  // 构造函数: {}内是可选值
  const Car({
    this.name,
    this.imageUrl,
  });

  // 名称
  final String name;
  // 图片链接
  final String imageUrl;
}

final List<Car> datas = [
  Car(
    name: '保时捷918 Spyder',
    imageUrl:
    'https://upload-images.jianshu.io/upload_images/2990730-7d8be6ebc4c7c95b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240',
  ),
  Car(
    name: '兰博基尼Aventador',
    imageUrl:
    'https://upload-images.jianshu.io/upload_images/2990730-e3bfd824f30afaac?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240',
  ),
  Car(
    name: '法拉利Enzo',
    imageUrl:
    'https://upload-images.jianshu.io/upload_images/2990730-a1d64cf5da2d9d99?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240',
  ),
  Car(
    name: 'Zenvo ST1',
    imageUrl:
    'https://upload-images.jianshu.io/upload_images/2990730-bf883b46690f93ce?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240',
  ),
  Car(
    name: '迈凯伦F1',
    imageUrl:
    'https://upload-images.jianshu.io/upload_images/2990730-5a7b5550a19b8342?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240',
  ),
  Car(
    name: '萨林S7',
    imageUrl:
    'https://upload-images.jianshu.io/upload_images/2990730-2e128d18144ad5b8?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240',
  )
];
2.5.2 设置ListView
  • 如果直接导入Car模型,需要导入头文件

三种导入头文件的方式

  1. 直接手写,在顶部import
  2. 左键点击Car,出现红色小灯泡,点击import Library
  3. 鼠标光标放在Car上,按住Option+Enter键,再按一次Enter键,可快捷导入import Library

final List<Car> datas = []

  • Container: 类似于htmldiv,也类似于iOSUIView,就是用来放东西的。需要设置大小(也可以被子控件撑出大小)
  • Column: 内容垂直排列的容器
  • row: 内容水平排列的容器
  • stack: 内容重叠的容器
  • Image图片可变组件network加载网络图片
  • SizedBox: 空容器,有大小。(有时为了便于内部插入元素,会直接使用Container
  • MaterialAppdebugShowCheckedModeBanner隐藏导航栏Debug角标
import 'package:flutter/material.dart';

import 'Model/car.dart';

// 入口,展示MyWidget组件
void main() => runApp(App());

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // MaterialApp:Flutter推荐方式,提供快速构建APP的方式(包括导航栏、内容、主题等)
    return MaterialApp(
      // 隐藏导航栏Debug角标
      debugShowCheckedModeBanner: false,
      // home: 类似于根控制器,需要指定一个widget
      home: Home(),
      // theme:主题,可控制主题色
      theme: ThemeData(
        primaryColor: Colors.yellow
      ),
    );
  }
}

// Home 组件
class Home extends StatelessWidget {

  // 回调函数,返回widget组件
  Widget _cellForRow(BuildContext context, int index) {
    // Container类似于html的div,也类似于iOS的UIView,就是用来放东西的
    // 需要大小(也可以被子控件撑出大小)
    return Container(
        color: Colors.white,
        // height: 20,
        margin:  EdgeInsets.all(10), //EdgeInsets.only(top: 1),
        // 子控件
        // Column 内容垂直排列的容器 row 内容水平排列的容器 stack 内容重叠的容器
        // Image图片可变组件,network加载网络图片
        child: Column(
          children: <Widget>[
            Image.network(datas[index].imageUrl),
            SizedBox(height: 8,),
            Text(
              datas[index].name,
              style: TextStyle(
                  fontWeight: FontWeight.bold,
                fontSize: 18.0,
                fontStyle: FontStyle.italic),),
            SizedBox(height: 8,)
          ]
        )
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.grey[100],
      appBar: AppBar(
        title: Text("Flutter Demo"),
      ),
      // ListView 列表组件(没有iOS的Section概念)
      body: ListView.builder(
        // cell个数
        itemCount: datas.length,
        // cell内容(等同与cellForRow)build 是渲染
        // iOS中是使用代理和协议完成,这里是直接使用回调函数,有两个入参
        itemBuilder: _cellForRow,
      ),
    );
  }
}

// 模型数组
// 没导入头文件时,会提示需要导入Car头文件
// 三种导入头文件的方式
// 1. 直接手写,在顶部import
// 2. 左键点击Car,出现红色小灯泡,点击import Library
// 3. 鼠标光标放在Car上,按住Option+Enter键,再按一次Enter键,可快捷导入import Library
// final List<Car> datas = []
  • 效果展示:


    image.png
  • 实际开发中,我们可以将ListView内容抽离出来,做成单独文件listView_demo.dart:

import 'package:flutter/material.dart';

import 'car.dart';

class ListViewDemo extends StatelessWidget {

  // 回调函数,返回widget组件
  Widget _cellForRow(BuildContext context, int index) {
    return Container(
        color: Colors.white,
        margin:  EdgeInsets.all(10)
        child: Column(
            children: <Widget>[
              Image.network(datas[index].imageUrl),
              SizedBox(height: 8,),
              Text(
                datas[index].name,
                style: TextStyle(
                    fontWeight: FontWeight.bold,
                    fontSize: 18.0,
                    fontStyle: FontStyle.italic),),
              SizedBox(height: 8,)
            ]
        )
    );
  }

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      // cell个数
      itemCount: datas.length,
      itemBuilder: _cellForRow,
    );
  }
}
  • main.dart中,直接使用我们封装的ListViewDemo导入头文件)即可:
import 'package:flutter/material.dart';

import 'Model/listView_demo.dart';

// 入口,展示MyWidget组件
void main() => runApp(App());

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // MaterialApp:Flutter推荐方式,提供快速构建APP的方式(包括导航栏、内容、主题等)
    return MaterialApp(
      // 隐藏导航栏Debug角标
      debugShowCheckedModeBanner: false,
      // 根组件
      home: Home(),
      // 主题
      theme: ThemeData(
        primaryColor: Colors.yellow
      ),
    );
  }
}

// Home 组件
class Home extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.grey[100],
      appBar: AppBar(
        title: Text("Flutter Demo"),
      ),
      // 列表
      body: ListViewDemo(),
    );
  }
}

3. 常用基础组件

  • 上面讲了简单封装,我们现在对几个基础组件进行简单封装

3.1 基础文本

  • 新建一个base_widget.dart文件,创建TextDemo组件:
  1. 通过属性声明变量内容样式),_开头的属性为私有属性
  2. $ + 属性名快捷插入变量内容。 如果后面有其他字符等信息,可使用{}包裹起来。
  3. Text可设置maxLines最大行数,超出部分样式通过overflow设置(ellipsis尾部省略号
import 'package:flutter/material.dart';

//【普通文本 Demo】
class TextDemo extends StatelessWidget {
  // 文本样式(私有属性)
  final TextStyle _textStyle = TextStyle(
    fontSize: 24.0,
  );

  final String _title = 'Flutter入门';
  final String _author = 'HT';

  @override
  Widget build(BuildContext context) {

    // $ + 属性名: 快捷插入变量内容。 如果后面有其他字符等信息,可使用{}包裹起来。
    return Text(
      '《$_title》这是一个TextDemo,使用Flutter开发。由iOS开发者${_author}开发,快速配置,简易上手的零基础学习方式。欢迎阅读和上手练习,不懂之处,留言交流',
      textAlign: TextAlign.center,
      style: _textStyle,
      maxLines: 4, // 最多4行
      overflow: TextOverflow.ellipsis, // 超出显示省略号
    );
  }
}
  • main.dart中指定bodyTextDemo:
    image.png
  • 展示样式:


    image.png

3.2 富文本

  • 使用RichText组件,通过给text指定TextSpan类型,添加children:<TextSpan>数组,数组内创建TextSpan,并赋值样式即可:
import 'package:flutter/material.dart';

//【富文本 Demo】
class RichTextDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RichText(
      //基础元素
      text: TextSpan(
          text: '《Flutter 入门》',
          style: TextStyle(
            fontSize: 30,
            color: Colors.black,
          ) ,
          // 子元素
          children:<TextSpan>[
            // 元素一
            TextSpan(
              text: 'HT',
              style: TextStyle(
                fontSize: 20,
                color: Colors.blue,
              ),
            ),
            // 元素二
            TextSpan(
              text: '666',
              style: TextStyle(
                fontSize: 40,
                color: Colors.red,
              ),
            ),
          ]),
    );
  }
}
  • main.dart中指定bodyRichTextDemo:
    image.png
  • 展示样式:


    image.png

3.3 基础容器Container

  • 创建基础组件BaseWidgetDemo,返回Container组件:
//【基础组件Demo】
class BaseWidgetDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 容器背景蓝色(没有给定大小时,根据子视图大小撑开)
    return Container(
      color: Colors.blue,
      // 子视图横向布局
      child: Row(
        children: <Widget>[
          Container(
            // 内边距
            padding: EdgeInsets.only(left: 10,top: 10,right: 10,bottom: 10),
            // 外边距
            margin: EdgeInsets.only(left: 10,top: 10,right: 10,bottom: 10),
            // 子元素红色
            color: Colors.red,
            // 子元素内容包含➕号图片
            child: Icon(Icons.add),
          ),
          Container(
            // 子元素红色
            color: Colors.red,
            // 子元素内容包含➕号图片
            child: Icon(Icons.add),
          ),
        ],
      ),
    );
  }
}
image.png
  • 展示样式:


    image.png
  • Dart中没有iOSButton概念:
    因为Button实际是图片 + 文字 + 手势 + 状态组成,以及封装相应的响应事件。我们一般使用手势小部件,包装图片文字即可。

【快捷键】
command + option + L:自动格式化
command + -:折叠当前函数
command + shift + -:折叠全部函数
command + {:回到上一步
command + }: 回到下一步
command + shift + O: 快速到指定文件
stless:不可变组件
stful: 可变组件


本节主要是Flutter基础体验,通过本节,其实我们已经感受到了Flutter便捷
下一节,Flutter入门三:自动布局(Row/Column/Stack)与两种Widget

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容