Flutter自定义Widget实践之简易五子棋

前言

最近在学习Flutter的自定义Widget相关内容,于是就自己写了一个flutter的简易五子棋的页面,以加强学习相关的内容。

实现原理及规则说明

主要原理就是通过flutter提供的CustomPaint 组件来实现自定义图形绘制。主要是自定义一个棋盘背景类,以及棋子类。

自定义棋盘背景类代码如下

class MyChessBg extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    print('paint bg');
    var rect = Offset.zero & size;
    print('paint bg ${rect.left} ${rect.right}');
    //画棋盘
    drawChessboard(canvas, rect);
    //画棋子
    // drawPieces(canvas, rect);
  }

  // 返回false, 后面介绍
  @override
  bool shouldRepaint(CustomPainter oldDelegate) => false;

  void drawChessboard(Canvas canvas, Rect rect) {
    //棋盘背景
    var paint = Paint()
      ..isAntiAlias = true
      ..style = PaintingStyle.fill //填充
      ..color = Color(0xFFDCC48C);
    canvas.drawRect(rect, paint);

    //画棋盘网格
    paint
      ..style = PaintingStyle.stroke //线
      ..color = Colors.black38
      ..strokeWidth = 1.0;

    //画横线
    for (int i = 0; i <= 15; ++i) {
      double dy = rect.top + rect.height / 15 * i;
      canvas.drawLine(Offset(rect.left, dy), Offset(rect.right, dy), paint);
    }

    for (int i = 0; i <= 15; ++i) {
      double dx = rect.left + rect.width / 15 * i;
      canvas.drawLine(Offset(dx, rect.top), Offset(dx, rect.bottom), paint);
    }
  }

自定义棋子类代码如下

class MyChessCh extends CustomPainter {
  MyChessCh({Key? key, required this.offset}) : super();

  late final List<Offset> offset;

  @override
  void paint(Canvas canvas, Size size) {
    print('paint ch');
    var rect = Offset.zero & size;
    //画棋子
    // drawPieces(canvas, rect);
    drawPieces1(canvas, offset);
  }

  void drawPieces1(Canvas canvas, List<Offset> offsets) {
    //画一个黑子
    var paint = Paint()
      ..style = PaintingStyle.fill
      ..color = Colors.black;
    for (var i = 0; i < offsets.length; i++) {
      //画一个黑子
      paint.color = Colors.black;
      if (i % 2 == 0) {
        //画一个黑子
        canvas.drawCircle(
          offsets[i],
          8,
          paint,
        );
      } else {
        //画一个白子
        paint.color = Colors.white;
        canvas.drawCircle(
          offsets[i],
          8,
          paint,
        );
      }
    }
  }

  //画棋子
  void drawPieces(Canvas canvas, Rect rect) {
    double eWidth = rect.width / 15;
    double eHeight = rect.height / 15;
    //画一个黑子
    var paint = Paint()
      ..style = PaintingStyle.fill
      ..color = Colors.black;
    //画一个黑子
    canvas.drawCircle(
      Offset(rect.center.dx - eWidth / 2, rect.center.dy - eHeight / 2),
      min(eWidth / 2, eHeight / 2) - 2,
      paint,
    );
    //画一个白子
    paint.color = Colors.white;
    canvas.drawCircle(
      Offset(rect.center.dx + eWidth / 2, rect.center.dy - eHeight / 2),
      min(eWidth / 2, eHeight / 2) - 2,
      paint,
    );
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return true;
  }
}

五子棋实现规则说明:
1.判赢规则说明:
主要是通过当前落子的8个方向去进行判断,是否有一个方向满足了同一颜色棋子以及满足5颗。如下图所示。


d9fb02d9e36578f3c0c34037221e741.png

以下是部分代码实现:

///判断获胜的方法。
  ///主要判断依据:判断当前点的8个方向是否能连成5个相同的颜色的子,8个方向依次遍历,符合条件就返回。
  ///offset : 当前点 , black:黑子还是白子 , offs:对应颜色的子的集合。
  ///注意:计数的值必须每个方向一个,如果用同一个技术标志,会导致技术值不正确。每个方向只要符合条件,都会令count加一,最后会变成一个方向没到5就出现获胜的情况。
  bool win(Offset offset, bool black, List<Offset> offs) {

    //向左遍历 ,步长为20
    List<Offset> l = <Offset>[];
    int l_conut = 1;
    for (var x = offset.dx - 20; x > 0; x = x - 20) {
      var item = Offset(x, offset.dy);
      if (offs.contains(item)) {
        l_conut++;
        l.add(item);
        if (l_conut >= 5) {
          print("左赢的列表:${l}");
          return true;
        }
      } else {
        break;
      }
    }

    //向右遍历
    int r_conut = 1;
    List<Offset> r = <Offset>[];
    for (var x = offset.dx + 20; x <= 300; x = x + 20) {
      var item = Offset(x, offset.dy);
      if (offs.contains(item)) {
        r_conut++;
        r.add(item);
        if (r_conut >= 5) {
          print("右赢的列表:${r}");
          return true;
        }
      } else {
        break;
      }
    }

    //向上遍历
    int t_conut = 1;
    List<Offset> t = <Offset>[];
    for (var y = offset.dy - 20; y > 0; y = y - 20) {
      var item = Offset(offset.dx, y);
      if (offs.contains(item)) {
        t_conut++;
        t.add(item);
        if (t_conut >= 5) {
          print("上赢的列表:${t}");
          return true;
        }
      } else {
        break;
      }
    }

    //向下遍历
    int b_conut = 1;
    List<Offset> b = <Offset>[];
    for (var y = offset.dy + 20; y <= 300; y = y + 20) {
      var item = Offset(offset.dx, y);
      if (offs.contains(item)) {
        b_conut++;
        b.add(item);
        if (b_conut >= 5) {
          print("下赢的列表:${b}");
          return true;
        }
      } else {
        break;
      }
    }

    //左上
    int lt_conut = 1;
    List<Offset> lt = <Offset>[];
    for (var x = offset.dx - 20, y = offset.dy - 20;
        x > 0 && y > 0;
        x = x - 20, y = y - 20) {
      var item = Offset(x, y);
      if (offs.contains(item)) {
        lt_conut++;
        lt.add(item);
        if (lt_conut >= 5) {
          print("左上赢的列表:${lt}");
          return true;
        }
      } else {
        break;
      }
    }

    //右上
    int rt_conut = 1;
    List<Offset> rt = <Offset>[];
    for (var x = offset.dx + 20, y = offset.dy - 20;
        x <= 300 && y > 0;
        x = x + 20, y = y - 20) {
      var item = Offset(x, y);
      if (offs.contains(item)) {
        rt_conut++;
        rt.add(item);
        if (rt_conut >= 5) {
          print("右上赢的列表:${rt}");
          return true;
        }
      } else {
        break;
      }
    }

    //左下
    int lb_conut = 1;
    List<Offset> lb = <Offset>[];
    for (var x = offset.dx - 20, y = offset.dy + 20;
        x > 0 && y <= 300;
        x = x - 20, y = y + 20) {
      var item = Offset(x, y);
      if (offs.contains(item)) {
        lb_conut++;
        lb.add(item);
        if (lb_conut >= 5) {
          print("左下赢的列表:${lb}");
          return true;
        }
      } else {
        break;
      }
    }

    //右下
    int rb_conut = 1;
    List<Offset> rb = <Offset>[];
    for (var x = offset.dx + 20, y = offset.dy + 20;
        x <= 300 && y <= 300;
        x = x + 20, y = y + 20) {
      var item = Offset(x, y);
      if (offs.contains(item)) {
        rb_conut++;
        rb.add(item);
        if (rb_conut >= 5) {
          print("右下赢的列表:${rb}");
          return true;
        }
      } else {
        break;
      }
    }

    return false;
  }

2.棋子位置判定:
当点击位置没有出现在棋盘的棋线的交叉点时,需要将棋子移动到最近的正确位置。当点击的点在框内时,计算当前点击的位置距离四周框边的距离,来判断应该落子的位置。


image.png

以下是部分代码实现:

 ///将点击位置转换成最近的有效的棋盘点位置。
  ///计算逻辑:x轴坐标 = 点击点位置 - 前一个竖线的x轴坐标。 如果值大于一半格子长度,就取下一个竖线的x坐标,反之取上一根竖线的x坐标
  ///        y轴坐标 = 点击点位置 - 前一个竖线的y轴坐标。 如果值大于一半格子长度,就取下一个竖线的y坐标,反之取上一根竖线的y坐标
  Offset transOffset(Offset offset) {
    double ddx = 0;//最终位子的x坐标
    double ddy = 0;//最终位子的y坐标
    double level = 20;//一格的宽度
    int modx = offset.dx ~/ level;//在x轴上,点击的位置左侧的格数
    if (offset.dx - level * modx <= 10) {
      //没过半格,取上一个点,否者取下一格
      ddx = level * modx;
    } else {
      ddx = level * (modx + 1);
    }

    int mody = offset.dy ~/ level;
    if (offset.dy - level * mody <= 10) {
      //没过半格,取上一个点,否者取下一格
      ddy = level * mody;
    } else {
      ddy = level * (mody + 1);
    }
    print("ddx= ${ddx} + ddy = ${ddy}");
    return Offset(ddx, ddy);
  }

整体实现

整体实现就是,在一个stack布局上,底部绘制棋盘背景,顶部绘制棋子,然后监控点击事件,在up事件中处理当前点击位置,来刷新棋盘上的棋子位置,但不刷新棋盘的位置,主要用到了RepaintBoundary组件来实现棋盘的重构刷新。
通过两个数组来存储黑白子。再通过另一个数组来存储整体棋子的位置顺序。然后进行双方下棋,直至出现胜利者。
整体代码如下:

import 'dart:math';

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

class CustomChessBg extends StatelessWidget {
  const CustomChessBg({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    Offset off = Offset.zero;
    List<Offset> offs = <Offset>[];//所有棋子的集合
    List<Offset> boffs = <Offset>[];//黑棋集合
    List<Offset> woffs = <Offset>[];//白棋集合
    // offs.add(off);
    return DecoratedBox(
      decoration: BoxDecoration(color: Colors.white),
      child: Stack(
        children: [
          Center(
              child: RepaintBoundary(
                  child: CustomPaint(
            size: const Size(300, 300), //指定画布大小
            painter: MyChessBg(),
          ))),
          Center(
            child: StatefulBuilder(
                builder: (BuildContext context, StateSetter setState) {
              return Listener(
                child: CustomPaint(
                  size: const Size(300, 300), //指定画布大小
                  painter: MyChessCh(offset: offs),
                ),
                onPointerUp: (event) {
                  print(event.localPosition);
                  var ll = transOffset(event.localPosition);
                  if (offs.contains(ll)) {
                    Fluttertoast.showToast(msg: "该位置已经下过子了,不能重复下");
                    return;
                  }
                  offs.add(ll);
                  if (offs.length % 2 == 1) {
                    //第一颗是黑棋
                    boffs.add(ll);
                    if (win(ll, true, boffs)) {
                      // Fluttertoast.showToast(msg: "黑棋获胜");
                      showDialog(
                          context: context,
                          barrierDismissible: false,
                          builder: (context) {
                            return AlertDialog(
                              title: Text("获胜提醒!"),
                              content: Text("黑棋获胜"),
                              actions: [
                                TextButton(
                                    onPressed: () {
                                      offs.clear();
                                      boffs.clear();
                                      woffs.clear();
                                      Navigator.of(context).pop();
                                      setState(() {});
                                    },
                                    child: Text("确定")),
                              ],
                            );
                          });
                    }
                  } else {
                    woffs.add(ll);
                    if (win(ll, false, woffs)) {
                      showDialog(
                          context: context,
                          barrierDismissible: false,
                          builder: (context) {
                            return AlertDialog(
                              title: Text("获胜提醒!"),
                              content: Text("白棋获胜"),
                              actions: [
                                TextButton(
                                    onPressed: () {
                                      offs.clear();
                                      boffs.clear();
                                      woffs.clear();
                                      Navigator.of(context).pop();
                                      setState(() {});
                                    },
                                    child: Text("确定")),
                              ],
                            );
                          });
                    }
                  }
                  setState(() {});
                },
              );
            }),
          )
        ],
      ),
    );
  }

  ///判断获胜的方法。
  ///主要判断依据:判断当前点的8个方向是否能连成5个相同的颜色的子,8个方向依次遍历,符合条件就返回。
  ///offset : 当前点 , black:黑子还是白子 , offs:对应颜色的子的集合。
  ///注意:计数的值必须每个方向一个,如果用同一个技术标志,会导致技术值不正确。每个方向只要符合条件,都会令count加一,最后会变成一个方向没到5就出现获胜的情况。
  bool win(Offset offset, bool black, List<Offset> offs) {

    //向左遍历 ,步长为20
    List<Offset> l = <Offset>[];
    int l_conut = 1;
    for (var x = offset.dx - 20; x > 0; x = x - 20) {
      var item = Offset(x, offset.dy);
      if (offs.contains(item)) {
        l_conut++;
        l.add(item);
        if (l_conut >= 5) {
          print("左赢的列表:${l}");
          return true;
        }
      } else {
        break;
      }
    }

    //向右遍历
    int r_conut = 1;
    List<Offset> r = <Offset>[];
    for (var x = offset.dx + 20; x <= 300; x = x + 20) {
      var item = Offset(x, offset.dy);
      if (offs.contains(item)) {
        r_conut++;
        r.add(item);
        if (r_conut >= 5) {
          print("右赢的列表:${r}");
          return true;
        }
      } else {
        break;
      }
    }

    //向上遍历
    int t_conut = 1;
    List<Offset> t = <Offset>[];
    for (var y = offset.dy - 20; y > 0; y = y - 20) {
      var item = Offset(offset.dx, y);
      if (offs.contains(item)) {
        t_conut++;
        t.add(item);
        if (t_conut >= 5) {
          print("上赢的列表:${t}");
          return true;
        }
      } else {
        break;
      }
    }

    //向下遍历
    int b_conut = 1;
    List<Offset> b = <Offset>[];
    for (var y = offset.dy + 20; y <= 300; y = y + 20) {
      var item = Offset(offset.dx, y);
      if (offs.contains(item)) {
        b_conut++;
        b.add(item);
        if (b_conut >= 5) {
          print("下赢的列表:${b}");
          return true;
        }
      } else {
        break;
      }
    }

    //左上
    int lt_conut = 1;
    List<Offset> lt = <Offset>[];
    for (var x = offset.dx - 20, y = offset.dy - 20;
        x > 0 && y > 0;
        x = x - 20, y = y - 20) {
      var item = Offset(x, y);
      if (offs.contains(item)) {
        lt_conut++;
        lt.add(item);
        if (lt_conut >= 5) {
          print("左上赢的列表:${lt}");
          return true;
        }
      } else {
        break;
      }
    }

    //右上
    int rt_conut = 1;
    List<Offset> rt = <Offset>[];
    for (var x = offset.dx + 20, y = offset.dy - 20;
        x <= 300 && y > 0;
        x = x + 20, y = y - 20) {
      var item = Offset(x, y);
      if (offs.contains(item)) {
        rt_conut++;
        rt.add(item);
        if (rt_conut >= 5) {
          print("右上赢的列表:${rt}");
          return true;
        }
      } else {
        break;
      }
    }

    //左下
    int lb_conut = 1;
    List<Offset> lb = <Offset>[];
    for (var x = offset.dx - 20, y = offset.dy + 20;
        x > 0 && y <= 300;
        x = x - 20, y = y + 20) {
      var item = Offset(x, y);
      if (offs.contains(item)) {
        lb_conut++;
        lb.add(item);
        if (lb_conut >= 5) {
          print("左下赢的列表:${lb}");
          return true;
        }
      } else {
        break;
      }
    }

    //右下
    int rb_conut = 1;
    List<Offset> rb = <Offset>[];
    for (var x = offset.dx + 20, y = offset.dy + 20;
        x <= 300 && y <= 300;
        x = x + 20, y = y + 20) {
      var item = Offset(x, y);
      if (offs.contains(item)) {
        rb_conut++;
        rb.add(item);
        if (rb_conut >= 5) {
          print("右下赢的列表:${rb}");
          return true;
        }
      } else {
        break;
      }
    }

    return false;
  }

  ///将点击位置转换成最近的有效的棋盘点位置。
  ///计算逻辑:x轴坐标 = 点击点位置 - 前一个竖线的x轴坐标。 如果值大于一半格子长度,就取下一个竖线的x坐标,反之取上一根竖线的x坐标
  ///        y轴坐标 = 点击点位置 - 前一个竖线的y轴坐标。 如果值大于一半格子长度,就取下一个竖线的y坐标,反之取上一根竖线的y坐标
  Offset transOffset(Offset offset) {
    double ddx = 0;//最终位子的x坐标
    double ddy = 0;//最终位子的y坐标
    double level = 20;//一格的宽度
    int modx = offset.dx ~/ level;//在x轴上,点击的位置左侧的格数
    if (offset.dx - level * modx <= 10) {
      //没过半格,取上一个点,否者取下一格
      ddx = level * modx;
    } else {
      ddx = level * (modx + 1);
    }

    int mody = offset.dy ~/ level;
    if (offset.dy - level * mody <= 10) {
      //没过半格,取上一个点,否者取下一格
      ddy = level * mody;
    } else {
      ddy = level * (mody + 1);
    }
    print("ddx= ${ddx} + ddy = ${ddy}");
    return Offset(ddx, ddy);
  }
}

///自定义棋子类
class MyChessCh extends CustomPainter {
  MyChessCh({Key? key, required this.offset}) : super();

  late final List<Offset> offset;

  @override
  void paint(Canvas canvas, Size size) {
    print('paint ch');
    var rect = Offset.zero & size;
    //画棋子
    // drawPieces(canvas, rect);
    drawPieces1(canvas, offset);
  }

  void drawPieces1(Canvas canvas, List<Offset> offsets) {
    //画一个黑子
    var paint = Paint()
      ..style = PaintingStyle.fill
      ..color = Colors.black;
    for (var i = 0; i < offsets.length; i++) {
      //画一个黑子
      paint.color = Colors.black;
      if (i % 2 == 0) {
        //画一个黑子
        canvas.drawCircle(
          offsets[i],
          8,
          paint,
        );
      } else {
        //画一个白子
        paint.color = Colors.white;
        canvas.drawCircle(
          offsets[i],
          8,
          paint,
        );
      }
    }
  }

  //画棋子
  void drawPieces(Canvas canvas, Rect rect) {
    double eWidth = rect.width / 15;
    double eHeight = rect.height / 15;
    //画一个黑子
    var paint = Paint()
      ..style = PaintingStyle.fill
      ..color = Colors.black;
    //画一个黑子
    canvas.drawCircle(
      Offset(rect.center.dx - eWidth / 2, rect.center.dy - eHeight / 2),
      min(eWidth / 2, eHeight / 2) - 2,
      paint,
    );
    //画一个白子
    paint.color = Colors.white;
    canvas.drawCircle(
      Offset(rect.center.dx + eWidth / 2, rect.center.dy - eHeight / 2),
      min(eWidth / 2, eHeight / 2) - 2,
      paint,
    );
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return true;
  }
}

///自定义棋盘背景类
class MyChessBg extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    print('paint bg');
    var rect = Offset.zero & size;
    print('paint bg ${rect.left} ${rect.right}');
    //画棋盘
    drawChessboard(canvas, rect);
    //画棋子
    // drawPieces(canvas, rect);
  }

  // 返回false, 后面介绍
  @override
  bool shouldRepaint(CustomPainter oldDelegate) => false;

  void drawChessboard(Canvas canvas, Rect rect) {
    //棋盘背景
    var paint = Paint()
      ..isAntiAlias = true
      ..style = PaintingStyle.fill //填充
      ..color = Color(0xFFDCC48C);
    canvas.drawRect(rect, paint);

    //画棋盘网格
    paint
      ..style = PaintingStyle.stroke //线
      ..color = Colors.black38
      ..strokeWidth = 1.0;

    //画横线
    for (int i = 0; i <= 15; ++i) {
      double dy = rect.top + rect.height / 15 * i;
      canvas.drawLine(Offset(rect.left, dy), Offset(rect.right, dy), paint);
    }

    for (int i = 0; i <= 15; ++i) {
      double dx = rect.left + rect.width / 15 * i;
      canvas.drawLine(Offset(dx, rect.top), Offset(dx, rect.bottom), paint);
    }
  }
}

总结:

该内容,目前只是实现简易的五子棋的基本功能,其中还有很多细节尚未完善,仅供参考。

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

推荐阅读更多精彩内容