Intraweb之EasyUI篇

来源:互联网 发布:拳皇97风云再起 mac 编辑:程序博客网 时间:2024/06/05 15:39
Intraweb一直是Delphi快速开发web应用的首选工具,但自带的控件较少,样式比较难看,TMS与IW倒是可用,可是要收费,对于我们这些习惯用免费的用户来说,想找个破解也比较费劲。EasyUI是基于JQuery开发的框架,内置的控件完全可以满足我们开发一般web程序的需求,而且是免费的,用起来也心安理得。下面我就IW如何结合EasyUI开发程序谈谈自己的一些心得,与大家交流一下。主要有以下几种方法:
  一、使用模板
  在IWForm内使用模板引入做好的html文件,结合IW自身的控件进行操控。这种方法虽说比较方便,但模板也有自身的缺点,内部不支持中文引用是一大Bug,目前IW都没有要解决的迹像。如果一定要用模板,也有方法规避,即汉字全部用网页转义“&#”加汉字的十进制编码。模板的使用有很多文章可供参考,也不是本章的重点,不做具体讲解。
  二、MVC设计模式
  IW使用MVC方式结合EasyUI设计程序,是本文的重点。我们知道IW与HTML静态页面的交互,可以通过javascript接口来实现,可以使用AddToInitProc('alert("欢迎")')这样的语句,也可以在控件的JavascriptEvent属性内添加js语句。本文介绍的方法完全将界面与数据处理分开,一律使用EasyUI来实现界面(完全不用IW的可视控件,数据库控件还是需要的),数据处理交给IW后台做。我们以开发一个简单的应用程序来一步步实现这些功能,同时会使用一定篇幅介绍EasyUI部分控件的使用(本文必须要有一定的javascript基础)。
  第一步:实现登陆界面。

  首先引入以下文件,后面其它页面也一样这样引用,直接贴代码:

 <span style="white-space:pre"></span><link rel="stylesheet" type="text/css" href="easyui/themes/default/easyui.css"><link rel="stylesheet" type="text/css" href="easyui/themes/icon.css"><script type="text/javascript" src="easyui/jquery.min.js"></script><script type="text/javascript" src="easyui/jquery.easyui.min.js"></script><script type="text/javascript" src="easyui/locale/easyui-lang-zh_CN.js"></script>
   这些文件是必须引用的,easyui.css是自带的样式,icon.css是使用中的各种图标,easyui-lang-zh_CN.js是汉化文件(EasyUI对中文支持还是很不错的,如果觉得汉化得不够好,可以打开这个文件自行修改)。界面部分:

<form id="ff" class="easyui-form" method="post" data-options="novalidate:true"><!--form提供了各种方法来操作执行表单字段,比如:ajax提交, load, clear等等。当提交表单的时候可以调用validate方法检查表单是否有效。“data-options”控件的各种属性,form有以下属性:属性名类型描述默认值novalidateboolean    定义是否验证表单的字段,true:验证,false:不验证。falseajaxboolean    定义是否使用ajax提交表单,true:使用,false:不使用。truequeryParams    object    当表单被提交到服务器的时候增加的额外参数列表。{}urlstring    提交表单动作的URL地址null-->    <table cellpadding="5">    <tr>    <td>用户名:</td>    <td><input class="easyui-textbox" type="text" name="username" data-options="required:true" style="width:150px"/></td> <!--TextBox(文本框)是一个增强的输入字段组件, 它允许用户非常简单的创建一组表单。它是一个用于构建其他组合控件的基础组件,如:combo,databox、spinner等 required:true表示文本框不能为空,下同。-->    </tr>    <tr>    <td>密 码:</td>    <td><input class="easyui-textbox" type="password" name="passw" data-options="required:true" style="width:150px"/></td>    </tr>    </table>    </form>    <div style="width:216px;padding:5px 0px;height:30px">    <a href="javascript:void(0)" class="easyui-linkbutton" onclick="submitForm()" style="width:80px;float:left">登陆</a><!--easyui-linkbutton按钮组件,使用超链接按钮创建,提示:不要将它改为button类别,IE9以下浏览器会不正常,submitForm()提交数据,clearForm()清空数据-->    <a href="javascript:void(0)" class="easyui-linkbutton" onclick="clearForm()" style="width:80px;float:right">取消</a>    </div>    </div></div>
  登陆界面基本完成,有些简陋,当然可以自行修改。下面实现提交数据和清空数据,用Javascript:

function submitForm(){$('#ff').form('submit',{//这是EasyUI的Form自带功能,就是提交数据url:'Login.php', //需要把数据提交到的页面onSubmit:function(){//验证数据是否为空,如果为空就返回。return $(this).form('enableValidation').form('validate');},success: function(data){//提交成功后的回调函数,data就是返回的数据if(parseInt(data)==1)//我们在这里返回1和0,1表示成功登陆,在后台实现{window.location='main.html';//登陆成功后,跳转到主程序}else{$.messager.alert('错误','用户名或密码错误!','error');/*EasyUI消息提示框,就是alert的改进用法,显示警告窗口。参数(依次调用):title:在头部面板显示的标题文本。msg:显示的消息文本。icon:显示的图标图像。可用值有:error,question,info,warning。fn: 在窗口关闭的时候触发该回调函数。 */$('#ff').form('clear');//清空数据,下同。}}    });}function clearForm(){$('#ff').form('clear');}

  文件另存为“index.html”,即首页,放在wwwroot下(注意easyui的相关文件也要放在这个目录下),启动程序后,就是直接访问这个页面了,没有“$”这个标志。
  登陆界面基本完成,数据需要提交到“Login.php”这个页面,按一般的做的法,新建一个IWForm,使用模板加载文件,本文用另一种思路,也是本文的关键:
用delphi新建一个Unit,命名Login单元,加入IW工程。

直接贴出代码(参考万一博客):

{新建Login 单元, 从 TContentBase 继承实现一个 TLogin 类}unit Login;interfaceuses Classes, IW.Content.Base, System.SysUtils,HTTPApp, IWApplication, IW.HTTP.Request, IW.HTTP.Reply, IWMimeTypes;type  TLogin = class(TContentBase)  protected    function Execute(aRequest: THttpRequest; aReply: THttpReply; const aPathname: string; aSession: TIWApplication; aParams: TStrings): Boolean; override;  public    constructor Create; override;  end;implementation uses ServerController,UserSessionUnit;{ TLogin }constructor TLogin.Create;begin  inherited;  mFileMustExist := False;end;function TLogin.Execute(aRequest: THttpRequest; aReply: THttpReply; const aPathname: string; aSession: TIWApplication; aParams: TStrings): Boolean;begin  aReply.ContentType := MIME_HTML;  aReply.WriteString('这里就是返回到客户端的数据');  Result := True;end;end. {在 IWServerControllerBase.OnConfig  映射login.php}uses  IWInit, IWGlobal, IW.Content.Handlers, Login;procedure TIWServerController.IWServerControllerBaseConfig(Sender: TObject);begin  THandlers.Add('', 'login.php', TLogin.Create);//添加虚拟文件名,映射到服务器end;

  直接列出代码大家可以不太清楚怎么回事,这里说明一下流程:
  客户端通过Form提交用户名和密码到“Login.php”,“Login.php”是通过服务器添加的一个虚拟文件,映射到从TContentBase继承实现的TLogin类,用THttpRequest接收提交的数据,并进行处理,用THttpReply.writestring写入返回客户端数据。这样登陆过程前台与后台代码均完成。

  第二步:实现主界面
  我们开发的是一个商品信息管理程序,主界面用EasyUI的Layout实现自适应浏览器(记得引入相关js和css):
<div data-options="region:'north',border:false" style="height:60px;background:#B3DFDA;padding:0px 10px;text-align:center"><h3>商品信息管理系统</h3></div><div data-options="region:'west',split:true,title:'商品分类'" style="width:200px;padding:10px;"></div><!--<div data-options="region:'east',split:true,collapsed:true,title:'East'" style="width:100px;padding:10px;">east region</div>--><div data-options="region:'south',border:false" style="height:50px;background:#A9FACD;padding:10px;">京ICP证000000号</div><div data-options="region:'center',title:'商品简要信息'"></div>

  很好理解,即左西右东,上北下南加上中央的布局,右边不需要,我把它注释掉。

  页面设计思路是这样的,左边放一个Tree,用来显示商品分类,中央放GridData,用来显示商品信息列表,通过两个控件实现删除、添加、修改功能。
  west这个DIV内加入Tree:
<ul id="easyui_tt" class="easyui-tree" data-options="animate:true,//动画lines:true,//显示树线url:'Treedata.php',//上面有解释,需要提交的页面method:'post',//提交方式Post,再强调一下必须用PostonClick: function(node){//鼠标单击事件QueryByID(node.id);//通过node.id来查询数据,讲DataGrid时再说},onContextMenu: function(e, node){//右键菜单e.preventDefault();//必须用的$(this).tree('select', node.target);//选择的Node$('#mm').menu('show', {//EasyUI的菜单,非常简单left: e.pageX,//弹出菜单的位置top: e.pageY});}"></ul>
  Tree的属性很多,其中一个比较重要的是node,即Tree的节点每个节点都具备以下属性:
  id:节点ID,对加载远程数据很重要。
  text:显示节点文本。
  state:节点状态,'open' 或 'closed',默认:'open'。
  如果为'closed'的时候,将不自动展开该节点。
  checked:表示该节点是否被选中。
  attributes: 被添加到节点的自定义属性。
  children: 一个节点数组声明了若干节点
  Tree的节点是通过url提交请求到服务器接收返回数据加载的,形成
  树的数据是JSon格式,我们可以分析一下:
[{        "id": 1,//对应node的ID,其他也是一一对应的        "text": "Node 1",        "state": "closed",        "children": [{ //子node           "id": 11,            "text": "Node 11"       },{            "id": 12,            "text": "Node 12"       }]    },{        "id": 2,        "text": "Node 2",        "state": "closed" //不展开节点 }]}  
  同上面的“登陆”,我们从TContentBase继承实现一个 TTreeData 类直接复制模板,修改一个即可,注意加入IW工程,并在ServerController内映射“TreeData.php”。我们现在需要通过delphi来实现树,Tree的层越多就越复杂,我发现
不管通过什么语言动态实现Tree,都是非常麻烦的一件事,EasyUI的例子只能实现两层树。从数据库读取Tree数据,在数据库设计的时候有一个技巧,不知道大家是怎样处理的,我这里说一个我的方法:树的上下级之间用代码表示,2位数字代表根,4位数字代表下一级,依此类推,代码不能用纯数字,这样不好排序,我在数字前加个字母,这样通过“select*from Tree order by id”就可以把上下级排列在一起,而不是按代码大小排序。数据库就不多讲了,不在本文的范围,大家看一下我的源码里的数据库就知道了。建树代码如下(本想用JSon,无奈学不到家,只能用字符串拼接):
function BuildTree:string;var i,j,old_ln,new_ln:Integer; id,s,title,ft:string;beginft:='{"id":"%s","text":"%s"},';//Json格式with UserSession.FDQuery1 dobegin  Open('select*from Tree order by id'); //按id排序可以将父子节点正好罗列在一起  s:='[';  old_ln:=0;//初始化开始节点ID的长度  for i := 0 to RecordCount-1 do    begin       id:=Fields.Fields[0].AsString;       title:=Fields.Fields[1].AsString;       new_ln:=id.Length-3;//新节点ID的长度,减去3除去了根节点的长度,方便计算       //通过比较与上一节点ID的长度来判断节点的上下级关系       if (new_ln=old_ln) then//与上一节点同等级          s:=s+Format(ft,[id,title]);       if new_ln>old_ln then //上一节点为父节点          begin            s:=s.Substring(0,s.Length-2);            s:=s+Format(',"state":"closed","children":['+ft,[id,title]);          end;       if (new_ln<old_ln) then //上一节点为子节点          begin            s:=s.Substring(0,s.Length-1);            for j :=1 to (old_ln-new_ln) div 2 do              s:=s+']}';            s:=s+Format(','+ft,[id,title]);          end;      Next;      old_ln:=new_ln;//将当前节点ID长度赋予旧节点    end;end;   s:=s.Substring(0,s.Length-1);   for i := 1 to new_ln div 2 do //结束时需要判断是否为子节点,有几层     s:=s+']}';   result:=s+']';end;
  以上代码已经注释,有什么不明白的地方我们再交流,可以实现N多级树,只要客户端支持,有的控件是不支持多级树的。Tree实现了,我们再实现右键菜单,onContextMenu:
onContextMenu: function(e, node){e.preventDefault();$(this).tree('select', node.target);$('#mm').menu('show', {left: e.pageX,top: e.pageY});
  注意$('#mm')这个就是右键菜单的JQuery标识,我们做一个删除、添加功能,代码如下:
<div id="mm" class="easyui-menu" style="width:120px;"><div onclick="addnode()" data-options="iconCls:'icon-add'">添加</div><div onclick="removeit()" data-options="iconCls:'icon-remove'">删除</div></div>

  提示:EasyUI很多情况下只需要引用$('')类似的标识就可以将其他控件加进去。实现addnode()、removeit()以及其他功能:

<span style="white-space:pre"></span>function appendn(r){//添加节点var t = $('#easyui_tt');var node =t.tree('getSelected');var pii=node.id;$.ajax({  type : "post",  url : "Treedata.php",  data : {Action:'Add',ID:pii,Title:r},  async : false,//这里必须用同步  success : function(data){  pii=data;}  });t.tree('append', {parent: (node?node.target:null),data: [{id:pii,text:r}]});}function removeit(){//删除节点var node = $('#easyui_tt').tree('getSelected');var pii=node.id;$.post('Treedata.php',{Action:'Del',ID:pii});$('#easyui_tt').tree('remove', node.target);}function collapse(){//树折叠var node = $('#easyui_tt').tree('getSelected');$('#easyui_tt').tree('collapse',node.target);}function expand(){//树展开var node = $('#easyui_tt').tree('getSelected');$('#easyui_tt').tree('expand',node.target);}function addnode(){//弹出添加节点对话框,用消息框$.messager.prompt('添加', '请输入需要添加的名称:', function(r){if (r){appendn(r);}});}

  这段代码是用JQuery实现向IW提交数据,即把data以JSon格式提交到服务器$.post('Treedata.php',{Action:'Del',ID:pii});提交删除功能,是$.ajax的简单实现,顺便说一下,万一的博客提到
function TestPost(){var mydata="TestMYPost测试一下";executeAjaxEvent("&data="+mydata, null, "DoCallBack1", false, null, false);//中文在IE下乱码}
  这样提交数据,用WebApplication.RegisterCallBack('IWCallBack1', DoCallBack1) 注册回调接收数据,我觉得用JQuery的post实现更简单,IWForm内用$.post需要这样:
$.post(GURLBase+"callback?",         {callback:"DoCallBack1",data:"测试一下可以吗-----?"},         function(data){processAjaxResponse(data);},"xml");//必须是xml格式}//效果是一样的,也需要注册回调函数
  注:GURLBase等于'/$/'(看着非常不爽的美元符号),修改一下万一的代码:
function TestPost(){var mydata=escape("TestMYPost测试一下");executeAjaxEvent("&data="+mydata, null, "DoCallBack1", false, null, false);//中文在IE下乱码,需要escape}
这样也支持中文了。
  服务器如何处理数据,登陆界面已经详解,基本类似,添加删除也不再列代码,大家可以直接看我的源码,用delphi实现真的很简易。
  Tree讲完,我们接着讲DataGrid:
  center这个DIV内加入:
<table class="easyui-datagrid" style="width:100%;height:400px" data-options="singleSelect:true,collapsible:true,fitColumns:true,url:'GridData.php',method:'post',pageSize:10,pagination:true,onDblClickRow:onDClickRow" <!--相同的属性不再说明,singleSelect选择单行collapsible定义是否显示可折叠按钮,EasyUI大部分控件继承自panel,一般可折叠fitColumns列宽自适应pageSize分页时每页显示的行数pagination是否分布-->toolbar="#dg_tb"//工具栏,EasyUI可以这种方式嵌入其他控件id="easyui_tb"><thead><!--头部--><tr><th data-options="field:'codeID',width:80,halign:'center',editor:'text'">商品编号</th><!--field对应数据库字段   halign标题居中editor:'text'编辑样式为文本框align:'center'整列居中--><th data-options="field:'p_name',width:100,halign:'center',editor:'text'">名称</th><th data-options="field:'p_type',width:80,halign:'center',align:'center',editor:'text'">型号</th><th data-options="field:'p_tid',width:80,halign:'center',align:'center',editor:'text'">类别</th><th data-options="field:'p_pinpai',halign:'center',width:250,editor:'text'">品牌</th><th data-options="field:'p_price',halign:'center',width:60,align:'center',editor:'text'">价格</th><th data-options="field:'p_discount',halign:'center',width:60,align:'center',editor:'text'">折扣</th></tr></thead></table><div id="dg_tb" style="padding:3px"><!--工具栏--><span>商品编号</span><input id="codeID" class="easyui-numberbox" style="line-height:22px;border:1px solid #ccc"><span>商品名称</span><input id="p_name" class="easyui-textbox" style="line-height:22px;border:1px solid #ccc"><a href="#" class="easyui-linkbutton" plain="true" onclick="doSearch()">查询</a><a href="javascript:void(0)" class="easyui-linkbutton" data-options="iconCls:'icon-add',plain:true" onclick="appendr();">添加</a><a href="javascript:void(0)" class="easyui-linkbutton" data-options="iconCls:'icon-remove',plain:true" onclick="remover()">删除</a><a href="javascript:void(0)" class="easyui-linkbutton" data-options="iconCls:'icon-save',plain:true" onclick="acceptr()">修改</a><a href="javascript:void(0)" class="easyui-linkbutton" data-options="iconCls:'icon-undo',plain:true" onclick="rejectr()">撤消</a></div>
DataGrid功能强大,也很复杂,本文只讲基本应用,大家可以看我上传的EasyUI帮助文件。从服务器端获取表格数据同上,数据也是JSon格式的,建一个MyGridData,上面讲Tree时留下一下函数未讲解QueryByID(node.id),JS如下:
function QueryByID(id){//以节点ID查询var tb=$('#easyui_tb');tb.datagrid({queryParams:{Action:'Q_ID',ID:id}});/*queryParams是DataGrid提交数据时的参数,也可以直接:tb.datagrid('load',:{Action:'Q_ID',ID:id}});但是在测试中发现,这样提交后数据为空时,表仍然显示有数据,也许是Bug,也许是我不会用。load即是post数据到服务器,同时接收返回数据,GridData全部封闭好了。*/tb.datagrid('load');}

  以ID查询数据在服务器端这样实现:

function QueryData(config:string):string;var arrjson:JSONArray; ajson:JSONObject; i,j:integer;begin  arrjson:=JSONArray.Create;  ajson:=JSONObject.Create;  with UserSession.FDQuery1 do   begin      Open('select*from product where '+config);      for I :=0 to RecordCount-1 do       begin         for j := 0 to Fields.Count-1 do            ajson.Put(Fields.Fields[j].DisplayName,Fields.Fields[j].AsString);//形成'{aaa:"BBB",ccc:"DDDD"}'这样的字符串,不需要拼接字符串了。         arrjson.AddJSON(ajson.ToString(4));//字面上理解就是JSon数组,即[{},{}];         ajson.Clear;//清除ajson内的数据,不然ajson会不停put数据,类似js的push用法          Next;       end;       end;  Result:='{"total":'+i.ToString+',"rows":'+arrjson.ToString(4)+'}';//ToString(4)表示以4个空格缩进,不这样使用json数据会被转义//datagrid数据多出的total是分页时用到的,表示总行数,rows表示当前显示页面 //如果不分页,可以直接: Result:=arrjson.ToString(4)';   arrjson.Free;  ajson.Free;end;

  我是直接以查询条件为参数的,以便于扩展,这里用到了yxdJson,在我上传的控件中有,比较好用,其实就是形成'{aaa:"BBB",ccc:"DDDD"}'这样的语句,特别简洁,不用拼接字符串(拼接字符串是很痛苦的)。Tree的数据我也想用的,但死活不行,只好放弃。GridData是用服务器实现分页的,也特简单,即提交page和rows这个两个参数到服务器,代码大家自已下载,delphi实现也非常简单,sql查询时加入limit (page-1)*rows,rows条件即可。注意字符串与整数的变换。实现查询、删除、添加、修改功能,客户端js:
function doSearch(){//查询功能$('#easyui_tb').datagrid('load',{Action:'Q_DN',id: $('#codeID').val(),p_name: $('#p_name').val()});}//load参数即可,上面有讲解var editIndex = undefined;var ExecType='';function endEditing(){//结束编辑if (editIndex == undefined){return true}if ($('#easyui_tb').datagrid('validateRow', editIndex)){var ed = $('#easyui_tb').datagrid('getEditor', {index:editIndex,field:'codeID'});$('#easyui_tb').datagrid('endEdit', editIndex);editIndex = undefined;return true;} else {return false;}}function onDClickRow(index){//双击编辑整行数据if (editIndex != index){if (endEditing()){var  tt=$('#easyui_tb').datagrid('selectRow', index);var EditID=tt.datagrid('getSelected')['codeID'];//选择行的codeID值tt.datagrid('beginEdit', index);editIndex = index;ExecType='update,'+EditID;//提交到服务器update} else {$('#easyui_tb').datagrid('selectRow', editIndex);//数据库必须依靠主键为标志来更新。}}}function appendr(){//添加if (endEditing()){$('#easyui_tb').datagrid('appendRow',{p_discount:'1.0'});editIndex = $('#easyui_tb').datagrid('getRows').length-1;$('#easyui_tb').datagrid('selectRow', editIndex).datagrid('beginEdit', editIndex);//添加一行空行 ExecType='insert into,';//提交到服务器insert}}function remover(){//删除var  tt=$('#easyui_tb').datagrid('getSelected');//找到选择行if (tt==undefined) return;//没有选择就退出var Delindex=$('#easyui_tb').datagrid('getRowIndex',tt);//选择行的行号var DelID=tt['codeID'];//主键,用于删除$.messager.confirm('删除','您确认想要删除记录吗?',function(r){ if (r){$('#easyui_tb').datagrid('deleteRow', Delindex);$.post('GridData.php',{Action:'delete',id:DelID});//提交delete}});  editIndex = undefined;//这个本程序没用上,是单击时用的}function acceptr(){//修改编辑的数据,添加或编辑后,需要修改数据,提交到服务器//不修改只是客户端更新,服务器端数据库没变if (endEditing()){$('#easyui_tb').datagrid('acceptChanges');if (ExecType=='') return;var selrow=$('#easyui_tb').datagrid('getSelected');var row=new Array();if(selrow!=undefined)row.push(selrow['codeID'],selrow['p_name'],selrow['p_type'],selrow['p_tid'],        selrow['p_pinpai'],selrow['p_price'],selrow['p_discount']);//push就是将数据压入数组var param=ExecType.split(',');//分割字符串为数组,delphi的用法类似if(param[0]=='update'){$.post('GridData.php',{Action:param[0],id:param[1],Rowdata:row.toString()},function(data){alert(data)});//update时要提交动作、codeID和更新后的数据,//post的参数function(data){alert(data)就是服务器返回数据。}if(param[0]=='insert into'){$.post('GridData.php',{Action:param[0],Rowdata:row.toString()},function(data){alert(data)});//insert时要提交动作和插入后的数据}ExecType='';}}function rejectr(){//取消$('#easyui_tb').datagrid('rejectChanges');ExecType='';editIndex = undefined;}function getChanges(){var rows = $('#easyui_tb').datagrid('getChanges');return rows;}
  服务器实现这些功能用下面这个函数:
function Exec_SQL(act,id,row:string):string;
  源码自己去看,很简单,就是操作数据库。本文基本完成,最后讲一下这种方式未完成的功能:直接使用http://xxx.xxx.xxx/main.html可以不用登陆就能进入主界面,显然不是我们所期望的,可以在主界面加入验证登陆的功能,也很简单,可以在页面加载之前$.post提交验证信息到login.php,里面代码已经写了,只是客户端没有添加。还有第三大点。
  三、动态加载
  动态加载简单说就是,客户端还是单独做出来,不用放在wwwroot下面,引用js和css时需要在路径前面多加一个“/”即可。仍然从TContentBase 继承实现一个类,在函数中这样实现 :
function TMyIndex.Execute(aRequest: THttpRequest; aReply: THttpReply; const aPathname: string; aSession: TIWApplication; aParams: TStrings): Boolean;var ss:Tstrings;begin  aReply.ContentType := MIME_HTML;  ss:=TstringList.create;  ss.loadformfile('做好静态页面');  aReply.WriteString(ss.text);  ss.free;  Result := True;end;
  这样显而易见是比较安全的,可以在加载主界面前先验证有无登陆,还可以在加载页面中加一些类似模板替换标识,动态加载时,将这些标识替换成本页面需要展示的内容,实现header、body、footer共用。

  本文所需要工具:delphiXE7+Intraweb 14.0.38

  源码


1 0