From 39ad7c97a282321a4b47bfca9624863ccbb11973 Mon Sep 17 00:00:00 2001
From: Karson <karsonzhang@163.com>
Date: Mon, 2 Apr 2018 00:09:12 +0800
Subject: [PATCH] 新增Token多种存储方式 新增fieldlist自定义模板功能 新增关闭会员中心接口 优化Token存储,采用加密存储方式

---
 application/admin/controller/Index.php           |   3 ++-
 application/admin/controller/general/Config.php  |  11 +++++++----
 application/admin/view/general/config/index.html |  13 +++----------
 application/common/library/Token.php             | 160 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------------------
 application/common/model/Token.php               |  36 ------------------------------------
 application/config.php                           |  18 +++++++++++++++++-
 application/index/controller/Index.php           |   1 +
 application/index/controller/User.php            |  69 +++++++++++++++++++++++++++------------------------------------------
 application/index/lang/zh-cn.php                 |   1 +
 application/index/lang/zh-cn/user.php            |   1 +
 application/index/view/index/index.html          |   3 ++-
 public/assets/js/require-backend.min.js          |  97 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
 public/assets/js/require-form.js                 |  97 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
 public/install.php                               |  12 ++++++------
 14 files changed, 346 insertions(+), 176 deletions(-)
 delete mode 100644 application/common/model/Token.php

diff --git a/application/admin/controller/Index.php b/application/admin/controller/Index.php
index 80d5ee4..611f9fe 100644
--- a/application/admin/controller/Index.php
+++ b/application/admin/controller/Index.php
@@ -105,7 +105,8 @@ class Index extends Backend
         {
             $this->redirect($url);
         }
-        $background = cdnurl(Config::get('fastadmin.login_background'));
+        $background = Config::get('fastadmin.login_background');
+        $background = stripos($background, 'http')===0 ? $background : config('site.cdnurl') . $background;
         $this->view->assign('background', $background);
         $this->view->assign('title', __('Login'));
         Hook::listen("login_init", $this->request);
diff --git a/application/admin/controller/general/Config.php b/application/admin/controller/general/Config.php
index b594fda..573a04e 100644
--- a/application/admin/controller/general/Config.php
+++ b/application/admin/controller/general/Config.php
@@ -25,6 +25,9 @@ class Config extends Backend
         $this->model = model('Config');
     }
 
+    /**
+     * 查看
+     */
     public function index()
     {
         $siteList = [];
@@ -48,10 +51,6 @@ class Config extends Backend
             {
                 $value['value'] = explode(',', $value['value']);
             }
-            if ($value['type'] == 'array')
-            {
-                $value['value'] = (array) json_decode($value['value'], TRUE);
-            }
             $value['content'] = json_decode($value['content'], TRUE);
             $siteList[$v['group']]['list'][] = $value;
         }
@@ -119,6 +118,10 @@ class Config extends Backend
         return $this->view->fetch();
     }
 
+    /**
+     * 编辑
+     * @param null $ids
+     */
     public function edit($ids = NULL)
     {
         if ($this->request->isPost())
diff --git a/application/admin/view/general/config/index.html b/application/admin/view/general/config/index.html
index 9013cd0..9875aab 100644
--- a/application/admin/view/general/config/index.html
+++ b/application/admin/view/general/config/index.html
@@ -54,20 +54,13 @@
                                                 <textarea name="row[{$item.name}]" class="form-control editor" data-rule="{$item.rule}" rows="5" data-tip="{$item.tip}" {$item.extend}>{$item.value}</textarea>
                                                 {/case}
                                                 {case array}
-                                                <dl class="fieldlist" rel="{$item.value|count}" data-name="row[{$item.name}]">
+                                                <dl class="fieldlist" data-name="row[{$item.name}]">
                                                     <dd>
                                                         <ins>{:__('Array key')}</ins>
                                                         <ins>{:__('Array value')}</ins>
                                                     </dd>
-                                                    {foreach $item.value as $key => $vo}
-                                                    <dd class="form-inline">
-                                                        <input type="text" name="row[{$item.name}][field][{$key}]" class="form-control" value="{$key}" size="10" />
-                                                        <input type="text" name="row[{$item.name}][value][{$key}]" class="form-control" value="{$vo}" size="40" />
-                                                        <span class="btn btn-sm btn-danger btn-remove"><i class="fa fa-times"></i></span>
-                                                        <span class="btn btn-sm btn-primary btn-dragsort"><i class="fa fa-arrows"></i></span>
-                                                    </dd>
-                                                    {/foreach}
-                                                    <dd><a href="javascript:;" class="append btn btn-sm btn-success"><i class="fa fa-plus"></i> {:__('Append')}</a></dd>
+                                                    <dd><a href="javascript:;" class="btn btn-sm btn-success btn-append"><i class="fa fa-plus"></i> {:__('Append')}</a></dd>
+                                                    <textarea name="row[{$item.name}]" class="form-control hide" cols="30" rows="5">{$item.value}</textarea>
                                                 </dl>
                                                 {/case}
                                                 {case datetime}
diff --git a/application/common/library/Token.php b/application/common/library/Token.php
index d42679c..2751f64 100644
--- a/application/common/library/Token.php
+++ b/application/common/library/Token.php
@@ -2,84 +2,158 @@
 
 namespace app\common\library;
 
+use app\common\library\token\Driver;
+use think\App;
+use think\Config;
+use think\Log;
+
 /**
  * Token操作类
  */
 class Token
 {
+    /**
+     * @var array Token的实例
+     */
+    public static $instance = [];
+
+    /**
+     * @var object 操作句柄
+     */
+    public static $handler;
 
     /**
-     * 存储Token
-     * @param   string    $token      Token
-     * @param   int       $user_id    会员ID
-     * @param   int       $expire     过期时长,0表示无限,单位秒
+     * 连接Token驱动
+     * @access public
+     * @param  array $options 配置数组
+     * @param  bool|string $name Token连接标识 true 强制重新连接
+     * @return Driver
      */
-    public static function set($token, $user_id, $expire = 0)
+    public static function connect(array $options = [], $name = false)
     {
-        $expiretime = $expire ? time() + $expire : 0;
-        \app\common\model\Token::create(['token' => $token, 'user_id' => $user_id, 'expiretime' => $expiretime]);
-        return TRUE;
+        $type = !empty($options['type']) ? $options['type'] : 'File';
+
+        if (false === $name) {
+            $name = md5(serialize($options));
+        }
+
+        if (true === $name || !isset(self::$instance[$name])) {
+            $class = false === strpos($type, '\\') ?
+                '\\app\\common\\library\\token\\driver\\' . ucwords($type) :
+                $type;
+
+            // 记录初始化信息
+            App::$debug && Log::record('[ TOKEN ] INIT ' . $type, 'info');
+
+            if (true === $name) {
+                return new $class($options);
+            }
+
+            self::$instance[$name] = new $class($options);
+        }
+
+        return self::$instance[$name];
     }
 
     /**
-     * 获取Token内的信息
-     * @param   string  $token 
-     * @return  array
+     * 自动初始化Token
+     * @access public
+     * @param  array $options 配置数组
+     * @return Driver
      */
-    public static function get($token)
+    public static function init(array $options = [])
     {
-        $data = \app\common\model\Token::get($token);
-        if ($data)
-        {
-            if (!$data['expiretime'] || $data['expiretime'] > time())
-            {
-                return $data;
-            }
-            else
-            {
-                self::delete($token);
+        if (is_null(self::$handler)) {
+            if (empty($options) && 'complex' == Config::get('token.type')) {
+                $default = Config::get('token.default');
+                // 获取默认Token配置,并连接
+                $options = Config::get('token.' . $default['type']) ?: $default;
+            } elseif (empty($options)) {
+                $options = Config::get('token');
             }
+
+            self::$handler = self::connect($options);
         }
-        return [];
+
+        return self::$handler;
+    }
+
+    /**
+     * 判断Token是否可用(check别名)
+     * @access public
+     * @param  string $token Token标识
+     * @return bool
+     */
+    public static function has($token, $user_id)
+    {
+        return self::check($token, $user_id);
     }
 
     /**
      * 判断Token是否可用
-     * @param   string    $token      Token
-     * @param   int       $user_id    会员ID
-     * @return  boolean
+     * @param string $token Token标识
+     * @return bool
      */
     public static function check($token, $user_id)
     {
-        $data = self::get($token);
-        return $data && $data['user_id'] == $user_id ? true : false;
+        return self::init()->check($token, $user_id);
+    }
+
+    /**
+     * 读取Token
+     * @access public
+     * @param  string $token Token标识
+     * @param  mixed $default 默认值
+     * @return mixed
+     */
+    public static function get($token, $default = false)
+    {
+        return self::init()->get($token, $default);
+    }
+
+    /**
+     * 写入Token
+     * @access public
+     * @param  string $token Token标识
+     * @param  mixed $user_id 存储数据
+     * @param  int|null $expire 有效时间 0为永久
+     * @return boolean
+     */
+    public static function set($token, $user_id, $expire = null)
+    {
+        return self::init()->set($token, $user_id, $expire);
+    }
+
+    /**
+     * 删除Token(delete别名)
+     * @access public
+     * @param  string $token Token标识
+     * @return boolean
+     */
+    public static function rm($token)
+    {
+        return self::delete($token);
     }
 
     /**
      * 删除Token
-     * @param   string  $token
-     * @return  boolean
+     * @param string $token 标签名
+     * @return bool
      */
     public static function delete($token)
     {
-        $data = \app\common\model\Token::get($token);
-        if ($data)
-        {
-            $data->delete();
-            return true;
-        }
-        return false;
+        return self::init()->delete($token);
     }
 
     /**
-     * 删除指定用户的所有Token
-     * @param   int     $user_id
-     * @return  boolean
+     * 清除Token
+     * @access public
+     * @param  string $token Token标记
+     * @return boolean
      */
-    public static function clear($user_id)
+    public static function clear($user_id = null)
     {
-        \app\common\model\Token::where('user_id', $user_id)->delete();
-        return true;
+        return self::init()->clear($user_id);
     }
 
 }
diff --git a/application/common/model/Token.php b/application/common/model/Token.php
deleted file mode 100644
index 978d362..0000000
--- a/application/common/model/Token.php
+++ /dev/null
@@ -1,36 +0,0 @@
-<?php
-
-namespace app\common\model;
-
-use think\Model;
-
-/**
- * Token模型
- */
-class Token Extends Model
-{
-
-    // 表名
-    protected $name = 'user_token';
-    // 开启自动写入时间戳字段
-    protected $autoWriteTimestamp = 'int';
-    // 定义时间戳字段名
-    protected $createTime = 'createtime';
-    protected $updateTime = false;
-    // 定义主键
-    protected $pk = 'token';
-    // 追加属性
-    protected $append = [
-        'expires_in'
-    ];
-    
-    /**
-     * 获取Token剩余有效期
-     * @return int
-     */
-    public function getExpiresInAttr($value, $data)
-    {
-        return $data['expiretime'] ? max(0, $data['expiretime'] - time()) : 365 * 86400;
-    }
-
-}
diff --git a/application/config.php b/application/config.php
index d8c7585..3bfb1d6 100755
--- a/application/config.php
+++ b/application/config.php
@@ -242,8 +242,23 @@ return [
         // 验证成功后是否重置
         'reset'    => true
     ],
+    // +----------------------------------------------------------------------
+    // | Token设置
+    // +----------------------------------------------------------------------
+    'token'                  => [
+        // 驱动方式
+        'type'     => 'Redis',
+        // 缓存前缀
+        'key'      => 'i3d6o32wo8fvs1fvdpwens',
+        // 加密方式
+        'hashalgo' => 'ripemd160',
+        // 缓存有效期 0表示永久缓存
+        'expire'   => 0,
+    ],
     //FastAdmin配置
     'fastadmin'              => [
+        //是否开启前台会员中心
+        'usercenter'       => true,
         //登录验证码
         'login_captcha'    => false,
         //是否同一账号同一时间只能在一个地方登录
@@ -253,7 +268,8 @@ return [
         //自动检测更新
         'checkupdate'      => false,
         //版本号
-        'version'          => '1.0.0.20180327_beta',
+        'version'          => '1.0.0.20180401_beta',
+        //API接口地址
         'api_url'          => 'https://api.fastadmin.net',
     ],
 ];
diff --git a/application/index/controller/Index.php b/application/index/controller/Index.php
index 78f2359..ba5aaf4 100755
--- a/application/index/controller/Index.php
+++ b/application/index/controller/Index.php
@@ -3,6 +3,7 @@
 namespace app\index\controller;
 
 use app\common\controller\Frontend;
+use app\common\library\Token;
 
 class Index extends Frontend
 {
diff --git a/application/index/controller/User.php b/application/index/controller/User.php
index 73e66ab..04ed25b 100644
--- a/application/index/controller/User.php
+++ b/application/index/controller/User.php
@@ -3,6 +3,7 @@
 namespace app\index\controller;
 
 use app\common\controller\Frontend;
+use think\Config;
 use think\Cookie;
 use think\Hook;
 use think\Session;
@@ -23,27 +24,30 @@ class User extends Frontend
         parent::_initialize();
         $auth = $this->auth;
 
+        if (!Config::get('fastadmin.usercenter')) {
+            $this->error(__('User center already closed'));
+        }
+
         $ucenter = get_addon_info('ucenter');
-        if ($ucenter && $ucenter['state'])
-        {
+        if ($ucenter && $ucenter['state']) {
             include ADDON_PATH . 'ucenter' . DS . 'uc.php';
         }
 
         //监听注册登录注销的事件
-        Hook::add('user_login_successed', function($user) use($auth) {
+        Hook::add('user_login_successed', function ($user) use ($auth) {
             $expire = input('post.keeplogin') ? 30 * 86400 : 0;
             Cookie::set('uid', $user->id, $expire);
             Cookie::set('token', $auth->getToken(), $expire);
         });
-        Hook::add('user_register_successed', function($user) use($auth) {
+        Hook::add('user_register_successed', function ($user) use ($auth) {
             Cookie::set('uid', $user->id);
             Cookie::set('token', $auth->getToken());
         });
-        Hook::add('user_delete_successed', function($user) use($auth) {
+        Hook::add('user_delete_successed', function ($user) use ($auth) {
             Cookie::delete('uid');
             Cookie::delete('token');
         });
-        Hook::add('user_logout_successed', function($user) use($auth) {
+        Hook::add('user_logout_successed', function ($user) use ($auth) {
             Cookie::delete('uid');
             Cookie::delete('token');
         });
@@ -66,8 +70,7 @@ class User extends Frontend
         $url = $this->request->request('url', url('user/index'));
         if ($this->auth->id)
             $this->success(__('You\'ve logged in, do not login again'), $url);
-        if ($this->request->isPost())
-        {
+        if ($this->request->isPost()) {
             $username = $this->request->post('username');
             $password = $this->request->post('password');
             $email = $this->request->post('email');
@@ -103,23 +106,18 @@ class User extends Frontend
             ];
             $validate = new Validate($rule, $msg);
             $result = $validate->check($data);
-            if (!$result)
-            {
+            if (!$result) {
                 $this->error(__($validate->getError()));
             }
-            if ($this->auth->register($username, $password, $email, $mobile))
-            {
+            if ($this->auth->register($username, $password, $email, $mobile)) {
                 $synchtml = '';
                 ////////////////同步到Ucenter////////////////
-                if (defined('UC_STATUS') && UC_STATUS)
-                {
+                if (defined('UC_STATUS') && UC_STATUS) {
                     $uc = new \addons\ucenter\library\client\Client();
                     $synchtml = $uc->uc_user_synregister($this->auth->id, $password);
                 }
                 $this->success(__('Sign up successful') . $synchtml, $url);
-            }
-            else
-            {
+            } else {
                 $this->error($this->auth->getError());
             }
         }
@@ -136,11 +134,10 @@ class User extends Frontend
         $url = $this->request->request('url', url('user/index'));
         if ($this->auth->id)
             $this->success(__('You\'ve logged in, do not login again'), $url);
-        if ($this->request->isPost())
-        {
+        if ($this->request->isPost()) {
             $account = $this->request->post('account');
             $password = $this->request->post('password');
-            $keeplogin = (int) $this->request->post('keeplogin');
+            $keeplogin = (int)$this->request->post('keeplogin');
             $token = $this->request->post('__token__');
             $rule = [
                 'account'   => 'require|length:3,50',
@@ -161,24 +158,19 @@ class User extends Frontend
             ];
             $validate = new Validate($rule, $msg);
             $result = $validate->check($data);
-            if (!$result)
-            {
+            if (!$result) {
                 $this->error(__($validate->getError()));
                 return FALSE;
             }
-            if ($this->auth->login($account, $password))
-            {
+            if ($this->auth->login($account, $password)) {
                 $synchtml = '';
                 ////////////////同步到Ucenter////////////////
-                if (defined('UC_STATUS') && UC_STATUS)
-                {
+                if (defined('UC_STATUS') && UC_STATUS) {
                     $uc = new \addons\ucenter\library\client\Client();
                     $synchtml = $uc->uc_user_synlogin($this->auth->id);
                 }
                 $this->success(__('Logged in successful') . $synchtml, $url);
-            }
-            else
-            {
+            } else {
                 $this->error($this->auth->getError());
             }
         }
@@ -195,8 +187,7 @@ class User extends Frontend
         $this->auth->logout();
         $synchtml = '';
         ////////////////同步到Ucenter////////////////
-        if (defined('UC_STATUS') && UC_STATUS)
-        {
+        if (defined('UC_STATUS') && UC_STATUS) {
             $uc = new \addons\ucenter\library\client\Client();
             $synchtml = $uc->uc_user_synlogout();
         }
@@ -217,8 +208,7 @@ class User extends Frontend
      */
     public function changepwd()
     {
-        if ($this->request->isPost())
-        {
+        if ($this->request->isPost()) {
             $oldpassword = $this->request->post("oldpassword");
             $newpassword = $this->request->post("newpassword");
             $renewpassword = $this->request->post("renewpassword");
@@ -245,26 +235,21 @@ class User extends Frontend
             ];
             $validate = new Validate($rule, $msg, $field);
             $result = $validate->check($data);
-            if (!$result)
-            {
+            if (!$result) {
                 $this->error(__($validate->getError()));
                 return FALSE;
             }
 
             $ret = $this->auth->changepwd($newpassword, $oldpassword);
-            if ($ret)
-            {
+            if ($ret) {
                 $synchtml = '';
                 ////////////////同步到Ucenter////////////////
-                if (defined('UC_STATUS') && UC_STATUS)
-                {
+                if (defined('UC_STATUS') && UC_STATUS) {
                     $uc = new \addons\ucenter\library\client\Client();
                     $synchtml = $uc->uc_user_synlogout();
                 }
                 $this->success(__('Reset password successful') . $synchtml, url('user/login'));
-            }
-            else
-            {
+            } else {
                 $this->error($this->auth->getError());
             }
         }
diff --git a/application/index/lang/zh-cn.php b/application/index/lang/zh-cn.php
index e2e141f..9c5c85c 100755
--- a/application/index/lang/zh-cn.php
+++ b/application/index/lang/zh-cn.php
@@ -81,6 +81,7 @@ return [
     'Github'                                                 => 'Github',
     'QQ group'                                               => 'QQ群',
     'Go to Dashboard'                                        => '登录后台',
+    'Go to Member center'                                    => '会员中心',
     'Contribution'                                           => '为FastAdmin贡献代码!',
     'Copyrights'                                             => '版权所有',
     'Responsive'                                             => '响应式开发',
diff --git a/application/index/lang/zh-cn/user.php b/application/index/lang/zh-cn/user.php
index 0f1fcf0..1ca3c23 100755
--- a/application/index/lang/zh-cn/user.php
+++ b/application/index/lang/zh-cn/user.php
@@ -59,6 +59,7 @@ return [
     'Sign up successful'                    => '注册成功',
     'Logged in successful'                  => '登录成功',
     'Logout successful'                     => '注销成功',
+    'User center already closed'            => '会员中心已经关闭',
     'Operation failed'                      => '操作失败',
     'Invalid parameters'                    => '参数不正确',
     'Change password failure'               => '修改密码失败',
diff --git a/application/index/view/index/index.html b/application/index/view/index/index.html
index 0e4571f..0ff5b96 100755
--- a/application/index/view/index/index.html
+++ b/application/index/view/index/index.html
@@ -62,7 +62,8 @@
                             <div class="header-content-inner">
                                 <h1>FastAdmin</h1>
                                 <h3>{:__('The fastest framework based on ThinkPHP5 and Bootstrap')}</h3>
-                                <a href="{:url('admin/index/login')}" class="btn btn-outline btn-xl page-scroll">{:__('Go to Dashboard')}</a>
+                                <a href="{:url('admin/index/login')}" class="btn btn-warning btn-xl page-scroll">{:__('Go to Dashboard')}</a>
+                                <a href="{:url('index/user/index')}" class="btn btn-outline btn-xl page-scroll">{:__('Go to Member center')}</a>
                             </div>
                         </div>
                     </div>
diff --git a/public/assets/js/require-backend.min.js b/public/assets/js/require-backend.min.js
index 1f5d610..8896312 100644
--- a/public/assets/js/require-backend.min.js
+++ b/public/assets/js/require-backend.min.js
@@ -11148,6 +11148,7 @@ define('validator',['validator-core', 'validator-lang'], function (Validator, un
 define('form',['jquery', 'bootstrap', 'upload', 'validator'], function ($, undefined, Upload, Validator) {
     var Form = {
         config: {
+            fieldlisttpl: '<dd class="form-inline"><input type="text" name="<%=name%>[<%=index%>][key]" class="form-control" value="<%=row.key%>" size="10" /> <input type="text" name="<%=name%>[<%=index%>][value]" class="form-control" value="<%=row.value%>" size="40" /> <span class="btn btn-sm btn-danger btn-remove"><i class="fa fa-times"></i></span> <span class="btn btn-sm btn-primary btn-dragsort"><i class="fa fa-arrows"></i></span></dd>'
         },
         events: {
             validator: function (form, success, error, submit) {
@@ -11173,7 +11174,7 @@ define('form',['jquery', 'bootstrap', 'upload', 'validator'], function ($, undef
                     },
                     target: function (input) {
                         var $formitem = $(input).closest('.form-group'),
-                                $msgbox = $formitem.find('span.msg-box');
+                            $msgbox = $formitem.find('span.msg-box');
                         if (!$msgbox.length) {
                             return [];
                         }
@@ -11269,7 +11270,8 @@ define('form',['jquery', 'bootstrap', 'upload', 'validator'], function ($, undef
             citypicker: function (form) {
                 //绑定城市远程插件
                 if ($("[data-toggle='city-picker']", form).size() > 0) {
-                    require(['citypicker'], function () {});
+                    require(['citypicker'], function () {
+                    });
                 }
             },
             datetimepicker: function (form) {
@@ -11387,27 +11389,90 @@ define('form',['jquery', 'bootstrap', 'upload', 'validator'], function ($, undef
                 }
             },
             fieldlist: function (form) {
+                //绑定fieldlist
                 if ($(".fieldlist", form).size() > 0) {
-                    $(".fieldlist", form).on("click", ".append", function () {
-                        var rel = parseInt($(this).closest("dl").attr("rel")) + 1;
-                        var name = $(this).closest("dl").data("name");
-                        $(this).closest("dl").attr("rel", rel);
-                        $('<dd class="form-inline"><input type="text" name="' + name + '[field][' + rel + ']" class="form-control" value="" size="10" /> <input type="text" name="' + name + '[value][' + rel + ']" class="form-control" value="" size="40" /> <span class="btn btn-sm btn-danger btn-remove"><i class="fa fa-times"></i></span> <span class="btn btn-sm btn-primary btn-dragsort"><i class="fa fa-arrows"></i></span></dd>').insertBefore($(this).parent());
-                    });
-                    $(".fieldlist", form).on("click", "dd .btn-remove", function () {
-                        $(this).parent().remove();
-                    });
-                    //拖拽排序
-                    require(['dragsort'], function () {
-                        //绑定拖动排序
+                    require(['dragsort', 'template'], function (undefined, Template) {
+                        //刷新隐藏textarea的值
+                        var refresh = function (name) {
+                            var data = {};
+                            var textarea = $("textarea[name='" + name + "']", form);
+                            var container = textarea.closest("dl");
+                            var template = container.data("template");
+                            console.log(name, container);
+                            $.each($("input,select", container).serializeArray(), function (i, j) {
+                                var reg = /\[(\w+)\]\[(\w+)\]$/g;
+                                var match = reg.exec(j.name);
+                                if (!match)
+                                    return true;
+                                match[1] = "x" + parseInt(match[1]);
+                                if (typeof data[match[1]] == 'undefined') {
+                                    data[match[1]] = {};
+                                }
+                                data[match[1]][match[2]] = j.value;
+                            });
+                            var result = template ? [] : {};
+                            $.each(data, function (i, j) {
+                                if (j) {
+                                    if (!template) {
+                                        if (j.key != '') {
+                                            result[j.key] = j.value;
+                                        }
+                                    } else {
+                                        result.push(j);
+                                    }
+                                }
+                            });
+                            textarea.val(JSON.stringify(result));
+                        };
+                        //监听文本框改变事件
+                        $(document).on('change keyup', ".fieldlist input,.fieldlist textarea,.fieldlist select", function () {
+                            refresh($(this).closest("dl").data("name"));
+                        });
+                        //追加控制
+                        $(".fieldlist", form).on("click", ".btn-append", function (e, row) {
+                            var container = $(this).closest("dl");
+                            var index = container.data("index");
+                            var name = container.data("name");
+                            var template = container.data("template");
+                            var data = container.data();
+                            index = index ? parseInt(index) : 0;
+                            container.data("index", index + 1);
+                            var row = row ? row : {};
+                            var vars = {index: index, name: name, data: data, row: row};
+                            var html = template ? Template(template, vars) : Template.render(Form.config.fieldlisttpl, vars);
+                            $(html).insertBefore($(this).closest("dd"));
+                            $(this).trigger("fa.event.appendfieldlist", $(this).closest("dd").prev());
+                        });
+                        //移除控制
+                        $(".fieldlist", form).on("click", "dd .btn-remove", function () {
+                            var container = $(this).closest("dl");
+                            $(this).closest("dd").remove();
+                            refresh(container.data("name"));
+                        });
+                        //拖拽排序
                         $("dl.fieldlist", form).dragsort({
                             itemSelector: 'dd',
-                            dragSelector: ".btn-dragsort",
+                            dragSelector: ".btn-fdragsort",
                             dragEnd: function () {
-
+                                refresh($(this).closest("dl").data("name"));
                             },
                             placeHolderTemplate: "<dd></dd>"
                         });
+                        //渲染数据
+                        $(".fieldlist", form).each(function () {
+                            var container = this;
+                            var textarea = $("textarea[name='" + $(this).data("name") + "']", form);
+                            if (textarea.val() == '') {
+                                return true;
+                            }
+                            var template = $(this).data("template");
+                            $.each(JSON.parse(textarea.val()), function (i, j) {
+                                $(".btn-append", container).trigger('click', template ? j : {
+                                    key: i,
+                                    value: j
+                                });
+                            });
+                        });
                     });
                 }
             },
diff --git a/public/assets/js/require-form.js b/public/assets/js/require-form.js
index 0747213..c1714de 100755
--- a/public/assets/js/require-form.js
+++ b/public/assets/js/require-form.js
@@ -1,6 +1,7 @@
 define(['jquery', 'bootstrap', 'upload', 'validator'], function ($, undefined, Upload, Validator) {
     var Form = {
         config: {
+            fieldlisttpl: '<dd class="form-inline"><input type="text" name="<%=name%>[<%=index%>][key]" class="form-control" value="<%=row.key%>" size="10" /> <input type="text" name="<%=name%>[<%=index%>][value]" class="form-control" value="<%=row.value%>" size="40" /> <span class="btn btn-sm btn-danger btn-remove"><i class="fa fa-times"></i></span> <span class="btn btn-sm btn-primary btn-dragsort"><i class="fa fa-arrows"></i></span></dd>'
         },
         events: {
             validator: function (form, success, error, submit) {
@@ -26,7 +27,7 @@ define(['jquery', 'bootstrap', 'upload', 'validator'], function ($, undefined, U
                     },
                     target: function (input) {
                         var $formitem = $(input).closest('.form-group'),
-                                $msgbox = $formitem.find('span.msg-box');
+                            $msgbox = $formitem.find('span.msg-box');
                         if (!$msgbox.length) {
                             return [];
                         }
@@ -122,7 +123,8 @@ define(['jquery', 'bootstrap', 'upload', 'validator'], function ($, undefined, U
             citypicker: function (form) {
                 //绑定城市远程插件
                 if ($("[data-toggle='city-picker']", form).size() > 0) {
-                    require(['citypicker'], function () {});
+                    require(['citypicker'], function () {
+                    });
                 }
             },
             datetimepicker: function (form) {
@@ -240,27 +242,90 @@ define(['jquery', 'bootstrap', 'upload', 'validator'], function ($, undefined, U
                 }
             },
             fieldlist: function (form) {
+                //绑定fieldlist
                 if ($(".fieldlist", form).size() > 0) {
-                    $(".fieldlist", form).on("click", ".append", function () {
-                        var rel = parseInt($(this).closest("dl").attr("rel")) + 1;
-                        var name = $(this).closest("dl").data("name");
-                        $(this).closest("dl").attr("rel", rel);
-                        $('<dd class="form-inline"><input type="text" name="' + name + '[field][' + rel + ']" class="form-control" value="" size="10" /> <input type="text" name="' + name + '[value][' + rel + ']" class="form-control" value="" size="40" /> <span class="btn btn-sm btn-danger btn-remove"><i class="fa fa-times"></i></span> <span class="btn btn-sm btn-primary btn-dragsort"><i class="fa fa-arrows"></i></span></dd>').insertBefore($(this).parent());
-                    });
-                    $(".fieldlist", form).on("click", "dd .btn-remove", function () {
-                        $(this).parent().remove();
-                    });
-                    //拖拽排序
-                    require(['dragsort'], function () {
-                        //绑定拖动排序
+                    require(['dragsort', 'template'], function (undefined, Template) {
+                        //刷新隐藏textarea的值
+                        var refresh = function (name) {
+                            var data = {};
+                            var textarea = $("textarea[name='" + name + "']", form);
+                            var container = textarea.closest("dl");
+                            var template = container.data("template");
+                            console.log(name, container);
+                            $.each($("input,select", container).serializeArray(), function (i, j) {
+                                var reg = /\[(\w+)\]\[(\w+)\]$/g;
+                                var match = reg.exec(j.name);
+                                if (!match)
+                                    return true;
+                                match[1] = "x" + parseInt(match[1]);
+                                if (typeof data[match[1]] == 'undefined') {
+                                    data[match[1]] = {};
+                                }
+                                data[match[1]][match[2]] = j.value;
+                            });
+                            var result = template ? [] : {};
+                            $.each(data, function (i, j) {
+                                if (j) {
+                                    if (!template) {
+                                        if (j.key != '') {
+                                            result[j.key] = j.value;
+                                        }
+                                    } else {
+                                        result.push(j);
+                                    }
+                                }
+                            });
+                            textarea.val(JSON.stringify(result));
+                        };
+                        //监听文本框改变事件
+                        $(document).on('change keyup', ".fieldlist input,.fieldlist textarea,.fieldlist select", function () {
+                            refresh($(this).closest("dl").data("name"));
+                        });
+                        //追加控制
+                        $(".fieldlist", form).on("click", ".btn-append", function (e, row) {
+                            var container = $(this).closest("dl");
+                            var index = container.data("index");
+                            var name = container.data("name");
+                            var template = container.data("template");
+                            var data = container.data();
+                            index = index ? parseInt(index) : 0;
+                            container.data("index", index + 1);
+                            var row = row ? row : {};
+                            var vars = {index: index, name: name, data: data, row: row};
+                            var html = template ? Template(template, vars) : Template.render(Form.config.fieldlisttpl, vars);
+                            $(html).insertBefore($(this).closest("dd"));
+                            $(this).trigger("fa.event.appendfieldlist", $(this).closest("dd").prev());
+                        });
+                        //移除控制
+                        $(".fieldlist", form).on("click", "dd .btn-remove", function () {
+                            var container = $(this).closest("dl");
+                            $(this).closest("dd").remove();
+                            refresh(container.data("name"));
+                        });
+                        //拖拽排序
                         $("dl.fieldlist", form).dragsort({
                             itemSelector: 'dd',
-                            dragSelector: ".btn-dragsort",
+                            dragSelector: ".btn-fdragsort",
                             dragEnd: function () {
-
+                                refresh($(this).closest("dl").data("name"));
                             },
                             placeHolderTemplate: "<dd></dd>"
                         });
+                        //渲染数据
+                        $(".fieldlist", form).each(function () {
+                            var container = this;
+                            var textarea = $("textarea[name='" + $(this).data("name") + "']", form);
+                            if (textarea.val() == '') {
+                                return true;
+                            }
+                            var template = $(this).data("template");
+                            $.each(JSON.parse(textarea.val()), function (i, j) {
+                                $(".btn-append", container).trigger('click', template ? j : {
+                                    key: i,
+                                    value: j
+                                });
+                            });
+                        });
                     });
                 }
             },
diff --git a/public/install.php b/public/install.php
index 971c968..c407b0e 100644
--- a/public/install.php
+++ b/public/install.php
@@ -4,7 +4,7 @@
  * 
  * 安装完成后建议删除此文件
  * @author Karson
- * @website http://www.fastadmin.net
+ * @website https://www.fastadmin.net
  */
 // error_reporting(E_ERROR | E_WARNING | E_PARSE | E_NOTICE);
 // ini_set('display_errors', '1');
@@ -41,9 +41,9 @@ $sitename = "FastAdmin";
 $link = array(
     'qqun'  => "https://jq.qq.com/?_wv=1027&amp;k=487PNBb",
     'gitee' => 'https://gitee.com/karson/fastadmin/attach_files',
-    'home'  => 'http://www.fastadmin.net?ref=install',
-    'forum' => 'http://forum.fastadmin.net?ref=install',
-    'doc'   => 'http://doc.fastadmin.net?ref=install',
+    'home'  => 'https://www.fastadmin.net?ref=install',
+    'forum' => 'https://forum.fastadmin.net?ref=install',
+    'doc'   => 'https://doc.fastadmin.net?ref=install',
 );
 
 // 检测目录是否存在
@@ -77,7 +77,7 @@ else if (!extension_loaded("PDO"))
 }
 else if (!is_really_writable($dbConfigFile))
 {
-    $errInfo = '当前权限不足,无法写入配置文件application/database.php<br><a href="http://forum.fastadmin.net/?q=%E6%9D%83%E9%99%90%E4%B8%8D%E8%B6%B3" target="_blank">点击查看解决办法</a>';
+    $errInfo = '当前权限不足,无法写入配置文件application/database.php<br><a href="https://forum.fastadmin.net/?q=%E6%9D%83%E9%99%90%E4%B8%8D%E8%B6%B3" target="_blank">点击查看解决办法</a>';
 }
 else
 {
@@ -86,7 +86,7 @@ else
     {
         if (!is_dir(ROOT_PATH . $v))
         {
-            $errInfo = '当前代码仅包含核心代码,请前往官网下载完整包或资源包覆盖后再尝试安装,<a href="http://www.fastadmin.net/download.html?ref=install" target="_blank">立即前往下载</a>';
+            $errInfo = '当前代码仅包含核心代码,请前往官网下载完整包或资源包覆盖后再尝试安装,<a href="https://www.fastadmin.net/download.html?ref=install" target="_blank">立即前往下载</a>';
             break;
         }
     }
--
libgit2 0.24.0