Compare commits

...

8 Commits

Author SHA1 Message Date
Karson 03a0e0dd4e 优化url检测
(cherry picked from commit dc466cb1c9)
2024-02-15 22:53:42 +08:00
Karson fb5b78e777 优化菜单规则管理
(cherry picked from commit 31a6a47dc3)
2024-02-15 22:52:59 +08:00
Karson 20dea7cb5b 优化CRUD生成
(cherry picked from commit a40420b8cd)
2024-02-15 22:52:29 +08:00
Karson 57c2f23106 增强后台安全限制
优化登录判断机制
移除冗余代码

(cherry picked from commit 7e6314a701)
2024-02-15 22:52:01 +08:00
Karson b44f3791b1 修复语言检测未匹配时的错误
优化分片上传

(cherry picked from commit ae57feebb6)
2024-02-15 22:42:49 +08:00
Karson 4f466551e2 新增自定义错误码模板配置
(cherry picked from commit 4845b08077)
2024-02-15 22:33:55 +08:00
Karson 1066b4108b 优化权限规则列表显示
(cherry picked from commit 2c66c67d71)
2024-02-15 22:28:29 +08:00
Karson 4badf65bd2 优化获取类名兼容性
(cherry picked from commit a9003ecce4)
2024-02-15 22:18:55 +08:00
23 changed files with 284 additions and 143 deletions

View File

@ -98,13 +98,13 @@ class Api extends Command
foreach ($files as $name => $file) {
if (!$file->isDir() && $file->getExtension() == 'php') {
$filePath = $file->getRealPath();
$classes[] = $this->get_class_from_file($filePath);
$classes[] = $this->getClassFromFile($filePath);
}
}
} else {
foreach ($controller as $index => $item) {
$filePath = $moduleDir . Config::get('url_controller_layer') . DS . $item . '.php';
$classes[] = $this->get_class_from_file($filePath);
$classes[] = $this->getClassFromFile($filePath);
}
}
@ -129,67 +129,61 @@ class Api extends Command
}
/**
* get full qualified class name
* 从文件获取命名空间和类名
*
* @param string $path_to_file
* @param string $filename
* @return string
* @author JBYRNE http://jarretbyrne.com/2015/06/197/
*/
protected function get_class_from_file($path_to_file)
protected function getClassFromFile($filename)
{
//Grab the contents of the file
$contents = file_get_contents($path_to_file);
//Start with a blank namespace and class
$namespace = $class = "";
//Set helper values to know that we have found the namespace/class token and need to collect the string values after them
$getting_namespace = $getting_class = false;
//Go through each token and evaluate it as necessary
foreach (token_get_all($contents) as $token) {
//If this token is the namespace declaring, then flag that the next tokens will be the namespace name
if (is_array($token) && $token[0] == T_NAMESPACE) {
$getting_namespace = true;
}
//If this token is the class declaring, then flag that the next tokens will be the class name
if (is_array($token) && $token[0] == T_CLASS) {
$getting_class = true;
}
//While we're grabbing the namespace name...
if ($getting_namespace === true) {
//If the token is a string or the namespace separator...
if (is_array($token) && in_array($token[0], version_compare(PHP_VERSION, '8.0.0', '<') ? [T_STRING, T_NS_SEPARATOR] : [T_NAME_QUALIFIED])) {
//Append the token's value to the name of the namespace
$namespace .= $token[1];
} elseif ($token === ';') {
//If the token is the semicolon, then we're done with the namespace declaration
$getting_namespace = false;
}
}
//While we're grabbing the class name...
if ($getting_class === true) {
//If the token is a string, it's the name of the class
if (is_array($token) && $token[0] == T_STRING) {
//Store the token's value as the class name
$class = $token[1];
//Got what we need, stope here
break;
$getNext = null;
$isNamespace = false;
$skipNext = false;
$namespace = '';
$class = '';
foreach (\PhpToken::tokenize(file_get_contents($filename)) as $token) {
if (!$token->isIgnorable()) {
$name = $token->getTokenName();
switch ($name) {
case 'T_NAMESPACE':
$isNamespace = true;
break;
case 'T_EXTENDS':
case 'T_USE':
case 'T_IMPLEMENTS':
$skipNext = true;
break;
case 'T_CLASS':
if ($skipNext) {
$skipNext = false;
} else {
$getNext = strtolower(substr($name, 2));
}
break;
case 'T_NAME_QUALIFIED':
case 'T_NS_SEPARATOR':
case 'T_STRING':
case ';':
if ($isNamespace) {
if ($name == ';') {
$isNamespace = false;
} else {
$namespace .= $token->text;
}
} elseif ($skipNext) {
$skipNext = false;
} elseif ($getNext == 'class') {
$class = $token->text;
$getNext = null;
break 2;
}
break;
default:
$getNext = null;
}
}
}
//Build the fully-qualified class name and return it
return $namespace ? $namespace . '\\' . $class : $class;
return $namespace . '\\' . $class;
}
}

View File

@ -712,6 +712,7 @@ class Crud extends Command
$headingHtml = '{:build_heading()}';
$controllerImport = '';
$importHtml = '';
$multipleHtml = '';
$recyclebinHtml = '';
if ($import) {
@ -996,6 +997,7 @@ class Crud extends Command
}
if ($this->headingFilterField && $this->headingFilterField == $field && $itemArr) {
$headingHtml = $this->getReplacedStub('html/heading-html', ['field' => $field, 'fieldName' => Loader::parseName($field, 1, false)]);
$multipleHtml = $this->getReplacedStub('html/multiple-html', ['field' => $field, 'fieldName' => Loader::parseName($field, 1, false), 'controllerUrl' => $controllerUrl]);
}
//排序方式,如果有指定排序字段,否则按主键排序
$order = $field == $this->sortField ? $this->sortField : $order;
@ -1119,6 +1121,7 @@ class Crud extends Command
'controllerIndex' => '',
'recyclebinJs' => '',
'headingHtml' => $headingHtml,
'multipleHtml' => $multipleHtml,
'importHtml' => $importHtml,
'recyclebinHtml' => $recyclebinHtml,
'visibleFieldList' => $fields ? "\$row->visible(['" . implode("','", array_filter(in_array($priKey, explode(',', $fields)) ? explode(',', $fields) : explode(',', $priKey . ',' . $fields))) . "']);" : '',
@ -1466,7 +1469,7 @@ EOD;
if ($content || !Lang::has($field)) {
$this->fieldMaxLen = strlen($field) > $this->fieldMaxLen ? strlen($field) : $this->fieldMaxLen;
$content = str_replace('', ',', $content);
if (stripos($content, ':') !== false && stripos($content, ',') && stripos($content, '=') !== false) {
if (stripos($content, ':') !== false && stripos($content, '=') !== false) {
list($fieldLang, $item) = explode(':', $content);
$itemArr = [$field => $fieldLang];
foreach (explode(',', $item) as $k => $v) {
@ -1474,6 +1477,9 @@ EOD;
if (count($valArr) == 2) {
list($key, $value) = $valArr;
$itemArr[$field . ' ' . $key] = $value;
if ($this->headingFilterField == $field) {
$itemArr['Set ' . $field . ' to ' . $key] = '设为' . $value;
}
$this->fieldMaxLen = strlen($field . ' ' . $key) > $this->fieldMaxLen ? strlen($field . ' ' . $key) : $this->fieldMaxLen;
}
}
@ -1549,7 +1555,7 @@ EOD;
return $itemArr;
}
protected function getFieldType(& $v)
protected function getFieldType(&$v)
{
$inputType = 'text';
switch ($v['DATA_TYPE']) {

View File

@ -0,0 +1,8 @@
<div class="dropdown btn-group {:$auth->check('{%controllerUrl%}/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">
{foreach name="{%fieldName%}List" item="vo"}
<li><a class="btn btn-link btn-multi btn-disabled disabled" href="javascript:" data-params="{%field%}={$key}">{:__('Set {%field%} to ' . $key)}</a></li>
{/foreach}
</ul>
</div>

View File

@ -12,13 +12,7 @@
<a href="javascript:;" class="btn btn-danger btn-del btn-disabled disabled {:$auth->check('{%controllerUrl%}/del')?'':'hide'}" title="{:__('Delete')}" ><i class="fa fa-trash"></i> {:__('Delete')}</a>
{%importHtml%}
<div class="dropdown btn-group {:$auth->check('{%controllerUrl%}/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>
{%multipleHtml%}
{%recyclebinHtml%}
</div>

View File

@ -423,7 +423,7 @@ CREATE TABLE `fa_test` (
-- Records of fa_test
-- ----------------------------
BEGIN;
INSERT INTO `fa_test` VALUES (1, 1, 1, 12, '12,13', '互联网,计算机', 'monday', 'hot,index', 'male', 'music,reading', '我是一篇测试文章', '<p>我是测试内容</p>', '/assets/img/avatar.png', '/assets/img/avatar.png,/assets/img/qrcode.png', '/assets/img/avatar.png', '关键字', '描述', '广西壮族自治区/百色市/平果县', '{\"a\":\"1\",\"b\":\"2\"}', '[{\"title\":\"标题一\",\"intro\":\"介绍一\",\"author\":\"小明\",\"age\":\"21\"}]', 0.00, 0, '2020-10-01 00:00:00 - 2021-10-31 23:59:59', '2017-07-10', '2017-07-10 18:24:45', 2017, '18:24:45', 1491635035, 1491635035, 1491635035, NULL, 0, 1, 'normal', '1');
INSERT INTO `fa_test` VALUES (1, 1, 1, 12, '12,13', '互联网,计算机', 'monday', 'hot,index', 'male', 'music,reading', '我是一篇测试文章', '<p>我是测试内容</p>', '/assets/img/avatar.png', '/assets/img/avatar.png,/assets/img/qrcode.png', '/assets/img/avatar.png', '关键字', '我是一篇测试文章描述,内容过多时将自动隐藏', '广西壮族自治区/百色市/平果县', '{\"a\":\"1\",\"b\":\"2\"}', '[{\"title\":\"标题一\",\"intro\":\"介绍一\",\"author\":\"小明\",\"age\":\"21\"}]', 0.00, 0, '2020-10-01 00:00:00 - 2021-10-31 23:59:59', '2017-07-10', '2017-07-10 18:24:45', 2017, '18:24:45', 1491635035, 1491635035, 1491635035, NULL, 0, 1, 'normal', '1');
COMMIT;
-- ----------------------------

View File

@ -70,6 +70,8 @@ class Index extends Backend
if ($this->auth->isLogin()) {
$this->success(__("You've logged in, do not login again"), $url);
}
//保持会话有效时长,单位:小时
$keeyloginhours = 24;
if ($this->request->isPost()) {
$username = $this->request->post('username');
$password = $this->request->post('password');
@ -95,7 +97,7 @@ class Index extends Backend
$this->error($validate->getError(), $url, ['token' => $this->request->token()]);
}
AdminLog::setTitle(__('Login'));
$result = $this->auth->login($username, $password, $keeplogin ? 86400 : 0);
$result = $this->auth->login($username, $password, $keeplogin ? $keeyloginhours * 3600 : 0);
if ($result === true) {
Hook::listen("admin_login_after", $this->request);
$this->success(__('Login successful'), $url, ['url' => $url, 'id' => $this->auth->id, 'username' => $username, 'avatar' => $this->auth->avatar]);
@ -113,6 +115,7 @@ class Index extends Backend
}
$background = Config::get('fastadmin.login_background');
$background = $background ? (stripos($background, 'http') === 0 ? $background : config('site.cdnurl') . $background) : '';
$this->view->assign('keeyloginhours', $keeyloginhours);
$this->view->assign('background', $background);
$this->view->assign('title', __('Login'));
Hook::listen("admin_login_init", $this->request);

View File

@ -129,7 +129,7 @@ class Admin extends Backend
exception(__("Please input correct password"));
}
$params['salt'] = Random::alnum();
$params['password'] = md5(md5($params['password']) . $params['salt']);
$params['password'] = $this->auth->getEncryptPassword($params['password'], $params['salt']);
$params['avatar'] = '/assets/img/avatar.png'; //设置新管理员默认头像。
$result = $this->model->validate('Admin.add')->save($params);
if ($result === false) {
@ -183,7 +183,7 @@ class Admin extends Backend
exception(__("Please input correct password"));
}
$params['salt'] = Random::alnum();
$params['password'] = md5(md5($params['password']) . $params['salt']);
$params['password'] = $this->auth->getEncryptPassword($params['password'], $params['salt']);
} else {
unset($params['password'], $params['salt']);
}
@ -192,7 +192,7 @@ class Admin extends Backend
$adminValidate->rule([
'username' => 'require|regex:\w{3,30}|unique:admin,username,' . $row->id,
'email' => 'require|email|unique:admin,email,' . $row->id,
'mobile' => 'regex:1[3-9]\d{9}|unique:admin,mobile,' . $row->id,
'mobile' => 'regex:1[3-9]\d{9}|unique:admin,mobile,' . $row->id,
'password' => 'regex:\S{32}',
]);
$result = $row->validate('Admin.edit')->save($params);

View File

@ -36,7 +36,7 @@ class Rule extends Backend
$v['title'] = __($v['title']);
}
unset($v);
Tree::instance()->init($ruleList);
Tree::instance()->init($ruleList)->icon = ['&nbsp;&nbsp;&nbsp;&nbsp;', '&nbsp;&nbsp;&nbsp;&nbsp;', '&nbsp;&nbsp;&nbsp;&nbsp;'];
$this->rulelist = Tree::instance()->getTreeList(Tree::instance()->getTreeArray(0), 'title');
$ruledata = [0 => __('None')];
foreach ($this->rulelist as $k => &$v) {

View File

@ -32,7 +32,7 @@ class Rule extends Backend
$v['remark'] = __($v['remark']);
}
unset($v);
Tree::instance()->init($ruleList);
Tree::instance()->init($ruleList)->icon = ['&nbsp;&nbsp;&nbsp;&nbsp;', '&nbsp;&nbsp;&nbsp;&nbsp;', '&nbsp;&nbsp;&nbsp;&nbsp;'];
$this->rulelist = Tree::instance()->getTreeList(Tree::instance()->getTreeArray(0), 'title');
$ruledata = [0 => __('None')];
foreach ($this->rulelist as $k => &$v) {

View File

@ -124,6 +124,8 @@ return [
'%d year%s after' => '%d年后',
'Set to normal' => '设为正常',
'Set to hidden' => '设为隐藏',
'Set status to normal' => '设为正常',
'Set status to hidden' => '设为隐藏',
'Recycle bin' => '回收站',
'Restore' => '还原',
'Restore all' => '还原全部',

View File

@ -65,5 +65,6 @@ return [
'Forum' => '交流社区',
'QQ qun' => 'QQ交流群',
'Captcha' => '验证码',
'The duration of the session is %s hours' => '设定会话有效时长为%s小时',
'Security tips' => '<i class="fa fa-warning"></i> 安全提示为了你的后台安全请勿将后台管理入口设置为admin或admin.php',
];

View File

@ -12,5 +12,8 @@ return [
'Menu tips' => '规则任意,请不可重复,仅做层级显示,无需匹配控制器和方法',
'Node tips' => '模块/控制器/方法名',
'Weigh' => '权重',
'Status' => '状态'
'Status' => '状态',
'Toggle all' => '显示全部',
'Toggle menu visible' => '点击切换菜单显示',
'Toggle sub menu' => '点击切换子菜单',
];

View File

@ -51,7 +51,7 @@ class Auth extends \fast\Auth
$this->setError('Please try again after 1 day');
return false;
}
if ($admin->password != md5(md5($password) . $admin->salt)) {
if ($admin->password != $this->getEncryptPassword($password, $admin->salt)) {
$admin->loginfailure++;
$admin->save();
$this->setError('Password is incorrect');
@ -63,7 +63,8 @@ class Auth extends \fast\Auth
$admin->token = Random::uuid();
$admin->save();
Session::set("admin", $admin->toArray());
$this->keeplogin($keeptime);
Session::set("admin.safecode", $this->getEncryptSafecode($admin));
$this->keeplogin($admin, $keeptime);
return true;
}
@ -101,7 +102,7 @@ class Auth extends \fast\Auth
return false;
}
//token有变更
if ($key != md5(md5($id) . md5($keeptime) . md5($expiretime) . $admin->token . config('token.key'))) {
if ($key != $this->getKeeploginKey($admin, $keeptime, $expiretime)) {
return false;
}
$ip = request()->ip();
@ -110,8 +111,9 @@ class Auth extends \fast\Auth
return false;
}
Session::set("admin", $admin->toArray());
Session::set("admin.safecode", $this->getEncryptSafecode($admin));
//刷新自动登录的时效
$this->keeplogin($keeptime);
$this->keeplogin($admin, $keeptime);
return true;
} else {
return false;
@ -124,18 +126,64 @@ class Auth extends \fast\Auth
* @param int $keeptime
* @return boolean
*/
protected function keeplogin($keeptime = 0)
protected function keeplogin($admin, $keeptime = 0)
{
if ($keeptime) {
$expiretime = time() + $keeptime;
$key = md5(md5($this->id) . md5($keeptime) . md5($expiretime) . $this->token . config('token.key'));
$data = [$this->id, $keeptime, $expiretime, $key];
Cookie::set('keeplogin', implode('|', $data), 86400 * 7);
$key = $this->getKeeploginKey($admin, $keeptime, $expiretime);
Cookie::set('keeplogin', implode('|', [$admin['id'], $keeptime, $expiretime, $key]), $keeptime);
return true;
}
return false;
}
/**
* 获取密码加密后的字符串
* @param string $password 密码
* @param string $salt 密码盐
* @return string
*/
public function getEncryptPassword($password, $salt = '')
{
return md5(md5($password) . $salt);
}
/**
* 获取密码加密后的自动登录码
* @param string $password 密码
* @param string $salt 密码盐
* @return string
*/
public function getEncryptKeeplogin($params, $keeptime)
{
$expiretime = time() + $keeptime;
$key = md5(md5($params['id']) . md5($keeptime) . md5($expiretime) . $params['token'] . config('token.key'));
return implode('|', [$this->id, $keeptime, $expiretime, $key]);
}
/**
* 获取自动登录Key
* @param $params
* @param $keeptime
* @param $expiretime
* @return string
*/
public function getKeeploginKey($params, $keeptime, $expiretime)
{
$key = md5(md5($params['id']) . md5($keeptime) . md5($expiretime) . $params['token'] . config('token.key'));
return $key;
}
/**
* 获取加密后的安全码
* @param $params
* @return string
*/
public function getEncryptSafecode($params)
{
return md5(md5($params['username']) . md5(substr($params['password'], 0, 6)) . config('token.key'));
}
public function check($name, $uid = '', $relation = 'or', $mode = 'url')
{
$uid = $uid ? $uid : $this->id;
@ -180,13 +228,19 @@ class Auth extends \fast\Auth
if (!$admin) {
return false;
}
$my = Admin::get($admin['id']);
if (!$my) {
return false;
}
//校验安全码,可用于判断关键信息发生了变更需要重新登录
if (!isset($admin['safecode']) || $this->getEncryptSafecode($my) !== $admin['safecode']) {
$this->logout();
return false;
}
//判断是否同一时间同一账号只能在一个地方登录
if (Config::get('fastadmin.login_unique')) {
$my = Admin::get($admin['id']);
if (!$my || $my['token'] != $admin['token']) {
$this->logined = false; //重置登录状态
Session::delete("admin");
Cookie::delete("keeplogin");
if ($my['token'] != $admin['token']) {
$this->logout();
return false;
}
}

View File

@ -13,22 +13,20 @@ class Admin extends Model
// 定义时间戳字段名
protected $createTime = 'createtime';
protected $updateTime = 'updatetime';
protected $hidden = [
'password',
'salt'
];
/**
* 重置用户密码
* @author baiyouwen
*/
public function resetPassword($uid, $NewPassword)
public static function init()
{
$passwd = $this->encryptPassword($NewPassword);
$ret = $this->where(['id' => $uid])->update(['password' => $passwd]);
return $ret;
}
// 密码加密
protected function encryptPassword($password, $salt = '', $encrypt = 'md5')
{
return $encrypt($password . $salt);
self::beforeWrite(function ($row) {
$changed = $row->getChangedData();
//如果修改了用户或或密码则需要重新登录
if (isset($changed['username']) || isset($changed['password']) || isset($changed['salt'])) {
$row->token = '';
}
});
}
}

View File

@ -28,7 +28,7 @@
box-shadow: 0 0 30px rgba(0, 0, 0, 0.1);
background: rgba(255, 255, 255, 1);
border: none;
overflow: hidden;
/*overflow: hidden;*/
padding: 0;
}
@ -55,6 +55,7 @@
.login-head {
background: #899fe1;
border-radius: 3px 3px 0 0;
}
.login-form {
@ -122,12 +123,14 @@
</div>
{/if}
<!--@CaptchaEnd-->
{if $keeyloginhours>0}
<div class="form-group checkbox">
<label class="inline" for="keeplogin">
<label class="inline" for="keeplogin" data-toggle="tooltip" title="{:__('The duration of the session is %s hours', $keeyloginhours)}">
<input type="checkbox" name="keeplogin" id="keeplogin" value="1"/>
{:__('Keep login')}
</label>
</div>
{/if}
<div class="form-group">
<button type="submit" class="btn btn-success btn-lg btn-block" style="background:#708eea;">{:__('Sign in')}</button>
</div>

View File

@ -14,6 +14,7 @@
<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>
<a href="javascript:;" class="btn btn-danger btn-toggle-all"><i class="fa fa-plus"></i> {:__('Toggle all')}</a>
</div>
<table id="table" class="table table-striped table-bordered table-hover table-nowrap"
data-operate-edit="{:$auth->check('user/rule/edit')}"

View File

@ -518,8 +518,8 @@ if (!function_exists('check_url_allowed')) {
//如果是站外链接则需要判断HOST是否允许
if (preg_match("/((http[s]?:\/\/)+(?>[a-z\-0-9]{2,}\.){1,}[a-z]{2,8})(?:\s|\/)/i", $url)) {
if (in_array(strtolower(parse_url($url, PHP_URL_HOST)), $allowedHostArr)) {
$chkHost = parse_url(strtolower($url), PHP_URL_HOST);
if ($chkHost && in_array($chkHost, $allowedHostArr)) {
return true;
}
}

View File

@ -305,7 +305,7 @@ class Upload
$attachment = $this->upload();
} catch (\Exception $e) {
@unlink($destFile);
@unlink($uploadPath);
throw new UploadException($e->getMessage());
}
return $attachment;

View File

@ -0,0 +1 @@
页面未找到

View File

@ -19,7 +19,7 @@ if (isset($_GET['lang'])) {
$langSet = strtolower($_COOKIE['think_var']);
} elseif (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
preg_match('/^([a-z\d\-]+)/i', $_SERVER['HTTP_ACCEPT_LANGUAGE'], $matches);
$langSet = strtolower($matches[1]);
$langSet = strtolower($matches[1] ?? '');
}
$langSet = $langSet && in_array($langSet, ['zh-cn', 'en']) ? $langSet : 'zh-cn';
$langSet == 'en' && $lang = array_combine(array_keys($lang), array_keys($lang));

View File

@ -42,7 +42,7 @@ return [
// 默认时区
'default_timezone' => 'PRC',
// 是否开启多语言
'lang_switch_on' => true,
'lang_switch_on' => false,
// 默认全局过滤方法 用逗号分隔多个
'default_filter' => '',
// 默认语言
@ -151,6 +151,11 @@ return [
// +----------------------------------------------------------------------
// 异常页面的模板文件
'exception_tmpl' => APP_PATH . 'common' . DS . 'view' . DS . 'tpl' . DS . 'think_exception.tpl',
// 自定义错误码模板
'http_exception_template' => [
// 定义404错误的模板渲染
// 404 => APP_PATH . 'common/view/tpl/404.tpl',
],
// 错误显示信息,非调试模式有效
'error_message' => '你所浏览的页面暂时无法访问',
// 显示错误信息

View File

@ -25,7 +25,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
[
{field: 'state', checkbox: true,},
{field: 'id', title: 'ID'},
{field: 'title', title: __('Title'), align: 'left', formatter: Controller.api.formatter.title},
{field: 'title', title: __('Title'), align: 'left', formatter: Controller.api.formatter.title, clickToSelect: !false},
{field: 'icon', title: __('Icon'), formatter: Controller.api.formatter.icon},
{field: 'name', title: __('Name'), align: 'left', formatter: Controller.api.formatter.name},
{field: 'weigh', title: __('Weigh')},
@ -37,12 +37,6 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
table: table,
formatter: Table.api.formatter.toggle
},
{
field: 'id',
title: '<a href="javascript:;" class="btn btn-success btn-xs btn-toggle" style="border-top:none;"><i class="fa fa-chevron-up"></i></a>',
operate: false,
formatter: Controller.api.formatter.subnode
},
{
field: 'operate',
title: __('Operate'),
@ -56,10 +50,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
search: false,
commonSearch: false,
rowAttributes: function (row, index) {
if (this.totalRows > 500) {
return row.pid == 0 ? {} : {style: "display:none"};
}
return row.haschild == 1 || row.ismenu == 1 ? {} : {style: "display:none"};
return row.pid == 0 ? {} : {style: "display:none"};
}
});
@ -101,14 +92,30 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
});
//显示隐藏子节点
$(document).on("click", ".btn-node-sub", function (e) {
var status = $(this).data("shown") ? true : false;
$("a.btn[data-pid='" + $(this).data("id") + "']").each(function () {
$(this).closest("tr").toggle(!status);
table.on('post-body.bs.table', function (e, settings, json, xhr) {
//显示隐藏子节点
$(">tbody>tr[data-index] > td", this).on('click', "a.btn-node-sub", function () {
var status = $(this).data("shown") ? true : false;
$("a[data-pid='" + $(this).data("id") + "']").each(function () {
$(this).closest("tr").toggle(!status);
});
if (status) {
$("a[data-pid='" + $(this).data("id") + "']").trigger("collapse");
}
$(this).data("shown", !status);
$("i", this).toggleClass("fa-caret-down").toggleClass("fa-caret-right");
return false;
});
$(this).data("shown", !status);
return false;
});
//隐藏子节点
$(document).on("collapse", ".btn-node-sub", function () {
if ($("i", this).length > 0) {
$("a[data-pid='" + $(this).data("id") + "']").trigger("collapse");
}
$("i", this).removeClass("fa-caret-down").addClass("fa-caret-right");
$(this).data("shown", false);
$(this).closest("tr").toggle(false);
});
//批量删除后的回调
@ -118,12 +125,11 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
//展开隐藏一级
$(document.body).on("click", ".btn-toggle", function (e) {
$("a.btn[data-id][data-pid][data-pid!=0].disabled").closest("tr").hide();
$("a[data-id][data-pid][data-pid!=0].disabled").closest("tr").hide();
var that = this;
var show = $("i", that).hasClass("fa-chevron-down");
$("i", that).toggleClass("fa-chevron-down", !show);
$("i", that).toggleClass("fa-chevron-up", show);
$("a.btn[data-id][data-pid][data-pid!=0]").not('.disabled').closest("tr").toggle(show);
$("i", that).toggleClass("fa-chevron-down", !show).toggleClass("fa-chevron-up", show);
$("a[data-id][data-pid][data-pid!=0]").not('.disabled').closest("tr").toggle(show);
$(".btn-node-sub[data-pid=0]").data("shown", show);
});
@ -131,10 +137,10 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
$(document.body).on("click", ".btn-toggle-all", function (e) {
var that = this;
var show = $("i", that).hasClass("fa-plus");
$("i", that).toggleClass("fa-plus", !show);
$("i", that).toggleClass("fa-minus", show);
$(".btn-node-sub.disabled").closest("tr").toggle(show);
$("i", that).toggleClass("fa-plus", !show).toggleClass("fa-minus", show);
$(".btn-node-sub:not([data-pid=0])").closest("tr").toggle(show);
$(".btn-node-sub").data("shown", show);
$(".btn-node-sub > i").toggleClass("fa-caret-down", show).toggleClass("fa-caret-right", !show);
});
},
add: function () {
@ -147,17 +153,18 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
formatter: {
title: function (value, row, index) {
value = value.toString().replace(/(&|&amp;)nbsp;/g, '&nbsp;');
return !row.ismenu || row.status == 'hidden' ? "<span class='text-muted'>" + value + "</span>" : value;
var caret = row.haschild == 1 || row.ismenu == 1 ? '<i class="fa fa-caret-right"></i>' : '';
value = value.indexOf("&nbsp;") > -1 ? value.replace(/(.*)&nbsp;/, "$1" + caret) : caret + value;
value = !row.ismenu || row.status == 'hidden' ? "<span class='text-muted'>" + value + "</span>" : value;
return '<a href="javascript:;" data-id="' + row.id + '" data-pid="' + row.pid + '" class="'
+ (row.haschild == 1 || row.ismenu == 1 ? 'text-primary' : 'disabled') + ' btn-node-sub">' + value + '</a>';
},
name: function (value, row, index) {
return !row.ismenu || row.status == 'hidden' ? "<span class='text-muted'>" + value + "</span>" : value;
},
icon: function (value, row, index) {
return '<span class="' + (!row.ismenu || row.status == 'hidden' ? 'text-muted' : '') + '"><i class="' + value + '"></i></span>';
},
subnode: function (value, row, index) {
return '<a href="javascript:;" data-toggle="tooltip" title="' + __('Toggle sub menu') + '" data-id="' + row.id + '" data-pid="' + row.pid + '" class="btn btn-xs '
+ (row.haschild == 1 || row.ismenu == 1 ? 'btn-success' : 'btn-default disabled') + ' btn-node-sub"><i class="fa fa-sitemap"></i></a>';
}
},
bindevent: function () {

View File

@ -21,18 +21,16 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
url: $.fn.bootstrapTable.defaults.extend.index_url,
pk: 'id',
sortName: 'weigh',
escape: false,
columns: [
[
{checkbox: true},
{field: 'id', title: __('Id')},
{field: 'pid', title: __('Pid'), visible: false},
{field: 'title', title: __('Title'), align: 'left', formatter:function (value, row, index) {
return value.toString().replace(/(&|&amp;)nbsp;/g, '&nbsp;');
}
},
{field: 'name', title: __('Name'), align: 'left'},
{field: 'title', title: __('Title'), align: 'left', formatter: Controller.api.formatter.title},
{field: 'name', title: __('Name'), align: 'left', formatter: Controller.api.formatter.name},
{field: 'remark', title: __('Remark')},
{field: 'ismenu', title: __('Ismenu'), formatter: Table.api.formatter.toggle},
// {field: 'ismenu', title: __('Ismenu'), formatter: Table.api.formatter.toggle},
{field: 'createtime', title: __('Createtime'), formatter: Table.api.formatter.datetime, operate: 'RANGE', addclass: 'datetimerange', sortable: true, visible: false},
{field: 'updatetime', title: __('Updatetime'), formatter: Table.api.formatter.datetime, operate: 'RANGE', addclass: 'datetimerange', sortable: true, visible: false},
{field: 'weigh', title: __('Weigh')},
@ -43,10 +41,59 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
pagination: false,
search: false,
commonSearch: false,
rowAttributes: function (row, index) {
return row.pid == 0 ? {} : {style: "display:none"};
}
});
// 为表格绑定事件
Table.api.bindevent(table);
table.on('post-body.bs.table', function (e, settings, json, xhr) {
//显示隐藏子节点
$(">tbody>tr[data-index] > td", this).on('click', "a.btn-node-sub", function () {
var status = $(this).data("shown") ? true : false;
$("a[data-pid='" + $(this).data("id") + "']").each(function () {
$(this).closest("tr").toggle(!status);
});
if (status) {
$("a[data-pid='" + $(this).data("id") + "']").trigger("collapse");
}
$(this).data("shown", !status);
$("i", this).toggleClass("fa-caret-down").toggleClass("fa-caret-right");
return false;
});
});
//隐藏子节点
$(document).on("collapse", ".btn-node-sub", function () {
if ($("i", this).length > 0) {
$("a[data-pid='" + $(this).data("id") + "']").trigger("collapse");
}
$("i", this).removeClass("fa-caret-down").addClass("fa-caret-right");
$(this).data("shown", false);
$(this).closest("tr").toggle(false);
});
//展开隐藏一级
$(document.body).on("click", ".btn-toggle", function (e) {
$("a[data-id][data-pid][data-pid!=0].disabled").closest("tr").hide();
var that = this;
var show = $("i", that).hasClass("fa-chevron-down");
$("i", that).toggleClass("fa-chevron-down", !show).toggleClass("fa-chevron-up", show);
$("a[data-id][data-pid][data-pid!=0]").not('.disabled').closest("tr").toggle(show);
$(".btn-node-sub[data-pid=0]").data("shown", show);
});
//展开隐藏全部
$(document.body).on("click", ".btn-toggle-all", function (e) {
var that = this;
var show = $("i", that).hasClass("fa-plus");
$("i", that).toggleClass("fa-plus", !show).toggleClass("fa-minus", show);
$(".btn-node-sub:not([data-pid=0])").closest("tr").toggle(show);
$(".btn-node-sub").data("shown", show);
$(".btn-node-sub > i").toggleClass("fa-caret-down", show).toggleClass("fa-caret-right", !show);
});
},
add: function () {
Controller.api.bindevent();
@ -55,6 +102,20 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
Controller.api.bindevent();
},
api: {
formatter: {
title: function (value, row, index) {
value = value.toString().replace(/(&|&amp;)nbsp;/g, '&nbsp;');
var caret = row.haschild == 1 || row.ismenu == 1 ? '<i class="fa fa-caret-right"></i>' : '';
value = value.indexOf("&nbsp;") > -1 ? value.replace(/(.*)&nbsp;/, "$1" + caret) : caret + value;
value = !row.ismenu || row.status == 'hidden' ? "<span class='text-muted'>" + value + "</span>" : value;
return '<a href="javascript:;" data-id="' + row.id + '" data-pid="' + row.pid + '" class="'
+ (row.haschild == 1 || row.ismenu == 1 ? 'text-primary' : 'disabled') + ' btn-node-sub">' + value + '</a>';
},
name: function (value, row, index) {
return !row.ismenu || row.status == 'hidden' ? "<span class='text-muted'>" + value + "</span>" : value;
},
},
bindevent: function () {
$(document).on('click', "input[name='row[ismenu]']", function () {
var name = $("input[name='row[name]']");