赞
踩
Odin是Unity里一款非常强大的编辑器插件,它提供了很多丰富的特性(Attribute),可以用来丰富编辑器的显示和交互。下面列举一些好用的特性。
功能:
Range用来限制字段范围;
PropertyRange 更强大一点,还可以限制属性范围;正常Unity内置的功能是显示不了字段的,这里我们用了Odin里的【ShowInInspector】特性,来把Property 属性显示出来
示例:
[Range(0, 10)]
public int Field = 2;
[ShowInInspector,PropertyRange(0, 10)]
public int Property { get; set; }
效果图:
进阶用法:
我们上面的最小最大值都是固定值,其实还可以用动态的字段值来作范围,如下面的例子
进阶示例:
[PropertyRange(0, “Max”)]
public int Dynamic = 6;
public int Max = 100;
功能:
Required可以用来限制一个资源不能为空
语法:
[Required(“当资源为空时的提示,可不填”)]
示例:
[Required]
public GameObject MyGameObject;
[Required(“Custom error message.”)]
public Rigidbody MyRigidbody;
效果图:
功能:
ValidateInput是个很实用的功能,当用户对字段赋值后,会触发回调方法。然后我们可以写代码逻辑来对用户赋值的实体进行检查。如果检查不通过,可以给予提示
语法:
[ValidateInput(“回调方法名”, “默认提示”)]
示例
[ValidateInput("HasMeshRendererDynamicMessageAndType", "Prefab must have a MeshRenderer component")] public GameObject DynamicMessageAndType; //返回类型为bool,表示是否通过检测 private bool HasMeshRendererDynamicMessageAndType(GameObject gameObject, ref string errorMessage, ref InfoMessageType? messageType) { //没有赋值时,通过检测 if (gameObject == null) return true; //如果赋值的GameObject没有MeshRenderer,return false检测不通过 if (gameObject.GetComponentInChildren<MeshRenderer>() == null) { // 设置提示信息 errorMessage = "\"" + gameObject.name + "\" should have a MeshRenderer component"; // //设置警告类型 messageType = InfoMessageType.Warning; return false; } return true; }
[ShowInInspector] [TypeFilter("GetFilteredTypeList")] public BaseClass A, B; [ShowInInspector] [TypeFilter("GetFilteredTypeList")] public BaseClass[] Array = new BaseClass[3]; public IEnumerable<Type> GetFilteredTypeList() { var q = typeof(BaseClass).Assembly.GetTypes() .Where(x => !x.IsAbstract) // 排除 BaseClass .Where(x => !x.IsGenericTypeDefinition) // 排除 泛型,如C1<> .Where(x => typeof(BaseClass).IsAssignableFrom(x)); // 排除没有继承BaseClass的类型 // 增加特定的类 q = q.AppendWith(typeof(C1<>).MakeGenericType(typeof(GameObject))); q = q.AppendWith(typeof(C1<>).MakeGenericType(typeof(AnimationCurve))); q = q.AppendWith(typeof(C1<>).MakeGenericType(typeof(List<float>))); return q; } public abstract class BaseClass { public int BaseField; } public class A1 : BaseClass { public int _A1; } public class A2 : A1 { public int _A2; } public class A3 : A2 { public int _A3; } public class B1 : BaseClass { public int _B1; } public class B2 : B1 { public int _B2; } public class B3 : B2 { public int _B3; } public class C1<T> : BaseClass { public T C; }
功能:
ReadOnly 可以让显示在面板上的属性不能修改
示例:
[ReadOnly]
public string MyString = “This is displayed as text”;
[ReadOnly]
public int MyInt = 9001;
[ReadOnly]
public int[] MyIntList = new int[] { 1, 2, 3, 4, 5, 6, 7, };
效果图:
这里列举一些实用但不好归类的特性
功能:
ShowInInspector 可以让属性,静态变量等平常不能再Inspector显示的字段显示在面板。挺使用的一个功能
示例:
[ShowInInspector]
public string DelayedProperty { get; set; }
[ShowInInspector]
public static string testStr;
效果图:
功能:
OnValueChanged 可以在字段的值变化后,调用回调方法
语法:
[OnValueChanged(“回调方法名”)]
示例:
[OnValueChanged(“OnValueChanged”)]
public int val;
private void OnValueChanged()
{
Debug.Log(“Value changed!”);
}
效果图:
功能:
Delayed 和 DelayedProperty 可以搭配上面的OnValueChanged使用,OnValueChanged能在值变化后回调,但每次变化都会回调,比如稍微拖拉一下值,你看上面的打印,回调了174次。这时Delayed 就派上用场了,可以在值变化完成后才回调,就不会有那么多回调了。Delayed 修饰通常字段,DelayedProperty 可以用来修饰属性
示例:
[Delayed]
[OnValueChanged(“OnValueChanged”)]
public int DelayedField;
[ShowInInspector, DelayedProperty]
[OnValueChanged(“OnValueChanged”)]
public string DelayedProperty { get; set; }
private void OnValueChanged()
{
Debug.Log(“Value changed!”);
}
效果图:
[GUIColor(0.3f, 0.8f, 0.8f, 1f)] public int ColoredInt1; [GUIColor(0.3f, 0.8f, 0.8f, 1f)] public int ColoredInt2; [ButtonGroup] [GUIColor(0, 1, 0)] private void Apply() { } [ButtonGroup] [GUIColor(1, 0.6f, 0.4f)] private void Cancel() { } [InfoBox("You can also reference a color member to dynamically change the color of a property.")] [GUIColor("GetButtonColor")] [Button("I Am Fabulous", ButtonSizes.Gigantic)] private static void IAmFabulous() { } [Button(ButtonSizes.Large)] [GUIColor("@Color.Lerp(Color.red, Color.green, Mathf.Abs(Mathf.Sin((float)EditorApplication.timeSinceStartup)))")] private static void Expressive() { } private static Color GetButtonColor() { Sirenix.Utilities.Editor.GUIHelper.RequestRepaint(); return Color.HSVToRGB(Mathf.Cos((float)UnityEditor.EditorApplication.timeSinceStartup + 1f) * 0.225f + 0.325f, 1, 1); }
嘿嘿,说到颜色,我们讲下颜色调色板,颜色调色板也是好用的功能。因为一个游戏通用是由3~4种主色调形成的。把这些颜色放在一个调色板里,一来不会出错,二来可以更方便的选择颜色
功能:
ColorPalette 用来修饰Color字段,可以在旁边列出一个调色板,供用户选择颜色
语法:
[ColorPalette] 可以在下拉框里任意选择调色板,接着可以选择该调色板下颜色
[ColorPalette(“调色板名称”)] 可以指定调色板名称,这样直接出现该调色板的颜色
示例:
//不指定调色板名称
[ColorPalette]
public Color ColorOptions;
//加点间距,不然太挤,不太好说明
[PropertySpace(SpaceBefore = 60)]
指定调色板名称为Underwater
[ColorPalette(“Underwater”)]
public Color UnderwaterColor;
效果图:
功能:
Title可以用来增加标题,HideLabel可以用来隐藏字段名
示例:
[Title(“Wide Colors”)]
[HideLabel]
[ColorPalette(“Fall”)]
public Color WideColor1;
效果图:
可以看到原字段名隐藏掉了,只显示我们定义的Title和调色板
功能:
PropertyOrder 可以控制字段的优先级,优先级值越小排在越上面
示例:
[PropertyOrder(1)]
public int Second;
[PropertyOrder(-1)]
public int First;
效果图:
功能:
PropertySpace 可以用来设置字段间隔,前面在讲颜色调色板时用到了PropertySpace
示例:
public int Space;
[PropertySpace(SpaceBefore = 10, SpaceAfter = 20)]
public int Space2;
public int Space3;
效果图:
看到效果图就一目了然了
[Searchable] public ExampleClass searchableClass = new ExampleClass(); [Serializable] public class ExampleClass { public string SomeString = "Saehrimnir is a tasty delicacy"; public int SomeInt = 13579; public DataContainer DataContainerOne = new DataContainer() { Name = "Example Data Set One" }; public DataContainer DataContainerTwo = new DataContainer() { Name = "Example Data Set Two" }; } [Serializable, Searchable] // You can also apply it on a type like this, and it will become searchable wherever it appears public class DataContainer { public string Name; public List<ExampleStruct> Data = new List<ExampleStruct>(Enumerable.Range(1, 10).Select(i => new ExampleStruct(i))); } [Serializable] public struct FilterableBySquareStruct : ISearchFilterable { public int Number; [ShowInInspector, DisplayAsString, EnableGUI] public int Square { get { return this.Number * this.Number; } } public FilterableBySquareStruct(int nr) { this.Number = nr; } public bool IsMatch(string searchString) { return searchString.Contains(Square.ToString()); } } [Serializable] public struct ExampleStruct { public string Name; public int Number; public ExampleEnum Enum; public ExampleStruct(int nr) : this() { this.Name = "Element " + nr; this.Number = nr; this.Enum = (ExampleEnum)ExampleHelper.RandomInt(0, 5); } } public enum ExampleEnum { One, Two, Three, Four, Five }
功能:
MultiLineProperty 可以用来修饰string字段和属性,让文本输入框变成指定的行数
示例:
[Multiline(10)]
public string UnityMultilineField = “”;
[InfoBox(“Odin supports properties, but Unity’s own Multiline attribute only works on fields.”)]
[ShowInInspector]
[MultiLineProperty(10)]
public string OdinMultilineProperty { get; set; }
效果图:
可以看到都有了10行的输入框,MultiLineProperty 能修饰属性,内置的MultiLine不可以
功能:
EnumPaging可以用来修饰枚举字段和属性,让枚举增加左右切换按钮
示例:
[EnumPaging] public SomeEnum SomeEnumField;
public enum SomeEnum
{
A,
B,
C
}
效果图:
//不加修饰的枚举
public SomeEnum OriginSomeEnum;
//去掉下拉框,把枚举选项横向排列显示出来
[EnumToggleButtons]
public SomeEnum SomeEnumField;
public enum SomeEnum
{
First,
Second,
Third,
Fourth,
AndSoOn
}
功能:
SuffixLabel 可以在输入框的末尾增加后缀,让使用工具的人知道当前是以什么为单位
示例:
[SuffixLabel(“ms”, Overlay = false)]
public float Speed1;
//Overlay表示后缀是否在输入框内
[SuffixLabel(“ms”, Overlay = true)]
public float Speed2;
效果图:
[InlineButton("A")] public int InlineButton; [InlineButton("A")] [InlineButton("B", "Custom Button Name")] public int ChainedButtons; private void A() { Debug.Log("A"); } private void B() { Debug.Log("B"); }
public string str1 = "str1";
[LabelText("使用中文显示str2")]
public string str2 = "str2";
啥叫"较特殊的特性",跟"通用特性"有什么区别。也许是靠自己的感觉,把它们归类到这里;也许是上面已经列举了不少,感觉得列到新的组里面…
功能:
Button 用来修饰方法,会在Inspector窗口出现一个按钮,点击后执行方法的逻辑
示例:
[Button(“Hello”)]
private void DefaultSizedButton()
{
Debug.Log(“Hello”);
}
[Button(“Hello,Doraemon”,buttonSize:ButtonSizes.Large),GUIColor(0,1,0)]
private void Button2()
{
Debug.Log(“Hello ~ Doraemon”);
}
效果图:
功能:
FilePath 可以打开一个文件窗口来选择文件。好像容易重名,完整的命名空间是这个:[Sirenix.OdinInspector.FilePath],在重名时可以指定
示例:
//默认返回相对路径
[FilePath]
public string UnityProjectPath;
//可以指定打开的目录(会返回与该目录的相对路径),指定文件格式,指定是否返回全路径,默认为false,即相对路径
[FilePath(ParentFolder = “Assets/OdinLearn”, Extensions = “cs”, AbsolutePath = true)]
public string ResourcePath;
效果图:
FolderPath 用法和上面的FilePath差不多,只是选择的是文件夹,而上面选择的是文件
//在Inspector窗口初始化绘制
[OnInspectorInit("@TimeFoldoutWasOpened = DateTime.Now.ToString()")]
public string TimeFoldoutWasOpened;
//修饰字段 [OnInspectorInit("@Texture = EditorIcons.OdinInspectorLogo")] [OnInspectorGUI("DrawPreview", append: true)]//append = true,表示回调方法DrawPreview在该字段绘制之后,再调用;false,则表示在原始绘制之前 public Texture2D Texture; //把选择的图标再绘制一遍出来 private void DrawPreview() { if (this.Texture == null) return; GUILayout.BeginVertical(GUI.skin.box); GUILayout.Label(this.Texture); GUILayout.EndVertical(); } //修饰方法,直接绘制 [OnInspectorGUI] private void OnInspectorGUI() { UnityEditor.EditorGUILayout.HelpBox("OnInspectorGUI can also be used on both methods and properties", UnityEditor.MessageType.Info); }
public List<string> list;
[OnStateUpdate("@#(list).State.Expanded = $value")]
public bool ExpandList;
[OnStateUpdate("@UnityEngine.Debug.Log(\"OnStateUpdate event invoked!\")")]
public bool Test;
//原始 public float From = 2, To = 7; //自定义显示字段1 [CustomValueDrawer("MyCustomDrawerStatic")] public float CustomDrawerStatic; //自定义显示方法1,使用固定值 private static float MyCustomDrawerStatic(float value, GUIContent label) { return EditorGUILayout.Slider(label, value, 0f, 10f); } //自定义显示字段2 [CustomValueDrawer("MyCustomDrawerInstance")] public float CustomDrawerInstance; //自定义显示方法2,使用From & To 作为变量 private float MyCustomDrawerInstance(float value, GUIContent label) { return EditorGUILayout.Slider(label, value, this.From, this.To); } //自定义显示字段3 [CustomValueDrawer("MyCustomDrawerAppendRange")] public float AppendRange; //自定义显示方法3,增加盒子和Label private float MyCustomDrawerAppendRange(float value, GUIContent label, Func<GUIContent, bool> callNextDrawer) { SirenixEditorGUI.BeginBox(); callNextDrawer(label); var result = EditorGUILayout.Slider(value, this.From, this.To); SirenixEditorGUI.EndBox(); return result; } //自定义显示字段4,修饰数组 [CustomValueDrawer("MyCustomDrawerArrayNoLabel")] public float[] CustomDrawerArrayNoLabel = new float[] { 3f, 5f, 6f }; //自定义显示方法4,去掉Label private float MyCustomDrawerArrayNoLabel(float value) { return EditorGUILayout.Slider(value, this.From, this.To); }
功能:
BoxGroup 可以让同组的字段在一个方框里
示例:
// Box with a title.
[BoxGroup(“Some Title”)]
public string A;
[BoxGroup(“Some Title”)]
public string B;
// Box with a centered title.
[BoxGroup(“Centered Title”, centerLabel: true)]
public string C;
[BoxGroup(“Centered Title”)]
public string D;
效果图:
// LabelWidth can be helpfull when dealing with HorizontalGroups. [HorizontalGroup("Group 1", LabelWidth = 20)] public int C; [HorizontalGroup("Group 1")] public int D; [HorizontalGroup("Group 1")] public int E; // Having multiple properties in a column can be achived using multiple groups. Checkout the "Combining Group Attributes" example. [HorizontalGroup("Split", 0.5f, LabelWidth = 20)] [BoxGroup("Split/Left")] public int L; [BoxGroup("Split/Right")] public int M; [BoxGroup("Split/Left")] public int N; [BoxGroup("Split/Right")] public int O;
功能:
VerticalGroup 跟HorizontalGroup用法差不多,实现的是竖直布局
示例:
[HorizontalGroup(“Split”)]
[VerticalGroup(“Split/Left”)]
public InfoMessageType First;
[VerticalGroup(“Split/Left”)]
public InfoMessageType Second;
[HideLabel]
[VerticalGroup(“Split/Right”)]
public int A;
[HideLabel]
[VerticalGroup(“Split/Right”)]
public int B;
效果图:
//不带组名字的一个组 [ButtonGroup] private void A() { } [ButtonGroup] private void B() { } [ButtonGroup] private void C() { } [ButtonGroup] private void D() { } //带组名字的一个组 [Button(ButtonSizes.Large)] [ButtonGroup("Button Group1")] private void E() { } [GUIColor(0, 1, 0)] [ButtonGroup("Button Group1")] private void F() { }
[OnInspectorGUI] private void Space1() { GUILayout.Space(20); } [ResponsiveButtonGroup] public void Foo() { } [ResponsiveButtonGroup] public void Bar() { } [ResponsiveButtonGroup] public void Baz() { } [OnInspectorGUI] private void Space2() { GUILayout.Space(20); } [ResponsiveButtonGroup("UniformGroup", UniformLayout = true)] public void Foo1() { } [ResponsiveButtonGroup("UniformGroup")] public void Foo2() { } [ResponsiveButtonGroup("UniformGroup")] public void LongesNameWins() { } [ResponsiveButtonGroup("UniformGroup")] public void Foo4() { } [ResponsiveButtonGroup("UniformGroup")] public void Foo5() { } [ResponsiveButtonGroup("UniformGroup")] public void Foo6() { } [OnInspectorGUI] private void Space3() { GUILayout.Space(20); } [ResponsiveButtonGroup("DefaultButtonSize", DefaultButtonSize = ButtonSizes.Small)] public void Bar1() { } [ResponsiveButtonGroup("DefaultButtonSize")] public void Bar2() { } [ResponsiveButtonGroup("DefaultButtonSize")] public void Bar3() { } [Button(ButtonSizes.Large), ResponsiveButtonGroup("DefaultButtonSize")] public void Bar4() { } [Button(ButtonSizes.Large), ResponsiveButtonGroup("DefaultButtonSize")] public void Bar5() { } [ResponsiveButtonGroup("DefaultButtonSize")] public void Bar6() { }
//Group 1 [FoldoutGroup("Group 1")] public int A; [FoldoutGroup("Group 1")] public int B; [FoldoutGroup("Group 1")] public int C; //Collapsed group,默认不展开 [FoldoutGroup("Collapsed group", expanded: false)] public int D; [FoldoutGroup("Collapsed group")] public int E; //使用‘$GroupTitle’字段的内容作为组名字,默认展开 [FoldoutGroup("$GroupTitle", expanded: true)] public int One; [FoldoutGroup("$GroupTitle")] public int Two; public string GroupTitle = "Dynamic group title";
[FoldoutGroup("测试按钮折叠")]
[Button("按钮1")]
public void FlodTest1()
{
}
[FoldoutGroup("测试按钮折叠")]
[Button("按钮2")]
public void FlodTest2()
{
}
功能:
TabGroup可以通过页签来拓展显示空间
TabGroup 包含2个重要的字段: GroupName(组名字)和 TabName(页签名字)
同 GroupName 组的会形成一个页签,可以在有限的空间里切换显示大大的容量
语法:
1个参数,相当于GroupName 为 空"",会被分配到同一个页签组里
[TabGroup(“TabName 页签名字”)]
2个参数,可以指定GroupName,同组的会被分配到同一个页签组里
[TabGroup(“GroupName 组名字”,“TabName 页签名字”)]
示例:
[TabGroup("Tab A")] public int One; [TabGroup("Tab A")] public int Two; [TabGroup("Tab A")] public int Three; [TabGroup("Tab B")] public string MyString; [TabGroup("Tab B")] public float MyFloat; [TabGroup("Tab C")] [HideLabel] public MyTabObject TabC; [TabGroup("New Group", "Tab A")] public int A; [TabGroup("New Group", "Tab A")] public int B; [TabGroup("New Group", "Tab A")] public int C; [TabGroup("New Group", "Tab B")] public string D; [TabGroup("New Group", "Tab B")] public float E; [TabGroup("New Group", "Tab C")] [HideLabel] public MyTabObject F; [Serializable] public class MyTabObject { public int A; public int B; public int C; }
// ShowIndexLabels是否显示序号,默认不显示 // DrawScrollView显示滑动条 // MaxScrollViewHeight 当高度高于多少时开始出现滚动条 [TableList(ShowIndexLabels = true,DrawScrollView = true, MaxScrollViewHeight = 200)] public List<CustomItem> List = new List<CustomItem>() { new CustomItem(), new CustomItem(), new CustomItem(), }; [Serializable] public class CustomItem { //指定宽度 [PreviewField(Height = 20)] [TableColumnWidth(30, Resizable = false)] public Texture2D Icon; //指定宽度 [TableColumnWidth(60)] public int ID; //隐藏该列 [HideInTables] public string Name; //图标默认值 [OnInspectorInit] private void CreateData() { Icon = ExampleHelper.GetTexture(); } }
// 普通下路框 [ValueDropdown("TextureSizes")] public int SomeSize1; private static int[] TextureSizes = new int[] {256, 512, 1024}; // 下拉框是string描述,值是int的下拉框 [ValueDropdown("FriendlyTextureSizes")] public int SomeSize2; private static IEnumerable FriendlyTextureSizes = new ValueDropdownList<int>() { {"Small", 256}, {"Medium", 512}, {"Large", 1024}, }; //下拉框是某个目录下的GameObject集合 [ValueDropdown("GetAllSirenixAssets", IsUniqueList = true)] public List<GameObject> UniqueGameobjectList; private static IEnumerable GetAllSirenixAssets() { var root = "Assets/Plugins/Sirenix/"; return UnityEditor.AssetDatabase.GetAllAssetPaths() .Where(x => x.StartsWith(root)) .Select(x => x.Substring(root.Length)) .Select(x => new ValueDropdownItem(x, UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(root + x))); } //下拉框是树形控件 [ValueDropdown("TreeViewOfInts", ExpandAllMenuItems = true)] public List<int> IntTreview = new List<int>() {1, 2, 7}; private IEnumerable TreeViewOfInts = new ValueDropdownList<int>() { {"Node 1/Node 1.1", 1}, {"Node 1/Node 1.2", 2}, {"Node 2/Node 2.1", 3}, {"Node 3/Node 3.1", 4}, {"Node 3/Node 3.2", 5}, {"Node 1/Node 3.1/Node 3.1.1", 6}, {"Node 1/Node 3.1/Node 3.1.2", 7}, };
public UnityEngine.Object SomeObject; [EnumToggleButtons] public InfoMessageType SomeEnum; public bool IsToggled; //1. 根据bool值 //当IsToggled = true;此字段Disable [DisableIf("IsToggled")] [ShowIf()()] public int DisableIfToggled; //2. 根据枚举值 //当枚举 SomeEnum = InfoMessageType.Info;此字段Disable [DisableIf("SomeEnum", InfoMessageType.Info)] public Vector2 Info; //当枚举 SomeEnum = InfoMessageType.Error;此字段Disable [DisableIf("SomeEnum", InfoMessageType.Error)] public Vector2 Error; //3. 根据GameObject //当GameObject SomeObject != null; 此字段Disable [DisableIf("SomeObject")] public Vector3 EnabledWhenNull; //4. 根据表达式 [DisableIf("@this.IsToggled && this.SomeObject != null || this.SomeEnum == InfoMessageType.Error")] public int DisableWithExpression;
[Title("Disabled in edit mode")]
[DisableInEditorMode]
public GameObject A;
[DisableInPlayMode]
public Material B;
Odin除了有丰富的特性来显示字段和方法,还支持创建窗口
现在我们可以回顾一下怎么创建一个原生的EditorWindow
public class OneWindow : EditorWindow { public string Text1; [MenuItem("Tools/打开Window")] private static void OpenWindow() { GetWindow<OneWindow>().Show(); } private void OnGUI() { Text1 = EditorGUILayout.TextField("输入Text1:",Text1); if (GUILayout.Button("按钮")) { Debug.Log("按下按钮"); } } }
接下来我们使用Odin来创建窗口。需要进行一点小改造
public class OneWindow : OdinEditorWindow { [LabelText("输入Text1")] public string Text1; [Button("按钮")] public void Btn() { Debug.Log("按下按钮"); } [MenuItem("Tools/打开Window")] private static void OpenWindow() { GetWindow<OneWindow>().Show(); } }
使用OdinMenuEditorWindow,我们可以实现一个包含多个窗口的窗口;
public class OdinMenuWindow : OdinMenuEditorWindow { [MenuItem("Tools/打开MeneuWindow")] private static void OpenWindow() { GetWindow<OdinMenuWindow>().Show(); } protected override OdinMenuTree BuildMenuTree() { var tree = new OdinMenuTree(); tree.Add("窗口1",new SubWindow());//添加OdinEditorWindow tree.Add("窗口2",new TestClass());//添加一个类 tree.Add("窗口2/子窗口",new ChildClass());//通过'/'来增加子窗口 return tree; } } public class SubWindow : OdinEditorWindow { [LabelText("窗口-SubWindow")] public string Text; } public class TestClass { [LabelText("窗口-TestClass")] public string Text; } public class ChildClass { [LabelText("窗口-ChildClass")] public string Text; }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。