当前位置: 首页 > news >正文

Flutter 学习之旅 之 flutter 使用 【验证码】输入组件的简单封装

Flutter 学习之旅 之 flutter 使用 【验证码】输入组件的简单封装

目录

Flutter 学习之旅 之 flutter 使用 【验证码】输入组件的简单封装

一、简单介绍

二、验证码组件

三、实现简单效果预览

四、验证码组件样式设置

五、关键代码


一、简单介绍

Flutter 是一款开源的 UI 软件开发工具包,由 Google 开发和维护。它允许开发者使用一套代码同时构建跨平台的应用程序,包括移动设备(iOS 和 Android)、Web 和桌面平台(Windows、macOS 和 Linux)。

Flutter 使用 Dart 编程语言,它可以将代码编译为 ARM 或 Intel 机器代码以及 JavaScript,从而实现快速的性能。Flutter 提供了一个丰富的预置小部件库,开发者可以根据自己的需求灵活地控制每个像素,从而创建自定义的、适应性强的设计,这些设计在任何屏幕上都能呈现出色的外观和感觉。

二、验证码组件

该验证码组件通过组合多个自定义的输入框(VerifyCodeBoxItem)和一个隐藏的 TextField 来实现。每个 VerifyCodeBoxItem 代表验证码的一位,可以自定义样式(如下划线或盒子)。隐藏的 TextField 用于实际的文本输入,其输入内容通过监听器(onChanged)更新到对应的 VerifyCodeBoxItem 中。

特点与优势

  1. 自定义样式:支持两种输入框样式(下划线和盒子),并允许自定义边框颜色、宽度、圆角大小等,满足不同设计需求。

  2. 光标显示:可选择是否显示光标,以及自定义光标颜色、宽度和位置,提升用户体验。

  3. 焦点管理:支持自动获取焦点和输入完成后失去焦点,方便用户操作。

  4. 输入限制:仅允许输入数字,且输入长度限制为验证码位数,避免无效输入。

  5. 回调函数:提供输入完成的回调函数,方便开发者处理输入结果。

  6. 易于集成:作为一个独立的组件,可以轻松集成到任何 Flutter 项目中。

以下是详细的实现原理:

1. 核心组件结构

  • VerifyCodeBox:这是主组件,负责管理整个验证码输入框的逻辑。

  • VerifyCodeBoxItem:这是子组件,用于表示验证码输入框中的每一位。每个 VerifyCodeBoxItem 显示一个字符,并可以自定义样式(如边框、圆角、光标等)。

  • 隐藏的 TextField:用于实际的文本输入。用户的所有输入操作都在这个隐藏的 TextField 中完成,而显示则通过 VerifyCodeBoxItem 来实现。

2. 实现步骤

  1. 初始化状态

    • _VerifyCodeBoxState 中,初始化一个 TextEditingController 和一个 FocusNode,分别用于控制输入内容和焦点。

    • 初始化一个 _contentList,用于存储每个输入框的文本内容。列表的长度等于验证码的位数(count)。

  2. 构建输入框

    • 使用 Stack 将隐藏的 TextField 和显示的 VerifyCodeBoxItem 组合在一起。

    • 隐藏的 TextField 通过 TextField 组件实现,设置为透明样式(TextStyle(color: Colors.transparent)),并隐藏边框(InputBorder.none)。

    • 每个 VerifyCodeBoxItem 通过 VerifyCodeBoxItem 组件实现,显示当前输入的内容,并根据是否获取焦点动态更新边框颜色。

  3. 输入监听

    • 监听隐藏的 TextFieldonChanged 事件,当用户输入内容时,动态更新 _contentList 中对应位置的字符。

    • 根据输入内容的长度,更新每个 VerifyCodeBoxItem 的显示内容。

  4. 焦点管理

    • 使用 GestureDetector 包裹整个输入框,点击时请求焦点。

    • 如果输入完成且 unfocus 属性为 true,则在输入完成后取消焦点,隐藏软键盘。

  5. 回调触发

    • 当输入内容的长度达到验证码位数时,触发 onSubmitted 回调函数,通知外部输入完成。

三、实现简单效果预览

四、验证码组件样式设置

1、矩形的验证码输入框

2、下划线验证码输入框

3、自定义验证码输入框

注意:设置了 decoration样式,会由于 type 显示

五、关键代码

1、main

import 'package:flutter/material.dart';
import 'package:test_verify_code/verify_code/verify_code_box.dart';
import 'package:test_verify_code/verify_code/verify_code_box_item.dart';void main() {runApp(const MyApp());
}class MyApp extends StatelessWidget {const MyApp({super.key});// This widget is the root of your application.@overrideWidget build(BuildContext context) {return MaterialApp(title: 'Flutter Demo', // 应用程序的标题theme: ThemeData(// 应用程序的主题配置colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), // 使用深紫色作为主题色种子),home: const MyHomePage(title: 'Flutter Demo Home Page'), // 应用程序的首页);}
}class MyHomePage extends StatefulWidget {const MyHomePage({super.key, required this.title});final String title; // 页面标题@overrideState<MyHomePage> createState() => _MyHomePageState();
}class _MyHomePageState extends State<MyHomePage> {int _counter = 0; // 计数器,用于记录按钮点击次数void _incrementCounter() {setState(() {_counter++; // 每次点击按钮,计数器加1});}@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(backgroundColor: Theme.of(context).colorScheme.inversePrimary, // 使用主题的反向主色作为AppBar背景title: Text(widget.title), // AppBar的标题),body: Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, // 子组件居中对齐children: <Widget>[const Text('You have pushed the button this many times:'), // 提示文本Text('$_counter', // 显示计数器的值style: Theme.of(context).textTheme.headlineMedium, // 使用主题的标题样式),VerifyCodeBox(onSubmitted: (value) {print("onSubmitted code = $value"); // 输入完成时打印验证码},/*decoration: BoxDecoration(image: DecorationImage(image: AssetImage('asserts/change_phone.png'), // 背景图片),),*/type: VerifyCodeBoxItemType.box, // 输入框样式为盒子样式textStyle: TextStyle(fontSize: 30), // 输入框文本样式focusBorderColor: Colors.lightBlue, // 获取焦点时的边框颜色borderColor: Colors.grey, // 默认边框颜色cursorColor: Colors.lightBlue, // 光标颜色showCursor: true, // 显示光标),],),),floatingActionButton: FloatingActionButton(onPressed: _incrementCounter, // 点击按钮时调用计数器加1的方法tooltip: 'Increment', // 按钮的提示文本child: const Icon(Icons.add), // 按钮的图标), // This trailing comma makes auto-formatting nicer for build methods.);}
}

代码注释说明

  1. 主入口

    • void main() 是程序的入口点,调用 runApp 启动应用程序。

  2. MyApp

    • MyApp 是应用程序的根组件,使用 MaterialApp 创建一个 Material Design 风格的应用程序。

    • 设置了应用程序的标题和主题颜色。

  3. MyHomePage

    • MyHomePage 是应用程序的首页,继承自 StatefulWidget

    • 包含一个计数器 _counter,用于记录按钮点击次数。

    • 提供了一个 _incrementCounter 方法,用于增加计数器的值。

  4. _MyHomePageState

    • _MyHomePageStateMyHomePage 的状态管理类,负责构建页面内容。

    • 使用 Scaffold 构建页面结构,包含 AppBarFloatingActionButton

    • 在页面主体中,使用 Column 布局,包含一个提示文本、计数器文本和一个验证码输入框(VerifyCodeBox)。

  5. 验证码输入框

    • VerifyCodeBox 是验证码输入框组件,配置了以下属性:

      • onSubmitted:输入完成时的回调函数,打印输入的验证码。

      • decoration:自定义背景图片。

      • type:输入框样式为盒子样式。

      • textStyle:输入框文本样式。

      • focusBorderColor:获取焦点时的边框颜色。

      • borderColor:默认边框颜色。

      • cursorColor:光标颜色。

      • showCursor:显示光标。

  6. 其他组件

    • FloatingActionButton:一个浮动按钮,点击时增加计数器的值。

    • Text:用于显示提示文本和计数器的值。

2、VerifyCodeBox

library flutter_verification_box;import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:test_verify_code/verify_code/verify_code_box_item.dart';///
/// 验证码输入框
/// 一个可自定义样式的验证码输入框组件,支持多种配置,如验证码位数、输入框样式、光标显示等。
///
class VerifyCodeBox extends StatefulWidget {VerifyCodeBox({this.count = 6, // 验证码的位数,默认为6位this.itemWidget = 45, // 每个输入框的宽度required this.onSubmitted, // 输入完成时的回调函数this.type = VerifyCodeBoxItemType.box, // 输入框的样式类型,默认为盒子样式this.decoration, // 自定义输入框的装饰this.borderWidth = 2.0, // 边框宽度this.borderRadius = 5.0, // 边框圆角大小required this.textStyle, // 输入框内文本的样式required this.focusBorderColor, // 输入框获取焦点时的边框颜色required this.borderColor, // 输入框默认边框颜色this.unfocus = true, // 输入完成后是否失去焦点,默认为truethis.autoFocus = true, // 是否自动获取焦点,默认为truethis.showCursor = false, // 是否显示光标,默认为falsethis.cursorWidth = 2, // 光标宽度required this.cursorColor, // 光标颜色this.cursorIndent = 10, // 光标距离顶部的距离this.cursorEndIndent = 10, // 光标距离底部的距离});////// 几位验证码,一般6位,还有4位的///final int count;////// 每一个item的宽度///final double itemWidget;////// 输入完成回调///final ValueChanged onSubmitted;////// 每个item的装饰类型,[VerifyCodeBoxItemType]///final VerifyCodeBoxItemType type;////// 每个item的样式///final Decoration? decoration;////// 边框宽度///final double borderWidth;////// 边框颜色///final Color borderColor;////// 获取焦点边框的颜色///final Color focusBorderColor;////// [VerifyCodeBoxItemType.box] 边框圆角///final double borderRadius;////// 文本样式///final TextStyle textStyle;////// 输入完成后是否失去焦点,默认true,失去焦点后,软键盘消失///final bool unfocus;////// 是否自动获取焦点///final bool autoFocus;////// 是否显示光标///final bool showCursor;////// 光标颜色///final Color cursorColor;////// 光标宽度///final double cursorWidth;////// 光标距离顶部距离///final double cursorIndent;////// 光标距离底部距离///final double cursorEndIndent;@overrideState<StatefulWidget> createState() => _VerifyCodeBoxState();
}class _VerifyCodeBoxState extends State<VerifyCodeBox> {late TextEditingController _controller; // 用于控制TextField的文本late FocusNode _focusNode; // 用于控制TextField的焦点List _contentList = []; // 用于存储每个输入框的文本内容@overridevoid initState() {// 初始化内容列表,根据验证码位数生成对应数量的空字符串List.generate(widget.count, (index) {_contentList.add('');});_controller = TextEditingController(); // 初始化文本控制器_focusNode = FocusNode(); // 初始化焦点节点super.initState();}@overrideWidget build(BuildContext context) {return GestureDetector(onTap: () {// 点击输入框时,请求焦点FocusScope.of(context).requestFocus(_focusNode);},child: Stack(children: <Widget>[// 使用Positioned.fill确保输入框占据整个区域Positioned.fill(child: Row(mainAxisAlignment: MainAxisAlignment.spaceAround, // 子组件均匀分布children: List.generate(widget.count, (index) {// 根据验证码位数生成对应的输入框return Container(width: widget.itemWidget, // 设置输入框宽度height: widget.itemWidget, // 设置输入框高度child: VerifyCodeBoxItem(data: _contentList[index], // 当前输入框的文本内容textStyle: widget.textStyle, // 文本样式type: widget.type, // 输入框样式类型decoration: widget.decoration, // 自定义装饰borderRadius: widget.borderRadius, // 边框圆角大小borderWidth: widget.borderWidth, // 边框宽度borderColor: (_controller.text.length == index? widget.focusBorderColor // 如果当前输入框获取焦点,使用焦点边框颜色: widget.borderColor) // 否则使用默认边框颜色??widget.borderColor, // 如果未指定默认边框颜色,则使用传入的边框颜色showCursor: widget.showCursor && _controller.text.length == index, // 是否显示光标cursorColor: widget.cursorColor, // 光标颜色cursorWidth: widget.cursorWidth, // 光标宽度cursorIndent: widget.cursorIndent, // 光标距离顶部的距离cursorEndIndent: widget.cursorEndIndent, // 光标距离底部的距离),);}),)),_buildTextField(), // 构建隐藏的TextField用于实际输入],),);}////// 构建TextField/// 构建一个隐藏的TextField用于实际输入,通过监听输入内容更新每个输入框的显示。///_buildTextField() {return TextField(controller: _controller, // 绑定文本控制器focusNode: _focusNode, // 绑定焦点节点decoration: InputDecoration(border: UnderlineInputBorder(borderSide: BorderSide(color: Colors.transparent)), // 隐藏边框enabledBorder: UnderlineInputBorder(borderSide: BorderSide(color: Colors.transparent)), // 隐藏边框focusedBorder: UnderlineInputBorder(borderSide: BorderSide(color: Colors.transparent)), // 隐藏边框),cursorWidth: 0, // 隐藏光标autofocus: widget.autoFocus, // 是否自动获取焦点inputFormatters: [FilteringTextInputFormatter.allow(RegExp("[0-9]")), // 仅允许输入数字],maxLength: widget.count, // 最大输入长度为验证码位数buildCounter: (BuildContext context, {required int currentLength, required int? maxLength, required bool isFocused}) {return Text(''); // 隐藏输入计数器},keyboardType: TextInputType.number, // 键盘类型为数字键盘style: TextStyle(color: Colors.transparent), // 隐藏输入文本onChanged: _onValueChange, // 监听输入内容变化);}////// 输入内容变化时的回调/// 更新每个输入框的显示内容,并在输入完成时触发回调。///_onValueChange(value) {for (int i = 0; i < widget.count; i++) {if (i < value.length) {// 如果当前输入框有内容,则更新内容_contentList[i] = value.substring(i, i + 1);} else {// 如果当前输入框没有内容,则清空内容_contentList[i] = '';}}setState(() {}); // 更新界面if (value.length == widget.count) {// 如果输入完成if (widget.unfocus) {// 如果需要失去焦点,则取消焦点_focusNode.unfocus();}if (widget.onSubmitted != null) {// 如果有输入完成回调,则触发回调widget.onSubmitted(value);}}}
}

代码注释说明:

  1. 类和构造函数注释

    • VerifyCodeBox 的构造函数参数进行了详细注释,解释了每个参数的用途和默认值。

    • 注释清晰地指出了该类的功能:一个可自定义样式的验证码输入框组件。

  2. 状态管理

    • _VerifyCodeBoxState 中,初始化了 TextEditingControllerFocusNode,用于控制输入框的文本和焦点。

    • 使用 _contentList 存储每个输入框的文本内容。

  3. 输入框构建

    • 使用 GestureDetector 包裹整个输入框,点击时请求焦点。

    • 使用 Stack 将隐藏的 TextField 和显示的输入框组合在一起。

3、VerifyCodeBoxItem

import 'package:flutter/material.dart';
import 'package:test_verify_code/verify_code/verify_code_box_cursor.dart';///
/// 输入框样式
/// 定义了两种输入框样式:下划线和盒子。
///
enum VerifyCodeBoxItemType {////// 下划线样式/// 仅在输入框底部绘制一条线。///underline,////// 盒子样式/// 绘制一个带有边框的矩形框。///box,
}///
/// 单个输入框
/// 用于绘制单个验证码输入框,支持两种样式(下划线和盒子),并可选择是否显示光标。
///
class VerifyCodeBoxItem extends StatelessWidget {VerifyCodeBoxItem({this.data = '', // 输入框中的文本数据required this.textStyle, // 文本的样式this.type = VerifyCodeBoxItemType.box, // 输入框的样式类型,默认为盒子样式this.decoration, // 自定义装饰,可覆盖默认样式this.borderRadius = 5.0, // 边框圆角大小this.borderWidth = 2.0, // 边框宽度required this.borderColor, // 边框颜色this.showCursor = false, // 是否显示光标required this.cursorColor, // 光标颜色this.cursorWidth = 2, // 光标宽度this.cursorIndent = 5, // 光标距离顶部的距离this.cursorEndIndent = 5, // 光标距离底部的距离});final String data; // 输入框中的文本final VerifyCodeBoxItemType type; // 输入框的样式类型final double borderWidth; // 边框宽度final Color borderColor; // 边框颜色final double borderRadius; // 边框圆角大小final TextStyle textStyle; // 文本样式final Decoration? decoration; // 自定义装饰////// 是否显示光标///final bool showCursor;////// 光标颜色///final Color cursorColor;////// 光标宽度///final double cursorWidth;////// 光标距离顶部距离///final double cursorIndent;////// 光标距离底部距离///final double cursorEndIndent;@overrideWidget build(BuildContext context) {// 如果没有指定边框颜色,则使用主题的分割线颜色var borderColor = this.borderColor ?? Theme.of(context).dividerColor;// 构建文本var text = _buildText();// 根据输入框类型构建装饰var widget;if (type == VerifyCodeBoxItemType.box) {// 如果是盒子样式,调用 _buildBoxDecoration 方法构建widget = _buildBoxDecoration(text, borderColor);} else {// 如果是下划线样式,调用 _buildUnderlineDecoration 方法构建widget = _buildUnderlineDecoration(text, borderColor);}// 使用 Stack 将装饰和光标组合在一起return Stack(children: <Widget>[widget, // 输入框装饰showCursor? Positioned.fill(child: VerifyCodeBoxCursor(// 如果需要显示光标,则添加一个 VerifyCodeBoxCursor 组件color: cursorColor ?? Theme.of(context).cursorColor,width: cursorWidth,indent: cursorIndent,endIndent: cursorEndIndent,),): Container() // 如果不需要显示光标,则添加一个空的 Container],);}////// 绘制盒子类型/// 构建一个带有边框的矩形框,用于盒子样式输入框。///_buildBoxDecoration(Widget child, Color borderColor) {return Container(alignment: Alignment.center, // 内容居中对齐decoration: decoration ??BoxDecoration(// 如果没有指定自定义装饰,则使用默认的盒子样式装饰borderRadius: BorderRadius.circular(borderRadius), // 边框圆角border: Border.all(color: borderColor, width: borderWidth), // 边框样式),child: child, // 子组件(文本));}////// 绘制下划线类型/// 构建一个带有下划线的输入框,用于下划线样式输入框。///_buildUnderlineDecoration(Widget child, Color borderColor) {return Container(alignment: Alignment.center, // 内容居中对齐decoration: decoration ??UnderlineTabIndicator(// 如果没有指定自定义装饰,则使用默认的下划线样式装饰borderSide: BorderSide(width: borderWidth, color: borderColor), // 下划线样式),child: child, // 子组件(文本));}////// 文本/// 构建输入框中的文本。///_buildText() {return Text(data, // 显示的文本内容style: textStyle, // 文本样式);}
}extension on ThemeData {// 扩展 ThemeData,添加一个默认的光标颜色get cursorColor => Colors.white;
}

代码注释说明:

  1. 枚举定义

    • VerifyCodeBoxItemType 枚举定义了两种输入框样式:underlinebox,并为每种样式添加了注释说明。

  2. 构造函数

    • VerifyCodeBoxItem 的构造函数参数进行了详细注释,解释了每个参数的用途和默认值。

  3. 状态构建

    • build 方法中,根据输入框的类型(boxunderline)选择不同的装饰方法,并将装饰和光标组合在一起。

  4. 装饰方法

    • _buildBoxDecoration_buildUnderlineDecoration 方法分别用于构建盒子样式和下划线样式的装饰。

    • 使用了 BoxDecorationUnderlineTabIndicator 来实现不同的样式。

  5. 文本构建

    • _buildText 方法用于构建输入框中的文本,使用了 Text 组件并应用了传入的文本样式。

  6. 光标显示

    • 如果 showCursortrue,则在 Stack 中添加一个 VerifyCodeBoxCursor 组件来显示光标。

  7. 主题扩展

    • 使用 extensionThemeData 添加了一个默认的光标颜色,以便在未指定光标颜色时使用。

4、VerifyCodeBoxCursor

import 'package:flutter/material.dart';///
/// 模拟光标
/// 该类用于创建一个闪烁的光标效果,通常用于验证码输入框中,表示当前的输入焦点位置。
///
class VerifyCodeBoxCursor extends StatefulWidget {VerifyCodeBoxCursor({required this.color, // 光标的颜色required this.width, // 光标的宽度required this.indent, // 光标距离顶部的距离required this.endIndent, // 光标距离底部的距离});////// 光标颜色///final Color color;////// 光标宽度///final double width;////// 光标距离顶部距离///final double indent;////// 光标距离底部距离///final double endIndent;@overrideState<StatefulWidget> createState() => _VerifyCodeBoxCursorState();
}class _VerifyCodeBoxCursorState extends State<VerifyCodeBoxCursor>with SingleTickerProviderStateMixin {late AnimationController _controller; // 用于控制光标闪烁的动画控制器@overridevoid initState() {// 初始化动画控制器,设置动画持续时间为500毫秒_controller = AnimationController(duration: Duration(milliseconds: 500),vsync: this,)// 添加状态监听器,用于控制光标的闪烁效果..addStatusListener((status) {if (status == AnimationStatus.completed) {// 当动画完成时,反向播放动画_controller.reverse();} else if (status == AnimationStatus.dismissed) {// 当动画被取消时,正向播放动画_controller.forward();}});// 开始播放动画_controller.forward();super.initState();}@overrideWidget build(BuildContext context) {// 使用FadeTransition组件实现光标的闪烁效果return FadeTransition(opacity: _controller, // 将动画控制器的值绑定到FadeTransition的opacity属性child: VerticalDivider( // 使用VerticalDivider组件绘制光标thickness: widget.width, // 设置光标的宽度color: widget.color, // 设置光标的颜色indent: widget.indent, // 设置光标距离顶部的距离endIndent: widget.endIndent, // 设置光标距离底部的距离),);}@overridevoid dispose() {// 销毁动画控制器,释放资源_controller.dispose();super.dispose();}
}

代码注释说明:

  1. 类和构造函数注释

    • VerifyCodeBoxCursor 类和构造函数的参数进行了详细说明,解释了每个参数的用途。

    • 注释清晰地指出了该类的功能:创建一个闪烁的光标效果。

  2. 状态管理

    • 使用 SingleTickerProviderStateMixin 提供动画控制器所需的 vsync

    • initState 方法中初始化动画控制器,并设置动画的持续时间和状态监听器,以实现光标的闪烁效果。

  3. 动画逻辑

    • 使用 FadeTransition 组件结合动画控制器的 opacity 属性,实现光标的渐隐和渐现效果。

    • 通过状态监听器在动画完成时反向播放,在动画取消时正向播放,确保光标持续闪烁。

  4. 资源释放

    • dispose 方法中销毁动画控制器,避免内存泄漏。

相关文章:

  • 如何安装Visio(win10)
  • 【阿里云大模型高级工程师ACP习题集】2.3 优化提示词改善答疑机器人回答质量
  • python实战项目64:selenium采集软科中国大学排名数据
  • Alertmanager的安装和详细使用步骤总结
  • 【Java面试笔记:基础】12.Java有几种文件拷贝方式?哪一种最高效?
  • JAVA程序获取SVN提交记录
  • SPSS ANOVA分析test
  • 云原生--CNCF-2-五层生态结构(成熟度3层分类,云原生生态5层结构)
  • 18487.1-2015-解读笔记之四-交流充电之流程分析
  • word内容使用python替换
  • 【go】go run-gcflags常用参数归纳,go逃逸分析执行语句,go返回局部变量指针是安全的
  • 连锁美业管理系统「数据分析」的重要左右分析︳博弈美业系统疗愈系统分享
  • 自动创建 中国古代故事人物一致性图画,看看扣子的空间是否能达到你的满意,自媒体的福音?
  • PCB规则
  • Python爬虫实战:获取xie程网敦煌景点数据,为51旅游路线做参考
  • Linux网络编程 从集线器到交换机的网络通信全流程——基于Packet Tracer的深度实验
  • Docker安装ES :确保 Kibana 正确连接 Elasticsearch
  • Unity中使用Cinemachine插件创建自由视角相机(freelookCamera)来实现第三人称漫游
  • WSL2-Ubuntu22.04下拉取Docker MongoDB镜像并启动
  • STM32F407 的通用定时器与串口配置深度解析
  • 研讨会丨明清区域史研究的比较与对话
  • 停止水资源共享、驱逐武官,印度对巴基斯坦宣布多项反制措施
  • 科普|结石疼痛背后的危机信号:疼痛消失≠警报解除
  • 郑庆华任同济大学党委书记
  • 高明士︱纪念坚苦卓绝的王寿南先生
  • 亮相!神二十乘组与中外媒体记者集体见面