v0.3.0版本添加了文章管理功能,可以实现文章的添加、编辑、删除等。文章添加和编辑时,使用CKEditor在线编辑器,可以在编辑器中上传图片。还可以在添加及编辑文章时,单独上传一张图片,后台会自动生成该图片的缩略图...
.......................................................... // 创建文章表 if(mysqlQuery(con, "DROP TABLE IF EXISTS article")) finish_with_error(con); endif if(mysqlQuery(con, "CREATE TABLE article( id int NOT NULL AUTO_INCREMENT, title varchar(100) NOT NULL DEFAULT '' COMMENT '文章标题', description varchar(255) NOT NULL DEFAULT '' COMMENT '文章描述', thumbnail varchar(255) NOT NULL DEFAULT '' COMMENT '文章缩略图', author varchar(100) NOT NULL DEFAULT '' COMMENT '文章作者', cid int NOT NULL DEFAULT '0' COMMENT '分类ID', content mediumtext NOT NULL COMMENT '文章内容', `created_at` timestamp NULL DEFAULT NULL COMMENT '创建时间', `updated_at` timestamp NULL DEFAULT NULL COMMENT '更新时间', PRIMARY KEY (id) ) ENGINE=MyISAM DEFAULT CHARSET utf8 COLLATE utf8_general_ci COMMENT='文章表'")) finish_with_error(con); endif .......................................................... |
inc 'common.zl'; inc 'helper.zl'; querys = rqtGetQuery(); action = querys['act'] ? querys['act'] : 'list'; if(action == 'list') Article.list(); elif(action == 'add') Article.add(); elif(action == 'edit') Article.edit(); elif(action == 'delete') Article.delete(); else print 'invalid act'; endif class Article // 显示文章列表 fun list() global menus, querys; data['title'] = '文章列表:'; setCurMenu(menus, 'article.zl'); data['csses', 0] = 'article_list.css'; data['head_js', 0] = 'category.js'; data['menus'] = menus; db = Article.initDB(); Article.getCategory(db, data, querys); where = " where 1 "; if(bltCount(data['category'])) where += " and cid=" + data['category', 'id']; endif stitle = bltStr(&querys['stitle']); if(stitle) where += ' and title like "%' + stitle + '%"'; data['stitle'] = stitle; endif tmp = Mysql.fetchOne(db, "select count(1) as cnt from article " + where); total = bltInt(tmp[cnt]); page = get_page(10, total, 10); for(i=page['start']; i <= page['end'];i++) cur = (page['curpage'] == i) ? ' class="active"' : ''; pages[] = '<li'+cur+'><a href="'+page['link']+'page='+i+'">'+i+'</a></li>'; endfor data['page'] = page; data['pages'] = pages; data['articles'] = Mysql.fetchAll(db, "select a.id,title,thumbnail,author,created_at,updated_at,c.name as cname from article as a left join category as c on c.id=a.cid "+ where + " order by id desc limit " + page['offset'] + "," + page['limit']); print bltMustacheFileRender("tpl/article_list.tpl", data); endfun // 初始化数据库连接 fun initDB() global config; db = bltArray(); Mysql.init(db, config, "tpl/error.tpl"); return db; endfun // 获取分类列表,或者某个分类的信息 fun getCategory(db, data, posts = 0) if(posts && posts['cid'] > 0) data['category'] = Mysql.fetchOne(db, "select id,name from category where id='"+posts['cid']+"'"); else data['categories'] = Mysql.fetchAll(db, "select id,name,childcnt from category where pid=0 order by id asc"); endif endfun // 显示添加或编辑文章的页面 fun showAdd(db, data, posts = 0, isadd = TRUE) global menus; setCurMenu(menus, 'article.zl'); csses = bltArray('article_add.css', 'jquery.fileupload.css'); js = bltArray('ckeditor/ckeditor.js', 'jquery.ui.widget.js', 'jquery.iframe-transport.js', 'jquery.fileupload.js', 'category.js'); data['csses'] = csses; data['head_js'] = js; data['menus'] = menus; data['title'] = isadd ? '添加文章' : '编辑文章'; data['act'] = isadd ? 'add' : 'edit'; Article.getCategory(db, data, posts); print bltMustacheFileRender("tpl/article_add.tpl",data); endfun // 校验数据 fun validate(db, posts, isadd = TRUE) bltUnset(&posts['thumbnail_up']); bltStr(&posts['title'], TRUE); bltStr(&posts['thumbnail'], TRUE); bltStr(&posts['description'], TRUE); bltStr(&posts['content'], TRUE); bltInt(&posts['cid'], TRUE); bltStr(&posts['author'], TRUE); if(!posts['title']) data['err_msg'] = '标题不能为空'; elif(!posts['content']) data['err_msg'] = '内容不能为空'; elif(!posts['cid']) data['err_msg'] = '请选择有效的分类'; elif(!posts['author']) data['err_msg'] = '作者不能为空'; elif(!isadd && !posts['id']) data['err_msg'] = '无效的文章ID'; endif if(data['err_msg']) data['posts'] = posts; if(posts['id']) data['id'] = posts['id']; endif Article.showAdd(db, data, posts, isadd); bltExit(); endif endfun // 添加文章 fun add() posts = rqtGetBodyAsArray(); db = Article.initDB(); if(posts['submit']) Article.validate(db, posts); bltUnset(&posts['submit']); posts['created_at'] = bltDate('%Y-%m-%d %H:%M:%S'); posts['updated_at'] = posts['created_at']; Mysql.Insert(db, 'article', posts); data['success_msg'] = '添加文章成功'; endif Article.showAdd(db, data); endfun // 编辑文章 fun edit() global querys; posts = rqtGetBodyAsArray(); db = Article.initDB(); if(posts['submit']) id = bltInt(posts['id']); Article.validate(db, posts, FALSE); bltUnset(&posts['submit']); posts['updated_at'] = bltDate('%Y-%m-%d %H:%M:%S'); Mysql.Update(db, 'article', posts, 'id=' + id); data['success_msg'] = '编辑文章成功'; else id = bltInt(querys['id']); endif data['posts'] = Mysql.fetchOne(db, "select * from article where id='"+id+"'"); if(!bltCount(data['posts'])) data['err_msg'] = '无效的文章ID'; id = 0; endif data['id'] = id; Article.showAdd(db, data, data['posts'], FALSE); endfun // 删除文章 fun delete() global querys; db = Article.initDB(); id = querys['id'] ? bltInt(querys['id']) : 0; if(id <= 0) ajax_return('无效的文章id'); endif Mysql.Exec(db, "DELETE FROM article WHERE id="+id); ajax_return(); endfun endclass |
inc 'common.zl'; use magick; querys = rqtGetQuery(); action = querys['act'] ? querys['act'] : 'list'; if(action == 'ckImage') Upload.ckImage(); elif(action == 'thumbImg') Upload.thumbImg(); else print 'invalid act'; endif class Upload // 检测上传图片的大小 fun checkImageSize(exit = TRUE) rqtGetBody(&body_count); headers = rqtGetHeaders(); if(headers['Content-Length'] > body_count) error = '上传的图片过大' + ' 上传大小:' + headers['Content-Length'] + ' 服务器截断大小:' + body_count; if(exit) rqtSetResponseHeader("Content-Type: application/json"); data['error'] = error; print bltJsonEncode(data); bltExit(); else return error; endif else return ''; endif endfun // 通过CKEditor编辑器上传图片后,需要返回的js脚本。如果上传成功,urlpath就表示上传成功的图片的url地址,如果上传失败,则通过msg来返回出错信息 fun returnCkMsg(msg, urlpath = '') global querys; funcNum = querys['CKEditorFuncNum']; print "<script type='text/javascript'>window.parent.CKEDITOR.tools.callFunction("+funcNum+", '"+ urlpath+"', '" + msg + "');</script>"; endfun // 下面的方法用于处理由CKEditor编辑器上传的图片 fun ckImage() error = Upload.checkImageSize(FALSE); if(error != '') Upload.returnCkMsg(error); return; endif body_array = rqtGetBodyAsArray(); if(bltCount(body_array['upload']) > 0) v = body_array['upload']; type = v['type']; // 通过图片类型确定需要设置的图片文件名后缀 if(type == 'image/png') ext = '.png'; elif(type == 'image/gif') ext = '.gif'; else ext = '.jpg'; endif dir = '/upload/image/' + bltDate('%Y%m%d') + '/'; // 根据当前日期,确定图片需要保存的目录 bltMkdir('..' + dir); urlpath = dir + bltMd5(v['filename']) + ext; // 使用上传文件名的md5值来确定需要保存的图片的文件名 bltWriteFile('..' + urlpath, v['content_ptr'], v['length']); wand = magickNewWand(); if(!magickReadImage(wand, '..' + urlpath)) msg = '上传失败,或者上传的不是有效的图片文件'; bltUnlink('..' + urlpath); urlpath = ''; else msg = ''; endif cookies = rqtGetCookie(); token = body_array['ckCsrfToken']; if(bltCount(token) == 0 || cookies['ckCsrfToken'] != token) print 'access denied'; bltExit(); endif Upload.returnCkMsg(msg, urlpath); else print 'no upload'; endif endfun // 下面方法用于处理缩略图的上传 fun thumbImg() global querys; Upload.checkImageSize(); body_array = rqtGetBodyAsArray(&body_count); if(bltCount(body_array['thumbnail_up']) > 0) v = body_array['thumbnail_up']; thumbdir = '/upload/thumb/' + bltDate('%Y%m%d') + '/'; // 根据当前日期,确定图片需要保存的目录 bltMkdir('..' + thumbdir); tmp = '..' + thumbdir + 'tmp'; // TODO 多用户操作时可能会覆盖 urlpath = thumbdir + bltMd5(v['filename']); // 使用上传文件名的md5值来确定需要保存的图片的文件名 if(v['type'] == 'image/gif') framepath = urlpath + '-0.jpg'; // gif的第一帧对应的jpg else framepath = ''; endif urlpath += '.jpg'; bltWriteFile(tmp, v['content_ptr'], v['length']); rqtSetResponseHeader("Content-Type: application/json"); wand = magickNewWand(); if(!magickReadImage(wand, tmp)) data['error'] = '上传失败,或者上传的不是有效的图片文件'; elif(!magickResizeImage(wand, 200, 150, "LanczosFilter")) data['error'] = '生成缩略图失败,调整大小出错'; elif(!magickWriteImage(wand, '..' + urlpath)) data['error'] = '生成缩略图失败,写入失败'; elif(bltFileExists('..' + urlpath)) data['urlpath'] = urlpath; elif(framepath != '' && bltFileExists('..' + framepath)) data['urlpath'] = framepath; else data['error'] = '生成缩略图失败,生成的缩略图不存在'; endif bltUnlink(tmp); print bltJsonEncode(data); else print 'no thumbnail'; endif endfun endclass |
{{> tpl/header.tpl}} <h2 class="sub-header">{{title}}<a href="?act=list" class="btn btn-primary pull-right" role="button">返回列表</a></h2> {{#err_msg}}<div class="alert alert-danger" role="alert">{{err_msg}}</div>{{/err_msg}} {{#success_msg}}<div class="alert alert-success" role="alert">{{success_msg}}</div>{{/success_msg}} <form action="?act={{act}}{{#id}}&id={{id}}{{/id}}" method="post" id="cate_form"> {{#id}}<input type="hidden" name="id" value="{{id}}">{{/id}} <div class="form-group"> <label for="title">标题:</label> <input type="text" class="form-control" name="title" {{#posts}}value="{{title}}"{{/posts}} id="title" placeholder="标题"> </div> <div class="form-group"> <label for="thumbnail-upload">缩略图:</label> <input id="thumbnail-upload" type="file" name="thumbnail_up" data-url="upload.zl?act=thumbImg"> <input id="thumbnail-hidden" type="hidden" name="thumbnail" {{#posts}}value="{{thumbnail}}"{{/posts}}> <span id="thumbnail-span"></span> <img {{#posts}}src="{{thumbnail}}"{{/posts}} style="display:none" id="thumbnail-img" /> </div> <div class="form-group"> <label for="description">描述:</label> <textarea class="form-control" rows="5" name="description" id="description">{{#posts}}{{description}}{{/posts}}</textarea> </div> <div class="form-group"> <label for="content">内容:</label> <textarea class="form-control" rows="20" cols="80" name="content" id="content">{{#posts}}{{content}}{{/posts}}</textarea> </div> {{#category}} <div class="form-group" id="p-cate"> <label>上级分类:<span id="p-cate-name">{{name}}(cid={{id}})</span></label> <input type="hidden" name="cid" value="{{id}}" id="pid"> <a href="javascript:void(0)" id="reset_category" class="btn btn-default" data-loading-text="加载分类列表...">重置上级分类</a> </div> {{/category}} {{^category}} <div class="form-group" id="p-cate"> <label for="pid">上级分类:<span id="p-cate-name">(cid=0)</span></label> <input type="hidden" name="cid" value="0" id="pid"> <select class="form-control sel_pid"> <option value="0" data-childcnt="0">选择上级分类</option> {{#categories}} <option value="{{id}}" data-childcnt="{{childcnt}}">{{name}}</option> {{/categories}} </select> </div> {{/category}} <div class="form-group"> <label for="author">作者:</label> <input type="text" class="form-control" name="author" {{#posts}}value="{{author}}"{{/posts}} id="author" placeholder="作者"> </div> <button name="submit" value="Submit" type="submit" class="btn btn-primary">提交</button> </form> <script type="text/javascript"> CKEDITOR.replace( 'content' ,{ height: 300, filebrowserUploadUrl: 'upload.zl?act=ckImage', filebrowserUploadMethod: 'form' }); $('#thumbnail-upload').fileupload({ dataType: 'json', formData: {}, add: function(e, data) { $('#thumbnail-span').text('上传中...'); data.submit(); }, done: function (e, data) { if(typeof data.result.error === "undefined") { var $img = $('#thumbnail-img'); $img.attr("src", data.result.urlpath); $img.show(); $('#thumbnail-span').text(''); $('#thumbnail-hidden').val(data.result.urlpath); } else { $('#thumbnail-img').hide(); $('#thumbnail-span').text(data.result.error); $('#thumbnail-hidden').val(""); } }, fail: function() { $('#thumbnail-span').text('上传失败'); } }); $(document).ready(function() { {{#category}} {{! 如果设置了上级分类,则添加重置上级分类的脚本 }} $('#reset_category').click(function(){ reset_category_ajax(); }); {{/category}} var $img = $('#thumbnail-img'); if($img.attr('src') != '') { // 如果所略图不为空,则在一开始就显示出来 $img.show(); } }); </script> {{> tpl/footer.tpl}} |
{{> tpl/header.tpl}} <h2 class="sub-header"> 文章列表 <a href="?act=add" class="btn btn-primary pull-right" role="button">添加文章</a> </h2> <div class="table-responsive"> <form action="?" method="get" id="cate_form"> <div class="form-group" id="p-title"> <input type="text" class="form-control" name="stitle" {{#stitle}}value="{{stitle}}"{{/stitle}} placeholder="标题"> </div> <div class="form-group" id="p-cate"> {{#category}} <label id="p-cate-label">所属分类:<span id="p-cate-name">{{name}}(cid={{id}})</span></label> <input type="hidden" name="cid" value="{{id}}" id="pid"> <a href="javascript:void(0)" id="reset_category" class="btn btn-default" data-loading-text="加载分类列表...">重置分类</a> {{/category}} {{^category}} <input type="hidden" name="cid" value="0" id="pid"> <select class="form-control sel_pid"> <option value="0" data-childcnt="0">选择上级分类</option> {{#categories}} <option value="{{id}}" data-childcnt="{{childcnt}}">{{name}}</option> {{/categories}} </select> {{/category}} </div> <div style="float:left"><button name="s" value="search" type="submit" class="btn btn-primary">搜索</button></div> <div style="clear:both"></div> </form> <table class="table table-hover"> <thead> <tr> <th>id</th> <th>缩略图</th> <th>标题</th> <th>作者</th> <th>所属分类</th> <th>创建时间</th> <th>更新时间</th> <th>操作</th> </tr> </thead> <tbody> {{#articles}} <tr> <td>{{id}}</td> <td>{{#thumbnail}}<img src="{{thumbnail}}" width="100" height="80"/>{{/thumbnail}}</td> <td>{{title}}</td> <td>{{author}}</td> <td>{{cname}}</td> <td>{{created_at}}</td> <td>{{updated_at}}</td> <td> <a href="?act=edit&id={{id}}" title="编辑" class="glyphicon glyphicon-edit" aria-hidden="true"></a> <a href="javascript:void(0)" data-id="{{id}}" title="删除" class="glyphicon glyphicon-trash del_article" aria-hidden="true"></a><span></span></td> </tr> {{/articles}} {{^articles}} <tr><td colspan=8>暂无文章</td></tr> {{/articles}} </tbody> </table> {{#page}} <nav aria-label="Page navigation"> <ul class="pagination"> <li> <a href="{{{link}}}page=1" aria-label="Previous"> <span aria-hidden="true">«</span> </a> </li> <li> <a href="{{{link}}}page={{prev}}" aria-label="Previous"> <span aria-hidden="true">‹</span> </a> </li> {{#pages}} {{{.}}} {{/pages}} <li> <a href="{{{link}}}page={{next}}" aria-label="Next"> <span aria-hidden="true">›</span> </a> </li> <li> <a href="{{{link}}}page={{totalpage}}" aria-label="Next"> <span aria-hidden="true">»</span> </a> </li> </ul> </nav> <span>总共{{total}}篇文章/共{{totalpage}}页</span> {{/page}} </div> <script type="text/javascript"> $( document ).ready(function() { $('.del_article').click(function(){ var id = $(this).data('id'); var next_span = $(this).next('span'); var r = confirm("删除文章确认[id: "+id+"]"); if(r == true) { var timestamp = new Date().getTime(); //IE浏览器ajax GET请求有缓存问题! $.ajax({ type: 'GET', url: "article.zl?timestamp="+timestamp, dataType: "json", data: { "act": "delete", "id": id }, beforeSend:function(){ next_span.text(' 删除中...'); }, success: function(data){ if(data.msg != 'success') { alert('删除失败: ' + data.errmsg); next_span.text(''); return; } alert('删除成功'); window.location.reload(); }, //调用出错执行的函数 error: function(err){ alert('未知异常'); next_span.text(''); } }); } }); {{#category}} {{! 如果设置了上级分类,则添加重置上级分类的脚本 }} $('#reset_category').click(function(){ reset_category_ajax(); $('#p-cate-label').hide(); }); {{/category}} }); </script> {{> tpl/footer.tpl}} |
// ajax返回成功或失败 fun ajax_return(errmsg = '') ret['msg'] = errmsg ? 'failed' : 'success'; ret['errmsg'] = errmsg; rqtSetResponseHeader("Content-Type: application/json"); print bltJsonEncode(ret); bltExit(); endfun /** * 获取分页相关的信息,第一个参数per表示一页显示多少条记录,total表示总记录数 * showpage表示需要显示的分页数,link表示需要访问的地址,以问号结尾 */ fun get_page(per, total, showpage, link = '?') global querys; totalpage = bltInt(total/per); // 根据总记录数和每页需要显示的记录数计算总页数 if(total % per > 0) // 如果有余数则将总页数加一 totalpage++; endif half = bltInt(showpage / 2); curpage = bltInt(querys['page']); // 从查询字符串中获取page也就是当前分页 if(curpage < 1) curpage = 1; elif(curpage > totalpage) curpage = totalpage; endif start = curpage - half; // 计算起始页号 if(start < 1) start = 1; endif end = curpage + half; // 计算结束页号 if(end > totalpage) end = totalpage; endif for(i=0;bltIterArray(querys,&i,&k,&v);) // 获取不包括page在内的查询字符串 if(k != 'page') link += k + '=' + v + '&'; endif endfor // 将分页信息存储到ret返回 ret['start'] = start; ret['end'] = end; ret['offset'] = (curpage - 1) * per; ret['limit'] = per; ret['prev'] = (curpage <= 1) ? 1 : (curpage - 1); // 计算上一页 ret['next'] = (curpage >= totalpage) ? totalpage : (curpage + 1); // 计算下一页 ret['total'] = total; ret['totalpage'] = totalpage; ret['curpage'] = curpage; ret['link'] = link; return ret; endfun |