handsontable在线编辑excel扩展功能-踩坑篇
2018-06-24 01:42:14来源:未知 阅读 ()
简述
先说一下背景,之所以封装handsontable插件,是因为公司要实现在线编辑导入excel文件的功能,然后我就找到了这个功能强大的插件handsontable。
具体功能
除了handsontable的功能外,还包括:
1、每一行数据统计错误数,重复数
2、每一列标记重复项,错误项
3、定位功能,当数据过多出现滚动条时,点击上一条/下一条按钮,定位到当前标记项。
4、表头标注每一列数据的校验规则。
5、当数据被编辑后,立即重新校验,并标记重复项、错误项
6、配置isValidate,true则本地校验,false则不校验
(css样式有待改进,后面会更新)
2018/3/30日,修复所有bug,
1、根绝插件局部渲染的特性,将每行的第一列的标注完成局部渲染,以及错误点定位,也根据局部渲染的特性,先滚动到指定行,再进行标记。
2、另外,修复时间控件在最前/后一列/行的情况下,会被遮挡的问题,修改了源码,根据当前单元格的位置来计算时间控件展示的位置。
3、修改源码,将时间控件的英文改为中文。(后面会附上源码修改部分)
2018/4/3日,修改保存错误定位的数据结构,并标记每一个td的row,col位置,保证做到,定位错误点100%准确
/* * author Happy Guo * date 2018-03-15 */ 'use strict'; define(["jquery","Handsontable"], function ($,Handsontable) { var HandsontableExtend = function(opt) { this.opt = $.extend(true, {}, opt); this.element = document.querySelector(this.opt.el); this.header = Object.keys(this.opt.dataObj.headMap); this.allErrorNum = [];//记录每一行的错误数 this.table = null; this.errorRow = 0;//统计有几行是有错误的 this.errorPosition = { index:0, row: 0, col: 0 };//记录当前被标注的错误位置 this.firstError = { isExist :false, row:0, col:0 } this.isPosition = false; this.position = null; //如果没有数据,默认给出一行空行 if(this.opt.dataObj.dataMap.length===0){ var obj = {}; this.header.map(function(item,index){ obj[item] = { value:'', errorType:'' } }); this.opt.dataObj.dataMap.push(obj) } } HandsontableExtend.prototype = { constructor: HandsontableExtend, // extraType: ["CODE_TYPE","Date"], //不需要校验的类型 typeMap:{'STRING':'文本','INTEGER':'整型','DOUBLE':'小数','DATE':'日期','CODE_TYPE':'枚举类型','BOOLEAN':'布尔','TIME':'小时分钟'}, dateFormat:['yyyy-MM-dd','yyyy/MM/dd','yyyy.MM.dd'], timePattern:/^([0-1]{1}\d|2[0-3]):([0-5]\d)$/, /*@method 整合table的列的配置项 */ setColumn: function() { var list = []; for (var i = 0; i < this.header.length; i++) { var obj = { data: this.header[i] + ".value", width: 150, height: 60 }; var typeData = this.opt.dataObj.headMap[this.header[i]]; if (typeData.type === "CODE_TYPE") { obj.type = "dropdown"; obj.source = this.getCodeValueList(typeData.codeValueList); } if (typeData.type === "BOOLEAN") { obj.type = "dropdown"; obj.source = ['是','否']; } if(typeData.type === 'TIME'){ obj.type = "time"; obj.dateFormat = "h:mm"; } if (typeData.type === "DATE") { obj.type = "date"; obj.dateFormat = "YYYY-MM-DD"; obj.datePickerConfig={ firstDay: 1, yearRange:100, showWeekNumber: true, minDate: new Date('1900-01-01') } } list.push(obj); } return list; this.opt.set.columns = list; }, setCell:function(){ var list = []; var that = this; this.opt.dataObj.dataMap.map(function(item,index){ Object.keys(item).forEach(function(key){ if(item[key].originalValue){ list.push({ row:index, col:that.header.indexOf(key), comment:{ value:item[key].originalValue } }) } }) }); return list; }, getCodeValueList:function(list){ var newList = []; if(list instanceof Array){ list.map(function(item,index){ newList.push(item.displayName); }); return newList; }else{ return []; } }, /*@method 初始化table的配置,以及事件监听 */ init: function() { var that = this; this.opt.set = $.extend(true, { data: that.opt.dataObj.dataMap, comments:true, columns: this.setColumn(), cells: function(row, col, prop) { //单元格渲染 this.renderer = function(instance,td,row,col,prop,value,cellProperties) { Handsontable.renderers.TextRenderer.apply(this, arguments); var obj = that.opt.dataObj.dataMap[row][that.header[col]]; $(td).attr({row:row,col:col}); if (typeof obj["errorType"] !== "undefined") { if (obj["errorType"] === "repeat") { $(td).attr({ repeat: true }); } else if (obj["errorType"] === "error") { $(td).attr({ error: true }); } } else { $(td).css({ border: "1px solid #ccc", color: "#999", "white-space": "normal", "word-break": "break-all" }); } if(obj.originalValue){ $(td).css({ 'background-color':'#F1F9FF' }) } }; }, cell:that.setCell(), stretchH: "all", width: "100%", autoWrapRow: true, autoRowSize: true, autoColumnSize: true, height: "600", maxRows: 1000, manualRowResize: false, manualColumnResize: false, // beforeKeyDown : function(e) { // // 禁止选中列后delete键和回退键清空整列数据 // if (e.keyCode === 8 || e.keyCode == 46) { // Handsontable.Dom.stopImmediatePropagation(e); // } // }, manualRowMove: true, manualColumnMove: true, contextMenu: true, filters: true, dropdownMenu: true }, this.opt.set ); this.opt.set.rowHeaders = function(index) { var repeatNum = 0; var errorNum = 0; if(that.allErrorNum[index] instanceof Array){ for(var j=0;j<that.allErrorNum[index].length;j++){ if(that.allErrorNum[index][j].type==='error'){ errorNum+=1; }else{ repeatNum+=1; } } } var html = "<span class='error-th' style='display:"+((that.allErrorNum[index]&&that.allErrorNum[index].length>0)?"block":"none")+"'></span>"; html += " <span class='error-content'>重复:" + repeatNum + ",错误:" + errorNum + "</span>"; html += "<span id='column_name' style='padding-right:6px;'>" + (index + 1) + "</span>"; return html; }; this.opt.set.colHeaders = function(index) { var desc = that.header[index]+"规则:"; var map = that.opt.dataObj.headMap[that.header[index]]; if(map.minLength&&map.maxLength){ desc=desc+'长度:'+map.minLength+'-'+map.maxLength; }else if(map.length){ desc=desc+'长度:'+map.length } if(map.type){ desc=desc+',类型:'+that.typeMap[map.type] } if(map.required){ desc=desc+',必填' } if(map.unique){ desc=desc+',不能重复' } var html = "<span class='remark-th'></span>"; html += " <span class='remark-content'>"+desc+"</span>"; if(map.required){ html += "<span id='column_name' style='color:#ED5565'>" + that.header[index] + "</span>"; }else{ html += "<span id='column_name'>" + that.header[index] + "</span>"; } return html; }; this.table = new Handsontable(this.element, this.opt.set); this.table.updateSettings({ contextMenu: { callback: function(key, options) { if (key === "about") { setTimeout(function() { // timeout is used to make sure the menu collapsed before alert is shown alert( "This is a context menu with default and custom options mixed" ); }, 100); } }, items: { row_above: { name: "向上插入一行", disabled: function() { return that.table.getSelected()[0] === 0; } }, remove_row: { name: "删除选中行", disabled: function() { // if first row, disable this option return that.table.getSelected()[0] === 0; } } } } }); window.onresize = this.opt.isValidate ? function() { that.render.call(that); } : null; this.table.addHook("afterChange", function() { that.opt.isValidate && that.render.call(that); }); this.table.addHook("afterRemoveRow", function() { that.opt.isValidate && that.render.call(that); }); var topValue = 0,leftValue = 0; var interval = null; $(this.opt.el + " .wtHolder")[0].onscroll = function() { if (interval == null) { interval = setInterval(isFinishScroll, 1000); } }; function isFinishScroll() { // 判断此刻到顶部的距离是否和1秒前的距离相等 if ($(that.opt.el + " .wtHolder")[0].scrollLeft === leftValue && $(that.opt.el + " .wtHolder")[0].scrollTop === topValue) { clearInterval(interval); interval = null; that.validate(); that.renderError(); that.remarkShow();//渲染表头、列头标注 if(that.position){ $("td[row='"+that.errorPosition.row+"'][col='"+that.errorPosition.col+"']").attr('current'); } } else { topValue = $(that.opt.el + " .wtHolder")[0].scrollTop; leftValue = $(that.opt.el + " .wtHolder")[0].scrollLeft; } } this.render(); return this; }, /*@method 渲染整个table数据 */ render: function() { var table = this.table; this.validate.call(this); //初始化,先验证,并标记重复项 table.render(); //并渲染在行首部 this.renderError(); this.remarkShow();//渲染表头、列头标注 $(".htInvalid").removeClass('htInvalid'); $("td[current]").removeAttr('current'); }, /*@method 渲染出表头和列头的标注信息 */ remarkShow:function(){ $(".remark-th").hover(function(){ var top = $(this).closest('th').offset().top; var left = $(this).closest('th').offset().left; $(this).next().css({'top':top,"left":left}) }); $(".error-th").hover(function(){ var top = $(this).closest('th').offset().top; var left = $(this).closest('th').offset().left+10; $(this).next().css({'top':top,"left":left}) }) }, /*@method 渲染出重复、错误的单元格 */ renderError: function() { var that = this; var rowHeaderTr = $(".ht_clone_left .htCore").eq(0).find("tbody").find("tr"); var tr = $(this.opt.el + " .htCore").eq(0).find("tbody").find("tr"); //渲染错误项(只渲染当前可视区域的), //此处天坑,行头是单独的table,之前滚动后渲染位置错误。 for (var i = 0; i < tr.length; i++) { var rowNum = $(tr[i]).find("th #column_name").text(); rowNum = parseInt(rowNum) - 1; //统计每一行错误项、重复项,如果列数过多,则列会渲染不完全,所以不能用选择器查出准确数据,只能使用统计出的数据 var repeatNum = 0; var errorNum = 0; if(that.allErrorNum[rowNum] instanceof Array){ for(var j=0;j<this.allErrorNum[rowNum].length;j++){ if(this.allErrorNum[rowNum][j].type==='error'){ errorNum+=1; }else{ repeatNum+=1; } } } var errorTH = $(rowHeaderTr[i]).find("th .error-th").eq(0); if(repeatNum+errorNum>0){ errorTH.css({'display':'block'}); var top = errorTH.offset().top; var left = errorTH.offset().left+10; errorTH.next().css({'top':top,"left":left}) errorTH.next().text('重复:'+repeatNum+',错误:'+errorNum); }else{ errorTH.css({'display':'none'}); } $(tr[i]).find("th .error-th,th .error-content").remove();//将另一处被渲染的行标注删除,防止误导 } this.remarkShow(); }, /*@method 验证 */ validate: function() { var table = this.table; var that = this; this.allErrorNum = [];//初始化统计错误array this.firstError = { isExist :false, row:0, col:0 };//初始化,第一个错误位置改为不存在 var cellLength = table.getDataAtRow(0).length; var errorRows = []; for (var i = 0; i < cellLength; i++) { var cellData = table.getDataAtCol(i); // var newArr = []; cellData.map(function(item, index, arr) { var NewCell; var validateRule = that.opt.dataObj.headMap[that.header[i]]; var dataMap = that.opt.dataObj.dataMap[index][that.header[i]]; //if (newArr.indexOf(index) === -1) { //如果被重复项最后一个索引已有渲染标志,则不删除渲染标志。 NewCell = table.getCell(index, i); $(NewCell).removeAttr("error"); $(NewCell).removeAttr("repeat"); dataMap["errorType"] = ""; //} if (validateRule.required && !item) { dataMap["errorType"] = "error"; $(NewCell).attr("error", true); that.setAllErrorNum.call(that,index,i,"error"); errorRows.indexOf(index)===-1 && errorRows.push(index); return; } if(item){ if (validateRule.type && validateRule.type === "CODE_TYPE") { var list = that.getCodeValueList(validateRule.codeValueList); if(list.indexOf(item)===-1){ dataMap["errorType"] = "error"; $(NewCell).attr("error", true); that.setAllErrorNum.call(that,index,i,"error"); errorRows.indexOf(index)===-1 && errorRows.push(index); return; } } if(validateRule.type && validateRule.type === "BOOLEAN"){ if(['是','否'].indexOf(item)===-1){ dataMap["errorType"] = "error"; $(NewCell).attr("error", true); that.setAllErrorNum.call(that,index,i,"error"); errorRows.indexOf(index)===-1 && errorRows.push(index); return; } } if ( validateRule.type && validateRule.type === "DATE" ) { var result = false; that.dateFormat.map(function(format,index){ if(new Date(item).format(format)===item){ result = true; } }) if(!result){ dataMap["errorType"] = "error"; $(NewCell).attr("error", true); that.setAllErrorNum.call(that,index,i,"error"); errorRows.indexOf(index)===-1 && errorRows.push(index); return; } } if(validateRule.type && validateRule.type === "TIME" && !that.timePattern.test(item)){ dataMap["errorType"] = "error"; $(NewCell).attr("error", true); that.setAllErrorNum.call(that,index,i,"error"); errorRows.indexOf(index)===-1 && errorRows.push(index); return; } if(validateRule.type && validateRule.type === "INTEGER" && parseInt(item)!=item){ dataMap["errorType"] = "error"; $(NewCell).attr("error", true); that.setAllErrorNum.call(that,index,i,"error"); errorRows.indexOf(index)===-1 && errorRows.push(index); return; } if(validateRule.type && validateRule.type === "DOUBLE" && parseFloat(item)!=item){ dataMap["errorType"] = "error"; $(NewCell).attr("error", true); that.setAllErrorNum.call(that,index,i,"error"); errorRows.indexOf(index)===-1 && errorRows.push(index); return; } if (parseInt(validateRule.minLength) && item.length < parseInt(validateRule.minLength)) { dataMap["errorType"] = "error"; $(NewCell).attr("error", true); that.setAllErrorNum.call(that,index,i,"error"); errorRows.indexOf(index)===-1 && errorRows.push(index); return; } if (parseInt(validateRule.maxLength) && item.length > parseInt(validateRule.maxLength)) { dataMap["errorType"] = "error"; $(NewCell).attr("error", true); that.setAllErrorNum.call(that,index,i,"error"); errorRows.indexOf(index)===-1 && errorRows.push(index); return; } if ((!validateRule.maxLength && !validateRule.minLength&& validateRule.length) && item.length !== validateRule.length) { dataMap["errorType"] = "error"; $(NewCell).attr("error", true); that.setAllErrorNum.call(that,index,i,"error"); errorRows.indexOf(index)===-1 && errorRows.push(index); return; } if (validateRule.regexp) { var pattern = new RegExp(validateRule.regexp); if(!pattern.test(item)){ dataMap["errorType"] = "error"; $(NewCell).attr("error", true); that.setAllErrorNum.call(that,index,i,"error"); errorRows.indexOf(index)===-1 && errorRows.push(index); return; } } if (validateRule.unique && that.getRepeatNum(arr,item,index)) { $(NewCell).attr("repeat", true); dataMap["errorType"] = "repeat"; that.setAllErrorNum.call(that,index,i,"repeat"); errorRows.indexOf(index)===-1 && errorRows.push(index); } } }); } $(".htInvalid").removeClass('htInvalid'); this.errorRow = errorRows.length; }, /*@method 辅助方法,获取table中第一个错误点的位置 */ setAllErrorNum:function(x,y,type){ if(!this.firstError.isExist||this.firstError.row>x){ this.firstError = { isExist:true, row:x, col:y }; } if(!this.allErrorNum[x]){ this.allErrorNum[x] = []; } this.allErrorNum[x].push({ type:type, row:x, col:y }); //如果在已经定位的情况下,又修改了其他单元格到处出现新的数据,那么此处的错误定位要重新定位 if(x === this.errorPosition.row && y === this.errorPosition.col){ this.errorPosition.index = this.allErrorNum[x].length-1; } }, /*@method 辅助方法,判断数组中某一个值是否有重复项 */ getRepeatNum:function(arr,val,i){ var result = false; arr.map(function(item,index,array){ if(item===val && index!==i){ result = true; } }) return result; }, /*@method 下一个错误调用方法 */ nextError: function() { if (this.position) { $("td[current]").removeAttr("current"); this.position = "next"; this.nextErrorPostion(); this.isPosition = true; this.scrollToError(); } }, /*@method 上一个错误调用方法 */ prevError: function() { if (this.position) { $("td[current]").removeAttr("current"); this.position = "prev"; this.prevErrorPosition(); this.isPosition = true; this.scrollToError(); } }, /*@method 计算出下一个错误单元格的位置 */ nextErrorPostion: function() { var length = this.allErrorNum[this.errorPosition.row].length; if (this.errorPosition.index < length - 1) { this.errorPosition.index += 1; this.errorPosition.col = this.allErrorNum[this.errorPosition.row][this.errorPosition.index].col; } else { var maxLength = this.allErrorNum.length; for (var i = this.errorPosition.row + 1; i < maxLength; i++) { if (this.allErrorNum[i] instanceof Array &&this.allErrorNum[i].length>0) { this.errorPosition = { index:0, row: i, col: this.allErrorNum[i][0].col }; return; } } if (i === maxLength && (this.errorPosition.index === this.allErrorNum[i-1].length-1)) { this.errorPosition = { index:0, row: this.firstError.row, col: this.firstError.col }; } } }, /*@method 计算出上一个错误单元格的位置 */ prevErrorPosition: function() { var length = this.allErrorNum[this.errorPosition.row].length; if (this.errorPosition.index > 0) { this.errorPosition.index -= 1; this.errorPosition.col = this.allErrorNum[this.errorPosition.row][this.errorPosition.index].col; } else { for (var i = this.errorPosition.row - 1; i >= 0; i--) { if (this.allErrorNum[i] instanceof Array &&this.allErrorNum[i].length>0) { var len = this.allErrorNum[i].length-1; this.errorPosition = { index:this.allErrorNum[i].length-1, row: i, col: this.allErrorNum[i][len].col }; return; } } } }, /*@method 滚动到errorPosition记录的位置 */ scrollToError: function() { var that = this; var tr = $(that.opt.el + " .htCore tbody").find("th #column_name:contains('"+(that.errorPosition.row+1)+"')").closest('tr'); var lastTd = (tr.length>0)?tr.find("td[col='"+this.errorPosition.col+"']"):''; //滚动到对应行的位置,并且插件会自动渲染出新的可视区域,此时再找出定位的单元格,并标记 $(this.opt.el + " .wtHolder").animate({ scrollTop: (this.errorPosition.row-5)*28 }, 300); lastTd = tr.find("td[col='"+that.errorPosition.col+"']"); $(this.opt.el + " .wtHolder").animate({ scrollLeft: (this.errorPosition.col-1)*150 }, 300); setTimeout(function(){ //此处因为是定时器,所以要注意tr和ladtTd会在定时器回掉函数开始执行时消失,所以要在定时器中重新定义,或者使用闭包 var tr = $(that.opt.el + " .htCore tbody").find("th #column_name:contains('"+(that.errorPosition.row+1)+"')").closest('tr'); lastTd = tr.find("td[col='"+that.errorPosition.col+"']"); $(lastTd).attr("current",true); that.isPosition = false; },600) }, /*@method 标记当前错误点 */ currentError: function(){ if(!this.position){ this.errorPosition.row = this.firstError.row; this.errorPosition.col = this.firstError.col; } this.isPosition = true; this.position = "current"; this.scrollToError(); }, /*@method 获取有多少条记录是有错误的、正确的 *@return array [总记录数,错误数,正确数] */ getErrorNum:function(){ var total = this.opt.dataObj.dataMap.length; return [total,this.errorRow,total-this.errorRow]; }, /*@method 获取到编辑后的table数据 */ getData: function() { return this.opt.dataObj; } }; $.fn.HandsontableExtend = function (opts) { var hand = new HandsontableExtend(opts); return hand.init(window); }; })
//修改源码部分: //第36508行,function showDatepicker(event)的部分,228,258分别为时间控件的高和宽 if(this.TD.offsetTop+228>holder.offsetHeight){ this.datePickerStyle.top = window.pageYOffset + offset.top - 228 + 'px'; }else{ this.datePickerStyle.top = window.pageYOffset + offset.top + (0, _element.outerHeight)(this.TD) + 'px'; } if(this.TD.offsetLeft+258>holder.offsetWidth){ this.datePickerStyle.left = window.pageXOffset + offset.left - (0, _element.outerWidth)(this.TD) + 'px'; }else{ this.datePickerStyle.left = window.pageXOffset + offset.left + 'px'; } //this.datePickerStyle.top = window.pageYOffset + offset.top + (0, _element.outerHeight)(this.TD) + 'px'; //this.datePickerStyle.left = window.pageXOffset + offset.left + 'px'; //i18n,json对象改为 i18n:{ previousMonth : '上一月', nextMonth : '下一月', months : ['一月','二月','三月','四月','五月','六月','七月','八月','九月','十月','十一月','十二月'], weekdays : ['星期日','星期一','星期二','星期三','星期四','星期五','星期六'], weekdaysShort : ['周日','周一','周二','周三','周四','周五','周六'] }
调用方法:
//dataObject接受的数据结构 dataObject = { headMap:{ '姓名':{ isRequired:true, type:'string', minLength:2, maxlength:20, isUnique:false }, '性别':{ isRequired:false, type:'string', isUnique:false }, '身份证号':{ isRequired:true, type:'string', minLength:9, maxlength:20, isUnique:true, regexp:/^[1-9][0-9]{5}(19|20)[0-9]{2}((01|03|05|07|08|10|12)(0[1-9]|[1-2][0-9]|31)|(04|06|09|11)(0[1-9]|[1-2][0-9]|30)|02(0[1-9]|[1-2][0-9]))[0-9]{3}([0-9]|x|X)$/ }, '联系电话':{ isRequired:true, type:'string', //length:11, isUnique:true }, '地址':{ isRequired:false, type:'string', maxLength:50, isUnique:false }, '职位':{ isRequired:false, type:'string', isUnique:false }, '部门':{ isRequired:false, type:'code_type', enumData:['it部','hr部','后勤部','销售部'], isUnique:false }, '入职日期':{ isRequired:false, type:'date', isUnique:false } }, dataMap:[ { '姓名':{ value:'张三', errorType:'' }, '性别':{ value:'男', errorType:'' }, '身份证号':{ value:'', errorType:'' }, '联系电话':{ value:'18817802351', errorType:'repeat' }, '地址':{ value:'上海浦东', errorType:'' }, '职位':{ value:'it', errorType:'' }, '部门':{ value:'it部', errorType:'' }, '入职日期':{ value:'it部', errorType:'' } },{ '姓名':{value:'李四1', errorType:''}, '性别':{value:'女', errorType:''}, '身份证号':{value:'111222', errorType:''}, '联系电话':{value:'18817802351', errorType:''}, '地址':{value:'上海浦东', errorType:''}, '职位':{value:'it', errorType:''}, '部门':{value:'it部', errorType:''}, '入职日期':{value:'it部', errorType:''} },{ '姓名':{value:'李四1', errorType:''}, '性别':{value:'女', errorType:''}, '身份证号':{value:'111222', errorType:''}, '联系电话':{value:'18817802351', errorType:''}, '地址':{value:'上海浦东', errorType:''}, '职位':{value:'it', errorType:''}, '部门':{value:'it部', errorType:''}, '入职日期':{value:'it部', errorType:''} } ] };
页面html代码
<div ng-controller="SurveyHandsontable"> <button id="prev" ng-click="prevError()">></button> <button id="next" ng-click="nextError()"><</button> <div id="hot"></div> <button ng-click="getData()">点击</button> </div>
标签:
版权申明:本站文章部分自网络,如有侵权,请联系:west999com@outlook.com
特别注意:本站所有转载文章言论不代表本站观点,本站所提供的摄影照片,插画,设计作品,如需使用,请与原作者联系,版权归原作者所有
上一篇:自言自语WEB前端面试题(二)
- JS实现在线ps功能 2019-08-14
- vue集成百度富文本编辑器 2019-08-14
- layui 框架 table插件 实现键盘快捷键 切换单元格编辑 2019-08-14
- TopJUI可编辑表格的列根据返回数据判断是使用 combobox 还是 2019-05-24
- 前端实现文件在线预览txt,pdf,doc,xls,ppt几种格式 2019-04-25
IDC资讯: 主机资讯 注册资讯 托管资讯 vps资讯 网站建设
网站运营: 建站经验 策划盈利 搜索优化 网站推广 免费资源
网络编程: Asp.Net编程 Asp编程 Php编程 Xml编程 Access Mssql Mysql 其它
服务器技术: Web服务器 Ftp服务器 Mail服务器 Dns服务器 安全防护
软件技巧: 其它软件 Word Excel Powerpoint Ghost Vista QQ空间 QQ FlashGet 迅雷
网页制作: FrontPages Dreamweaver Javascript css photoshop fireworks Flash