Browse Source

海报生成插件

lizhen_gitee 8 months ago
parent
commit
9176707087
33 changed files with 4329 additions and 0 deletions
  1. 1 0
      addons/poster/.addonrc
  2. 83 0
      addons/poster/Poster.php
  3. 34 0
      addons/poster/README.md
  4. 7 0
      addons/poster/bootstrap.js
  5. 5 0
      addons/poster/config.php
  6. 13 0
      addons/poster/controller/Index.php
  7. 10 0
      addons/poster/info.ini
  8. 31 0
      addons/poster/install.sql
  9. 191 0
      addons/poster/library/Image.php
  10. 3 0
      addons/poster/view/hook/user_sidenav_after.html
  11. 320 0
      application/admin/controller/Poster.php
  12. 23 0
      application/admin/lang/zh-cn/poster.php
  13. 44 0
      application/admin/model/Poster.php
  14. 29 0
      application/admin/model/PosterLog.php
  15. 27 0
      application/admin/validate/Poster.php
  16. 271 0
      application/admin/view/poster/add.html
  17. 300 0
      application/admin/view/poster/edit.html
  18. 42 0
      application/admin/view/poster/index.html
  19. 16 0
      application/admin/view/poster/posterrecord.html
  20. 3 0
      application/extra/addons.php
  21. 65 0
      application/index/controller/Poster.php
  22. 35 0
      application/index/view/poster/index.html
  23. BIN
      public/assets/addons/poster/images/colorful.png
  24. BIN
      public/assets/addons/poster/images/default.png
  25. BIN
      public/assets/addons/poster/images/head.jpg
  26. BIN
      public/assets/addons/poster/images/img.jpg
  27. BIN
      public/assets/addons/poster/images/qr.jpg
  28. 568 0
      public/assets/addons/poster/js/designer.js
  29. 24 0
      public/assets/addons/poster/js/jquery.colorpicker.min.js
  30. 1686 0
      public/assets/addons/poster/js/jquery.contextMenu.js
  31. 8 0
      public/assets/js/addons.js
  32. 470 0
      public/assets/js/backend/poster.js
  33. 20 0
      public/assets/js/frontend/poster.js

+ 1 - 0
addons/poster/.addonrc

@@ -0,0 +1 @@
+{"files":["application\\admin\\controller\\Poster.php","application\\admin\\lang\\zh-cn\\poster.php","application\\admin\\model\\Poster.php","application\\admin\\model\\PosterLog.php","application\\admin\\validate\\Poster.php","application\\admin\\view\\poster\\add.html","application\\admin\\view\\poster\\edit.html","application\\admin\\view\\poster\\index.html","application\\admin\\view\\poster\\posterrecord.html","application\\index\\controller\\Poster.php","application\\index\\view\\poster\\index.html","public\\assets\\js\\backend\\poster.js","public\\assets\\js\\frontend\\poster.js","public\\assets\\addons\\poster\\images\\colorful.png","public\\assets\\addons\\poster\\images\\default.png","public\\assets\\addons\\poster\\images\\head.jpg","public\\assets\\addons\\poster\\images\\img.jpg","public\\assets\\addons\\poster\\images\\qr.jpg","public\\assets\\addons\\poster\\js\\designer.js","public\\assets\\addons\\poster\\js\\jquery.colorpicker.min.js","public\\assets\\addons\\poster\\js\\jquery.contextMenu.js"],"license":"regular","licenseto":"19079","licensekey":"xd9CwLmiq3FPrXeU 5FTPqeuuHBKyTk+FSEpfFA==","domains":["fastadmin2024.com"],"licensecodes":[],"validations":["05ce8531634d889b2059dda76b3aa45a"],"menus":["poster","poster\/index","poster\/add","poster\/edit","poster\/del","poster\/multi","poster\/posterrecord","poster\/clear"]}

+ 83 - 0
addons/poster/Poster.php

@@ -0,0 +1,83 @@
+<?php
+
+namespace addons\poster;
+
+use app\common\library\Menu;
+use think\Addons;
+use think\Request;
+
+/**
+ * 超级海报管理插件
+ */
+class Poster extends Addons
+{
+    /**
+     * 插件安装方法
+     * @return bool
+     */
+    public function install()
+    {
+        $menu = [
+            [
+                'name'    => 'poster',
+                'title'   => '超级海报',
+                'icon'    => 'fa fa-qrcode',
+                'remark'  => '设计生成海报,支持会员昵称、头像、自定义图片、自定义二维码。',
+                'sublist' => [
+                    ['name' => 'poster/index', 'title' => '查看'],
+                    ['name' => 'poster/add', 'title' => '添加'],
+                    ['name' => 'poster/edit', 'title' => '修改'],
+                    ['name' => 'poster/del', 'title' => '删除'],
+                    ['name' => 'poster/multi', 'title' => '批量更新'],
+                    ['name' => 'poster/posterrecord', 'title' => '查看记录'],
+                    ['name' => 'poster/clear', 'title' => '清除海报'],
+                ]
+            ]
+        ];
+        Menu::create($menu);
+        return true;
+    }
+
+    /**
+     * 插件卸载方法
+     * @return bool
+     */
+    public function uninstall()
+    {
+        Menu::delete('poster');
+        return true;
+    }
+    
+    /**
+     * 插件启用方法
+     */
+    public function enable()
+    {
+        Menu::enable('poster');
+    }
+
+    /**
+     * 插件禁用方法
+     */
+    public function disable()
+    {
+        Menu::disable('poster');
+    }
+
+    /**
+     * 会员中心边栏后
+     * @return mixed
+     * @throws \Exception
+     */
+    public function userSidenavAfter()
+    {
+        $request = Request::instance();
+        $controllername = strtolower($request->controller());
+        $actionname = strtolower($request->action());
+        $data = [
+            'actionname'     => $actionname,
+            'controllername' => $controllername
+        ];
+        return $this->fetch('view/hook/user_sidenav_after', $data);
+    }
+}

+ 34 - 0
addons/poster/README.md

@@ -0,0 +1,34 @@
+# 超级海报
+
+#### 介绍
+1.支持生成和管理和生成多个海报
+2.可查看会员海报缓存和一键清除海报缓存数据和文件
+3.海报支持,会员头像,昵称,图片,自定义二维码
+4.可拖放移动位置,可拖拉放大缩小
+5.昵称支持颜色设置和字体大小设置
+
+#### 注意事项
+为了兼容更多系统,二维码采用了自定义表字段的形式生成,图片地址远程地址和本地均支持。
+二维码表: 你的二维码所在表
+二维码字段:二维码字段(本地文件或远程地址)
+二维码关联字段:(与会员关联的字段)
+
+#### 软件架构
+基于fastadmin开发的应用插件
+
+
+#### 安装教程
+
+1. 安装FastAdmin已经安装的请忽略,未安装的可以参考[FastAdmin框架安装文档](https://doc.fastadmin.net/doc/install.html)
+
+2. 如果你是直接从官网插件市场购买下载的,那么你下载的压缩包只是插件的核心文件,你需要先安装FastAdmin,然后在后台管理->插件管理->在线/离线安装即可。
+
+
+#### 码云特技
+
+1.  使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md
+2.  码云官方博客 [blog.gitee.com](https://blog.gitee.com)
+3.  你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解码云上的优秀开源项目
+4.  [GVP](https://gitee.com/gvp) 全称是码云最有价值开源项目,是码云综合评定出的优秀开源项目
+5.  码云官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help)
+6.  码云封面人物是一档用来展示码云会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)

+ 7 - 0
addons/poster/bootstrap.js

@@ -0,0 +1,7 @@
+require.config({
+    paths: {
+        'designer': '../addons/poster/js/designer',
+        'jquery.contextMenu': '../addons/poster/js/jquery.contextMenu',
+        'jquery-colorpicker': '../addons/poster/js/jquery.colorpicker.min',
+    }
+});

+ 5 - 0
addons/poster/config.php

@@ -0,0 +1,5 @@
+<?php
+
+return [
+    
+];

+ 13 - 0
addons/poster/controller/Index.php

@@ -0,0 +1,13 @@
+<?php
+
+namespace addons\poster\controller;
+
+use think\addons\Controller;
+
+class Index extends Controller
+{
+    public function index()
+    {
+        $this->error("当前插件暂无前台页面");
+    }
+}

+ 10 - 0
addons/poster/info.ini

@@ -0,0 +1,10 @@
+name = poster
+title = 超级海报
+intro = 设计生成海报,支持会员昵称、头像、自定义图片、自定义二维码。
+author = Xing6
+website = http://www.fastadmin.net
+version = 1.0.4
+state = 1
+url = /addons/poster
+license = regular
+licenseto = 19079

+ 31 - 0
addons/poster/install.sql

@@ -0,0 +1,31 @@
+-- ----------------------------
+-- Table structure for __PREFIX__poster
+-- ----------------------------
+CREATE TABLE `__PREFIX__poster` (
+  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID',
+  `title` varchar(255) DEFAULT '' COMMENT '海报名称',
+  `waittext` varchar(255) DEFAULT '' COMMENT '生成等待文字',
+  `bg_image` varchar(255) DEFAULT '' COMMENT '背景图片',
+  `data` text COMMENT '数据',
+  `status` enum('normal','hidden') NOT NULL DEFAULT 'normal' COMMENT '状态',
+  `weigh` int(10) NOT NULL DEFAULT '0' COMMENT '权重',
+  `createtime` int(10) DEFAULT NULL COMMENT '创建时间',
+  `updatetime` int(10) DEFAULT NULL COMMENT '更新时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_createtime` (`createtime`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='超级海报表';
+
+-- ----------------------------
+-- Table structure for __PREFIX__poster_log
+-- ----------------------------
+CREATE TABLE `__PREFIX__poster_log` (
+  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '记录ID',
+  `poster_id` int(11) DEFAULT '0' COMMENT '海报ID',
+  `user_id` int(11) DEFAULT '0' COMMENT '会员ID',
+  `image` varchar(255) DEFAULT '' COMMENT '海报图片',
+  `createtime` int(10) DEFAULT NULL COMMENT '创建时间',
+  `updatetime` int(10) DEFAULT NULL COMMENT '更新时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_poster_id` (`poster_id`),
+  KEY `idx_user_id` (`user_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='海报记录表';

+ 191 - 0
addons/poster/library/Image.php

@@ -0,0 +1,191 @@
+<?php
+
+namespace addons\poster\library;
+
+use think\Db;
+
+class Image
+{
+    protected $target;      //图像资源
+    protected $poster_url;  //海报url
+    protected $model;       //海报模型
+
+    public function __construct()
+    {
+        $this->model = new \app\admin\model\Poster;
+    }
+
+
+    /**
+     * 创建海报
+     * @author Created by Xing <464401240@qq.com>
+     */
+    public function createPosterImage($poster, $member)
+    {
+        $path = $this->model->getDirs($poster['id']);
+        if (!is_dir($path)) {
+            $this->mkdirs($path);
+        }
+        $md5 = md5(json_encode(array(
+            'user_id'   => $member['id'],
+            'bg'        => $poster['bg_image'],
+            'data'      => $poster['data'],
+            'poster_id' => $poster['id']
+        )));
+        $file = $md5 . '.png';
+        $this->poster_url = $path . $file;
+        if (is_file($this->poster_url)) {
+            return $this->poster_url; //文件存在则直接返回
+        }
+
+        set_time_limit(0);
+        @ini_set('memory_limit', '256M');
+        $this->target = imagecreatetruecolor(640, 1008);
+
+        $white = imagecolorallocate($this->target, 255, 255, 255);
+        imagefill($this->target, 0, 0, $white);
+        $bg = $this->createImage($poster['bg_image']);
+        if (!empty($bg)) {
+            $bgWidth = imagesx($bg);
+            $bgHeight = imagesy($bg);
+            $ratio = 640 / $bgWidth;
+            $newWidth = imagesx($bg) * $ratio;
+            $newHeight = imagesy($bg) * $ratio;
+
+            $bgClone = imagecreatetruecolor($newWidth, $newHeight);
+            imagecopyresampled($bgClone, $bg, 0, 0, 0, 0, $newWidth, $newHeight, $bgWidth, $bgHeight);
+
+            imagecopy($this->target, $bgClone, 0, 0, 0, 0, $newWidth, $newHeight);
+
+            imagedestroy($bg);
+            imagedestroy($bgClone);
+        }
+        $data = json_decode(str_replace('&quot;', '\'', $poster['data']), true);
+
+        foreach ($data as $d) {
+            $d = $this->getRealData($d);
+            if ($d['type'] == 'head') {
+                $avatar = preg_replace('/\\/0$/i', '/96', $member['avatar']);
+                $this->mergeImage($d, $avatar);
+            } elseif ($d['type'] == 'img' && isset($d['src'])) {
+                $this->mergeImage($d, $d['src']);
+            } elseif ($d['type'] == 'qr' && isset($d['qr_table']) && isset($d['qr_relation']) && isset($d['qr_field'])) {
+                $exist = Db::query('show tables like "' . $d['qr_table'] . '"');
+                if ($exist) {
+                    $fields = Db::getConnection()->getFields($d['qr_table']); //传入数据表名称 $tablename
+                    if (isset($fields[$d['qr_relation']])) {
+                        $qrimg = Db::table($d['qr_table'])->where([$d['qr_relation'] => $member['id']])->value($d['qr_field']);
+                        $this->mergeImage($d, $qrimg);
+                    }
+                }
+            } elseif ($d['type'] == 'nickname') {
+                $this->mergeText($d, $member['nickname']);
+            }
+        }
+
+        imagepng($this->target, $path . $file);
+        imagedestroy($this->target);
+
+        $params['poster_id'] = $poster['id'];
+        $params['user_id'] = $member['id'];
+        $params['image'] = $this->poster_url;
+        $posterLog = new \app\admin\model\PosterLog();
+        $posterLog->save($params);
+        return $this->poster_url;
+    }
+
+    public function mergeImage($data, $imgurl)
+    {
+        $img = $this->createImage($imgurl);
+        if (!$img) {
+            return false;
+        }
+        $w = imagesx($img);
+        $h = imagesy($img);
+        imagecopyresized($this->target, $img, $data['left'], $data['top'], 0, 0, $data['width'], $data['height'], $w, $h);
+        imagedestroy($img);
+    }
+
+    public function mergeText($data, $text)
+    {
+        $font = ROOT_PATH . '/public/assets/fonts/SourceHanSansK-Regular.ttf';
+        if (!isset($data['color'])) {
+            $data['color'] = '#000';
+        }
+        $colors = $this->hex2rgb($data['color']);
+        $color = imagecolorallocate($this->target, $colors['red'], $colors['green'], $colors['blue']);
+        imagettftext($this->target, $data['size'], 0, $data['left'], $data['top'] + $data['size'], $color, $font, $text);
+    }
+
+    public function createImage($imgurl)
+    {
+        if (strpos($imgurl, 'data:image') !== false) {
+            $imgurl = '/assets/addons/poster/images/head.jpg';
+        }
+        if (strpos($imgurl, '://') === false) {
+            $imgurl = ROOT_PATH . '/public' . $imgurl;
+            if (!is_file($imgurl)) {
+                return '';
+            }
+        } else {
+            $domain = 'thirdwx.qlogo.cn';
+            if (strpos($imgurl, $domain) !== false) {
+                $ip = gethostbyname($domain);
+                if ($ip) {
+                    $imgurl = str_replace($domain, $ip, $imgurl);
+                }
+            }
+        }
+        @$content = file_get_contents($imgurl);
+        if ($content) {
+            return imagecreatefromstring($content);
+        }
+        return '';
+    }
+
+    private function getRealData($data)
+    {
+        $data['left'] = isset($data['left']) ? intval(str_replace('px', '', $data['left'])) * 2 : 0;
+        $data['top'] = isset($data['top']) ? intval(str_replace('px', '', $data['top'])) * 2 : 0;
+        $data['width'] = isset($data['width']) ? intval(str_replace('px', '', $data['width'])) * 2 : 0;
+        $data['height'] = isset($data['height']) ? intval(str_replace('px', '', $data['height'])) * 2 : 0;
+        $data['size'] = isset($data['size']) ? intval(str_replace('px', '', $data['size'])) * 2 : 0;
+        return $data;
+    }
+
+    /**
+     * 递归创建文件夹
+     * @author Created by Xing <464401240@qq.com>
+     */
+    private function mkdirs($path)
+    {
+        if (!is_dir($path)) {
+            $this->mkdirs(dirname($path));
+            mkdir($path);
+        }
+        return is_dir($path);
+    }
+
+    /**
+     * hex转rgb
+     * @param $colour
+     * @return array|bool
+     */
+    private function hex2rgb($colour)
+    {
+        if ($colour[0] == '#') {
+            $colour = substr($colour, 1);
+        }
+        if (strlen($colour) == 6) {
+            list($r, $g, $b) = array($colour[0] . $colour[1], $colour[2] . $colour[3], $colour[4] . $colour[5]);
+        } elseif (strlen($colour) == 3) {
+            list($r, $g, $b) = array($colour[0] . $colour[0], $colour[1] . $colour[1], $colour[2] . $colour[2]);
+        } else {
+            return false;
+        }
+        $r = hexdec($r);
+        $g = hexdec($g);
+        $b = hexdec($b);
+        return array('red' => $r, 'green' => $g, 'blue' => $b);
+    }
+}

+ 3 - 0
addons/poster/view/hook/user_sidenav_after.html

@@ -0,0 +1,3 @@
+<ul class="list-group">
+    <li class="list-group-item {:$controllername =='poster'?'active':''}"><a href="{:url('index/poster/index')}"><i class="fa fa-qrcode"></i> {:__('我的海报')}</a></li>
+</ul>

+ 320 - 0
application/admin/controller/Poster.php

@@ -0,0 +1,320 @@
+<?php
+
+namespace app\admin\controller;
+
+use app\common\controller\Backend;
+use Exception;
+use think\Db;
+use think\Config;
+use think\exception\PDOException;
+use think\exception\ValidateException;
+
+/**
+ * @icon fa fa-circle-o
+ */
+class Poster extends Backend
+{
+    /**
+     * Poster模型对象
+     * @var \app\admin\model\Poster
+     */
+    protected $model = null;
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new \app\admin\model\Poster;
+        $this->view->assign("statusList", $this->model->getStatusList());
+
+        $tableList = [];
+        $list = \think\Db::query("SHOW TABLES");
+        foreach ($list as $key => $row) {
+            $tableList[reset($row)] = reset($row);
+        }
+        $this->view->assign("tableList", $tableList);
+    }
+
+    /**
+     * 查看
+     */
+    public function index()
+    {
+        //当前是否为关联查询
+        $this->relationSearch = false;
+        //设置过滤方法
+        $this->request->filter(['strip_tags', 'trim']);
+        if ($this->request->isAjax()) {
+            //如果发送的来源是Selectpage,则转发到Selectpage
+            if ($this->request->request('keyField')) {
+                return $this->selectpage();
+            }
+            list($where, $sort, $order, $offset, $limit) = $this->buildparams();
+            $total = $this->model
+                ->where($where)
+                ->order($sort, $order)
+                ->count();
+
+            $list = $this->model
+                ->where($where)
+                ->order($sort, $order)
+                ->limit($offset, $limit)
+                ->select();
+
+            foreach ($list as $row) {
+                $row->visible(['id','title','bg_image','weigh','status','createtime','updatetime']);
+            }
+            $list = collection($list)->toArray();
+            $result = array("total" => $total, "rows" => $list);
+
+            return json($result);
+        }
+        return $this->view->fetch();
+    }
+
+    /**
+     * 获取字段列表
+     * @internal
+     */
+    public function get_field_list()
+    {
+        $dbname = Config::get('database.database');
+        $prefix = Config::get('database.prefix');
+        $table = $this->request->request('table');
+        //从数据库中获取表字段信息
+        $sql = "SELECT * FROM `information_schema`.`columns` "
+            . "WHERE TABLE_SCHEMA = ? AND table_name = ? "
+            . "ORDER BY ORDINAL_POSITION";
+        //加载主表的列
+        $columnList = Db::query($sql, [$dbname, $table]);
+        $fieldlist = [];
+        foreach ($columnList as $index => $item) {
+            $fieldlist[] = $item['COLUMN_NAME'];
+        }
+        $this->success("", null, ['fieldlist' => $fieldlist]);
+    }
+
+    /**
+     * 添加
+     */
+    public function add()
+    {
+        if ($this->request->isPost()) {
+            $params = $this->request->post("row/a");
+            if ($params) {
+                $params = method_exists($this, 'preExcludeFields') ? $this->preExcludeFields($params) : $params;
+
+                if ($this->dataLimit && $this->dataLimitFieldAutoFill) {
+                    $params[$this->dataLimitField] = $this->auth->id;
+                }
+                $result = false;
+                Db::startTrans();
+                try {
+                    //是否采用模型验证
+                    if ($this->modelValidate) {
+                        $name = str_replace("\\model\\", "\\validate\\", get_class($this->model));
+                        $validate = is_bool($this->modelValidate) ? ($this->modelSceneValidate ? $name . '.add' : $name) : $this->modelValidate;
+                        $this->model->validateFailException(true)->validate($validate);
+                    }
+                    $result = $this->model->allowField(true)->save($params);
+                    Db::commit();
+                } catch (ValidateException $e) {
+                    Db::rollback();
+                    $this->error($e->getMessage());
+                } catch (PDOException $e) {
+                    Db::rollback();
+                    $this->error($e->getMessage());
+                } catch (Exception $e) {
+                    Db::rollback();
+                    $this->error($e->getMessage());
+                }
+                if ($result !== false) {
+                    $this->success();
+                } else {
+                    $this->error(__('No rows were inserted'));
+                }
+            }
+            $this->error(__('Parameter %s can not be empty', ''));
+        }
+        return $this->view->fetch();
+    }
+
+    /**
+     * 编辑
+     */
+    public function edit($ids = null)
+    {
+        $row = $this->model->get($ids);
+        if (!$row) {
+            $this->error(__('No Results were found'));
+        }
+        $adminIds = $this->getDataLimitAdminIds();
+        if (is_array($adminIds)) {
+            if (!in_array($row[$this->dataLimitField], $adminIds)) {
+                $this->error(__('You have no permission'));
+            }
+        }
+        if ($this->request->isPost()) {
+            $params = $this->request->post("row/a");
+            if ($params) {
+                $params = method_exists($this, 'preExcludeFields') ? $this->preExcludeFields($params) : $params;
+                $result = false;
+                Db::startTrans();
+                try {
+                    //是否采用模型验证
+                    if ($this->modelValidate) {
+                        $name = str_replace("\\model\\", "\\validate\\", get_class($this->model));
+                        $validate = is_bool($this->modelValidate) ? ($this->modelSceneValidate ? $name . '.edit' : $name) : $this->modelValidate;
+                        $row->validateFailException(true)->validate($validate);
+                    }
+                    $result = $row->allowField(true)->save($params);
+                    Db::commit();
+                } catch (ValidateException $e) {
+                    Db::rollback();
+                    $this->error($e->getMessage());
+                } catch (PDOException $e) {
+                    Db::rollback();
+                    $this->error($e->getMessage());
+                } catch (Exception $e) {
+                    Db::rollback();
+                    $this->error($e->getMessage());
+                }
+                if ($result !== false) {
+                    $this->success();
+                } else {
+                    $this->error(__('No rows were updated'));
+                }
+            }
+            $this->error(__('Parameter %s can not be empty', ''));
+        }
+
+        $row['data'] = json_decode(str_replace('&quot;', '\'', $row['data']), true);
+        $this->view->assign("row", $row);
+        return $this->view->fetch();
+    }
+
+    /**
+     * 删除
+     */
+    public function del($ids = "")
+    {
+        if ($ids) {
+            $pk = $this->model->getPk();
+            $adminIds = $this->getDataLimitAdminIds();
+            if (is_array($adminIds)) {
+                $this->model->where($this->dataLimitField, 'in', $adminIds);
+            }
+            $list = $this->model->where($pk, 'in', $ids)->select();
+
+            $count = 0;
+            Db::startTrans();
+            try {
+                foreach ($list as $k => $v) {
+                    $count += $v->delete();
+                    rmdirs(ROOT_PATH . 'public/' . $this->model->getDirs($v['id']), true);
+                }
+                Db::commit();
+            } catch (PDOException $e) {
+                Db::rollback();
+                $this->error($e->getMessage());
+            } catch (Exception $e) {
+                Db::rollback();
+                $this->error($e->getMessage());
+            }
+            if ($count) {
+                $this->success();
+            } else {
+                $this->error(__('No rows were deleted'));
+            }
+        }
+        $this->error(__('Parameter %s can not be empty', 'ids'));
+    }
+
+    /**
+     * 海报记录
+     * @author Created by Xing <464401240@qq.com>
+     */
+    public function posterrecord()
+    {
+        $ids = $this->request->param('ids');
+
+        //当前是否为关联查询
+        $this->relationSearch = true;
+        //设置过滤方法
+        $this->request->filter(['strip_tags', 'trim']);
+        if ($this->request->isAjax()) {
+            $this->model = new \app\admin\model\PosterLog();
+            $condition = $ids ? ['poster_id' => $ids] : [];
+            list($where, $sort, $order, $offset, $limit) = $this->buildparams();
+            $total = $this->model
+                ->with(['user'])
+                ->where($condition)
+                ->where($where)
+                ->order($sort, $order)
+                ->count();
+
+            $list = $this->model
+                ->with(['user'])
+                ->where($condition)
+                ->where($where)
+                ->order($sort, $order)
+                ->limit($offset, $limit)
+                ->select();
+
+            foreach ($list as $row) {
+                $row->visible(['id','image','createtime','updatetime']);
+                $row->visible(['user']);
+                $row->getRelation('user')->visible(['nickname']);
+                $row['image'] = '/' . $row['image'];
+            }
+            $list = collection($list)->toArray();
+            $result = array("total" => $total, "rows" => $list);
+
+            return json($result);
+        }
+        $this->assignconfig('ids', $ids);
+        $this->view->assign("ids", $ids);
+        return $this->view->fetch();
+    }
+
+    /**
+     * 清除海报数据
+     * @author Created by Xing <464401240@qq.com>
+     */
+    public function clear()
+    {
+        $ids = $this->request->param('poster_id');
+        $where = ['poster_id' => ['gt', 0]];
+        if ($ids) {
+            $where = ['poster_id' => $ids];
+        }
+
+        $posterLogModel = new \app\admin\model\PosterLog();
+        $adminIds = $this->getDataLimitAdminIds();
+        if (is_array($adminIds)) {
+            $posterLogModel->where($this->dataLimitField, 'in', $adminIds);
+        }
+        $list = $posterLogModel->where($where)->select();
+
+        $count = 0;
+        Db::startTrans();
+        try {
+            foreach ($list as $k => $v) {
+                $count += $v->delete();
+            }
+            Db::commit();
+        } catch (PDOException $e) {
+            Db::rollback();
+            $this->error($e->getMessage());
+        } catch (Exception $e) {
+            Db::rollback();
+            $this->error($e->getMessage());
+        }
+        rmdirs(ROOT_PATH . 'public/' . $this->model->getDirs($ids), true);
+        if ($count) {
+            $this->success();
+        } else {
+            $this->error(__('No rows were deleted'));
+        }
+        $this->error(__('Parameter %s can not be empty', 'ids'));
+    }
+}

+ 23 - 0
application/admin/lang/zh-cn/poster.php

@@ -0,0 +1,23 @@
+<?php
+
+return [
+    'Id'                 => 'ID',
+    'Qr_table'           => '二维码表',
+    'Qr_field'           => '二维码字段',
+    'Qr_relation'        => '会员关联字段',
+    'Title'              => '海报名称',
+    'Waittext'           => '生成等待文字',
+    'Bg_image'           => '背景图片',
+    'Data'               => '数据',
+    'Status'             => '状态',
+    'Createtime'         => '创建时间',
+    'Updatetime'         => '更新时间',
+    'Posterrecord'       => '查看记录',
+    'Graphics Settings'  => '图片设置',
+    'Qr code size'       => '二维码尺寸',
+    'Nickname size'      => '昵称大小',
+    'Nickname color'     => '昵称颜色',
+    'Poster elements'    => '海报元素',
+    'Nickname'           => '会员昵称',
+    'Image'              => '海报图片',
+];

+ 44 - 0
application/admin/model/Poster.php

@@ -0,0 +1,44 @@
+<?php
+
+namespace app\admin\model;
+
+use think\Model;
+
+class Poster extends Model
+{
+    // 表名
+    protected $name = 'poster';
+    
+    // 自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    protected $deleteTime = false;
+
+    // 追加属性
+    protected $append = [
+        'status_text'
+    ];
+
+    public function getStatusList()
+    {
+        return ['normal' => __('Normal'), 'hidden' => __('Hidden')];
+    }
+
+    public function getStatusTextAttr($value, $data)
+    {
+        $value = $value ? $value : (isset($data['status']) ? $data['status'] : '');
+        $list = $this->getStatusList();
+        return isset($list[$value]) ? $list[$value] : '';
+    }
+    
+    public function getDirs($poster_id = '')
+    {
+        if ($poster_id) {
+            $poster_id .= '/';
+        }
+        return 'uploads/poster/' . $poster_id;
+    }
+}

+ 29 - 0
application/admin/model/PosterLog.php

@@ -0,0 +1,29 @@
+<?php
+
+namespace app\admin\model;
+
+use think\Model;
+
+class PosterLog extends Model
+{
+    // 表名
+    protected $name = 'poster_log';
+
+    // 自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    protected $deleteTime = false;
+
+    // 追加属性
+    protected $append = [
+
+    ];
+    
+    public function user()
+    {
+        return $this->belongsTo('user', 'user_id', 'id', [], 'LEFT')->setEagerlyType(0);
+    }
+}

+ 27 - 0
application/admin/validate/Poster.php

@@ -0,0 +1,27 @@
+<?php
+
+namespace app\admin\validate;
+
+use think\Validate;
+
+class Poster extends Validate
+{
+    /**
+     * 验证规则
+     */
+    protected $rule = [
+    ];
+    /**
+     * 提示消息
+     */
+    protected $message = [
+    ];
+    /**
+     * 验证场景
+     */
+    protected $scene = [
+        'add'  => [],
+        'edit' => [],
+    ];
+    
+}

+ 271 - 0
application/admin/view/poster/add.html

@@ -0,0 +1,271 @@
+<style type='text/css'>
+    #poster {
+        width:320px;height:504px;border:1px solid #ccc;position:relative
+    }
+    #poster .bg { position:absolute;width:100%;z-index:0}
+    #poster .drag[type=img] img,#poster .drag[type=thumb] img { width:100%;height:100%; }
+    #poster .drag { position: absolute; width:80px;height:80px; border:1px solid #000; }
+    #poster .drag[type=nickname] { width:80px;height:40px; font-size:16px; font-family: 黑体;}
+    #poster .drag img {position:absolute;z-index:0;width:100%; }
+    #poster .rRightDown,.rLeftDown,.rLeftUp,.rRightUp,.rRight,.rLeft,.rUp,.rDown{  position:absolute;  width:7px;  height:7px;  z-index:1;  font-size:0;  }
+    #poster .rRightDown,.rLeftDown,.rLeftUp,.rRightUp,.rRight,.rLeft,.rUp,.rDown{  background:#C00;  }
+    .rLeftDown,.rRightUp{cursor:ne-resize;}
+    .rRightDown,.rLeftUp{cursor:nw-resize;}
+    .rRight,.rLeft{cursor:e-resize;}
+    .rUp,.rDown{cursor:n-resize;}
+    .rLeftDown{left:-4px;bottom:-4px;}
+    .rRightUp{right:-4px;top:-4px;}
+    .rRightDown{right:-4px;bottom:-4px;}
+    .rLeftUp{left:-4px;top:-4px;}
+    .rRight{right:-4px;top:50%;margin-top:-4px;}
+    .rLeft{left:-4px;top:50%;margin-top:-4px;}
+    .rUp{top:-4px;left:50%;margin-left:-4px;}
+    .rDown{bottom:-4px;left:50%;margin-left:-4px;}
+    .context-menu-layer { z-index:9999;}
+    .context-menu-list { z-index:9999;}
+    .context-menu-list {
+        margin:0;
+        padding:0;
+        min-width: 120px;
+        max-width: 250px;
+        display: inline-block;
+        position: absolute;
+        list-style-type: none;
+        border: 1px solid #DDD;
+        background: #EEE;
+        -webkit-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5);
+        -moz-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5);
+        -ms-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5);
+        -o-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5);
+        box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5);
+        font-family: Verdana, Arial, Helvetica, sans-serif;
+        font-size: 11px;
+    }
+    .context-menu-item {
+        padding: 2px 2px 2px 24px;
+        background-color: #EEE;
+        position: relative;
+        -webkit-user-select: none;
+        -moz-user-select: -moz-none;
+        -ms-user-select: none;
+        user-select: none;
+    }
+    .context-menu-separator {
+        padding-bottom:0;
+        border-bottom: 1px solid #DDD;
+    }
+    .context-menu-item > label > input,
+    .context-menu-item > label > textarea {
+        -webkit-user-select: text;
+        -moz-user-select: text;
+        -ms-user-select: text;
+        user-select: text;
+    }
+    .context-menu-item.hover {
+        cursor: pointer;
+        background-color: #39F;
+    }
+    .context-menu-item.disabled {
+        color: #666;
+    }
+    .context-menu-input.hover,
+    .context-menu-item.disabled.hover {
+        cursor: default;
+        background-color: #EEE;
+    }
+    .context-menu-submenu:after {
+        content: ">";
+        color: #666;
+        position: absolute;
+        top: 0;
+        right: 3px;
+        z-index: 1;
+    }
+    .context-menu-item.icon { min-height: 18px; background-repeat: no-repeat; background-position: 4px 2px; }
+    /* vertically align inside labels */
+    .context-menu-input > label > * { vertical-align: top; }
+    /* position checkboxes and radios as icons */
+    .context-menu-input > label > input[type="checkbox"],
+    .context-menu-input > label > input[type="radio"] {
+        margin-left: -17px;
+    }
+    .context-menu-input > label > span {
+        margin-left: 5px;
+    }
+    .context-menu-input > label,
+    .context-menu-input > label > input[type="text"],
+    .context-menu-input > label > textarea,
+    .context-menu-input > label > select {
+        display: block;
+        width: 100%;
+        -webkit-box-sizing: border-box;
+        -moz-box-sizing: border-box;
+        -ms-box-sizing: border-box;
+        -o-box-sizing: border-box;
+        box-sizing: border-box;
+    }
+    .context-menu-input > label > textarea {
+        height: 100px;
+    }
+    .context-menu-item > .context-menu-list {
+        display: none;
+        right: -5px;
+        top: 5px;
+    }
+    .context-menu-item.hover > .context-menu-list {
+        display: block;
+    }
+    .context-menu-accesskey {
+        text-decoration: underline;
+    }
+    .n-msg{display: none!important;}
+</style>
+
+<form id="add-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action="">
+
+    <div class="tab-pane active" id="tab_design">
+        <div class="form-group">
+            <div class="col-sm-12">
+                <table style="width:100%;">
+                    <tbody>
+                    <tr>
+                        <td style="width:320px;padding:10px;" valign="top">
+                            <div id="poster"></div>
+                        </td>
+                        <td valign="top" style="padding:10px;">
+                            <div class="panel panel-default">
+                                <div class="panel-body">
+                                    <div class="form-group">
+                                        <label class="control-label col-xs-12 col-sm-3">{:__('Title')}:</label>
+                                        <div class="col-xs-12 col-sm-8">
+                                            <input id="c-title" data-rule="required" class="form-control" name="row[title]" type="text" placeholder="此项必须填写" value="">
+                                        </div>
+                                    </div>
+                                    <div class="form-group">
+                                        <label class="control-label col-xs-12 col-sm-3">{:__('Waittext')}:</label>
+                                        <div class="col-xs-12 col-sm-8">
+                                            <input id="c-waittext" class="form-control" name="row[waittext]" type="text" placeholder="例如:您的专属海报正在拼命生成中,请等待片刻..." value="您的专属海报正在拼命生成中,请等待片刻...">
+                                        </div>
+                                    </div>
+                                    <div class="form-group">
+                                        <label class="control-label col-xs-12 col-sm-3">{:__('Status')}:</label>
+                                        <div class="col-xs-12 col-sm-8">
+                                            <div class="radio">
+                                                {foreach name="statusList" item="vo"}
+                                                <label for="row[status]-{$key}"><input id="row[status]-{$key}" name="row[status]" type="radio" value="{$key}" {in name="key" value="normal"}checked{/in} /> {$vo}</label>
+                                                {/foreach}
+                                            </div>
+                                        </div>
+                                    </div>
+                                    <div class="form-group">
+                                        <label for="c-weigh" class="control-label col-xs-12 col-sm-3">{:__('Weigh')}:</label>
+                                        <div class="col-xs-12 col-sm-8">
+                                            <input id="c-weigh" class="form-control" name="row[weigh]" type="number" value="0">
+                                        </div>
+                                    </div>
+                                    <div class="form-group">
+                                        <label class="control-label col-xs-12 col-sm-3">{:__('Bg_image')}:</label>
+                                        <div class="col-xs-12 col-sm-8">
+                                            <div class="input-group">
+                                                <input id="c-bg_image" class="form-control" size="50" name="row[bg_image]" type="text" value="">
+                                                <div class="input-group-addon no-border no-padding">
+                                                    <span><button type="button" id="plupload-bg_image" class="btn btn-danger plupload" data-input-id="c-bg_image" data-mimetype="image/gif,image/jpeg,image/png,image/jpg,image/bmp" data-multiple="false" data-preview-id="p-bg_image"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
+                                                    <span><button type="button" id="fachoose-bg_image" class="btn btn-primary fachoose" data-input-id="c-bg_image" data-mimetype="image/*" data-multiple="false"><i class="fa fa-list"></i> {:__('Choose')}</button></span>
+                                                </div>
+                                                <span class="msg-box n-right" for="c-bg_image"></span>
+                                            </div>
+                                            <ul class="row list-inline plupload-preview" id="p-bg_image"></ul>
+                                        </div>
+                                        <label class="control-label col-xs-12 col-sm-3"></label>
+                                        <div class="col-xs-12 col-sm-8">
+                                            <span class="help-block">背景图片尺寸: 640 * 1008</span>
+                                        </div>
+                                    </div>
+
+                                    <div class="form-group">
+                                        <label class="control-label col-xs-12 col-sm-3">{:__('Poster elements')}:</label>
+                                        <div class="col-xs-12 col-sm-8">
+                                            <button class="btn btn-default btn-com" type="button" data-type="head">头像</button>
+                                            <button class="btn btn-default btn-com" type="button" data-type="nickname">昵称</button>
+                                            <button class="btn btn-default btn-com" type="button" data-type="qr">二维码</button>
+                                            <button class="btn btn-default btn-com" type="button" data-type="img">图片</button>
+                                        </div>
+                                    </div>
+
+                                    <div class="form-group nameset" style='display:none'>
+                                        <label class="control-label col-xs-12 col-sm-3">{:__('Nickname color')}:</label>
+                                        <div class="col-xs-12 col-sm-8">
+                                            <div class="row">
+                                                <div class="col-sm-8 col-xs-12">
+                                                    <input id="c-color" data-rule="required" class="form-control" name="color" type="text" value="" style="width: 50%">
+                                                    <span class="input-group-btn" style="display: unset;">
+                                                    <button type="button" class="btn btn-default btn-color colorpicker" style="padding:0;margin-left:1px;" title="选择颜色"><img src="__CDN__/assets/addons/poster/images/colorful.png" height="29" alt=""></button>
+                                                    <span class="msg-box n-right" for="c-color"></span>
+                                                </div>
+                                                <div class="col-sm-4"></div>
+                                            </div>
+                                        </div>
+                                    </div>
+
+                                    <div class="form-group nameset" style='display:none'>
+                                        <label class="control-label col-xs-12 col-sm-3">{:__('Nickname size')}:</label>
+                                        <div class="col-xs-12 col-sm-8">
+                                            <input type="text" id="namesize" data-rule="required" class="form-control namesize" placeholder="例如: 14,16" style="width: 90%;"/>
+                                            <div class="input-group-addon" style="display: contents;">px</div>
+                                        </div>
+                                    </div>
+
+
+                                    <div class="form-group qrset" style='display:none'>
+                                        <label class="control-label col-xs-12 col-sm-3">{:__('Qr_table')}:</label>
+                                        <div class="col-xs-12 col-sm-8">
+                                            {:build_select('qr_table',$tableList,null,['class'=>'form-control selectpicker']);}
+                                        </div>
+                                    </div>
+
+                                    <div class="form-group qrset" style='display:none'>
+                                        <label class="control-label col-xs-12 col-sm-3">{:__('Qr_field')}:</label>
+                                        <div class="col-xs-12 col-sm-8">
+                                            <select id="c-qr_field" data-rule="required" style="height:30px;" class="form-control selectpicker"></select>
+                                        </div>
+                                    </div>
+
+                                    <div class="form-group qrset" style='display:none'>
+                                        <label class="control-label col-xs-12 col-sm-3">{:__('Qr_relation')}:</label>
+                                        <div class="col-xs-12 col-sm-8">
+                                            <select id="c-qr_relation" data-rule="required" style="height:30px;" class="form-control selectpicker"></select>
+                                        </div>
+                                    </div>
+
+                                    <div class="form-group imgset" style='display:none'>
+                                        <label class="control-label col-xs-12 col-sm-3">{:__('Graphics Settings')}:</label>
+                                        <div class="col-xs-12 col-sm-8">
+                                            <div class="input-group">
+                                                <input id="c-img" data-rule="required" class="form-control" size="50" name="img" type="text" value="">
+                                                <div class="input-group-addon no-border no-padding">
+                                                    <span><button type="button" id="plupload-img" class="btn btn-danger plupload" data-input-id="c-img" data-mimetype="image/gif,image/jpeg,image/png,image/jpg,image/bmp" data-multiple="false" data-preview-id="p-img"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
+                                                    <span><button type="button" id="fachoose-img" class="btn btn-primary fachoose" data-input-id="c-img" data-mimetype="image/*" data-multiple="false"><i class="fa fa-list"></i> {:__('Choose')}</button></span>
+                                                </div>
+                                                <span class="msg-box n-right" for="c-img"></span>
+                                            </div>
+                                            <ul class="row list-inline plupload-preview" id="p-img"></ul>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+                        </td>
+                    </tr>
+                    </tbody>
+                </table>
+            </div>
+        </div>
+    </div>
+    <div class="form-group layer-footer">
+        <label class="control-label col-xs-12 col-sm-2"></label>
+        <div class="col-xs-12 col-sm-8">
+            <input type="hidden" name="row[data]" value="" />
+            <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
+            <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
+        </div>
+    </div>
+</form>

+ 300 - 0
application/admin/view/poster/edit.html

@@ -0,0 +1,300 @@
+<style type='text/css'>
+    #poster {
+        width:320px;height:504px;border:1px solid #ccc;position:relative
+    }
+    #poster .bg { position:absolute;width:100%;z-index:0}
+    #poster .drag[type=img] img,#poster .drag[type=thumb] img { width:100%;height:100%; }
+    #poster .drag { position: absolute; width:80px;height:80px; border:1px solid #000; }
+    #poster .drag[type=nickname] { width:80px;height:40px; font-size:16px; font-family: 黑体;}
+    #poster .drag img {position:absolute;z-index:0;width:100%; }
+    #poster .rRightDown,.rLeftDown,.rLeftUp,.rRightUp,.rRight,.rLeft,.rUp,.rDown{  position:absolute;  width:7px;  height:7px;  z-index:1;  font-size:0;  }
+    #poster .rRightDown,.rLeftDown,.rLeftUp,.rRightUp,.rRight,.rLeft,.rUp,.rDown{  background:#C00;  }
+    .rLeftDown,.rRightUp{cursor:ne-resize;}
+    .rRightDown,.rLeftUp{cursor:nw-resize;}
+    .rRight,.rLeft{cursor:e-resize;}
+    .rUp,.rDown{cursor:n-resize;}
+    .rLeftDown{left:-4px;bottom:-4px;}
+    .rRightUp{right:-4px;top:-4px;}
+    .rRightDown{right:-4px;bottom:-4px;}
+    .rLeftUp{left:-4px;top:-4px;}
+    .rRight{right:-4px;top:50%;margin-top:-4px;}
+    .rLeft{left:-4px;top:50%;margin-top:-4px;}
+    .rUp{top:-4px;left:50%;margin-left:-4px;}
+    .rDown{bottom:-4px;left:50%;margin-left:-4px;}
+    .context-menu-layer { z-index:9999;}
+    .context-menu-list { z-index:9999;}
+    .context-menu-list {
+        margin:0;
+        padding:0;
+        min-width: 120px;
+        max-width: 250px;
+        display: inline-block;
+        position: absolute;
+        list-style-type: none;
+        border: 1px solid #DDD;
+        background: #EEE;
+        -webkit-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5);
+        -moz-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5);
+        -ms-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5);
+        -o-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5);
+        box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5);
+        font-family: Verdana, Arial, Helvetica, sans-serif;
+        font-size: 11px;
+    }
+    .context-menu-item {
+        padding: 2px 2px 2px 24px;
+        background-color: #EEE;
+        position: relative;
+        -webkit-user-select: none;
+        -moz-user-select: -moz-none;
+        -ms-user-select: none;
+        user-select: none;
+    }
+    .context-menu-separator {
+        padding-bottom:0;
+        border-bottom: 1px solid #DDD;
+    }
+    .context-menu-item > label > input,
+    .context-menu-item > label > textarea {
+        -webkit-user-select: text;
+        -moz-user-select: text;
+        -ms-user-select: text;
+        user-select: text;
+    }
+    .context-menu-item.hover {
+        cursor: pointer;
+        background-color: #39F;
+    }
+    .context-menu-item.disabled {
+        color: #666;
+    }
+    .context-menu-input.hover,
+    .context-menu-item.disabled.hover {
+        cursor: default;
+        background-color: #EEE;
+    }
+    .context-menu-submenu:after {
+        content: ">";
+        color: #666;
+        position: absolute;
+        top: 0;
+        right: 3px;
+        z-index: 1;
+    }
+    .context-menu-item.icon { min-height: 18px; background-repeat: no-repeat; background-position: 4px 2px; }
+    /* vertically align inside labels */
+    .context-menu-input > label > * { vertical-align: top; }
+    /* position checkboxes and radios as icons */
+    .context-menu-input > label > input[type="checkbox"],
+    .context-menu-input > label > input[type="radio"] {
+        margin-left: -17px;
+    }
+    .context-menu-input > label > span {
+        margin-left: 5px;
+    }
+    .context-menu-input > label,
+    .context-menu-input > label > input[type="text"],
+    .context-menu-input > label > textarea,
+    .context-menu-input > label > select {
+        display: block;
+        width: 100%;
+        -webkit-box-sizing: border-box;
+        -moz-box-sizing: border-box;
+        -ms-box-sizing: border-box;
+        -o-box-sizing: border-box;
+        box-sizing: border-box;
+    }
+    .context-menu-input > label > textarea {
+        height: 100px;
+    }
+    .context-menu-item > .context-menu-list {
+        display: none;
+        right: -5px;
+        top: 5px;
+    }
+    .context-menu-item.hover > .context-menu-list {
+        display: block;
+    }
+    .context-menu-accesskey {
+        text-decoration: underline;
+    }
+    .n-msg{display: none!important;}
+</style>
+
+<form id="add-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action="">
+
+    <div class="tab-pane active" id="tab_design">
+        <div class="form-group">
+            <div class="col-sm-12">
+                <table style="width:100%;">
+                    <tbody>
+                    <tr>
+                        <td style="width:320px;padding:10px;" valign="top">
+                            <div id="poster">
+                                <img src='{$row.bg_image|htmlentities}' class='bg'/>
+                                {if !empty($row['data'])}
+                                {foreach name="row.data" item="d"}
+                                <div class="drag" type="{$d['type']}" index="{++$key}" style="zindex:{$key};left:{$d['left']};top:{$d['top']};width:{$d['width']};height:{$d['height']}" src="{$d['src']|default=''}" size="{$d['size']|default='15px'}" color="{$d['color']|default='#000'}" qr_table="{$d['qr_table']|default=''}" qr_field="{$d['qr_field']|default=''}" qr_relation="{$d['qr_relation']|default=''}">
+                                    {if $d['type']=='qr'}
+                                    <img src="/assets/addons/poster/images/qr.jpg" />
+                                    {elseif $d['type']=='head'}
+                                    <img src="/assets/addons/poster/images/head.jpg" />
+                                    {elseif $d['type']=='img'}
+                                    {if empty($d['src'])}
+                                    <img src="/assets/addons/poster/images/img.jpg" />
+                                    {else}
+                                    <img src="{$d['src']}" />
+                                    {/if}
+                                    {elseif $d['type']=='nickname'}
+                                    <div class="text" style="font-size:{$d.size|default='15px'};color:{$d.color|default='#000'}">昵称</div>
+                                    {/if}
+                                    <div class="rRightDown"></div>
+                                    <div class="rLeftDown"></div>
+                                    <div class="rRightUp"></div>
+                                    <div class="rLeftUp"></div>
+                                    <div class="rRight"></div>
+                                    <div class="rLeft"></div>
+                                    <div class="rUp"></div>
+                                    <div class="rDown"></div>
+                                </div>
+                                {/foreach}
+                                {/if}
+                            </div>
+                        </td>
+                        <td valign="top" style="padding:10px;">
+                            <div class="panel panel-default">
+                                <div class="panel-body">
+                                    <div class="form-group">
+                                        <label class="control-label col-xs-12 col-sm-3">{:__('Title')}:</label>
+                                        <div class="col-xs-12 col-sm-8">
+                                            <input id="c-title" data-rule="required" class="form-control" name="row[title]" type="text" placeholder="此项必须填写" value="{$row.title|htmlentities}">
+                                        </div>
+                                    </div>
+                                    <div class="form-group">
+                                        <label class="control-label col-xs-12 col-sm-3">{:__('Waittext')}:</label>
+                                        <div class="col-xs-12 col-sm-8">
+                                            <input id="c-waittext" class="form-control" name="row[waittext]" type="text" placeholder="例如:您的专属海报正在拼命生成中,请等待片刻..." value="{$row.waittext|htmlentities}">
+                                        </div>
+                                    </div>
+                                    <div class="form-group">
+                                        <label class="control-label col-xs-12 col-sm-3">{:__('Status')}:</label>
+                                        <div class="col-xs-12 col-sm-8">
+                                            <div class="radio">
+                                                {foreach name="statusList" item="vo"}
+                                                <label for="row[status]-{$key}"><input id="row[status]-{$key}" name="row[status]" type="radio" value="{$key}" {in name="key" value="$row.status"}checked{/in} /> {$vo}</label>
+                                                {/foreach}
+                                            </div>
+                                        </div>
+                                    </div>
+                                    <div class="form-group">
+                                        <label for="c-weigh" class="control-label col-xs-12 col-sm-3">{:__('Weigh')}:</label>
+                                        <div class="col-xs-12 col-sm-8">
+                                            <input id="c-weigh" class="form-control" name="row[weigh]" type="number" value="{$row.weigh}">
+                                        </div>
+                                    </div>
+                                    <div class="form-group">
+                                        <label class="control-label col-xs-12 col-sm-3">{:__('Bg_image')}:</label>
+                                        <div class="col-xs-12 col-sm-8">
+                                            <div class="input-group">
+                                                <input id="c-bg_image" class="form-control" size="50" name="row[bg_image]" type="text" value="{$row.bg_image|htmlentities}">
+                                                <div class="input-group-addon no-border no-padding">
+                                                    <span><button type="button" id="plupload-bg_image" class="btn btn-danger plupload" data-input-id="c-bg_image" data-mimetype="image/gif,image/jpeg,image/png,image/jpg,image/bmp" data-multiple="false" data-preview-id="p-bg_image"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
+                                                    <span><button type="button" id="fachoose-bg_image" class="btn btn-primary fachoose" data-input-id="c-bg_image" data-mimetype="image/*" data-multiple="false"><i class="fa fa-list"></i> {:__('Choose')}</button></span>
+                                                </div>
+                                                <span class="msg-box n-right" for="c-bg_image"></span>
+                                            </div>
+                                            <ul class="row list-inline plupload-preview" id="p-bg_image"></ul>
+                                        </div>
+                                        <label class="control-label col-xs-12 col-sm-3"></label>
+                                        <div class="col-xs-12 col-sm-8">
+                                            <span class="help-block">背景图片尺寸: 640 * 1008</span>
+                                        </div>
+                                    </div>
+
+                                    <div class="form-group">
+                                        <label class="control-label col-xs-12 col-sm-3">{:__('Poster elements')}:</label>
+                                        <div class="col-xs-12 col-sm-8">
+                                            <button class="btn btn-default btn-com" type="button" data-type="head">头像</button>
+                                            <button class="btn btn-default btn-com" type="button" data-type="nickname">昵称</button>
+                                            <button class="btn btn-default btn-com" type="button" data-type="qr">二维码</button>
+                                            <button class="btn btn-default btn-com" type="button" data-type="img">图片</button>
+                                        </div>
+                                    </div>
+
+                                    <div class="form-group nameset" style='display:none'>
+                                        <label class="control-label col-xs-12 col-sm-3">{:__('Nickname color')}:</label>
+                                        <div class="col-xs-12 col-sm-8">
+                                            <div class="row">
+                                                <div class="col-sm-8 col-xs-12">
+                                                    <input id="c-color" data-rule="required" class="form-control" name="color" type="text" value="" style="width: 50%">
+                                                    <span class="input-group-btn" style="display: unset;">
+                                                    <button type="button" class="btn btn-default btn-color colorpicker" style="padding:0;margin-left:1px;" title="选择颜色"><img src="__CDN__/assets/addons/poster/images/colorful.png" height="29" alt=""></button>
+                                                    <span class="msg-box n-right" for="c-color"></span>
+                                                </div>
+                                                <div class="col-sm-4"></div>
+                                            </div>
+                                        </div>
+                                    </div>
+
+                                    <div class="form-group nameset" style='display:none'>
+                                        <label class="control-label col-xs-12 col-sm-3">{:__('Nickname size')}:</label>
+                                        <div class="col-xs-12 col-sm-8">
+                                            <input type="text" id="namesize" data-rule="required" class="form-control namesize" placeholder="例如: 14,16" style="width: 90%;"/>
+                                            <div class="input-group-addon" style="display: contents;">px</div>
+                                        </div>
+                                    </div>
+
+
+                                    <div class="form-group qrset" style='display:none'>
+                                        <label class="control-label col-xs-12 col-sm-3">{:__('Qr_table')}:</label>
+                                        <div class="col-xs-12 col-sm-8">
+                                            {:build_select('qr_table',$tableList,'',['class'=>'form-control selectpicker']);}
+                                        </div>
+                                    </div>
+
+                                    <div class="form-group qrset" style='display:none'>
+                                        <label class="control-label col-xs-12 col-sm-3">{:__('Qr_field')}:</label>
+                                        <div class="col-xs-12 col-sm-8">
+                                            <select id="c-qr_field" data-rule="required" style="height:30px;" class="form-control selectpicker"></select>
+                                        </div>
+                                    </div>
+
+                                    <div class="form-group qrset" style='display:none'>
+                                        <label class="control-label col-xs-12 col-sm-3">{:__('Qr_relation')}:</label>
+                                        <div class="col-xs-12 col-sm-8">
+                                            <select id="c-qr_relation" data-rule="required" style="height:30px;" class="form-control selectpicker"></select>
+                                        </div>
+                                    </div>
+
+                                    <div class="form-group imgset" style='display:none'>
+                                        <label class="control-label col-xs-12 col-sm-3">{:__('Graphics Settings')}:</label>
+                                        <div class="col-xs-12 col-sm-8">
+                                            <div class="input-group">
+                                                <input id="c-img" data-rule="required" class="form-control" size="50" name="img" type="text" value="">
+                                                <div class="input-group-addon no-border no-padding">
+                                                    <span><button type="button" id="plupload-img" class="btn btn-danger plupload" data-input-id="c-img" data-mimetype="image/gif,image/jpeg,image/png,image/jpg,image/bmp" data-multiple="false" data-preview-id="p-img"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
+                                                    <span><button type="button" id="fachoose-img" class="btn btn-primary fachoose" data-input-id="c-img" data-mimetype="image/*" data-multiple="false"><i class="fa fa-list"></i> {:__('Choose')}</button></span>
+                                                </div>
+                                                <span class="msg-box n-right" for="c-img"></span>
+                                            </div>
+                                            <ul class="row list-inline plupload-preview" id="p-img"></ul>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+                        </td>
+                    </tr>
+                    </tbody>
+                </table>
+            </div>
+        </div>
+    </div>
+    <div class="form-group layer-footer">
+        <label class="control-label col-xs-12 col-sm-2"></label>
+        <div class="col-xs-12 col-sm-8">
+            <input type="hidden" name="row[data]" value="" />
+            <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
+            <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
+        </div>
+    </div>
+</form>

+ 42 - 0
application/admin/view/poster/index.html

@@ -0,0 +1,42 @@
+<div class="panel panel-default panel-intro">
+    
+    <div class="panel-heading">
+        {:build_heading(null,FALSE)}
+        <ul class="nav nav-tabs" data-field="status">
+            <li class="active"><a href="#t-all" data-value="" data-toggle="tab">{:__('All')}</a></li>
+            {foreach name="statusList" item="vo"}
+            <li><a href="#t-{$key}" data-value="{$key}" data-toggle="tab">{$vo}</a></li>
+            {/foreach}
+        </ul>
+    </div>
+    
+    <div class="panel-body">
+        <div id="myTabContent" class="tab-content">
+            <div class="tab-pane fade active in" id="one">
+                <div class="widget-body no-padding">
+                    <div id="toolbar" class="toolbar">
+                        <a href="javascript:;" class="btn btn-primary btn-refresh" title="{:__('Refresh')}" ><i class="fa fa-refresh"></i> </a>
+                        <a href="javascript:;" class="btn btn-success btn-add {:$auth->check('poster/add')?'':'hide'}" data-area='["1024px", "678px"]' title="{:__('Add')}" ><i class="fa fa-plus"></i> {:__('Add')}</a>
+                        <a href="javascript:;" class="btn btn-success btn-edit btn-disabled disabled {:$auth->check('poster/edit')?'':'hide'}" data-area='["1024px", "678px"]' title="{:__('Edit')}" ><i class="fa fa-pencil"></i> {:__('Edit')}</a>
+                        <a href="javascript:;" class="btn btn-danger btn-del btn-disabled disabled {:$auth->check('poster/del')?'':'hide'}" title="{:__('Delete')}" ><i class="fa fa-trash"></i> {:__('Delete')}</a>
+                        <a class="btn btn-danger btn-ajax" data-confirm="确认清除所有海报数据?" href="poster/clear"><i class="fa fa-trash"></i> 清除所有海报数据</a>
+
+                        <div class="dropdown btn-group {:$auth->check('poster/multi')?'':'hide'}">
+                            <a class="btn btn-primary btn-more dropdown-toggle btn-disabled disabled" data-toggle="dropdown"><i class="fa fa-cog"></i> {:__('More')}</a>
+                            <ul class="dropdown-menu text-left" role="menu">
+                                <li><a class="btn btn-link btn-multi btn-disabled disabled" href="javascript:;" data-params="status=normal"><i class="fa fa-eye"></i> {:__('Set to normal')}</a></li>
+                                <li><a class="btn btn-link btn-multi btn-disabled disabled" href="javascript:;" data-params="status=hidden"><i class="fa fa-eye-slash"></i> {:__('Set to hidden')}</a></li>
+                            </ul>
+                        </div>
+                    </div>
+                    <table id="table" class="table table-striped table-bordered table-hover table-nowrap"
+                           data-operate-edit="{:$auth->check('poster/edit')}" 
+                           data-operate-del="{:$auth->check('poster/del')}" 
+                           width="100%">
+                    </table>
+                </div>
+            </div>
+
+        </div>
+    </div>
+</div>

+ 16 - 0
application/admin/view/poster/posterrecord.html

@@ -0,0 +1,16 @@
+<div class="panel panel-default panel-intro">
+    
+    <div class="panel-body">
+        <div id="myTabContent" class="tab-content">
+            <div class="tab-pane fade active in" id="one">
+                <div class="widget-body no-padding">
+                    <div id="toolbar" class="toolbar">
+                        <a class="btn btn-danger btn-clear" data-confirm="确认清除当前海报数据?" href="javascript:;" data-url="poster/clear/poster_id/{$ids}"><i class="fa fa-trash"></i> 清除当前海报数据</a>
+                    </div>
+                    <table id="table" class="table table-striped table-bordered table-hover table-nowrap" width="100%"></table>
+                </div>
+            </div>
+
+        </div>
+    </div>
+</div>

+ 3 - 0
application/extra/addons.php

@@ -12,6 +12,9 @@ return [
         'action_begin' => [
             'epay',
         ],
+        'user_sidenav_after' => [
+            'poster',
+        ],
         'app_init' => [
             'qrcode',
         ],

+ 65 - 0
application/index/controller/Poster.php

@@ -0,0 +1,65 @@
+<?php
+
+namespace app\index\controller;
+
+use app\common\controller\Frontend;
+
+/**
+ * 我的海报
+ */
+class Poster extends Frontend
+{
+
+    protected $layout = 'default';
+    protected $noNeedRight = ["*"];
+    // Child模型对象
+    protected $model = null;
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new \app\admin\model\Poster;
+        $posterLog = new \app\admin\model\PosterLog;
+    }
+
+    /**
+     * 我的海报列表
+     *
+     * @param string  $token Token
+     * @param integer $page  页码
+     */
+    public function index()
+    {
+        $where = array(
+            'status' => 'normal'
+        );
+        $list = $this->model->where($where)->order('weigh DESC')->paginate(1);
+        foreach ($list as $row) {
+            $row->visible(['id', 'title', 'bg_image', 'data']);
+        }
+        $this->view->assign('list', $list);
+        $this->view->assign('title', "我的海报");
+        return $this->view->fetch();
+    }
+
+    /**
+     * 生成海报
+     * @author Created by Xing <464401240@qq.com>
+     */
+    public function getposter($poster_id = null)
+    {
+        $poster = $this->model->get($poster_id);
+        if (!$poster) {
+            $this->error(__('未找到海报' . $poster_id));
+        }
+
+        $image = new \addons\poster\library\Image();
+        $imgurl = $image->createPosterImage($poster, $this->auth->getUser());
+
+        if (!$imgurl) {
+            $this->error('生成海报出错');
+        }
+
+        $this->success('', '', '/' . $imgurl);
+    }
+}

+ 35 - 0
application/index/view/poster/index.html

@@ -0,0 +1,35 @@
+<style>
+    .poster-body img {
+        border: 1px solid #ddd;
+    }
+</style>
+<div id="content-container" class="container">
+    <div class="row">
+        <div class="col-md-3">
+            {include file="common/sidenav" /}
+        </div>
+        <div class="col-md-9">
+            <div class="row statistics">
+                <div class="col-xs-12">
+                    <div class="panel panel-default">
+                        <div class="panel-title">
+                            <h5>我的海报</h5>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <div class="panel panel-default orders" style="min-height: 300px">
+                <div class="panel-body mt-2" style="padding:0;">
+                    <div class="table-responsive mt-2 poster-body">
+                        {foreach name="list" item="vo"}
+                        <p style="text-align: center;padding: 20px;" class="poster" data-poster_id="{$vo.id}">{$vo.waittext}</p>
+                        {/foreach}
+                        <div class="pager">
+                            {$list->render()}
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>

BIN
public/assets/addons/poster/images/colorful.png


BIN
public/assets/addons/poster/images/default.png


BIN
public/assets/addons/poster/images/head.jpg


BIN
public/assets/addons/poster/images/img.jpg


BIN
public/assets/addons/poster/images/qr.jpg


+ 568 - 0
public/assets/addons/poster/js/designer.js

@@ -0,0 +1,568 @@
+var isIE = (document.all) ? true : false;
+
+var dobj = function (id) {
+	return "string" == typeof id ? document.getElementById(id) : id;
+};
+
+var Class = {
+	create: function() {
+		return function() { this.initialize.apply(this, arguments); }
+	}
+}
+
+var Extend = function(destination, source) {
+	for (var property in source) {
+		destination[property] = source[property];
+	}
+}
+
+var Bind = function(object, fun) {
+	return function() {
+		return fun.apply(object, arguments);
+	}
+}
+var BindAsEventListener = function(object, fun) {
+	var args = Array.prototype.slice.call(arguments).slice(2);
+	return function(event) {
+		return fun.apply(object, [event || window.event].concat(args));
+	}
+}
+
+var CurrentStyle = function(element){
+	return element.currentStyle || document.defaultView.getComputedStyle(element, null);
+}
+
+function addEventHandler(oTarget, sEventType, fnHandler) {
+	if (oTarget.addEventListener) {
+		oTarget.addEventListener(sEventType, fnHandler, false);
+	} else if (oTarget.attachEvent) {
+		oTarget.attachEvent("on" + sEventType, fnHandler);
+	} else {
+		oTarget["on" + sEventType] = fnHandler;
+	}
+};
+
+function removeEventHandler(oTarget, sEventType, fnHandler) {
+    if (oTarget.removeEventListener) {
+        oTarget.removeEventListener(sEventType, fnHandler, false);
+    } else if (oTarget.detachEvent) {
+        oTarget.detachEvent("on" + sEventType, fnHandler);
+    } else { 
+        oTarget["on" + sEventType] = null;
+    }
+};
+//缩放程序
+var Resize = Class.create();
+Resize.prototype = {
+  //缩放对象
+  initialize: function(obj, options) {
+	this._obj = obj.get(0);//缩放对象
+	
+	this._styleWidth = this._styleHeight = this._styleLeft = this._styleTop = 0;//样式参数
+	this._sideRight = this._sideDown = this._sideLeft = this._sideUp = 0;//坐标参数
+	this._fixLeft = this._fixTop = 0;//定位参数
+	this._scaleLeft = this._scaleTop = 0;//定位坐标
+	
+	this._mxSet = function(){};//范围设置程序
+	this._mxRightWidth = this._mxDownHeight = this._mxUpHeight = this._mxLeftWidth = 0;//范围参数
+	this._mxScaleWidth = this._mxScaleHeight = 0;//比例范围参数
+	
+	this._fun = function(){};//缩放执行程序
+	
+	//获取边框宽度
+	var _style = CurrentStyle(this._obj);
+	this._borderX = (parseInt(_style.borderLeftWidth) || 0) + (parseInt(_style.borderRightWidth) || 0);
+	this._borderY = (parseInt(_style.borderTopWidth) || 0) + (parseInt(_style.borderBottomWidth) || 0);
+	//事件对象(用于绑定移除事件)
+	this._fR = BindAsEventListener(this, this.Resize);
+	this._fS = Bind(this, this.Stop);
+	
+	this.SetOptions(options);
+	//范围限制
+	this.Max = !!this.options.Max;
+	this._mxContainer = $(this.options.mxContainer).get(0) || null;
+	this.mxLeft = Math.round(this.options.mxLeft);
+	this.mxRight = Math.round(this.options.mxRight);
+	this.mxTop = Math.round(this.options.mxTop);
+	this.mxBottom = Math.round(this.options.mxBottom);
+	//宽高限制
+	this.Min = !!this.options.Min;
+	this.minWidth = Math.round(this.options.minWidth);
+	this.minHeight = Math.round(this.options.minHeight);
+	//按比例缩放
+	this.Scale = !!this.options.Scale;
+	this.Ratio = Math.max(this.options.Ratio, 0);
+	
+	this.onResize = this.options.onResize;
+	
+	this._obj.style.position = "absolute";
+	!this._mxContainer || CurrentStyle(this._mxContainer).position == "relative" || (this._mxContainer.style.position = "relative");
+  },
+  //设置默认属性
+  SetOptions: function(options) {
+    this.options = {//默认值
+		Max:		false,//是否设置范围限制(为true时下面mx参数有用)
+		mxContainer:"",//指定限制在容器内
+		mxLeft:		0,//左边限制
+		mxRight:	9999,//右边限制
+		mxTop:		0,//上边限制
+		mxBottom:	9999,//下边限制
+		Min:		false,//是否最小宽高限制(为true时下面min参数有用)
+		minWidth:	50,//最小宽度
+		minHeight:	50,//最小高度
+		Scale:		false,//是否按比例缩放
+		Ratio:		0,//缩放比例(宽/高)
+		onResize:	function(){}//缩放时执行
+    };
+    Extend(this.options, options || {});
+  },
+  //设置触发对象
+  Set: function(resize, side) {
+	var fun;
+	if(!resize) return; 
+	//根据方向设置
+	switch (side.toLowerCase()) {
+	case "up" :
+		fun = this.Up;
+		break;
+	case "down" :
+		fun = this.Down;
+		break;
+	case "left" :
+		fun = this.Left;
+		break;
+	case "right" :
+		fun = this.Right;
+		break;
+	case "left-up" :
+		fun = this.LeftUp;
+		break;
+	case "right-up" :
+		fun = this.RightUp;
+		break;
+	case "left-down" :
+		fun = this.LeftDown;
+		break;
+	case "right-down" :
+	default :
+		fun = this.RightDown;
+	};
+	//设置触发对象
+	addEventHandler(resize.get(0), "mousedown", BindAsEventListener(this, this.Start, fun));
+  },
+  //准备缩放
+  Start: function(e, fun, touch) {	
+ 
+	//防止冒泡(跟拖放配合时设置)
+	e.stopPropagation ? e.stopPropagation() : (e.cancelBubble = true);
+	//设置执行程序
+	this._fun = fun;
+	//样式参数值
+	this._styleWidth = this._obj.clientWidth;
+	this._styleHeight = this._obj.clientHeight;
+	this._styleLeft = this._obj.offsetLeft;
+	this._styleTop = this._obj.offsetTop;
+	//四条边定位坐标
+	this._sideLeft = e.clientX - this._styleWidth;
+	this._sideRight = e.clientX + this._styleWidth;
+	this._sideUp = e.clientY - this._styleHeight;
+	this._sideDown = e.clientY + this._styleHeight;
+	//top和left定位参数
+	this._fixLeft = this._styleLeft + this._styleWidth;
+	this._fixTop = this._styleTop + this._styleHeight;
+	//缩放比例
+	if(this.Scale){
+		//设置比例
+		this.Ratio = Math.max(this.Ratio, 0) || this._styleWidth / this._styleHeight;
+		//left和top的定位坐标
+		this._scaleLeft = this._styleLeft + this._styleWidth / 2;
+		this._scaleTop = this._styleTop + this._styleHeight / 2;
+	};
+	//范围限制
+	if(this.Max){
+		//设置范围参数
+		var mxLeft = this.mxLeft, mxRight = this.mxRight, mxTop = this.mxTop, mxBottom = this.mxBottom;
+		//如果设置了容器,再修正范围参数
+		if(!!this._mxContainer){
+			mxLeft = Math.max(mxLeft, 0);
+			mxTop = Math.max(mxTop, 0);
+			mxRight = Math.min(mxRight, this._mxContainer.clientWidth);
+			mxBottom = Math.min(mxBottom, this._mxContainer.clientHeight);
+		};
+		//根据最小值再修正
+		mxRight = Math.max(mxRight, mxLeft + (this.Min ? this.minWidth : 0) + this._borderX);
+		mxBottom = Math.max(mxBottom, mxTop + (this.Min ? this.minHeight : 0) + this._borderY);
+		//由于转向时要重新设置所以写成function形式
+		this._mxSet = function(){
+			this._mxRightWidth = mxRight - this._styleLeft - this._borderX;
+			this._mxDownHeight = mxBottom - this._styleTop - this._borderY;
+			this._mxUpHeight = Math.max(this._fixTop - mxTop, this.Min ? this.minHeight : 0);
+			this._mxLeftWidth = Math.max(this._fixLeft - mxLeft, this.Min ? this.minWidth : 0);
+		};
+		this._mxSet();
+		//有缩放比例下的范围限制
+		if(this.Scale){
+			this._mxScaleWidth = Math.min(this._scaleLeft - mxLeft, mxRight - this._scaleLeft - this._borderX) * 2;
+			this._mxScaleHeight = Math.min(this._scaleTop - mxTop, mxBottom - this._scaleTop - this._borderY) * 2;
+		};
+	};
+	//mousemove时缩放 mouseup时停止
+	addEventHandler(document, "mousemove", this._fR);
+	addEventHandler(document, "mouseup", this._fS);
+	if(isIE){
+		addEventHandler(this._obj, "losecapture", this._fS);
+		this._obj.setCapture();
+	}else{
+		addEventHandler(window, "blur", this._fS);
+		e.preventDefault();
+	};
+  },
+  //缩放
+  Resize: function(e) {
+	//清除选择
+	window.getSelection ? window.getSelection().removeAllRanges() : document.selection.empty();
+	//执行缩放程序
+	this._fun(e);
+	//设置样式,变量必须大于等于0否则ie出错
+	with(this._obj.style){
+		width = this._styleWidth + "px"; height = this._styleHeight + "px";
+		top = this._styleTop + "px"; left = this._styleLeft + "px";
+	}
+	//附加程序
+	this.onResize();
+  },
+  //缩放程序
+  //上
+  Up: function(e) {
+	this.RepairY(this._sideDown - e.clientY, this._mxUpHeight);
+	this.RepairTop();
+	this.TurnDown(this.Down);
+  },
+  //下
+  Down: function(e) {
+	this.RepairY(e.clientY - this._sideUp, this._mxDownHeight);
+	this.TurnUp(this.Up);
+  },
+  //右
+  Right: function(e) {
+	this.RepairX(e.clientX - this._sideLeft, this._mxRightWidth);
+	this.TurnLeft(this.Left);
+  },
+  //左
+  Left: function(e) {
+	this.RepairX(this._sideRight - e.clientX, this._mxLeftWidth);
+	this.RepairLeft();
+	this.TurnRight(this.Right);
+  },
+  //右下
+  RightDown: function(e) {
+	this.RepairAngle(
+		e.clientX - this._sideLeft, this._mxRightWidth,
+		e.clientY - this._sideUp, this._mxDownHeight
+	);
+	this.TurnLeft(this.LeftDown) || this.Scale || this.TurnUp(this.RightUp);
+  },
+  //右上
+  RightUp: function(e) {
+	this.RepairAngle(
+		e.clientX - this._sideLeft, this._mxRightWidth,
+		this._sideDown - e.clientY, this._mxUpHeight
+	);
+	this.RepairTop();
+	this.TurnLeft(this.LeftUp) || this.Scale || this.TurnDown(this.RightDown);
+  },
+  //左下
+  LeftDown: function(e) {
+	this.RepairAngle(
+		this._sideRight - e.clientX, this._mxLeftWidth,
+		e.clientY - this._sideUp, this._mxDownHeight
+	);
+	this.RepairLeft();
+	this.TurnRight(this.RightDown) || this.Scale || this.TurnUp(this.LeftUp);
+  },
+  //左上
+  LeftUp: function(e) {
+	this.RepairAngle(
+		this._sideRight - e.clientX, this._mxLeftWidth,
+		this._sideDown - e.clientY, this._mxUpHeight
+	);
+	this.RepairTop(); this.RepairLeft();
+	this.TurnRight(this.RightUp) || this.Scale || this.TurnDown(this.LeftDown);
+  },
+  //修正程序
+  //水平方向
+  RepairX: function(iWidth, mxWidth) {
+	iWidth = this.RepairWidth(iWidth, mxWidth);
+	if(this.Scale){
+		var iHeight = this.RepairScaleHeight(iWidth);
+		if(this.Max && iHeight > this._mxScaleHeight){
+			iHeight = this._mxScaleHeight;
+			iWidth = this.RepairScaleWidth(iHeight);
+		}else if(this.Min && iHeight < this.minHeight){
+			var tWidth = this.RepairScaleWidth(this.minHeight);
+			if(tWidth < mxWidth){ iHeight = this.minHeight; iWidth = tWidth; }
+		}
+		this._styleHeight = iHeight;
+		this._styleTop = this._scaleTop - iHeight / 2;
+	}
+	this._styleWidth = iWidth;
+  },
+  //垂直方向
+  RepairY: function(iHeight, mxHeight) {
+	iHeight = this.RepairHeight(iHeight, mxHeight);
+	if(this.Scale){
+		var iWidth = this.RepairScaleWidth(iHeight);
+		if(this.Max && iWidth > this._mxScaleWidth){
+			iWidth = this._mxScaleWidth;
+			iHeight = this.RepairScaleHeight(iWidth);
+		}else if(this.Min && iWidth < this.minWidth){
+			var tHeight = this.RepairScaleHeight(this.minWidth);
+			if(tHeight < mxHeight){ iWidth = this.minWidth; iHeight = tHeight; }
+		}
+		this._styleWidth = iWidth;
+		this._styleLeft = this._scaleLeft - iWidth / 2;
+	}
+	this._styleHeight = iHeight;
+  },
+  //对角方向
+  RepairAngle: function(iWidth, mxWidth, iHeight, mxHeight) {
+	iWidth = this.RepairWidth(iWidth, mxWidth);	
+	if(this.Scale){
+		iHeight = this.RepairScaleHeight(iWidth);
+		if(this.Max && iHeight > mxHeight){
+			iHeight = mxHeight;
+			iWidth = this.RepairScaleWidth(iHeight);
+		}else if(this.Min && iHeight < this.minHeight){
+			var tWidth = this.RepairScaleWidth(this.minHeight);
+			if(tWidth < mxWidth){ iHeight = this.minHeight; iWidth = tWidth; }
+		}
+	}else{
+		iHeight = this.RepairHeight(iHeight, mxHeight);
+	}
+	this._styleWidth = iWidth;
+	this._styleHeight = iHeight;
+  },
+  //top
+  RepairTop: function() {
+	this._styleTop = this._fixTop - this._styleHeight;
+  },
+  //left
+  RepairLeft: function() {
+	this._styleLeft = this._fixLeft - this._styleWidth;
+  },
+  //height
+  RepairHeight: function(iHeight, mxHeight) {
+	iHeight = Math.min(this.Max ? mxHeight : iHeight, iHeight);
+	iHeight = Math.max(this.Min ? this.minHeight : iHeight, iHeight, 0);
+	return iHeight;
+  },
+  //width
+  RepairWidth: function(iWidth, mxWidth) {
+	iWidth = Math.min(this.Max ? mxWidth : iWidth, iWidth);
+	iWidth = Math.max(this.Min ? this.minWidth : iWidth, iWidth, 0);
+	return iWidth;
+  },
+  //比例高度
+  RepairScaleHeight: function(iWidth) {
+	return Math.max(Math.round((iWidth + this._borderX) / this.Ratio - this._borderY), 0);
+  },
+  //比例宽度
+  RepairScaleWidth: function(iHeight) {
+	return Math.max(Math.round((iHeight + this._borderY) * this.Ratio - this._borderX), 0);
+  },
+  //转向程序
+  //转右
+  TurnRight: function(fun) {
+	if(!(this.Min || this._styleWidth)){
+		this._fun = fun;
+		this._sideLeft = this._sideRight;
+		this.Max && this._mxSet();
+		return true;
+	}
+  },
+  //转左
+  TurnLeft: function(fun) {
+	if(!(this.Min || this._styleWidth)){
+		this._fun = fun;
+		this._sideRight = this._sideLeft;
+		this._fixLeft = this._styleLeft;
+		this.Max && this._mxSet();
+		return true;
+	}
+  },
+  //转上
+  TurnUp: function(fun) {
+	if(!(this.Min || this._styleHeight)){
+		this._fun = fun;
+		this._sideDown = this._sideUp;
+		this._fixTop = this._styleTop;
+		this.Max && this._mxSet();
+		return true;
+	}
+  },
+  //转下
+  TurnDown: function(fun) {
+	if(!(this.Min || this._styleHeight)){
+		this._fun = fun;
+		this._sideUp = this._sideDown;
+		this.Max && this._mxSet();
+		return true;
+	}
+  },
+  //停止缩放
+  Stop: function() {
+	removeEventHandler(document, "mousemove", this._fR);
+	removeEventHandler(document, "mouseup", this._fS);
+	if(isIE){
+		removeEventHandler(this._obj, "losecapture", this._fS);
+		this._obj.releaseCapture();
+	}else{
+		removeEventHandler(window, "blur", this._fS);
+	}
+  }
+};
+
+//拖放程序
+var Drag = Class.create();
+Drag.prototype = {
+  //拖放对象
+  initialize: function(drag, options) {
+	this.Drag = drag.get(0);//拖放对象
+	this._x = this._y = 0;//记录鼠标相对拖放对象的位置
+	this._marginLeft = this._marginTop = 0;//记录margin
+	//事件对象(用于绑定移除事件)
+	this._fM = BindAsEventListener(this, this.Move);
+	this._fS = Bind(this, this.Stop);
+	
+	this.SetOptions(options);
+	
+	this.Limit = !!this.options.Limit;
+	this.mxLeft = parseInt(this.options.mxLeft);
+	this.mxRight = parseInt(this.options.mxRight);
+	this.mxTop = parseInt(this.options.mxTop);
+	this.mxBottom = parseInt(this.options.mxBottom);
+	
+	this.LockX = !!this.options.LockX;
+	this.LockY = !!this.options.LockY;
+	this.Lock = !!this.options.Lock;
+	
+	this.onStart = this.options.onStart;
+	this.onMove = this.options.onMove;
+	this.onStop = this.options.onStop;
+	
+	this._Handle = $(this.options.Handle).get(0) || this.Drag;
+	this._mxContainer = $(this.options.mxContainer).get(0) || null;
+	
+	this.Drag.style.position = "absolute";
+	//透明
+	if(isIE && !!this.options.Transparent){
+		//填充拖放对象
+		with(this._Handle.appendChild(document.createElement("div")).style){
+			width = height = "100%"; backgroundColor = "#fff"; filter = "alpha(opacity:0)"; fontSize = 0;
+		}
+	}
+	//修正范围
+	this.Repair();
+	addEventHandler(this._Handle, "mousedown", BindAsEventListener(this, this.Start));
+  },
+  //设置默认属性
+  SetOptions: function(options) {
+	this.options = {//默认值
+		Handle:			"",//设置触发对象(不设置则使用拖放对象)
+		Limit:			false,//是否设置范围限制(为true时下面参数有用,可以是负数)
+		mxLeft:			0,//左边限制
+		mxRight:		9999,//右边限制
+		mxTop:			0,//上边限制
+		mxBottom:		9999,//下边限制
+		mxContainer:	"",//指定限制在容器内
+		LockX:			false,//是否锁定水平方向拖放
+		LockY:			false,//是否锁定垂直方向拖放
+		Lock:			false,//是否锁定
+		Transparent:	false,//是否透明
+		onStart:		function(){},//开始移动时执行
+		onMove:			function(){},//移动时执行
+		onStop:			function(){}//结束移动时执行
+	};
+	Extend(this.options, options || {});
+  },
+  //准备拖动
+  Start: function(oEvent) {
+	if(this.Lock){ return; }
+	this.Repair();
+	//记录鼠标相对拖放对象的位置
+	this._x = oEvent.clientX - this.Drag.offsetLeft;
+	this._y = oEvent.clientY - this.Drag.offsetTop;
+	//记录margin
+	this._marginLeft = parseInt(CurrentStyle(this.Drag).marginLeft) || 0;
+	this._marginTop = parseInt(CurrentStyle(this.Drag).marginTop) || 0;
+	//mousemove时移动 mouseup时停止
+	addEventHandler(document, "mousemove", this._fM);
+	addEventHandler(document, "mouseup", this._fS);
+	if(isIE){
+		//焦点丢失
+		addEventHandler(this._Handle, "losecapture", this._fS);
+		//设置鼠标捕获
+		this._Handle.setCapture();
+	}else{
+		//焦点丢失
+		addEventHandler(window, "blur", this._fS);
+		//阻止默认动作
+		oEvent.preventDefault();
+	};
+	//附加程序
+	this.onStart();
+  },
+  //修正范围
+  Repair: function() {
+	if(this.Limit){
+		//修正错误范围参数
+		this.mxRight = Math.max(this.mxRight, this.mxLeft + this.Drag.offsetWidth);
+		this.mxBottom = Math.max(this.mxBottom, this.mxTop + this.Drag.offsetHeight);
+		//如果有容器必须设置position为relative或absolute来相对或绝对定位,并在获取offset之前设置
+		!this._mxContainer || CurrentStyle(this._mxContainer).position == "relative" || CurrentStyle(this._mxContainer).position == "absolute" || (this._mxContainer.style.position = "relative");
+	}
+  },
+  //拖动
+  Move: function(oEvent) {
+	//判断是否锁定
+	if(this.Lock){ this.Stop(); return; };
+	//清除选择
+	window.getSelection ? window.getSelection().removeAllRanges() : document.selection.empty();
+	//设置移动参数
+	var iLeft = oEvent.clientX - this._x, iTop = oEvent.clientY - this._y;
+	//设置范围限制
+	if(this.Limit){
+		//设置范围参数
+		var mxLeft = this.mxLeft, mxRight = this.mxRight, mxTop = this.mxTop, mxBottom = this.mxBottom;
+		//如果设置了容器,再修正范围参数
+		if(!!this._mxContainer){
+			mxLeft = Math.max(mxLeft, 0);
+			mxTop = Math.max(mxTop, 0);
+			mxRight = Math.min(mxRight, this._mxContainer.clientWidth);
+			mxBottom = Math.min(mxBottom, this._mxContainer.clientHeight);
+		};
+		//修正移动参数
+		iLeft = Math.max(Math.min(iLeft, mxRight - this.Drag.offsetWidth), mxLeft);
+		iTop = Math.max(Math.min(iTop, mxBottom - this.Drag.offsetHeight), mxTop);
+	}
+	//设置位置,并修正margin
+	if(!this.LockX){ this.Drag.style.left = iLeft - this._marginLeft + "px"; }
+	if(!this.LockY){ this.Drag.style.top = iTop - this._marginTop + "px"; }
+	//附加程序
+	this.onMove();
+  },
+  //停止拖动
+  Stop: function() {
+	//移除事件
+	removeEventHandler(document, "mousemove", this._fM);
+	removeEventHandler(document, "mouseup", this._fS);
+	if(isIE){
+		removeEventHandler(this._Handle, "losecapture", this._fS);
+		this._Handle.releaseCapture();
+	}else{
+		removeEventHandler(window, "blur", this._fS);
+	};
+	//附加程序
+	this.onStop();
+  }
+};

File diff suppressed because it is too large
+ 24 - 0
public/assets/addons/poster/js/jquery.colorpicker.min.js


+ 1686 - 0
public/assets/addons/poster/js/jquery.contextMenu.js

@@ -0,0 +1,1686 @@
+/*!
+ * jQuery contextMenu - Plugin for simple contextMenu handling
+ *
+ * Version: 1.6.6
+ *
+ * Authors: Rodney Rehm, Addy Osmani (patches for FF)
+ * Web: http://medialize.github.com/jQuery-contextMenu/
+ *
+ * Licensed under
+ *   MIT License http://www.opensource.org/licenses/mit-license
+ *   GPL v3 http://opensource.org/licenses/GPL-3.0
+ *
+ */
+
+(function($, undefined){
+
+    // TODO: -
+    // ARIA stuff: menuitem, menuitemcheckbox und menuitemradio
+    // create <menu> structure if $.support[htmlCommand || htmlMenuitem] and !opt.disableNative
+
+// determine html5 compatibility
+    $.support.htmlMenuitem = ('HTMLMenuItemElement' in window);
+    $.support.htmlCommand = ('HTMLCommandElement' in window);
+    $.support.eventSelectstart = ("onselectstart" in document.documentElement);
+    /* // should the need arise, test for css user-select
+     $.support.cssUserSelect = (function(){
+     var t = false,
+     e = document.createElement('div');
+
+     $.each('Moz|Webkit|Khtml|O|ms|Icab|'.split('|'), function(i, prefix) {
+     var propCC = prefix + (prefix ? 'U' : 'u') + 'serSelect',
+     prop = (prefix ? ('-' + prefix.toLowerCase() + '-') : '') + 'user-select';
+
+     e.style.cssText = prop + ': text;';
+     if (e.style[propCC] == 'text') {
+     t = true;
+     return false;
+     }
+
+     return true;
+     });
+
+     return t;
+     })();
+     */
+
+    if (!$.ui || !$.ui.widget) {
+        // duck punch $.cleanData like jQueryUI does to get that remove event
+        // https://github.com/jquery/jquery-ui/blob/master/ui/jquery.ui.widget.js#L16-24
+        var _cleanData = $.cleanData;
+        $.cleanData = function( elems ) {
+            for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) {
+                try {
+                    $( elem ).triggerHandler( "remove" );
+                    // http://bugs.jquery.com/ticket/8235
+                } catch( e ) {}
+            }
+            _cleanData( elems );
+        };
+    }
+
+    var // currently active contextMenu trigger
+        $currentTrigger = null,
+    // is contextMenu initialized with at least one menu?
+        initialized = false,
+    // window handle
+        $win = $(window),
+    // number of registered menus
+        counter = 0,
+    // mapping selector to namespace
+        namespaces = {},
+    // mapping namespace to options
+        menus = {},
+    // custom command type handlers
+        types = {},
+    // default values
+        defaults = {
+            // selector of contextMenu trigger
+            selector: null,
+            // where to append the menu to
+            appendTo: null,
+            // method to trigger context menu ["right", "left", "hover"]
+            trigger: "right",
+            // hide menu when mouse leaves trigger / menu elements
+            autoHide: false,
+            // ms to wait before showing a hover-triggered context menu
+            delay: 200,
+            // flag denoting if a second trigger should simply move (true) or rebuild (false) an open menu
+            // as long as the trigger happened on one of the trigger-element's child nodes
+            reposition: true,
+            // determine position to show menu at
+            determinePosition: function($menu) {
+                // position to the lower middle of the trigger element
+                if ($.ui && $.ui.position) {
+                    // .position() is provided as a jQuery UI utility
+                    // (...and it won't work on hidden elements)
+                    $menu.css('display', 'block').position({
+                        my: "center top",
+                        at: "center bottom",
+                        of: this,
+                        offset: "0 5",
+                        collision: "fit"
+                    }).css('display', 'none');
+                } else {
+                    // determine contextMenu position
+                    var offset = this.offset();
+                    offset.top += this.outerHeight();
+                    offset.left += this.outerWidth() / 2 - $menu.outerWidth() / 2;
+                    $menu.css(offset);
+                }
+            },
+            // position menu
+            position: function(opt, x, y) {
+                var $this = this,
+                    offset;
+                // determine contextMenu position
+                if (!x && !y) {
+                    opt.determinePosition.call(this, opt.$menu);
+                    return;
+                } else if (x === "maintain" && y === "maintain") {
+                    // x and y must not be changed (after re-show on command click)
+                    offset = opt.$menu.position();
+                } else {
+                    // x and y are given (by mouse event)
+                    offset = {top: y, left: x};
+                }
+
+                // correct offset if viewport demands it
+                var bottom = $win.scrollTop() + $win.height(),
+                    right = $win.scrollLeft() + $win.width(),
+                    height = opt.$menu.height(),
+                    width = opt.$menu.width();
+
+                if (offset.top + height > bottom) {
+                    offset.top -= height;
+                }
+
+                if (offset.left + width > right) {
+                    offset.left -= width;
+                }
+
+                opt.$menu.css(offset);
+            },
+            // position the sub-menu
+            positionSubmenu: function($menu) {
+                if ($.ui && $.ui.position) {
+                    // .position() is provided as a jQuery UI utility
+                    // (...and it won't work on hidden elements)
+                    $menu.css('display', 'block').position({
+                        my: "left top",
+                        at: "right top",
+                        of: this,
+                        collision: "flipfit fit"
+                    }).css('display', '');
+                } else {
+                    // determine contextMenu position
+                    var offset = {
+                        top: 0,
+                        left: this.outerWidth()
+                    };
+                    $menu.css(offset);
+                }
+            },
+            // offset to add to zIndex
+            zIndex: 1,
+            // show hide animation settings
+            animation: {
+                duration: 50,
+                show: 'slideDown',
+                hide: 'slideUp'
+            },
+            // events
+            events: {
+                show: $.noop,
+                hide: $.noop
+            },
+            // default callback
+            callback: null,
+            // list of contextMenu items
+            items: {}
+        },
+    // mouse position for hover activation
+        hoveract = {
+            timer: null,
+            pageX: null,
+            pageY: null
+        },
+    // determine zIndex
+        zindex = function($t) {
+            var zin = 0,
+                $tt = $t;
+
+            while (true) {
+                zin = Math.max(zin, parseInt($tt.css('z-index'), 10) || 0);
+                $tt = $tt.parent();
+                if (!$tt || !$tt.length || "html body".indexOf($tt.prop('nodeName').toLowerCase()) > -1 ) {
+                    break;
+                }
+            }
+
+            return zin;
+        },
+    // event handlers
+        handle = {
+            // abort anything
+            abortevent: function(e){
+                e.preventDefault();
+                e.stopImmediatePropagation();
+            },
+
+            // contextmenu show dispatcher
+            contextmenu: function(e) {
+                var $this = $(this);
+
+                // disable actual context-menu
+                e.preventDefault();
+                e.stopImmediatePropagation();
+
+                // abort native-triggered events unless we're triggering on right click
+                if (e.data.trigger != 'right' && e.originalEvent) {
+                    return;
+                }
+
+                // abort event if menu is visible for this trigger
+                if ($this.hasClass('context-menu-active')) {
+                    return;
+                }
+
+                if (!$this.hasClass('context-menu-disabled')) {
+                    // theoretically need to fire a show event at <menu>
+                    // http://www.whatwg.org/specs/web-apps/current-work/multipage/interactive-elements.html#context-menus
+                    // var evt = jQuery.Event("show", { data: data, pageX: e.pageX, pageY: e.pageY, relatedTarget: this });
+                    // e.data.$menu.trigger(evt);
+
+                    $currentTrigger = $this;
+                    if (e.data.build) {
+                        var built = e.data.build($currentTrigger, e);
+                        // abort if build() returned false
+                        if (built === false) {
+                            return;
+                        }
+
+                        // dynamically build menu on invocation
+                        e.data = $.extend(true, {}, defaults, e.data, built || {});
+
+                        // abort if there are no items to display
+                        if (!e.data.items || $.isEmptyObject(e.data.items)) {
+                            // Note: jQuery captures and ignores errors from event handlers
+                            if (window.console) {
+                                (console.error || console.log)("No items specified to show in contextMenu");
+                            }
+
+                            throw new Error('No Items specified');
+                        }
+
+                        // backreference for custom command type creation
+                        e.data.$trigger = $currentTrigger;
+
+                        op.create(e.data);
+                    }
+                    // show menu
+                    op.show.call($this, e.data, e.pageX, e.pageY);
+                }
+            },
+            // contextMenu left-click trigger
+            click: function(e) {
+                e.preventDefault();
+                e.stopImmediatePropagation();
+                $(this).trigger($.Event("contextmenu", { data: e.data, pageX: e.pageX, pageY: e.pageY }));
+            },
+            // contextMenu right-click trigger
+            mousedown: function(e) {
+                // register mouse down
+                var $this = $(this);
+
+                // hide any previous menus
+                if ($currentTrigger && $currentTrigger.length && !$currentTrigger.is($this)) {
+                    $currentTrigger.data('contextMenu').$menu.trigger('contextmenu:hide');
+                }
+
+                // activate on right click
+                if (e.button == 2) {
+                    $currentTrigger = $this.data('contextMenuActive', true);
+                }
+            },
+            // contextMenu right-click trigger
+            mouseup: function(e) {
+                // show menu
+                var $this = $(this);
+                if ($this.data('contextMenuActive') && $currentTrigger && $currentTrigger.length && $currentTrigger.is($this) && !$this.hasClass('context-menu-disabled')) {
+                    e.preventDefault();
+                    e.stopImmediatePropagation();
+                    $currentTrigger = $this;
+                    $this.trigger($.Event("contextmenu", { data: e.data, pageX: e.pageX, pageY: e.pageY }));
+                }
+
+                $this.removeData('contextMenuActive');
+            },
+            // contextMenu hover trigger
+            mouseenter: function(e) {
+                var $this = $(this),
+                    $related = $(e.relatedTarget),
+                    $document = $(document);
+
+                // abort if we're coming from a menu
+                if ($related.is('.context-menu-list') || $related.closest('.context-menu-list').length) {
+                    return;
+                }
+
+                // abort if a menu is shown
+                if ($currentTrigger && $currentTrigger.length) {
+                    return;
+                }
+
+                hoveract.pageX = e.pageX;
+                hoveract.pageY = e.pageY;
+                hoveract.data = e.data;
+                $document.on('mousemove.contextMenuShow', handle.mousemove);
+                hoveract.timer = setTimeout(function() {
+                    hoveract.timer = null;
+                    $document.off('mousemove.contextMenuShow');
+                    $currentTrigger = $this;
+                    $this.trigger($.Event("contextmenu", { data: hoveract.data, pageX: hoveract.pageX, pageY: hoveract.pageY }));
+                }, e.data.delay );
+            },
+            // contextMenu hover trigger
+            mousemove: function(e) {
+                hoveract.pageX = e.pageX;
+                hoveract.pageY = e.pageY;
+            },
+            // contextMenu hover trigger
+            mouseleave: function(e) {
+                // abort if we're leaving for a menu
+                var $related = $(e.relatedTarget);
+                if ($related.is('.context-menu-list') || $related.closest('.context-menu-list').length) {
+                    return;
+                }
+
+                try {
+                    clearTimeout(hoveract.timer);
+                } catch(e) {}
+
+                hoveract.timer = null;
+            },
+
+            // click on layer to hide contextMenu
+            layerClick: function(e) {
+                var $this = $(this),
+                    root = $this.data('contextMenuRoot'),
+                    mouseup = false,
+                    button = e.button,
+                    x = e.pageX,
+                    y = e.pageY,
+                    target,
+                    offset,
+                    selectors;
+
+                e.preventDefault();
+                e.stopImmediatePropagation();
+
+                setTimeout(function() {
+                    var $window, hideshow, possibleTarget;
+                    var triggerAction = ((root.trigger == 'left' && button === 0) || (root.trigger == 'right' && button === 2));
+
+                    // find the element that would've been clicked, wasn't the layer in the way
+                    if (document.elementFromPoint) {
+                        root.$layer.hide();
+                        target = document.elementFromPoint(x - $win.scrollLeft(), y - $win.scrollTop());
+                        root.$layer.show();
+                    }
+
+                    if (root.reposition && triggerAction) {
+                        if (document.elementFromPoint) {
+                            if (root.$trigger.is(target) || root.$trigger.has(target).length) {
+                                root.position.call(root.$trigger, root, x, y);
+                                return;
+                            }
+                        } else {
+                            offset = root.$trigger.offset();
+                            $window = $(window);
+                            // while this looks kinda awful, it's the best way to avoid
+                            // unnecessarily calculating any positions
+                            offset.top += $window.scrollTop();
+                            if (offset.top <= e.pageY) {
+                                offset.left += $window.scrollLeft();
+                                if (offset.left <= e.pageX) {
+                                    offset.bottom = offset.top + root.$trigger.outerHeight();
+                                    if (offset.bottom >= e.pageY) {
+                                        offset.right = offset.left + root.$trigger.outerWidth();
+                                        if (offset.right >= e.pageX) {
+                                            // reposition
+                                            root.position.call(root.$trigger, root, x, y);
+                                            return;
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+
+                    if (target && triggerAction) {
+                        root.$trigger.one('contextmenu:hidden', function() {
+                            $(target).contextMenu({x: x, y: y});
+                        });
+                    }
+
+                    root.$menu.trigger('contextmenu:hide');
+                }, 50);
+            },
+            // key handled :hover
+            keyStop: function(e, opt) {
+                if (!opt.isInput) {
+                    e.preventDefault();
+                }
+
+                e.stopPropagation();
+            },
+            key: function(e) {
+                var opt = $currentTrigger.data('contextMenu') || {};
+
+                switch (e.keyCode) {
+                    case 9:
+                    case 38: // up
+                        handle.keyStop(e, opt);
+                        // if keyCode is [38 (up)] or [9 (tab) with shift]
+                        if (opt.isInput) {
+                            if (e.keyCode == 9 && e.shiftKey) {
+                                e.preventDefault();
+                                opt.$selected && opt.$selected.find('input, textarea, select').blur();
+                                opt.$menu.trigger('prevcommand');
+                                return;
+                            } else if (e.keyCode == 38 && opt.$selected.find('input, textarea, select').prop('type') == 'checkbox') {
+                                // checkboxes don't capture this key
+                                e.preventDefault();
+                                return;
+                            }
+                        } else if (e.keyCode != 9 || e.shiftKey) {
+                            opt.$menu.trigger('prevcommand');
+                            return;
+                        }
+                    // omitting break;
+
+                    // case 9: // tab - reached through omitted break;
+                    case 40: // down
+                        handle.keyStop(e, opt);
+                        if (opt.isInput) {
+                            if (e.keyCode == 9) {
+                                e.preventDefault();
+                                opt.$selected && opt.$selected.find('input, textarea, select').blur();
+                                opt.$menu.trigger('nextcommand');
+                                return;
+                            } else if (e.keyCode == 40 && opt.$selected.find('input, textarea, select').prop('type') == 'checkbox') {
+                                // checkboxes don't capture this key
+                                e.preventDefault();
+                                return;
+                            }
+                        } else {
+                            opt.$menu.trigger('nextcommand');
+                            return;
+                        }
+                        break;
+
+                    case 37: // left
+                        handle.keyStop(e, opt);
+                        if (opt.isInput || !opt.$selected || !opt.$selected.length) {
+                            break;
+                        }
+
+                        if (!opt.$selected.parent().hasClass('context-menu-root')) {
+                            var $parent = opt.$selected.parent().parent();
+                            opt.$selected.trigger('contextmenu:blur');
+                            opt.$selected = $parent;
+                            return;
+                        }
+                        break;
+
+                    case 39: // right
+                        handle.keyStop(e, opt);
+                        if (opt.isInput || !opt.$selected || !opt.$selected.length) {
+                            break;
+                        }
+
+                        var itemdata = opt.$selected.data('contextMenu') || {};
+                        if (itemdata.$menu && opt.$selected.hasClass('context-menu-submenu')) {
+                            opt.$selected = null;
+                            itemdata.$selected = null;
+                            itemdata.$menu.trigger('nextcommand');
+                            return;
+                        }
+                        break;
+
+                    case 35: // end
+                    case 36: // home
+                        if (opt.$selected && opt.$selected.find('input, textarea, select').length) {
+                            return;
+                        } else {
+                            (opt.$selected && opt.$selected.parent() || opt.$menu)
+                                .children(':not(.disabled, .not-selectable)')[e.keyCode == 36 ? 'first' : 'last']()
+                                .trigger('contextmenu:focus');
+                            e.preventDefault();
+                            return;
+                        }
+                        break;
+
+                    case 13: // enter
+                        handle.keyStop(e, opt);
+                        if (opt.isInput) {
+                            if (opt.$selected && !opt.$selected.is('textarea, select')) {
+                                e.preventDefault();
+                                return;
+                            }
+                            break;
+                        }
+                        opt.$selected && opt.$selected.trigger('mouseup');
+                        return;
+
+                    case 32: // space
+                    case 33: // page up
+                    case 34: // page down
+                        // prevent browser from scrolling down while menu is visible
+                        handle.keyStop(e, opt);
+                        return;
+
+                    case 27: // esc
+                        handle.keyStop(e, opt);
+                        opt.$menu.trigger('contextmenu:hide');
+                        return;
+
+                    default: // 0-9, a-z
+                        var k = (String.fromCharCode(e.keyCode)).toUpperCase();
+                        if (opt.accesskeys[k]) {
+                            // according to the specs accesskeys must be invoked immediately
+                            opt.accesskeys[k].$node.trigger(opt.accesskeys[k].$menu
+                                ? 'contextmenu:focus'
+                                : 'mouseup'
+                            );
+                            return;
+                        }
+                        break;
+                }
+                // pass event to selected item,
+                // stop propagation to avoid endless recursion
+                e.stopPropagation();
+                opt.$selected && opt.$selected.trigger(e);
+            },
+
+            // select previous possible command in menu
+            prevItem: function(e) {
+                e.stopPropagation();
+                var opt = $(this).data('contextMenu') || {};
+
+                // obtain currently selected menu
+                if (opt.$selected) {
+                    var $s = opt.$selected;
+                    opt = opt.$selected.parent().data('contextMenu') || {};
+                    opt.$selected = $s;
+                }
+
+                var $children = opt.$menu.children(),
+                    $prev = !opt.$selected || !opt.$selected.prev().length ? $children.last() : opt.$selected.prev(),
+                    $round = $prev;
+
+                // skip disabled
+                while ($prev.hasClass('disabled') || $prev.hasClass('not-selectable')) {
+                    if ($prev.prev().length) {
+                        $prev = $prev.prev();
+                    } else {
+                        $prev = $children.last();
+                    }
+                    if ($prev.is($round)) {
+                        // break endless loop
+                        return;
+                    }
+                }
+
+                // leave current
+                if (opt.$selected) {
+                    handle.itemMouseleave.call(opt.$selected.get(0), e);
+                }
+
+                // activate next
+                handle.itemMouseenter.call($prev.get(0), e);
+
+                // focus input
+                var $input = $prev.find('input, textarea, select');
+                if ($input.length) {
+                    $input.focus();
+                }
+            },
+            // select next possible command in menu
+            nextItem: function(e) {
+                e.stopPropagation();
+                var opt = $(this).data('contextMenu') || {};
+
+                // obtain currently selected menu
+                if (opt.$selected) {
+                    var $s = opt.$selected;
+                    opt = opt.$selected.parent().data('contextMenu') || {};
+                    opt.$selected = $s;
+                }
+
+                var $children = opt.$menu.children(),
+                    $next = !opt.$selected || !opt.$selected.next().length ? $children.first() : opt.$selected.next(),
+                    $round = $next;
+
+                // skip disabled
+                while ($next.hasClass('disabled') || $next.hasClass('not-selectable')) {
+                    if ($next.next().length) {
+                        $next = $next.next();
+                    } else {
+                        $next = $children.first();
+                    }
+                    if ($next.is($round)) {
+                        // break endless loop
+                        return;
+                    }
+                }
+
+                // leave current
+                if (opt.$selected) {
+                    handle.itemMouseleave.call(opt.$selected.get(0), e);
+                }
+
+                // activate next
+                handle.itemMouseenter.call($next.get(0), e);
+
+                // focus input
+                var $input = $next.find('input, textarea, select');
+                if ($input.length) {
+                    $input.focus();
+                }
+            },
+
+            // flag that we're inside an input so the key handler can act accordingly
+            focusInput: function(e) {
+                var $this = $(this).closest('.context-menu-item'),
+                    data = $this.data(),
+                    opt = data.contextMenu,
+                    root = data.contextMenuRoot;
+
+                root.$selected = opt.$selected = $this;
+                root.isInput = opt.isInput = true;
+            },
+            // flag that we're inside an input so the key handler can act accordingly
+            blurInput: function(e) {
+                var $this = $(this).closest('.context-menu-item'),
+                    data = $this.data(),
+                    opt = data.contextMenu,
+                    root = data.contextMenuRoot;
+
+                root.isInput = opt.isInput = false;
+            },
+
+            // :hover on menu
+            menuMouseenter: function(e) {
+                var root = $(this).data().contextMenuRoot;
+                root.hovering = true;
+            },
+            // :hover on menu
+            menuMouseleave: function(e) {
+                var root = $(this).data().contextMenuRoot;
+                if (root.$layer && root.$layer.is(e.relatedTarget)) {
+                    root.hovering = false;
+                }
+            },
+
+            // :hover done manually so key handling is possible
+            itemMouseenter: function(e) {
+                var $this = $(this),
+                    data = $this.data(),
+                    opt = data.contextMenu,
+                    root = data.contextMenuRoot;
+
+                root.hovering = true;
+
+                // abort if we're re-entering
+                if (e && root.$layer && root.$layer.is(e.relatedTarget)) {
+                    e.preventDefault();
+                    e.stopImmediatePropagation();
+                }
+
+                // make sure only one item is selected
+                (opt.$menu ? opt : root).$menu
+                    .children('.hover').trigger('contextmenu:blur');
+
+                if ($this.hasClass('disabled') || $this.hasClass('not-selectable')) {
+                    opt.$selected = null;
+                    return;
+                }
+
+                $this.trigger('contextmenu:focus');
+            },
+            // :hover done manually so key handling is possible
+            itemMouseleave: function(e) {
+                var $this = $(this),
+                    data = $this.data(),
+                    opt = data.contextMenu,
+                    root = data.contextMenuRoot;
+
+                if (root !== opt && root.$layer && root.$layer.is(e.relatedTarget)) {
+                    root.$selected && root.$selected.trigger('contextmenu:blur');
+                    e.preventDefault();
+                    e.stopImmediatePropagation();
+                    root.$selected = opt.$selected = opt.$node;
+                    return;
+                }
+
+                $this.trigger('contextmenu:blur');
+            },
+            // contextMenu item click
+            itemClick: function(e) {
+                var $this = $(this),
+                    data = $this.data(),
+                    opt = data.contextMenu,
+                    root = data.contextMenuRoot,
+                    key = data.contextMenuKey,
+                    callback;
+
+                // abort if the key is unknown or disabled or is a menu
+                if (!opt.items[key] || $this.is('.disabled, .context-menu-submenu, .context-menu-separator, .not-selectable')) {
+                    return;
+                }
+
+                e.preventDefault();
+                e.stopImmediatePropagation();
+
+                if ($.isFunction(root.callbacks[key]) && Object.prototype.hasOwnProperty.call(root.callbacks, key)) {
+                    // item-specific callback
+                    callback = root.callbacks[key];
+                } else if ($.isFunction(root.callback)) {
+                    // default callback
+                    callback = root.callback;
+                } else {
+                    // no callback, no action
+                    return;
+                }
+
+                // hide menu if callback doesn't stop that
+                if (callback.call(root.$trigger, key, root) !== false) {
+                    root.$menu.trigger('contextmenu:hide');
+                } else if (root.$menu.parent().length) {
+                    op.update.call(root.$trigger, root);
+                }
+            },
+            // ignore click events on input elements
+            inputClick: function(e) {
+                e.stopImmediatePropagation();
+            },
+
+            // hide <menu>
+            hideMenu: function(e, data) {
+                var root = $(this).data('contextMenuRoot');
+                op.hide.call(root.$trigger, root, data && data.force);
+            },
+            // focus <command>
+            focusItem: function(e) {
+                e.stopPropagation();
+                var $this = $(this),
+                    data = $this.data(),
+                    opt = data.contextMenu,
+                    root = data.contextMenuRoot;
+
+                $this.addClass('hover')
+                    .siblings('.hover').trigger('contextmenu:blur');
+
+                // remember selected
+                opt.$selected = root.$selected = $this;
+
+                // position sub-menu - do after show so dumb $.ui.position can keep up
+                if (opt.$node) {
+                    root.positionSubmenu.call(opt.$node, opt.$menu);
+                }
+            },
+            // blur <command>
+            blurItem: function(e) {
+                e.stopPropagation();
+                var $this = $(this),
+                    data = $this.data(),
+                    opt = data.contextMenu,
+                    root = data.contextMenuRoot;
+
+                $this.removeClass('hover');
+                opt.$selected = null;
+            }
+        },
+    // operations
+        op = {
+            show: function(opt, x, y) {
+                var $trigger = $(this),
+                    offset,
+                    css = {};
+
+                // hide any open menus
+                $('#context-menu-layer').trigger('mousedown');
+
+                // backreference for callbacks
+                opt.$trigger = $trigger;
+
+                // show event
+                if (opt.events.show.call($trigger, opt) === false) {
+                    $currentTrigger = null;
+                    return;
+                }
+
+                // create or update context menu
+                op.update.call($trigger, opt);
+
+                // position menu
+                opt.position.call($trigger, opt, x, y);
+
+                // make sure we're in front
+                if (opt.zIndex) {
+                    css.zIndex = zindex($trigger) + opt.zIndex;
+                }
+
+                // add layer
+                op.layer.call(opt.$menu, opt, css.zIndex);
+
+                // adjust sub-menu zIndexes
+                opt.$menu.find('ul').css('zIndex', css.zIndex + 1);
+
+                // position and show context menu
+                opt.$menu.css( css )[opt.animation.show](opt.animation.duration, function() {
+                    $trigger.trigger('contextmenu:visible');
+                });
+                // make options available and set state
+                $trigger
+                    .data('contextMenu', opt)
+                    .addClass("context-menu-active");
+
+                // register key handler
+                $(document).off('keydown.contextMenu').on('keydown.contextMenu', handle.key);
+                // register autoHide handler
+                if (opt.autoHide) {
+                    // mouse position handler
+                    $(document).on('mousemove.contextMenuAutoHide', function(e) {
+                        // need to capture the offset on mousemove,
+                        // since the page might've been scrolled since activation
+                        var pos = $trigger.offset();
+                        pos.right = pos.left + $trigger.outerWidth();
+                        pos.bottom = pos.top + $trigger.outerHeight();
+
+                        if (opt.$layer && !opt.hovering && (!(e.pageX >= pos.left && e.pageX <= pos.right) || !(e.pageY >= pos.top && e.pageY <= pos.bottom))) {
+                            // if mouse in menu...
+                            opt.$menu.trigger('contextmenu:hide');
+                        }
+                    });
+                }
+            },
+            hide: function(opt, force) {
+                var $trigger = $(this);
+                if (!opt) {
+                    opt = $trigger.data('contextMenu') || {};
+                }
+
+                // hide event
+                if (!force && opt.events && opt.events.hide.call($trigger, opt) === false) {
+                    return;
+                }
+
+                // remove options and revert state
+                $trigger
+                    .removeData('contextMenu')
+                    .removeClass("context-menu-active");
+
+                if (opt.$layer) {
+                    // keep layer for a bit so the contextmenu event can be aborted properly by opera
+                    setTimeout((function($layer) {
+                        return function(){
+                            $layer.remove();
+                        };
+                    })(opt.$layer), 10);
+
+                    try {
+                        delete opt.$layer;
+                    } catch(e) {
+                        opt.$layer = null;
+                    }
+                }
+
+                // remove handle
+                $currentTrigger = null;
+                // remove selected
+                opt.$menu.find('.hover').trigger('contextmenu:blur');
+                opt.$selected = null;
+                // unregister key and mouse handlers
+                //$(document).off('.contextMenuAutoHide keydown.contextMenu'); // http://bugs.jquery.com/ticket/10705
+                $(document).off('.contextMenuAutoHide').off('keydown.contextMenu');
+                // hide menu
+                opt.$menu && opt.$menu[opt.animation.hide](opt.animation.duration, function (){
+                    // tear down dynamically built menu after animation is completed.
+                    if (opt.build) {
+                        opt.$menu.remove();
+                        $.each(opt, function(key, value) {
+                            switch (key) {
+                                case 'ns':
+                                case 'selector':
+                                case 'build':
+                                case 'trigger':
+                                    return true;
+
+                                default:
+                                    opt[key] = undefined;
+                                    try {
+                                        delete opt[key];
+                                    } catch (e) {}
+                                    return true;
+                            }
+                        });
+                    }
+
+                    setTimeout(function() {
+                        $trigger.trigger('contextmenu:hidden');
+                    }, 10);
+                });
+            },
+            create: function(opt, root) {
+                if (root === undefined) {
+                    root = opt;
+                }
+                // create contextMenu
+                opt.$menu = $('<ul class="context-menu-list"></ul>').addClass(opt.className || "").data({
+                    'contextMenu': opt,
+                    'contextMenuRoot': root
+                });
+
+                $.each(['callbacks', 'commands', 'inputs'], function(i,k){
+                    opt[k] = {};
+                    if (!root[k]) {
+                        root[k] = {};
+                    }
+                });
+
+                root.accesskeys || (root.accesskeys = {});
+
+                // create contextMenu items
+                $.each(opt.items, function(key, item){
+                    var $t = $('<li class="context-menu-item"></li>').addClass(item.className || ""),
+                        $label = null,
+                        $input = null;
+
+                    // iOS needs to see a click-event bound to an element to actually
+                    // have the TouchEvents infrastructure trigger the click event
+                    $t.on('click', $.noop);
+
+                    item.$node = $t.data({
+                        'contextMenu': opt,
+                        'contextMenuRoot': root,
+                        'contextMenuKey': key
+                    });
+
+                    // register accesskey
+                    // NOTE: the accesskey attribute should be applicable to any element, but Safari5 and Chrome13 still can't do that
+                    if (item.accesskey) {
+                        var aks = splitAccesskey(item.accesskey);
+                        for (var i=0, ak; ak = aks[i]; i++) {
+                            if (!root.accesskeys[ak]) {
+                                root.accesskeys[ak] = item;
+                                item._name = item.name.replace(new RegExp('(' + ak + ')', 'i'), '<span class="context-menu-accesskey">$1</span>');
+                                break;
+                            }
+                        }
+                    }
+
+                    if (typeof item == "string") {
+                        $t.addClass('context-menu-separator not-selectable');
+                    } else if (item.type && types[item.type]) {
+                        // run custom type handler
+                        types[item.type].call($t, item, opt, root);
+                        // register commands
+                        $.each([opt, root], function(i,k){
+                            k.commands[key] = item;
+                            if ($.isFunction(item.callback)) {
+                                k.callbacks[key] = item.callback;
+                            }
+                        });
+                    } else {
+                        // add label for input
+                        if (item.type == 'html') {
+                            $t.addClass('context-menu-html not-selectable');
+                        } else if (item.type) {
+                            $label = $('<label></label>').appendTo($t);
+                            $('<span></span>').html(item._name || item.name).appendTo($label);
+                            $t.addClass('context-menu-input');
+                            opt.hasTypes = true;
+                            $.each([opt, root], function(i,k){
+                                k.commands[key] = item;
+                                k.inputs[key] = item;
+                            });
+                        } else if (item.items) {
+                            item.type = 'sub';
+                        }
+
+                        switch (item.type) {
+                            case 'text':
+                                $input = $('<input type="text" value="1" name="" value="">')
+                                    .attr('name', 'context-menu-input-' + key)
+                                    .val(item.value || "")
+                                    .appendTo($label);
+                                break;
+
+                            case 'textarea':
+                                $input = $('<textarea name=""></textarea>')
+                                    .attr('name', 'context-menu-input-' + key)
+                                    .val(item.value || "")
+                                    .appendTo($label);
+
+                                if (item.height) {
+                                    $input.height(item.height);
+                                }
+                                break;
+
+                            case 'checkbox':
+                                $input = $('<input type="checkbox" value="1" name="" value="">')
+                                    .attr('name', 'context-menu-input-' + key)
+                                    .val(item.value || "")
+                                    .prop("checked", !!item.selected)
+                                    .prependTo($label);
+                                break;
+
+                            case 'radio':
+                                $input = $('<input type="radio" value="1" name="" value="">')
+                                    .attr('name', 'context-menu-input-' + item.radio)
+                                    .val(item.value || "")
+                                    .prop("checked", !!item.selected)
+                                    .prependTo($label);
+                                break;
+
+                            case 'select':
+                                $input = $('<select name="">')
+                                    .attr('name', 'context-menu-input-' + key)
+                                    .appendTo($label);
+                                if (item.options) {
+                                    $.each(item.options, function(value, text) {
+                                        $('<option></option>').val(value).text(text).appendTo($input);
+                                    });
+                                    $input.val(item.selected);
+                                }
+                                break;
+
+                            case 'sub':
+                                // FIXME: shouldn't this .html() be a .text()?
+                                $('<span></span>').html(item._name || item.name).appendTo($t);
+                                item.appendTo = item.$node;
+                                op.create(item, root);
+                                $t.data('contextMenu', item).addClass('context-menu-submenu');
+                                item.callback = null;
+                                break;
+
+                            case 'html':
+                                $(item.html).appendTo($t);
+                                break;
+
+                            default:
+                                $.each([opt, root], function(i,k){
+                                    k.commands[key] = item;
+                                    if ($.isFunction(item.callback)) {
+                                        k.callbacks[key] = item.callback;
+                                    }
+                                });
+                                // FIXME: shouldn't this .html() be a .text()?
+                                $('<span></span>').html(item._name || item.name || "").appendTo($t);
+                                break;
+                        }
+
+                        // disable key listener in <input>
+                        if (item.type && item.type != 'sub' && item.type != 'html') {
+                            $input
+                                .on('focus', handle.focusInput)
+                                .on('blur', handle.blurInput);
+
+                            if (item.events) {
+                                $input.on(item.events, opt);
+                            }
+                        }
+
+                        // add icons
+                        if (item.icon) {
+                            $t.addClass("icon icon-" + item.icon);
+                        }
+                    }
+
+                    // cache contained elements
+                    item.$input = $input;
+                    item.$label = $label;
+
+                    // attach item to menu
+                    $t.appendTo(opt.$menu);
+
+                    // Disable text selection
+                    if (!opt.hasTypes && $.support.eventSelectstart) {
+                        // browsers support user-select: none,
+                        // IE has a special event for text-selection
+                        // browsers supporting neither will not be preventing text-selection
+                        $t.on('selectstart.disableTextSelect', handle.abortevent);
+                    }
+                });
+                // attach contextMenu to <body> (to bypass any possible overflow:hidden issues on parents of the trigger element)
+                if (!opt.$node) {
+                    opt.$menu.css('display', 'none').addClass('context-menu-root');
+                }
+                opt.$menu.appendTo(opt.appendTo || document.body);
+            },
+            resize: function($menu, nested) {
+                // determine widths of submenus, as CSS won't grow them automatically
+                // position:absolute within position:absolute; min-width:100; max-width:200; results in width: 100;
+                // kinda sucks hard...
+
+                // determine width of absolutely positioned element
+                $menu.css({position: 'absolute', display: 'block'});
+                // don't apply yet, because that would break nested elements' widths
+                // add a pixel to circumvent word-break issue in IE9 - #80
+                $menu.data('width', Math.ceil($menu.width()) + 1);
+                // reset styles so they allow nested elements to grow/shrink naturally
+                $menu.css({
+                    position: 'static',
+                    minWidth: '0px',
+                    maxWidth: '100000px'
+                });
+                // identify width of nested menus
+                $menu.find('> li > ul').each(function() {
+                    op.resize($(this), true);
+                });
+                // reset and apply changes in the end because nested
+                // elements' widths wouldn't be calculatable otherwise
+                if (!nested) {
+                    $menu.find('ul').andSelf().css({
+                        position: '',
+                        display: '',
+                        minWidth: '',
+                        maxWidth: ''
+                    }).width(function() {
+                        return $(this).data('width');
+                    });
+                }
+            },
+            update: function(opt, root) {
+                var $trigger = this;
+                if (root === undefined) {
+                    root = opt;
+                    op.resize(opt.$menu);
+                }
+                // re-check disabled for each item
+                opt.$menu.children().each(function(){
+                    var $item = $(this),
+                        key = $item.data('contextMenuKey'),
+                        item = opt.items[key],
+                        disabled = ($.isFunction(item.disabled) && item.disabled.call($trigger, key, root)) || item.disabled === true;
+
+                    // dis- / enable item
+                    $item[disabled ? 'addClass' : 'removeClass']('disabled');
+
+                    if (item.type) {
+                        // dis- / enable input elements
+                        $item.find('input, select, textarea').prop('disabled', disabled);
+
+                        // update input states
+                        switch (item.type) {
+                            case 'text':
+                            case 'textarea':
+                                item.$input.val(item.value || "");
+                                break;
+
+                            case 'checkbox':
+                            case 'radio':
+                                item.$input.val(item.value || "").prop('checked', !!item.selected);
+                                break;
+
+                            case 'select':
+                                item.$input.val(item.selected || "");
+                                break;
+                        }
+                    }
+
+                    if (item.$menu) {
+                        // update sub-menu
+                        op.update.call($trigger, item, root);
+                    }
+                });
+            },
+            layer: function(opt, zIndex) {
+                // add transparent layer for click area
+                // filter and background for Internet Explorer, Issue #23
+                var $layer = opt.$layer = $('<div id="context-menu-layer" style="position:fixed; z-index:' + zIndex + '; top:0; left:0; opacity: 0; filter: alpha(opacity=0); background-color: #000;"></div>')
+                    .css({height: $win.height(), width: $win.width(), display: 'block'})
+                    .data('contextMenuRoot', opt)
+                    .insertBefore(this)
+                    .on('contextmenu', handle.abortevent)
+                    .on('mousedown', handle.layerClick);
+
+                // IE6 doesn't know position:fixed;
+                if (!$.support.fixedPosition) {
+                    $layer.css({
+                        'position' : 'absolute',
+                        'height' : $(document).height()
+                    });
+                }
+
+                return $layer;
+            }
+        };
+
+// split accesskey according to http://www.whatwg.org/specs/web-apps/current-work/multipage/editing.html#assigned-access-key
+    function splitAccesskey(val) {
+        var t = val.split(/\s+/),
+            keys = [];
+
+        for (var i=0, k; k = t[i]; i++) {
+            k = k[0].toUpperCase(); // first character only
+            // theoretically non-accessible characters should be ignored, but different systems, different keyboard layouts, ... screw it.
+            // a map to look up already used access keys would be nice
+            keys.push(k);
+        }
+
+        return keys;
+    }
+
+// handle contextMenu triggers
+    $.fn.contextMenu = function(operation) {
+        if (operation === undefined) {
+            this.first().trigger('contextmenu');
+        } else if (operation.x && operation.y) {
+            this.first().trigger($.Event("contextmenu", {pageX: operation.x, pageY: operation.y}));
+        } else if (operation === "hide") {
+            var $menu = this.data('contextMenu').$menu;
+            $menu && $menu.trigger('contextmenu:hide');
+        } else if (operation === "destroy") {
+            $.contextMenu("destroy", {context: this});
+        } else if ($.isPlainObject(operation)) {
+            operation.context = this;
+            $.contextMenu("create", operation);
+        } else if (operation) {
+            this.removeClass('context-menu-disabled');
+        } else if (!operation) {
+            this.addClass('context-menu-disabled');
+        }
+
+        return this;
+    };
+
+// manage contextMenu instances
+    $.contextMenu = function(operation, options) {
+        if (typeof operation != 'string') {
+            options = operation;
+            operation = 'create';
+        }
+
+        if (typeof options == 'string') {
+            options = {selector: options};
+        } else if (options === undefined) {
+            options = {};
+        }
+
+        // merge with default options
+        var o = $.extend(true, {}, defaults, options || {});
+        var $document = $(document);
+        var $context = $document;
+        var _hasContext = false;
+
+        if (!o.context || !o.context.length) {
+            o.context = document;
+        } else {
+            // you never know what they throw at you...
+            $context = $(o.context).first();
+            o.context = $context.get(0);
+            _hasContext = o.context !== document;
+        }
+
+        switch (operation) {
+            case 'create':
+                // no selector no joy
+                if (!o.selector) {
+                    throw new Error('No selector specified');
+                }
+                // make sure internal classes are not bound to
+                if (o.selector.match(/.context-menu-(list|item|input)($|\s)/)) {
+                    throw new Error('Cannot bind to selector "' + o.selector + '" as it contains a reserved className');
+                }
+                if (!o.build && (!o.items || $.isEmptyObject(o.items))) {
+                    throw new Error('No Items specified');
+                }
+                counter ++;
+                o.ns = '.contextMenu' + counter;
+                if (!_hasContext) {
+                    namespaces[o.selector] = o.ns;
+                }
+                menus[o.ns] = o;
+
+                // default to right click
+                if (!o.trigger) {
+                    o.trigger = 'right';
+                }
+
+                if (!initialized) {
+                    // make sure item click is registered first
+                    $document
+                        .on({
+                            'contextmenu:hide.contextMenu': handle.hideMenu,
+                            'prevcommand.contextMenu': handle.prevItem,
+                            'nextcommand.contextMenu': handle.nextItem,
+                            'contextmenu.contextMenu': handle.abortevent,
+                            'mouseenter.contextMenu': handle.menuMouseenter,
+                            'mouseleave.contextMenu': handle.menuMouseleave
+                        }, '.context-menu-list')
+                        .on('mouseup.contextMenu', '.context-menu-input', handle.inputClick)
+                        .on({
+                            'mouseup.contextMenu': handle.itemClick,
+                            'contextmenu:focus.contextMenu': handle.focusItem,
+                            'contextmenu:blur.contextMenu': handle.blurItem,
+                            'contextmenu.contextMenu': handle.abortevent,
+                            'mouseenter.contextMenu': handle.itemMouseenter,
+                            'mouseleave.contextMenu': handle.itemMouseleave
+                        }, '.context-menu-item');
+
+                    initialized = true;
+                }
+
+                // engage native contextmenu event
+                $context
+                    .on('contextmenu' + o.ns, o.selector, o, handle.contextmenu);
+
+                if (_hasContext) {
+                    // add remove hook, just in case
+                    $context.on('remove' + o.ns, function() {
+                        $(this).contextMenu("destroy");
+                    });
+                }
+
+                switch (o.trigger) {
+                    case 'hover':
+                        $context
+                            .on('mouseenter' + o.ns, o.selector, o, handle.mouseenter)
+                            .on('mouseleave' + o.ns, o.selector, o, handle.mouseleave);
+                        break;
+
+                    case 'left':
+                        $context.on('click' + o.ns, o.selector, o, handle.click);
+                        break;
+                    /*
+                     default:
+                     // http://www.quirksmode.org/dom/events/contextmenu.html
+                     $document
+                     .on('mousedown' + o.ns, o.selector, o, handle.mousedown)
+                     .on('mouseup' + o.ns, o.selector, o, handle.mouseup);
+                     break;
+                     */
+                }
+
+                // create menu
+                if (!o.build) {
+                    op.create(o);
+                }
+                break;
+
+            case 'destroy':
+                var $visibleMenu;
+                if (_hasContext) {
+                    // get proper options
+                    var context = o.context;
+                    $.each(menus, function(ns, o) {
+                        if (o.context !== context) {
+                            return true;
+                        }
+
+                        $visibleMenu = $('.context-menu-list').filter(':visible');
+                        if ($visibleMenu.length && $visibleMenu.data().contextMenuRoot.$trigger.is($(o.context).find(o.selector))) {
+                            $visibleMenu.trigger('contextmenu:hide', {force: true});
+                        }
+
+                        try {
+                            if (menus[o.ns].$menu) {
+                                menus[o.ns].$menu.remove();
+                            }
+
+                            delete menus[o.ns];
+                        } catch(e) {
+                            menus[o.ns] = null;
+                        }
+
+                        $(o.context).off(o.ns);
+
+                        return true;
+                    });
+                } else if (!o.selector) {
+                    $document.off('.contextMenu .contextMenuAutoHide');
+                    $.each(menus, function(ns, o) {
+                        $(o.context).off(o.ns);
+                    });
+
+                    namespaces = {};
+                    menus = {};
+                    counter = 0;
+                    initialized = false;
+
+                    $('#context-menu-layer, .context-menu-list').remove();
+                } else if (namespaces[o.selector]) {
+                    $visibleMenu = $('.context-menu-list').filter(':visible');
+                    if ($visibleMenu.length && $visibleMenu.data().contextMenuRoot.$trigger.is(o.selector)) {
+                        $visibleMenu.trigger('contextmenu:hide', {force: true});
+                    }
+
+                    try {
+                        if (menus[namespaces[o.selector]].$menu) {
+                            menus[namespaces[o.selector]].$menu.remove();
+                        }
+
+                        delete menus[namespaces[o.selector]];
+                    } catch(e) {
+                        menus[namespaces[o.selector]] = null;
+                    }
+
+                    $document.off(namespaces[o.selector]);
+                }
+                break;
+
+            case 'html5':
+                // if <command> or <menuitem> are not handled by the browser,
+                // or options was a bool true,
+                // initialize $.contextMenu for them
+                if ((!$.support.htmlCommand && !$.support.htmlMenuitem) || (typeof options == "boolean" && options)) {
+                    $('menu[type="context"]').each(function() {
+                        if (this.id) {
+                            $.contextMenu({
+                                selector: '[contextmenu=' + this.id +']',
+                                items: $.contextMenu.fromMenu(this)
+                            });
+                        }
+                    }).css('display', 'none');
+                }
+                break;
+
+            default:
+                throw new Error('Unknown operation "' + operation + '"');
+        }
+
+        return this;
+    };
+
+// import values into <input> commands
+    $.contextMenu.setInputValues = function(opt, data) {
+        if (data === undefined) {
+            data = {};
+        }
+
+        $.each(opt.inputs, function(key, item) {
+            switch (item.type) {
+                case 'text':
+                case 'textarea':
+                    item.value = data[key] || "";
+                    break;
+
+                case 'checkbox':
+                    item.selected = data[key] ? true : false;
+                    break;
+
+                case 'radio':
+                    item.selected = (data[item.radio] || "") == item.value ? true : false;
+                    break;
+
+                case 'select':
+                    item.selected = data[key] || "";
+                    break;
+            }
+        });
+    };
+
+// export values from <input> commands
+    $.contextMenu.getInputValues = function(opt, data) {
+        if (data === undefined) {
+            data = {};
+        }
+
+        $.each(opt.inputs, function(key, item) {
+            switch (item.type) {
+                case 'text':
+                case 'textarea':
+                case 'select':
+                    data[key] = item.$input.val();
+                    break;
+
+                case 'checkbox':
+                    data[key] = item.$input.prop('checked');
+                    break;
+
+                case 'radio':
+                    if (item.$input.prop('checked')) {
+                        data[item.radio] = item.value;
+                    }
+                    break;
+            }
+        });
+
+        return data;
+    };
+
+// find <label for="xyz">
+    function inputLabel(node) {
+        return (node.id && $('label[for="'+ node.id +'"]').val()) || node.name;
+    }
+
+// convert <menu> to items object
+    function menuChildren(items, $children, counter) {
+        if (!counter) {
+            counter = 0;
+        }
+
+        $children.each(function() {
+            var $node = $(this),
+                node = this,
+                nodeName = this.nodeName.toLowerCase(),
+                label,
+                item;
+
+            // extract <label><input>
+            if (nodeName == 'label' && $node.find('input, textarea, select').length) {
+                label = $node.text();
+                $node = $node.children().first();
+                node = $node.get(0);
+                nodeName = node.nodeName.toLowerCase();
+            }
+
+            /*
+             * <menu> accepts flow-content as children. that means <embed>, <canvas> and such are valid menu items.
+             * Not being the sadistic kind, $.contextMenu only accepts:
+             * <command>, <menuitem>, <hr>, <span>, <p> <input [text, radio, checkbox]>, <textarea>, <select> and of course <menu>.
+             * Everything else will be imported as an html node, which is not interfaced with contextMenu.
+             */
+
+            // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#concept-command
+            switch (nodeName) {
+                // http://www.whatwg.org/specs/web-apps/current-work/multipage/interactive-elements.html#the-menu-element
+                case 'menu':
+                    item = {name: $node.attr('label'), items: {}};
+                    counter = menuChildren(item.items, $node.children(), counter);
+                    break;
+
+                // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-a-element-to-define-a-command
+                case 'a':
+                // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-button-element-to-define-a-command
+                case 'button':
+                    item = {
+                        name: $node.text(),
+                        disabled: !!$node.attr('disabled'),
+                        callback: (function(){ return function(){ $node.click(); }; })()
+                    };
+                    break;
+
+                // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-command-element-to-define-a-command
+
+                case 'menuitem':
+                case 'command':
+                    switch ($node.attr('type')) {
+                        case undefined:
+                        case 'command':
+                        case 'menuitem':
+                            item = {
+                                name: $node.attr('label'),
+                                disabled: !!$node.attr('disabled'),
+                                callback: (function(){ return function(){ $node.click(); }; })()
+                            };
+                            break;
+
+                        case 'checkbox':
+                            item = {
+                                type: 'checkbox',
+                                disabled: !!$node.attr('disabled'),
+                                name: $node.attr('label'),
+                                selected: !!$node.attr('checked')
+                            };
+                            break;
+
+                        case 'radio':
+                            item = {
+                                type: 'radio',
+                                disabled: !!$node.attr('disabled'),
+                                name: $node.attr('label'),
+                                radio: $node.attr('radiogroup'),
+                                value: $node.attr('id'),
+                                selected: !!$node.attr('checked')
+                            };
+                            break;
+
+                        default:
+                            item = undefined;
+                    }
+                    break;
+
+                case 'hr':
+                    item = '-------';
+                    break;
+
+                case 'input':
+                    switch ($node.attr('type')) {
+                        case 'text':
+                            item = {
+                                type: 'text',
+                                name: label || inputLabel(node),
+                                disabled: !!$node.attr('disabled'),
+                                value: $node.val()
+                            };
+                            break;
+
+                        case 'checkbox':
+                            item = {
+                                type: 'checkbox',
+                                name: label || inputLabel(node),
+                                disabled: !!$node.attr('disabled'),
+                                selected: !!$node.attr('checked')
+                            };
+                            break;
+
+                        case 'radio':
+                            item = {
+                                type: 'radio',
+                                name: label || inputLabel(node),
+                                disabled: !!$node.attr('disabled'),
+                                radio: !!$node.attr('name'),
+                                value: $node.val(),
+                                selected: !!$node.attr('checked')
+                            };
+                            break;
+
+                        default:
+                            item = undefined;
+                            break;
+                    }
+                    break;
+
+                case 'select':
+                    item = {
+                        type: 'select',
+                        name: label || inputLabel(node),
+                        disabled: !!$node.attr('disabled'),
+                        selected: $node.val(),
+                        options: {}
+                    };
+                    $node.children().each(function(){
+                        item.options[this.value] = $(this).text();
+                    });
+                    break;
+
+                case 'textarea':
+                    item = {
+                        type: 'textarea',
+                        name: label || inputLabel(node),
+                        disabled: !!$node.attr('disabled'),
+                        value: $node.val()
+                    };
+                    break;
+
+                case 'label':
+                    break;
+
+                default:
+                    item = {type: 'html', html: $node.clone(true)};
+                    break;
+            }
+
+            if (item) {
+                counter++;
+                items['key' + counter] = item;
+            }
+        });
+
+        return counter;
+    }
+
+// convert html5 menu
+    $.contextMenu.fromMenu = function(element) {
+        var $this = $(element),
+            items = {};
+
+        menuChildren(items, $this.children());
+
+        return items;
+    };
+
+// make defaults accessible
+    $.contextMenu.defaults = defaults;
+    $.contextMenu.types = types;
+// export internal functions - undocumented, for hacking only!
+    $.contextMenu.handle = handle;
+    $.contextMenu.op = op;
+    $.contextMenu.menus = menus;
+
+})(jQuery);

+ 8 - 0
public/assets/js/addons.js

@@ -1,6 +1,14 @@
 define([], function () {
     require.config({
     paths: {
+        'designer': '../addons/poster/js/designer',
+        'jquery.contextMenu': '../addons/poster/js/jquery.contextMenu',
+        'jquery-colorpicker': '../addons/poster/js/jquery.colorpicker.min',
+    }
+});
+
+require.config({
+    paths: {
         'summernote': '../addons/summernote/lang/summernote-zh-CN.min'
     },
     shim: {

+ 470 - 0
public/assets/js/backend/poster.js

@@ -0,0 +1,470 @@
+define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) {
+
+    var imgsettimer = 0 ;
+    var nametimer = 0;
+    var bgtimer = 0 ;
+    var qrtimer = 0 ;
+    var Controller = {
+        index: function () {
+            // 初始化表格参数配置
+            Table.api.init({
+                extend: {
+                    index_url: 'poster/index' + location.search,
+                    add_url: 'poster/add',
+                    edit_url: 'poster/edit',
+                    del_url: 'poster/del',
+                    multi_url: 'poster/multi',
+                    table: 'poster',
+                }
+            });
+
+            var table = $("#table");
+
+            // 初始化表格
+            table.bootstrapTable({
+                url: $.fn.bootstrapTable.defaults.extend.index_url,
+                pk: 'id',
+                sortName: 'weigh',
+                commonSearch: false,
+                columns: [
+                    [
+                        {checkbox: true},
+                        {field: 'id', title: __('Id')},
+                        {field: 'title', title: __('Title')},
+                        {field: 'bg_image', title: __('Bg_image'), events: Table.api.events.image, formatter: Table.api.formatter.image},
+                        {field: 'status', title: __('Status'), searchList: {"normal":__('Normal'),"hidden":__('Hidden')}, formatter: Table.api.formatter.status},
+                        {field: 'weigh', title: __('Weigh')},
+                        {field: 'createtime', title: __('Createtime'), operate:'RANGE', addclass:'datetimerange', formatter: Table.api.formatter.datetime},
+                        {field: 'updatetime', title: __('Updatetime'), operate:'RANGE', addclass:'datetimerange', formatter: Table.api.formatter.datetime},
+                        {
+                            field: 'datalist', title: __('Operate'), table: table,operate:false,
+                            buttons: [
+                                {
+                                    name: 'content',
+                                    text: __('Posterrecord'),
+                                    extend: 'data-area=\'["1024px", "678px"]\'',
+                                    classname: 'btn btn-xs btn-success btn-dialog',
+                                    icon: 'fa fa-list-ul',
+                                    url: 'poster/posterrecord/ids/{ids}'
+                                }
+                            ],
+                            formatter: Table.api.formatter.buttons
+                        },
+                        {field: 'operate', title: __('Operate'), table: table, events: Table.api.events.operate, formatter: Table.api.formatter.operate}
+                    ]
+                ]
+            });
+            // 为表格绑定事件
+            Table.api.bindevent(table);
+
+            table.on('post-body.bs.table', function () {
+                $(".btn-editone").data("area", ["1024px", "678px"]);
+            });
+        },
+        posterrecord: function () {
+            // 初始化表格参数配置
+            Table.api.init({
+                extend: {
+                    index_url: 'poster/posterrecord' + location.search + '&ids=' + Config.ids,
+                    table: 'poster',
+                }
+            });
+
+            var table = $("#table");
+
+            // 初始化表格
+            table.bootstrapTable({
+                url: $.fn.bootstrapTable.defaults.extend.index_url,
+                pk: 'id',
+                sortName: 'id',
+                commonSearch: false,
+                columns: [
+                    [
+                        {field: 'id', title: __('Id')},
+                        {field: 'user.nickname', title: __('Nickname')},
+                        {field: 'image', title: __('Image'), events: Controller.api.events.image, formatter: Controller.api.image},
+                        {field: 'createtime', title: __('Createtime'), operate:'RANGE', addclass:'datetimerange', formatter: Table.api.formatter.datetime},
+                        {field: 'updatetime', title: __('Updatetime'), operate:'RANGE', addclass:'datetimerange', formatter: Table.api.formatter.datetime},
+                    ]
+                ]
+            });
+            // 为表格绑定事件
+            Table.api.bindevent(table);
+
+
+            $('.btn-clear').click(function(){
+                var url = $(this).data('url');
+                Fast.api.ajax({
+                    url: url,
+                }, function (data, ret) {
+                    table.bootstrapTable('refresh', {});
+                }, function (data, ret) {
+
+                });
+            });
+        },
+        add: function () {
+            Controller.api.bindevent();
+        },
+        edit: function () {
+            Controller.api.bindevent();
+        },
+        api: {
+            bindevent: function () {
+                $("form[role=form]").data("validator-options", {ignore: ':hidden'});
+                //表单提交事件绑定
+                Form.api.bindevent($("form[role=form]"), function (data, ret) {}, function (data, ret) {}, function (success, error) {
+                    var data = [];
+                    $('.drag').each(function () {
+                        var obj = $(this);
+                        var type = obj.attr('type');
+                        var left = obj.css('left'), top = obj.css('top');
+                        var d = {
+                            left: left,
+                            top: top,
+                            type: obj.attr('type'),
+                            width: obj.css('width'),
+                            height: obj.css('height')
+                        };
+                        if (type == 'nickname') {
+                            d.size = obj.attr('size');
+                            d.color = obj.attr('color');
+                        } else if (type == 'qr') {
+                            d.size = obj.attr('size');
+                            d.qr_table = obj.attr('qr_table');
+                            d.qr_field = obj.attr('qr_field');
+                            d.qr_relation = obj.attr('qr_relation');
+                        } else if (type == 'img') {
+                            d.src = Fast.api.cdnurl(obj.attr('src'));
+                        }
+                        data.push(d);
+                    });
+                    $(':input[name="row[data]"]').val(JSON.stringify(data));
+                });
+
+
+                //图片地址变化
+                $('#c-bg_image').on('change', function () {
+                    $('.bg').remove();
+                    $('#poster').prepend("<img src='" + Fast.api.cdnurl($(this).val()) + "' class='bg'/>");
+                });
+                $('#c-img').on('change', function () {
+                    $('.drag').each(function () {
+                        if ($(this).attr("type") == 'img') {
+                            $(this).find('img').attr('src', Fast.api.cdnurl($('#c-img').val()));
+                        }
+                    });
+                });
+                // 移除图片事件
+                $(document.body).on("click", "#p-bg_image .btn-trash", function () {
+                    $('.bg').attr('src', '');
+                });
+                $(document.body).on("click", "#p-img .btn-trash", function () {
+                    $('.drag').each(function () {
+                        if ($(this).attr("type") == 'img') {
+                            $(this).attr('src', '');
+                            $(this).find('img').attr('src', '/assets/addons/poster/images/img.jpg');
+                        }
+                    });
+                });
+
+                require(['designer'], function () {
+                    //添加元素
+                    $('.btn-com').click(function(){
+                        var imgset = $('#imgset'), nameset = $("#nameset"),qrset = $('#qrset');
+                        imgset.hide(),nameset.hide(),qrset.hide();
+                        Controller.api.clearTimers();
+                        var type = $(this).data('type');
+                        $('.drag').each(function () {
+                            if ($(this).attr("type") == type) {
+                                type = false;
+                            }
+                        });
+
+                        if(type == false){
+                            Layer.msg('请不要重复添加');
+                        }else{
+                            var img = "";
+                            if(type=='qr'){
+                                img = '<img src="/assets/addons/poster/images/qr.jpg" />';
+                            } else if(type=='head'){
+                                img = '<img src="/assets/addons/poster/images/head.jpg" />';
+                            } else if(type=='img' || type=='thumb'){
+                                img = '<img src="/assets/addons/poster/images/img.jpg" />';
+                            } else if(type=='nickname'){
+                                img = '<div class=text>昵称</div>';
+                            }
+                            var index = $('#poster .drag').length + 1;
+                            var obj = $('<div class="drag" type="' + type +'" index="' + index +'" style="z-index:' + index+'">' + img+'<div class="rRightDown"> </div><div class="rLeftDown"> </div><div class="rRightUp"> </div><div class="rLeftUp"> </div><div class="rRight"> </div><div class="rLeft"> </div><div class="rUp"> </div><div class="rDown"></div></div>');
+                            $('#poster').append(obj);
+                            Controller.api.bindEvents(obj);
+                        }
+                    });
+
+                    $('.drag').each(function () {
+                        Controller.api.bindEvents($(this));
+                        Controller.api.bind($(this));
+                    });
+
+                });
+
+                //颜色
+                Controller.api.color();
+
+
+                //选择二维码数据表
+                $(document).on('change', "select[name='qr_table']", function () {
+                    var that = this;
+                    Fast.api.ajax({
+                        url: "poster/get_field_list",
+                        data: {table: $(that).val()},
+                    }, function (data, ret) {
+                        mainfields = data.fieldlist;
+                        renderselect($("#c-qr_field"), mainfields, 'qr_field');//渲染数据
+                        renderselect($("#c-qr_relation"), mainfields, 'qr_relation');//渲染数据
+                        return false;
+                    });
+                    return false;
+                });
+                var renderselect = function (select, data, field_type) {
+                    var html = [];
+                    html.push("<option value=''>请选择</option>");
+                    for (var i = 0; i < data.length; i++) {
+                        html.push("<option value='" + data[i] + "'>" + data[i] + "</option>");
+                    }
+                    select.html(html.join(""));
+                    select.trigger("change");
+                    if (select.data("selectpicker")) {
+                        $('.drag').each(function () {
+                            var type = $(this).attr('type');
+                            if(type == 'qr'){
+                                if(field_type == 'qr_field'){
+                                    var qr_field = $(this).attr('qr_field') || "";
+                                    select.selectpicker('val',qr_field).trigger("change").trigger("validate");;
+                                } else {
+                                    var qr_relation = $(this).attr('qr_relation') || "";
+                                    select.selectpicker('val',qr_relation).trigger("change").trigger("validate");;
+                                }
+                            }
+                        });
+                        select.selectpicker('refresh').trigger("change");
+                    }
+                    return select;
+                };
+                $("select[name='qr_table']").trigger("change");
+            },
+            bindEvents: function (obj) {
+                var index = obj.attr('index');
+                var rs = new Resize(obj, { Max: true, mxContainer: "#poster" });
+                rs.Set($(".rRightDown",obj), "right-down");
+                rs.Set($(".rLeftDown",obj), "left-down");
+                rs.Set($(".rRightUp",obj), "right-up");
+                rs.Set($(".rLeftUp",obj), "left-up");
+                rs.Set($(".rRight",obj), "right");
+                rs.Set($(".rLeft",obj), "left");
+                rs.Set($(".rUp",obj), "up");
+                rs.Set($(".rDown",obj), "down");
+                rs.Scale = true;
+                var type = obj.attr('type');
+                if(type=='nickname' || type=='img' || type=='title' || type=='marketprice' || type=='productprice'){
+                    rs.Scale = false;
+                }
+                new Drag(obj, { Limit: true, mxContainer: "#poster" });
+                $('.drag .remove').unbind('click').click(function(){
+                    $(this).parent().remove();
+                })
+                require(['jquery.contextMenu'],function(){
+                    $.contextMenu({
+                        selector: '.drag[index=' + index + ']',
+                        callback: function(key, options) {
+                            var index = parseInt($(this).attr('zindex'));
+
+                            if(key=='next'){
+                                var nextdiv = $(this).next('.drag');
+                                if(nextdiv.length>0 ){
+                                    nextdiv.insertBefore($(this));
+                                }
+                            } else if(key=='prev'){
+                                var prevdiv = $(this).prev('.drag');
+                                if(prevdiv.length>0 ){
+                                    $(this).insertBefore(prevdiv);
+                                }
+                            } else if(key=='last'){
+                                var len = $('.drag').length;
+                                if(index >=len-1){
+                                    return;
+                                }
+                                var last = $('#poster .drag:last');
+                                if(last.length>0){
+                                    $(this).insertAfter(last);
+                                }
+                            }else if(key=='first'){
+                                var index = $(this).index();
+                                if(index<=1){
+                                    return;
+                                }
+                                var first = $('#poster .drag:first');
+                                if(first.length>0){
+                                    $(this).insertBefore(first);
+                                }
+                            }else if(key=='delete'){
+                                $(this).remove();
+                            }
+                            var n =1 ;
+                            $('.drag').each(function(){
+                                $(this).css("z-index",n);
+                                n++;
+                            })
+                        },
+                        items: {
+                            "last": {name: "调整到最顶层"},
+                            "first": {name: "调整到最低层"},
+                            "delete": {name: "删除元素"}
+                        }
+                    });
+                    obj.unbind('click').click(function () {
+                        Controller.api.bind($(this));
+                    });
+                });
+            },
+            color: function () {
+                var refreshStyle = function () {
+                    var style = [];
+                    if ($(".btn-color").hasClass("active")) {
+                        style.push($(".btn-color").data("color"));
+                    }
+                    $("#c-color").val(style.join("|"));
+                };
+                if ($(".btn-color").hasClass("active")) {
+                    style.push($(".btn-color").data("color"));
+                }
+                require(['jquery-colorpicker'], function () {
+                    $('.colorpicker').colorpicker({
+                        color: function () {
+                            var color = "#000000";
+                            var rgb = $("#c-color").css('color').match(/^rgb\(((\d+),\s*(\d+),\s*(\d+))\)$/);
+                            if (rgb) {
+                                color = rgb[1];
+                            }
+                            return color;
+                        }
+                    }, function (event, obj) {
+                        $("#c-color").css('color', '#' + obj.hex);
+                        $(event).addClass("active").data("color", '#' + obj.hex);
+                        refreshStyle();
+                    }, function (event) {
+                        $("#c-color").css('color', 'inherit');
+                        $(event).removeClass("active");
+                        refreshStyle();
+                    });
+                });
+            },
+            bind: function (obj) {
+                var imgset = $('.imgset'), nameset = $(".nameset"),qrset = $('.qrset');
+                imgset.hide(),nameset.hide(),qrset.hide();
+                Controller.api.clearTimers();
+                var type = obj.attr('type');
+
+                //图片
+                if(type=='img'){
+                    imgset.show();
+                    var src = obj.attr('src');
+                    var input = imgset.find('#c-img');
+
+                    if(typeof(src)!='undefined' && src!=''){
+                        input.val(src).trigger("change").trigger("validate");
+                    }
+                    imgsettimer = setInterval(function(){
+                        if(input.val()!=src && input.val()!=''){
+                            var url = input.val();
+                            obj.attr('src',Fast.api.cdnurl(input.val()));
+                        }
+                    },50);
+
+                    //昵称
+                } else if(type=='nickname'){
+                    nameset.show();
+                    var color = obj.attr('color') || "#000";
+                    var size = obj.attr('size') || "16";
+                    var input = nameset.find('input:first');
+                    var namesize = nameset.find('#namesize');
+                    var picker = nameset.find('.sp-preview-inner');
+                    input.val(color); namesize.val(size.replace("px",""));
+                    picker.css( {'background-color':color,'font-size':size});
+
+                    nametimer = setInterval(function(){
+                        obj.attr('color',input.val()).find('.text').css('color',input.val());
+                        obj.attr('size',namesize.val() +"px").find('.text').css('font-size',namesize.val() +"px");
+                    },50);
+
+                    //二维码
+                } else if(type=='qr'){
+                    qrset.show();
+                    //得到属性上的值
+                    var qr_table = obj.attr('qr_table') || "";
+                    var qr_field = obj.attr('qr_field') || "";
+                    var qr_relation = obj.attr('qr_relation') || "";
+
+                    //表单
+                    var qrtable = qrset.find("select[name='qr_table']");
+                    var qrfield = qrset.find("#c-qr_field");
+                    var qrrelation = qrset.find("#c-qr_relation");
+
+                    //编辑的时候吧属性值设置到编辑框
+                    if(typeof(qr_table)!='undefined' && qr_table!=''){
+                        qrtable.val(qr_table).trigger("change").trigger("validate");
+                    }
+                    if(typeof(qr_relation)!='undefined' && qr_relation!=''){
+                        qrrelation.val(qr_relation).trigger("change").trigger("validate");
+                    }
+
+                    //修改内容的时候设置attr
+                    qrtimer = setInterval(function(){
+                        if(qrtable.val()!=qr_table && qrtable.val()!='' && qrtable.val() != 'null' && qrtable.val() != null){
+                            obj.attr('qr_table',qrtable.val());
+                        }
+                        if(qrfield.val()!=qr_field && qrfield.val()!='' && qrfield.val() != 'null' && qrfield.val() != null){
+                            obj.attr('qr_field',qrfield.val());
+                        }
+                        if(qrrelation.val()!=qr_relation && qrrelation.val()!='' && qrrelation.val() != 'null' && qrrelation.val() != null){
+                            obj.attr('qr_relation',qrrelation.val());
+                        }
+                    },50);
+                }
+            },
+            clearTimers: function () {
+                clearInterval(imgsettimer);
+                clearInterval(nametimer);
+                clearInterval(bgtimer);
+                clearInterval(qrtimer);
+            },
+            image: function (value, row, index) {
+                value = value ? value : '/assets/img/blank.gif';
+                var classname = typeof this.classname !== 'undefined' ? this.classname : 'img-sm img-center';
+                return '<a href="javascript:"><img class="' + classname + '" src="' + value + '" /></a>';
+            },
+            events: {
+                //单元格图片预览
+                image: {
+                    'click .img-center': function (e, value, row, index) {
+                        var data = [];
+                        value = value.toString().split(",");
+                        $.each(value, function (index, value) {
+                            data.push({
+                                src: value,
+                            });
+                        });
+                        Layer.photos({
+                            photos: {
+                                "start": $(this).parent().index(),
+                                "data": data
+                            },
+                            anim: 5 //0-6的选择,指定弹出图片动画类型,默认随机(请注意,3.0之前的版本用shift参数)
+                        });
+                    },
+                }
+            },
+        }
+    };
+    return Controller;
+});

+ 20 - 0
public/assets/js/frontend/poster.js

@@ -0,0 +1,20 @@
+define(['jquery', 'bootstrap', 'frontend', 'template', 'form'], function ($, undefined, Frontend, Template, Form) {
+    var Controller = {
+        index: function () {
+            $('.poster').each(function () {
+                var poster_id = $(this).data('poster_id');
+                Fast.api.ajax({
+                    url: "poster/getposter/poster_id/" + poster_id,
+                }, function (data, ret) {
+                    console.log(ret);
+                    $('.poster').html('<img src="' + ret.data + '" style="width: 400px;">');
+                    return false;
+                }, function (data, ret) {
+
+                });
+            });
+            return false;
+        }
+    };
+    return Controller;
+});

Some files were not shown because too many files changed in this diff