Compare commits

...

7 Commits

Author SHA1 Message Date
Karson 10150d7f13 优化权限规则 2026-01-07 18:13:26 +08:00
Karson 063773b3b5 优化视图模板 2026-01-07 18:10:50 +08:00
Karson f3f86fb6c4 优化Switch组件 2025-12-18 15:14:14 +08:00
Karson cee5f9b252 优化上传通用返回方法,增加额外属性返回 2025-12-18 15:13:58 +08:00
Karson 5631ca6dec 优化重置密码后Token清除逻辑 2025-12-18 15:08:27 +08:00
Karson 14207687b4 优化switch组件未知状态展示和逻辑 2025-12-18 15:08:06 +08:00
Karson fdf92e550e 优化菜单规则支持子路径检测 2025-12-18 11:44:41 +08:00
19 changed files with 130 additions and 27 deletions

View File

@ -883,8 +883,8 @@ EOD;
$itemArr = [$yes => 'Yes', $no => 'No']; $itemArr = [$yes => 'Yes', $no => 'No'];
} }
$stateNoClass = 'fa-flip-horizontal text-gray'; $stateNoClass = 'fa-flip-horizontal text-gray';
$formAddElement = $this->getReplacedStub('html/' . $inputType, ['field' => $field, 'fieldName' => $fieldName, 'fieldYes' => $yes, 'fieldNo' => $no, 'attrStr' => Form::attributes($attrArr), 'fieldValue' => $defaultValue, 'fieldSwitchClass' => $defaultValue == $no ? $stateNoClass : '']); $formAddElement = $this->getReplacedStub('html/' . $inputType, ['field' => $field, 'fieldName' => $fieldName, 'fieldYes' => $yes, 'fieldNo' => $no, 'attrStr' => Form::attributes($attrArr), 'fieldValue' => $defaultValue, 'fieldSwitchClass' => $defaultValue == $yes ? '' : $stateNoClass]);
$formEditElement = $this->getReplacedStub('html/' . $inputType, ['field' => $field, 'fieldName' => $fieldName, 'fieldYes' => $yes, 'fieldNo' => $no, 'attrStr' => Form::attributes($attrArr), 'fieldValue' => "{\$row.{$field}}", 'fieldSwitchClass' => "{eq name=\"\$row.{$field}\" value=\"{$no}\"}fa-flip-horizontal text-gray{/eq}"]); $formEditElement = $this->getReplacedStub('html/' . $inputType, ['field' => $field, 'fieldName' => $fieldName, 'fieldYes' => $yes, 'fieldNo' => $no, 'attrStr' => Form::attributes($attrArr), 'fieldValue' => "{\$row.{$field}}", 'fieldSwitchClass' => "{neq name=\"\$row.{$field}\" value=\"{$yes}\"}fa-flip-horizontal text-gray{/neq}"]);
} elseif ($inputType == 'citypicker') { } elseif ($inputType == 'citypicker') {
$attrArr['class'] = implode(' ', $cssClassArr); $attrArr['class'] = implode(' ', $cssClassArr);
$attrArr['data-toggle'] = "city-picker"; $attrArr['data-toggle'] = "city-picker";

View File

@ -96,7 +96,7 @@ class Ajax extends Backend
} catch (UploadException $e) { } catch (UploadException $e) {
$this->error($e->getMessage()); $this->error($e->getMessage());
} }
$this->success(__('Uploaded successful'), '', ['url' => $attachment->url, 'fullurl' => cdnurl($attachment->url, true)]); $this->success(__('Uploaded successful'), '', $attachment->getBaseData());
} elseif ($method == 'clean') { } elseif ($method == 'clean') {
//删除冗余的分片文件 //删除冗余的分片文件
try { try {
@ -129,7 +129,7 @@ class Ajax extends Backend
$this->error($e->getMessage()); $this->error($e->getMessage());
} }
$this->success(__('Uploaded successful'), '', ['url' => $attachment->url, 'fullurl' => cdnurl($attachment->url, true)]); $this->success(__('Uploaded successful'), '', $attachment->getBaseData());
} }
} }

View File

@ -99,7 +99,8 @@ class Rule extends Backend
if (!$params['ismenu'] && !$params['pid']) { if (!$params['ismenu'] && !$params['pid']) {
$this->error(__('The non-menu rule must have parent')); $this->error(__('The non-menu rule must have parent'));
} }
$result = $this->model->validate()->save($params); $validate = \think\Loader::validate('AuthRule', 'validate', false, 'admin');
$result = $this->model->validate($validate)->save($params);
if ($result === false) { if ($result === false) {
$this->error($this->model->getError()); $this->error($this->model->getError());
} }
@ -137,11 +138,11 @@ class Rule extends Backend
} }
} }
//这里需要针对name做唯一验证 //这里需要针对name做唯一验证
$ruleValidate = \think\Loader::validate('AuthRule'); $ruleValidate = \think\Loader::validate('AuthRule', 'validate', false, 'admin');
$ruleValidate->rule([ $ruleValidate->rule([
'name' => 'require|unique:AuthRule,name,' . $row->id, 'name' => 'require|regex:format|unique:AuthRule,name,' . $row->id,
]); ]);
$result = $row->validate()->save($params); $result = $row->validate($ruleValidate)->save($params);
if ($result === false) { if ($result === false) {
$this->error($row->getError()); $this->error($row->getError());
} }

View File

@ -181,6 +181,10 @@ return [
'This page will be re-directed in %s seconds' => '页面将在 %s 秒后自动跳转', 'This page will be re-directed in %s seconds' => '页面将在 %s 秒后自动跳转',
'Click to uncheck all' => '点击取消全部', 'Click to uncheck all' => '点击取消全部',
'Multiple selection mode: %s checked' => '跨页选择模式,已选 %s 项', 'Multiple selection mode: %s checked' => '跨页选择模式,已选 %s 项',
'Please select switch status' => '请选择开关状态',
'switch status on' => '设定为开启',
'switch status off' => '设定为关闭',
'Status unknown, please manually change the status' => '状态未知,请手动修改开关状态',
//菜单 //菜单
'Dashboard' => '控制台', 'Dashboard' => '控制台',
'General' => '常规管理', 'General' => '常规管理',

View File

@ -16,7 +16,7 @@ class AuthRule extends Validate
* 验证规则 * 验证规则
*/ */
protected $rule = [ protected $rule = [
'name' => 'require|unique:AuthRule', 'name' => 'require|regex:format|unique:AuthRule',
'title' => 'require', 'title' => 'require',
]; ];
@ -24,7 +24,7 @@ class AuthRule extends Validate
* 提示消息 * 提示消息
*/ */
protected $message = [ protected $message = [
'name.format' => 'URL规则只能是小写字母、数字、下划线和/组成' 'name.regex' => 'URL规则只能是小写字母、数字、下划线和/组成'
]; ];
/** /**
@ -45,7 +45,7 @@ class AuthRule extends Validate
'name' => __('Name'), 'name' => __('Name'),
'title' => __('Title'), 'title' => __('Title'),
]; ];
$this->message['name.format'] = __('Name only supports letters, numbers, underscore and slash'); $this->message['name.regex'] = __('Name only supports letters, numbers, underscore and slash');
parent::__construct($rules, $message, $field); parent::__construct($rules, $message, $field);
} }

View File

@ -19,7 +19,7 @@
<!--如果不想在顶部显示角标,则给ul加上disable-top-badge类即可--> <!--如果不想在顶部显示角标,则给ul加上disable-top-badge类即可-->
<ul class="nav nav-tabs nav-addtabs disable-top-badge hidden-xs" role="tablist"> <ul class="nav nav-tabs nav-addtabs disable-top-badge hidden-xs" role="tablist">
{if !$Think.config.fastadmin.simplenav} {if !$Think.config.fastadmin.simplenav}
{$navlist} {:$navlist}
{/if} {/if}
</ul> </ul>

View File

@ -33,7 +33,7 @@
<ul class="sidebar-menu {if $Think.config.fastadmin.show_submenu}show-submenu{/if}"> <ul class="sidebar-menu {if $Think.config.fastadmin.show_submenu}show-submenu{/if}">
<!-- 菜单可以在 后台管理->权限管理->菜单规则 中进行增删改排序 --> <!-- 菜单可以在 后台管理->权限管理->菜单规则 中进行增删改排序 -->
{$menulist} {:$menulist}
</ul> </ul>
</section> </section>

View File

@ -20,6 +20,6 @@
<![endif]--> <![endif]-->
<script type="text/javascript"> <script type="text/javascript">
var require = { var require = {
config: {$config|json_encode} config: {:json_encode($config)}
}; };
</script> </script>

View File

@ -4,7 +4,7 @@
<div class="form-group"> <div class="form-group">
<label class="control-label col-xs-12 col-sm-2">{:__('Group')}:</label> <label class="control-label col-xs-12 col-sm-2">{:__('Group')}:</label>
<div class="col-xs-12 col-sm-4"> <div class="col-xs-12 col-sm-4">
{$groupList} {:$groupList}
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">

View File

@ -106,7 +106,7 @@ class Common extends Api
} catch (UploadException $e) { } catch (UploadException $e) {
$this->error($e->getMessage()); $this->error($e->getMessage());
} }
$this->success(__('Uploaded successful'), ['url' => $attachment->url, 'fullurl' => cdnurl($attachment->url, true)]); $this->success(__('Uploaded successful'), $attachment->getBaseData());
} elseif ($method == 'clean') { } elseif ($method == 'clean') {
//删除冗余的分片文件 //删除冗余的分片文件
try { try {
@ -141,7 +141,7 @@ class Common extends Api
$this->error($e->getMessage()); $this->error($e->getMessage());
} }
$this->success(__('Uploaded successful'), ['url' => $attachment->url, 'fullurl' => cdnurl($attachment->url, true)]); $this->success(__('Uploaded successful'), $attachment->getBaseData());
} }
} }

View File

@ -152,14 +152,18 @@ class Backend extends Controller
$url = $url ? $url : $this->request->url(); $url = $url ? $url : $this->request->url();
if (in_array($this->request->pathinfo(), ['/', 'index/index'])) { if (in_array($this->request->pathinfo(), ['/', 'index/index'])) {
$this->redirect('index/login', [], 302, ['referer' => $url]); $this->redirect('index/login', [], 302, ['referer' => $url]);
exit;
} }
$this->error(__('Please login first'), url('index/login', ['url' => $url])); $this->error(__('Please login first'), url('index/login', ['url' => $url]));
} }
// 判断是否需要验证权限 // 判断是否需要验证权限
if (!$this->auth->match($this->noNeedRight)) { if (!$this->auth->match($this->noNeedRight)) {
// 判断控制器和方法是否有对应权限 // 判断控制器和方法是否有对应权限
if (!$this->auth->check($path)) { $subpath = str_replace('.', '/', $this->request->path());
// 判断当前路径和子路径是否都无权限
$hasPathPermission = $this->auth->check($path);
$hasSubpathPermission = ($path === $subpath) ? $hasPathPermission : $this->auth->check($subpath);
if (!$hasPathPermission && !$hasSubpathPermission) {
Hook::listen('admin_nopermission', $this); Hook::listen('admin_nopermission', $this);
$this->error(__('You have no permission'), ''); $this->error(__('You have no permission'), '');
} }
@ -583,7 +587,7 @@ class Backend extends Controller
} }
$result['pid'] = isset($item['pid']) ? $item['pid'] : (isset($item['parent_id']) ? $item['parent_id'] : 0); $result['pid'] = isset($item['pid']) ? $item['pid'] : (isset($item['parent_id']) ? $item['parent_id'] : 0);
// 修改为安全的htmlentities调用兼容php8+版本 // 修改为安全的htmlentities调用兼容php8+版本
$result = array_map(function($value) { $result = array_map(function ($value) {
return $value === null ? '' : htmlentities((string)$value); return $value === null ? '' : htmlentities((string)$value);
}, $result); }, $result);
$list[] = $result; $list[] = $result;

View File

@ -286,7 +286,7 @@ class Auth
$newpassword = $this->getEncryptPassword($newpassword, $salt); $newpassword = $this->getEncryptPassword($newpassword, $salt);
$this->_user->save(['loginfailure' => 0, 'password' => $newpassword, 'salt' => $salt]); $this->_user->save(['loginfailure' => 0, 'password' => $newpassword, 'salt' => $salt]);
Token::delete($this->_token); Token::clear($this->_user->id);
//修改密码成功的事件 //修改密码成功的事件
Hook::listen("user_changepwd_successed", $this->_user); Hook::listen("user_changepwd_successed", $this->_user);
Db::commit(); Db::commit();

View File

@ -95,4 +95,20 @@ class Attachment extends Model
$data['unclassed'] = __('Unclassed'); $data['unclassed'] = __('Unclassed');
return $data; return $data;
} }
/**
* 返回上传文件的相关信息
*/
public function getBaseData()
{
$data = [
'url' => $this->url,
'fullurl' => cdnurl($this->url, true),
'category' => $this->category,
'filename' => $this->filename,
'filesize' => $this->filesize,
'mimetype' => $this->mimetype,
];
return $data;
}
} }

View File

@ -21,6 +21,6 @@
<![endif]--> <![endif]-->
<script type="text/javascript"> <script type="text/javascript">
var require = { var require = {
config: {$config|json_encode} config: {:json_encode($config)}
}; };
</script> </script>

View File

@ -520,14 +520,14 @@ class FormBuilder
$yes = $options['yes']; $yes = $options['yes'];
$no = $options['no']; $no = $options['no'];
} }
$selected = $no == $value ? "fa-flip-horizontal text-gray" : ""; $grayclass = $yes == $value ? "": "fa-flip-horizontal text-gray";
$disabled = (isset($options['disabled']) && $options['disabled']) || in_array('disabled', $options) ? "disabled" : ''; $disabled = (isset($options['disabled']) && $options['disabled']) || in_array('disabled', $options) ? "disabled" : '';
$color = isset($options['color']) ? $options['color'] : 'success'; $color = $options['color'] ?? 'success';
unset($options['yes'], $options['no'], $options['color'], $options['disabled']); unset($options['yes'], $options['no'], $options['color'], $options['disabled']);
$attr = $this->attributes($options); $attr = $this->attributes($options);
$html = <<<EOD $html = <<<EOD
{$btn} {$btn}
<a href="javascript:;" data-toggle="switcher" class="btn-switcher {$disabled}" data-input-id="c-{$domname}" data-yes="{$yes}" data-no="{$no}" {$attr}><i class="fa fa-toggle-on text-{$color} {$selected} fa-2x"></i></a> <a href="javascript:;" data-toggle="switcher" class="btn-switcher {$disabled}" data-input-id="c-{$domname}" data-yes="{$yes}" data-no="{$no}" {$attr}><i class="fa fa-toggle-on text-{$color} {$grayclass} fa-2x"></i></a>
EOD; EOD;
return $html; return $html;
} }

View File

@ -1659,4 +1659,16 @@ table.table-nowrap thead > tr > th {
color: #fff; color: #fff;
outline: none; outline: none;
} }
.btn-switcher {
margin-top: 5px;
display: inline-block;
min-height: 30px;
}
.switcher-unknown-val {
width: 30px;
border-radius: 10px;
background: #f2f281;
display: inline-block;
text-align: center;
}
/*# sourceMappingURL=backend.css.map */ /*# sourceMappingURL=backend.css.map */

View File

@ -470,6 +470,28 @@ define(['jquery', 'bootstrap', 'upload', 'validator', 'validator-lang'], functio
if ($(this).hasClass("disabled")) { if ($(this).hasClass("disabled")) {
return false; return false;
} }
var input = $(this).prev("input");
var yes = $(this).data("yes");
var no = $(this).data("no");
var unknownSpan = $(this).find(".switcher-unknown-val");
if (unknownSpan.length > 0) {
Layer.alert(__('Please select switch status'), {
icon: 0,
title: __('Warning'),
btn: [__('Switch status on'), __('Switch status off')],
btn1: function (index) {
unknownSpan.replaceWith('<i class="fa fa-toggle-on text-success fa-2x"></i>')
input.val(yes).trigger("change");
Layer.close(index);
},
btn2: function (index) {
unknownSpan.replaceWith('<i class="fa fa-toggle-on text-success fa-flip-horizontal text-gray fa-2x"></i>');
input.val(no).trigger("change");
Layer.close(index);
}
});
return false;
}
var switcher = $.proxy(function () { var switcher = $.proxy(function () {
var input = $(this).prev("input"); var input = $(this).prev("input");
input = $(this).data("input-id") ? $("#" + $(this).data("input-id")) : input; input = $(this).data("input-id") ? $("#" + $(this).data("input-id")) : input;

View File

@ -570,10 +570,36 @@ define(['jquery', 'bootstrap', 'moment', 'moment/locale/zh-cn', 'bootstrap-table
table.bootstrapTable($(this).prop("checked") ? 'checkBy' : 'uncheckBy', {field: options.pk, values: [ids]}); table.bootstrapTable($(this).prop("checked") ? 'checkBy' : 'uncheckBy', {field: options.pk, values: [ids]});
}); });
table.on("click", "[data-id].btn-change", function (e) { table.on("click", "[data-id].btn-change", function (e) {
var that = this;
e.preventDefault(); e.preventDefault();
var changer = $.proxy(function () { var changer = $.proxy(function () {
Table.api.multi($(this).data("action") ? $(this).data("action") : '', [$(this).data("id")], table, this); Table.api.multi($(this).data("action") ? $(this).data("action") : '', [$(this).data("id")], table, this);
}, this); }, this);
var unknownSpan = $(this).find(".switcher-unknown-val");
if (unknownSpan.length > 0) {
var field = $(this).data("field");
var yes = $(this).data("yes");
var no = $(this).data("no");
Layer.alert(__('Please select switch status'), {
icon: 0,
title: __('Warning'),
btn: [__('Switch status on'), __('Switch status off')],
btn1: function (index) {
unknownSpan.replaceWith('<i class="fa fa-toggle-on text-success fa-2x"></i>')
$(that).data("params", field + "=" + yes);
changer();
Layer.close(index);
},
btn2: function (index) {
unknownSpan.replaceWith('<i class="fa fa-toggle-on text-success fa-flip-horizontal text-gray fa-2x"></i>');
$(that).data("params", field + "=" + no);
changer();
Layer.close(index);
}
});
return false;
}
if (typeof $(this).data("confirm") !== 'undefined') { if (typeof $(this).data("confirm") !== 'undefined') {
Layer.confirm($(this).data("confirm"), function (index) { Layer.confirm($(this).data("confirm"), function (index) {
changer(); changer();
@ -864,8 +890,12 @@ define(['jquery', 'bootstrap', 'moment', 'moment/locale/zh-cn', 'bootstrap-table
if (typeof this.disable !== "undefined") { if (typeof this.disable !== "undefined") {
disable = typeof this.disable === "function" ? this.disable.call(this, value, row, index) : this.disable; disable = typeof this.disable === "function" ? this.disable.call(this, value, row, index) : this.disable;
} }
return "<a href='javascript:;' data-toggle='tooltip' data-value='" + value + "' title='" + __('Click to toggle') + "' class='btn-change " + (disable ? 'btn disabled no-padding' : '') + "' data-index='" + index + "' data-id='" var isUnknown = yes == value || no == value ? false : true;
+ row[pk] + "' " + (url ? "data-url='" + url + "'" : "") + (confirm ? "data-confirm='" + confirm + "'" : "") + " data-params='" + this.field + "=" + (value == yes ? no : yes) + "'><i class='fa fa-toggle-on text-success text-" + color + " " + (value == yes ? '' : 'fa-flip-horizontal text-gray') + " fa-2x'></i></a>"; var html = isUnknown
? '<span class="switcher-unknown-val" data-toggle="tooltip" title="' + __('Status unknown, please manually change the status') + '"><i class="fa fa-question-circle text-warning"></i></span>'
: '<i class="fa fa-toggle-on text-success text-' + color + ' ' + (value == yes ? '' : 'fa-flip-horizontal text-gray') + ' fa-2x"></i>';
return "<a href='javascript:;' " + (isUnknown ? "" : "data-toggle='tooltip' title='" + __('Click to toggle') + "'") + " data-value='" + value + "' data-field='" + this.field + "' data-yes='" + yes + "' data-no='" + no + "' class='btn-change " + (disable ? 'btn disabled no-padding' : '') + "' data-index='" + index + "' data-id='"
+ row[pk] + "' " + (url ? "data-url='" + url + "'" : "") + (confirm ? "data-confirm='" + confirm + "'" : "") + " data-params='" + this.field + "=" + (isUnknown ? value : (value == yes ? no : yes)) + "'>" + html + "</a>";
}, },
url: function (value, row, index) { url: function (value, row, index) {
value = value == null || value.length === 0 ? '' : value.toString(); value = value == null || value.length === 0 ? '' : value.toString();

View File

@ -1709,4 +1709,18 @@ table.table-nowrap {
outline: none; outline: none;
} }
} }
}
.btn-switcher {
margin-top: 5px;
display: inline-block;
min-height: 30px;
}
.switcher-unknown-val {
width: 30px;
border-radius: 10px;
background: #f2f281;
display: inline-block;
text-align: center;
} }