TagSL框架设计(1)----先来点简介

开篇

       最近在做一个Silverlight的框架。 

       噢,这是一个仅仅工作一年半的没学历的我做的!!

       现在还在开发中.....     因最近领导要看。我想先写一篇简单的文章来介绍我的这种TagSL编程。

       事先声明,这种编程与传统的编程模式格格不入。不用ORM,不写实体类,设计模式貌似没有,甚至会要在XAML里写Sql语句哦,并且强制要求没特殊情况不能动服务端的代码啊!并且没用WCF等高深技术啊!甚至于你会发现怎么我还提倡XAML文件用中文命名!.............  如果你是自认为‘大牛’的并且喜欢乱喷的人,请绕道,寒舍承受不了您大驾光临。当然,如果你有一定的编程经验,并愿意以平等的交流技术和建议的口吻来沟通,小弟感激不尽。因为真理越辩越明的。当然也非常欢迎你提一些问题。我发誓会一一解答。

       噢,这篇文章是一篇介绍性的文章,又或者你可以理解为我52天后要开源的框架的预告版。所以不是‘附带源代码实例’的。请见谅呵呵。

 

大纲

           1、TagSL的核心目标

    2、把那些那么多的XAML放到服务端去。

           3、那两个牛逼的查询对象树的方法。

           4、不写实体类了?

           5、一个表单开始....

           6、怎么获得表单的值?

           7、这样弄有啥好处?

           8、约定大于配置??

1、TagSL的核心目标

       根据《TagSL配置文档》(还在书写中哈哈),将常见业务交给每个控件都有的Tag属性(用JSON字符安文档串描述业务),不常见的业务交给代码后置。

       (TagSL即Siverlight标签编程法,又或者叫Silverlight半配置编程法)

2、把那些那么多的XAML放到服务端去。

       在有大型的Silverlight项目的时候,碍于天朝的网速,我们常常郁闷的问题就是项目越大,我们要使用的XAML文件越多。而XAML文件是一次打包到客户端的。这样导致XAML过大。并且如果一个项目有100个XAML文件。当你修改了其中的一个,要发布,也要将整个项目编译一次才能看到效果。真麻烦!!

      我的TagSL做的第一件事就是就将XAML放到服务端。放到某个特定的目录下。在做实际项目开发的时候,每个单独的界面都实时从服务端下载(包括样式文件),下载到客户端之后使用 XamlReader.Load方法形成对橡树。

      或许你会问,那代码CS的文件呢?

      我会写一个方法,唯一的参数,参数就是这个XAML文件形成的对橡树,并在对橡树形成之后执行这个方法。...... 然后就OK 了。这样会有两个问题,1是这样会不会比传统的CS文件做代码后置有些功能不能实现?

      我的答案是是:如果你真正了解Silverlight,你会知道代码CS文件能做到的,我的这种机制也能做到。因为对橡树的根传过来了。我就能很轻易的操作这个对橡树。而且因为我的TagSL编程在编码过程中,要常常对XAML文件进行更改,反之用C#代码操纵对橡树的方面不多(初步估计在8.5:1的样子)。故在这样的模式下,采用将XAML放在服务端是更好的。

 

3、那几个牛逼的查询对象树的方法。

       这个两个方法我想不一定要在我的TagSL编程中。在其他的编程中同样可以用到。

       查找对橡树可以查孩子和父辈。而要知道Silvlight对橡树分为逻辑对橡树和可视化对橡树(至于其区别请自行百度,个人认为这两个概念灰常重要)。

      在HTML里我们最喜欢Jqurey了,呵呵动不动就是$()   这样去查找一个或者一些DOM元素。而在Silverlight里这样的方法也是非常重要的。呵呵。

       这两个方法在我这个框架中用的非常的多,而我相信他对你的编码也会有帮助,当然这是初始版本的,注释啥的都不全,若可以请等待一个月后的开源版本呵呵。

      包含的方法主要有。可视化对橡树和逻辑对橡树的  查询孩子和父辈元素的方法。特别注意过滤条件的写法噢 ....  呵呵

View Code
 #region 可视化对象树查询方式--孩子

public static List<T> Inspect<T>(DependencyObject dpObj, Func<T, bool> filter) where T : class
{
List<T> objList = new List<T>();
if (filter == null)
filter = _T => {
return true;
};
Inspect<T>(0, dpObj, ref objList, filter);
return objList;
}

private static void Inspect<T>(int level, DependencyObject dpObj, ref List<T> list, Func<T, bool> filter) where T : class
{
if ((dpObj is T))
{
if (filter(dpObj as T))
list.Add(dpObj as T);
}
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(dpObj); i++)
{
Inspect<T>(level + 1, VisualTreeHelper.GetChild(dpObj, i), ref list, filter);
}
}

#endregion

#region 对象树查询方式--孩子

/// <summary>
/// 查询孩子,用逻辑对象树
/// </summary>
/// <param name="Prent">根对象</param>
/// <param name="filter">过滤条件</param>
/// <param name="list">返回对象集合</param>
public static void getAllChild(DependencyObject Prent, Func<FrameworkElement, bool> filter, ref List<FrameworkElement> list)
{
if (((Prent is FrameworkElement)) && filter(Prent as FrameworkElement))
{
list.Add(Prent as FrameworkElement);
}


if (Prent is UserControl)
{
UserControl _contentControl = Prent as UserControl;
if (_contentControl.Content is DependencyObject)
getAllChild(_contentControl.Content as DependencyObject, filter, ref list);
}
if (Prent is ContentControl)
{
ContentControl _contentControl = Prent as ContentControl;
if (_contentControl.Content is DependencyObject)
getAllChild(_contentControl.Content as DependencyObject, filter, ref list);
}
if (Prent is Panel)
{
Panel p = Prent as Panel;
foreach (UIElement item in p.Children)
{
if (item is DependencyObject)
getAllChild(item as DependencyObject, filter, ref list);
}
}
if (Prent is ItemsControl)
{
ItemsControl p = Prent as ItemsControl;
foreach (var item in p.Items)
{
if (item is DependencyObject)
getAllChild(item as DependencyObject, filter, ref list);
}
}

}

public static List<T> getAllChild<T>(DependencyObject Prent, Func<T, bool> filter) where T : class
{
List<T> list_T = new List<T>();
if (filter == null)
filter = _frameworkElement => {
return true;
};
getAllChild<T>(Prent, filter, ref list_T);
return list_T;
}

public static void getAllChild<T>(DependencyObject Prent, Func<T, bool> filter, ref List<T> list) where T : class
{
if (((Prent is T)) && filter(Prent as T))
{
list.Add(Prent as T);
}


if (Prent is UserControl)
{
UserControl _contentControl = Prent as UserControl;
if (_contentControl.Content is DependencyObject)
getAllChild(_contentControl.Content as DependencyObject, filter, ref list);
}
if (Prent is ContentControl)
{
ContentControl _contentControl = Prent as ContentControl;
if (_contentControl.Content is DependencyObject)
getAllChild(_contentControl.Content as DependencyObject, filter, ref list);
}
if (Prent is Panel)
{
Panel p = Prent as Panel;
foreach (UIElement item in p.Children)
{
if (item is DependencyObject)
getAllChild(item as DependencyObject, filter, ref list);
}
}
if (Prent is ItemsControl)
{
ItemsControl p = Prent as ItemsControl;
foreach (var item in p.Items)
{
if (item is DependencyObject)
getAllChild(item as DependencyObject, filter, ref list);
}
}

}


#endregion

#region 查询祖辈
public static void getAncestor(DependencyObject dobj, Func<FrameworkElement, bool> filter, ref List<FrameworkElement> _ancestor)
{
if ((dobj is FrameworkElement))
{
FrameworkElement parent = dobj as FrameworkElement;
if (filter(parent))
_ancestor.Add(parent);
if (parent.Parent is FrameworkElement)
{
getAncestor(parent.Parent as FrameworkElement, filter, ref _ancestor);
}
}
}
public static void getAncestor<T>(DependencyObject dobj, Func<T, bool> filter, ref List<T> _ancestor) where T : FrameworkElement
{
T parent = dobj as T;
if (filter == null)
filter = _T =>
{
return true;
};
if (dobj is T && filter(parent))
_ancestor.Add(parent);
if (dobj is FrameworkElement)
{
getAncestor<T>((dobj as FrameworkElement).Parent, filter, ref _ancestor);
}
}
public static List<T> getAncestor<T>(DependencyObject dobj, Func<T, bool> filter) where T : FrameworkElement
{
List<T> _ancestor = new List<T>();
getAncestor<T>(dobj, filter, ref _ancestor);
return _ancestor;
}


#endregion

 

4、不写实体类了?

      关于要不要实体类,实体类到底带来了什么,我想我会再开博文呵呵。不过在这里要确定的一点是,在Silverlight里,不写实体类是可以的,但实体类不能没有!

     因为Silerlight最重要的一点就是数据绑定。而这里数据源就一定需要是对象。而这里的对象大部分就是我们从数据库中取出来的数据并放到实体类中的单个对象或者对象集合。

     但是不写实体类,怎么来‘对象呢’?-----反射!

    

View Code
    #region 形成实体类
public static List<String> GetCoumsByJson(string Json)
{
List<String> str_list = new List<string>();

try
{

JsonValue ja = JsonArray.Parse(Json);
foreach (string item in (((JsonObject)(ja[0]))).Keys)
{
if (!str_list.Contains(item))
str_list.Add(item.ToUpper());
}
}
catch (Exception e)
{
DealWithExpception(e, "根据Json获得要生成的实体类的列名的集合的时候出错了,JSON内容为" + Json);
}

return str_list;
}


public static Type getTypeByStrList(List<String> str_list)
{


string classInfo = "";
str_list.ForEach((s) => {
classInfo += s;
});

if (TagSlUserControl._AllTypes.ContainsKey(classInfo))
{
return TagSlUserControl._AllTypes[classInfo];
}

string className = "TempType" + Guid.NewGuid().ToString().Replace('-', 'a');
try
{
#region 反射形成类型的初始工作
AssemblyName an = new AssemblyName("TempAssembly" + Guid.NewGuid());
AssemblyBuilder assemblyBuilder =
AppDomain.CurrentDomain.DefineDynamicAssembly(
an, AssemblyBuilderAccess.Run);
ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule");
TypeBuilder typeBuilder = moduleBuilder.DefineType(className
, TypeAttributes.Public |
TypeAttributes.Class |
TypeAttributes.AutoClass |
TypeAttributes.AnsiClass |
TypeAttributes.BeforeFieldInit |
TypeAttributes.AutoLayout
, typeof(object));
ConstructorBuilder constructor =
typeBuilder.DefineDefaultConstructor(
MethodAttributes.Public |
MethodAttributes.SpecialName |
MethodAttributes.RTSpecialName);
#endregion

#region 给类型加字段


foreach (string item in str_list)
{
Type type = typeof(string);
FieldBuilder fieldABuilder = typeBuilder.DefineField(item, type, FieldAttributes.Private);
fieldABuilder.SetConstant("");
PropertyBuilder propertyABuilder = typeBuilder.DefineProperty(item, System.Reflection.PropertyAttributes.None, type, null);
MethodBuilder getPropertyABuilder = typeBuilder.DefineMethod("get",
MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig,
type,
Type.EmptyTypes);

ILGenerator getAIL = getPropertyABuilder.GetILGenerator();
getAIL.Emit(OpCodes.Ldarg_0);
getAIL.Emit(OpCodes.Ldfld, fieldABuilder);
getAIL.Emit(OpCodes.Ret);
//定义属性A的set方法
MethodBuilder setPropertyABuilder = typeBuilder.DefineMethod("set",
MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig,
null,
new Type[] { type });
//生成属性A的set方法的IL代码,即设置私有字段_a值为传入的参数1的值
ILGenerator setAIL = setPropertyABuilder.GetILGenerator();
setAIL.Emit(OpCodes.Ldarg_0);
setAIL.Emit(OpCodes.Ldarg_1);
setAIL.Emit(OpCodes.Stfld, fieldABuilder);
setAIL.Emit(OpCodes.Ret);
//设置属性A的get和set方法
propertyABuilder.SetGetMethod(getPropertyABuilder);
propertyABuilder.SetSetMethod(setPropertyABuilder);

}
#endregion

TagSlUserControl._AllTypes.Add(classInfo, typeBuilder.CreateType());
return TagSlUserControl._AllTypes[classInfo];
}
catch (Exception e)
{
string msg = "";
foreach (string item in str_list)
{
msg += item + ";";
}
DealWithExpception(e, "根据字段生成实体的时候错了,getTypeByStrList(),字段内容为" + msg);
return "".GetType();
}
}


#endregion

 =

   如我的代码,其中有两个方法。第一个是根据JSON数据形成一个List<String> 的集合,然后根据这个集合的‘字段’,遍历形成一个实体类。当然在这个时候必须要关注的是,反射会影响性能吗?

我的回答是,不会!为什么呢?因为我的反射机制.....  呵呵 Emit不是盖的!

   当然需要注意的是,这里的JSON数据是根据DataTable形成的。

   而接下来我们只需要采取反序列化的方式,即可将数据放到这个实体里或者实体集合里。然后写一个 _panel.DataContext=实体或者实体集合...  即可。

5、举例说明一个‘表单’的配置

         如果你看到了这。我可以明确的告诉你,上面四点。其实就是我的TagSL编程的最核心的东西 呵呵。 如果说我这个框架用到了什么‘高深’的技术的话,上面几点算是最高深的了呵呵。

         下面直入主题。我们来配置一个简单的表单配置

         下面就是我的界面的样子 呵呵。

         

   

   而配置XAML如下

  

View Code
<UserControl
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x
="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d
="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc
="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable
="d" Tag=" {PKValue:'9A1D8CC2-8789-5B1C-ABB7-334C94F9FA51'} "
xmlns:Intersoft
="http://intersoft.clientui.com/schemas"
xmlns:Converters
="clr-namespace:Contacts_MVVM.Converters"
xmlns:System
="clr-namespace:System;assembly=mscorlib"
xmlns:design
="clr-namespace:Contacts_MVVM.ViewModels"
x:Class
="MyXaml.UserControl1" FontFamily="Arial,SimSun"
d:DesignWidth
="640" d:DesignHeight="480">
<UserControl.Resources>
<Style x:Key="TextBoxStyle" TargetType="Intersoft:UXTextBox">
<Setter Property="Margin" Value="10,0,0,0"/>
<Setter Property="Height" Value="26.2"></Setter>
</Style>
<Style x:Key="FieldLabelStyle" TargetType="Intersoft:StylishLabel">
<Setter Property="Width" Value="80"/>
<Setter Property="HorizontalContentAlignment" Value="Right"/>
<Setter Property="Padding" Value="4,0"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="Background" Value="{x:Null}"/>
<Setter Property="BorderBrush" Value="{x:Null}"/>
<Setter Property="Foreground" Value="#FF6F6D6D"/>
</Style>
<Style x:Key="FormStyle" TargetType="StackPanel">
<Setter Property="Orientation" Value="Horizontal"/>
<Setter Property="Margin" Value="0,2"/>
<Setter Property="MinHeight" Value="20"/>
</Style>
</UserControl.Resources>

<Grid>
<Grid Background="White" Tag=" {TableName: 'Test-Student', 'ControlType': 1} ">
<Intersoft:ExpandableGroupBox FontSize="14" Header="学生【基本信息】">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="42"/>
<RowDefinition Height="42"/>
<RowDefinition Height="42"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="0.33*"/>
<ColumnDefinition Width="0.33*"/>
</Grid.ColumnDefinitions>
<StackPanel Style="{StaticResource FormStyle}" Orientation="Horizontal" Grid.Row="0" Grid.Column="0">
<Intersoft:StylishLabel Style="{StaticResource FieldLabelStyle}" Content="姓名:" />
<Intersoft:UXTextBox Style="{StaticResource TextBoxStyle}" Width="180" Tag=" {ActField:'StudentName'} "/>
</StackPanel>
<StackPanel Style="{StaticResource FormStyle}" Orientation="Horizontal" Grid.Row="1" Grid.Column="0">
<Intersoft:StylishLabel Style="{StaticResource FieldLabelStyle}" Content="邮箱:" />
<Intersoft:UXTextBox Style="{StaticResource TextBoxStyle}" Width="180" Tag=" {ActField:'Email',RegEx:'\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*'} "/>
</StackPanel>
<StackPanel Style="{StaticResource FormStyle}" Orientation="Horizontal" Grid.Row="1" Grid.Column="1">
<Intersoft:StylishLabel Style="{StaticResource FieldLabelStyle}" Content="移动电话:" />
<Intersoft:UXTextBox Style="{StaticResource TextBoxStyle}" Width="180" Tag=" {ActField:'Call'} "/>
</StackPanel>
<StackPanel Style="{StaticResource FormStyle}" Orientation="Horizontal" Grid.Row="0" Grid.Column="1" >
<Intersoft:StylishLabel Style="{StaticResource FieldLabelStyle}" Content="生日:" />
<Intersoft:UXDateTimePicker Margin="10,10,0,0" Width="180" Tag=" {ActField:'Birthday'} "/>
</StackPanel>
<StackPanel Style="{StaticResource FormStyle}" Orientation="Horizontal" Grid.Row="2" Grid.Column="1">
<Intersoft:StylishLabel Style="{StaticResource FieldLabelStyle}" Content="班级:" />
<Intersoft:UXComboBox Height="22" Margin="10,0,0,0" Width="180" Tag=" {ActField:'ClassID',ShowField:'[Test-Class].ClassName'} ">

</Intersoft:UXComboBox>
</StackPanel>
<StackPanel Style="{StaticResource FormStyle}" Orientation="Horizontal" Grid.Row="2" Grid.Column="0">
<Intersoft:StylishLabel Style="{StaticResource FieldLabelStyle}" Content="年级:" />
<Intersoft:UXComboBox Height="22" Tag=" {ShowField:'[Test-Grade].GradeName',CascadeSql:'
SELECT dbo.[Test-Class].ClassName, dbo.[Test-Grade].GradeName
FROM dbo.[Test-Class] INNER JOIN dbo.[Test-Grade] ON dbo.[Test-Class].GradeID = dbo.[Test-Grade].GradeID'
} "

Margin
="10,0,0,0" Width="180">

</Intersoft:UXComboBox>
</StackPanel>
</Grid>


</Intersoft:ExpandableGroupBox>

<Intersoft:UXCommandBar Height="44" VerticalAlignment="Bottom" Grid.Row="1" FontSize="12">

<Intersoft:UXButton x:Name="btnSave" Tag=" {BtnType:'Save'} " Content="保存" Width="80" IsDefault="True"
DialogResult
="None"/>
<Intersoft:UXButton x:Name="bbbbbbbbbbbbbbbbbbbbbbbbbbb" Content="取消" Width="80" IsCancel="True"/>

</Intersoft:UXCommandBar>
</Grid>
</Grid>
</UserControl>

 

 

 首先我确定的一件事是,表单元素的容器一定是一个Pannl。(几乎一定是Grid,呵呵)。

 

 我们会在这个Pannl上配置(注意这里的配置即Tag的值

{TableName: 'Test-Student',  'ControlType': 1}

 这样的话,我的控件初始化器(其实就是几个方法)就会找到这个Pannel并根据ControlType为1 知道这是一个表单的容器。并知道这个表单对应的表是Test-Student这个表。

 接下来每个字段都回有相应的配置。你会注意到几乎都有{:''}   这个配置。这个配置我的用意就是告诉表单初始化器这个表单拥有的这个字段。还有日期字段。日期字段我的控件初始化器会自动识别其为日期字段,并做响应的转换。当然你会注意到还有下拉列表框。注意如果这个下拉列表框的值是由另一个表的话。配置的方法是这样   ShowField:'[Test-Class].ClassName'    。即‘[表名].字段名’的方式。  这样这个下拉列表框就会自动的填充值。呵呵。

 特别要注意以下几点

A:我的服务端会根据表名自动的知道每个表单的表的主键是哪一个。甚至我会根据表的字段信息查询这个字段的非空属性等等。

B:还有一些功能比如每个字段还可以加入正则表达式验证等等。而这些只需要在XAML的控件的Tag里加一个简单的配置而已呵呵。

C:而且如果你自信你会发现UserControl里有这么一个配置 Tag=" {PKValue:'9A1D8CC2-8789-5B1C-ABB7-334C94F9FA51'} "。呵呵,这个PKValue就是指当前这个UserControl------这个样的UserContrl我称之为  Page,即一个界面。这个PKValue就是我定义的一个界面级别的值。  注意上图我的那个 表单里头的值就是通过这个主键找到的。当然这么长的一个主键是不需要你配置呵呵。比如当我们在一个列表当中点击某一条数据编辑的时候,我们的做法要将UserControl的Tag属性赋值。就象我们在HTML的列表里点击某条数据编辑一条记录弹出一个窗口我们要URL传值过去一样呵呵》。。。。。     举这例子只为说明Tag在我的框架里除了作为配置之外。另外一个重要的用途就是‘界面传递值’ 呵呵。

D:我会根据ActField这个配置自动的生成一个绑定器,当然是双向绑定。

6、怎么获得‘表单’的值呢?

        string data = JsonConvert.SerializeObject(_panel.DataContext);

       这里的data就是我们要获得的表单的所有的值,JsonConvert是一NewTonSoft里的一个类。SerializeObject是反序列化。_panel就是我们的那个表单。

       呵呵,没错。我们这样就获得了表单当中的值。呵呵。

 

7、这样弄有啥好处呢?

       A: 加快了编程速度。传统咱们要弄一个‘表单’啥的要前后折腾半天。而且那啥下拉列表框的级联等等也要搞半天.....   在我这你只需要两个配置即可。呵呵而且不只表单哦。。。。  列表,树,ListBox......。都能用这个配置直接配置出来噢.....

      B:让代码更加统一了。其实以前我们的开发。比较蛋疼的事情就是如果 十个人实现同一个功能,往往写出来的代码会有十个模样(即便是团队有着非常详细的编码文档,代码也可能五花八门)。当我们做代码交接的时候往往要将代码解释半天。而用这样的配置,则会让代码风格非常统一。

      C:牛逼的培训。呵呵,正式版本发布的时候,一定是伴随这非常详细的文档,实例例子,甚至视频教学噢。我想一个框架给力不给力其实很重要的一部分就是学习资料爽不爽。我想这一点因为哥的存在不会差  哈哈。

 

 

8、约定大于配置?

       这话我想你听过。。。。呵呵。编程大牛常常说这话。那么按照这个字面意思理解,是不是我的编程方式不如‘约定编程’呢。

       错。呵呵,要知道这话你是不能只理解面的意思的。

      约定大于配置本意就是要约束开发人员按照某约定的模式编程,尽量把更多的东西约定好统一好,而不是‘配置’(此配置非彼配置)。

     那么我的这种编码模式刚好符合这种理念。又或者我的编码模式可以叫约定型编程。即约定了咱们就这么在Tag里写那些来实现业务呵呵。

 

未完待续

       呵呵,现在我的TagSL还在开发期,还有一些细节的一些东西需要优化。我曾说过要在52天后放出一个开源版本。呵呵。我明年可能还会将工作流啥的集成进来噢.... 哈哈。

  

      诚然,我想我一定会遭到一部分兄弟的喷击。毕竟我才入行一年,就居然说要做框架。怎么这么狂妄呵呵(一个哥们对我说的原话)......     其实我只是想寻找更优秀的软件制作方式而已。唉....

      噢,忘了说了,虽然我只编程了一年半,但要注意的是我的框架是站在居然的肩膀上...   呵呵。

      为什么我会如此执着的弄着一个‘框架’?为什么我一个才两年不到编程的人敢于弄框架?为什么我对‘配置’如此.....    请看下集 《我在hangar学配置》

      谈的是在一个几乎天天谈配置的公司的学习经历。

      这家公司有不懂编程语言的人凭配置做了两年开发了..  你信吗??

 

posted @ 2011-12-11 22:10  银光小子  阅读(3364)  评论(19编辑  收藏  举报