olym 7 rokov pred
commit
fb16da2445
100 zmenil súbory, kde vykonal 11346 pridanie a 0 odobranie
  1. 30 0
      .autod.conf.js
  2. 2 0
      .eslintignore
  3. 17 0
      .eslintrc
  4. 9 0
      .gitignore
  5. 11 0
      .travis.yml
  6. 49 0
      README.zh-CN.md
  7. 46 0
      app.js
  8. 83 0
      app/base/base_controller.js
  9. 126 0
      app/base/base_service.js
  10. 24 0
      app/const/cld_office.js
  11. 15 0
      app/const/message_type.js
  12. 49 0
      app/controller/login_controller.js
  13. 191 0
      app/extend/helper.js
  14. 81 0
      app/lib/captcha.js
  15. 92 0
      app/lib/sql_builder.js
  16. 85 0
      app/lib/sso.js
  17. 33 0
      app/middleware/auto_logger.js
  18. 18 0
      app/middleware/datetime_fill.js
  19. 25 0
      app/middleware/error_handler.js
  20. 71 0
      app/middleware/permission_filter.js
  21. 34 0
      app/middleware/session_auth.js
  22. 25 0
      app/middleware/sort_filter.js
  23. 27 0
      app/middleware/url_parse.js
  24. 29 0
      app/middleware/white_list_filter.js
  25. 9 0
      app/public/css/bootstrap/bootstrap-datetimepicker.min.css
  26. 7 0
      app/public/css/bootstrap/bootstrap.min.css
  27. 4 0
      app/public/css/font-awesome/font-awesome.min.css
  28. BIN
      app/public/css/font-awesome/fonts/FontAwesome.otf
  29. BIN
      app/public/css/font-awesome/fonts/fontawesome-webfont.eot
  30. 2671 0
      app/public/css/font-awesome/fonts/fontawesome-webfont.svg
  31. BIN
      app/public/css/font-awesome/fonts/fontawesome-webfont.ttf
  32. BIN
      app/public/css/font-awesome/fonts/fontawesome-webfont.woff
  33. BIN
      app/public/css/font-awesome/fonts/fontawesome-webfont.woff2
  34. BIN
      app/public/css/logo.png
  35. 330 0
      app/public/css/main.css
  36. BIN
      app/public/css/ztree/img/diy/1_close.png
  37. BIN
      app/public/css/ztree/img/diy/1_open.png
  38. BIN
      app/public/css/ztree/img/diy/2.png
  39. BIN
      app/public/css/ztree/img/diy/3.png
  40. BIN
      app/public/css/ztree/img/diy/4.png
  41. BIN
      app/public/css/ztree/img/diy/5.png
  42. BIN
      app/public/css/ztree/img/diy/6.png
  43. BIN
      app/public/css/ztree/img/diy/7.png
  44. BIN
      app/public/css/ztree/img/diy/8.png
  45. BIN
      app/public/css/ztree/img/diy/9.png
  46. BIN
      app/public/css/ztree/img/line_conn.gif
  47. BIN
      app/public/css/ztree/img/loading.gif
  48. BIN
      app/public/css/ztree/img/zTreeStandard.gif
  49. BIN
      app/public/css/ztree/img/zTreeStandard.png
  50. 97 0
      app/public/css/ztree/zTreeStyle.css
  51. BIN
      app/public/fonts/FontAwesome.otf
  52. BIN
      app/public/fonts/fontawesome-webfont.eot
  53. 2671 0
      app/public/fonts/fontawesome-webfont.svg
  54. BIN
      app/public/fonts/fontawesome-webfont.ttf
  55. BIN
      app/public/fonts/fontawesome-webfont.woff
  56. BIN
      app/public/fonts/fontawesome-webfont.woff2
  57. 1 0
      app/public/js/bootstrap/bootstrap-datetimepicker.min.js
  58. 657 0
      app/public/js/bootstrap/bootstrap-paginator.js
  59. 6 0
      app/public/js/bootstrap/bootstrap.min.js
  60. 16 0
      app/public/js/bootstrap/locales/bootstrap-datetimepicker.zh-CN.js
  61. 45 0
      app/public/js/captcha.js
  62. 28 0
      app/public/js/form_validate.js
  63. 47 0
      app/public/js/global.js
  64. 293 0
      app/public/js/gt.js
  65. 4 0
      app/public/js/jquery/jquery-3.2.1.min.js
  66. 1578 0
      app/public/js/jquery/jquery.validate.js
  67. 24 0
      app/public/js/message.js
  68. 33 0
      app/public/js/messages_zh.js
  69. 5 0
      app/public/js/popper/popper.min.js
  70. 56 0
      app/public/js/switch_btn.js
  71. 681 0
      app/public/js/ueditor/dialogs/attachment/attachment.css
  72. 60 0
      app/public/js/ueditor/dialogs/attachment/attachment.html
  73. 754 0
      app/public/js/ueditor/dialogs/attachment/attachment.js
  74. BIN
      app/public/js/ueditor/dialogs/attachment/fileTypeImages/icon_chm.gif
  75. BIN
      app/public/js/ueditor/dialogs/attachment/fileTypeImages/icon_default.png
  76. BIN
      app/public/js/ueditor/dialogs/attachment/fileTypeImages/icon_doc.gif
  77. BIN
      app/public/js/ueditor/dialogs/attachment/fileTypeImages/icon_exe.gif
  78. BIN
      app/public/js/ueditor/dialogs/attachment/fileTypeImages/icon_jpg.gif
  79. BIN
      app/public/js/ueditor/dialogs/attachment/fileTypeImages/icon_mp3.gif
  80. BIN
      app/public/js/ueditor/dialogs/attachment/fileTypeImages/icon_mv.gif
  81. BIN
      app/public/js/ueditor/dialogs/attachment/fileTypeImages/icon_pdf.gif
  82. BIN
      app/public/js/ueditor/dialogs/attachment/fileTypeImages/icon_ppt.gif
  83. BIN
      app/public/js/ueditor/dialogs/attachment/fileTypeImages/icon_psd.gif
  84. BIN
      app/public/js/ueditor/dialogs/attachment/fileTypeImages/icon_rar.gif
  85. BIN
      app/public/js/ueditor/dialogs/attachment/fileTypeImages/icon_txt.gif
  86. BIN
      app/public/js/ueditor/dialogs/attachment/fileTypeImages/icon_xls.gif
  87. BIN
      app/public/js/ueditor/dialogs/attachment/images/alignicon.gif
  88. BIN
      app/public/js/ueditor/dialogs/attachment/images/alignicon.png
  89. BIN
      app/public/js/ueditor/dialogs/attachment/images/bg.png
  90. BIN
      app/public/js/ueditor/dialogs/attachment/images/file-icons.gif
  91. BIN
      app/public/js/ueditor/dialogs/attachment/images/file-icons.png
  92. BIN
      app/public/js/ueditor/dialogs/attachment/images/icons.gif
  93. BIN
      app/public/js/ueditor/dialogs/attachment/images/icons.png
  94. BIN
      app/public/js/ueditor/dialogs/attachment/images/image.png
  95. BIN
      app/public/js/ueditor/dialogs/attachment/images/progress.png
  96. BIN
      app/public/js/ueditor/dialogs/attachment/images/success.gif
  97. BIN
      app/public/js/ueditor/dialogs/attachment/images/success.png
  98. 43 0
      app/public/js/ueditor/dialogs/emotion/emotion.css
  99. 54 0
      app/public/js/ueditor/dialogs/emotion/emotion.html
  100. 0 0
      app/public/js/ueditor/dialogs/emotion/emotion.js

+ 30 - 0
.autod.conf.js

@@ -0,0 +1,30 @@
+'use strict';
+
+module.exports = {
+  write: true,
+  prefix: '^',
+  plugin: 'autod-egg',
+  test: [
+    'test',
+    'benchmark',
+  ],
+  dep: [
+    'egg',
+    'egg-scripts',
+  ],
+  devdep: [
+    'egg-ci',
+    'egg-bin',
+    'egg-mock',
+    'autod',
+    'autod-egg',
+    'eslint',
+    'eslint-config-egg',
+    'webstorm-disable-index',
+  ],
+  exclude: [
+    './test/fixtures',
+    './dist',
+  ],
+};
+

+ 2 - 0
.eslintignore

@@ -0,0 +1,2 @@
+coverage
+app/public/*

+ 17 - 0
.eslintrc

@@ -0,0 +1,17 @@
+{
+    "extends": "eslint-config-egg",
+    "rules": {
+        // 空格4个
+        "indent": ["error", 4, {"SwitchCase": 1}],
+        // 指定数组的元素之间要以空格隔开(,后面), never参数:[ 之前和 ] 之后不能带空格,always参数:[ 之前和 ] 之后必须带空格
+        "array-bracket-spacing": [2, "never"],
+        // if while function 后面的{必须与if在同一行,java风格。
+        "brace-style": [2, "1tbs", {"allowSingleLine": true}],
+        // 未用数据不提示
+        "no-unused-vars": 0,
+        // 链式调用是否强制换行
+        "newline-per-chained-call": 0,
+        // 换行风格
+        "linebreak-style": [0, "windows"]
+    }
+}

+ 9 - 0
.gitignore

@@ -0,0 +1,9 @@
+logs/
+npm-debug.log
+node_modules/
+coverage/
+.idea/
+run/
+.DS_Store
+*.swp
+

+ 11 - 0
.travis.yml

@@ -0,0 +1,11 @@
+sudo: false
+language: node_js
+node_js:
+  - '6'
+  - '8'
+install:
+  - npm i npminstall && npminstall
+script:
+  - npm run ci
+after_script:
+  - npminstall codecov && codecov

+ 49 - 0
README.zh-CN.md

@@ -0,0 +1,49 @@
+# calculation
+
+计量支付Web 前台部分
+- 本程序使用mysql数据库作为数据存储
+- 本程序使用redis作为数据缓存
+
+## 快速入门
+
+<!-- 在此次添加使用文档 -->
+
+如需进一步了解,参见 [egg 文档][egg]。
+
+### 本地开发
+- 首先导入sql
+- 开启redis
+- 执行以下命令
+```bash
+$ npm i
+$ npm run dev
+$ open http://localhost:7001/
+```
+
+### 部署
+
+```bash
+$ npm start
+$ npm stop
+```
+
+### 单元测试
+- [egg-bin] 内置了 [mocha], [thunk-mocha], [power-assert], [istanbul] 等框架,让你可以专注于写单元测试,无需理会配套工具。
+- 断言库非常推荐使用 [power-assert]。
+- 具体参见 [egg 文档 - 单元测试](https://eggjs.org/zh-cn/core/unittest)。
+
+```bash
+$ npm run test-local  本地运行单元测试
+$ npm test            线上运行单元测试
+```
+
+> 注意: 运行单元测试必须停掉运行的网站,命令才能执行成功
+
+### 内置指令
+
+- 使用 `npm run lint` 来做代码风格检查。
+- 使用 `npm test` 来执行单元测试。
+- 使用 `npm run autod` 来自动检测依赖更新,详细参见 [autod](https://www.npmjs.com/package/autod) 。
+
+
+[egg]: https://eggjs.org

+ 46 - 0
app.js

@@ -0,0 +1,46 @@
+'use strict';
+
+/**
+ * 自定义启动文件
+ *
+ * @author CaiAoLin
+ * @date 2017/8/29
+ * @version
+ */
+
+const BaseService = require('./app/base/base_service');
+const BaseController = require('./app/base/base_controller');
+module.exports = app => {
+    // 数据模型基类
+    app.BaseService = BaseService;
+
+    // 控制器基类
+    app.BaseController = BaseController;
+
+    // 自定义手机校验规则
+    app.validator.addRule('isMobile', (rule, value) => {
+        try {
+            const regPhone = /^1([34578]\d)\d{8}$/;
+            if (!(value.length === 11 && regPhone.test(value))) {
+                throw 'please enter the correct phone number';
+            }
+        } catch (error) {
+            return error;
+        }
+    });
+
+    // 自定义手机IP规则
+    app.validator.addRule('isIP', (rule, value) => {
+        try {
+            if (value === '') {
+                throw 'please enter the correct ip address';
+            }
+            const regIP = /^(\d{2,})\.(\d+)\.(\d+)\.(\d+)$/;
+            if (!regIP.test(value)) {
+                throw 'please enter the correct ip address';
+            }
+        } catch (error) {
+            return error;
+        }
+    });
+};

+ 83 - 0
app/base/base_controller.js

@@ -0,0 +1,83 @@
+'use strict';
+
+/**
+ * 控制器基类
+ *
+ * @author CaiAoLin
+ * @date 2017/10/11
+ * @version
+ */
+const moment = require('moment');
+const messageType = require('../const/message_type');
+const Controller = require('egg').Controller;
+class BaseController extends Controller {
+
+    /**
+     * 构造函数
+     *
+     * @param {Object} ctx - egg全局context
+     * @return {void}
+     */
+    constructor(ctx) {
+        super(ctx);
+        this.messageType = messageType;
+    }
+
+    /**
+     * 渲染layout
+     *
+     * @param {String} view - 渲染的view
+     * @param {Object} data - 渲染的数据
+     * @return {void}
+     */
+    async layout(view, data = {}) {
+        data.moment = moment;
+        // 获取消息提示
+        const message = this.ctx.session.message;
+        // 取出后删除
+        this.ctx.session.message = null;
+
+        const viewString = await this.ctx.renderView(view, data);
+        const renderData = {
+            content: viewString,
+            message: JSON.stringify(message),
+            dropDownMenu: data.dropDownMenu === undefined ? [] : data.dropDownMenu,
+            breadCrumb: data.breadCrumb === undefined ? '' : data.breadCrumb,
+        };
+        await this.ctx.render('layout/layout.ejs', renderData);
+    }
+
+    /**
+     * 设置提示
+     *
+     * @param {String} message - 提示信息
+     * @param {String} type - 提示类型
+     * @return {void}
+     */
+    setMessage(message, type) {
+        let icon = '';
+        switch (type) {
+            case messageType.SUCCESS:
+                icon = 'check';
+                break;
+            case messageType.ERROR:
+                icon = 'exclamation-circle';
+                break;
+            case messageType.INFO:
+                icon = 'info-circle';
+                break;
+            case messageType.WARNING:
+                icon = 'warning';
+                break;
+            default:
+                break;
+        }
+        this.ctx.session.message = {
+            type,
+            icon,
+            message,
+        };
+    }
+}
+
+module.exports = BaseController;

+ 126 - 0
app/base/base_service.js

@@ -0,0 +1,126 @@
+'use strict';
+
+/**
+ * 业务逻辑基类
+ *
+ * @author CaiAoLin
+ * @date 2017/10/11
+ * @version
+ */
+const Service = require('egg').Service;
+// 数据模型基类
+class BaseService extends Service {
+    /**
+     * 构造函数
+     *
+     * @param {Object} ctx - egg全局context
+     * @return {void}
+     */
+    constructor(ctx) {
+        super(ctx);
+        this.db = this.app.mysql;
+        this.cache = this.app.redis;
+    }
+
+    /**
+     * 设置表名
+     *
+     * @param {String} table - 表名
+     * @return {void}
+     */
+    set tableName(table) {
+        this._table = this.app.config.tablePrefix + table;
+    }
+
+    /**
+     * 获取表名
+     *
+     * @return {String} - 返回表名
+     */
+    get tableName() {
+        return this._table;
+    }
+
+    /**
+     * 根据id查找数据
+     *
+     * @param {Number} id - 数据库中的id
+     * @return {Object} - 返回单条数据
+     */
+    async getDataById(id) {
+        return await this.db.get(this.tableName, { id });
+    }
+
+    /**
+     * 根据id删除数据
+     *
+     * @param {Number} id - 数据库中的id
+     * @return {boolean} - 返回是否删除成功
+     */
+    async deleteById(id) {
+        const result = await this.db.delete(this.tableName, { id });
+        return result.affectedRows > 0;
+    }
+
+    /**
+     * 获取总数
+     *
+     * @param {Object} option - 过滤条件(参考select方法中的condition)
+     * @return {Number} - 返回查找的总数
+     */
+    async count(option = {}) {
+        const result = await this.db.count(this.tableName, option);
+        return result;
+    }
+
+    /**
+     * 获取分页数据(SqlBuilder版本)
+     *
+     * @return {Array} - 返回分页数据
+     */
+    async getListWithBuilder() {
+
+        // 由于egg-mysql不提供like、不等于操作,所以需要自己组sql
+        if (this.sqlBuilder === null) {
+            return [];
+        }
+
+        // 分页相关
+        this.sqlBuilder.limit = this.app.config.pageSize;
+        this.sqlBuilder.offset = this.app.config.pageSize * (this.ctx.page - 1);
+
+        // 数据筛选
+        if (this.ctx.sort !== undefined && this.ctx.sort.length > 0) {
+            this.sqlBuilder.orderBy = [this.ctx.sort];
+        }
+
+        const [sql, sqlParam] = this.sqlBuilder.build(this.tableName);
+        const list = await this.db.query(sql, sqlParam);
+
+        return list;
+    }
+
+    /**
+     * 获取分页数据
+     *
+     * @param {Object} condition - 搜索条件
+     * @return {Array} - 返回分页数据
+     */
+    async getList(condition) {
+        const page = this.ctx.page;
+
+        // 分页设置
+        condition.limit = condition.limit === undefined ? this.app.config.pageSize : condition.limit;
+        condition.offset = condition.offset === undefined ? this.app.config.pageSize * (page - 1) : condition.offset;
+
+        if (this.ctx.sort !== undefined && this.ctx.sort.length > 0) {
+            condition.orders = [this.ctx.sort];
+        }
+        const list = await this.db.select(this.tableName, condition);
+
+        return list;
+    }
+
+
+}
+module.exports = BaseService;

+ 24 - 0
app/const/cld_office.js

@@ -0,0 +1,24 @@
+'use strict';
+
+/**
+ * CLD办事处
+ *
+ * @author CaiAoLin
+ * @date 2017/10/31
+ * @version
+ */
+const officeType = {
+    HEAD_OFFICE: 1,
+    GUANGDONG: 2,
+    CHONGQING: 3,
+};
+
+const office = [];
+office[officeType.HEAD_OFFICE] = '总部';
+office[officeType.GUANGDONG] = '广东办事处';
+office[officeType.CHONGQING] = '重庆办事处';
+
+module.exports = {
+    type: officeType,
+    list: office,
+};

+ 15 - 0
app/const/message_type.js

@@ -0,0 +1,15 @@
+'use strict';
+/**
+ * 消息提示类型
+ *
+ * @author CaiAoLin
+ * @date 2017/9/29
+ * @version
+ */
+const messageType = {
+    SUCCESS: 'success',
+    ERROR: 'error',
+    INFO: 'info',
+    WARNING: 'warning',
+};
+module.exports = messageType;

+ 49 - 0
app/controller/login_controller.js

@@ -0,0 +1,49 @@
+'use strict';
+
+/**
+ * 登录页面控制器
+ *
+ * @author CaiAoLin
+ * @date 2017/11/15
+ * @version
+ */
+
+const SSO = require('../lib/sso');
+module.exports = app => {
+
+    class LoginController extends app.BaseController {
+
+        /**
+         * 登录页面
+         *
+         * @param {Object} ctx - egg全局页面
+         * @return {void}
+         */
+        async index(ctx) {
+            const renderData = {};
+            await ctx.render('login/login.ejs', renderData);
+        }
+
+        /**
+         * 登录操作
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        async login(ctx) {
+            const username = ctx.request.body.username;
+            const password = ctx.request.body.password;
+
+            try {
+                const sso = new SSO(ctx);
+                const result = await sso.loginValid(username, password);
+            } catch (error) {
+                console.log(error);
+            }
+
+            ctx.body = 'success';
+        }
+    }
+
+    return LoginController;
+};

+ 191 - 0
app/extend/helper.js

@@ -0,0 +1,191 @@
+'use strict';
+
+/**
+ * 辅助方法扩展
+ *
+ * @author CaiAoLin
+ * @date 2017/9/28
+ * @version
+ */
+module.exports = {
+    /**
+     * 内置验证器转换为前端jquery验证器
+     *
+     * @param {Object} rule - 后端service中的rule
+     * @return {Object} - 返回转换后的数据
+     */
+    validateConvert(rule) {
+        const result = {};
+        if (Object.keys(rule).length <= 0) {
+            return rule;
+        }
+
+        for (const index in rule) {
+            result[index] = {};
+            const type = rule[index].type !== undefined && rule[index].type !== '' ? rule[index].type : '';
+            const stringType = ['string', 'password'];
+            // 是否必填
+            if (rule[index].required !== undefined) {
+                result[index].required = rule[index].required;
+            }
+
+            // 最小长度
+            if (stringType.indexOf(type) >= 0 && rule[index].min !== undefined) {
+                result[index].minlength = rule[index].min;
+            }
+
+            // 最大长度
+            if (stringType.indexOf(type) >= 0 && rule[index].max !== undefined) {
+                result[index].maxlength = rule[index].max;
+            }
+
+            // 密码相关
+            if (type === 'password' && rule[index].compare !== undefined) {
+                result[index].equalTo = '#' + rule[index].compare;
+            }
+
+            // 最小值
+            const integerType = ['integer', 'int', 'Number'];
+            if (integerType.indexOf(type) >= 0 && rule[index].min !== undefined) {
+                result[index].min = rule[index].min;
+            }
+
+            // 最大值
+            if (integerType.indexOf(type) >= 0 && rule[index].max !== undefined) {
+                result[index].max = rule[index].max;
+            }
+
+            // 自定义判断
+            if (rule[index] === 'isMobile' || rule[index] === 'isIP') {
+                result[index] = rule[index];
+            }
+
+        }
+
+        return result;
+    },
+
+    /**
+     * 生成随机字符串
+     *
+     * @param {Number} length - 需要生成字符串的长度
+     * @param {Number} type - 1为数字和字符 2为纯数字 3为纯字母
+     * @return {String} - 返回生成结果
+     */
+    generateRandomString(length, type = 1) {
+        length = parseInt(length);
+        length = isNaN(length) ? 1 : length;
+        let randSeed = [];
+        let numberSeed = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
+        let stringSeed = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
+            'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
+            'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'];
+
+        switch (type) {
+            case 1:
+                randSeed = stringSeed.concat(numberSeed);
+                stringSeed = numberSeed = null;
+                break;
+            case 2:
+                randSeed = numberSeed;
+                break;
+            case 3:
+                randSeed = stringSeed;
+                break;
+            default:
+                break;
+        }
+
+        const seedLength = randSeed.length - 1;
+        let result = '';
+        for (let i = 0; i < length; i++) {
+            const index = Math.ceil(Math.random() * seedLength);
+            result += randSeed[index];
+        }
+
+        return result;
+    },
+
+    /**
+     * 显示排序符号
+     *
+     * @param {String} field - 字段名称
+     * @return {String} - 返回字段排序的符号
+     */
+    showSortFlag(field) {
+        const sort = this.ctx.sort;
+        if (!(sort instanceof Array) || sort.length !== 2) {
+            return '';
+        }
+        sort[1] = sort[1].toUpperCase();
+        return (sort[0] === field && sort[1] === 'DESC') ? '' : '-';
+    },
+
+    /**
+     * 判断是否为ajax请求
+     *
+     * @param {Object} request - 请求数据
+     * @return {boolean} 判断结果
+     */
+    isAjax(request) {
+        let headerInfo = request.headers['x-requested-with'] === undefined ? '' : request.headers['x-requested-with'];
+        headerInfo = headerInfo.toLowerCase();
+        return headerInfo === 'xmlhttprequest';
+    },
+
+    /**
+     * 模拟发送请求
+     *
+     * @param {String} url - 请求地址
+     * @param {Object} data - 请求数据
+     * @param {String} type - 请求类型(POST) POST | GET
+     * @return {Object} - 请求结果
+     */
+    async sendRequest(url, data, type = 'POST') {
+        // 发起请求
+        const response = await this.ctx.curl(url, {
+            method: type,
+            data,
+            dataType: 'json',
+        });
+        if (response.status !== 200) {
+            throw '请求失败';
+        }
+
+        return response.data;
+    },
+
+    /**
+     * 转换面包屑导航数据
+     *
+     * @param {Object} breadCrumb - 面包屑数据
+     * @param {String} mode - 模式 append 为追加型模式 \ replace 为全部替换模式
+     * @return {String} - 转换为字符串数据
+     */
+    convertBreadCrumb(breadCrumb, mode = 'append') {
+        let result = '';
+        if (!breadCrumb instanceof Array || breadCrumb.length <= 0) {
+            return result;
+        }
+        if (mode === 'append') {
+            const tmpBreadCrumb = [];
+            tmpBreadCrumb.push({ name: this.ctx.topPermission.name });
+            tmpBreadCrumb.push.apply(tmpBreadCrumb, breadCrumb);
+            tmpBreadCrumb.push({ name: this.ctx.currentName });
+            breadCrumb = tmpBreadCrumb;
+        }
+        const tmpList = [];
+        for (const data of breadCrumb) {
+            if (data.name === undefined) {
+                continue;
+            }
+            const tmp = data.url === undefined || data.url === '' ?
+                data.name : '<a href="/' + data.url + '">' + data.name + '</a>';
+            tmpList.push(tmp);
+        }
+        result = tmpList.join(' / ');
+
+        return result;
+    },
+
+};

+ 81 - 0
app/lib/captcha.js

@@ -0,0 +1,81 @@
+'use strict';
+
+/**
+ * 验证码
+ *
+ * @author CaiAoLin
+ * @date 2017/10/13
+ * @version
+ */
+const Geetest = require('gt3-sdk');
+class Captcha {
+
+    /**
+     * 构造函数
+     *
+     * @param {Object} app - egg中的全局变量app(用于获取配置)
+     * @return {void}
+     */
+    constructor(app) {
+        this.captcha = new Geetest({
+            geetest_id: app.config.geetest.id,
+            geetest_key: app.config.geetest.key,
+        });
+    }
+
+    /**
+     * 验证码注册
+     *
+     * @param {Object} ctx - egg中的全局变量context
+     * @return {String} - 返回生成所需的json数据
+     */
+    async register(ctx) {
+        let response = {
+            success: 0,
+        };
+        try {
+            const data = await this.captcha.register(null);
+            ctx.session.fallback = !data.success;
+            response = data;
+        } catch (error) {
+            response.success = 0;
+            response.message = error;
+            JSON.stringify(response);
+        }
+
+        return response;
+    }
+
+    /**
+     * 验证码校验
+     *
+     * @param {Object} ctx - egg中的全局变量context
+     * @return {Boolean} - 返回是否校验成功
+     */
+    async validate(ctx) {
+        let result = false;
+
+        const challenge = ctx.request.body.geetest_challenge;
+        const validate = ctx.request.body.geetest_validate;
+        const seccode = ctx.request.body.geetest_seccode;
+
+        try {
+            if (challenge === '' || validate === '' || seccode === '') {
+                throw '参数错误';
+            }
+            const fallback = ctx.session.fallback;
+
+            result = await this.captcha.validate(fallback, {
+                geetest_challenge: challenge,
+                geetest_validate: validate,
+                geetest_seccode: seccode,
+            });
+
+        } catch (error) {
+            result = false;
+        }
+
+        return result;
+    }
+}
+module.exports = Captcha;

+ 92 - 0
app/lib/sql_builder.js

@@ -0,0 +1,92 @@
+'use strict';
+
+/**
+ * sql语句构建器
+ *
+ * @author CaiAoLin
+ * @date 2017/10/11
+ * @version
+ */
+class SqlBuilder {
+
+    /**
+     * 构造函数
+     *
+     * @return {void}
+     */
+    constructor() {
+        this.andWhere = {};
+        this.orWhere = {};
+        this.columns = [];
+        this.limit = -1;
+        this.offset = -1;
+        this.orderBy = [];
+    }
+
+    /**
+     * 设置andWhere数据
+     *
+     * @param {String} field - where中的字段名称
+     * @param {Object} data - where中的value部分
+     * @return {void}
+     */
+    setAndWhere(field, data) {
+        if (Object.keys(data).length <= 0) {
+            return;
+        }
+        this.andWhere[field] = data;
+    }
+
+    /**
+     * 构建sql
+     *
+     * @param {String} tableName - 表名
+     * @return {Array} - 返回数组,第一个元素为sql语句,第二个元素为sql中问号部分的param
+     */
+    build(tableName) {
+        let sql = this.columns.length === 0 ? 'SELECT * FROM ??' : 'SELECT ?? FROM ??';
+        const sqlParam = this.columns.length === 0 ? [tableName] : [this.columns, tableName];
+
+        if (Object.keys(this.andWhere).length > 0) {
+            const whereArr = [];
+            for (const index in this.andWhere) {
+                whereArr.push(' ?? ' + this.andWhere[index].operate + ' ' + this.andWhere[index].value);
+                sqlParam.push(index);
+            }
+            const whereString = whereArr.join(' AND ');
+            sql += ' WHERE ' + whereString;
+        }
+
+        if (Object.keys(this.orWhere).length > 0) {
+            const whereArr = [];
+            for (const index in this.orWhere) {
+                whereArr.push(' ?? ' + this.orWhere[index].operate + ' ' + this.orWhere[index].value);
+                sqlParam.push(index);
+            }
+            const whereString = whereArr.join(' OR ');
+            // 如果前面已经有设置过WHERE则不需要再重复添加
+            sql += sql.indexOf('WHERE') > 0 ? whereString : ' WHERE ' + whereString;
+        }
+
+        if (typeof this.limit === 'number' && this.limit > 0) {
+            this.offset = parseInt(this.offset);
+            this.offset = isNaN(this.offset) || this.offset < 0 ? 0 : this.offset;
+            const limitString = this.offset >= 0 ? this.offset + ',' + this.limit : this.limit;
+            sql += ' LIMIT ' + limitString;
+        }
+
+        if (this.orderBy.length > 0) {
+            const orderArr = [];
+            for (const index in this.orderBy) {
+                orderArr.push(' ?? ' + this.orderBy[index][1]);
+                sqlParam.push(this.orderBy[index][0]);
+            }
+            const orderByString = orderArr.join(',');
+            sql += ' ORDER BY ' + orderByString;
+        }
+
+        return [sql, sqlParam];
+    }
+
+}
+module.exports = SqlBuilder;

+ 85 - 0
app/lib/sso.js

@@ -0,0 +1,85 @@
+'use strict';
+
+/**
+ * sso相关接口
+ *
+ * @author CaiAoLin
+ * @date 2017/11/15
+ * @version
+ */
+
+// 加密类
+const crypto = require('crypto');
+class SSO {
+
+    /**
+     * 构造函数
+     *
+     * @param {Object} ctx - egg全局变量
+     * @return {void}
+     */
+    constructor(ctx) {
+        this.authUrl = 'http://sso.smartcost.com.cn/api/jzlogin';
+        this.ctx = ctx;
+    }
+
+    /**
+     * SSO登录验证
+     *
+     * @param {String} username - cld用户名
+     * @param {String} password - cld密码
+     * @return {boolean} - 验证结果
+     */
+    async loginValid(username, password) {
+        let result = false;
+        try {
+            if (username === '' || password === '') {
+                throw '用户名或密码错误';
+            }
+            // 生成加密token
+            const postData = {
+                username,
+                userpasswd: password,
+            };
+
+            const responseData = await this.ctx.helper.sendRequest(this.authUrl, postData);
+
+            if (responseData.length <= 0) {
+                throw '接口返回错误:' + responseData.err;
+            }
+            // 如果验证成功,则新增SSO数据到数据库
+            if (responseData.data !== '') {
+                const addResult = await this.ctx.service.customer.addSSOUser(responseData[0]);
+                if (!addResult) {
+                    console.log('sso user add error');
+                }
+            }
+            result = true;
+        } catch (error) {
+            console.log('sso:' + error);
+            result = false;
+        }
+
+        return result;
+    }
+
+    /**
+     * 获取SSO用户数据
+     *
+     * @param {Number} ssoID - sso中的id
+     * @return {String} - 返回json数据
+     */
+    async getSSOUserData(ssoID) {
+        let result = {};
+        try {
+            result = ssoID;
+        } catch (error) {
+            result = {};
+        }
+
+        return result;
+    }
+
+}
+
+module.exports = SSO;

+ 33 - 0
app/middleware/auto_logger.js

@@ -0,0 +1,33 @@
+'use strict';
+
+/**
+ * 自动记录日志
+ *
+ * @author CaiAoLin
+ * @date 2017/10/30
+ * @version
+ */
+
+module.exports = options => {
+    return function* autoLogger(next) {
+        // 自动记录log的action
+        const autoLogAction = ['save', 'delete'];
+        if (this.actionName !== undefined && autoLogAction.indexOf(this.actionName) >= 0) {
+            // 操作数据的id
+            const idReg = /\/(\d+)/;
+            const paramInfo = this.request.originalUrl.match(idReg);
+            let targetId = paramInfo[1] !== undefined ? paramInfo[1] : -1;
+            targetId = parseInt(targetId);
+
+            const logData = {
+                controller: this.controllerName,
+                action: this.actionName,
+                operation: this.currentName === undefined ? '保存数据' : this.currentName,
+                target_id: targetId,
+            };
+            yield this.service.log.addLog(logData);
+        }
+
+        yield next;
+    };
+};

+ 18 - 0
app/middleware/datetime_fill.js

@@ -0,0 +1,18 @@
+'use strict';
+
+/**
+ * 自动填充时间戳
+ *
+ * @author CaiAoLin
+ * @date 2017/10/25
+ * @version
+ */
+
+module.exports = options => {
+    return function* datetimeFill(next) {
+        let currentTime = new Date().getTime() / 1000;
+        currentTime = parseInt(currentTime);
+        this.request.body.create_time = currentTime;
+        yield next;
+    };
+};

+ 25 - 0
app/middleware/error_handler.js

@@ -0,0 +1,25 @@
+'use strict';
+
+/**
+ * 异常处理
+ *
+ * @author CaiAoLin
+ * @date 2017/10/25
+ * @version
+ */
+
+module.exports = () => {
+    return function* errorHandler(next) {
+        try {
+            yield next;
+        } catch (err) {
+            // 注意:自定义的错误统一处理函数捕捉到错误后也要 `app.emit('error', err, this)`
+            // 框架会统一监听,并打印对应的错误日志
+            this.app.emit('error', err, this);
+            // mysql重连
+            if (err.code === 'PROTOCOL_CONNECTION_LOST') {
+                this.app.mysql.createInstance(this.app.config.mysql.client);
+            }
+        }
+    };
+};

+ 71 - 0
app/middleware/permission_filter.js

@@ -0,0 +1,71 @@
+'use strict';
+
+module.exports = option => {
+    /**
+     * 用户权限筛选中间件
+     *
+     * @param {function} next - 中间件继续执行的方法
+     * @return {void}
+     */
+    return function* permissionFilter(next) {
+        // 获取所有权限数据
+        const permissionData = yield this.service.permission.getAllData(true, true);
+        this.currentName = '';
+        let currentPermissionId = 0;
+        // 查找controller和action名称相同的数据
+        for (const index in permissionData) {
+            if (permissionData[index].controller === this.controllerName && permissionData[index].action === this.actionName) {
+                this.currentName = permissionData[index].name;
+                currentPermissionId = permissionData[index].id;
+                break;
+            }
+        }
+        // 如果页面没有录入db,则允许通过,否则进入权限判断
+        if (currentPermissionId > 0) {
+            // 查找当前用户是否有对应页面权限
+            const managerSession = this.session.managerSession;
+            const permission = managerSession.permission;
+            try {
+                checkPermission(permission, currentPermissionId);
+            } catch (error) {
+                this.session.message = {
+                    type: 'error',
+                    icon: 'exclamation-sign',
+                    message: error.toString(),
+                };
+                return this.redirect('/dashboard');
+            }
+        }
+
+        // 找出对应页面的最顶层id
+        this.permissionRootId = 0;
+        this.service.permission.getTopPid(currentPermissionId, permissionData);
+
+        // 查找最顶层数据
+        this.topPermission = yield this.service.permission.getCacheDataById(this.permissionRootId);
+
+        this.currentName = this.currentName === '' ? '后台管理' : this.currentName;
+        yield next;
+    };
+};
+/**
+ * 判断权限
+ *
+ * @param {String} permissionList - 当前用户权限字符串
+ * @param {Number} currentPermissionId - 当前被访问页面的权限id
+ * @return {void}
+ */
+function checkPermission(permissionList, currentPermissionId) {
+    if (permissionList === '') {
+        throw '用户组权限为空';
+    }
+    // 如果是超级管理员则直接返回
+    if (permissionList === 'all') {
+        return;
+    }
+    const permissionIdList = permissionList.split(',');
+    if (permissionIdList.indexOf(currentPermissionId + '') < 0) {
+        throw '当前用户组没有对应权限';
+    }
+
+}

+ 34 - 0
app/middleware/session_auth.js

@@ -0,0 +1,34 @@
+'use strict';
+
+// 加密类
+const crypto = require('crypto');
+module.exports = options => {
+    /**
+     * session判断中间件
+     *
+     * @param {function} next - 中间件继续执行的方法
+     * @return {void}
+     */
+    return function* sessionAuth(next) {
+        try {
+            // 判断session
+            const managerSession = this.session.managerSession;
+            if (managerSession === undefined) {
+                throw '不存在session';
+            }
+            // 校验session
+            if (managerSession.username === undefined || managerSession.loginTime === undefined) {
+                throw '用户数据不完整';
+            }
+            // 校验session
+            const sessionToken = crypto.createHmac('sha1', managerSession.loginTime + '')
+                .update(managerSession.username).digest().toString('base64');
+            if (sessionToken !== managerSession.sessionToken) {
+                throw 'session数据错误';
+            }
+        } catch (error) {
+            return this.redirect('/');
+        }
+        yield next;
+    };
+};

+ 25 - 0
app/middleware/sort_filter.js

@@ -0,0 +1,25 @@
+'use strict';
+
+module.exports = options => {
+    /**
+     * 排序筛选
+     *
+     * @param {function} next - 中间件继续执行的方法
+     * @return {void}
+     */
+    return function* sortFilter(next) {
+        this.sort = [];
+        // 排序相关
+        const sort = this.request.query.sort;
+        if (sort !== undefined) {
+            const sortData = sort.split('-');
+            this.sort = sortData.length > 1 ? [sortData[1], 'DESC'] : [sortData[0], 'ASC'];
+        }
+        // 分页数据相关
+        let page = this.request.query.page;
+        page = parseInt(page);
+        page = isNaN(page) || page <= 0 ? 1 : page;
+        this.page = page;
+        yield next;
+    };
+};

+ 27 - 0
app/middleware/url_parse.js

@@ -0,0 +1,27 @@
+'use strict';
+
+// url类
+const Url = require('url');
+module.exports = options => {
+    /**
+     * 控制器以及动作的获取
+     *
+     * @param {function} next - 中间件继续执行的方法
+     * @return {void}
+     */
+    return function* urlParse(next) {
+        this.actionName = '';
+        // 获取当前控制器和动作名称
+        const urlInfo = Url.parse(this.request.originalUrl, true);
+        const url = urlInfo.pathname.substr(1);
+        const actionInfo = url.split('/');
+
+        this.controllerName = typeof actionInfo === 'object' && actionInfo[0] !== undefined ? actionInfo[0] : '';
+        this.actionName = typeof actionInfo === 'object' && actionInfo[1] !== undefined ? actionInfo[1] : '';
+
+        this.urlInfo = urlInfo;
+        // 防止为空
+        this.request.headers.referer = this.request.headers.referer === undefined ? '/dashboard' : this.request.headers.referer;
+        yield next;
+    };
+};

+ 29 - 0
app/middleware/white_list_filter.js

@@ -0,0 +1,29 @@
+'use strict';
+
+module.exports = options => {
+    /**
+     * 白名单过滤器
+     *
+     * @param {function} next - 中间件继续执行的方法
+     * @return {void}
+     */
+    return function* whiteListFilter(next) {
+        // 获取当前ip
+        const ip = this.request.ip;
+        // 获取白名单数据
+        const whiteList = yield this.service.whiteList.getAllData();
+        // 判断是否为ajax请求
+        if (this.helper.isAjax(this.request)) {
+            // 检测token @todo
+        } else {
+            // 不是ajax请求则判断ip是否为合法
+            if (whiteList[ip + ':1'] === undefined) {
+                this.status = 404;
+                return;
+            }
+        }
+
+        yield next;
+    };
+};
+

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 9 - 0
app/public/css/bootstrap/bootstrap-datetimepicker.min.css


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 7 - 0
app/public/css/bootstrap/bootstrap.min.css


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 4 - 0
app/public/css/font-awesome/font-awesome.min.css


BIN
app/public/css/font-awesome/fonts/FontAwesome.otf


BIN
app/public/css/font-awesome/fonts/fontawesome-webfont.eot


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 2671 - 0
app/public/css/font-awesome/fonts/fontawesome-webfont.svg


BIN
app/public/css/font-awesome/fonts/fontawesome-webfont.ttf


BIN
app/public/css/font-awesome/fonts/fontawesome-webfont.woff


BIN
app/public/css/font-awesome/fonts/fontawesome-webfont.woff2


BIN
app/public/css/logo.png


+ 330 - 0
app/public/css/main.css

@@ -0,0 +1,330 @@
+/*building SAAS 0.1*/
+/*bootstrap 初始化*/
+body {
+    font-size: 0.9rem;
+    overflow: hidden;
+    background: #e4e7ea
+}
+.dropdown-menu {
+    font-size: 0.9rem
+}
+.btn.disabled, .btn:disabled {
+  color:#999
+}
+/*自定义css*/
+.form-signin {
+    max-width: 500px;
+    margin: 150px auto;
+}
+.has-danger {
+    -webkit-animation: shake 1s .2s ease both;
+    -moz-animation: shake 1s .2s ease both;
+    animation: shake 1s .2s ease both;
+}
+@-webkit-keyframes shake {
+    0%, 100% {
+        -webkit-transform: translateX(0);
+    }
+    10%, 30%, 50%, 70%, 90% {
+        -webkit-transform: translateX(-10px);
+    }
+    20%, 40%, 60%, 80% {
+        -webkit-transform: translateX(10px);
+    }
+}
+@-moz-keyframes shake {
+    0%, 100% {
+        -moz-transform: translateX(0);
+    }
+    10%, 30%, 50%, 70%, 90% {
+        -moz-transform: translateX(-10px);
+    }
+    20%, 40%, 60%, 80% {
+        -moz-transform: translateX(10px);
+    }
+}
+@keyframes shake {
+    0%, 100% {
+        transform: translateX(0);
+    }
+    10%, 30%, 50%, 70%, 90% {
+        transform: translateX(-10px);
+    }
+    20%, 40%, 60%, 80% {
+        transform: translateX(10px);
+    }
+}
+/*2.主体框架*/
+.header {
+  background:#fff;
+  position: fixed;
+  z-index: 10;
+  width: 100%;
+  height: 50px;
+  top: 0;
+  left: 0
+}
+.main{
+  position: relative;
+  z-index: 4;
+}
+.main-nav {
+  position: fixed;
+  z-index: 99;
+  width:55px;
+  left: 0;
+  top: 0;
+  height: 100%;
+  background: #33425b;
+}
+.main-panel{
+  padding-left:55px;
+  box-sizing: border-box;
+}
+.panel-sidebar{
+  box-sizing: border-box;
+  background: #fbfcfd;
+  position: fixed;
+  height: 100%;
+  z-index: 4;
+  left:55px;
+  padding-top: 100px;
+  border-right: 1px solid #ddd;
+  width: 250px;
+}
+.panel-content{
+  padding:115px 0 0;
+  position: relative;
+  z-index: 3;
+  box-sizing: border-box;
+  overflow-y: auto;
+  height: 100vh;
+}
+.panel-content .content-wrap{
+  margin:0 15px 15px;
+}
+.panel-sidebar+.panel-content{
+  padding: 115px 0 0 250px;
+}
+.panel-title, .panel-title>.title-bar {
+  height:50px;
+  line-height: 50px
+}
+.panel-title{
+  position: fixed;
+  top: 50px;
+  z-index: 98;
+  width: 100%;
+  box-sizing: border-box;
+  background: #fff;
+  box-shadow: 0 1px 3px rgba(0,0,0,.05);
+  border-top: 1px solid #ddd;
+}
+.panel-sidebar .panel-title{
+  width:250px;
+  border-right: 1px solid #ddd;
+  box-shadow: 0 1px 3px rgba(0,0,0,.1);
+}
+.panel-content .panel-title{
+  left: 0;
+  padding-left: 305px;
+  padding-right: 20px;
+}
+.panel-content .panel-title.fluid{
+  padding-left:55px
+}
+.panel-title>.title-bar{
+  padding-left: 20px
+}
+.panel-title>.title-bar>h2,.panel-title>.title-main>h2{
+  font-size: 16px;
+  margin:0;
+  height: 50px;
+  line-height: 50px;
+  display:block
+}
+.panel-title>.title-bar>h2 .btn{
+  margin-right:15px
+}
+.panel-title>.title-main .btn {
+  margin:0 0 0 20px
+}
+.panel-title>.title-main .btn.pull-right {
+  margin:10px 0 0 10px
+}
+.panel-title>.title-main{
+  padding-left: 15px
+}
+/*滚动*/
+.scrollbar-auto {
+    overflow-y: auto;
+    position: absolute;
+    bottom: 0;
+    left: 0;
+    top: 0;
+    right: 0;
+}
+.panel-sidebar .scrollbar-auto{
+    padding-top: 20px;
+    box-sizing: border-box;
+}
+.panel-sidebar .scrollbar-auto {
+    height: 100%;
+    width: 100%;
+    overflow-y: auto;
+    position: static;
+}
+/*头部*/
+.header .logo {
+  float: left;
+  box-shadow: 1px 0 6px rgba(0,0,0,.06);
+  margin-right: 20px;
+  margin:0
+}
+.header .logo>a{
+  width:55px;
+  height:50px;
+  line-height: 50px;
+  display: inline-block;
+  color:#fff;
+  font-size:24px;
+  padding:0 10px;
+  transition: all ease .4s;
+  background:#207fd1 url(logo.png) no-repeat;
+  text-indent: -9999px;
+  vertical-align: top
+}
+.header .logo>a:hover{
+  background-color:#5596cf;
+  text-decoration: none;
+}
+.header-user > div {
+  float:left
+}
+.avatar .pic {
+  height: 35px;
+  width: 35px;
+  border-radius: 100%;
+  display: inline-block;
+  float:left;
+  margin:7px 7px 0 0
+}
+.avatar .pic img{
+  display: block;
+  width: 100%;
+  height: 100%;
+  border-radius: 100%;
+}
+.avatar > a,.msg >a{
+  display: block;
+  height:50px;
+  line-height: 50px;
+  color:#666;
+  padding:0 15px;
+  cursor: pointer;
+}
+.avatar > a:hover,.msg > a:hover{
+  text-decoration: none;
+  box-shadow: inset 0 3px 5px rgba(0,0,0,.125)
+}
+.header-user .msg{
+  border-left:1px solid #eee
+}
+.header-user .msg .glyphicon{
+  font-size:20px;
+  vertical-align: middle;
+}
+.header-user .msg .badge{
+  margin:0 0 0 5px
+}
+.header .poj-name {
+  float:left;
+  padding:0 0 0 15px;
+  font-size:18px
+}
+.header .poj-name a{
+  color:#666
+}
+.header .poj-name > span{
+  height:50px;
+  line-height:50px;
+}
+/*登陆相关*/
+.login-body{
+  background: #fff
+}
+.login-infoinput {
+  margin-top:15%
+}
+/*侧栏主菜单*/
+.nav-top{
+  padding-top: 50px
+}
+.bg-nav a{
+  color:#7786ab;
+  font-size: 24px;
+  width:55px;
+  height: 55px;
+  line-height: 55px;
+  display: inline-block;
+}
+.bg-nav > li.active > a{
+  border-radius: 0;
+  background: #192948
+}
+.bg-nav > li > a:hover,.bg-nav > li.active > a:hover{
+  background: #192948;
+  color:#f2f2f2
+}
+.bg-nav > li + li {
+    margin-top:0;
+}
+.nav-box h3{
+  font-size: 14px;
+  font-weight: 700;
+  padding-bottom: 4px;
+  border-bottom: 1px solid #e2eaec;
+  padding-right: 15px;
+  margin-bottom: 10px;
+  margin-left: 20px
+}
+.nav-list li a{
+  color: #333;
+  display: block;
+  height: 35px;
+  line-height: 35px;
+  box-sizing: border-box;
+  padding-left: 17px;
+  padding-right: 45px;
+  text-overflow: ellipsis;
+  position: relative;
+}
+.nav-list li a:hover{
+  text-decoration: none;
+  background:#e4e7ea;
+  cursor: pointer;
+}
+.nav-list li a .badge{
+  position: absolute;
+  right:17px;
+  top:9px
+}
+.nav-list li.active a{
+  background:#e4e7ea;
+  font-weight: 600
+}
+.nav-list li.active .badge{
+  background:#207fd1
+}
+/*内容区*/
+.c-header {
+  padding:0 0 5px
+}
+.c-body{
+  padding:15px;
+  background:#fff;
+}
+.form-group .necessary{
+  font-size:18px;
+  color:#f90000
+}

BIN
app/public/css/ztree/img/diy/1_close.png


BIN
app/public/css/ztree/img/diy/1_open.png


BIN
app/public/css/ztree/img/diy/2.png


BIN
app/public/css/ztree/img/diy/3.png


BIN
app/public/css/ztree/img/diy/4.png


BIN
app/public/css/ztree/img/diy/5.png


BIN
app/public/css/ztree/img/diy/6.png


BIN
app/public/css/ztree/img/diy/7.png


BIN
app/public/css/ztree/img/diy/8.png


BIN
app/public/css/ztree/img/diy/9.png


BIN
app/public/css/ztree/img/line_conn.gif


BIN
app/public/css/ztree/img/loading.gif


BIN
app/public/css/ztree/img/zTreeStandard.gif


BIN
app/public/css/ztree/img/zTreeStandard.png


+ 97 - 0
app/public/css/ztree/zTreeStyle.css

@@ -0,0 +1,97 @@
+/*-------------------------------------
+zTree Style
+
+version:	3.5.19
+author:		Hunter.z
+email:		hunter.z@263.net
+website:	http://code.google.com/p/jquerytree/
+
+-------------------------------------*/
+
+.ztree * {padding:0; margin:0; font-size:12px; font-family: Verdana, Arial, Helvetica, AppleGothic, sans-serif}
+.ztree {margin:0; padding:5px; color:#333}
+.ztree li{padding:0; margin:0; list-style:none; line-height:14px; text-align:left; white-space:nowrap; outline:0}
+.ztree li ul{ margin:0; padding:0 0 0 18px}
+.ztree li ul.line{ background:url(./img/line_conn.gif) 0 0 repeat-y;}
+
+.ztree li a {padding:1px 3px 0 0; margin:0; cursor:pointer; height:17px; color:#333; background-color: transparent;
+	text-decoration:none; vertical-align:top; display: inline-block}
+.ztree li a:hover {text-decoration:underline}
+.ztree li a.curSelectedNode {padding-top:0px; background-color:#FFE6B0; color:black; height:16px; border:1px #FFB951 solid; opacity:0.8;}
+.ztree li a.curSelectedNode_Edit {padding-top:0px; background-color:#FFE6B0; color:black; height:16px; border:1px #FFB951 solid; opacity:0.8;}
+.ztree li a.tmpTargetNode_inner {padding-top:0px; background-color:#316AC5; color:white; height:16px; border:1px #316AC5 solid;
+	opacity:0.8; filter:alpha(opacity=80)}
+.ztree li a.tmpTargetNode_prev {}
+.ztree li a.tmpTargetNode_next {}
+.ztree li a input.rename {height:14px; width:80px; padding:0; margin:0;
+	font-size:12px; border:1px #7EC4CC solid; *border:0px}
+.ztree li span {line-height:16px; margin-right:2px}
+.ztree li span.button {line-height:0; margin:0; width:16px; height:16px; display: inline-block; vertical-align:middle;
+	border:0 none; cursor: pointer;outline:none;
+	background-color:transparent; background-repeat:no-repeat; background-attachment: scroll;
+	background-image:url("./img/zTreeStandard.png"); *background-image:url("./img/zTreeStandard.gif")}
+
+.ztree li span.button.chk {width:13px; height:13px; margin:0 3px 0 0; cursor: auto}
+.ztree li span.button.chk.checkbox_false_full {background-position:0 0}
+.ztree li span.button.chk.checkbox_false_full_focus {background-position:0 -14px}
+.ztree li span.button.chk.checkbox_false_part {background-position:0 -28px}
+.ztree li span.button.chk.checkbox_false_part_focus {background-position:0 -42px}
+.ztree li span.button.chk.checkbox_false_disable {background-position:0 -56px}
+.ztree li span.button.chk.checkbox_true_full {background-position:-14px 0}
+.ztree li span.button.chk.checkbox_true_full_focus {background-position:-14px -14px}
+.ztree li span.button.chk.checkbox_true_part {background-position:-14px -28px}
+.ztree li span.button.chk.checkbox_true_part_focus {background-position:-14px -42px}
+.ztree li span.button.chk.checkbox_true_disable {background-position:-14px -56px}
+.ztree li span.button.chk.radio_false_full {background-position:-28px 0}
+.ztree li span.button.chk.radio_false_full_focus {background-position:-28px -14px}
+.ztree li span.button.chk.radio_false_part {background-position:-28px -28px}
+.ztree li span.button.chk.radio_false_part_focus {background-position:-28px -42px}
+.ztree li span.button.chk.radio_false_disable {background-position:-28px -56px}
+.ztree li span.button.chk.radio_true_full {background-position:-42px 0}
+.ztree li span.button.chk.radio_true_full_focus {background-position:-42px -14px}
+.ztree li span.button.chk.radio_true_part {background-position:-42px -28px}
+.ztree li span.button.chk.radio_true_part_focus {background-position:-42px -42px}
+.ztree li span.button.chk.radio_true_disable {background-position:-42px -56px}
+
+.ztree li span.button.switch {width:18px; height:18px}
+.ztree li span.button.root_open{background-position:-92px -54px}
+.ztree li span.button.root_close{background-position:-74px -54px}
+.ztree li span.button.roots_open{background-position:-92px 0}
+.ztree li span.button.roots_close{background-position:-74px 0}
+.ztree li span.button.center_open{background-position:-92px -18px}
+.ztree li span.button.center_close{background-position:-74px -18px}
+.ztree li span.button.bottom_open{background-position:-92px -36px}
+.ztree li span.button.bottom_close{background-position:-74px -36px}
+.ztree li span.button.noline_open{background-position:-92px -72px}
+.ztree li span.button.noline_close{background-position:-74px -72px}
+.ztree li span.button.root_docu{ background:none;}
+.ztree li span.button.roots_docu{background-position:-56px 0}
+.ztree li span.button.center_docu{background-position:-56px -18px}
+.ztree li span.button.bottom_docu{background-position:-56px -36px}
+.ztree li span.button.noline_docu{ background:none;}
+
+.ztree li span.button.ico_open{margin-right:2px; background-position:-110px -16px; vertical-align:top; *vertical-align:middle}
+.ztree li span.button.ico_close{margin-right:2px; background-position:-110px 0; vertical-align:top; *vertical-align:middle}
+.ztree li span.button.ico_docu{margin-right:2px; background-position:-110px -32px; vertical-align:top; *vertical-align:middle}
+.ztree li span.button.edit {margin-right:2px; background-position:-110px -48px; vertical-align:top; *vertical-align:middle}
+.ztree li span.button.remove {margin-right:2px; background-position:-110px -64px; vertical-align:top; *vertical-align:middle}
+
+.ztree li span.button.ico_loading{margin-right:2px; background:url(./img/loading.gif) no-repeat scroll 0 0 transparent; vertical-align:top; *vertical-align:middle}
+
+ul.tmpTargetzTree {background-color:#FFE6B0; opacity:0.8; filter:alpha(opacity=80)}
+
+span.tmpzTreeMove_arrow {width:16px; height:16px; display: inline-block; padding:0; margin:2px 0 0 1px; border:0 none; position:absolute;
+	background-color:transparent; background-repeat:no-repeat; background-attachment: scroll;
+	background-position:-110px -80px; background-image:url("./img/zTreeStandard.png"); *background-image:url("./img/zTreeStandard.gif")}
+
+ul.ztree.zTreeDragUL {margin:0; padding:0; position:absolute; width:auto; height:auto;overflow:hidden; background-color:#cfcfcf; border:1px #00B83F dotted; opacity:0.8; filter:alpha(opacity=80)}
+.zTreeMask {z-index:10000; background-color:#cfcfcf; opacity:0.0; filter:alpha(opacity=0); position:absolute}
+
+/* level style*/
+/*.ztree li span.button.level0 {
+	display:none;
+}
+.ztree li ul.level0 {
+	padding:0;
+	background:none;
+}*/

BIN
app/public/fonts/FontAwesome.otf


BIN
app/public/fonts/fontawesome-webfont.eot


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 2671 - 0
app/public/fonts/fontawesome-webfont.svg


BIN
app/public/fonts/fontawesome-webfont.ttf


BIN
app/public/fonts/fontawesome-webfont.woff


BIN
app/public/fonts/fontawesome-webfont.woff2


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 1 - 0
app/public/js/bootstrap/bootstrap-datetimepicker.min.js


+ 657 - 0
app/public/js/bootstrap/bootstrap-paginator.js

@@ -0,0 +1,657 @@
+/**
+ * bootstrap-paginator.js v0.5
+ * --
+ * Copyright 2013 Yun Lai <lyonlai1984@gmail.com>
+ * --
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+(function ($) {
+
+    "use strict"; // jshint ;_;
+
+
+    /* Paginator PUBLIC CLASS DEFINITION
+     * ================================= */
+
+    /**
+     * Boostrap Paginator Constructor
+     *
+     * @param element element of the paginator
+     * @param options the options to config the paginator
+     *
+     * */
+    var BootstrapPaginator = function (element, options) {
+        this.init(element, options);
+    },
+        old = null;
+
+    BootstrapPaginator.prototype = {
+
+        /**
+         * Initialization function of the paginator, accepting an element and the options as parameters
+         *
+         * @param element element of the paginator
+         * @param options the options to config the paginator
+         *
+         * */
+        init: function (element, options) {
+
+            this.$element = $(element);
+
+            var version = (options && options.bootstrapMajorVersion) ? options.bootstrapMajorVersion : $.fn.bootstrapPaginator.defaults.bootstrapMajorVersion,
+                id = this.$element.attr("id");
+
+            if (version === 2 && !this.$element.is("div")) {
+
+                throw "in Bootstrap version 2 the pagination must be a div element. Or if you are using Bootstrap pagination 3. Please specify it in bootstrapMajorVersion in the option";
+            } else if (version > 2 && !this.$element.is("ul")) {
+                throw "in Bootstrap version 3 the pagination root item must be an ul element."
+            }
+
+
+
+            this.currentPage = 1;
+
+            this.lastPage = 1;
+
+            this.setOptions(options);
+
+            this.initialized = true;
+        },
+
+        /**
+         * Update the properties of the paginator element
+         *
+         * @param options options to config the paginator
+         * */
+        setOptions: function (options) {
+
+            this.options = $.extend({}, (this.options || $.fn.bootstrapPaginator.defaults), options);
+
+            this.totalPages = parseInt(this.options.totalPages, 10);  //setup the total pages property.
+            this.numberOfPages = parseInt(this.options.numberOfPages, 10); //setup the numberOfPages to be shown
+
+            //move the set current page after the setting of total pages. otherwise it will cause out of page exception.
+            if (options && typeof (options.currentPage)  !== 'undefined') {
+
+                this.setCurrentPage(options.currentPage);
+            }
+
+            this.listen();
+
+            //render the paginator
+            this.render();
+
+            if (!this.initialized && this.lastPage !== this.currentPage) {
+                this.$element.trigger("page-changed", [this.lastPage, this.currentPage]);
+            }
+
+        },
+
+        /**
+         * Sets up the events listeners. Currently the pageclicked and pagechanged events are linked if available.
+         *
+         * */
+        listen: function () {
+
+            this.$element.off("page-clicked");
+
+            this.$element.off("page-changed");// unload the events for the element
+
+            if (typeof (this.options.onPageClicked) === "function") {
+                this.$element.bind("page-clicked", this.options.onPageClicked);
+            }
+
+            if (typeof (this.options.onPageChanged) === "function") {
+                this.$element.on("page-changed", this.options.onPageChanged);
+            }
+
+            this.$element.bind("page-clicked", this.onPageClicked);
+        },
+
+
+        /**
+         *
+         *  Destroys the paginator element, it unload the event first, then empty the content inside.
+         *
+         * */
+        destroy: function () {
+
+            this.$element.off("page-clicked");
+
+            this.$element.off("page-changed");
+
+            this.$element.removeData('bootstrapPaginator');
+
+            this.$element.empty();
+
+        },
+
+        /**
+         * Shows the page
+         *
+         * */
+        show: function (page) {
+
+            this.setCurrentPage(page);
+
+            this.render();
+
+            if (this.lastPage !== this.currentPage) {
+                this.$element.trigger("page-changed", [this.lastPage, this.currentPage]);
+            }
+        },
+
+        /**
+         * Shows the next page
+         *
+         * */
+        showNext: function () {
+            var pages = this.getPages();
+
+            if (pages.next) {
+                this.show(pages.next);
+            }
+
+        },
+
+        /**
+         * Shows the previous page
+         *
+         * */
+        showPrevious: function () {
+            var pages = this.getPages();
+
+            if (pages.prev) {
+                this.show(pages.prev);
+            }
+
+        },
+
+        /**
+         * Shows the first page
+         *
+         * */
+        showFirst: function () {
+            var pages = this.getPages();
+
+            if (pages.first) {
+                this.show(pages.first);
+            }
+
+        },
+
+        /**
+         * Shows the last page
+         *
+         * */
+        showLast: function () {
+            var pages = this.getPages();
+
+            if (pages.last) {
+                this.show(pages.last);
+            }
+
+        },
+
+        /**
+         * Internal on page item click handler, when the page item is clicked, change the current page to the corresponding page and
+         * trigger the pageclick event for the listeners.
+         *
+         *
+         * */
+        onPageItemClicked: function (event) {
+
+            var type = event.data.type,
+                page = event.data.page;
+
+            this.$element.trigger("page-clicked", [event, type, page]);
+
+        },
+
+        onPageClicked: function (event, originalEvent, type, page) {
+
+            //show the corresponding page and retrieve the newly built item related to the page clicked before for the event return
+
+            var currentTarget = $(event.currentTarget);
+
+            switch (type) {
+            case "first":
+                currentTarget.bootstrapPaginator("showFirst");
+                break;
+            case "prev":
+                currentTarget.bootstrapPaginator("showPrevious");
+                break;
+            case "next":
+                currentTarget.bootstrapPaginator("showNext");
+                break;
+            case "last":
+                currentTarget.bootstrapPaginator("showLast");
+                break;
+            case "page":
+                currentTarget.bootstrapPaginator("show", page);
+                break;
+            }
+
+        },
+
+        /**
+         * Renders the paginator according to the internal properties and the settings.
+         *
+         *
+         * */
+        render: function () {
+
+            //fetch the container class and add them to the container
+            var containerClass = this.getValueFromOption(this.options.containerClass, this.$element),
+                size = this.options.size || "normal",
+                alignment = this.options.alignment || "left",
+                pages = this.getPages(),
+                listContainer = this.options.bootstrapMajorVersion === 2 ? $("<ul></ul>") : this.$element,
+                listContainerClass = this.options.bootstrapMajorVersion === 2 ? this.getValueFromOption(this.options.listContainerClass, listContainer) : null,
+                first = null,
+                prev = null,
+                next = null,
+                last = null,
+                p = null,
+                i = 0;
+
+
+            this.$element.prop("class", "");
+
+            this.$element.addClass("pagination");
+
+            switch (size.toLowerCase()) {
+            case "large":
+            case "small":
+            case "mini":
+                this.$element.addClass($.fn.bootstrapPaginator.sizeArray[this.options.bootstrapMajorVersion][size.toLowerCase()]);
+                break;
+            default:
+                break;
+            }
+
+            if (this.options.bootstrapMajorVersion === 2) {
+                switch (alignment.toLowerCase()) {
+                case "center":
+                    this.$element.addClass("pagination-centered");
+                    break;
+                case "right":
+                    this.$element.addClass("pagination-right");
+                    break;
+                default:
+                    break;
+                }
+            }
+
+
+            this.$element.addClass(containerClass);
+
+            //empty the outter most container then add the listContainer inside.
+            this.$element.empty();
+
+            if (this.options.bootstrapMajorVersion === 2) {
+                this.$element.append(listContainer);
+
+                listContainer.addClass(listContainerClass);
+            }
+
+            //update the page element reference
+            this.pageRef = [];
+
+            if (pages.first) {//if the there is first page element
+                first = this.buildPageItem("first", pages.first);
+
+                if (first) {
+                    listContainer.append(first);
+                }
+
+            }
+
+            if (pages.prev) {//if the there is previous page element
+
+                prev = this.buildPageItem("prev", pages.prev);
+
+                if (prev) {
+                    listContainer.append(prev);
+                }
+
+            }
+
+
+            for (i = 0; i < pages.length; i = i + 1) {//fill the numeric pages.
+
+                p = this.buildPageItem("page", pages[i]);
+
+                if (p) {
+                    listContainer.append(p);
+                }
+            }
+
+            if (pages.next) {//if there is next page
+
+                next = this.buildPageItem("next", pages.next);
+
+                if (next) {
+                    listContainer.append(next);
+                }
+            }
+
+            if (pages.last) {//if there is last page
+
+                last = this.buildPageItem("last", pages.last);
+
+                if (last) {
+                    listContainer.append(last);
+                }
+            }
+        },
+
+        /**
+         *
+         * Creates a page item base on the type and page number given.
+         *
+         * @param page page number
+         * @param type type of the page, whether it is the first, prev, page, next, last
+         *
+         * @return Object the constructed page element
+         * */
+        buildPageItem: function (type, page) {
+
+            var itemContainer = $("<li></li>"),//creates the item container
+                itemContent = $("<a></a>"),//creates the item content
+                text = "",
+                title = "",
+                itemContainerClass = this.options.itemContainerClass(type, page, this.currentPage),
+                itemContentClass = this.getValueFromOption(this.options.itemContentClass, type, page, this.currentPage),
+                tooltipOpts = null;
+
+
+            switch (type) {
+
+            case "first":
+                if (!this.getValueFromOption(this.options.shouldShowPage, type, page, this.currentPage)) { return; }
+                text = this.options.itemTexts(type, page, this.currentPage);
+                title = this.options.tooltipTitles(type, page, this.currentPage);
+                break;
+            case "last":
+                if (!this.getValueFromOption(this.options.shouldShowPage, type, page, this.currentPage)) { return; }
+                text = this.options.itemTexts(type, page, this.currentPage);
+                title = this.options.tooltipTitles(type, page, this.currentPage);
+                break;
+            case "prev":
+                if (!this.getValueFromOption(this.options.shouldShowPage, type, page, this.currentPage)) { return; }
+                text = this.options.itemTexts(type, page, this.currentPage);
+                title = this.options.tooltipTitles(type, page, this.currentPage);
+                break;
+            case "next":
+                if (!this.getValueFromOption(this.options.shouldShowPage, type, page, this.currentPage)) { return; }
+                text = this.options.itemTexts(type, page, this.currentPage);
+                title = this.options.tooltipTitles(type, page, this.currentPage);
+                break;
+            case "page":
+                if (!this.getValueFromOption(this.options.shouldShowPage, type, page, this.currentPage)) { return; }
+                text = this.options.itemTexts(type, page, this.currentPage);
+                title = this.options.tooltipTitles(type, page, this.currentPage);
+                break;
+            }
+
+            itemContainer.addClass(itemContainerClass).append(itemContent);
+
+            itemContent.addClass(itemContentClass).html(text).on("click", null, {type: type, page: page}, $.proxy(this.onPageItemClicked, this));
+
+            if (this.options.pageUrl) {
+                itemContent.attr("href", this.getValueFromOption(this.options.pageUrl, type, page, this.currentPage));
+            }
+
+            if (this.options.useBootstrapTooltip) {
+                tooltipOpts = $.extend({}, this.options.bootstrapTooltipOptions, {title: title});
+
+                itemContent.tooltip(tooltipOpts);
+            } else {
+                itemContent.attr("title", title);
+            }
+
+            return itemContainer;
+
+        },
+
+        setCurrentPage: function (page) {
+            if (page > this.totalPages || page < 1) {// if the current page is out of range, throw exception.
+
+                throw "Page out of range";
+
+            }
+
+            this.lastPage = this.currentPage;
+
+            this.currentPage = parseInt(page, 10);
+
+        },
+
+        /**
+         * Gets an array that represents the current status of the page object. Numeric pages can be access via array mode. length attributes describes how many numeric pages are there. First, previous, next and last page can be accessed via attributes first, prev, next and last. Current attribute marks the current page within the pages.
+         *
+         * @return object output objects that has first, prev, next, last and also the number of pages in between.
+         * */
+        getPages: function () {
+
+            var totalPages = this.totalPages,// get or calculate the total pages via the total records
+                pageStart = (this.currentPage % this.numberOfPages === 0) ? (parseInt(this.currentPage / this.numberOfPages, 10) - 1) * this.numberOfPages + 1 : parseInt(this.currentPage / this.numberOfPages, 10) * this.numberOfPages + 1,//calculates the start page.
+                output = [],
+                i = 0,
+                counter = 0;
+
+            pageStart = pageStart < 1 ? 1 : pageStart;//check the range of the page start to see if its less than 1.
+
+            for (i = pageStart, counter = 0; counter < this.numberOfPages && i <= totalPages; i = i + 1, counter = counter + 1) {//fill the pages
+                output.push(i);
+            }
+
+            output.first = 1;//add the first when the current page leaves the 1st page.
+
+            if (this.currentPage > 1) {// add the previous when the current page leaves the 1st page
+                output.prev = this.currentPage - 1;
+            } else {
+                output.prev = 1;
+            }
+
+            if (this.currentPage < totalPages) {// add the next page when the current page doesn't reach the last page
+                output.next = this.currentPage + 1;
+            } else {
+                output.next = totalPages;
+            }
+
+            output.last = totalPages;// add the last page when the current page doesn't reach the last page
+
+            output.current = this.currentPage;//mark the current page.
+
+            output.total = totalPages;
+
+            output.numberOfPages = this.options.numberOfPages;
+
+            return output;
+
+        },
+
+        /**
+         * Gets the value from the options, this is made to handle the situation where value is the return value of a function.
+         *
+         * @return mixed value that depends on the type of parameters, if the given parameter is a function, then the evaluated result is returned. Otherwise the parameter itself will get returned.
+         * */
+        getValueFromOption: function (value) {
+
+            var output = null,
+                args = Array.prototype.slice.call(arguments, 1);
+
+            if (typeof value === 'function') {
+                output = value.apply(this, args);
+            } else {
+                output = value;
+            }
+
+            return output;
+
+        }
+
+    };
+
+
+    /* TYPEAHEAD PLUGIN DEFINITION
+     * =========================== */
+
+    old = $.fn.bootstrapPaginator;
+
+    $.fn.bootstrapPaginator = function (option) {
+
+        var args = arguments,
+            result = null;
+
+        $(this).each(function (index, item) {
+            var $this = $(item),
+                data = $this.data('bootstrapPaginator'),
+                options = (typeof option !== 'object') ? null : option;
+
+            if (!data) {
+                data = new BootstrapPaginator(this, options);
+
+                $this = $(data.$element);
+
+                $this.data('bootstrapPaginator', data);
+
+                return;
+            }
+
+            if (typeof option === 'string') {
+
+                if (data[option]) {
+                    result = data[option].apply(data, Array.prototype.slice.call(args, 1));
+                } else {
+                    throw "Method " + option + " does not exist";
+                }
+
+            } else {
+                result = data.setOptions(option);
+            }
+        });
+
+        return result;
+
+    };
+
+    $.fn.bootstrapPaginator.sizeArray = {
+
+        "2": {
+            "large": "pagination-large",
+            "small": "pagination-small",
+            "mini": "pagination-mini"
+        },
+        "3": {
+            "large": "pagination-lg",
+            "small": "pagination-sm",
+            "mini": ""
+        }
+
+    };
+
+    $.fn.bootstrapPaginator.defaults = {
+        containerClass: "",
+        size: "normal",
+        alignment: "left",
+        bootstrapMajorVersion: 2,
+        listContainerClass: "",
+        itemContainerClass: function (type, page, current) {
+            return (page === current) ? "active" : "";
+        },
+        itemContentClass: function (type, page, current) {
+            return "";
+        },
+        currentPage: 1,
+        numberOfPages: 5,
+        totalPages: 1,
+        pageUrl: function (type, page, current) {
+            return null;
+        },
+        onPageClicked: null,
+        onPageChanged: null,
+        useBootstrapTooltip: false,
+        shouldShowPage: function (type, page, current) {
+
+            var result = true;
+
+            switch (type) {
+            case "first":
+                result = (current !== 1);
+                break;
+            case "prev":
+                result = (current !== 1);
+                break;
+            case "next":
+                result = (current !== this.totalPages);
+                break;
+            case "last":
+                result = (current !== this.totalPages);
+                break;
+            case "page":
+                result = true;
+                break;
+            }
+
+            return result;
+
+        },
+        itemTexts: function (type, page, current) {
+            switch (type) {
+            case "first":
+                return "&lt;&lt;";
+            case "prev":
+                return "&lt;";
+            case "next":
+                return "&gt;";
+            case "last":
+                return "&gt;&gt;";
+            case "page":
+                return page;
+            }
+        },
+        tooltipTitles: function (type, page, current) {
+
+            switch (type) {
+            case "first":
+                return "Go to first page";
+            case "prev":
+                return "Go to previous page";
+            case "next":
+                return "Go to next page";
+            case "last":
+                return "Go to last page";
+            case "page":
+                return (page === current) ? "Current page is " + page : "Go to page " + page;
+            }
+        },
+        bootstrapTooltipOptions: {
+            animation: true,
+            html: true,
+            placement: 'top',
+            selector: false,
+            title: "",
+            container: false
+        }
+    };
+
+    $.fn.bootstrapPaginator.Constructor = BootstrapPaginator;
+
+
+
+}(window.jQuery));

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 6 - 0
app/public/js/bootstrap/bootstrap.min.js


+ 16 - 0
app/public/js/bootstrap/locales/bootstrap-datetimepicker.zh-CN.js

@@ -0,0 +1,16 @@
+/**
+ * Simplified Chinese translation for bootstrap-datetimepicker
+ * Yuan Cheung <advanimal@gmail.com>
+ */
+;(function($){
+	$.fn.datetimepicker.dates['zh-CN'] = {
+			days: ["星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"],
+			daysShort: ["周日", "周一", "周二", "周三", "周四", "周五", "周六", "周日"],
+			daysMin:  ["日", "一", "二", "三", "四", "五", "六", "日"],
+			months: ["一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"],
+			monthsShort: ["一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"],
+			today: "今天",
+			suffix: [],
+			meridiem: ["上午", "下午"]
+	};
+}(jQuery));

+ 45 - 0
app/public/js/captcha.js

@@ -0,0 +1,45 @@
+/**
+ * 验证码相关js
+ *
+ * @author CaiAoLin
+ * @date 2017/10/13
+ * @version
+ */
+$(document).ready(function() {
+
+    // 载入时先获取相关参数
+    $.ajax({
+        url: '/captcha',
+        type: 'get',
+        data: '',
+        timeout: 5000,
+        error: function() {
+            $("#captcha-box").html('验证码加载失败');
+        },
+        beforeSend: function() {
+            $("#captcha-box").html('正在加载验证码');
+        },
+        success: function(response) {
+            $("#captcha-box").html('');
+            if (response.success === 0) {
+                alert('验证码初始化失败!');
+                return false;
+            }
+
+            initGeetest({
+                // 以下配置参数来自服务端 SDK
+                gt: response.gt,
+                challenge: response.challenge,
+                offline: !response.success,
+                new_captcha: response.new_captcha,
+                width: '100%'
+            }, function (captchaObj) {
+                captchaObj.appendTo('#captcha-box');
+                captchaObj.onSuccess(function () {
+                    $(".btn-area").slideDown("fast");
+                });
+            })
+        }
+    });
+
+});

+ 28 - 0
app/public/js/form_validate.js

@@ -0,0 +1,28 @@
+/**
+ * 表单统一验证
+ *
+ * @author CaiAoLin
+ * @date 2017/9/29
+ * @version
+ */
+$(document).ready(function() {
+    $("form").validate({
+        rules: rule === undefined ? {} : rule,
+        messages: message === undefined ? {} : message,
+        errorPlacement: function(error, element) {
+            $(element).addClass('is-invalid');
+            $(element).parent().next("div.col-lg-4").append(error);
+        },
+        errorClass: "invalid-feedback",
+        errorElement: "div",
+        highlight: false,
+        success: function(element) {
+            $(element).parent().parent().find("input").removeClass('is-invalid');
+        }
+    });
+
+
+    $("button").click(function() {
+        $("form").valid();
+    });
+});

+ 47 - 0
app/public/js/global.js

@@ -0,0 +1,47 @@
+/*全局自适应高度*/
+function autoFlashHeight(){
+    var headerHeight = $(".header").height();
+    var toolsbarHeight = $(".toolsbar").height();
+    var ftoolsbarHeight = $(".toolsbar-f").height();
+    var bottomContentHeight = $(".bottom-content").height();
+    var toolsBarHeightQ = $(".tools-bar-height-q").height();
+    var toolsBarHeightD = $(".tools-bar-height-d").height();
+    $(".main-data-side-q").height($(window).height()-headerHeight-toolsbarHeight-toolsBarHeightQ-202);
+    $(".main-data-side-d").height($(window).height()-headerHeight-toolsbarHeight-toolsBarHeightD-202);
+    $(".main-data-top").height($(window).height()-headerHeight-toolsbarHeight-bottomContentHeight-1);
+    $(".main-data-full").height($(window).height()-headerHeight-toolsbarHeight-1);
+    $(".main-data-full-fl").height($(window).height()-headerHeight-toolsbarHeight-37);
+    $(".main-data-not").height($(window).height()-headerHeight-1);
+    $(".main-data-side-search").height($(window).height()-headerHeight-toolsbarHeight-64);
+    $(".side-content").height($(window).height()-headerHeight );
+    $(".poj-list").height($(window).height()-headerHeight-toolsbarHeight);
+    $(".form-view").height($(window).height()-headerHeight-ftoolsbarHeight);
+    $(".form-list").height($(window).height()-headerHeight-50 );
+};
+$(window).resize(autoFlashHeight);
+/*全局自适应高度结束*/
+$(function(){
+/*侧滑*/
+$(".open-sidebar").click(function(){
+    $(".slide-sidebar").animate({width:"800"}).addClass("open");
+});
+$("body").click(function(event){
+        var e = event || window.event; //浏览器兼容性
+        if(!$(event.target).is('a')) {
+            var elem = event.target || e.srcElement;
+            while (elem) { //循环判断至跟节点,防止点击的是div子元素
+                if (elem.className == "open-sidebar" || elem.className == 'slide-sidebar open') {
+                    return false;
+                }
+                elem = elem.parentNode;
+            }
+            $(".slide-sidebar").animate({width:"0"}).removeClass("open")// 关闭处理
+        }
+
+    });
+/*侧滑*/
+/*工具提示*/
+$(function () {
+  $('[data-toggle="tooltip"]').tooltip()
+});
+});

+ 293 - 0
app/public/js/gt.js

@@ -0,0 +1,293 @@
+"v0.4.6 Geetest Inc.";
+
+(function (window) {
+    "use strict";
+    if (typeof window === 'undefined') {
+        throw new Error('Geetest requires browser environment');
+    }
+
+var document = window.document;
+var Math = window.Math;
+var head = document.getElementsByTagName("head")[0];
+
+function _Object(obj) {
+    this._obj = obj;
+}
+
+_Object.prototype = {
+    _each: function (process) {
+        var _obj = this._obj;
+        for (var k in _obj) {
+            if (_obj.hasOwnProperty(k)) {
+                process(k, _obj[k]);
+            }
+        }
+        return this;
+    }
+};
+
+function Config(config) {
+    var self = this;
+    new _Object(config)._each(function (key, value) {
+        self[key] = value;
+    });
+}
+
+Config.prototype = {
+    api_server: 'api.geetest.com',
+    protocol: 'http://',
+    typePath: '/gettype.php',
+    fallback_config: {
+        slide: {
+            static_servers: ["static.geetest.com", "dn-staticdown.qbox.me"],
+            type: 'slide',
+            slide: '/static/js/geetest.0.0.0.js'
+        },
+        fullpage: {
+            static_servers: ["static.geetest.com", "dn-staticdown.qbox.me"],
+            type: 'fullpage',
+            fullpage: '/static/js/fullpage.0.0.0.js'
+        }
+    },
+    _get_fallback_config: function () {
+        var self = this;
+        if (isString(self.type)) {
+            return self.fallback_config[self.type];
+        } else if (self.new_captcha) {
+            return self.fallback_config.fullpage;
+        } else {
+            return self.fallback_config.slide;
+        }
+    },
+    _extend: function (obj) {
+        var self = this;
+        new _Object(obj)._each(function (key, value) {
+            self[key] = value;
+        })
+    }
+};
+var isNumber = function (value) {
+    return (typeof value === 'number');
+};
+var isString = function (value) {
+    return (typeof value === 'string');
+};
+var isBoolean = function (value) {
+    return (typeof value === 'boolean');
+};
+var isObject = function (value) {
+    return (typeof value === 'object' && value !== null);
+};
+var isFunction = function (value) {
+    return (typeof value === 'function');
+};
+
+var callbacks = {};
+var status = {};
+
+var random = function () {
+    return parseInt(Math.random() * 10000) + (new Date()).valueOf();
+};
+
+var loadScript = function (url, cb) {
+    var script = document.createElement("script");
+    script.charset = "UTF-8";
+    script.async = true;
+
+    script.onerror = function () {
+        cb(true);
+    };
+    var loaded = false;
+    script.onload = script.onreadystatechange = function () {
+        if (!loaded &&
+            (!script.readyState ||
+            "loaded" === script.readyState ||
+            "complete" === script.readyState)) {
+
+            loaded = true;
+            setTimeout(function () {
+                cb(false);
+            }, 0);
+        }
+    };
+    script.src = url;
+    head.appendChild(script);
+};
+
+var normalizeDomain = function (domain) {
+    // special domain: uems.sysu.edu.cn/jwxt/geetest/
+    // return domain.replace(/^https?:\/\/|\/.*$/g, ''); uems.sysu.edu.cn
+    return domain.replace(/^https?:\/\/|\/$/g, ''); // uems.sysu.edu.cn/jwxt/geetest
+};
+var normalizePath = function (path) {
+    path = path.replace(/\/+/g, '/');
+    if (path.indexOf('/') !== 0) {
+        path = '/' + path;
+    }
+    return path;
+};
+var normalizeQuery = function (query) {
+    if (!query) {
+        return '';
+    }
+    var q = '?';
+    new _Object(query)._each(function (key, value) {
+        if (isString(value) || isNumber(value) || isBoolean(value)) {
+            q = q + encodeURIComponent(key) + '=' + encodeURIComponent(value) + '&';
+        }
+    });
+    if (q === '?') {
+        q = '';
+    }
+    return q.replace(/&$/, '');
+};
+var makeURL = function (protocol, domain, path, query) {
+    domain = normalizeDomain(domain);
+
+    var url = normalizePath(path) + normalizeQuery(query);
+    if (domain) {
+        url = protocol + domain + url;
+    }
+
+    return url;
+};
+
+var load = function (protocol, domains, path, query, cb) {
+    var tryRequest = function (at) {
+
+        var url = makeURL(protocol, domains[at], path, query);
+        loadScript(url, function (err) {
+            if (err) {
+                if (at >= domains.length - 1) {
+                    cb(true);
+                } else {
+                    tryRequest(at + 1);
+                }
+            } else {
+                cb(false);
+            }
+        });
+    };
+    tryRequest(0);
+};
+
+
+var jsonp = function (domains, path, config, callback) {
+    if (isObject(config.getLib)) {
+        config._extend(config.getLib);
+        callback(config);
+        return;
+    }
+    if (config.offline) {
+        callback(config._get_fallback_config());
+        return;
+    }
+
+    var cb = "geetest_" + random();
+    window[cb] = function (data) {
+        if (data.status == 'success') {
+            callback(data.data);
+        } else if (!data.status) {
+            callback(data);
+        } else {
+            callback(config._get_fallback_config());
+        }
+        window[cb] = undefined;
+        try {
+            delete window[cb];
+        } catch (e) {
+        }
+    };
+    load(config.protocol, domains, path, {
+        gt: config.gt,
+        callback: cb
+    }, function (err) {
+        if (err) {
+            callback(config._get_fallback_config());
+        }
+    });
+};
+
+var throwError = function (errorType, config) {
+    var errors = {
+        networkError: '网络错误',
+        gtTypeError: 'gt字段不是字符串类型'
+    };
+    if (typeof config.onError === 'function') {
+        config.onError(errors[errorType]);
+    } else {
+        throw new Error(errors[errorType]);
+    }
+};
+
+var detect = function () {
+    return window.Geetest || document.getElementById("gt_lib");
+};
+
+if (detect()) {
+    status.slide = "loaded";
+}
+
+window.initGeetest = function (userConfig, callback) {
+
+    var config = new Config(userConfig);
+
+    if (userConfig.https) {
+        config.protocol = 'https://';
+    } else if (!userConfig.protocol) {
+        config.protocol = window.location.protocol + '//';
+    }
+
+    // for KFC
+    if (userConfig.gt === '050cffef4ae57b5d5e529fea9540b0d1' ||
+        userConfig.gt === '3bd38408ae4af923ed36e13819b14d42') {
+        config.apiserver = 'yumchina.geetest.com/'; // for old js
+        config.api_server = 'yumchina.geetest.com';
+    }
+
+    if (isObject(userConfig.getType)) {
+        config._extend(userConfig.getType);
+    }
+    jsonp([config.api_server || config.apiserver], config.typePath, config, function (newConfig) {
+        var type = newConfig.type;
+        var init = function () {
+            config._extend(newConfig);
+            callback(new window.Geetest(config));
+        };
+
+        callbacks[type] = callbacks[type] || [];
+        var s = status[type] || 'init';
+        if (s === 'init') {
+            status[type] = 'loading';
+
+            callbacks[type].push(init);
+
+            load(config.protocol, newConfig.static_servers || newConfig.domains, newConfig[type] || newConfig.path, null, function (err) {
+                if (err) {
+                    status[type] = 'fail';
+                    throwError('networkError', config);
+                } else {
+                    status[type] = 'loaded';
+                    var cbs = callbacks[type];
+                    for (var i = 0, len = cbs.length; i < len; i = i + 1) {
+                        var cb = cbs[i];
+                        if (isFunction(cb)) {
+                            cb();
+                        }
+                    }
+                    callbacks[type] = [];
+                }
+            });
+        } else if (s === "loaded") {
+            init();
+        } else if (s === "fail") {
+            throwError('networkError', config);
+        } else if (s === "loading") {
+            callbacks[type].push(init);
+        }
+    });
+
+};
+
+
+})(window);

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 4 - 0
app/public/js/jquery/jquery-3.2.1.min.js


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 1578 - 0
app/public/js/jquery/jquery.validate.js


+ 24 - 0
app/public/js/message.js

@@ -0,0 +1,24 @@
+/**
+ * 消息管理相关js
+ *
+ * @author CaiAoLin
+ * @date 2017/10/25
+ * @version
+ */
+$(document).ready(function() {
+
+    // 切换发布设置
+    $("input[name='release_type']").change(function() {
+        let releaseType = $(this).val();
+        releaseType = parseInt(releaseType);
+        if (releaseType === 1) {
+            $("#release-time-area").hide("fast");
+            $("#release-time-area").children().find("input").attr("disabled", "disabled");
+        } else {
+            $("#release-time-area").show("fast");
+            $("#release-time-area").children().find("input").removeAttr("disabled");
+        }
+    });
+
+
+});

+ 33 - 0
app/public/js/messages_zh.js

@@ -0,0 +1,33 @@
+(function( factory ) {
+	if ( typeof define === "function" && define.amd ) {
+		define( ["jquery", "../jquery.validate"], factory );
+	} else {
+		factory( jQuery );
+	}
+}(function( $ ) {
+
+/*
+ * Translated default messages for the jQuery validation plugin.
+ * Locale: ZH (Chinese, 中文 (Zhōngwén), 汉语, 漢語)
+ */
+$.extend($.validator.messages, {
+	required: "这是必填字段",
+	remote: "请修正此字段",
+	email: "请输入有效的电子邮件地址",
+	url: "请输入有效的网址",
+	date: "请输入有效的日期",
+	dateISO: "请输入有效的日期 (YYYY-MM-DD)",
+	number: "请输入有效的数字",
+	digits: "只能输入数字",
+	creditcard: "请输入有效的信用卡号码",
+	equalTo: "你的输入不相同",
+	extension: "请输入有效的后缀",
+	maxlength: $.validator.format("最多可以输入 {0} 个字符"),
+	minlength: $.validator.format("最少要输入 {0} 个字符"),
+	rangelength: $.validator.format("请输入长度在 {0} 到 {1} 之间的字符串"),
+	range: $.validator.format("请输入范围在 {0} 到 {1} 之间的数值"),
+	max: $.validator.format("请输入不大于 {0} 的数值"),
+	min: $.validator.format("请输入不小于 {0} 的数值")
+});
+
+}));

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 5 - 0
app/public/js/popper/popper.min.js


+ 56 - 0
app/public/js/switch_btn.js

@@ -0,0 +1,56 @@
+/**
+ * 选择开关
+ *
+ * @author CaiAoLin
+ * @date 2017/10/23
+ * @version
+ */
+(function($){
+    $.fn.switchBtn = function(options){
+        var defaults = {
+            enableText: '开启',
+            disableText: '禁止',
+        };
+
+        var opts = $.extend(defaults, options);
+        var switchBtn = $(this);
+        var status = switchBtn.data('status');
+        status = parseInt(status);
+        status = isNaN(status) ? 0 : status;
+        var name = switchBtn.data('name');
+        name = name === '' || name === undefined ? 'switch_btn' : name;
+        var html = '';
+        var inputHtml = '<input type="hidden" name="'+ name +'" value="'+ status +'" />';
+
+        if (status > 0) {
+            html += '<button class="btn btn-success btn-sm disabled="disabled">已'+ opts.enableText +'</button>' + inputHtml +
+                '<button class="btn btn-outline-danger btn-sm">'+ opts.disableText +'</button>';
+        } else {
+            html += '<button class="btn btn-outline-success btn-sm">'+ opts.enableText +'</button>' + inputHtml +
+                '<button class="btn btn-danger btn-sm disabled" disabled="disabled">已'+ opts.disableText +'</button>';
+        }
+        switchBtn.append(html);
+
+        // 点击事件绑定
+        switchBtn.on('click', 'button', function() {
+            var currentStatus = $("input[name='"+ name +"']").val();
+            if (currentStatus > 0) {
+                // 切换为false的状态
+                $(this).siblings("button").removeClass("btn-success").removeClass("disabled")
+                    .addClass("btn-outline-success").removeAttr("disabled").text(opts.enableText);
+                $(this).removeClass("btn-outline-danger").addClass("btn-danger").attr("disabled", "disabled")
+                    .text("已" + opts.disableText);
+                $("input[name='"+ name +"']").val(0);
+                switchBtn.attr("data-status", 0);
+            } else {
+                // 切换为true的状态
+                $(this).removeClass("btn-outline-success").addClass("btn-success").addClass("disabled")
+                    .attr("disabled", "disabled").text("已" + opts.enableText);
+                $(this).siblings("button").removeClass("btn-danger").removeClass("disabled")
+                    .addClass("btn-outline-danger").removeAttr("disabled").text(opts.disableText);
+                $("input[name='"+ name +"']").val(1);
+                switchBtn.attr("data-status", 1);
+            }
+        });
+    };
+})(jQuery);

+ 681 - 0
app/public/js/ueditor/dialogs/attachment/attachment.css

@@ -0,0 +1,681 @@
+@charset "utf-8";
+/* dialog样式 */
+.wrapper {
+    zoom: 1;
+    width: 630px;
+    *width: 626px;
+    height: 380px;
+    margin: 0 auto;
+    padding: 10px;
+    position: relative;
+    font-family: sans-serif;
+}
+
+/*tab样式框大小*/
+.tabhead {
+    float:left;
+}
+.tabbody {
+    width: 100%;
+    height: 346px;
+    position: relative;
+    clear: both;
+}
+
+.tabbody .panel {
+    position: absolute;
+    width: 0;
+    height: 0;
+    background: #fff;
+    overflow: hidden;
+    display: none;
+}
+
+.tabbody .panel.focus {
+    width: 100%;
+    height: 346px;
+    display: block;
+}
+
+/* 上传附件 */
+.tabbody #upload.panel {
+    width: 0;
+    height: 0;
+    overflow: hidden;
+    position: absolute !important;
+    clip: rect(1px, 1px, 1px, 1px);
+    background: #fff;
+    display: block;
+}
+
+.tabbody #upload.panel.focus {
+    width: 100%;
+    height: 346px;
+    display: block;
+    clip: auto;
+}
+
+#upload .queueList {
+    margin: 0;
+    width: 100%;
+    height: 100%;
+    position: absolute;
+    overflow: hidden;
+}
+
+#upload p {
+    margin: 0;
+}
+
+.element-invisible {
+    width: 0 !important;
+    height: 0 !important;
+    border: 0;
+    padding: 0;
+    margin: 0;
+    overflow: hidden;
+    position: absolute !important;
+    clip: rect(1px, 1px, 1px, 1px);
+}
+
+#upload .placeholder {
+    margin: 10px;
+    border: 2px dashed #e6e6e6;
+    *border: 0px dashed #e6e6e6;
+    height: 172px;
+    padding-top: 150px;
+    text-align: center;
+    background: url(./images/image.png) center 70px no-repeat;
+    color: #cccccc;
+    font-size: 18px;
+    position: relative;
+    top:0;
+    *top: 10px;
+}
+
+#upload .placeholder .webuploader-pick {
+    font-size: 18px;
+    background: #00b7ee;
+    border-radius: 3px;
+    line-height: 44px;
+    padding: 0 30px;
+    *width: 120px;
+    color: #fff;
+    display: inline-block;
+    margin: 0 auto 20px auto;
+    cursor: pointer;
+    box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
+}
+
+#upload .placeholder .webuploader-pick-hover {
+    background: #00a2d4;
+}
+
+
+#filePickerContainer {
+    text-align: center;
+}
+
+#upload .placeholder .flashTip {
+    color: #666666;
+    font-size: 12px;
+    position: absolute;
+    width: 100%;
+    text-align: center;
+    bottom: 20px;
+}
+
+#upload .placeholder .flashTip a {
+    color: #0785d1;
+    text-decoration: none;
+}
+
+#upload .placeholder .flashTip a:hover {
+    text-decoration: underline;
+}
+
+#upload .placeholder.webuploader-dnd-over {
+    border-color: #999999;
+}
+
+#upload .filelist {
+    list-style: none;
+    margin: 0;
+    padding: 0;
+    overflow-x: hidden;
+    overflow-y: auto;
+    position: relative;
+    height: 300px;
+}
+
+#upload .filelist:after {
+    content: '';
+    display: block;
+    width: 0;
+    height: 0;
+    overflow: hidden;
+    clear: both;
+}
+
+#upload .filelist li {
+    width: 113px;
+    height: 113px;
+    background: url(./images/bg.png);
+    text-align: center;
+    margin: 9px 0 0 9px;
+    *margin: 6px 0 0 6px;
+    position: relative;
+    display: block;
+    float: left;
+    overflow: hidden;
+    font-size: 12px;
+}
+
+#upload .filelist li p.log {
+    position: relative;
+    top: -45px;
+}
+
+#upload .filelist li p.title {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    overflow: hidden;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+    top: 5px;
+    text-indent: 5px;
+    text-align: left;
+}
+
+#upload .filelist li p.progress {
+    position: absolute;
+    width: 100%;
+    bottom: 0;
+    left: 0;
+    height: 8px;
+    overflow: hidden;
+    z-index: 50;
+    margin: 0;
+    border-radius: 0;
+    background: none;
+    -webkit-box-shadow: 0 0 0;
+}
+
+#upload .filelist li p.progress span {
+    display: none;
+    overflow: hidden;
+    width: 0;
+    height: 100%;
+    background: #1483d8 url(./images/progress.png) repeat-x;
+
+    -webit-transition: width 200ms linear;
+    -moz-transition: width 200ms linear;
+    -o-transition: width 200ms linear;
+    -ms-transition: width 200ms linear;
+    transition: width 200ms linear;
+
+    -webkit-animation: progressmove 2s linear infinite;
+    -moz-animation: progressmove 2s linear infinite;
+    -o-animation: progressmove 2s linear infinite;
+    -ms-animation: progressmove 2s linear infinite;
+    animation: progressmove 2s linear infinite;
+
+    -webkit-transform: translateZ(0);
+}
+
+@-webkit-keyframes progressmove {
+    0% {
+        background-position: 0 0;
+    }
+    100% {
+        background-position: 17px 0;
+    }
+}
+
+@-moz-keyframes progressmove {
+    0% {
+        background-position: 0 0;
+    }
+    100% {
+        background-position: 17px 0;
+    }
+}
+
+@keyframes progressmove {
+    0% {
+        background-position: 0 0;
+    }
+    100% {
+        background-position: 17px 0;
+    }
+}
+
+#upload .filelist li p.imgWrap {
+    position: relative;
+    z-index: 2;
+    line-height: 113px;
+    vertical-align: middle;
+    overflow: hidden;
+    width: 113px;
+    height: 113px;
+
+    -webkit-transform-origin: 50% 50%;
+    -moz-transform-origin: 50% 50%;
+    -o-transform-origin: 50% 50%;
+    -ms-transform-origin: 50% 50%;
+    transform-origin: 50% 50%;
+
+    -webit-transition: 200ms ease-out;
+    -moz-transition: 200ms ease-out;
+    -o-transition: 200ms ease-out;
+    -ms-transition: 200ms ease-out;
+    transition: 200ms ease-out;
+}
+#upload .filelist li p.imgWrap.notimage {
+    margin-top: 0;
+    width: 111px;
+    height: 111px;
+    border: 1px #eeeeee solid;
+}
+#upload .filelist li p.imgWrap.notimage i.file-preview {
+    margin-top: 15px;
+}
+
+#upload .filelist li img {
+    width: 100%;
+}
+
+#upload .filelist li p.error {
+    background: #f43838;
+    color: #fff;
+    position: absolute;
+    bottom: 0;
+    left: 0;
+    height: 28px;
+    line-height: 28px;
+    width: 100%;
+    z-index: 100;
+    display:none;
+}
+
+#upload .filelist li .success {
+    display: block;
+    position: absolute;
+    left: 0;
+    bottom: 0;
+    height: 40px;
+    width: 100%;
+    z-index: 200;
+    background: url(./images/success.png) no-repeat right bottom;
+    background-image: url(./images/success.gif) \9;
+}
+
+#upload .filelist li.filePickerBlock {
+    width: 113px;
+    height: 113px;
+    background: url(./images/image.png) no-repeat center 12px;
+    border: 1px solid #eeeeee;
+    border-radius: 0;
+}
+#upload .filelist li.filePickerBlock div.webuploader-pick  {
+    width: 100%;
+    height: 100%;
+    margin: 0;
+    padding: 0;
+    opacity: 0;
+    background: none;
+    font-size: 0;
+}
+
+#upload .filelist div.file-panel {
+    position: absolute;
+    height: 0;
+    filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr='#80000000', endColorstr='#80000000') \0;
+    background: rgba(0, 0, 0, 0.5);
+    width: 100%;
+    top: 0;
+    left: 0;
+    overflow: hidden;
+    z-index: 300;
+}
+
+#upload .filelist div.file-panel span {
+    width: 24px;
+    height: 24px;
+    display: inline;
+    float: right;
+    text-indent: -9999px;
+    overflow: hidden;
+    background: url(./images/icons.png) no-repeat;
+    background: url(./images/icons.gif) no-repeat \9;
+    margin: 5px 1px 1px;
+    cursor: pointer;
+    -webkit-tap-highlight-color: rgba(0,0,0,0);
+    -webkit-user-select: none;
+    -moz-user-select: none;
+    -ms-user-select: none;
+    user-select: none;
+}
+
+#upload .filelist div.file-panel span.rotateLeft {
+    display:none;
+    background-position: 0 -24px;
+}
+
+#upload .filelist div.file-panel span.rotateLeft:hover {
+    background-position: 0 0;
+}
+
+#upload .filelist div.file-panel span.rotateRight {
+    display:none;
+    background-position: -24px -24px;
+}
+
+#upload .filelist div.file-panel span.rotateRight:hover {
+    background-position: -24px 0;
+}
+
+#upload .filelist div.file-panel span.cancel {
+    background-position: -48px -24px;
+}
+
+#upload .filelist div.file-panel span.cancel:hover {
+    background-position: -48px 0;
+}
+
+#upload .statusBar {
+    height: 45px;
+    border-bottom: 1px solid #dadada;
+    margin: 0 10px;
+    padding: 0;
+    line-height: 45px;
+    vertical-align: middle;
+    position: relative;
+}
+
+#upload .statusBar .progress {
+    border: 1px solid #1483d8;
+    width: 198px;
+    background: #fff;
+    height: 18px;
+    position: absolute;
+    top: 12px;
+    display: none;
+    text-align: center;
+    line-height: 18px;
+    color: #6dbfff;
+    margin: 0 10px 0 0;
+}
+#upload .statusBar .progress span.percentage {
+    width: 0;
+    height: 100%;
+    left: 0;
+    top: 0;
+    background: #1483d8;
+    position: absolute;
+}
+#upload .statusBar .progress span.text {
+    position: relative;
+    z-index: 10;
+}
+
+#upload .statusBar .info {
+    display: inline-block;
+    font-size: 14px;
+    color: #666666;
+}
+
+#upload .statusBar .btns {
+    position: absolute;
+    top: 7px;
+    right: 0;
+    line-height: 30px;
+}
+
+#filePickerBtn {
+    display: inline-block;
+    float: left;
+}
+#upload .statusBar .btns .webuploader-pick,
+#upload .statusBar .btns .uploadBtn,
+#upload .statusBar .btns .uploadBtn.state-uploading,
+#upload .statusBar .btns .uploadBtn.state-paused {
+    background: #ffffff;
+    border: 1px solid #cfcfcf;
+    color: #565656;
+    padding: 0 18px;
+    display: inline-block;
+    border-radius: 3px;
+    margin-left: 10px;
+    cursor: pointer;
+    font-size: 14px;
+    float: left;
+    -webkit-user-select: none;
+    -moz-user-select: none;
+    -ms-user-select: none;
+    user-select: none;
+}
+#upload .statusBar .btns .webuploader-pick-hover,
+#upload .statusBar .btns .uploadBtn:hover,
+#upload .statusBar .btns .uploadBtn.state-uploading:hover,
+#upload .statusBar .btns .uploadBtn.state-paused:hover {
+    background: #f0f0f0;
+}
+
+#upload .statusBar .btns .uploadBtn,
+#upload .statusBar .btns .uploadBtn.state-paused{
+    background: #00b7ee;
+    color: #fff;
+    border-color: transparent;
+}
+#upload .statusBar .btns .uploadBtn:hover,
+#upload .statusBar .btns .uploadBtn.state-paused:hover{
+    background: #00a2d4;
+}
+
+#upload .statusBar .btns .uploadBtn.disabled {
+    pointer-events: none;
+    filter:alpha(opacity=60);
+    -moz-opacity:0.6;
+    -khtml-opacity: 0.6;
+    opacity: 0.6;
+}
+
+
+
+/* 图片管理样式 */
+#online {
+    width: 100%;
+    height: 336px;
+    padding: 10px 0 0 0;
+}
+#online #fileList{
+    width: 100%;
+    height: 100%;
+    overflow-x: hidden;
+    overflow-y: auto;
+    position: relative;
+}
+#online ul {
+    display: block;
+    list-style: none;
+    margin: 0;
+    padding: 0;
+}
+#online li {
+    float: left;
+    display: block;
+    list-style: none;
+    padding: 0;
+    width: 113px;
+    height: 113px;
+    margin: 0 0 9px 9px;
+    *margin: 0 0 6px 6px;
+    background-color: #eee;
+    overflow: hidden;
+    cursor: pointer;
+    position: relative;
+}
+#online li.clearFloat {
+    float: none;
+    clear: both;
+    display: block;
+    width:0;
+    height:0;
+    margin: 0;
+    padding: 0;
+}
+#online li img {
+    cursor: pointer;
+}
+#online li div.file-wrapper {
+    cursor: pointer;
+    position: absolute;
+    display: block;
+    width: 111px;
+    height: 111px;
+    border: 1px solid #eee;
+    background: url("./images/bg.png") repeat;
+}
+#online li div span.file-title{
+    display: block;
+    padding: 0 3px;
+    margin: 3px 0 0 0;
+    font-size: 12px;
+    height: 13px;
+    color: #555555;
+    text-align: center;
+    width: 107px;
+    white-space: nowrap;
+    word-break: break-all;
+    overflow: hidden;
+    text-overflow: ellipsis;
+}
+#online li .icon {
+    cursor: pointer;
+    width: 113px;
+    height: 113px;
+    position: absolute;
+    top: 0;
+    left: 0;
+    z-index: 2;
+    border: 0;
+    background-repeat: no-repeat;
+}
+#online li .icon:hover {
+    width: 107px;
+    height: 107px;
+    border: 3px solid #1094fa;
+}
+#online li.selected .icon {
+    background-image: url(images/success.png);
+    background-image: url(images/success.gif) \9;
+    background-position: 75px 75px;
+}
+#online li.selected .icon:hover {
+    width: 107px;
+    height: 107px;
+    border: 3px solid #1094fa;
+    background-position: 72px 72px;
+}
+
+
+/* 在线文件的文件预览图标 */
+i.file-preview {
+    display: block;
+    margin: 10px auto;
+    width: 70px;
+    height: 70px;
+    background-image: url("./images/file-icons.png");
+    background-image: url("./images/file-icons.gif") \9;
+    background-position: -140px center;
+    background-repeat: no-repeat;
+}
+i.file-preview.file-type-dir{
+    background-position: 0 center;
+}
+i.file-preview.file-type-file{
+    background-position: -140px center;
+}
+i.file-preview.file-type-filelist{
+    background-position: -210px center;
+}
+i.file-preview.file-type-zip,
+i.file-preview.file-type-rar,
+i.file-preview.file-type-7z,
+i.file-preview.file-type-tar,
+i.file-preview.file-type-gz,
+i.file-preview.file-type-bz2{
+    background-position: -280px center;
+}
+i.file-preview.file-type-xls,
+i.file-preview.file-type-xlsx{
+    background-position: -350px center;
+}
+i.file-preview.file-type-doc,
+i.file-preview.file-type-docx{
+    background-position: -420px center;
+}
+i.file-preview.file-type-ppt,
+i.file-preview.file-type-pptx{
+    background-position: -490px center;
+}
+i.file-preview.file-type-vsd{
+    background-position: -560px center;
+}
+i.file-preview.file-type-pdf{
+    background-position: -630px center;
+}
+i.file-preview.file-type-txt,
+i.file-preview.file-type-md,
+i.file-preview.file-type-json,
+i.file-preview.file-type-htm,
+i.file-preview.file-type-xml,
+i.file-preview.file-type-html,
+i.file-preview.file-type-js,
+i.file-preview.file-type-css,
+i.file-preview.file-type-php,
+i.file-preview.file-type-jsp,
+i.file-preview.file-type-asp{
+    background-position: -700px center;
+}
+i.file-preview.file-type-apk{
+    background-position: -770px center;
+}
+i.file-preview.file-type-exe{
+    background-position: -840px center;
+}
+i.file-preview.file-type-ipa{
+    background-position: -910px center;
+}
+i.file-preview.file-type-mp4,
+i.file-preview.file-type-swf,
+i.file-preview.file-type-mkv,
+i.file-preview.file-type-avi,
+i.file-preview.file-type-flv,
+i.file-preview.file-type-mov,
+i.file-preview.file-type-mpg,
+i.file-preview.file-type-mpeg,
+i.file-preview.file-type-ogv,
+i.file-preview.file-type-webm,
+i.file-preview.file-type-rm,
+i.file-preview.file-type-rmvb{
+    background-position: -980px center;
+}
+i.file-preview.file-type-ogg,
+i.file-preview.file-type-wav,
+i.file-preview.file-type-wmv,
+i.file-preview.file-type-mid,
+i.file-preview.file-type-mp3{
+    background-position: -1050px center;
+}
+i.file-preview.file-type-jpg,
+i.file-preview.file-type-jpeg,
+i.file-preview.file-type-gif,
+i.file-preview.file-type-bmp,
+i.file-preview.file-type-png,
+i.file-preview.file-type-psd{
+    background-position: -140px center;
+}

+ 60 - 0
app/public/js/ueditor/dialogs/attachment/attachment.html

@@ -0,0 +1,60 @@
+<!doctype html>
+<html>
+<head>
+    <meta charset="UTF-8">
+    <title>ueditor图片对话框</title>
+    <script type="text/javascript" src="../internal.js"></script>
+
+    <!-- jquery -->
+    <script type="text/javascript" src="../../third-party/jquery-1.10.2.min.js"></script>
+
+    <!-- webuploader -->
+    <script src="../../third-party/webuploader/webuploader.min.js"></script>
+    <link rel="stylesheet" type="text/css" href="../../third-party/webuploader/webuploader.css">
+
+    <!-- attachment dialog -->
+    <link rel="stylesheet" href="attachment.css" type="text/css" />
+</head>
+<body>
+
+    <div class="wrapper">
+        <div id="tabhead" class="tabhead">
+            <span class="tab focus" data-content-id="upload"><var id="lang_tab_upload"></var></span>
+            <span class="tab" data-content-id="online"><var id="lang_tab_online"></var></span>
+        </div>
+        <div id="tabbody" class="tabbody">
+            <!-- 上传图片 -->
+            <div id="upload" class="panel focus">
+                <div id="queueList" class="queueList">
+                    <div class="statusBar element-invisible">
+                        <div class="progress">
+                            <span class="text">0%</span>
+                            <span class="percentage"></span>
+                        </div><div class="info"></div>
+                        <div class="btns">
+                            <div id="filePickerBtn"></div>
+                            <div class="uploadBtn"><var id="lang_start_upload"></var></div>
+                        </div>
+                    </div>
+                    <div id="dndArea" class="placeholder">
+                        <div class="filePickerContainer">
+                            <div id="filePickerReady"></div>
+                        </div>
+                    </div>
+                    <ul class="filelist element-invisible">
+                        <li id="filePickerBlock" class="filePickerBlock"></li>
+                    </ul>
+                </div>
+            </div>
+
+            <!-- 在线图片 -->
+            <div id="online" class="panel">
+                <div id="fileList"><var id="lang_imgLoading"></var></div>
+            </div>
+
+        </div>
+    </div>
+    <script type="text/javascript" src="attachment.js"></script>
+
+</body>
+</html>

+ 754 - 0
app/public/js/ueditor/dialogs/attachment/attachment.js

@@ -0,0 +1,754 @@
+/**
+ * User: Jinqn
+ * Date: 14-04-08
+ * Time: 下午16:34
+ * 上传图片对话框逻辑代码,包括tab: 远程图片/上传图片/在线图片/搜索图片
+ */
+
+(function () {
+
+    var uploadFile,
+        onlineFile;
+
+    window.onload = function () {
+        initTabs();
+        initButtons();
+    };
+
+    /* 初始化tab标签 */
+    function initTabs() {
+        var tabs = $G('tabhead').children;
+        for (var i = 0; i < tabs.length; i++) {
+            domUtils.on(tabs[i], "click", function (e) {
+                var target = e.target || e.srcElement;
+                setTabFocus(target.getAttribute('data-content-id'));
+            });
+        }
+
+        setTabFocus('upload');
+    }
+
+    /* 初始化tabbody */
+    function setTabFocus(id) {
+        if(!id) return;
+        var i, bodyId, tabs = $G('tabhead').children;
+        for (i = 0; i < tabs.length; i++) {
+            bodyId = tabs[i].getAttribute('data-content-id')
+            if (bodyId == id) {
+                domUtils.addClass(tabs[i], 'focus');
+                domUtils.addClass($G(bodyId), 'focus');
+            } else {
+                domUtils.removeClasses(tabs[i], 'focus');
+                domUtils.removeClasses($G(bodyId), 'focus');
+            }
+        }
+        switch (id) {
+            case 'upload':
+                uploadFile = uploadFile || new UploadFile('queueList');
+                break;
+            case 'online':
+                onlineFile = onlineFile || new OnlineFile('fileList');
+                break;
+        }
+    }
+
+    /* 初始化onok事件 */
+    function initButtons() {
+
+        dialog.onok = function () {
+            var list = [], id, tabs = $G('tabhead').children;
+            for (var i = 0; i < tabs.length; i++) {
+                if (domUtils.hasClass(tabs[i], 'focus')) {
+                    id = tabs[i].getAttribute('data-content-id');
+                    break;
+                }
+            }
+
+            switch (id) {
+                case 'upload':
+                    list = uploadFile.getInsertList();
+                    var count = uploadFile.getQueueCount();
+                    if (count) {
+                        $('.info', '#queueList').html('<span style="color:red;">' + '还有2个未上传文件'.replace(/[\d]/, count) + '</span>');
+                        return false;
+                    }
+                    break;
+                case 'online':
+                    list = onlineFile.getInsertList();
+                    break;
+            }
+
+            editor.execCommand('insertfile', list);
+        };
+    }
+
+
+    /* 上传附件 */
+    function UploadFile(target) {
+        this.$wrap = target.constructor == String ? $('#' + target) : $(target);
+        this.init();
+    }
+    UploadFile.prototype = {
+        init: function () {
+            this.fileList = [];
+            this.initContainer();
+            this.initUploader();
+        },
+        initContainer: function () {
+            this.$queue = this.$wrap.find('.filelist');
+        },
+        /* 初始化容器 */
+        initUploader: function () {
+            var _this = this,
+                $ = jQuery,    // just in case. Make sure it's not an other libaray.
+                $wrap = _this.$wrap,
+            // 图片容器
+                $queue = $wrap.find('.filelist'),
+            // 状态栏,包括进度和控制按钮
+                $statusBar = $wrap.find('.statusBar'),
+            // 文件总体选择信息。
+                $info = $statusBar.find('.info'),
+            // 上传按钮
+                $upload = $wrap.find('.uploadBtn'),
+            // 上传按钮
+                $filePickerBtn = $wrap.find('.filePickerBtn'),
+            // 上传按钮
+                $filePickerBlock = $wrap.find('.filePickerBlock'),
+            // 没选择文件之前的内容。
+                $placeHolder = $wrap.find('.placeholder'),
+            // 总体进度条
+                $progress = $statusBar.find('.progress').hide(),
+            // 添加的文件数量
+                fileCount = 0,
+            // 添加的文件总大小
+                fileSize = 0,
+            // 优化retina, 在retina下这个值是2
+                ratio = window.devicePixelRatio || 1,
+            // 缩略图大小
+                thumbnailWidth = 113 * ratio,
+                thumbnailHeight = 113 * ratio,
+            // 可能有pedding, ready, uploading, confirm, done.
+                state = '',
+            // 所有文件的进度信息,key为file id
+                percentages = {},
+                supportTransition = (function () {
+                    var s = document.createElement('p').style,
+                        r = 'transition' in s ||
+                            'WebkitTransition' in s ||
+                            'MozTransition' in s ||
+                            'msTransition' in s ||
+                            'OTransition' in s;
+                    s = null;
+                    return r;
+                })(),
+            // WebUploader实例
+                uploader,
+                actionUrl = editor.getActionUrl(editor.getOpt('fileActionName')),
+                fileMaxSize = editor.getOpt('fileMaxSize'),
+                acceptExtensions = (editor.getOpt('fileAllowFiles') || []).join('').replace(/\./g, ',').replace(/^[,]/, '');;
+
+            if (!WebUploader.Uploader.support()) {
+                $('#filePickerReady').after($('<div>').html(lang.errorNotSupport)).hide();
+                return;
+            } else if (!editor.getOpt('fileActionName')) {
+                $('#filePickerReady').after($('<div>').html(lang.errorLoadConfig)).hide();
+                return;
+            }
+
+            uploader = _this.uploader = WebUploader.create({
+                pick: {
+                    id: '#filePickerReady',
+                    label: lang.uploadSelectFile
+                },
+                swf: '../../third-party/webuploader/Uploader.swf',
+                server: actionUrl,
+                fileVal: editor.getOpt('fileFieldName'),
+                duplicate: true,
+                fileSingleSizeLimit: fileMaxSize,
+                compress: false
+            });
+            uploader.addButton({
+                id: '#filePickerBlock'
+            });
+            uploader.addButton({
+                id: '#filePickerBtn',
+                label: lang.uploadAddFile
+            });
+
+            setState('pedding');
+
+            // 当有文件添加进来时执行,负责view的创建
+            function addFile(file) {
+                var $li = $('<li id="' + file.id + '">' +
+                        '<p class="title">' + file.name + '</p>' +
+                        '<p class="imgWrap"></p>' +
+                        '<p class="progress"><span></span></p>' +
+                        '</li>'),
+
+                    $btns = $('<div class="file-panel">' +
+                        '<span class="cancel">' + lang.uploadDelete + '</span>' +
+                        '<span class="rotateRight">' + lang.uploadTurnRight + '</span>' +
+                        '<span class="rotateLeft">' + lang.uploadTurnLeft + '</span></div>').appendTo($li),
+                    $prgress = $li.find('p.progress span'),
+                    $wrap = $li.find('p.imgWrap'),
+                    $info = $('<p class="error"></p>').hide().appendTo($li),
+
+                    showError = function (code) {
+                        switch (code) {
+                            case 'exceed_size':
+                                text = lang.errorExceedSize;
+                                break;
+                            case 'interrupt':
+                                text = lang.errorInterrupt;
+                                break;
+                            case 'http':
+                                text = lang.errorHttp;
+                                break;
+                            case 'not_allow_type':
+                                text = lang.errorFileType;
+                                break;
+                            default:
+                                text = lang.errorUploadRetry;
+                                break;
+                        }
+                        $info.text(text).show();
+                    };
+
+                if (file.getStatus() === 'invalid') {
+                    showError(file.statusText);
+                } else {
+                    $wrap.text(lang.uploadPreview);
+                    if ('|png|jpg|jpeg|bmp|gif|'.indexOf('|'+file.ext.toLowerCase()+'|') == -1) {
+                        $wrap.empty().addClass('notimage').append('<i class="file-preview file-type-' + file.ext.toLowerCase() + '"></i>' +
+                        '<span class="file-title" title="' + file.name + '">' + file.name + '</span>');
+                    } else {
+                        if (browser.ie && browser.version <= 7) {
+                            $wrap.text(lang.uploadNoPreview);
+                        } else {
+                            uploader.makeThumb(file, function (error, src) {
+                                if (error || !src) {
+                                    $wrap.text(lang.uploadNoPreview);
+                                } else {
+                                    var $img = $('<img src="' + src + '">');
+                                    $wrap.empty().append($img);
+                                    $img.on('error', function () {
+                                        $wrap.text(lang.uploadNoPreview);
+                                    });
+                                }
+                            }, thumbnailWidth, thumbnailHeight);
+                        }
+                    }
+                    percentages[ file.id ] = [ file.size, 0 ];
+                    file.rotation = 0;
+
+                    /* 检查文件格式 */
+                    if (!file.ext || acceptExtensions.indexOf(file.ext.toLowerCase()) == -1) {
+                        showError('not_allow_type');
+                        uploader.removeFile(file);
+                    }
+                }
+
+                file.on('statuschange', function (cur, prev) {
+                    if (prev === 'progress') {
+                        $prgress.hide().width(0);
+                    } else if (prev === 'queued') {
+                        $li.off('mouseenter mouseleave');
+                        $btns.remove();
+                    }
+                    // 成功
+                    if (cur === 'error' || cur === 'invalid') {
+                        showError(file.statusText);
+                        percentages[ file.id ][ 1 ] = 1;
+                    } else if (cur === 'interrupt') {
+                        showError('interrupt');
+                    } else if (cur === 'queued') {
+                        percentages[ file.id ][ 1 ] = 0;
+                    } else if (cur === 'progress') {
+                        $info.hide();
+                        $prgress.css('display', 'block');
+                    } else if (cur === 'complete') {
+                    }
+
+                    $li.removeClass('state-' + prev).addClass('state-' + cur);
+                });
+
+                $li.on('mouseenter', function () {
+                    $btns.stop().animate({height: 30});
+                });
+                $li.on('mouseleave', function () {
+                    $btns.stop().animate({height: 0});
+                });
+
+                $btns.on('click', 'span', function () {
+                    var index = $(this).index(),
+                        deg;
+
+                    switch (index) {
+                        case 0:
+                            uploader.removeFile(file);
+                            return;
+                        case 1:
+                            file.rotation += 90;
+                            break;
+                        case 2:
+                            file.rotation -= 90;
+                            break;
+                    }
+
+                    if (supportTransition) {
+                        deg = 'rotate(' + file.rotation + 'deg)';
+                        $wrap.css({
+                            '-webkit-transform': deg,
+                            '-mos-transform': deg,
+                            '-o-transform': deg,
+                            'transform': deg
+                        });
+                    } else {
+                        $wrap.css('filter', 'progid:DXImageTransform.Microsoft.BasicImage(rotation=' + (~~((file.rotation / 90) % 4 + 4) % 4) + ')');
+                    }
+
+                });
+
+                $li.insertBefore($filePickerBlock);
+            }
+
+            // 负责view的销毁
+            function removeFile(file) {
+                var $li = $('#' + file.id);
+                delete percentages[ file.id ];
+                updateTotalProgress();
+                $li.off().find('.file-panel').off().end().remove();
+            }
+
+            function updateTotalProgress() {
+                var loaded = 0,
+                    total = 0,
+                    spans = $progress.children(),
+                    percent;
+
+                $.each(percentages, function (k, v) {
+                    total += v[ 0 ];
+                    loaded += v[ 0 ] * v[ 1 ];
+                });
+
+                percent = total ? loaded / total : 0;
+
+                spans.eq(0).text(Math.round(percent * 100) + '%');
+                spans.eq(1).css('width', Math.round(percent * 100) + '%');
+                updateStatus();
+            }
+
+            function setState(val, files) {
+
+                if (val != state) {
+
+                    var stats = uploader.getStats();
+
+                    $upload.removeClass('state-' + state);
+                    $upload.addClass('state-' + val);
+
+                    switch (val) {
+
+                        /* 未选择文件 */
+                        case 'pedding':
+                            $queue.addClass('element-invisible');
+                            $statusBar.addClass('element-invisible');
+                            $placeHolder.removeClass('element-invisible');
+                            $progress.hide(); $info.hide();
+                            uploader.refresh();
+                            break;
+
+                        /* 可以开始上传 */
+                        case 'ready':
+                            $placeHolder.addClass('element-invisible');
+                            $queue.removeClass('element-invisible');
+                            $statusBar.removeClass('element-invisible');
+                            $progress.hide(); $info.show();
+                            $upload.text(lang.uploadStart);
+                            uploader.refresh();
+                            break;
+
+                        /* 上传中 */
+                        case 'uploading':
+                            $progress.show(); $info.hide();
+                            $upload.text(lang.uploadPause);
+                            break;
+
+                        /* 暂停上传 */
+                        case 'paused':
+                            $progress.show(); $info.hide();
+                            $upload.text(lang.uploadContinue);
+                            break;
+
+                        case 'confirm':
+                            $progress.show(); $info.hide();
+                            $upload.text(lang.uploadStart);
+
+                            stats = uploader.getStats();
+                            if (stats.successNum && !stats.uploadFailNum) {
+                                setState('finish');
+                                return;
+                            }
+                            break;
+
+                        case 'finish':
+                            $progress.hide(); $info.show();
+                            if (stats.uploadFailNum) {
+                                $upload.text(lang.uploadRetry);
+                            } else {
+                                $upload.text(lang.uploadStart);
+                            }
+                            break;
+                    }
+
+                    state = val;
+                    updateStatus();
+
+                }
+
+                if (!_this.getQueueCount()) {
+                    $upload.addClass('disabled')
+                } else {
+                    $upload.removeClass('disabled')
+                }
+
+            }
+
+            function updateStatus() {
+                var text = '', stats;
+
+                if (state === 'ready') {
+                    text = lang.updateStatusReady.replace('_', fileCount).replace('_KB', WebUploader.formatSize(fileSize));
+                } else if (state === 'confirm') {
+                    stats = uploader.getStats();
+                    if (stats.uploadFailNum) {
+                        text = lang.updateStatusConfirm.replace('_', stats.successNum).replace('_', stats.successNum);
+                    }
+                } else {
+                    stats = uploader.getStats();
+                    text = lang.updateStatusFinish.replace('_', fileCount).
+                        replace('_KB', WebUploader.formatSize(fileSize)).
+                        replace('_', stats.successNum);
+
+                    if (stats.uploadFailNum) {
+                        text += lang.updateStatusError.replace('_', stats.uploadFailNum);
+                    }
+                }
+
+                $info.html(text);
+            }
+
+            uploader.on('fileQueued', function (file) {
+                fileCount++;
+                fileSize += file.size;
+
+                if (fileCount === 1) {
+                    $placeHolder.addClass('element-invisible');
+                    $statusBar.show();
+                }
+
+                addFile(file);
+            });
+
+            uploader.on('fileDequeued', function (file) {
+                fileCount--;
+                fileSize -= file.size;
+
+                removeFile(file);
+                updateTotalProgress();
+            });
+
+            uploader.on('filesQueued', function (file) {
+                if (!uploader.isInProgress() && (state == 'pedding' || state == 'finish' || state == 'confirm' || state == 'ready')) {
+                    setState('ready');
+                }
+                updateTotalProgress();
+            });
+
+            uploader.on('all', function (type, files) {
+                switch (type) {
+                    case 'uploadFinished':
+                        setState('confirm', files);
+                        break;
+                    case 'startUpload':
+                        /* 添加额外的GET参数 */
+                        var params = utils.serializeParam(editor.queryCommandValue('serverparam')) || '',
+                            url = utils.formatUrl(actionUrl + (actionUrl.indexOf('?') == -1 ? '?':'&') + 'encode=utf-8&' + params);
+                        uploader.option('server', url);
+                        setState('uploading', files);
+                        break;
+                    case 'stopUpload':
+                        setState('paused', files);
+                        break;
+                }
+            });
+
+            uploader.on('uploadBeforeSend', function (file, data, header) {
+                //这里可以通过data对象添加POST参数
+                header['X_Requested_With'] = 'XMLHttpRequest';
+            });
+
+            uploader.on('uploadProgress', function (file, percentage) {
+                var $li = $('#' + file.id),
+                    $percent = $li.find('.progress span');
+
+                $percent.css('width', percentage * 100 + '%');
+                percentages[ file.id ][ 1 ] = percentage;
+                updateTotalProgress();
+            });
+
+            uploader.on('uploadSuccess', function (file, ret) {
+                var $file = $('#' + file.id);
+                try {
+                    var responseText = (ret._raw || ret),
+                        json = utils.str2json(responseText);
+                    if (json.state == 'SUCCESS') {
+                        _this.fileList.push(json);
+                        $file.append('<span class="success"></span>');
+                    } else {
+                        $file.find('.error').text(json.state).show();
+                    }
+                } catch (e) {
+                    $file.find('.error').text(lang.errorServerUpload).show();
+                }
+            });
+
+            uploader.on('uploadError', function (file, code) {
+            });
+            uploader.on('error', function (code, file) {
+                if (code == 'Q_TYPE_DENIED' || code == 'F_EXCEED_SIZE') {
+                    addFile(file);
+                }
+            });
+            uploader.on('uploadComplete', function (file, ret) {
+            });
+
+            $upload.on('click', function () {
+                if ($(this).hasClass('disabled')) {
+                    return false;
+                }
+
+                if (state === 'ready') {
+                    uploader.upload();
+                } else if (state === 'paused') {
+                    uploader.upload();
+                } else if (state === 'uploading') {
+                    uploader.stop();
+                }
+            });
+
+            $upload.addClass('state-' + state);
+            updateTotalProgress();
+        },
+        getQueueCount: function () {
+            var file, i, status, readyFile = 0, files = this.uploader.getFiles();
+            for (i = 0; file = files[i++]; ) {
+                status = file.getStatus();
+                if (status == 'queued' || status == 'uploading' || status == 'progress') readyFile++;
+            }
+            return readyFile;
+        },
+        getInsertList: function () {
+            var i, link, data, list = [],
+                prefix = editor.getOpt('fileUrlPrefix');
+            for (i = 0; i < this.fileList.length; i++) {
+                data = this.fileList[i];
+                link = data.url;
+                list.push({
+                    title: data.original || link.substr(link.lastIndexOf('/') + 1),
+                    url: prefix + link
+                });
+            }
+            return list;
+        }
+    };
+
+
+    /* 在线附件 */
+    function OnlineFile(target) {
+        this.container = utils.isString(target) ? document.getElementById(target) : target;
+        this.init();
+    }
+    OnlineFile.prototype = {
+        init: function () {
+            this.initContainer();
+            this.initEvents();
+            this.initData();
+        },
+        /* 初始化容器 */
+        initContainer: function () {
+            this.container.innerHTML = '';
+            this.list = document.createElement('ul');
+            this.clearFloat = document.createElement('li');
+
+            domUtils.addClass(this.list, 'list');
+            domUtils.addClass(this.clearFloat, 'clearFloat');
+
+            this.list.appendChild(this.clearFloat);
+            this.container.appendChild(this.list);
+        },
+        /* 初始化滚动事件,滚动到地步自动拉取数据 */
+        initEvents: function () {
+            var _this = this;
+
+            /* 滚动拉取图片 */
+            domUtils.on($G('fileList'), 'scroll', function(e){
+                var panel = this;
+                if (panel.scrollHeight - (panel.offsetHeight + panel.scrollTop) < 10) {
+                    _this.getFileData();
+                }
+            });
+            /* 选中图片 */
+            domUtils.on(this.list, 'click', function (e) {
+                var target = e.target || e.srcElement,
+                    li = target.parentNode;
+
+                if (li.tagName.toLowerCase() == 'li') {
+                    if (domUtils.hasClass(li, 'selected')) {
+                        domUtils.removeClasses(li, 'selected');
+                    } else {
+                        domUtils.addClass(li, 'selected');
+                    }
+                }
+            });
+        },
+        /* 初始化第一次的数据 */
+        initData: function () {
+
+            /* 拉取数据需要使用的值 */
+            this.state = 0;
+            this.listSize = editor.getOpt('fileManagerListSize');
+            this.listIndex = 0;
+            this.listEnd = false;
+
+            /* 第一次拉取数据 */
+            this.getFileData();
+        },
+        /* 向后台拉取图片列表数据 */
+        getFileData: function () {
+            var _this = this;
+
+            if(!_this.listEnd && !this.isLoadingData) {
+                this.isLoadingData = true;
+                ajax.request(editor.getActionUrl(editor.getOpt('fileManagerActionName')), {
+                    timeout: 100000,
+                    data: utils.extend({
+                            start: this.listIndex,
+                            size: this.listSize
+                        }, editor.queryCommandValue('serverparam')),
+                    method: 'get',
+                    onsuccess: function (r) {
+                        try {
+                            var json = eval('(' + r.responseText + ')');
+                            if (json.state == 'SUCCESS') {
+                                _this.pushData(json.list);
+                                _this.listIndex = parseInt(json.start) + parseInt(json.list.length);
+                                if(_this.listIndex >= json.total) {
+                                    _this.listEnd = true;
+                                }
+                                _this.isLoadingData = false;
+                            }
+                        } catch (e) {
+                            if(r.responseText.indexOf('ue_separate_ue') != -1) {
+                                var list = r.responseText.split(r.responseText);
+                                _this.pushData(list);
+                                _this.listIndex = parseInt(list.length);
+                                _this.listEnd = true;
+                                _this.isLoadingData = false;
+                            }
+                        }
+                    },
+                    onerror: function () {
+                        _this.isLoadingData = false;
+                    }
+                });
+            }
+        },
+        /* 添加图片到列表界面上 */
+        pushData: function (list) {
+            var i, item, img, filetype, preview, icon, _this = this,
+                urlPrefix = editor.getOpt('fileManagerUrlPrefix');
+            for (i = 0; i < list.length; i++) {
+                if(list[i] && list[i].url) {
+                    item = document.createElement('li');
+                    icon = document.createElement('span');
+                    filetype = list[i].url.substr(list[i].url.lastIndexOf('.') + 1);
+
+                    if ( "png|jpg|jpeg|gif|bmp".indexOf(filetype) != -1 ) {
+                        preview = document.createElement('img');
+                        domUtils.on(preview, 'load', (function(image){
+                            return function(){
+                                _this.scale(image, image.parentNode.offsetWidth, image.parentNode.offsetHeight);
+                            };
+                        })(preview));
+                        preview.width = 113;
+                        preview.setAttribute('src', urlPrefix + list[i].url + (list[i].url.indexOf('?') == -1 ? '?noCache=':'&noCache=') + (+new Date()).toString(36) );
+                    } else {
+                        var ic = document.createElement('i'),
+                            textSpan = document.createElement('span');
+                        textSpan.innerHTML = list[i].url.substr(list[i].url.lastIndexOf('/') + 1);
+                        preview = document.createElement('div');
+                        preview.appendChild(ic);
+                        preview.appendChild(textSpan);
+                        domUtils.addClass(preview, 'file-wrapper');
+                        domUtils.addClass(textSpan, 'file-title');
+                        domUtils.addClass(ic, 'file-type-' + filetype);
+                        domUtils.addClass(ic, 'file-preview');
+                    }
+                    domUtils.addClass(icon, 'icon');
+                    item.setAttribute('data-url', urlPrefix + list[i].url);
+                    if (list[i].original) {
+                        item.setAttribute('data-title', list[i].original);
+                    }
+
+                    item.appendChild(preview);
+                    item.appendChild(icon);
+                    this.list.insertBefore(item, this.clearFloat);
+                }
+            }
+        },
+        /* 改变图片大小 */
+        scale: function (img, w, h, type) {
+            var ow = img.width,
+                oh = img.height;
+
+            if (type == 'justify') {
+                if (ow >= oh) {
+                    img.width = w;
+                    img.height = h * oh / ow;
+                    img.style.marginLeft = '-' + parseInt((img.width - w) / 2) + 'px';
+                } else {
+                    img.width = w * ow / oh;
+                    img.height = h;
+                    img.style.marginTop = '-' + parseInt((img.height - h) / 2) + 'px';
+                }
+            } else {
+                if (ow >= oh) {
+                    img.width = w * ow / oh;
+                    img.height = h;
+                    img.style.marginLeft = '-' + parseInt((img.width - w) / 2) + 'px';
+                } else {
+                    img.width = w;
+                    img.height = h * oh / ow;
+                    img.style.marginTop = '-' + parseInt((img.height - h) / 2) + 'px';
+                }
+            }
+        },
+        getInsertList: function () {
+            var i, lis = this.list.children, list = [];
+            for (i = 0; i < lis.length; i++) {
+                if (domUtils.hasClass(lis[i], 'selected')) {
+                    var url = lis[i].getAttribute('data-url');
+                    var title = lis[i].getAttribute('data-title') || url.substr(url.lastIndexOf('/') + 1);
+                    list.push({
+                        title: title,
+                        url: url
+                    });
+                }
+            }
+            return list;
+        }
+    };
+
+
+})();

BIN
app/public/js/ueditor/dialogs/attachment/fileTypeImages/icon_chm.gif


BIN
app/public/js/ueditor/dialogs/attachment/fileTypeImages/icon_default.png


BIN
app/public/js/ueditor/dialogs/attachment/fileTypeImages/icon_doc.gif


BIN
app/public/js/ueditor/dialogs/attachment/fileTypeImages/icon_exe.gif


BIN
app/public/js/ueditor/dialogs/attachment/fileTypeImages/icon_jpg.gif


BIN
app/public/js/ueditor/dialogs/attachment/fileTypeImages/icon_mp3.gif


BIN
app/public/js/ueditor/dialogs/attachment/fileTypeImages/icon_mv.gif


BIN
app/public/js/ueditor/dialogs/attachment/fileTypeImages/icon_pdf.gif


BIN
app/public/js/ueditor/dialogs/attachment/fileTypeImages/icon_ppt.gif


BIN
app/public/js/ueditor/dialogs/attachment/fileTypeImages/icon_psd.gif


BIN
app/public/js/ueditor/dialogs/attachment/fileTypeImages/icon_rar.gif


BIN
app/public/js/ueditor/dialogs/attachment/fileTypeImages/icon_txt.gif


BIN
app/public/js/ueditor/dialogs/attachment/fileTypeImages/icon_xls.gif


BIN
app/public/js/ueditor/dialogs/attachment/images/alignicon.gif


BIN
app/public/js/ueditor/dialogs/attachment/images/alignicon.png


BIN
app/public/js/ueditor/dialogs/attachment/images/bg.png


BIN
app/public/js/ueditor/dialogs/attachment/images/file-icons.gif


BIN
app/public/js/ueditor/dialogs/attachment/images/file-icons.png


BIN
app/public/js/ueditor/dialogs/attachment/images/icons.gif


BIN
app/public/js/ueditor/dialogs/attachment/images/icons.png


BIN
app/public/js/ueditor/dialogs/attachment/images/image.png


BIN
app/public/js/ueditor/dialogs/attachment/images/progress.png


BIN
app/public/js/ueditor/dialogs/attachment/images/success.gif


BIN
app/public/js/ueditor/dialogs/attachment/images/success.png


+ 43 - 0
app/public/js/ueditor/dialogs/emotion/emotion.css

@@ -0,0 +1,43 @@
+.jd img{
+    background:transparent url(images/jxface2.gif?v=1.1) no-repeat scroll left top;
+    cursor:pointer;width:35px;height:35px;display:block;
+}
+.pp img{
+    background:transparent url(images/fface.gif?v=1.1) no-repeat scroll left top;
+    cursor:pointer;width:25px;height:25px;display:block;
+}
+.ldw img{
+    background:transparent url(images/wface.gif?v=1.1) no-repeat scroll left top;
+    cursor:pointer;width:35px;height:35px;display:block;
+}
+.tsj img{
+    background:transparent url(images/tface.gif?v=1.1) no-repeat scroll left top;
+    cursor:pointer;width:35px;height:35px;display:block;
+}
+.cat img{
+    background:transparent url(images/cface.gif?v=1.1) no-repeat scroll left top;
+    cursor:pointer;width:35px;height:35px;display:block;
+}
+.bb img{
+    background:transparent url(images/bface.gif?v=1.1) no-repeat scroll left top;
+    cursor:pointer;width:35px;height:35px;display:block;
+}
+.youa img{
+    background:transparent url(images/yface.gif?v=1.1) no-repeat scroll left top;
+    cursor:pointer;width:35px;height:35px;display:block;
+}
+
+.smileytable td {height: 37px;}
+#tabPanel{margin-left:5px;overflow: hidden;}
+#tabContent {float:left;background:#FFFFFF;}
+#tabContent div{display: none;width:480px;overflow:hidden;}
+#tabIconReview.show{left:17px;display:block;}
+.menuFocus{background:#ACCD3C;}
+.menuDefault{background:#FFFFFF;}
+#tabIconReview{position:absolute;left:406px;left:398px \9;top:41px;z-index:65533;width:90px;height:76px;}
+img.review{width:90px;height:76px;border:2px solid #9cb945;background:#FFFFFF;background-position:center;background-repeat:no-repeat;}
+
+.wrapper .tabbody{position:relative;float:left;clear:both;padding:10px;width: 95%;}
+.tabbody table{width: 100%;}
+.tabbody td{border:1px solid #BAC498;}
+.tabbody td span{display: block;zoom:1;padding:0 4px;}

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 54 - 0
app/public/js/ueditor/dialogs/emotion/emotion.html


+ 0 - 0
app/public/js/ueditor/dialogs/emotion/emotion.js


Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov