GUILayout类和EditorGUILayout类

GUILayout类和EditorGUILayout类

目录

一、GUILayout 类

官方中文文档

重要:

Begin+名字 的方法,需要使用 End+名字 方法结束。

1.布局

BeginArea 固定区域

在固定的屏幕区域开始GUI控件的GUILayout块。超出此区域会被隐藏。

参数:

Rect screenRect:位置和大小

string text:可选文本显示在该区域。

Texture image:可选纹理图片显示在该区域。

BeginHorizontal 和 BeginVertical 水平和垂直布局

开启一个水平和垂直的控制组

参数:

string text:要显示在组上的文本

Texture image:可选纹理显示在组上

BeginScrollView 自动布局的滚动视图

开始一个自动布局的滚动视图。

参数:

Vector2 scrollPosition:使用显示的位置

GUIStyle horizontalScrollbar:用于水平滚动条

GUIStyle verticalScrollbar:用于垂直滚动条

bool alwaysShowHorizontal:可选参数,以始终显示水平滚动条。false 时,内容不超不显示。

bool alwaysShowVertical:可选参数,以始终显示垂直滚动条。false 时,内容不超不显示。

返回值:

Vector2:修改后的scrollPosition。如示例所示,将其返回到传入的变量中

备注:

在 OnGUI 使用时,因为会一直执行 OnGUI 方法,所以如果不使用一个变量获取返回值,滚动会一直在代码设置的那个位置,无法滑动。

例如:

一直在顶部

GUILayout.BeginScrollView(Vector2.zero,new GUILayoutOption[] { GUILayout.Width(300), GUILayout.Height(300) });

正确方法:

在 OnGUI 方法外定义 Vector2 scrollPosition

scrollPosition=GUILayout.BeginScrollView(scrollPosition,new GUILayoutOption[] { GUILayout.Width(300), GUILayout.Height(300) });

2.Box 创建一个自动布局框

创建一个自动布局框

参数:

string text:可选文本显示在该区域。

Texture image:可选纹理图片显示在该区域。

3.按钮类型

Button 和 RepeatButton 按钮

Button: 按下然后松开的时候返回 true

RepeatButton :制作重复按钮。只要用户按住鼠标,按钮就返回true。(介绍是这样的,但是我试的时候只有刚按下和松开的时候才会返回true,也就是只返回了两次)

参数:

string text:显示文字

Texture image:显示纹理图片

返回值:bool

if (GUILayout.Button(“按钮”,GUILayout.Width(100),GUILayout.Height(40)))
{
Debug.Log(“hello”);
}

SelectionGrid 选择网格

制作一个选择网格

参数::

int selected:所选按钮的索引,从 0 开始

string[] texts:显示在按钮上的字符串数组

Texture[] images:显示在按钮上的纹理图片数组

int xCount:水平方向容纳多少个元素。除非样式定义了要使用的fixedWidth,否则元素将被缩放。控件的高度将由元素的数量决定

返回值:int 所选按钮的索引, 从 0 开始

Toolbar 工具栏

工具栏 和 SelectionGrid 功能一样,SelectionGrid 展示的是一堆按钮,Toolbar 展示的是工具栏

参数:

int selected:所选按钮的索引,从 0 开始

string[] texts:显示在按钮上的字符串数组

Texture[] images:显示在按钮上的纹理图片数组

GUI.ToolbarButtonSize buttonSize:确定如何计算工具栏按钮大小

返回值:int 所选按钮的索引, 从 0 开始

Toggle 开关

做一个开/关切换按钮

参数:

bool value:这个按钮是开着的还是关着的?

string text:要显示在按钮上的文本

Texture image:要显示在按钮上的纹理图片

返回值:

按钮的新值

4.滚动条和滑动条

HorizontalScrollbar 和 VerticalScrollbar 滚动条

制作一个水平/垂直滚动条

参数:

float value:最小值和最大值之间的位置

float size:滑块的大小,如果 两端值为0和10,size 为5,滑动滑块返回值只会在05之间

float leftValue:左端的值

float rightValue:右端的值

float topValue:上端的值

float bottomValue:下端的值

返回值:float 修改后的值。用户可以通过拖动滚动条或单击最后的箭头来改变这一点

HorizontalSlider 和 VerticalSlider 滑动条

一个水平/垂直滑块,用户可以拖动它来改变最小值和最大值之间的值,这个只能拖,右边没有框添值

参数:

float value:滑块显示的值。这决定了可拖动拇指的位置

float leftValue 和 float rightValue :两端的值

GUIStyle slider:滑动条的样式 没必要

GUIStyle thumb:滑块的样式 没必要

返回值:float 由用户设置的值

5.输入框

TextField/TextArea 单行/多行输入框

创建一个单行/多行文本输入框,用户可以在其中编辑字符串

参数:

string text:文本编辑。这个函数的返回值应该被赋给这个字符串

int maxLength:字符串的最大长度。如果不输入,用户可以一直输入下去

返回值:string 框内文本

PasswordField 密码输入框

创建一个文本输入框,用户可以在其中输入密码

参数:

string password:密码编辑。这个函数的返回值应该被赋给这个字符串

char maskChar:用来屏蔽密码的字符(一般用 ‘*’)

int maxLength:字符串的最大长度。如果不输入,用户可以一直输入下去

返回值:string 密码内容

6.Window 弹出窗口

创建一个对自身内容进行自动布局的弹出窗口(没试过)

参数:

int id:用于每个窗口的唯一 ID。您将使用该 ID 与其进行交互

Rect screenRect:屏幕上用于窗口的矩形。布局系统会尝试将窗口适配到其内部 - 如果无法完成,则调整矩形以进行适配

GUI.WindowFunction func:在窗口内创建 GUI 的函数。该函数必须接收一个参数 - 要为其创建 GUI 的窗口的 id

string text:要显示为窗口标题的文本

Texture image:在标题栏中显示的纹理图片

返回值:Rect 窗口所在的矩形。其位置和大小可以与您传入的矩形不同

7.展示

Label 展示标签

创建一个自动布局标签

参数:

string text:要在标签上显示的文本

Texture image:要在标签上显示的纹理图片

FlexibleSpace 灵活的空白元素

灵活的空白元素将占用布局中的任何剩余空间

**注意:**这将覆盖 GUILayout.ExpandWidth 和 GUILayout.ExpandHeight

Space 当前布局中插入空白元素

在当前布局组中插入空白元素

空白元素的方向取决于发出命令时当前所在的布局组

**注意:**这将覆盖 GUILayout.ExpandWidth 和 GUILayout.ExpandHeight

参数:

pixels:插入的像素

二、EditorGUILayout 类

官方中文文档

重要:

Begin+名字 的方法,需要使用 End+名字 方法结束。

1.布局

BeginHorizontal 和 BeginVertical 水平和垂直布局

水平布局和垂直布局,和 GUILayout 中相似

返回值:Rect 位置和大小

BeginScrollView 自动布局滚动视图

和 GUILayout 中相似

返回值:Vector2 修改后的 scrollPosition

BeginToggleGroup 开关组

可一次性启用或禁用所有控件的开关,未启用时,里面所有控件均不能进行操作

参数:

string label:要显示在切换控件上方的标签

bool toggle:开关组启用状态

返回值:bool 用户所选的启用状态

BeginFadeGroup 显示隐藏组

可显示隐藏的组,并且过渡生成动画

参数:

float value:值介于 0 和 1 之间,值为 0 时将隐藏组;值为 1 时将使组完全可见

返回值:bool 组是否可见

如果要动画,那就让这个值一点点变到0或1,感觉没必要

if (EditorGUILayout.BeginFadeGroup(value))
{
}

2.滑动条

Slider 和 IntSlider 滑动条

创建一个滑动条,用户可以进行拖动以在最小值和最大值之间更改值

IntSlder 是整数滑动条,用法和 Slider 一样,Slider 参数是 float,IntSlider 是 int

参数:

label:标签

value:滑动条显示的值。该值决定可拖动滑块的位置

leftValue:滑动条左端的值

rightValue:滑动条右端的值

property:修改的字段

两种:

  1. 带返回值,返回当前值
  2. 在参数里面,传入 SerializedProperty,滑动时直接修改该字段的值

MinMaxSlider 范围滑动条

创建一个特殊滑动条,用户可利用该滑动条指定最小值和最大值之间的一个范围

滑动条右侧有个输入框,可以输入滑动值

参数:

label:标签

ref float minValue:滑动条显示的范围内下限值

ref float maxValue:滑动条显示的范围内上限值

float minLimit:滑动条左端限值

float maxLimit:滑动条右端限值

3.按钮、开关、下拉框和选择器

Toggle 和 ToggleLeft 开关

创建一个开关

ToggleLeft :开关在左,标签在右

参数:

label:标签

bool value:编辑的值

返回值:bool 当前开关值

Popup、IntPopup 和 EnumPopup 弹出选择框

Popup:

通用弹出选择框

参数:

label:标签

int selectedIndex:显示的选项索引(从0开始)

string[]/GUIContent[] displayedOptions:弹出菜单中所示选项的数组

返回值:int 用户选择选项的索引

IntPopup:

整数弹出选择框,每个选项对应一个数字

参数:

label:标签

int selectedValue:显示的选项的值

string[]/GUIContent[] displayedOptions:弹出菜单中所示选项的数组

int[] optionValues:具有每个选项值的数组

返回值:int 用户选择选项对应的值

点击弹出框中的选项,从 optionValues 找到对应位置的数字

如果 optionValues 长度小于 displayedOptions 长度,点击没有对应数字的将不会赋值,会返回原来的值

optionValues 中有值相同时,展示出来的会对应 displayedOptions 中第一个该值索引

// num=0 展示出来的是 c
num = EditorGUILayout.IntPopup(num,new string[] {
    
     "a","b","c","d"},new int[] {
    
     1,5,0,0});

EnumPopup:

枚举弹出选择框

参数:

label:标签

Enum selected:显示的枚举选项

bool includeObsolete:设置为 true 将使 ObsoleteAttribute 附带枚举值。设置为 false 将使 ObsoleteAttribute 不附带枚举值

Func<Enum, bool> checkEnabled:为每个显示的枚举值调用的方法。如果可以选择该选项,则指定的方法应返回 true,否则返回 false

返回值:Enum 用户选择的枚举选型(需要强转)

enum OPTIONS{
    
    A,B,C}
OPTIONS op = OPTIONS.A;
private void OnGUI()
{
    
    
	op = (OPTIONS)EditorGUILayout.EnumPopup(op);
}

TagField 和 LayerField 弹出选择框

TagField:unity tag 选择

参数:

label:标签

string tag:显示的 tag

返回值 string 选择的 tag

LayerField:unity layer 选择

参数:

label:标签

int layer:显示的 layer

返回值 int 选择的 layer

DropdownButton 对鼠标按下做出反应的按钮

一个能够对鼠标按下做出反应的按钮,用于显示您自己的下拉菜单内容

参数:

GUIContent content:标签

FocusType focusType:按钮是否可以通过键盘选择 Keyboard(是)

返回值:bool 当用户单击按钮时,返回 true

这可用于以下拉菜单形式打开 GenericMenu 或您自己的自定义 EditorWindow 的按钮。

与 GenericMenu 结合使用时,请使用 GenericMenu.Dropdown 并向此方法传递与用于按钮的矩形相同的矩形,可使用 GUILayoutUtility.GetLastRect 获取此矩形。

与自定义 EditorWindow 结合使用时,请使用 EditorWindow.ShowAsDropdown 并向此方法传递与用于按钮的矩形相同的矩形,可使用 GUILayoutUtility.GetLastRect 获取此矩形。

EnumFlagsField 枚举掩码选择框

单击后,系统会为枚举类型的每个值显示带有选项的菜单,

参数:

label:标签

Enum enumValue:枚举标志值

bool includeObsolete:设置为 true 将使 ObsoleteAttribute 附带枚举值。设置为 false 将使 ObsoleteAttribute 不附带枚举值

返回值:Enum 用户修改的枚举标志值。这是一个选择位掩码,其中每个位代表一个枚举值索引。(请注意,此返回值不是枚举本身。)

注意:此方法仅支持其基础类型(sbyte、short、int、byte、ushort 或 uint)受 Unity 的序列化系统支持的枚举。如果枚举由无符号类型提供支持,那么“Everything”选项应具有与所有已设置的位对应的值(即未选中的上下文中的 ~0 或该类型的 MaxValue 常量)

例如:出入值 op 是 10-1010,使用掩码,B 和 C 满足,则这是点开选项,应该是 B 和 C 都勾上了

enum OPTIONS
{
    
    
	None=0,
	A=1<<0,//1-1
	B=1<<1,//2-10
	C=1<<3,//8-1000
	AB=A|B,//3-11
	ABC=A|B|C,//11-1011
}
OPTIONS op = (OPTIONS)10;
private void OnGUI()
{
    
    
    op = (OPTIONS)EditorGUILayout.EnumFlagsField(op);
}

MaskField 掩码的选择框

适用于掩码的选择框,会自动创建全不选 Nothing 和全选 Everything,对应值 0 和 -1

参数:

label:标签

int mask:显示的当前掩码

string[] displayedOptions:包含每个标志的标签的字符串数组

返回值:int 用户修改的值

ColorField 颜色选择器

创建一个颜色选择字段,点击弹出颜色选择框

参数:

label:标签

Color value:要编辑的颜色

bool showEyedropper:如果为 true,拾色器应显示吸管控件。如果为 false,则不显示吸管控件。默认为 true

bool showAlpha: 如果为 true,则允许用户为颜色设置 Alpha 值。如果为 false,则隐藏 Alpha 分量。默认为 true

bool hdr:如果为 true,则将颜色视为 HDR 值。如果为 false,则将其视为标准 LDR 值。默认为 true

返回值 Color 选择的颜色

GradientField Gradient 选择器

创建一个颜色渐变选择字段,点击弹出颜色渐变选择框

label:标签

Gradient value:要编辑的渐变

返回值 Gradient 用户编辑的渐变

CurveField AnimationCurve 动画编辑

AnimationCurve 用户编辑的曲线

label:标签

AnimationCurve value:要编辑的曲线

Color color:显示曲线时使用的颜色

Rect ranges:约束曲线范围的可选矩形

返回值:AnimationCurve 编辑后的曲线

SerializedProperty property:如果使用这个,会将颜色直接设置到这个字段里面,就没有返回值了

Foldout 可折叠的标签

一个左侧带有折叠箭头的标签,创建一个类似文件夹结构

参数:

content:标签

bool foldout:显示折叠状态

bool toggleOnLabelClick:指定单击标签时是否切换折叠状态。默认值为 false。如果设置为 true,则在可单击区域包含标签

返回值:用户选择的折叠状态,为 true,则应渲染子对象

bool foldout = false;
private void OnGUI()
{
    
    
	GUILayout.BeginVertical();
	foldout = EditorGUILayout.Foldout(foldout, "折叠");
	if (foldout)
	{
    
    
		if (GUILayout.Button("曲线生成C#代码")){
    
    }
	}
	GUILayout.EndVertical();
}

InspectorTitlebar 窗口标题栏

类似于 Inspector 窗口的标题栏

参数:

bool foldout:箭头显示的折叠状态

UnityEngine.Object[] targetObj:标题栏对饮的一个对象或组件 可多个

返回值:bool 用户选择的折叠状态

4.输入框

Bounds 输入框

方法参数对应类型:

BoundsField BoundsIntField
Bounds BoundsInt

BoundsField 输入 Bounds 的 Center 和 Extents 字段

BoundsIntField 输入 BoundsInt 的 Position 和 Size 字段

参数:

label:标签

value:要编辑的值

返回值:用户输入的值

Rect 输入框

方法参数对应类型:

RectField RectIntField
Rect Rect

用于编辑 Rect 的 X、Y、W 和 H 字段的输入框

参数:

label:标签

value:要编辑的值

返回值:用户输入的值

Vector 输入框

方法参数对应类型:

Vector2Field Vector2IntField Vector3Field Vector3IntField Vector4Field
Vector2 Vector2Int Vector3 Vector3Int Vector4

用于编辑 Vector 的 X、Y、Z 和 W 字段的输入框

参数:

label:标签

value:要编辑的值

返回值:用户输入的值

数字、文本输入框

方法参数对应类型:

DoubleField FloatField IntField LongField TextField/TextArea
double float int long string

用于编辑文本或者数字的输入框

TextArea:可换行文本输入框

参数:

label:标签

value:要编辑的值

返回值:用户输入的值

Delay 延迟输入框

方法参数对应类型:

DelayedDoubleField DelayedFloatField DelayedIntField DelayedTextField
double float int string

用于编辑文本或者数字的延迟输入框,在用户按 Enter 键或将焦点从 double 字段移开之前,返回值不会更改

参数:

label:标签

value:要编辑的值

返回值:用户输入的值

SerializedProperty property:如果使用这个,会将值直接设置到这个字段里面,就没有返回值了

PasswordField 密码输入框

用于输入密码的输入框,输入后展示用 ***** 替代

参数:

label:标签

string password:要编辑的密码

返回值:用户输入的密码

ObjectField 输入框

可接收任何对象类型的输入框

参数:

label:标签

UnityEngine.Object obj:显示的对象

Type objType:对象类型

bool allowSceneObjects:允许分配场景对象。如果对象引用作为资源的一部分进行存储,请确保为 false,因为资源无法存储对场景中对象的引用

返回值:UnityEngine.Object 用户已设置的对象

SerializedProperty property:如果使用这个,会将值直接设置到这个字段里面,就没有返回值了

5.展示

GetControlRect 获取编辑器控件的矩形

参数:

bool hasLabel:可选布尔值,用于指定控件是否有标签。默认值为 true

float height:控件的高度(以像素为单位)。默认值为 EditorGUIUtility.singleLineHeight

返回值:Rect

在创建新的编辑器控件时,用于实现实际控件的声音设计并不依赖于 GUILayout,而是让控件将 Rect 作为参数,类似于 EditorGUI 类中的控件。这样可确保该控件还可以用于 PropertyDrawer 等类中,而此类中不允许使用 GUILayout。

实现控件的非布局版本后,也可以轻松创建布局版本,只需调用非布局版本即可。要获取适合控件的矩形,可以使用 GetControlRect 函数。

HelpBox 提示框

带有消息提示的帮助框

参数:

string message:消息内容

MessageType type:消息类型 默认 MessageType.None

bool wide:如果为 true,则框将覆盖窗口的整个宽度;否则,框将仅覆盖控件部分 默认为true

MessageType:

None:普通 不带图标

Info:信息 带消息图标

Warning:警告 带黄色警告图标

Error:报错 带红色报错图标

LabelField 标签展示

标签展示

参数:

label:标签字段前的标签

label2:显示在右侧的标签

SelectableLabel 可选择标签

鼠标可以选中,然后进行复制粘贴的只读信息

参数:

text:要显示文本

PrefixLabel 特定控件前的标签

一个显示在特定控件前的标签

参数:

label:要显示在控件左侧的标签

注意:大多数编辑器控件都已内置了可指定为其中一个参数的可选标签。如果没有可用的此类内置标签,或要从头开始创建自己的编辑器控件,则可以使用 PrefixLabel。

意思就是没啥事别用,都自带的有

PropertyField SerializedProperty 生成一个字段

为 SerializedProperty 生成一个字段

如果要在 Inspector 中自定义游戏对象的选项外观,可使用此方法。使用此方法为序列化属性创建字段

参数:

label:标签

SerializedProperty property:要为其创建字段的 SerializedProperty

bool includeChildren:如果为 true,将绘制包含子项的属性;否则仅绘制控件本身(例如,只有一个折叠箭头,其下不含任何内容)

返回值:bool 如果属性拥有子项且已展开,并且 includeChildren 设置为 false,则为 True;否则为 false

Space 控件之间的空间

在上一个控件和下一个控件之间留出一个小空间

三、GUIContent 和 GUIStyle、GUILayoutOption

1.GUILayoutOption

使用 GUILayout.方法

float 参数方法:

MinWidth MinHeight MaxWidth MaxHeight

Width Height

bool 参数方法:

ExpandWidth ExpandHeight 选项,以允许或禁止水平展开。

2.GUIContent 类

new GUIContent()

构建一个包含文本、图像和定义了工具提示的GUIContent。

当用户将鼠标悬停在它上面时,全局GUI。工具提示设置为工具提示。

GUI元素的内容。

参数:

string text: 文本

Texture image:图标图像

string tooltip:工具提示文本,鼠标悬停时弹出

变量: 和参数一样

3.GUIStyle

自定义组件的外观,写个插件要啥外观,自带的就行

四、Utility 辅助类

EditorGUIUtility EditorUtility

ClearDirty 清除 target 的“脏”标志。
ClearProgressBar Removes the progress bar.
CollectDeepHierarchy 收集层级视图中以每个指定对象为根对象的所有对象。
CollectDependencies 计算并返回 roots 中列出的资源所依赖的所有资源的列表。
CompressCubemapTexture 压缩立方体贴图纹理。
CompressTexture 压缩纹理。
CopySerialized 复制 Unity Object 的所有设置。
CopySerializedIfDifferent 将 Unity Object 的所有设置复制到第二个 Object(如果两者不同)。
CopySerializedManagedFieldsOnly 在不同的托管对象之间复制可序列化的字段。
CreateGameObjectWithHideFlags 使用 HideFlags 和指定组件创建游戏对象。
DisplayCancelableProgressBar 显示或更新含有 Cancel 按钮的进度条。
DisplayDialog 此方法显示模态对话框。
DisplayDialogComplex 显示含有三个按钮的模态对话框。
DisplayPopupMenu 显示弹出菜单。
DisplayProgressBar 显示或更新进度条。
FocusProjectWindow 将 Project 窗口置于前面并聚焦该窗口。
FormatBytes 返回有关字节数的文本。
GetDialogOptOutDecision 此方法显示模态对话框,使用户可以选择不再次显示当前对话框。
GetDirtyCount 返回一个整数,指示更改指定对象的序列化属性的次数。
GetObjectEnabled 对象是否已启用(0 表示已禁用,1 表示已启用,-1 表示没有 Enabled 按钮)。
InstanceIDToObject 将实例 ID 转换为对对象的引用。
IsDirty 获取一个布尔值,指示指定对象自上次保存以来是否已更改。
IsPersistent 确定对象是否存储在磁盘中。
IsRunningUnderCPUEmulation Gets a boolean value. This value indicates whether your CPU is unable to execute Unity natively and is running an emulated version.
NaturalCompare 仿人类排序。
OpenFilePanel 显示“打开文件”对话框并返回所选的路径名称。
OpenFilePanelWithFilters 显示“打开文件”对话框并返回所选的路径名称。
OpenFolderPanel 显示“打开文件夹”对话框并返回所选的路径名称。
OpenPropertyEditor Open properties editor for an Object.
RequestScriptReload Unity 编辑器会在下一帧上异步重新加载脚本程序集。这会重置所有脚本的状态,但是 Unity 不会编译任何自上次编译以来已更改的代码。
SaveFilePanel 显示“保存文件”对话框并返回所选的路径名称。
SaveFilePanelInProject 在项目的 Assets 文件夹中显示“保存文件”对话框并返回所选的路径名称。
SaveFolderPanel 显示“保存文件夹”对话框并返回所选的路径名称。
SetCameraAnimateMaterials 设置此摄像机以启用编辑器中的材质动画。
SetCameraAnimateMaterialsTime 为此摄像机设置渲染时使用的全局时间。
SetCustomDiffTool Set custom diff tool settings.
SetDefaultParentObject Sets the default parent object for the active Scene.
SetDialogOptOutDecision 此方法显示模态对话框,使用户可以选择不再次显示当前对话框。
SetDirty Marks target object as dirty.
SetObjectEnabled 设置对象的启用状态。
SetSelectedRenderState 为此渲染器将场景视图设置为所选的显示模式。
UnloadUnusedAssetsImmediate 卸载未使用的资源。
UpdateGlobalShaderProperties 更新渲染时使用的全局着色器属性。

五、扩展

1.动画曲线生成 C# 代码

// 动画曲线生成 C# 代码,生成代码直接复制使用
AnimationCurve curve = new AnimationCurve(new Keyframe(0f, 0f, 0f, 0f), new Keyframe(1f, 1f, 0f, 0f));
private void OnGUI()
{
    
    
	GUILayout.BeginHorizontal();
	curve = EditorGUILayout.CurveField("动画曲线", curve);
	if (GUILayout.Button("曲线生成C#代码"))
	{
    
    
		string keys = "new AnimationCurve(";
		foreach (Keyframe k in curve.keys)
		{
    
    
			string key = string.Format("new Keyframe({0}f,{1}f,{2}f,{3}f),", k.time, k.value, k.inTangent, k.outTangent);
			keys += key;
		}
		keys = keys.TrimEnd(',');
		keys += ")";
		Debug.Log(keys);
	}
	GUILayout.EndHorizontal();
}

猜你喜欢

转载自blog.csdn.net/weixin_42235716/article/details/124495048
今日推荐