mirror of
https://gitee.com/zoujingli/ThinkAdmin.git
synced 2026-07-08 17:11:07 +08:00
Compare commits
17 Commits
cfc5071a8c
...
ab4845c28c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab4845c28c | ||
|
|
150e1f16bf | ||
|
|
464faac034 | ||
|
|
a0858a03ac | ||
|
|
307030ed6f | ||
|
|
c08c75d61f | ||
|
|
d35a82a74a | ||
|
|
bebae75691 | ||
|
|
8bb6262ccc | ||
|
|
119ca2dd4e | ||
|
|
64e0b8aa4d | ||
|
|
43a2e04f03 | ||
|
|
77641b101c | ||
|
|
0c6628e1a3 | ||
|
|
913f491fdf | ||
|
|
15ac93059b | ||
|
|
a61baf954a |
@ -64,7 +64,7 @@ class Module extends Controller
|
||||
$pattern = "|^(\d{4})\.(\d{2})\.(\d{2})\.(\d+)$|";
|
||||
$this->module['change'] = array_reverse($this->module['change']);
|
||||
foreach ($this->module['change'] as $version => &$change) {
|
||||
$change = ['content' => $change, 'version' => preg_replace($pattern, '$1年$2月$3日 第 $4 次更新', $version)];
|
||||
$change = ['content' => $change, 'version' => preg_replace($pattern, '$1年$2月$3日 更新', $version)];
|
||||
}
|
||||
$this->fetch();
|
||||
} else {
|
||||
|
||||
@ -47,16 +47,14 @@ class Plugs extends Controller
|
||||
*/
|
||||
public function script(): \think\Response
|
||||
{
|
||||
$debug = $this->app->isDebug() ? 'true' : 'false';
|
||||
$editor = sysconf("base.editor") ?: "ckeditor4";
|
||||
$taRoot = sysuri("admin/index/index", [], false);
|
||||
$taDebug = $this->app->isDebug() ? 'true' : 'false';
|
||||
$taAdmin = sysuri('admin/index/index', [], false);
|
||||
$taEditor = sysconf('base.editor') ?: 'ckeditor4';
|
||||
return response(join("\n", [
|
||||
"window.taDebug = {$debug};",
|
||||
"window.taAdmin = '{$taRoot}';",
|
||||
"window.taEditor = '{$editor}';",
|
||||
]))->header([
|
||||
'Content-Type' => 'application/x-javascript'
|
||||
]);
|
||||
"window.taDebug = {$taDebug};",
|
||||
"window.taAdmin = '{$taAdmin}';",
|
||||
"window.taEditor = '{$taEditor}';",
|
||||
]))->contentType('application/x-javascript');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -77,9 +77,10 @@ class Upload extends Controller
|
||||
'hash.require' => '哈希不能为空!',
|
||||
'xext.require' => '后缀不能为空!',
|
||||
'size.require' => '大小不能为空!',
|
||||
'mime.require' => "类型不能为空!",
|
||||
'mime.default' => '',
|
||||
'status.value' => 1
|
||||
]));
|
||||
if (empty($file['mime'])) $file['mime'] = Storage::mime($file['xext']);
|
||||
if ($info = Storage::instance($data['uptype'])->info($data['key'], $safe, $name)) {
|
||||
$file->save(['xurl' => $info['url'], 'isfast' => 1, 'issafe' => $data['safe']]);
|
||||
$extr = ['id' => $file->id ?? 0, 'url' => $info['url'], 'key' => $info['key']];
|
||||
@ -128,8 +129,8 @@ class Upload extends Controller
|
||||
$data = $this->_vali([
|
||||
'id.require' => '编号不能为空!',
|
||||
'hash.require' => '哈希不能为空!',
|
||||
'uuid.value' => AdminService::getUserId()
|
||||
]);
|
||||
$data['uuid'] = AdminService::getUserId();
|
||||
$file = SystemFile::mk()->where($data)->findOrEmpty();
|
||||
if ($file->isEmpty()) $this->error('文件不存在!');
|
||||
if ($file->save(['status' => 2])) {
|
||||
|
||||
@ -1,2 +0,0 @@
|
||||
* 系统模块初始化成功
|
||||
* 这次更新了许多内容哦
|
||||
@ -1 +1,7 @@
|
||||
* 少量更新,修复部分BUG
|
||||
* 系统模块初始化成功
|
||||
* 这次更新了许多内容哦
|
||||
* 少量更新,修复部分BUG
|
||||
* 修正系统用户搜索
|
||||
* 系统菜单支持外链
|
||||
* 修复任意下载问题
|
||||
* 优化模块管理机制
|
||||
@ -1,2 +0,0 @@
|
||||
* 修正系统用户搜索
|
||||
* 系统菜单支持外链
|
||||
@ -1,2 +0,0 @@
|
||||
* 修复任意下载问题
|
||||
* 优化模块管理机制
|
||||
13
app/admin/module/change/2022.07.11.01.md
Normal file
13
app/admin/module/change/2022.07.11.01.md
Normal file
@ -0,0 +1,13 @@
|
||||
* 增加 CORS 跨域规则配置,配置参数置放于 config/app.php,需要更新 ThinkLibrary。
|
||||
* 修复 layui.table 导致基于 ThinkPHP 模板输出自动转义 XSS 过滤机制失效,需要更新 ThinkLibrary。
|
||||
* 修复在模板中使用 {:input(NAME)} 取值而产生的 XSS 问题,模板取值更换为 {$get.NAME|default=''}。
|
||||
* 修复 CKEDITOR4 配置文件,禁用所有标签的on事件,阻止 xss 脚本注入,需要更新 ckeditor/config.js。
|
||||
* 修复上传入口的后缀验证,读取真实文件后缀与配置对比,阻止不合法的文件上传并存储到本地服务器。
|
||||
* 修改 JsonRpc 接口异常处理机制,当服务端绑定 Exception 时,客户端将能收到 error 消息及异常数据。
|
||||
* 修改 location.hash 访问机制,禁止直接访问外部URL资源链接,防止外部XSS攻击读取本地缓存数据。
|
||||
* 增加后台主题样式配置,支持全局默认+用户个性配置,需要更新 ThinkLibrary, static, admin 组件及模块。
|
||||
* 升级 layui 版本到 2.7.4,引入 jszip 插件
|
||||
* 框架核心服务层静态化重构,增加后台皮肤样式选择与配置
|
||||
* 重构文件上传机制,增加文件记录,增加进度 tips 提示,增加图片压缩
|
||||
* 引入 ckeditor5 富文本编辑器,同时保存原 ckeditor4 版本,可后台切换
|
||||
* 全局改写 layTable 动态数据表格,支持静态刷新,同时兼容原生 table 方式
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "admin",
|
||||
"author": "Anyon",
|
||||
"version": "2022.03.06.01",
|
||||
"version": "2022.07.11.01",
|
||||
"content": "ThinkAdmin 系统基础模块"
|
||||
}
|
||||
@ -15,6 +15,8 @@ define(['md5', 'notify'], function (SparkMD5, Notify, allowMime) {
|
||||
this.option.quality = parseFloat(this.option.elem.data('quality') || '1.0');
|
||||
this.option.maxWidth = parseInt(this.option.elem.data('max-width') || '0');
|
||||
this.option.maxHeight = parseInt(this.option.elem.data('max-height') || '0');
|
||||
this.option.cutWidth = parseInt(this.option.elem.data('cut-width') || '0');
|
||||
this.option.cutHeight = parseInt(this.option.elem.data('cut-height') || '0');
|
||||
|
||||
/*! 查找表单元素, 如果没有找到将不会自动写值 */
|
||||
if (!this.option.elem.data('input') && this.option.elem.data('field')) {
|
||||
@ -29,17 +31,20 @@ define(['md5', 'notify'], function (SparkMD5, Notify, allowMime) {
|
||||
|
||||
/*! 初始化上传组件 */
|
||||
this.adapter = new Adapter(this.option, layui.upload.render({
|
||||
url: '{:url("admin/api.upload/file")}', auto: false, elem: elem, accept: 'file', multiple: this.option.mult, exts: this.option.exts.join('|'), acceptMime: this.option.mimes.join(','), choose: function (object) {
|
||||
object.files = object.pushFile();
|
||||
layui.each(object.files, function (idx, file) {
|
||||
url: '{:url("admin/api.upload/file")}', auto: false, elem: elem, accept: 'file', multiple: this.option.mult, exts: this.option.exts.join('|'), acceptMime: this.option.mimes.join(','), choose: function (obj) {
|
||||
obj.items = [], obj.files = obj.pushFile();
|
||||
layui.each(obj.files, function (idx, file) {
|
||||
obj.items.push(file);
|
||||
file.quality = that.option.quality;
|
||||
file.maxWidth = that.option.maxWidth;
|
||||
file.maxHeight = that.option.maxHeight;
|
||||
file.cutWidth = that.option.cutWidth;
|
||||
file.cutHeight = that.option.cutHeight;
|
||||
});
|
||||
that.adapter.event('upload.choose', object.files);
|
||||
that.adapter.upload(object.files, done);
|
||||
layui.each(object.files, function (idx) {
|
||||
delete object.files[idx];
|
||||
that.adapter.event('upload.choose', obj.items);
|
||||
that.adapter.upload(obj.items, done);
|
||||
layui.each(obj.files, function (idx) {
|
||||
delete obj.files[idx];
|
||||
});
|
||||
}
|
||||
}));
|
||||
@ -74,14 +79,17 @@ define(['md5', 'notify'], function (SparkMD5, Notify, allowMime) {
|
||||
// 禁传异常状态文件
|
||||
if (typeof file.xstate === 'number' && file.xstate === -1) return;
|
||||
// 图片限宽限高压缩
|
||||
if (/^image\/*$/.test(file.type) && file.maxWidth > 0 || file.maxHeight > 0 || file.quality !== 1) {
|
||||
FileToBase64(file).then(function (base64) {
|
||||
ImageToThumb(base64, file).then(function (base64) {
|
||||
files[index] = Base64ToFile(base64, file.name);
|
||||
files[index].notify = file.notify;
|
||||
that.hash(files[index]).then(function (file) {
|
||||
that.event('upload.hash', file).request(file, done);
|
||||
});
|
||||
if (/^image\//.test(file.type) && (file.maxWidth + file.maxHeight + file.cutWidth + file.cutHeight > 0 || file.quality !== 1)) {
|
||||
require(['compressor'], function (Compressor) {
|
||||
new Compressor(file, {
|
||||
quality: file.quality, resize: 'cover', width: file.cutWidth || 0, height: file.cutHeight || 0, maxWidth: file.maxWidth, maxHeight: file.maxHeight, success(blob) {
|
||||
files[index] = blob, blob.index = file.index, files[index].notify = file.notify;
|
||||
that.hash(files[index]).then(function (file) {
|
||||
that.event('upload.hash', file).request(file, done);
|
||||
});
|
||||
}, error: function () {
|
||||
that.event('upload.error', {file: file}, file, '压缩失败');
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
@ -132,7 +140,7 @@ define(['md5', 'notify'], function (SparkMD5, Notify, allowMime) {
|
||||
uploader.form.append('authorization', ret.data['authorization']);
|
||||
uploader.form.append('Content-Disposition', 'inline;filename=' + encodeURIComponent(file.name));
|
||||
}
|
||||
uploader.form.append('file', file), jQuery.ajax({
|
||||
uploader.form.append('file', file, file.name), jQuery.ajax({
|
||||
url: uploader.url, data: uploader.form, type: 'post', xhr: function (xhr) {
|
||||
xhr = new XMLHttpRequest();
|
||||
return xhr.upload.addEventListener('progress', function (event) {
|
||||
@ -263,61 +271,6 @@ define(['md5', 'notify'], function (SparkMD5, Notify, allowMime) {
|
||||
|
||||
return UploadAdapter;
|
||||
|
||||
/**
|
||||
* Base64 转 File 对象
|
||||
* @param {String} base64 Base64内容
|
||||
* @param {String} filename 新文件名称
|
||||
* @return {File}
|
||||
*/
|
||||
function Base64ToFile(base64, filename) {
|
||||
var arr = base64.split(',');
|
||||
var mime = arr[0].match(/:(.*?);/)[1], suffix = mime.split('/')[1];
|
||||
var bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n);
|
||||
while (n--) u8arr[n] = bstr.charCodeAt(n);
|
||||
return new File([u8arr], filename + '.' + suffix, {type: mime});
|
||||
}
|
||||
|
||||
/**
|
||||
* File 对象转 Base64
|
||||
* @param {File} file 文件对象
|
||||
* @return {Promise}
|
||||
*/
|
||||
function FileToBase64(file) {
|
||||
var defer = jQuery.Deferred(), reader = new FileReader();
|
||||
return (reader.onload = function () {
|
||||
defer.resolve(this.result);
|
||||
}), reader.readAsDataURL(file), defer.promise();
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片压缩处理
|
||||
* @param {String} url 图片链接
|
||||
* @param {Object} option 压缩参数
|
||||
* @return {Promise}
|
||||
*/
|
||||
function ImageToThumb(url, option) {
|
||||
var defer = jQuery.Deferred(), image = new Image();
|
||||
image.src = url, image.onload = function () {
|
||||
var canvas = document.createElement('canvas'), context = canvas.getContext('2d');
|
||||
option.maxWidth = option.maxWidth || this.width, option.maxHeight = option.maxHeight || this.height;
|
||||
var originWidth = this.width, originHeight = this.height, targetWidth = originWidth, targetHeight = originHeight;
|
||||
if (originWidth > option.maxWidth || originHeight > option.maxHeight) {
|
||||
if (originWidth / option.maxWidth > option.maxWidth / option.maxHeight) {
|
||||
targetWidth = option.maxWidth;
|
||||
targetHeight = Math.round(option.maxWidth * (originHeight / originWidth));
|
||||
} else {
|
||||
targetHeight = option.maxHeight;
|
||||
targetWidth = Math.round(option.maxHeight * (originWidth / originHeight));
|
||||
}
|
||||
}
|
||||
canvas.width = targetWidth, canvas.height = targetHeight;
|
||||
context.clearRect(0, 0, targetWidth, targetHeight);
|
||||
context.drawImage(this, 0, 0, targetWidth, targetHeight);
|
||||
defer.resolve(canvas.toDataURL('image/jpeg', option.quality || 0.9));
|
||||
};
|
||||
return defer.promise();
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传状态提示扩展插件
|
||||
* @param {File} file 文件对象
|
||||
|
||||
@ -15,11 +15,7 @@
|
||||
<link rel="stylesheet" href="__ROOT__/static/extra/style.css?at={:date('md')}">
|
||||
{block name="style"}{/block}
|
||||
<script src="__ROOT__/static/plugs/jquery/pace.min.js"></script>
|
||||
<script>
|
||||
window.taDebug = '{:intval($app->isDebug())}' > 0;
|
||||
window.taEditor = '{:sysconf("base.editor")?:"ckeditor4"}';
|
||||
window.tapiRoot = '{:sysuri("admin/index/index",[],false)}';
|
||||
</script>
|
||||
<script src="{:url('admin/api.plugs/script',[],false,false)}"></script>
|
||||
</head>
|
||||
<body class="layui-layout-body">
|
||||
{block name='body'}
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
|
||||
<div class="layui-row layui-col-space15">
|
||||
<div class="layui-col-xs2 text-center padding-top-15">
|
||||
<input type="hidden" name="headimg" value="{$vo.headimg|default=''}">
|
||||
<input type="hidden" data-cut-width="500" data-cut-height="500" data-max-width="500" data-max-height="500" name="headimg" value="{$vo.headimg|default=''}">
|
||||
<script>$('[name=headimg]').uploadOneImage();</script>
|
||||
</div>
|
||||
<div class="layui-col-xs5">
|
||||
|
||||
@ -82,13 +82,13 @@
|
||||
<tr>
|
||||
<td class="text-center text-top padding-0">
|
||||
<div class="help-images">
|
||||
<input name="cover" type="hidden" value="{$vo.cover|default=''}">
|
||||
<input name="cover" data-max-width="500" data-max-height="500" type="hidden" value="{$vo.cover|default=''}">
|
||||
<script>$('[name="cover"]').uploadOneImage();</script>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-left padding-0">
|
||||
<div class="help-images">
|
||||
<input name="slider" type="hidden" value="{$vo.slider|default=''}">
|
||||
<input name="slider" data-max-width="2048" data-max-height="1024" type="hidden" value="{$vo.slider|default=''}">
|
||||
<script>$('[name="slider"]').uploadMultipleImage();</script>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@ -70,6 +70,7 @@ require.config({
|
||||
'filesaver': ['plugs/jquery/filesaver.min'],
|
||||
'websocket': ['plugs/socket/websocket'],
|
||||
'pcasunzips': ['plugs/jquery/pcasunzips'],
|
||||
'compressor': ['plugs/jquery/compressor.min'],
|
||||
'sortablejs': ['plugs/sortable/sortable.min'],
|
||||
'vue.sortable': ['plugs/sortable/vue.draggable.min'],
|
||||
'jquery.ztree': ['plugs/ztree/ztree.all.min'],
|
||||
@ -604,7 +605,11 @@ $(function () {
|
||||
event.stopPropagation(), $in.val() && $.previewImage(encodeURI($in.val()));
|
||||
}).on('click', 'i.layui-icon-close', function (event) {
|
||||
event.stopPropagation(), $bt.attr('style', ''), $in.val('');
|
||||
}).find('[data-file]').data('input', this).attr('data-size', $in.data('size') || 0).attr('data-type', $in.data('type') || 'png,jpg,gif,jpeg');
|
||||
}).find('[data-file]').data('input', this).attr({
|
||||
'data-size': $in.data('size') || 0, 'data-type': $in.data('type') || 'gif,png,jpg,jpeg',
|
||||
'data-max-width': $in.data('max-width') || 0, 'data-max-height': $in.data('max-height') || 0,
|
||||
'data-cut-width': $in.data('cut-width') || 0, 'data-cut-height': $in.data('cut-height') || 0,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@ -614,7 +619,11 @@ $(function () {
|
||||
if ($(this).data('inited')) return true; else $(this).data('inited', true);
|
||||
var $bt = $('<div class="uploadimage"><span><a data-file="mul" class="layui-icon layui-icon-upload-drag"></a></span><span data-file="images"></span></div>');
|
||||
var ims = this.value ? this.value.split('|') : [], $in = $(this).after($bt);
|
||||
$bt.find('[data-file]').attr('data-size', $in.data('size') || 0).attr('data-type', $in.data('type') || 'gif,png,jpg,jpeg').on('push', function (evt, src) {
|
||||
$bt.find('[data-file]').attr({
|
||||
'data-size': $in.data('size') || 0, 'data-type': $in.data('type') || 'gif,png,jpg,jpeg',
|
||||
'data-max-width': $in.data('max-width') || 0, 'data-max-height': $in.data('max-height') || 0,
|
||||
'data-cut-width': $in.data('cut-width') || 0, 'data-cut-height': $in.data('cut-height') || 0,
|
||||
}).on('push', function (evt, src) {
|
||||
ims.push(src), $in.val(ims.join('|')), showImageContainer([src]);
|
||||
}) && (ims.length > 0 && showImageContainer(ims));
|
||||
|
||||
@ -879,14 +888,6 @@ $(function () {
|
||||
if (typeof this.dataset.file === 'string' && /^images?$/.test(this.dataset.file)) {
|
||||
return $.form.modal(tapiRoot + '/api.upload/image', this.dataset, '图片选择器')
|
||||
}
|
||||
// 上传视频,支持单视频或多视频选择,分别是 video|videos
|
||||
if (typeof this.dataset.file === 'string' && /^videos?$/.test(this.dataset.file)) {
|
||||
return $.form.modal(tapiRoot + '/api.upload/video', this.dataset, '视频选择器')
|
||||
}
|
||||
// 上传文档,支持单视频或多视频选择,分别是 document|documents
|
||||
if (typeof this.dataset.file === 'string' && /^videos?$/.test(this.dataset.file)) {
|
||||
return $.form.modal(tapiRoot + '/api.upload/document', this.dataset, '文档选择器')
|
||||
}
|
||||
// 其他文件上传
|
||||
if ($(this).data('inited') !== true) (function (that) {
|
||||
that.uploadFile(undefined, function () {
|
||||
|
||||
10
public/static/plugs/jquery/compressor.min.js
vendored
Normal file
10
public/static/plugs/jquery/compressor.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user