初始化提交

This commit is contained in:
maticarmy
2025-02-10 10:39:00 +08:00
commit 59cd2c19d1
491 changed files with 54545 additions and 0 deletions

69
app/admin/Service.php Normal file
View File

@@ -0,0 +1,69 @@
<?php
// +----------------------------------------------------------------------
// | Admin Plugin for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免责声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 代码仓库https://gitee.com/zoujingli/think-plugs-admin
// | github 代码仓库https://github.com/zoujingli/think-plugs-admin
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace app\admin;
use think\admin\Plugin;
/**
* 插件服务注册
* @class Service
* @package app\admin
*/
class Service extends Plugin
{
/**
* 定义插件名称
* @var string
*/
protected $appName = '系统管理';
/**
* 定义安装包名
* @var string
*/
protected $package = 'zoujingli/think-plugs-admin';
/**
* 定义插件中心菜单
* @return array
*/
public static function menu(): array
{
return [
[
'name' => '系统配置',
'subs' => [
['name' => '系统参数配置', 'icon' => 'layui-icon layui-icon-set', 'node' => 'admin/config/index'],
['name' => '系统任务管理', 'icon' => 'layui-icon layui-icon-log', 'node' => 'admin/queue/index'],
['name' => '系统日志管理', 'icon' => 'layui-icon layui-icon-form', 'node' => 'admin/oplog/index'],
['name' => '数据字典管理', 'icon' => 'layui-icon layui-icon-code-circle', 'node' => 'admin/base/index'],
['name' => '系统文件管理', 'icon' => 'layui-icon layui-icon-carousel', 'node' => 'admin/file/index'],
['name' => '系统菜单管理', 'icon' => 'layui-icon layui-icon-layouts', 'node' => 'admin/menu/index'],
],
],
[
'name' => '权限管理',
'subs' => [
['name' => '系统权限管理', 'icon' => 'layui-icon layui-icon-vercode', 'node' => 'admin/auth/index'],
['name' => '系统用户管理', 'icon' => 'layui-icon layui-icon-username', 'node' => 'admin/user/index'],
],
],
];
}
}

View File

@@ -0,0 +1,134 @@
<?php
// +----------------------------------------------------------------------
// | Admin Plugin for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免责声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 代码仓库https://gitee.com/zoujingli/think-plugs-admin
// | github 代码仓库https://github.com/zoujingli/think-plugs-admin
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace app\admin\controller;
use think\admin\Controller;
use think\admin\helper\QueryHelper;
use think\admin\model\SystemAuth;
use think\admin\model\SystemNode;
use think\admin\Plugin;
use think\admin\service\AdminService;
/**
* 系统权限管理
* @class Auth
* @package app\admin\controller
*/
class Auth extends Controller
{
/**
* 系统权限管理
* @auth true
* @menu true
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function index()
{
SystemAuth::mQuery()->layTable(function () {
$this->title = '系统权限管理';
}, static function (QueryHelper $query) {
$query->like('title,desc')->equal('status,utype')->dateBetween('create_at');
});
}
/**
* 修改权限状态
* @auth true
*/
public function state()
{
SystemAuth::mSave($this->_vali([
'status.in:0,1' => '状态值范围异常!',
'status.require' => '状态值不能为空!',
]));
}
/**
* 删除系统权限
* @auth true
*/
public function remove()
{
SystemAuth::mDelete();
}
/**
* 添加系统权限
* @auth true
*/
public function add()
{
SystemAuth::mForm('form');
}
/**
* 编辑系统权限
* @auth true
*/
public function edit()
{
SystemAuth::mForm('form');
}
/**
* 表单后置数据处理
* @param array $data
*/
protected function _form_filter(array $data)
{
if ($this->request->isGet()) {
$this->title = empty($data['title']) ? "添加访问授权" : "编辑【{$data['title']}】授权";
} elseif ($this->request->post('action') === 'json') {
if ($this->app->isDebug()) AdminService::clear();
$ztree = AdminService::getTree(empty($data['id']) ? [] : SystemNode::mk()->where(['auth' => $data['id']])->column('node'));
usort($ztree, static function ($a, $b) {
if (explode('-', $a['node'])[0] !== explode('-', $b['node'])[0]) {
if (stripos($a['node'], 'plugin-') === 0) return 1;
}
return $a['node'] === $b['node'] ? 0 : ($a['node'] > $b['node'] ? 1 : -1);
});
[$ps, $cs] = [Plugin::get(), (array)$this->app->config->get('app.app_names', [])];
foreach ($ztree as &$n) $n['title'] = lang($cs[$n['node']] ?? (($ps[$n['node']] ?? [])['name'] ?? $n['title']));
$this->success('获取权限节点成功!', $ztree);
} elseif (empty($data['nodes'])) {
$this->error('未配置功能节点!');
}
}
/**
* 节点更新处理
* @param boolean $state
* @param array $post
* @return void
*/
protected function _form_result(bool $state, array $post)
{
if ($state && $this->request->post('action') === 'save') {
[$map, $data] = [['auth' => $post['id']], []];
foreach ($post['nodes'] ?? [] as $node) $data[] = $map + ['node' => $node];
SystemNode::mk()->where($map)->delete();
count($data) > 0 && SystemNode::mk()->insertAll($data);
sysoplog('系统权限管理', "配置系统权限[{$map['auth']}]授权成功");
$this->success('权限修改成功!', 'javascript:history.back()');
}
}
}

View File

@@ -0,0 +1,113 @@
<?php
// +----------------------------------------------------------------------
// | Admin Plugin for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免责声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 代码仓库https://gitee.com/zoujingli/think-plugs-admin
// | github 代码仓库https://github.com/zoujingli/think-plugs-admin
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace app\admin\controller;
use think\admin\Controller;
use think\admin\helper\QueryHelper;
use think\admin\model\SystemBase;
/**
* 数据字典管理
* @class Base
* @package app\admin\controller
*/
class Base extends Controller
{
/**
* 数据字典管理
* @auth true
* @menu true
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function index()
{
SystemBase::mQuery()->layTable(function () {
$this->title = '数据字典管理';
$this->types = SystemBase::types();
$this->type = $this->get['type'] ?? ($this->types[0] ?? '-');
}, static function (QueryHelper $query) {
$query->where(['deleted' => 0])->equal('type');
$query->like('code,name,status')->dateBetween('create_at');
});
}
/**
* 添加数据字典
* @auth true
*/
public function add()
{
SystemBase::mForm('form');
}
/**
* 编辑数据字典
* @auth true
*/
public function edit()
{
SystemBase::mForm('form');
}
/**
* 表单数据处理
* @param array $data
* @throws \think\db\exception\DbException
*/
protected function _form_filter(array &$data)
{
if ($this->request->isGet()) {
$this->types = SystemBase::types();
$this->types[] = '--- ' . lang('新增类型') . ' ---';
$this->type = $this->get['type'] ?? ($this->types[0] ?? '-');
} else {
$map = [];
$map[] = ['deleted', '=', 0];
$map[] = ['code', '=', $data['code']];
$map[] = ['type', '=', $data['type']];
$map[] = ['id', '<>', $data['id'] ?? 0];
if (SystemBase::mk()->where($map)->count() > 0) {
$this->error("数据编码已经存在!");
}
}
}
/**
* 修改数据状态
* @auth true
*/
public function state()
{
SystemBase::mSave($this->_vali([
'status.in:0,1' => '状态值范围异常!',
'status.require' => '状态值不能为空!',
]));
}
/**
* 删除数据记录
* @auth true
*/
public function remove()
{
SystemBase::mDelete();
}
}

View File

@@ -0,0 +1,146 @@
<?php
// +----------------------------------------------------------------------
// | Admin Plugin for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免责声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 代码仓库https://gitee.com/zoujingli/think-plugs-admin
// | github 代码仓库https://github.com/zoujingli/think-plugs-admin
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace app\admin\controller;
use think\admin\Controller;
use think\admin\Plugin;
use think\admin\service\AdminService;
use think\admin\service\ModuleService;
use think\admin\service\RuntimeService;
use think\admin\service\SystemService;
use think\admin\Storage;
use think\admin\storage\AliossStorage;
use think\admin\storage\QiniuStorage;
use think\admin\storage\TxcosStorage;
/**
* 系统参数配置
* @class Config
* @package app\admin\controller
*/
class Config extends Controller
{
const themes = [
'default' => '默认色0',
'white' => '简约白0',
'red-1' => '玫瑰红1',
'blue-1' => '深空蓝1',
'green-1' => '小草绿1',
'black-1' => '经典黑1',
'red-2' => '玫瑰红2',
'blue-2' => '深空蓝2',
'green-2' => '小草绿2',
'black-2' => '经典黑2',
];
/**
* 系统参数配置
* @auth true
* @menu true
*/
public function index()
{
$this->title = '系统参数配置';
$this->files = Storage::types();
$this->plugins = Plugin::get(null, true);
$this->issuper = AdminService::isSuper();
$this->systemid = ModuleService::getRunVar('uni');
$this->framework = ModuleService::getLibrarys('topthink/framework');
$this->thinkadmin = ModuleService::getLibrarys('zoujingli/think-library');
if (AdminService::isSuper() && $this->app->session->get('user.password') === md5('admin')) {
$url = url('admin/index/pass', ['id' => AdminService::getUserId()]);
$this->showErrorMessage = lang("超级管理员账号的密码未修改,建议立即<a data-modal='%s'>修改密码</a>", [$url]);
}
uasort($this->plugins, static function ($a, $b) {
if ($a['space'] === $b['space']) return 0;
return $a['space'] > $b['space'] ? 1 : -1;
});
$this->fetch();
}
/**
* 修改系统参数
* @auth true
* @throws \think\admin\Exception
*/
public function system()
{
if ($this->request->isGet()) {
$this->title = '修改系统参数';
$this->themes = static::themes;
$this->fetch();
} else {
$post = $this->request->post();
// 修改网站后台入口路径
if (!empty($post['xpath'])) {
if (!preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $post['xpath'])) {
$this->error('后台入口格式错误!');
}
if ($post['xpath'] !== 'admin') {
if (is_dir(syspath("app/{$post['xpath']}")) || !empty(Plugin::get($post['xpath']))) {
$this->error(lang('已存在 %s 应用!', [$post['xpath']]));
}
}
RuntimeService::set(null, [$post['xpath'] => 'admin']);
}
// 修改网站 ICON 图标,替换 public/favicon.ico
if (preg_match('#^https?://#', $post['site_icon'] ?? '')) try {
SystemService::setFavicon($post['site_icon'] ?? '');
} catch (\Exception $exception) {
trace_file($exception);
}
// 数据数据到系统配置表
foreach ($post as $k => $v) sysconf($k, $v);
sysoplog('系统配置管理', "修改系统参数成功");
$this->success('数据保存成功!', admuri('admin/config/index'));
}
}
/**
* 修改文件存储
* @auth true
* @throws \think\admin\Exception
*/
public function storage()
{
$this->_applyFormToken();
if ($this->request->isGet()) {
$this->type = input('type', 'local');
if ($this->type === 'alioss') {
$this->points = AliossStorage::region();
} elseif ($this->type === 'qiniu') {
$this->points = QiniuStorage::region();
} elseif ($this->type === 'txcos') {
$this->points = TxcosStorage::region();
}
$this->fetch("storage-{$this->type}");
} else {
$post = $this->request->post();
if (!empty($post['storage']['allow_exts'])) {
$deny = ['sh', 'asp', 'bat', 'cmd', 'exe', 'php'];
$exts = array_unique(str2arr(strtolower($post['storage']['allow_exts'])));
if (count(array_intersect($deny, $exts)) > 0) $this->error('禁止上传可执行的文件!');
$post['storage']['allow_exts'] = join(',', $exts);
}
foreach ($post as $name => $value) sysconf($name, $value);
sysoplog('系统配置管理', "修改系统存储参数");
$this->success('修改文件存储成功!');
}
}
}

View File

@@ -0,0 +1,118 @@
<?php
// +----------------------------------------------------------------------
// | Admin Plugin for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免责声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 代码仓库https://gitee.com/zoujingli/think-plugs-admin
// | github 代码仓库https://github.com/zoujingli/think-plugs-admin
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace app\admin\controller;
use think\admin\Controller;
use think\admin\helper\QueryHelper;
use think\admin\model\SystemFile;
use think\admin\service\AdminService;
use think\admin\Storage;
/**
* 系统文件管理
* @class File
* @package app\admin\controller
*/
class File extends Controller
{
/**
* 存储类型
* @var array
*/
protected $types;
/**
* 控制器初始化
* @return void
*/
protected function initialize()
{
$this->types = Storage::types();
}
/**
* 系统文件管理
* @auth true
* @menu true
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function index()
{
SystemFile::mQuery()->layTable(function () {
$this->title = '系统文件管理';
$this->xexts = SystemFile::mk()->distinct()->column('xext');
}, static function (QueryHelper $query) {
$query->like('name,hash,xext')->equal('type')->dateBetween('create_at');
$query->where(['issafe' => 0, 'status' => 2, 'uuid' => AdminService::getUserId()]);
});
}
/**
* 数据列表处理
* @param array $data
* @return void
*/
protected function _page_filter(array &$data)
{
foreach ($data as &$vo) {
$vo['ctype'] = $this->types[$vo['type']] ?? $vo['type'];
}
}
/**
* 编辑系统文件
* @auth true
* @return void
*/
public function edit()
{
SystemFile::mForm('form');
}
/**
* 删除系统文件
* @auth true
* @return void
*/
public function remove()
{
if (!AdminService::isSuper()) {
$where = ['uuid' => AdminService::getUserId()];
}
SystemFile::mDelete('', $where ?? []);
}
/**
* 清理重复文件
* @auth true
* @return void
* @throws \think\db\exception\DbException
*/
public function distinct()
{
$map = ['uuid' => AdminService::getUserId()];
$db1 = SystemFile::mk()->fieldRaw('max(id) id')->where($map)->group('type,xkey');
$db2 = $this->app->db->table($db1->buildSql())->alias('dt')->field('id');
SystemFile::mk()->whereRaw("id not in {$db2->buildSql()}")->delete();
SystemFile::mk()->where($map)->where(['status' => 1])->delete();
$this->success('清理重复文件成功!');
}
}

View File

@@ -0,0 +1,157 @@
<?php
// +----------------------------------------------------------------------
// | Admin Plugin for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免责声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 代码仓库https://gitee.com/zoujingli/think-plugs-admin
// | github 代码仓库https://github.com/zoujingli/think-plugs-admin
// +----------------------------------------------------------------------
namespace app\admin\controller;
use think\admin\Controller;
use think\admin\model\SystemUser;
use think\admin\service\AdminService;
use think\admin\service\MenuService;
/**
* 后台界面入口
* @class Index
* @package app\admin\controller
*/
class Index extends Controller
{
/**
* 显示后台首页
* @throws \think\admin\Exception
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function index()
{
/*! 根据运行模式刷新权限 */
AdminService::apply($this->app->isDebug());
/*! 读取当前用户权限菜单树 */
$this->menus = MenuService::getTree();
/*! 判断当前用户的登录状态 */
$this->login = AdminService::isLogin();
/*! 菜单为空且未登录跳转到登录页 */
if (empty($this->menus) && empty($this->login)) {
$this->redirect(sysuri('admin/login/index'));
} else {
$this->title = '系统管理后台';
$this->super = AdminService::isSuper();
$this->theme = AdminService::getUserTheme();
$this->fetch();
}
}
/**
* 后台主题切换
* @login true
* @return void
* @throws \think\admin\Exception
*/
public function theme()
{
if ($this->request->isGet()) {
$this->theme = AdminService::getUserTheme();
$this->themes = Config::themes;
$this->fetch();
} else {
$data = $this->_vali(['site_theme.require' => '主题名称不能为空!']);
if (AdminService::setUserTheme($data['site_theme'])) {
$this->success('主题配置保存成功!');
} else {
$this->error('主题配置保存失败!');
}
}
}
/**
* 修改用户资料
* @login true
* @param mixed $id 用户ID
*/
public function info($id = 0)
{
$this->_applyFormToken();
if (AdminService::getUserId() === intval($id)) {
SystemUser::mForm('user/form', 'id', [], ['id' => $id]);
} else {
$this->error('只能修改自己的资料!');
}
}
/**
* 资料修改表单处理
* @param array $data
*/
protected function _info_form_filter(array &$data)
{
if ($this->request->isPost()) {
unset($data['username'], $data['authorize']);
}
}
/**
* 资料修改结果处理
* @param bool $status
*/
protected function _info_form_result(bool $status)
{
if ($status) {
$this->success('用户资料修改成功!', 'javascript:location.reload()');
}
}
/**
* 修改当前用户密码
* @login true
* @param mixed $id
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function pass($id = 0)
{
$this->_applyFormToken();
if (AdminService::getUserId() !== intval($id)) {
$this->error('禁止修改他人密码!');
}
if ($this->app->request->isGet()) {
$this->verify = true;
SystemUser::mForm('user/pass', 'id', [], ['id' => $id]);
} else {
$data = $this->_vali([
'password.require' => '登录密码不能为空!',
'repassword.require' => '重复密码不能为空!',
'oldpassword.require' => '旧的密码不能为空!',
'password.confirm:repassword' => '两次输入的密码不一致!',
]);
$user = SystemUser::mk()->find($id);
if (empty($user)) $this->error('用户不存在!');
if (md5($data['oldpassword']) !== $user['password']) {
$this->error('旧密码验证失败,请重新输入!');
}
if ($user->save(['password' => md5($data['password'])])) {
sysoplog('系统用户管理', "修改用户[{$user['id']}]密码成功");
// 修改密码同步事件处理
$this->app->event->trigger('PluginAdminChangePassword', [
'uuid' => intval($user['id']), 'pass' => $data['password']
]);
$this->success('密码修改成功,下次请使用新密码登录!', '');
} else {
$this->error('密码修改失败,请稍候再试!');
}
}
}
}

View File

@@ -0,0 +1,137 @@
<?php
// +----------------------------------------------------------------------
// | Admin Plugin for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免责声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 代码仓库https://gitee.com/zoujingli/think-plugs-admin
// | github 代码仓库https://github.com/zoujingli/think-plugs-admin
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace app\admin\controller;
use think\admin\Controller;
use think\admin\extend\CodeExtend;
use think\admin\model\SystemUser;
use think\admin\service\AdminService;
use think\admin\service\CaptchaService;
use think\admin\service\RuntimeService;
use think\admin\service\SystemService;
/**
* 用户登录管理
* @class Login
* @package app\admin\controller
*/
class Login extends Controller
{
/**
* 后台登录入口
* @return void
* @throws \think\admin\Exception
*/
public function index()
{
if ($this->app->request->isGet()) {
if (AdminService::isLogin()) {
$this->redirect(sysuri('admin/index/index'));
} else {
// 加载登录模板
$this->title = '系统登录';
// 登录验证令牌
$this->captchaType = 'LoginCaptcha';
$this->captchaToken = CodeExtend::uuid();
// 当前运行模式
$this->runtimeMode = RuntimeService::check();
// 后台背景处理
$images = str2arr(sysconf('login_image|raw') ?: '', '|');
if (empty($images)) $images = [
SystemService::uri('/static/theme/img/login/bg1.jpg'),
SystemService::uri('/static/theme/img/login/bg2.jpg'),
];
$this->loginStyle = sprintf('style="background-image:url(%s)" data-bg-transition="%s"', $images[0], join(',', $images));
// 更新后台主域名,用于部分无法获取域名的场景调用
if ($this->request->domain() !== sysconf('base.site_host|raw')) {
sysconf('base.site_host', $this->request->domain());
}
$this->fetch();
}
} else {
$data = $this->_vali([
'username.require' => '登录账号不能为空!',
'username.min:4' => '账号不能少于4位字符!',
'password.require' => '登录密码不能为空!',
'password.min:4' => '密码不能少于4位字符!',
'verify.require' => '图形验证码不能为空!',
'uniqid.require' => '图形验证标识不能为空!',
]);
if (!CaptchaService::instance()->check($data['verify'], $data['uniqid'])) {
$this->error('图形验证码验证失败,请重新输入!');
}
/*! 用户信息验证 */
$map = ['username' => $data['username'], 'is_deleted' => 0];
$user = SystemUser::mk()->where($map)->findOrEmpty();
if ($user->isEmpty()) {
$this->app->session->set('LoginInputSessionError', true);
$this->error('登录账号或密码错误,请重新输入!');
}
if (empty($user['status'])) {
$this->app->session->set('LoginInputSessionError', true);
$this->error('账号已经被禁用,请联系管理员!');
}
if (md5("{$user['password']}{$data['uniqid']}") !== $data['password']) {
$this->app->session->set('LoginInputSessionError', true);
$this->error('登录账号或密码错误,请重新输入!');
}
$user->hidden(['sort', 'status', 'password', 'is_deleted']);
$this->app->session->set('user', $user->toArray());
$this->app->session->delete('LoginInputSessionError');
// 更新登录次数
$user->where(['id' => $user->getAttr('id')])->inc('login_num')->update([
'login_at' => date('Y-m-d H:i:s'), 'login_ip' => $this->app->request->ip(),
]);
// 刷新用户权限
AdminService::apply(true);
sysoplog('系统用户登录', '登录系统后台成功');
$this->success('登录成功', sysuri('admin/index/index'));
}
}
/**
* 生成验证码
* @return void
*/
public function captcha()
{
$input = $this->_vali([
'type.require' => '类型不能为空!',
'token.require' => '标识不能为空!',
]);
$image = CaptchaService::instance()->initialize();
$captcha = ['image' => $image->getData(), 'uniqid' => $image->getUniqid()];
// 未发生异常时,直接返回验证码内容
if (!$this->app->session->get('LoginInputSessionError')) {
$captcha['code'] = $image->getCode();
}
$this->success('生成验证码成功', $captcha);
}
/**
* 退出登录
* @return void
*/
public function out()
{
$this->app->session->destroy();
$this->success('退出登录成功!', sysuri('admin/login/index'));
}
}

View File

@@ -0,0 +1,147 @@
<?php
// +----------------------------------------------------------------------
// | Admin Plugin for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免责声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 代码仓库https://gitee.com/zoujingli/think-plugs-admin
// | github 代码仓库https://github.com/zoujingli/think-plugs-admin
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace app\admin\controller;
use think\admin\Controller;
use think\admin\extend\DataExtend;
use think\admin\model\SystemMenu;
use think\admin\service\AdminService;
use think\admin\service\MenuService;
use think\admin\service\NodeService;
/**
* 系统菜单管理
* @class Menu
* @package app\admin\controller
*/
class Menu extends Controller
{
/**
* 系统菜单管理
* @auth true
* @menu true
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function index()
{
$this->title = '系统菜单管理';
$this->type = $this->get['type'] ?? 'index';
SystemMenu::mQuery()->layTable();
}
/**
* 列表数据处理
* @param array $data
*/
protected function _index_page_filter(array &$data)
{
$data = DataExtend::arr2tree($data);
// 回收站过滤有效菜单
if ($this->type === 'recycle') foreach ($data as $k1 => &$p1) {
if (!empty($p1['sub'])) foreach ($p1['sub'] as $k2 => &$p2) {
if (!empty($p2['sub'])) foreach ($p2['sub'] as $k3 => $p3) {
if ($p3['status'] > 0) unset($p2['sub'][$k3]);
}
if (empty($p2['sub']) && ($p2['url'] === '#' or $p2['status'] > 0)) unset($p1['sub'][$k2]);
}
if (empty($p1['sub']) && ($p1['url'] === '#' or $p1['status'] > 0)) unset($data[$k1]);
}
// 菜单数据树数据变平化
$data = DataExtend::arr2table($data);
foreach ($data as &$vo) {
if ($vo['url'] !== '#' && !preg_match('/^(https?:)?(\/\/|\\\\)/i', $vo['url'])) {
$vo['url'] = trim(url($vo['url']) . ($vo['params'] ? "?{$vo['params']}" : ''), '\\/');
}
}
}
/**
* 添加系统菜单
* @auth true
*/
public function add()
{
$this->_applyFormToken();
SystemMenu::mForm('form');
}
/**
* 编辑系统菜单
* @auth true
*/
public function edit()
{
$this->_applyFormToken();
SystemMenu::mForm('form');
}
/**
* 表单数据处理
* @param array $vo
*/
protected function _form_filter(array &$vo)
{
if ($this->request->isGet()) {
$debug = $this->app->isDebug();
/* 清理权限节点 */
$debug && AdminService::clear();
/* 读取系统功能节点 */
$this->auths = [];
$this->nodes = MenuService::getList($debug);
foreach (NodeService::getMethods($debug) as $node => $item) {
if ($item['isauth'] && substr_count($node, '/') >= 2) {
$this->auths[] = ['node' => $node, 'title' => $item['title']];
}
}
/* 选择自己上级菜单 */
$vo['pid'] = $vo['pid'] ?? input('pid', '0');
/* 列出可选上级菜单 */
$menus = SystemMenu::mk()->order('sort desc,id asc')->column('id,pid,icon,url,node,title,params', 'id');
$this->menus = DataExtend::arr2table(array_merge($menus, [['id' => '0', 'pid' => '-1', 'url' => '#', 'title' => '顶部菜单']]));
if (isset($vo['id'])) foreach ($this->menus as $menu) if ($menu['id'] === $vo['id']) $vo = $menu;
foreach ($this->menus as $key => $menu) if ($menu['spt'] >= 3 || $menu['url'] !== '#') unset($this->menus[$key]);
if (isset($vo['spt']) && isset($vo['spc']) && in_array($vo['spt'], [1, 2]) && $vo['spc'] > 0) {
foreach ($this->menus as $key => $menu) if ($vo['spt'] <= $menu['spt']) unset($this->menus[$key]);
}
}
}
/**
* 修改菜单状态
* @auth true
*/
public function state()
{
SystemMenu::mSave($this->_vali([
'status.in:0,1' => '状态值范围异常!',
'status.require' => '状态值不能为空!',
]));
}
/**
* 删除系统菜单
* @auth true
*/
public function remove()
{
SystemMenu::mDelete();
}
}

View File

@@ -0,0 +1,95 @@
<?php
// +----------------------------------------------------------------------
// | Admin Plugin for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免责声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 代码仓库https://gitee.com/zoujingli/think-plugs-admin
// | github 代码仓库https://github.com/zoujingli/think-plugs-admin
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace app\admin\controller;
use Ip2Region;
use think\admin\Controller;
use think\admin\helper\QueryHelper;
use think\admin\model\SystemOplog;
use think\exception\HttpResponseException;
/**
* 系统日志管理
* @class Oplog
* @package app\admin\controller
*/
class Oplog extends Controller
{
/**
* 系统日志管理
* @auth true
* @menu true
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function index()
{
SystemOplog::mQuery()->layTable(function () {
$this->title = '系统日志管理';
$columns = SystemOplog::mk()->column('action,username', 'id');
$this->users = array_unique(array_column($columns, 'username'));
$this->actions = array_unique(array_column($columns, 'action'));
}, static function (QueryHelper $query) {
$query->dateBetween('create_at')->equal('username,action')->like('content,geoip,node');
});
}
/**
* 列表数据处理
* @param array $data
* @throws \Exception
*/
protected function _index_page_filter(array &$data)
{
$region = new Ip2Region();
foreach ($data as &$vo) try {
$vo['geoisp'] = $region->simple($vo['geoip']);
} catch (\Exception $exception) {
$vo['geoip'] = $exception->getMessage();
}
}
/**
* 清理系统日志
* @auth true
*/
public function clear()
{
try {
SystemOplog::mQuery()->empty();
sysoplog('系统运维管理', '成功清理所有日志');
$this->success('日志清理成功!');
} catch (HttpResponseException $exception) {
throw $exception;
} catch (\Exception $exception) {
trace_file($exception);
$this->error(lang("日志清理失败,%s", [$exception->getMessage()]));
}
}
/**
* 删除系统日志
* @auth true
*/
public function remove()
{
SystemOplog::mDelete();
}
}

View File

@@ -0,0 +1,117 @@
<?php
// +----------------------------------------------------------------------
// | Admin Plugin for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免责声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 代码仓库https://gitee.com/zoujingli/think-plugs-admin
// | github 代码仓库https://github.com/zoujingli/think-plugs-admin
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace app\admin\controller;
use think\admin\Controller;
use think\admin\helper\QueryHelper;
use think\admin\model\SystemQueue;
use think\admin\service\AdminService;
use think\admin\service\ProcessService;
use think\admin\service\QueueService;
use think\exception\HttpResponseException;
/**
* 系统任务管理
* @class Queue
* @package app\admin\controller
*/
class Queue extends Controller
{
/**
* 系统任务管理
* @auth true
* @menu true
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function index()
{
SystemQueue::mQuery()->layTable(function () {
$this->title = '系统任务管理';
$this->iswin = ProcessService::iswin();
if ($this->super = AdminService::isSuper()) {
$this->command = ProcessService::think('xadmin:queue start');
if (!$this->iswin && !empty($_SERVER['USER'])) {
$this->command = "sudo -u {$_SERVER['USER']} {$this->command}";
}
}
}, static function (QueryHelper $query) {
$query->equal('status')->like('code|title#title,command');
$query->timeBetween('enter_time,exec_time')->dateBetween('create_at');
});
}
/**
* 分页数据回调处理
* @param array $data
* @param array $result
* @return void
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
protected function _index_page_filter(array $data, array &$result)
{
$result['extra'] = ['dos' => 0, 'pre' => 0, 'oks' => 0, 'ers' => 0];
SystemQueue::mk()->field('status,count(1) count')->group('status')->select()->map(static function ($item) use (&$result) {
if (intval($item['status']) === 1) $result['extra']['pre'] = $item['count'];
if (intval($item['status']) === 2) $result['extra']['dos'] = $item['count'];
if (intval($item['status']) === 3) $result['extra']['oks'] = $item['count'];
if (intval($item['status']) === 4) $result['extra']['ers'] = $item['count'];
});
}
/**
* 重启系统任务
* @auth true
*/
public function redo()
{
try {
$data = $this->_vali(['code.require' => '任务编号不能为空!']);
$queue = QueueService::instance()->initialize($data['code'])->reset();
$queue->progress(1, '>>> 任务重置成功 <<<', '0.00');
$this->success('任务重置成功!', $queue->code);
} catch (HttpResponseException $exception) {
throw $exception;
} catch (\Exception $exception) {
trace_file($exception);
$this->error($exception->getMessage());
}
}
/**
* 清理运行数据
* @auth true
*/
public function clean()
{
$this->_queue('定时清理系统运行数据', "xadmin:queue clean", 0, [], 0, 3600);
}
/**
* 删除系统任务
* @auth true
*/
public function remove()
{
SystemQueue::mDelete();
}
}

View File

@@ -0,0 +1,182 @@
<?php
// +----------------------------------------------------------------------
// | Admin Plugin for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免责声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 代码仓库https://gitee.com/zoujingli/think-plugs-admin
// | github 代码仓库https://github.com/zoujingli/think-plugs-admin
// +----------------------------------------------------------------------
namespace app\admin\controller;
use think\admin\Controller;
use think\admin\helper\QueryHelper;
use think\admin\model\SystemAuth;
use think\admin\model\SystemBase;
use think\admin\model\SystemUser;
use think\admin\service\AdminService;
/**
* 系统用户管理
* @class User
* @package app\admin\controller
*/
class User extends Controller
{
/**
* 系统用户管理
* @auth true
* @menu true
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function index()
{
$this->type = $this->get['type'] ?? 'index';
SystemUser::mQuery()->layTable(function () {
$this->title = '系统用户管理';
$this->bases = SystemBase::items('身份权限');
}, function (QueryHelper $query) {
// 加载对应数据列表
$query->where(['is_deleted' => 0, 'status' => intval($this->type === 'index')]);
// 关联用户身份资料
/** @var \think\model\Relation|\think\db\Query $query */
$query->with(['userinfo' => static function ($query) {
$query->field('code,name,content');
}]);
// 数据列表搜索过滤
$query->equal('status,usertype')->dateBetween('login_at,create_at');
$query->like('username|nickname#username,contact_phone#phone,contact_mail#mail');
});
}
/**
* 添加系统用户
* @auth true
*/
public function add()
{
SystemUser::mForm('form');
}
/**
* 编辑系统用户
* @auth true
*/
public function edit()
{
SystemUser::mForm('form');
}
/**
* 修改用户密码
* @auth true
*/
public function pass()
{
$this->_applyFormToken();
if ($this->request->isGet()) {
$this->verify = false;
SystemUser::mForm('pass');
} else {
$data = $this->_vali([
'id.require' => '用户ID不能为空',
'password.require' => '登录密码不能为空!',
'repassword.require' => '重复密码不能为空!',
'repassword.confirm:password' => '两次输入的密码不一致!',
]);
$user = SystemUser::mk()->findOrEmpty($data['id']);
if ($user->isExists() && $user->save(['password' => md5($data['password'])])) {
// 修改密码同步事件处理
$this->app->event->trigger('PluginAdminChangePassword', [
'uuid' => $data['id'], 'pass' => $data['password']
]);
sysoplog('系统用户管理', "修改用户[{$data['id']}]密码成功");
$this->success('密码修改成功,请使用新密码登录!', '');
} else {
$this->error('密码修改失败,请稍候再试!');
}
}
}
/**
* 表单数据处理
* @param array $data
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
protected function _form_filter(array &$data)
{
if ($this->request->isPost()) {
// 检查资料是否完整
empty($data['username']) && $this->error('登录账号不能为空!');
if ($data['username'] !== AdminService::getSuperName()) {
empty($data['authorize']) && $this->error('未配置权限!');
}
// 处理上传的权限格式
$data['authorize'] = arr2str($data['authorize'] ?? []);
if (empty($data['id'])) {
// 检查账号是否重复
$map = ['username' => $data['username'], 'is_deleted' => 0];
if (SystemUser::mk()->where($map)->count() > 0) {
$this->error("账号已经存在,请使用其它账号!");
}
// 新添加的用户密码与账号相同
$data['password'] = md5($data['username']);
} else {
unset($data['username']);
}
} else {
// 权限绑定处理
$data['authorize'] = str2arr($data['authorize'] ?? '');
$this->auths = SystemAuth::items();
$this->bases = SystemBase::items('身份权限');
$this->super = AdminService::getSuperName();
}
}
/**
* 修改用户状态
* @auth true
*/
public function state()
{
$this->_checkInput();
SystemUser::mSave($this->_vali([
'status.in:0,1' => '状态值范围异常!',
'status.require' => '状态值不能为空!',
]));
}
/**
* 删除系统用户
* @auth true
*/
public function remove()
{
$this->_checkInput();
SystemUser::mDelete();
}
/**
* 检查输入变量
*/
private function _checkInput()
{
if (in_array('10000', str2arr(input('id', '')))) {
$this->error('系统超级账号禁止删除!');
}
}
}

View File

@@ -0,0 +1,114 @@
<?php
declare (strict_types = 1);
namespace app\admin\controller\api;
use think\admin\Controller;
/**
* 邮箱配置接口
*/
class Mail extends Controller
{
/**
* 邮箱配置列表
*/
private $mailConfigs = [
[
'name' => '默认配置1',
'config' => [
'DOMAIN' => '586vip.cn',
'TEMP_MAIL' => 'ademyyk',
'TEMP_MAIL_EXT' => '@mailto.plus',
'BROWSER_USER_AGENT' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.92 Safari/537.36',
// 'BROWSER_PROXY' => 'http://127.0.0.1:2080',
// 'BROWSER_HEADLESS' => 'True',
'MAIL_SERVER' => 'https://tempmail.plus'
]
],
[
'name' => '备用配置1',
'config' => [
'DOMAIN' => 'nosqli.com',
'TEMP_MAIL' => 'ademyyk',
'TEMP_MAIL_EXT' => '@mailto.plus',
'BROWSER_USER_AGENT' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.92 Safari/537.36',
'MAIL_SERVER' => 'https://tempmail.plus'
]
]
// ,
// [
// 'name' => 'IMAP配置',
// 'config' => [
// 'DOMAIN' => 'wuen.site',
// 'TEMP_MAIL' => null,
// 'IMAP_SERVER' => 'imap.163.com',
// 'IMAP_PORT' => 993,
// 'IMAP_USER' => 'maticarmy@163.com',
// 'IMAP_PASS' => 'LQer6rsSWan6vtuz',
// 'BROWSER_USER_AGENT' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.92 Safari/537.36',
// 'MAIL_SERVER' => 'https://tempmail.plus'
// ]
// ]
];
/**
* 获取所有配置
*/
public function getAll()
{
return json([
'code' => 0,
'msg' => '获取成功',
'data' => $this->mailConfigs
]);
}
/**
* 获取随机配置
*/
public function getRandom()
{
// 随机获取一个配置
$config = $this->mailConfigs[array_rand($this->mailConfigs)];
return json([
'code' => 0,
'msg' => '获取成功',
'data' => [
'name' => $config['name'],
'env' => $config['config']
]
]);
}
/**
* 获取指定配置
*/
public function getConfig()
{
$name = input('name', '');
// 如果未指定名称,返回随机配置
if (empty($name)) {
return $this->getRandom();
}
// 查找指定配置
foreach ($this->mailConfigs as $config) {
if ($config['name'] === $name) {
return json([
'code' => 0,
'msg' => '获取成功',
'data' => [
'name' => $config['name'],
'env' => $config['config']
]
]);
}
}
// 未找到指定配置,返回随机配置
return $this->getRandom();
}
}

View File

@@ -0,0 +1,85 @@
<?php
declare (strict_types = 1);
namespace app\admin\controller\api;
use app\manager\model\Member as MemberModel;
use think\admin\Controller;
/**
* 会员接口管理
*/
class Member extends Controller
{
/**
* 验证会员状态
*/
public function check()
{
// 接收参数
$keyword = trim(input('keyword', ''));
if (empty($keyword)) {
return json([
'code' => 1,
'msg' => '请输入查询关键字'
]);
}
// 查询会员信息 (email = xxx OR order_id = xxx)
$member = MemberModel::whereOr('email', '=', $keyword)
->whereOr('order_id', '=', $keyword)
->find();
if (empty($member)) {
return json([
'code' => 1,
'msg' => '会员不存在'
]);
}
// 检查会员状态
if ($member['status'] != 1) {
return json([
'code' => 1,
'msg' => '会员已被禁用'
]);
}
// 检查有效期
if (strtotime($member['expire_time']) < time()) {
return json([
'code' => 1,
'msg' => '会员已过期'
]);
}
// 检查使用次数
if ($member['usage_limit'] > 0 && $member['used_count'] >= $member['usage_limit']) {
return json([
'code' => 1,
'msg' => '使用次数已达上限'
]);
}
// 更新使用次数和最后登录信息
$member->used_count = $member->used_count + 1;
$member->last_login_time = date('Y-m-d H:i:s');
$member->last_login_ip = $this->request->ip();
$member->save();
// 返回成功
return json([
'code' => 0,
'msg' => '验证通过',
'data' => [
'email' => $member['email'],
'order_id' => $member['order_id'],
'expire_time' => $member['expire_time'],
'usage_limit' => $member['usage_limit'],
'used_count' => $member['used_count'],
'last_login_time' => $member['last_login_time']
]
]);
}
}

View File

@@ -0,0 +1,91 @@
<?php
// +----------------------------------------------------------------------
// | Admin Plugin for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免责声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 代码仓库https://gitee.com/zoujingli/think-plugs-admin
// | github 代码仓库https://github.com/zoujingli/think-plugs-admin
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace app\admin\controller\api;
use think\admin\Controller;
use think\admin\service\AdminService;
use think\Response;
/**
* 扩展插件管理
* @class Plugs
* @package app\admin\controller\api
*/
class Plugs extends Controller
{
/**
* 图标选择器
* @login true
*/
public function icon()
{
$this->title = '图标选择器';
// 读取 layui 字体图标
if (empty($this->layuiIcons = $this->app->cache->get('LayuiIcons', []))) {
$style = file_get_contents(syspath('public/static/plugs/layui/css/layui.css'));
if (preg_match_all('#\.(layui-icon-[\w-]+):#', $style, $matches)) {
if (count($this->layuiIcons = $matches[1]) > 0) {
$this->app->cache->set('LayuiIcons', $this->layuiIcons, 60);
}
}
}
// 读取自定义字体图标
if (empty($this->thinkIcons = $this->app->cache->get('ThinkAdminSelfIcons', []))) {
$style = file_get_contents(syspath('public/static/theme/css/iconfont.css'));
if (preg_match_all('#\.(iconfont-[\w-]+):#', $style, $matches)) {
if (count($this->thinkIcons = $matches[1]) > 0) {
$this->app->cache->set('ThinkAdminSelfIcons', $this->thinkIcons, 60);
}
}
}
$this->field = $this->app->request->get('field', 'icon');
$this->fetch(dirname(__DIR__, 2) . '/view/api/icon.html');
}
/**
* 前端脚本变量
* @return \think\Response
* @throws \think\admin\Exception
*/
public function script(): Response
{
$token = $this->request->get('uptoken', '');
$domain = boolval(AdminService::withUploadUnid($token));
return response(join("\r\n", [
sprintf("window.taDebug = %s;", $this->app->isDebug() ? 'true' : 'false'),
sprintf("window.taAdmin = '%s';", sysuri('admin/index/index', [], false, $domain)),
sprintf("window.taEditor = '%s';", sysconf('base.editor|raw') ?: 'ckeditor4'),
]))->contentType('application/javascript');
}
/**
* 优化数据库
* @login true
*/
public function optimize()
{
if (AdminService::isSuper()) {
sysoplog('系统运维管理', '创建数据库优化任务');
$this->_queue('优化数据库所有数据表', 'xadmin:database optimize');
} else {
$this->error('请使用超管账号操作!');
}
}
}

View File

@@ -0,0 +1,91 @@
<?php
declare (strict_types = 1);
namespace app\admin\controller\api;
use think\admin\Controller;
/**
* 程序更新接口
*/
class Program extends Controller
{
/**
* 程序目录配置
*/
private $programPath = 'program/';
/**
* 获取程序信息
*/
public function info()
{
$path = public_path() . $this->programPath;
if (!is_dir($path)) {
return json([
'code' => 1,
'msg' => '程序目录不存在'
]);
}
// 获取目录中最新的文件
$files = glob($path . '*');
if (empty($files)) {
return json([
'code' => 1,
'msg' => '暂无可用程序'
]);
}
// 获取最新文件
$latest = array_reduce($files, function($carry, $file) {
return (!$carry || filemtime($file) > filemtime($carry)) ? $file : $carry;
});
$name = basename($latest);
$size = filesize($latest);
$md5 = md5_file($latest);
return json([
'code' => 0,
'msg' => '获取成功',
'data' => [
'name' => $name,
'size' => $size,
'md5' => $md5,
'url' => request()->domain() . '/' . $this->programPath . $name,
'time' => date('Y-m-d H:i:s', filemtime($latest))
]
]);
}
/**
* 下载程序
*/
public function down()
{
$path = public_path() . $this->programPath;
if (!is_dir($path)) {
return json([
'code' => 1,
'msg' => '程序目录不存在'
]);
}
// 获取目录中最新的文件
$files = glob($path . '*');
if (empty($files)) {
return json([
'code' => 1,
'msg' => '暂无可用程序'
]);
}
// 获取最新文件
$latest = array_reduce($files, function($carry, $file) {
return (!$carry || filemtime($file) > filemtime($carry)) ? $file : $carry;
});
return download($latest, basename($latest));
}
}

View File

@@ -0,0 +1,118 @@
<?php
// +----------------------------------------------------------------------
// | Admin Plugin for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免责声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 代码仓库https://gitee.com/zoujingli/think-plugs-admin
// | github 代码仓库https://github.com/zoujingli/think-plugs-admin
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace app\admin\controller\api;
use Psr\Log\NullLogger;
use think\admin\Controller;
use think\admin\model\SystemQueue;
use think\admin\service\AdminService;
use think\exception\HttpResponseException;
/**
* 任务监听服务管理
* @class Queue
* @package app\admin\controller\api
*/
class Queue extends Controller
{
/**
* 停止监听服务
* @login true
*/
public function stop()
{
if (AdminService::isSuper()) try {
$message = $this->app->console->call('xadmin:queue', ['stop'])->fetch();
if (stripos($message, 'sent end signal to process')) {
sysoplog('系统运维管理', '尝试停止任务监听服务');
$this->success('停止任务监听服务成功!');
} elseif (stripos($message, 'processes to stop')) {
$this->success('没有找到需要停止的服务!');
} else {
$this->error(nl2br($message));
}
} catch (HttpResponseException $exception) {
throw $exception;
} catch (\Exception $exception) {
trace_file($exception);
$this->error($exception->getMessage());
} else {
$this->error('请使用超管账号操作!');
}
}
/**
* 启动监听服务
* @login true
*/
public function start()
{
if (AdminService::isSuper()) try {
$message = $this->app->console->call('xadmin:queue', ['start'])->fetch();
if (stripos($message, 'daemons started successfully for pid')) {
sysoplog('系统运维管理', '尝试启动任务监听服务');
$this->success('任务监听服务启动成功!');
} elseif (stripos($message, 'daemons already exist for pid')) {
$this->success('任务监听服务已经启动!');
} else {
$this->error(nl2br($message));
}
} catch (HttpResponseException $exception) {
throw $exception;
} catch (\Exception $exception) {
trace_file($exception);
$this->error($exception->getMessage());
} else {
$this->error('请使用超管账号操作!');
}
}
/**
* 检查监听服务
* @login true
*/
public function status()
{
if (AdminService::isSuper()) try {
$message = $this->app->console->call('xadmin:queue', ['status'])->fetch();
if (preg_match('/process.*?\d+.*?running/', $message)) {
echo "<span class='color-green pointer' data-tips-text='{$message}'>{$this->app->lang->get('已启动')}</span>";
} else {
echo "<span class='color-red pointer' data-tips-text='{$message}'>{$this->app->lang->get('未启动')}</span>";
}
} catch (\Error|\Exception $exception) {
echo "<span class='color-red pointer' data-tips-text='{$exception->getMessage()}'>{$this->app->lang->get('异 常')}</span>";
} else {
$message = lang('只有超级管理员才能操作!');
echo "<span class='color-red pointer' data-tips-text='{$message}'>{$this->app->lang->get('无权限')}</span>";
}
}
/**
* 查询任务进度
* @login true
*/
public function progress()
{
$input = $this->_vali(['code.require' => '任务编号不能为空!']);
$this->app->db->setLog(new NullLogger()); /* 关闭数据库请求日志 */
$message = SystemQueue::mk()->where($input)->value('message', '');
$this->success('获取任务进度成功d', json_decode($message, true));
}
}

View File

@@ -0,0 +1,138 @@
<?php
// +----------------------------------------------------------------------
// | Admin Plugin for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免责声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 代码仓库https://gitee.com/zoujingli/think-plugs-admin
// | github 代码仓库https://github.com/zoujingli/think-plugs-admin
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace app\admin\controller\api;
use think\admin\Controller;
use think\admin\model\SystemConfig;
use think\admin\service\AdminService;
use think\admin\service\RuntimeService;
use think\exception\HttpResponseException;
/**
* 系统运行管理
* @class System
* @package app\admin\controller\api
*/
class System extends Controller
{
/**
* 网站压缩发布
* @login true
*/
public function push()
{
if (AdminService::isSuper()) try {
RuntimeService::push() && sysoplog('系统运维管理', '刷新发布运行缓存');
$this->success('网站缓存加速成功!', 'javascript:location.reload()');
} catch (HttpResponseException $exception) {
throw $exception;
} catch (\Exception $exception) {
trace_file($exception);
$this->error($exception->getMessage());
} else {
$this->error('请使用超管账号操作!');
}
}
/**
* 清理运行缓存
* @login true
*/
public function clear()
{
if (AdminService::isSuper()) try {
RuntimeService::clear() && sysoplog('系统运维管理', '清理网站日志缓存');
$this->success('清空日志缓存成功!', 'javascript:location.reload()');
} catch (HttpResponseException $exception) {
throw $exception;
} catch (\Exception $exception) {
trace_file($exception);
$this->error($exception->getMessage());
} else {
$this->error('请使用超管账号操作!');
}
}
/**
* 当前运行模式
* @login true
*/
public function debug()
{
if (AdminService::isSuper()) if (input('state')) {
RuntimeService::set('product');
sysoplog('系统运维管理', '开发模式切换为生产模式');
$this->success('已切换为生产模式!', 'javascript:location.reload()');
} else {
RuntimeService::set('debug');
sysoplog('系统运维管理', '生产模式切换为开发模式');
$this->success('已切换为开发模式!', 'javascript:location.reload()');
} else {
$this->error('请使用超管账号操作!');
}
}
/**
* 修改富文本编辑器
* @return void
* @throws \think\admin\Exception
*/
public function editor()
{
if (AdminService::isSuper()) {
$editor = input('editor', 'auto');
sysconf('base.editor', $editor);
sysoplog('系统运维管理', "切换编辑器为{$editor}");
$this->success('已切换后台编辑器!', 'javascript:location.reload()');
} else {
$this->error('请使用超管账号操作!');
}
}
/**
* 清理系统配置
* @login true
*/
public function config()
{
if (AdminService::isSuper()) try {
[$tmpdata, $newdata] = [[], []];
foreach (SystemConfig::mk()->order('type,name asc')->cursor() as $item) {
$tmpdata[$item['type']][$item['name']] = $item['value'];
}
foreach ($tmpdata as $type => $items) foreach ($items as $name => $value) {
$newdata[] = ['type' => $type, 'name' => $name, 'value' => $value];
}
$this->app->db->transaction(static function () use ($newdata) {
SystemConfig::mQuery()->empty()->insertAll($newdata);
});
$this->app->cache->delete('SystemConfig');
sysoplog('系统运维管理', '清理系统配置参数');
$this->success('清理系统配置成功!', 'javascript:location.reload()');
} catch (HttpResponseException $exception) {
throw $exception;
} catch (\Exception $exception) {
trace_file($exception);
$this->error($exception->getMessage());
} else {
$this->error('请使用超管账号操作!');
}
}
}

View File

@@ -0,0 +1,336 @@
<?php
// +----------------------------------------------------------------------
// | Admin Plugin for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免责声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 代码仓库https://gitee.com/zoujingli/think-plugs-admin
// | github 代码仓库https://github.com/zoujingli/think-plugs-admin
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace app\admin\controller\api;
use think\admin\Controller;
use think\admin\helper\QueryHelper;
use think\admin\model\SystemFile;
use think\admin\service\AdminService;
use think\admin\Storage;
use think\admin\storage\AliossStorage;
use think\admin\storage\AlistStorage;
use think\admin\storage\LocalStorage;
use think\admin\storage\QiniuStorage;
use think\admin\storage\TxcosStorage;
use think\admin\storage\UpyunStorage;
use think\exception\HttpResponseException;
use think\file\UploadedFile;
use think\Response;
/**
* 文件上传接口
* @class Upload
* @package app\admin\controller\api
*/
class Upload extends Controller
{
/**
* 文件上传脚本
* @return Response
* @throws \think\admin\Exception
*/
public function index(): Response
{
$data = ['exts' => []];
[$uuid, $unid, $exts] = $this->initUnid(false);
$allows = str2arr(sysconf('storage.allow_exts|raw'));
if (empty($uuid) && $unid > 0) $allows = array_intersect($exts, $allows);
foreach ($allows as $ext) $data['exts'][$ext] = Storage::mime($ext);
$data['exts'] = json_encode($data['exts'], JSON_UNESCAPED_UNICODE);
$data['nameType'] = sysconf('storage.name_type|raw') ?: 'xmd5';
return view(dirname(__DIR__, 2) . '/view/api/upload.js', $data)->contentType('application/x-javascript');
}
/**
* 文件选择器
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function image()
{
[$uuid, $unid] = $this->initUnid();
SystemFile::mQuery()->layTable(function () {
$this->title = '文件选择器';
}, function (QueryHelper $query) use ($unid, $uuid) {
if ($unid && $uuid) $query->where(function ($query) use ($uuid, $unid) {
/** @var \think\db\Query $query */
$query->whereOr([['uuid', '=', $uuid], ['unid', '=', $unid]]);
}); else {
$query->where($unid ? ['unid' => $unid] : ['uuid' => $uuid]);
}
$query->where(['status' => 2, 'issafe' => 0])->in('xext#type');
$query->like('name,hash')->dateBetween('create_at')->order('id desc');
});
}
/**
* 文件上传检查
*/
public function state()
{
try {
[$uuid, $unid] = $this->initUnid();
[$name, $safe] = [input('name'), $this->getSafe()];
$data = ['uptype' => $this->getType(), 'safe' => intval($safe), 'key' => input('key')];
$file = SystemFile::mk()->data($this->_vali([
'xkey.value' => $data['key'],
'type.value' => $this->getType(),
'uuid.value' => $uuid,
'unid.value' => $unid,
'name.require' => '名称不能为空!',
'hash.require' => '哈希不能为空!',
'xext.require' => '后缀不能为空!',
'size.require' => '大小不能为空!',
'mime.default' => '',
'status.value' => 1,
]));
$mime = $file->getAttr('mime');
if (empty($mime)) $file->setAttr('mime', Storage::mime($file->getAttr('xext')));
$info = Storage::instance($data['uptype'])->info($data['key'], $safe, $name);
if (isset($info['url']) && isset($info['key'])) {
$file->save(['xurl' => $info['url'], 'isfast' => 1, 'issafe' => $data['safe']]);
$extr = ['id' => $file->id ?? 0, 'url' => $info['url'], 'key' => $info['key']];
$this->success('文件已经上传', array_merge($data, $extr), 200);
} elseif ('local' === $data['uptype']) {
$local = LocalStorage::instance();
$data['url'] = $local->url($data['key'], $safe, $name);
$data['server'] = $local->upload();
} elseif ('qiniu' === $data['uptype']) {
$qiniu = QiniuStorage::instance();
$data['url'] = $qiniu->url($data['key'], $safe, $name);
$data['token'] = $qiniu->token($data['key'], 3600, $name);
$data['server'] = $qiniu->upload();
} elseif ('alioss' === $data['uptype']) {
$alioss = AliossStorage::instance();
$token = $alioss->token($data['key'], 3600, $name);
$data['url'] = $token['siteurl'];
$data['policy'] = $token['policy'];
$data['signature'] = $token['signature'];
$data['OSSAccessKeyId'] = $token['keyid'];
$data['server'] = $alioss->upload();
} elseif ('txcos' === $data['uptype']) {
$txcos = TxcosStorage::instance();
$token = $txcos->token($data['key'], 3600, $name);
$data['url'] = $token['siteurl'];
$data['q-ak'] = $token['q-ak'];
$data['policy'] = $token['policy'];
$data['q-key-time'] = $token['q-key-time'];
$data['q-signature'] = $token['q-signature'];
$data['q-sign-algorithm'] = $token['q-sign-algorithm'];
$data['server'] = $txcos->upload();
} elseif ('upyun' === $data['uptype']) {
$upyun = UpyunStorage::instance();
$token = $upyun->token($data['key'], 3600, $name, input('hash', ''));
$data['url'] = $token['siteurl'];
$data['policy'] = $token['policy'];
$data['server'] = $upyun->upload();
$data['authorization'] = $token['authorization'];
} elseif ('alist' === $data['uptype']) {
$alist = AlistStorage::instance();
$data['url'] = $alist->url($data['key']);
$data['server'] = $alist->upload();
$data['filepath'] = $alist->real($data['key']);
$data['authorization'] = $alist->token();
} else {
$this->error('未知的存储引擎!');
}
$file->save(['xurl' => $data['url'], 'isfast' => 0, 'issafe' => $data['safe']]);
$this->success('获取上传授权参数', array_merge($data, ['id' => $file->id ?? 0]), 404);
} catch (HttpResponseException $exception) {
throw $exception;
} catch (\Exception $exception) {
$this->error($exception->getMessage());
}
}
/**
* 更新文件状态
* @return void
*/
public function done()
{
[$uuid, $unid] = $this->initUnid();
$data = $this->_vali([
'id.require' => '编号不能为空!',
'hash.require' => '哈希不能为空!',
'uuid.value' => $uuid,
'unid.value' => $unid,
]);
$file = SystemFile::mk()->where($data)->findOrEmpty();
if ($file->isEmpty()) $this->error('文件不存在!');
if ($file->save(['status' => 2])) {
$this->success('更新成功!');
} else {
$this->error('更新失败!');
}
}
/**
* 文件上传入口
* @throws \think\admin\Exception
*/
public function file()
{
[$uuid, $unid, $unexts] = $this->initUnid();
// 开始处理文件上传
$file = $this->getFile();
$extension = strtolower($file->getOriginalExtension());
$saveFileName = input('key') ?: Storage::name($file->getPathname(), $extension, '', 'md5_file');
// 检查文件名称是否合法
if (strpos($saveFileName, '..') !== false) {
$this->error('文件路径不能出现跳级操作!');
}
// 检查文件后缀是否被恶意修改
if (strtolower(pathinfo(parse_url($saveFileName, PHP_URL_PATH), PATHINFO_EXTENSION)) !== $extension) {
$this->error('文件后缀异常,请重新上传文件!');
}
// 屏蔽禁止上传指定后缀的文件
if (!in_array($extension, str2arr(sysconf('storage.allow_exts|raw')))) {
$this->error('文件类型受限,请在后台配置规则!');
}
// 前端用户上传后缀检查处理
if (empty($uuid) && $unid > 0 && !in_array($extension, $unexts)) {
$this->error('文件类型受限,请上传允许的文件类型!');
}
if (in_array($extension, ['sh', 'asp', 'bat', 'cmd', 'exe', 'php'])) {
$this->error('文件安全保护,禁止上传可执行文件!');
}
try {
$safeMode = $this->getSafe();
if (($type = $this->getType()) === 'local') {
$local = LocalStorage::instance();
$distName = $local->path($saveFileName, $safeMode);
if (PHP_SAPI === 'cli') {
is_dir(dirname($distName)) || mkdir(dirname($distName), 0777, true);
rename($file->getPathname(), $distName);
} else {
$file->move(dirname($distName), basename($distName));
}
$info = $local->info($saveFileName, $safeMode, $file->getOriginalName());
if (in_array($extension, ['jpg', 'gif', 'png', 'bmp', 'jpeg', 'wbmp'])) {
if ($this->imgNotSafe($distName) && $local->del($saveFileName)) {
$this->error('图片未通过安全检查!');
}
[$width, $height] = getimagesize($distName);
if (($width < 1 || $height < 1) && $local->del($saveFileName)) {
$this->error('读取图片的尺寸失败!');
}
}
} else {
$bina = file_get_contents($file->getPathname());
$info = Storage::instance($type)->set($saveFileName, $bina, $safeMode, $file->getOriginalName());
}
if (isset($info['url'])) {
$this->success('文件上传成功!', ['url' => $safeMode ? $saveFileName : $info['url']]);
} else {
$this->error('文件处理失败,请稍候再试!');
}
} catch (HttpResponseException $exception) {
throw $exception;
} catch (\Exception $exception) {
trace_file($exception);
$this->error($exception->getMessage());
}
}
/**
* 获取上传类型
* @return boolean
*/
private function getSafe(): bool
{
return boolval(input('safe', '0'));
}
/**
* 获取上传方式
* @return string
* @throws \think\admin\Exception
*/
private function getType(): string
{
$type = strtolower(input('uptype', ''));
if (in_array($type, array_keys(Storage::types()))) {
return $type;
} else {
return strtolower(sysconf('storage.type|raw'));
}
}
/**
* 获取文件对象
* @return UploadedFile|void
*/
private function getFile(): UploadedFile
{
try {
$file = $this->request->file('file');
if ($file instanceof UploadedFile) {
return $file;
} else {
$this->error('读取临时文件失败!');
}
} catch (HttpResponseException $exception) {
throw $exception;
} catch (\Exception $exception) {
trace_file($exception);
$this->error(lang($exception->getMessage()));
}
}
/**
* 初始化用户状态
* @param boolean $check
* @return array
*/
private function initUnid(bool $check = true): array
{
$uuid = AdminService::getUserId();
[$unid, $exts] = AdminService::withUploadUnid();
if ($check && empty($uuid) && empty($unid)) {
$this->error('未登录,禁止使用文件上传!');
} else {
return [$uuid, $unid, $exts];
}
}
/**
* 检查图片是否安全
* @param string $filename
* @return boolean
*/
private function imgNotSafe(string $filename): bool
{
$source = fopen($filename, 'rb');
if (($size = filesize($filename)) > 512) {
$hexs = bin2hex(fread($source, 512));
fseek($source, $size - 512);
$hexs .= bin2hex(fread($source, 512));
} else {
$hexs = bin2hex(fread($source, $size));
}
if (is_resource($source)) fclose($source);
$bins = hex2bin($hexs);
/* 匹配十六进制中的 <% ( ) %> 或 <? ( ) ?> 或 <script | /script> */
foreach (['<?php ', '<% ', '<script '] as $key) if (stripos($bins, $key) !== false) return true;
$result = preg_match("/(3c25.*?28.*?29.*?253e)|(3c3f.*?28.*?29.*?3f3e)|(3C534352495054)|(2F5343524950543E)|(3C736372697074)|(2F7363726970743E)/is", $hexs);
return $result === false || $result > 0;
}
}

200
app/admin/lang/en-us.php Normal file
View File

@@ -0,0 +1,200 @@
<?php
// +----------------------------------------------------------------------
// | Admin Plugin for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免责声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 代码仓库https://gitee.com/zoujingli/think-plugs-admin
// | github 代码仓库https://github.com/zoujingli/think-plugs-admin
// +----------------------------------------------------------------------
$extra = [];
$extra['开发人员或在功能调试时使用,系统异常时会显示详细的错误信息,同时还会记录操作日志及数据库 SQL 语句信息。'] = 'Developers may use it during functional debugging. When there are system exceptions, detailed error messages will be displayed, and operation logs and database SQL statement information will also be recorded.';
$extra['项目正式部署上线后使用,系统异常时统一显示 “%s”只记录重要的异常日志信息强烈推荐上线后使用此模式。'] = 'After the project is officially deployed and launched, it will be used. When there are system exceptions, " %s " will be displayed uniformly, and only important exception log information will be recorded. It is strongly recommended to use this mode after launch.';
$extra['旧版本编辑器,对浏览器兼容较好,但内容编辑体验稍有不足。'] = 'The old version of the editor is compatible with browsers, but the content editing experience is slightly insufficient.';
$extra['新版本编辑器,只支持新特性浏览器,对内容编辑体验较好,推荐使用。'] = 'The new version of the editor only supports the new feature browser and has a good experience in content editing. It is recommended to use it.';
$extra['优先使用新版本编辑器,若浏览器不支持新版本时自动降级为旧版本编辑器。'] = 'Priority should be given to using the new version of the editor. If the browser does not support the new version, it will automatically be downgraded to the old version of the editor.';
$extra['文件上传到本地服务器的 `static/upload` 目录,不支持大文件上传,占用服务器磁盘空间,访问时消耗服务器带宽流量。'] = 'Uploading files to the `static/upload` directory of the local server does not support uploading large files, occupying server disk space, and consuming server bandwidth traffic during access.';
$extra['文件上传到 Alist 存储的服务器或云存储空间,根据服务配置可支持大文件上传,不占用本身服务器空间及服务器带宽流量。'] = 'Files can be uploaded to the Alist storage server or Cloud storage space. According to the service configuration, large file upload can be supported without occupying the server space and server bandwidth traffic.';
$extra['文件上传到七牛云存储空间,支持大文件上传,不占用服务器空间及服务器带宽流量,支持 CDN 加速访问,访问量大时推荐使用。'] = 'Files can be uploaded to Qiniu Cloud storage space. It supports large file upload, does not occupy server space and server bandwidth traffic, and supports CDN accelerated access. It is recommended when there is a large amount of access.';
$extra['文件上传到又拍云 USS 存储空间,支持大文件上传,不占用服务器空间及服务器带宽流量,支持 CDN 加速访问,访问量大时推荐使用。'] = "Uploading files to Upyun Cloud's USS storage space supports large file uploads without occupying server space or bandwidth traffic. It supports CDN accelerated access and is recommended for high traffic.";
$extra['文件上传到阿里云 OSS 存储空间,支持大文件上传,不占用服务器空间及服务器带宽流量,支持 CDN 加速访问,访问量大时推荐使用。'] = "Uploading files to Aliyun Cloud's OSS storage space supports large file uploads without occupying server space or bandwidth traffic. It supports CDN accelerated access and is recommended for high traffic.";
$extra['文件上传到腾讯云 COS 存储空间,支持大文件上传,不占用服务器空间及服务器带宽流量,支持 CDN 加速访问,访问量大时推荐使用。'] = "Uploading files to Tencent Cloud's COS storage space supports large file uploads without occupying server space or bandwidth traffic. It supports CDN accelerated access and is recommended for high traffic.";
$extra['网站名称及网站图标,将显示在浏览器的标签上。'] = "The website name and icon will be displayed on the browser's label.";
$extra['管理程序名称,将显示在后台左上角标题。'] = 'The name of the management program will be displayed in the header in the upper left corner of the background.';
$extra['管理程序版本,将显示在后台左上角标题。'] = 'The management program version will be displayed in the top left corner of the background with a title.';
$extra['网站版权信息,在后台登录页面显示版本信息并链接到备案到信息备案管理系统。'] = 'Website copyright information is displayed on the backend login page and linked to the information filing management system.';
$extra['网站备案号,可以在 %s 查询获取,将显示在登录页面下面。'] = 'The website registration number can be found at %s and will be displayed below the login page.';
$extra['公安备案号,可以在 %s 查询获取,将在登录页面下面显示。'] = 'The public security registration number can be obtained by searching at %s and will be displayed below the login page.';
$extra['点击可复制【服务启动指令】'] = "Click to copy the 'Service Start Command'";
$extra['待处理 %s 个任务,处理中 %s 个任务,已完成 %s 个任务,已失败 %s 个任务。'] = 'There are %s tasks to be processed, %s tasks in progress, %s tasks completed, and %s tasks failed.';
$extra['确定要切换到生产模式运行吗?'] = 'Are you sure you want to switch to Production mode?';
$extra['确定要切换到开发模式运行吗?'] = 'Are you sure you want to switch to Development mode?';
$extra["超级管理员账号的密码未修改,建议立即<a data-modal='%s'>修改密码</a>"] = "The super administrator password has not been changed. Suggest <a data-modal='%s'>changing password</a>.";
$extra['等待处理'] = 'Pending';
$extra['正在处理'] = 'Processing';
$extra['处理完成'] = 'Completed';
$extra['处理失败'] = 'Failed';
$extra['条件搜索'] = 'Search';
$extra['批量删除'] = 'Batch Delete';
$extra['上传进度 %s'] = 'Upload progress %s';
$extra['文件上传出错!'] = 'File upload error.';
$extra['文件上传失败!'] = 'File upload failed.';
$extra['大小超出限制!'] = 'Size exceeds limit.';
$extra['文件秒传成功!'] = 'Successfully transmitted the file in seconds.';
$extra['上传接口异常!'] = 'Abnormal upload interface.';
$extra['文件上传成功!'] = 'File uploaded successfully.';
$extra['图片压缩失败!'] = 'Image compression failed.';
$extra['无效的文件上传对象!'] = 'Invalid file upload object.';
return array_merge($extra, [
// 系统操作
'基本资料' => 'Basic information',
'安全设置' => 'Security setting',
'缓存加速' => 'Cache acceleration',
'清理缓存' => 'Clean cache',
'配色方案' => 'Color scheme',
'立即登录' => 'Login',
'退出登录' => 'Logout',
'系统提示:' => 'System Notify: ',
'清空日志缓存成功!' => 'Successfully cleared the log cache.',
'获取任务进度成功!' => 'Successfully obtained task progress.',
'网站缓存加速成功!' => 'Website cache acceleration successful.',
'请使用超管账号操作!' => 'Please use a super managed account to operate.',
'停止任务监听服务成功!' => 'Successfully stopped task listening service.',
'任务监听服务启动成功!' => 'Task monitoring service started successfully.',
'任务监听服务已经启动!' => 'The task monitoring service has started.',
'没有找到需要停止的服务!' => 'No services found that need to be stopped.',
'已切换后台编辑器!' => 'Switched to background editor.',
// 其他搜索器提示
'请选择登录时间' => 'Please select the Login time',
'请选择创建时间' => 'Please select the creation time',
'请输入账号或名称' => 'Please enter an account or name',
'请输入权限名称' => 'Please enter the permission name',
'请输入数据编码' => 'Please enter the data code',
'请输入数据名称' => 'Please enter the data name',
'请输入文件名称' => 'Please enter the file name',
'请输入文件哈希' => 'Please enter the file hash',
'请输入操作节点' => 'Please enter the operate node',
'请输入操作内容' => 'Please enter the operate content',
'请输入访问地址' => 'Please enter the access Geoip',
// 系统配置
'运行模式' => 'Running Mode',
'生产模式' => 'Production mode',
'开发模式' => 'Development mode',
'以开发模式运行' => 'Running in Development mode',
'以生产模式运行' => 'Running in Production mode',
'清理无效配置' => 'Clean up Invalid Configurations',
'修改系统参数' => 'Modify System Parameters',
'清理系统配置成功!' => 'Successfully cleaned.',
'自适应模式' => 'Adaptive Mode',
'富编辑器' => 'RichText Editor',
'存储引擎' => 'Storage Engine',
'系统参数' => 'System Parameter',
'网站名称' => 'Site Name',
'管理程序名称' => 'Program Name',
'管理程序版本' => 'Program Version',
'公安备案号' => 'Public security registration number',
'网站备案号' => 'Website registration number',
'网站版权信息' => 'Website copyright information',
'系统信息' => 'System Information',
'应用插件' => 'Plugin Information',
'核心框架' => 'Core Framework',
'平台框架' => 'Platform Framework',
'操作系统' => 'Operating System',
'运行环境' => 'Runtime Environment',
'仅开发模式可见' => 'Visible only in Development mode',
'仅生产模式可见' => 'Visible only in Production mode',
'插件名称' => 'Plugin Name',
'应用名称' => 'App Name',
'插件包名' => 'Package Name',
'插件版本' => 'Plugin Version',
'授权协议' => 'License',
'文件默认存储方式' => 'Default storage method for file upload',
'当前系统配置参数' => 'Current system configuration parameters',
'仅超级管理员可配置' => 'Only super administrators can configure',
// 系统任务管理
'优化数据库' => 'Optimize Database',
'开启服务' => 'Start Service',
'关闭服务' => 'Shutdown Service',
'定时清理' => 'Regular cleaning',
'服务状态' => 'Service',
'任务统计' => 'Total',
'编号名称' => 'Name',
'任务指令' => 'Command',
'任务状态' => 'Status',
'计划时间' => 'scheduled time',
'任务名称' => 'Name',
'检查中' => 'Checking',
'任务计划' => 'Scheduled',
'重 置' => 'Reset',
'日 志' => 'Logs',
'异 常' => 'Abnormal',
'无权限' => 'Denied',
'已启动' => 'Started',
'未启动' => 'Stopped',
// 数据字典管理
'数据编码' => 'Code',
'数据名称' => 'Name',
'操作账号' => 'User',
'操作节点' => "Node",
'操作行为' => 'Action',
'操作内容' => "Content",
'访问地址' => 'Geo IP',
'网络服务商' => 'ISP.',
'日志清理成功!' => 'Logger Clear Complate.',
'成功清理所有日志' => 'Successfully cleared all logs.',
// 系统文件管理
'文件名称' => 'Name',
'文件哈希' => "HASH",
'文件大小' => "Size",
'文件后缀' => 'Exts',
'存储方式' => 'Storage Type',
'清理重复' => 'Clear Replace',
'上传方式' => 'Upload Type',
'查看文件' => 'View',
'文件链接' => 'Link',
'秒传' => 'Speedy',
'普通' => 'Normal',
// 系统菜单管理
'图 标' => "Icon",
'添加菜单' => 'Add',
'禁用菜单' => 'Forbid',
'激活菜单' => "Resume",
'系统菜单' => 'Menus',
'菜单名称' => 'Name',
'跳转链接' => 'Link',
'上级菜单' => 'Parent',
'菜单链接' => 'Link',
'链接参数' => 'Params',
'权限节点' => "Node",
'菜单图标' => 'Icon',
'选择图标' => 'Select Icon',
// 系统权限管理
"授 权" => 'Auth',
'添加权限' => 'Add',
'权限名称' => "Name",
'权限描述' => 'Description',
'请输入权限描述' => 'Please enter a permission description',
// 系统用户管理
'账号名称' => 'Username',
'添加用户' => 'Add User',
'最后登录' => "Last Login Time",
'头像' => "Head",
'登录账号' => 'Username',
'用户名称' => 'Nickname',
'登录次数' => 'Login Times',
'系统用户' => 'System User',
'密 码' => 'Password',
'系统用户管理' => 'Users',
]);

52
app/admin/route/demo.php Normal file
View File

@@ -0,0 +1,52 @@
<?php
// +----------------------------------------------------------------------
// | Admin Plugin for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免责声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 代码仓库https://gitee.com/zoujingli/think-plugs-admin
// | github 代码仓库https://github.com/zoujingli/think-plugs-admin
// +----------------------------------------------------------------------
use think\admin\Library;
use think\admin\service\RuntimeService;
/*! 演示环境禁止操作路由绑定 */
if (RuntimeService::check('demo')) {
Library::$sapp->route->post('index/pass', static function () {
return json(['code' => 0, 'info' => lang('演示环境禁止修改用户密码!')]);
});
Library::$sapp->route->post('config/system', static function () {
return json(['code' => 0, 'info' => lang('演示环境禁止修改系统配置!')]);
});
Library::$sapp->route->post('config/storage', static function () {
return json(['code' => 0, 'info' => lang('演示环境禁止修改系统配置!')]);
});
Library::$sapp->route->post('menu', static function () {
return json(['code' => 0, 'info' => lang('演示环境禁止给菜单排序!')]);
});
Library::$sapp->route->post('menu/index', static function () {
return json(['code' => 0, 'info' => lang('演示环境禁止给菜单排序!')]);
});
Library::$sapp->route->post('menu/add', static function () {
return json(['code' => 0, 'info' => lang('演示环境禁止添加菜单!')]);
});
Library::$sapp->route->post('menu/edit', static function () {
return json(['code' => 0, 'info' => lang('演示环境禁止编辑菜单!')]);
});
Library::$sapp->route->post('menu/state', static function () {
return json(['code' => 0, 'info' => lang('演示环境禁止禁用菜单!')]);
});
Library::$sapp->route->post('menu/remove', static function () {
return json(['code' => 0, 'info' => lang('演示环境禁止删除菜单!')]);
});
Library::$sapp->route->post('user/pass', static function () {
return json(['code' => 0, 'info' => lang('演示环境禁止修改密码!')]);
});
}

View File

@@ -0,0 +1,125 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>{block name="title"}{$title|default=''}{if !empty($title)} · {/if}{:sysconf('site_name')}{/block}</title>
<meta name="renderer" content="webkit">
<meta name="format-detection" content="telephone=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1">
<link rel="stylesheet" href="__ROOT__/static/theme/css/iconfont.css?at={:date('md')}">
<link rel="stylesheet" href="__ROOT__/static/plugs/layui/css/layui.css?v={:date('ymd')}">
<style>
::-webkit-input-placeholder {
color: #aaa
}
::-webkit-scrollbar {
width: 3px;
height: 3px
}
::-webkit-scrollbar-track {
background: #ccc
}
::-webkit-scrollbar-thumb {
background-color: #666
}
::selection {
background-color: #ec494e;
color: #fff
}
::-moz-selection {
background-color: #ec494e;
color: #fff
}
:-webkit-autofill {
-webkit-box-shadow: 0 0 0 1000px white inset;
-webkit-text-fill-color: #333
}
ul li {
width: 20%;
height: 65px;
display: block;
float: left;
margin-right: -1px;
margin-left: -2px;
margin-bottom: -2px;
cursor: pointer;
font-size: 14px;
overflow: hidden;
text-align: center;
padding: 15px 0 10px 0;
border: 1px solid #e2e2e2;
background-color: #efefef;
user-select: none;
-ms-user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
transition: all .2s linear;
-o-transition: all .2s linear;
-moz-transition: all .2s linear;
-webkit-transition: all .2s linear
}
ul li:hover {
color: #fff;
background-color: #563d7c
}
ul li:hover i, ul li:hover div {
color: #fff;
}
ul li i {
color: #333;
display: inline-block;
font-size: 30px !important
}
ul li div {
color: #333;
height: 35px;
font-size: 13px;
line-height: 35px;
white-space: nowrap
}
</style>
</head>
<body>
<ul>
{foreach $layuiIcons??[] as $icon}
<li>
<i class="layui-icon {$icon}"></i>
<div class="icon-title">{$icon}</div>
</li>
{/foreach}
{foreach $thinkIcons??[] as $icon}
<li>
<i class="iconfont {$icon}"></i>
<div class="icon-title">{$icon}</div>
</li>
{/foreach}
</ul>
<script src="__ROOT__/static/plugs/jquery/jquery.min.js" type="text/javascript"></script>
<script>
$(function () {
$('li').on('click', function (className) {
if ((className = $(this).find('i').get(0).className)) {
top.$('[name="{$field}"]').val(className).trigger('change');
top.layer.close(top.layer.getFrameIndex(window.name));
}
});
});
</script>
</body>
</html>

View File

@@ -0,0 +1,327 @@
define(['md5', 'notify'], function (SparkMD5, Notify, allowMime) {
allowMime = JSON.parse('{$exts|raw}');
function UploadAdapter(elem, done) {
return new (function (elem, done) {
let that = this;
/*! 初始化变量 */
this.option = {elem: $(elem), exts: [], mimes: []};
this.option.size = this.option.elem.data('size') || 0;
this.option.safe = this.option.elem.data('safe') ? 1 : 0;
this.option.hide = this.option.elem.data('hload') ? 1 : 0;
this.option.mult = this.option.elem.data('multiple') > 0;
this.option.path = (this.option.elem.data('path') || '').replace(/\W/g, '');
this.option.type = this.option.safe ? 'local' : this.option.elem.attr('data-uptype') || '';
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.input = $(this.option.elem.data('input'))
} else if (this.option.elem.data('field')) {
this.option.input = $('input[name="' + this.option.elem.data('field') + '"]:not([type=file])');
this.option.elem.data('input', this.option.input.size() > 0 ? this.option.input.get(0) : null);
}
/*! 文件选择筛选,使用 MIME 规则过滤文件列表 */
$((this.option.elem.data('type') || '').split(',')).map(function (i, e) {
if (allowMime[e]) that.option.exts.push(e), that.option.mimes.push(allowMime[e]);
});
/*! 初始化上传组件 */
this.adapter = new Adapter(this.option, layui.upload.render({
url: '{:url("admin/api.upload/file",[],false,true)}', 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.path = that.option.path;
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', obj.items);
that.adapter.upload(obj.items, done);
layui.each(obj.files, function (idx) {
delete obj.files[idx];
});
}
}));
})(elem, done)
}
// 创建对象
UploadAdapter.adapter = window.AdminUploadAdapter = Adapter;
// 上传文件
function Adapter(option, uploader) {
this.uploader = uploader, this.config = function (option) {
return (this.option = Object.assign({}, this.option || {}, option || {})), this;
}, this.init = function (option) {
this.uploader && this.uploader.config.elem.next().val('');
this.files = {}, this.loader = 0, this.count = {total: 0, error: 0, success: 0};
return this.config(option).config({safe: this.option.safe || 0, type: this.option.type || ''});
}, this.init(option);
}
// 文件推送
Adapter.prototype.upload = function (files, done) {
let that = this.init();
layui.each(files, function (index, file) {
that.count.total++, file.index = index, that.files[index] = file;
if (!that.option.hide && !file.notify) {
file.notify = new NotifyExtend(file);
}
if (that.option.size && file.size > that.option.size) {
that.event('upload.error', {file: file}, file, '{:lang("大小超出限制!")}');
}
});
layui.each(files, function (index, file) {
// 禁传异常状态文件
if (typeof file.xstate === 'number' && file.xstate === -1) return;
// 图片限宽限高压缩
let isGif = /^image\/gif/.test(file.type);
if (!isGif && /^image\//.test(file.type) && (file.maxWidth + file.maxHeight + file.cutWidth + file.cutHeight > 0 || file.quality !== 1)) {
require(['compressor'], function (Compressor) {
let options = {quality: file.quality, resize: 'cover'};
if (file.cutWidth) options.width = file.cutWidth;
if (file.cutHeight) options.height = file.cutHeight;
if (file.maxWidth) options.maxWidth = file.maxWidth;
if (file.maxHeight) options.maxHeight = file.maxHeight;
new Compressor(file, Object.assign(options, {
success(blob) {
blob.index = file.index, blob.notify = file.notify, blob.path = file.path, files[index] = blob;
that.hash(files[index]).then(function (file) {
that.event('upload.hash', file).request(file, done);
});
}, error: function () {
that.event('upload.error', {file: file}, file, '{:lang("图片压缩失败!")}');
}
}));
});
} else {
that.hash(file).then(function (file) {
that.event('upload.hash', file).request(file, done);
});
}
});
};
// 文件上传
Adapter.prototype.request = function (file, done) {
let that = this, data = {key: file.xkey, safe: that.option.safe, uptype: that.option.type};
data.size = file.size, data.name = file.name, data.hash = file.xmd5, data.mime = file.type, data.xext = file.xext;
jQuery.ajax("{:url('admin/api.upload/state',[],false,true)}", {
data: data, method: 'post', success: function (ret) {
file.id = ret.data.id || 0, file.xurl = ret.data.url;
file.xsafe = ret.data.safe, file.xpath = ret.data.key, file.xtype = ret.data.uptype;
if (parseInt(ret.code) === 404) {
let uploader = {};
uploader.uptype = ret.data.uptype;
uploader.url = ret.data.server;
uploader.head = {};
uploader.form = new FormData();
uploader.form.append('key', ret.data.key);
uploader.form.append('safe', ret.data.safe);
uploader.form.append('uptype', ret.data.uptype);
if (ret.data.uptype === 'qiniu') {
uploader.form.append('token', ret.data.token);
} else if (ret.data.uptype === 'alist') {
uploader.type = 'put';
uploader.head['file-path'] = ret.data['filepath'];
uploader.head['authorization'] = ret.data['authorization'];
uploader.form = new FormData();
} else if (ret.data.uptype === 'alioss') {
uploader.form.append('policy', ret.data['policy']);
uploader.form.append('signature', ret.data['signature']);
uploader.form.append('OSSAccessKeyId', ret.data['OSSAccessKeyId']);
uploader.form.append('success_action_status', '200');
uploader.form.append('Content-Disposition', 'inline;filename=' + encodeURIComponent(file.name));
} else if (ret.data.uptype === 'txcos') {
uploader.form.append('q-ak', ret.data['q-ak']);
uploader.form.append('policy', ret.data['policy']);
uploader.form.append('q-key-time', ret.data['q-key-time']);
uploader.form.append('q-signature', ret.data['q-signature']);
uploader.form.append('q-sign-algorithm', ret.data['q-sign-algorithm']);
uploader.form.append('success_action_status', '200');
uploader.form.append('Content-Disposition', 'inline;filename=' + encodeURIComponent(file.name));
} else if (ret.data.uptype === 'upyun') {
uploader.form.delete('key');
uploader.form.delete('safe');
uploader.form.delete('uptype');
uploader.form.append('save-key', ret.data['key']);
uploader.form.append('policy', ret.data['policy']);
uploader.form.append('authorization', ret.data['authorization']);
uploader.form.append('Content-Disposition', 'inline;filename=' + encodeURIComponent(file.name));
}
uploader.form.append('file', file, file.name), jQuery.ajax({
xhrFields: {withCredentials: ret.data.uptype === 'local'}, headers: uploader.head, url: uploader.url, data: uploader.form, type: uploader.type || 'post', xhr: function (xhr) {
xhr = new XMLHttpRequest();
return xhr.upload.addEventListener('progress', function (event) {
file.xtotal = event.total, file.xloaded = event.loaded || 0;
that.progress((file.xloaded / file.xtotal * 100).toFixed(2), file)
}), xhr;
}, contentType: false, error: function () {
that.event('upload.error', {file: file}, file, '{:lang("上传接口异常!")}');
}, processData: false, success: function (ret) {
// 兼容数据格式
if (typeof ret === 'string' && ret.length > 0) try {
ret = JSON.parse(ret) || ret;
} catch (e) {
console.log(e)
}
if (typeof ret !== 'object') {
ret = {code: 1, url: file.xurl, info: '{:lang("文件上传成功!")}'};
}
/*! 检查单个文件上传返回的结果 */
if (typeof ret === 'object' && ret.code < 1) {
that.event('upload.error', {file: file}, file, ret.info || '{:lang("文件上传失败!")}');
} else if (uploader.uptype === 'alist' && parseInt(ret.code) !== 200) {
that.event('upload.error', {file: file}, file, ret.message || '{:lang("文件上传失败!")}');
} else {
that.done(ret, file.index, file, done, '{:lang("文件上传成功!")}');
}
}
});
} else if (parseInt(ret.code) === 200) {
(file.xurl = ret.data.url), that.progress('100.00', file);
that.done({code: 1, url: file.xurl, info: file.xstats, data: {code: 200, url: file.xurl}}, file.index, file, done, '{:lang("文件秒传成功!")}');
} else {
that.event('upload.error', {file: file}, file, ret.info || ret.error.message || '{:lang("文件上传出错!")}');
}
}
});
};
// 上传进度
Adapter.prototype.progress = function (number, file) {
this.event('upload.progress', {number: number, file: file});
if (file.notify) file.notify.setProgress(number);
};
// 上传结果
Adapter.prototype.done = function (ret, idx, file, done, message) {
/*! 检查单个文件上传返回的结果 */
if (ret.code < 1) return $.msg.tips(ret.info || '{:lang("文件上传失败!")}');
if (typeof file.xurl !== 'string') return $.msg.tips('{:lang("无效的文件上传对象!")}');
jQuery.post("{:url('admin/api.upload/done',[],false,true)}", {id: file.id, hash: file.xmd5});
/*! 单个文件上传成功结果处理 */
if (typeof done === 'function') {
done.call(this.option.elem, file.xurl, this.files['id']);
} else if (this.option.mult < 1 && this.option.elem.data('input')) {
$(this.option.elem.data('input')).val(file.xurl).trigger('change', file);
}
// 文件上传成功事件
this.event('push', file.xurl).event('upload.done', {file: file, data: ret}, file, message);
/*! 所有文件上传完成后结果处理 */
if (this.count.success + this.count.error >= this.count.total) {
this.option.hide || $.msg.close(this.loader);
if (this.option.mult > 0 && this.option.elem.data('input')) {
let urls = this.option.elem.data('input').value || [];
if (typeof urls === 'string') urls = urls.split('|');
for (let i in this.files) urls.push(this.files[i].xurl);
$(this.option.elem.data('input')).val(urls.join('|')).trigger('change', this.files);
}
this.event('upload.complete', {file: this.files}, file).init().uploader && this.uploader.reload();
}
};
/*! 触发事件过程 */
Adapter.prototype.event = function (name, data, file, message) {
if (name === 'upload.error') {
this.count.error++, file.xstate = -1, file.xstats = message;
if (file.notify) file.notify.setError(message || file.xstats || '');
} else if (name === 'upload.done') {
this.count.success++, file.xstate = 1, file.xstats = message;
if (file.notify) file.notify.setSuccess(message || file.xstats || '')
}
if (this.option.elem) {
this.option.elem.triggerHandler(name, data);
if (this.option.input) this.option.input.triggerHandler(name, data);
}
return this;
};
/**
* 计算文件 HASH 值
* @param {File} file 文件对象
* @return {Promise}
*/
Adapter.prototype.hash = function (file) {
let defer = jQuery.Deferred();
file.xext = file.name.indexOf('.') > -1 ? file.name.split('.').pop() : 'tmp';
/*! 兼容不能计算文件 HASH 的情况 */
let IsDate = '{$nameType|default=""}'.indexOf('date') > -1;
if (!window.FileReader || IsDate) return jQuery.when((function (xmd5, chars) {
while (xmd5.length < 32) xmd5 += chars.charAt(Math.floor(Math.random() * chars.length));
return SetFileXdata(file, xmd5, 6), defer.promise();
})(layui.util.toDateString(Date.now(), 'yyyyMMddHHmmss-'), '0123456789'));
/*! 读取文件并计算 HASH 值 */
return new LoadNextChunk(file).ReadAsChunk();
function SetFileXdata(file, xmd5, slice) {
file.xmd5 = xmd5, file.xstate = 0, file.xstats = '';
file.xkey = file.xmd5.substring(0, slice || 2) + '/' + file.xmd5.substring(slice || 2) + '.' + file.xext;
if (file.path) file.xkey = file.path + '/' + file.xkey;
return defer.resolve(file, file.xmd5, file.xkey), file;
}
function LoadNextChunk(file) {
let that = this, reader = new FileReader(), spark = new SparkMD5.ArrayBuffer();
let slice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
this.chunkIdx = 0, this.chunkSize = 2097152, this.chunkTotal = Math.ceil(file.size / this.chunkSize);
reader.onload = function (event) {
spark.append(event.target.result);
++that.chunkIdx < that.chunkTotal ? that.ReadAsChunk() : SetFileXdata(file, spark.end());
}, reader.onerror = function () {
defer.reject();
}, this.ReadAsChunk = function () {
this.start = that.chunkIdx * that.chunkSize;
this.loaded = this.start + that.chunkSize >= file.size ? file.size : this.start + that.chunkSize;
reader.readAsArrayBuffer(slice.call(file, this.start, this.loaded));
defer.notify(file, (this.loaded / file.size * 100).toFixed(2));
return defer.promise();
};
}
};
return UploadAdapter;
/**
* 上传状态提示扩展插件
* @param {File} file 文件对象
* @constructor
*/
function NotifyExtend(file) {
let that = this, message = "{:lang('上传进度 %s', ['<span data-upload-progress>0%</span>'])}";
this.notify = Notify.notify({width: 260, title: file.name, showProgress: true, description: message, type: 'default', position: 'top-right', closeTimeout: 0});
this.$elem = $(this.notify.notification.nodes);
this.$elem.find('.growl-notification__progress').addClass('is-visible');
this.$elem.find('.growl-notification__progress-bar').addClass('transition');
this.setProgress = function (number) {
this.$elem.find('[data-upload-progress]').html(number + '%');
this.$elem.find('.growl-notification__progress-bar').css({width: number + '%'});
return this;
}, this.setError = function (message) {
this.$elem.find('.growl-notification__desc').html(message || '{:lang("文件上传失败!")}');
this.$elem.removeClass('growl-notification--default').addClass('growl-notification--error')
return this.close();
}, this.setSuccess = function (message) {
this.setProgress('100.00');
this.$elem.find('.growl-notification__desc').html(message || '{:lang("文件上传成功!")}');
this.$elem.removeClass('growl-notification--default').addClass('growl-notification--success');
return this.close();
}, this.close = function (timeout) {
return setTimeout(function () {
that.notify.close();
}, timeout || 2000), this;
};
}
});

View File

@@ -0,0 +1,161 @@
<div class="image-dialog" id="ImageDialog">
<div class="image-dialog-head">
<label class="pull-left flex">
<input class="layui-input margin-right-5" v-model="keys" style="height:30px;line-height:30px" placeholder="{:lang('请输入搜索关键词')}">
<a class="layui-btn layui-btn-sm layui-btn-normal" @click="search">{:lang('搜 索')}</a>
</label>
<div class="pull-right">
<a class="layui-btn layui-btn-sm layui-btn-normal" @click="uploadImage">{:lang('上传图片')}</a>
</div>
</div>
<div class="image-dialog-body">
<div class="image-dialog-item" v-for="x in list" @click="setItem(x)" style="display:none" v-show="show" :class="{'image-dialog-checked':x.checked}">
<div class="uploadimage" :style="x.style"></div>
<p class="image-dialog-item-name layui-elip" v-text="x.name"></p>
<div class="image-dialog-item-tool">
<span class="image-dialog-item-type">{{x.xext.toUpperCase()}}</span>
<span class="image-dialog-item-size">{{formatSize(x.size)}}</span>
{if auth('admin/file/remove')}
<span class="layui-icon layui-icon-close image-dialog-item-close" @click.stop="remove(x)"></span>
{/if}
</div>
</div>
</div>
<div class="image-dialog-foot">
<div id="ImageDialogPage" class="image-dialog-page"></div>
<div id="ImageDialogButton layui-hide" class="image-dialog-button layui-btn layui-btn-normal" v-if="data.length>0" @click="confirm">
{php} $tag = '{{data.length}}'; {/php}
{:lang('已选 %s 张,确认', [$tag])}
</div>
</div>
</div>
<script>
require(['vue'], function (vue) {
var app = new vue({
el: '#ImageDialog',
data: {
didx: 0,
page: 1, limit: 15, show: false, mult: false,
keys: '', list: [], data: [], idxs: {}, urls: [],
},
created: function () {
this.didx = $.msg.mdx.pop();
this.$btn = $('#{$get.id|default=""}');
this.$ups = $('#ImageDialogUploadLayout [data-file]');
this.mult = "{$get.file|default='image'}" === 'images';
this.loadPage(), setTimeout(function () {
$('#ImageDialogButton').removeClass('layui-hide');
}, 1000);
},
methods: {
// 搜索刷新数据
search: function () {
this.page = 1;
this.loadPage();
},
// 确认选择数据
confirm: function () {
this.urls = [];
this.data.forEach(function (file) {
app.setValue(file.xurl);
});
this.setInput();
},
// 删除指定的图片
remove: function (x) {
$.msg.confirm('确认要移除这张图片吗?', function () {
$.form.load('{:url("admin/file/remove")}', {id: x.id}, 'POST', function (ret) {
ret.code > 0 ? app.loadPage() : $.msg.error(ret.info);
return app.$forceUpdate(), false;
})
})
},
// 格式文件大小
formatSize: function (size) {
return $.formatFileSize(size);
},
// 设置单项数据
setItem: function (item) {
if (!this.mult) {
this.setValue(item.xurl).setInput();
} else if ((item.checked = !this.idxs[item.id])) {
(this.idxs[item.id] = item) && this.data.push(item);
} else {
delete this.idxs[item.id];
this.data.forEach(function (temp, idx) {
temp.id === item.id && app.data.splice(idx, 1);
});
}
},
// 更新列表数据
setList: function (items, count) {
this.list = items;
this.list.forEach(function (item) {
item.checked = !!app.idxs[item.id]
item.style = 'background-image:url(' + item.xurl + ')';
});
this.addPage(count);
},
// 设置选择数据
setValue: function (xurl) {
$.msg.close(this.didx);
this.urls.push(xurl) && this.$btn.triggerHandler('push', xurl);
return this;
},
// 设置输入表单
setInput: function () {
if (this.$btn.data('input')) {
$(this.$btn.data('input')).val(this.urls.join('|')).trigger('change');
}
},
// 创建分页工具条
addPage: function (count) {
this.show = true;
layui.laypage.render({
curr: this.page, count: count, limit: app.limit,
layout: ['count', 'prev', 'page', 'next', 'refresh'],
elem: 'ImageDialogPage', jump: function (obj, first) {
if (!first) app.loadPage(app.page = obj.curr);
},
});
},
// 加载页面数据
loadPage: function () {
this.params = {page: this.page, limit: this.limit, output: 'layui.table', name: this.keys || ''};
this.params.type = '{$get.type|default="gif,png,jpg,jpeg"}';
$.form.load('{:url("image",[],false,true)}', this.params, 'get', function (ret) {
return app.setList(ret.data, ret.count), false;
});
},
// 上传图片文件
uploadImage: function () {
this.urls = [];
this.$ups.off('push').on('push', function (e, xurl) {
app.setValue(xurl);
}).off('upload.complete').on('upload.complete', function () {
app.setInput();
}).click();
},
}
});
});
</script>
<label class="layui-hide" id="ImageDialogUploadLayout">
<!-- 图片上传组件 开始 -->
{if isset($get.file) && $get.file eq 'image'}
<button data-file="one" data-type="{$get.type|default='gif,png,jpg,jpeg'}"
data-path="{$get.path|default=''}" data-size="{$get.size|default=0}"
data-cut-width="{$get.cutWidth|default=0}" data-cut-height="{$get.cutHeight|default=0}"
data-max-width="{$get.maxWidth|default=0}" data-max-height="{$get.maxHeight|default=0}"
></button>
{else}
<button data-file="mul" data-type="{$get.type|default='gif,png,jpg,jpeg'}"
data-path="{$get.path|default=''}" data-size="{$get.size|default=0}"
data-cut-width="{$get.cutWidth|default=0}" data-cut-height="{$get.cutHeight|default=0}"
data-max-width="{$get.maxWidth|default=0}" data-max-height="{$get.maxHeight|default=0}"
></button>
{/if}
<!-- 图片上传组件 结束 -->
</label>

View File

@@ -0,0 +1,143 @@
{extend name='main'}
{block name="button"}
<button data-target-submit class='layui-btn layui-btn-sm'>{:lang('保存数据')}</button>
<button data-target-backup class="layui-btn layui-btn-sm layui-btn-danger">{:lang('取消编辑')}</button>
{/block}
{block name="content"}
<div class="think-box-shadow">
<form method="post" id="RoleForm" class="layui-form layui-card">
<div class="layui-card-body">
<label class="layui-form-item relative block">
<span class="help-label"><b>{:lang('权限名称')}</b>Auth Name</span>
<input maxlength="100" class="layui-input" name="title" value='{$vo.title|default=""}' required vali-name="{:lang('权限名称')}" placeholder="{:lang('请输入权限名称')}">
<span class="help-block">{:lang('访问权限名称需要保持不重复,在给用户授权时需要根据名称选择!')}</span>
</label>
<label class="layui-form-item relative block">
<span class="help-label"><b>{:lang('权限描述')}</b>Auth Remark</span>
<textarea placeholder="{:lang('请输入权限描述')}" maxlength="200" class="layui-textarea" name="desc">{$vo.desc|default=""}</textarea>
</label>
<div class="layui-form-item">
<span class="help-label label-required-prev"><b>{:lang('功能节点')}</b>Auth Nodes</span>
<ul id="zTree" class="ztree notselect"></ul>
</div>
<div class="hr-line-dashed"></div>
{notempty name='vo.id'}<input name="id" value="{$vo.id}" type="hidden"/>{/notempty}
<div class="layui-form-item text-center">
<button data-target-submit class="layui-btn">{:lang('保存数据')}</button>
<button data-target-backup class="layui-btn layui-btn-danger" type="button">{:lang('取消编辑')}</button>
</div>
</div>
</form>
</div>
{/block}
{block name="script"}
<script>
require(['jquery.ztree'], function () {
new function () {
let that = this;
this.data = {}, this.ztree = null, this.setting = {
view: {showLine: false, showIcon: false, dblClickExpand: false},
check: {enable: true, nocheck: false, chkboxType: {"Y": "ps", "N": "ps"}}, callback: {
beforeClick: function (id, node) {
node.children.length < 1 ? that.ztree.checkNode(node, !node.checked, true, true) : that.ztree.expandNode(node);
return false;
}
}
};
this.renderChildren = function (list, level) {
let childrens = [];
for (let i in list) childrens.push({
open: true, node: list[i].node, name: list[i].title || list[i].node,
checked: list[i].checked || false, children: this.renderChildren(list[i]._sub_, level + 1)
});
return childrens;
};
this.syncData = function () {
$.form.load('{:sysuri()}', {id: '{$vo.id|default=0}', action: 'json'}, 'post', function (ret) {
return (that.data = that.renderChildren(ret.data, 1)), that.showTree(), false;
});
};
this.showTree = function () {
this.ztree = $.fn.zTree.init($("#zTree"), this.setting, this.data);
while (true) {
let nodes = this.ztree.getNodesByFilter(function (node) {
return (!node.node && node.children.length < 1);
});
if (nodes.length < 1) break;
for (let i in nodes) this.ztree.removeNode(nodes[i]);
}
};
// 刷新数据
this.syncData();
// 监听表单提交
$('#RoleForm').vali(function (form) {
let data = that.ztree.getCheckedNodes(true);
Object.assign(form, {nodes: [], action: 'save'})
for (let i in data) if (data[i].node) form.nodes.push(data[i].node);
$.form.load('{:sysuri()}', form, 'post');
});
};
});
</script>
{/block}
{block name="style"}
<style>
ul.ztree li {
line-height: 24px;
white-space: normal !important;
}
ul.ztree li span.button.switch {
margin-right: 5px;
}
ul.ztree ul ul li {
display: inline-block;
white-space: normal;
}
ul.ztree > li {
border: 1px solid rgba(0, 0, 0, 0.10);
padding: 15px;
background: rgba(0, 0, 0, 0.05);
border-radius: 3px;
margin-bottom: 10px;
}
ul.ztree > li > ul {
padding: 10px;
margin-top: 10px;
background: rgba(0, 0, 0, 0.05);
border-radius: 3px;
}
ul.ztree > li > ul > li {
padding: 5px 0;
}
ul.ztree > li > a > span {
font-size: 15px;
font-weight: 700;
}
ul.ztree .level2 .button.level2 {
width: 0;
}
ul.ztree li span.button.noline_open {
background-position-y: -73px;
}
ul.ztree li span.button.noline_close {
background-position-y: -73px;
}
ul.ztree .level1 > .node_name {
font-weight: bold;
}
</style>
{/block}

View File

@@ -0,0 +1,76 @@
{extend name='table'}
{block name="button"}
<!--{if auth("add")}-->
<button data-open='{:url("add")}' data-table-id="RoleTable" class='layui-btn layui-btn-sm layui-btn-primary'>{:lang('添加权限')}</button>
<!--{/if}-->
<!--{if auth("remove")}-->
<button data-action='{:url("remove")}' data-rule="id#{id}" data-table-id="RoleTable" data-confirm="{:lang('确定要批量删除权限吗?')}" class='layui-btn layui-btn-sm layui-btn-primary'>{:lang('批量删除')}</button>
<!--{/if}-->
{/block}
{block name="content"}
<div class="think-box-shadow">
{include file='auth/index_search'}
<table id="RoleTable" data-url="{:request()->url()}" data-target-search="form.form-search"></table>
</div>
{/block}
{block name='script'}
<script>
$(function () {
// 初始化表格组件
$('#RoleTable').layTable({
even: true, height: 'full',
sort: {field: 'sort desc,id', type: 'desc'},
cols: [[
{checkbox: true, fixed: true},
{field: 'sort', title: '{:lang("排序权重")}', align: 'center', width: 100, sort: true, templet: '#SortInputRoleTableTpl'},
{field: 'title', title: '{:lang("权限名称")}', align: 'center', minWidth: 140},
{field: 'desc', title: '{:lang("权限描述")}', align: 'center', minWidth: 110, templet: '<div>{{d.desc||"-"}}</div>'},
{field: 'status', title: '{:lang("使用状态")}', align: 'center', minWidth: 110, templet: '#StatusSwitchRoleTableTpl'},
{field: 'create_at', title: '{:lang("创建时间")}', align: 'center', minWidth: 170, sort: true},
{toolbar: '#ToolbarRoleTableTpl', title: '{:lang("操作面板")}', align: 'center', minWidth: 210, fixed: 'right'},
]]
});
// 数据状态切换操作
layui.form.on('switch(StatusSwitchRoleTable)', function (obj) {
let data = {id: obj.value, status: obj.elem.checked > 0 ? 1 : 0};
$.form.load("{:url('state')}", data, 'post', function (ret) {
if (ret.code < 1) $.msg.error(ret.info, 3, function () {
$('#RoleTable').trigger('reload');
});
return false;
}, false);
});
});
</script>
<!-- 列表排序权重模板 -->
<script type="text/html" id="SortInputRoleTableTpl">
<input type="number" min="0" data-blur-number="0" data-action-blur="{:request()->url()}" data-value="id#{{d.id}};action#sort;sort#{value}" data-loading="false" value="{{d.sort}}" class="layui-input text-center">
</script>
<!-- 数据状态切换模板 -->
<script type="text/html" id="StatusSwitchRoleTableTpl">
<!--{if auth("state")}-->
<input type="checkbox" value="{{d.id}}" lay-skin="switch" lay-text="{:lang('已激活')}|{:lang('已禁用')}" lay-filter="StatusSwitchRoleTable" {{-d.status>0?'checked':''}}>
<!--{else}-->
{{-d.status ? '<b class="color-green">{:lang("已启用")}</b>' : '<b class="color-red">{:lang("已禁用")}</b>'}}
<!--{/if}-->
</script>
<!-- 数据操作工具条模板 -->
<script type="text/html" id="ToolbarRoleTableTpl">
<!--{if auth('edit')}-->
<a class="layui-btn layui-btn-primary layui-btn-sm" data-open='{:url("edit")}?id={{d.id}}'>{:lang("编 辑")}</a>
<!--{/if}-->
<!--{if auth("remove")}-->
<a class="layui-btn layui-btn-danger layui-btn-sm" data-action="{:url('remove')}" data-value="id#{{d.id}}" data-confirm="{:lang('确定要删除权限吗?')}">{:lang("删 除")}</a>
<!--{/if}-->
</script>
{/block}

View File

@@ -0,0 +1,45 @@
<fieldset>
<legend>{:lang('条件搜索')}</legend>
<form class="layui-form layui-form-pane form-search" action="{:sysuri()}" onsubmit="return false" method="get" autocomplete="off">
<div class="layui-form-item layui-inline">
<label class="layui-form-label">{:lang('权限名称')}</label>
<div class="layui-input-inline">
<input name="title" value="{$get.title|default=''}" placeholder="{:lang('请输入权限名称')}" class="layui-input">
</div>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">{:lang('权限描述')}</label>
<div class="layui-input-inline">
<input name="desc" value="{$get.desc|default=''}" placeholder="{:lang('请输入权限描述')}" class="layui-input">
</div>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">{:lang('使用状态')}</label>
<div class="layui-input-inline">
<select class="layui-select" name="status">
<option value=''>-- {:lang('全部')} --</option>
{foreach [lang('已禁用记录'),lang('已激活记录')] as $k=>$v}
{if isset($get.status) and $get.status eq $k.""}
<option selected value="{$k}">{$v}</option>
{else}
<option value="{$k}">{$v}</option>
{/if}{/foreach}
</select>
</div>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">{:lang('创建时间')}</label>
<div class="layui-input-inline">
<input data-date-range name="create_at" value="{$get.create_at|default=''}" placeholder="{:lang('请选择创建时间')}" class="layui-input">
</div>
</div>
<div class="layui-form-item layui-inline">
<button class="layui-btn layui-btn-primary"><i class="layui-icon">&#xe615;</i> {:lang('搜 索')}</button>
</div>
</form>
</fieldset>

View File

@@ -0,0 +1,68 @@
<form action="{:sysuri()}" method="post" data-auto="true" class="layui-form layui-card" data-table-id="BaseTable">
<div class="layui-card-body padding-left-40">
<div class="layui-form-item label-required-prev">
<div class="help-label"><b>数据类型</b>Data Type</div>
{if isset($vo.type)}
<label><input readonly value="{$vo.type|default=''}" class="layui-input think-bg-gray"></label>
{else}
<select class="layui-select" lay-filter="DataType">
{foreach $types as $type}{if (isset($vo.type) and $type eq $vo.type) or ($type eq input('get.type'))}
<option selected value="{$type}">{$type}</option>
{else}
<option value="{$type}">{$type}</option>
{/if}{/foreach}
</select>
<script>
(function (callable) {
layui.form.on('select(DataType)', callable);
callable({value: "{$vo.type|default=''}" || $('[lay-filter=DataType]').val()});
})(function (data) {
if (data.value === '--- 新增类型 ---') {
$('#DataTypeInput').removeClass('layui-hide').find('input').val('').focus();
} else {
$('#DataTypeInput').addClass('layui-hide').find('input').val(data.value);
}
});
</script>
{/if}
<p class="help-block">请选择数据类型,数据创建后不能再次修改哦 ~</p>
<div id="DataTypeInput" class="layui-hide relative">
<input class="layui-input" maxlength="20" name="type" required vali-name="数据类型" placeholder="请输入数据类型" value="{$vo.type|default=''}">
<p class="help-block">请输入新的数据类型,数据创建后不能再次修改哦 ~</p>
</div>
</div>
<label class="layui-form-item relative block">
<span class="help-label"><b>数据编码</b>Data Code</span>
{if isset($vo.code)}
<input readonly maxlength="100" class="layui-input think-bg-gray" name="code" value='{$vo.code|default=""}' required placeholder="请输入数据编码">
{else}
<input maxlength="100" class="layui-input" name="code" value='{$vo.code|default=""}' required vali-name="数据编码" placeholder="请输入数据编码">
{/if}
<span class="help-block">请输入新的数据编码,数据创建后不能再次修改,同种数据类型的数据编码不能出现重复 ~</span>
</label>
<label class="layui-form-item relative block">
<span class="help-label"><b>数据名称</b>Data Name</span>
<input maxlength="500" class="layui-input" name="name" value='{$vo.name|default=""}' required vali-name="数据名称" placeholder="请输入数据名称">
<span class="help-block">请输入当前数据名称,请尽量保持名称的唯一性,数据名称尽量不要出现重复 ~</span>
</label>
<label class="layui-form-item relative block">
<span class="help-label"><b>数据内容</b>Data Content</span>
<textarea name="content" class="layui-textarea" placeholder="请输入数据内容">{$vo.content|default=''}</textarea>
</label>
</div>
<div class="hr-line-dashed"></div>
{notempty name='vo.id'}<input type='hidden' value='{$vo.id}' name='id'>{/notempty}
<div class="layui-form-item text-center">
<button class="layui-btn" type='submit'>保存数据</button>
<button class="layui-btn layui-btn-danger" type='button' data-confirm="确定要取消编辑吗?" data-close>取消编辑</button>
</div>
</form>

View File

@@ -0,0 +1,86 @@
{extend name='table'}
{block name="button"}
<!--{if auth("add")}-->
<button data-table-id="BaseTable" data-modal='{:url("add")}?type={$type|default=""}' class='layui-btn layui-btn-sm layui-btn-primary'>添加数据</button>
<!--{/if}-->
<!--{if auth("remove")}-->
<button data-table-id="BaseTable" data-action='{:url("remove")}' data-rule="id#{id}" data-confirm="确定要批量删除数据吗?" class='layui-btn layui-btn-sm layui-btn-primary'>批量删除</button>
<!--{/if}-->
{/block}
{block name="content"}
<div class="layui-tab layui-tab-card">
<ul class="layui-tab-title">
{foreach $types as $t}{if isset($type) and $type eq $t}
<li class="layui-this" data-open="{:sysuri()}?type={$t}">{$t}</li>
{else}
<li data-open="{:sysuri()}?type={$t}">{$t}</li>
{/if}{/foreach}
</ul>
<div class="layui-tab-content">
{include file='base/index_search'}
<table id="BaseTable" data-url="{:request()->url()}" data-target-search="form.form-search"></table>
</div>
</div>
{/block}
{block name='script'}
<script>
$(function () {
// 初始化表格组件
$('#BaseTable').layTable({
even: true, height: 'full',
sort: {field: 'sort desc,id', type: 'asc'},
where: {type: '{$type|default=""}'},
cols: [[
{checkbox: true, fixed: true},
{field: 'sort', title: '{:lang("排序权重")}', width: 100, align: 'center', sort: true, templet: '#SortInputTpl'},
// {field: 'type', title: '数据类型', minWidth: 140, align: 'center'},
{field: 'code', title: '数据编码', width: '20%', align: 'left'},
{field: 'name', title: '数据名称', width: '30%', align: 'left'},
{field: 'status', title: '数据状态', minWidth: 110, align: 'center', templet: '#StatusSwitchTpl'},
{field: 'create_at', title: '创建时间', minWidth: 170, align: 'center', sort: true},
{toolbar: '#toolbar', align: 'center', minWidth: 150, title: '数据操作', fixed: 'right'},
]]
});
// 数据状态切换操作
layui.form.on('switch(StatusSwitch)', function (obj) {
var data = {id: obj.value, status: obj.elem.checked > 0 ? 1 : 0};
$.form.load("{:url('state')}", data, 'post', function (ret) {
if (ret.code < 1) $.msg.error(ret.info, 3, function () {
$('#BaseTable').trigger('reload');
});
return false;
}, false);
});
});
</script>
<!-- 列表排序权重模板 -->
<script type="text/html" id="SortInputTpl">
<input type="number" min="0" data-blur-number="0" data-action-blur="{:sysuri()}" data-value="id#{{d.id}};action#sort;sort#{value}" data-loading="false" value="{{d.sort}}" class="layui-input text-center">
</script>
<!-- 数据状态切换模板 -->
<script type="text/html" id="StatusSwitchTpl">
<!--{if auth("state")}-->
<input type="checkbox" value="{{d.id}}" lay-skin="switch" lay-text="已激活|已禁用" lay-filter="StatusSwitch" {{-d.status>0?'checked':''}}>
<!--{else}-->
{{-d.status ? '<b class="color-green">已启用</b>' : '<b class="color-red">已禁用</b>'}}
<!--{/if}-->
</script>
<!-- 数据操作工具条模板 -->
<script type="text/html" id="toolbar">
<!--{if auth('edit')}-->
<a class="layui-btn layui-btn-primary layui-btn-sm" data-event-dbclick data-title="编辑数据" data-modal='{:url("edit")}?id={{d.id}}'> </a>
<!--{/if}-->
<!--{if auth("remove")}-->
<a class="layui-btn layui-btn-danger layui-btn-sm" data-confirm="确定要删除数据吗?" data-action="{:url('remove')}" data-value="id#{{d.id}}"> </a>
<!--{/if}-->
</script>
{/block}

View File

@@ -0,0 +1,42 @@
<form class="layui-form layui-form-pane form-search" action="{:sysuri()}" onsubmit="return false" method="get" autocomplete="off">
<div class="layui-form-item layui-inline">
<label class="layui-form-label">{:lang('数据编码')}</label>
<div class="layui-input-inline">
<input name="code" value="{$get.code|default=''}" placeholder="{:lang('请输入数据编码')}" class="layui-input">
</div>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">{:lang('数据名称')}</label>
<div class="layui-input-inline">
<input name="name" value="{$get.name|default=''}" placeholder="{:lang('请输入数据名称')}" class="layui-input">
</div>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">{:lang('使用状态')}</label>
<div class="layui-input-inline">
<select class="layui-select" name="status">
<option value=''>-- {:lang('全部')} --</option>
{foreach [lang('已禁用记录'),lang('已激活记录')] as $k=>$v}
{if isset($get.status) and $get.status eq $k.""}
<option selected value="{$k}">{$v}</option>
{else}
<option value="{$k}">{$v}</option>
{/if}{/foreach}
</select>
</div>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">{:lang('创建时间')}</label>
<div class="layui-input-inline">
<input data-date-range name="create_at" value="{$get.create_at|default=''}" placeholder="{:lang('请选择创建时间')}" class="layui-input">
</div>
</div>
<div class="layui-form-item layui-inline">
<button class="layui-btn layui-btn-primary"><i class="layui-icon">&#xe615;</i> {:lang('搜 索')}</button>
</div>
</form>

View File

@@ -0,0 +1,234 @@
{extend name="main"}
{block name="button"}
<!--{if isset($super) and $super}-->
<a class="layui-btn layui-btn-sm layui-btn-primary" data-load="{:url('admin/api.system/config')}">{:lang('清理无效配置')}</a>
<!--{/if}-->
<!--{if auth('system')}-->
<a class="layui-btn layui-btn-sm layui-btn-primary" data-modal="{:url('system')}">{:lang('修改系统参数')}</a>
<!--{/if}-->
{/block}
{block name="content"}
<!--{notempty name='issuper'}-->
<div class="layui-card padding-20 shadow">
<div class="layui-card-header notselect">
<span class="help-label">
<b style="color:#333!important;">{:lang('运行模式')}</b>( {:lang('仅超级管理员可配置')} )
</span>
</div>
<div class="layui-card-body">
<div class="layui-btn-group shadow-mini nowrap">
<!--{if $app->isDebug()}-->
<a class="layui-btn layui-btn-sm layui-btn-active">{:lang('以开发模式运行')}</a>
<a class="layui-btn layui-btn-sm layui-btn-primary" data-confirm="{:lang('确定要切换到生产模式运行吗?')}" data-load="{:url('admin/api.system/debug')}?state=1">{:lang('以生产模式运行')}</a>
<!--{else}-->
<a class="layui-btn layui-btn-sm layui-btn-primary" data-confirm="{:lang('确定要切换到开发模式运行吗?')}" data-load="{:url('admin/api.system/debug')}?state=0">{:lang('以开发模式运行')}</a>
<a class="layui-btn layui-btn-sm layui-btn-active">{:lang('以生产模式运行')}</a>
<!--{/if}-->
</div>
<div class="margin-top-20">
<p><b>{:lang('开发模式')}</b>{:lang('开发人员或在功能调试时使用,系统异常时会显示详细的错误信息,同时还会记录操作日志及数据库 SQL 语句信息。')}</p>
<p><b>{:lang('生产模式')}</b>{:lang('项目正式部署上线后使用,系统异常时统一显示 “%s”只记录重要的异常日志信息强烈推荐上线后使用此模式。',[config('app.error_message')])}</p>
</div>
</div>
</div>
<div class="layui-card padding-20 shadow">
<div class="layui-card-header notselect">
<span class="help-label">
<b style="color:#333!important;">{:lang('富编辑器')}</b>( {:lang('仅超级管理员可配置')} )
</span>
</div>
<div class="layui-card-body layui-clear">
<div class="layui-btn-group shadow-mini">
{if !in_array(sysconf('base.editor'),['ckeditor4','ckeditor5','wangEditor','auto'])}{php}sysconf('base.editor','ckeditor4');{/php}{/if}
{foreach ['ckeditor4'=>'CKEditor4','ckeditor5'=>'CKEditor5','wangEditor'=>'wangEditor','auto'=>lang('自适应模式')] as $k => $v}{if sysconf('base.editor') eq $k}
{if auth('storage')}<a data-title="配置{$v}" class="layui-btn layui-btn-sm layui-btn-active">{$v}</a>{else}<a class="layui-btn layui-btn-sm layui-btn-active">{$v}</a>{/if}
{else}
{if auth('storage')}<a data-title="配置{$v}" data-action="{:url('admin/api.system/editor')}" data-value="editor#{$k}" class="layui-btn layui-btn-sm layui-btn-primary">{$v}</a>{else}<a class="layui-btn layui-btn-sm layui-btn-primary">{$v}</a>{/if}
{/if}{/foreach}
</div>
<div class="margin-top-20 full-width pull-left">
<p><b>CKEditor4</b>{:lang('旧版本编辑器,对浏览器兼容较好,但内容编辑体验稍有不足。')}</p>
<p><b>CKEditor5</b>{:lang('新版本编辑器,只支持新特性浏览器,对内容编辑体验较好,推荐使用。')}</p>
<p><b>wangEditor</b>{:lang('国产优质富文本编辑器对于小程序及App内容支持会更友好推荐使用。')}</p>
<p><b>{:lang('自适应模式')}</b>{:lang('优先使用新版本编辑器,若浏览器不支持新版本时自动降级为旧版本编辑器。')}</p>
</div>
</div>
</div>
<!--{/notempty}-->
<div class="layui-card padding-20 shadow">
<div class="layui-card-header notselect">
<span class="help-label">
<b style="color:#333!important;">{:lang('存储引擎')}</b>( {:lang('文件默认存储方式')} )
</span>
</div>
<!-- 初始化存储配置 -->
{if !sysconf('storage.type')}{php}sysconf('storage.type','local');{/php}{/if}
{if !sysconf('storage.link_type')}{php}sysconf('storage.link_type','none');{/php}{/if}
{if !sysconf('storage.name_type')}{php}sysconf('storage.name_type','xmd5');{/php}{/if}
{if !sysconf('storage.allow_exts')}{php}sysconf('storage.allow_exts','doc,gif,ico,jpg,mp3,mp4,p12,pem,png,rar,xls,xlsx');{/php}{/if}
{if !sysconf('storage.local_http_protocol')}{php}sysconf('storage.local_http_protocol','follow');{/php}{/if}
<div class="layui-card-body">
<div class="layui-btn-group shadow-mini nowrap">
{foreach $files as $k => $v}{if sysconf('storage.type') eq $k}
{if auth('storage')}<a data-title="配置{$v}" data-modal="{:url('storage')}?type={$k}" class="layui-btn layui-btn-sm layui-btn-active">{$v}</a>{else}<a class="layui-btn layui-btn-sm layui-btn-active">{$v}</a>{/if}
{else}
{if auth('storage')}<a data-title="配置{$v}" data-modal="{:url('storage')}?type={$k}" class="layui-btn layui-btn-sm layui-btn-primary">{$v}</a>{else}<a class="layui-btn layui-btn-sm layui-btn-primary">{$v}</a>{/if}
{/if}{/foreach}
</div>
<div class="margin-top-20 full-width">
<p><b>{:lang('本地服务器存储')}</b>{:lang('文件上传到本地服务器的 `static/upload` 目录,不支持大文件上传,占用服务器磁盘空间,访问时消耗服务器带宽流量。')}</p>
<p><b>{:lang('自建Alist存储')}</b>{:lang('文件上传到 Alist 存储的服务器或云存储空间,根据服务配置可支持大文件上传,不占用本身服务器空间及服务器带宽流量。')}</p>
<p><b>{:lang('七牛云对象存储')}</b>{:lang('文件上传到七牛云存储空间,支持大文件上传,不占用服务器空间及服务器带宽流量,支持 CDN 加速访问,访问量大时推荐使用。')}</p>
<p><b>{:lang('又拍云USS存储')}</b>{:lang('文件上传到又拍云 USS 存储空间,支持大文件上传,不占用服务器空间及服务器带宽流量,支持 CDN 加速访问,访问量大时推荐使用。')}</p>
<p><b>{:lang('阿里云OSS存储')}</b>{:lang('文件上传到阿里云 OSS 存储空间,支持大文件上传,不占用服务器空间及服务器带宽流量,支持 CDN 加速访问,访问量大时推荐使用。')}</p>
<p><b>{:lang('腾讯云COS存储')}</b>{:lang('文件上传到腾讯云 COS 存储空间,支持大文件上传,不占用服务器空间及服务器带宽流量,支持 CDN 加速访问,访问量大时推荐使用。')}</p>
</div>
</div>
</div>
<div class="layui-card padding-20 shadow">
<div class="layui-card-header notselect">
<span class="help-label">
<b style="color:#333!important;">{:lang('系统参数')}</b>( {:lang('当前系统配置参数')} )
</span>
</div>
<div class="layui-card-body">
<div class="layui-form-item">
<div class="help-label"><b>{:lang('网站名称')}</b>Website</div>
<label class="relative block">
<input readonly value="{:sysconf('site_name')}" class="layui-input layui-bg-gray">
<a data-copy="{:sysconf('site_name')}" class="layui-icon layui-icon-release input-right-icon"></a>
</label>
<div class="help-block">{:lang('网站名称及网站图标,将显示在浏览器的标签上。')}</div>
</div>
<div class="layui-form-item">
<div class="help-label"><b>{:lang('管理程序名称')}</b>Name</div>
<label class="relative block">
<input readonly value="{:sysconf('app_name')}" class="layui-input layui-bg-gray">
<a data-copy="{:sysconf('app_name')}" class="layui-icon layui-icon-release input-right-icon"></a>
</label>
<div class="help-block">{:lang('管理程序名称,将显示在后台左上角标题。')}</div>
</div>
<div class="layui-form-item">
<div class="help-label"><b>{:lang('管理程序版本')}</b>Version</div>
<label class="relative block">
<input readonly value="{:sysconf('app_version')}" class="layui-input layui-bg-gray">
<a data-copy="{:sysconf('app_version')}" class="layui-icon layui-icon-release input-right-icon"></a>
</label>
<div class="help-block">{:lang('管理程序版本,将显示在后台左上角标题。')}</div>
</div>
<div class="layui-form-item">
<div class="help-label"><b>{:lang('公安备案号')}</b>Beian</div>
<label class="relative block">
<input readonly value="{:sysconf('beian')?:'-'}" class="layui-input layui-bg-gray">
<a data-copy="{:sysconf('beian')}" class="layui-icon layui-icon-release input-right-icon"></a>
</label>
<p class="help-block">
{:lang('公安备案号,可以在 %s 查询获取,将在登录页面下面显示。',['<a target="_blank" href="https://www.beian.gov.cn/portal/registerSystemInfo">www.beian.gov.cn</a>'])}
</p>
</div>
<div class="layui-form-item">
<div class="help-label"><b>{:lang('网站备案号')}</b>Miitbeian</div>
<label class="relative block">
<input readonly value="{:sysconf('miitbeian')?:'-'}" class="layui-input layui-bg-gray">
<a data-copy="{:sysconf('miitbeian')}" class="layui-icon layui-icon-release input-right-icon"></a>
</label>
<div class="help-block">
{:lang('网站备案号,可以在 %s 查询获取,将显示在登录页面下面。',['<a target="_blank" href="https://beian.miit.gov.cn">beian.miit.gov.cn</a>'])}
</div>
</div>
<div class="layui-form-item">
<div class="help-label"><b>{:lang('网站版权信息')}</b>Copyright</div>
<label class="relative block">
<input readonly value="{:sysconf('site_copy')}" class="layui-input layui-bg-gray">
<a data-copy="{:sysconf('site_copy')}" class="layui-icon layui-icon-release input-right-icon"></a>
</label>
<div class="help-block">{:lang('网站版权信息,在后台登录页面显示版本信息并链接到备案到信息备案管理系统。')}</div>
</div>
</div>
</div>
<!--{if $app->isDebug()}-->
<div class="layui-card padding-20 shadow">
<div class="layui-card-header notselect">
<span class="help-label">
<b style="color:#333!important;">{:lang('系统信息')}</b>( {:lang('仅开发模式可见')} )
</span>
</div>
<div class="layui-card-body">
<table class="layui-table" lay-even>
<tbody>
<tr>
<th class="nowrap text-center">{:lang('核心框架')}</th>
<td><a target="_blank" href="https://www.thinkphp.cn">ThinkPHP Version {$framework.version|default='None'}</a></td>
</tr>
<tr>
<th class="nowrap text-center">{:lang('平台框架')}</th>
<td><a target="_blank" href="https://thinkadmin.top">ThinkAdmin Version {$thinkadmin.version|default='6.0.0'}</a></td>
</tr>
<tr>
<th class="nowrap text-center">{:lang('操作系统')}</th>
<td>{:php_uname()}</td>
</tr>
<tr>
<th class="nowrap text-center">{:lang('运行环境')}</th>
<td>{:ucfirst($request->server('SERVER_SOFTWARE',php_sapi_name()))} & PHP {$Think.const.PHP_VERSION} & {:ucfirst(app()->db->connect()->getConfig('type'))}</td>
</tr>
<!-- {notempty name='systemid'} -->
<tr>
<th class="nowrap text-center">{:lang('系统序号')}</th>
<td>{$systemid|default=''}</td>
</tr>
<!-- {/notempty} -->
</tbody>
</table>
</div>
</div>
{notempty name='plugins'}
<div class="layui-card padding-20 shadow">
<div class="layui-card-header notselect">
<span class="help-label">
<b style="color:#333!important;">{:lang('应用插件')}</b>( {:lang('仅开发模式可见')} )
</span>
</div>
<div class="layui-card-body">
<table class="layui-table" lay-even>
<thead>
<tr>
<th class="nowrap text-center">{:lang('应用名称')}</th>
<th class="nowrap text-center">{:lang('插件名称')}</th>
<th class="nowrap text-left">{:lang('插件包名')}</th>
<th class="nowrap text-center">{:lang('插件版本')}</th>
<th class="nowrap text-center">{:lang('授权协议')}</th>
</tr>
</thead>
<tbody>
{foreach $plugins as $key=>$plugin}
<tr>
<td class="nowrap text-center">{$key}</td>
<td class="nowrap text-center">{$plugin.name|lang}</td>
<td class="nowrap text-left">
{if empty($plugin.install.document)}{$plugin.package}
{else}<a target="_blank" href="{$plugin.install.document}">{$plugin.package}</a>{/if}
</td>
<td class="nowrap text-center">{$plugin.install.version|default='unknow'}</td>
<td class="nowrap text-center">
{if empty($plugin.install.license)} -
{elseif is_array($plugin.install.license)}{$plugin.install.license|join='、',###}
{else}{$plugin.install.license|default='-'}{/if}
</td>
</tr>
{/foreach}
</tbody>
</table>
</div>
</div>
{/notempty}
<!--{/if}-->
{/block}

View File

@@ -0,0 +1,49 @@
<div class="layui-form-item">
<label class="layui-form-label label-required">
<b class="color-green">命名方式</b><br><span class="nowrap color-desc">NameType</span>
</label>
<div class="layui-input-block">
<div class="layui-input help-checks">
{foreach ['xmd5'=>'文件哈希值 ( 支持秒传 )','date'=>'日期+随机 ( 普通上传 )'] as $k=>$v}
<label class="think-radio notselect">
{if sysconf('storage.name_type') eq $k}
<input checked type="radio" name="storage.name_type" value="{$k}" lay-ignore> {$v}
{else}
<input type="radio" name="storage.name_type" value="{$k}" lay-ignore> {$v}
{/if}
</label>
{/foreach}
</div>
<p class="help-block">类型为“文件哈希”时可以实现文件秒传功能,同一个文件只需上传一次节省存储空间,推荐使用。</p>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label label-required">
<b class="color-green">链接类型</b><br><span class="nowrap color-desc">LinkType</span>
</label>
<div class="layui-input-block">
<div class="layui-input help-checks">
{foreach ['none'=>'简洁链接','full'=>'完整链接','none+compress'=>'简洁并压缩图片','full+compress'=>'完整并压缩图片'] as $k=>$v}
<label class="think-radio notselect">
{if sysconf('storage.link_type') eq $k}
<input checked type="radio" name="storage.link_type" value="{$k}" lay-ignore> {$v}
{else}
<input type="radio" name="storage.link_type" value="{$k}" lay-ignore> {$v}
{/if}
</label>
{/foreach}
</div>
<p class="help-block">类型为“简洁链接”时链接将只返回 hash 地址,而“完整链接”将携带参数保留文件名,图片压缩功能云平台会单独收费。</p>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label" for="storage.allow_exts">
<b class="color-green">允许类型</b><br><span class="nowrap color-desc">AllowExts</span>
</label>
<div class="layui-input-block">
<input id="storage.allow_exts" type="text" name="storage.allow_exts" value="{:sysconf('storage.allow_exts')}" required vali-name="文件后缀" placeholder="请输入系统文件上传后缀" class="layui-input">
<p class="help-block">设置系统允许上传的文件后缀多个以英文逗号隔开如png,jpg,rar,doc未包含在设置内的文件后缀将不被允许上传。</p>
</div>
</div>

View File

@@ -0,0 +1,98 @@
<form action="{:sysuri()}" method="post" data-auto="true" class="layui-form layui-card">
<div class="layui-card-body padding-top-20">
<div class="color-text layui-code text-center layui-bg-gray" style="border-left-width:1px;margin:0 0 15px 40px">
<p class="margin-bottom-5 font-w7">文件将上传到 <a target="_blank" href="https://www.aliyun.com/minisite/goods?taskCode=shareNew2205&recordId=3646641&userCode=ztlqlu4v">阿里云</a> OSS 存储,需要配置 OSS 公开访问及跨域策略</p>
<p>配置跨域访问 CORS 规则,设置:来源 Origin *,允许 Methods POST允许 Headers *</p>
</div>
{include file='config/storage-0'}
<div class="layui-form-item">
<label class="layui-form-label label-required">
<b class="color-green">访问协议</b><br><span class="nowrap color-desc">Protocol</span>
</label>
<div class="layui-input-block">
{if !sysconf('storage.alioss_http_protocol')}{php}sysconf('storage.alioss_http_protocol','http');{/php}{/if}
<div class="layui-input help-checks">
{foreach ['http'=>'HTTP','https'=>'HTTPS','auto'=>"AUTO"] as $protocol=>$remark}
<label class="think-radio">
{if sysconf('storage.alioss_http_protocol') eq $protocol}
<input checked type="radio" name="storage.alioss_http_protocol" value="{$protocol}" lay-ignore> {$remark}
{else}
<input type="radio" name="storage.alioss_http_protocol" value="{$protocol}" lay-ignore> {$remark}
{/if}
</label>
{/foreach}
</div>
<p class="help-block">阿里云OSS存储访问协议其中 HTTPS 需要配置证书才能使用AUTO 为相对协议)</p>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">
<b class="color-green">存储区域</b><br><span class="nowrap color-desc label-required">Region</span>
</label>
<div class="layui-input-block">
<select class="layui-select" name="storage.alioss_point" lay-search>
{foreach $points as $point => $title}
{if sysconf('storage.alioss_point') eq $point}
<option selected value="{$point}">{$title} {$point} </option>
{else}
<option value="{$point}">{$title} {$point} </option>
{/if}{/foreach}
</select>
<p class="help-block">阿里云OSS存储空间所在区域需要严格对应储存所在区域才能上传文件</p>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label" for="storage.alioss_bucket">
<b class="color-green">空间名称</b><br><span class="nowrap color-desc">Bucket</span>
</label>
<div class="layui-input-block">
<input id="storage.alioss_bucket" type="text" name="storage.alioss_bucket" value="{:sysconf('storage.alioss_bucket')}" required vali-name="空间名称" placeholder="请输入阿里云OSS存储 Bucket (空间名称)" class="layui-input">
<p class="help-block">填写阿里云OSS存储空间名称think-admin-oss需要是全区唯一的值不存在时会自动创建</p>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label" for="storage.alioss_http_domain">
<b class="color-green">访问域名</b><br><span class="nowrap color-desc">Domain</span>
</label>
<div class="layui-input-block">
<input id="storage.alioss_http_domain" type="text" name="storage.alioss_http_domain" value="{:sysconf('storage.alioss_http_domain')}" required vali-name="访问域名" placeholder="请输入阿里云OSS存储 Domain (访问域名)" class="layui-input">
<p class="help-block">填写阿里云OSS存储外部访问域名不需要填写访问协议static.alioss.thinkadmin.top</p>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label" for="storage.alioss_access_key">
<b class="color-green">访问密钥</b><br><span class="nowrap color-desc">AccessKey</span>
</label>
<div class="layui-input-block">
<input id="storage.alioss_access_key" type="text" name="storage.alioss_access_key" value="{:sysconf('storage.alioss_access_key')}" required vali-name="访问密钥" placeholder="请输入阿里云OSS存储 AccessKey (访问密钥)" class="layui-input">
<p class="help-block">可以在 [ 阿里云 > 个人中心 ] 设置并获取到访问密钥</p>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label" for="storage.alioss_secret_key">
<b class="color-green">安全密钥</b><br><span class="nowrap color-desc">SecretKey</span>
</label>
<div class="layui-input-block">
<input id="storage.alioss_secret_key" type="text" name="storage.alioss_secret_key" value="{:sysconf('storage.alioss_secret_key')}" maxlength="43" required vali-name="安全密钥" placeholder="请输入阿里云OSS存储 SecretKey (安全密钥)" class="layui-input">
<p class="help-block">可以在 [ 阿里云 > 个人中心 ] 设置并获取到安全密钥</p>
</div>
</div>
<div class="hr-line-dashed margin-left-40"></div>
<input type="hidden" name="storage.type" value="alioss">
<div class="layui-form-item text-center padding-left-40">
<button class="layui-btn" type="submit">保存配置</button>
<button class="layui-btn layui-btn-danger" type='button' data-confirm="确定要取消修改吗?" data-close>取消修改</button>
</div>
</div>
</form>

View File

@@ -0,0 +1,81 @@
<form action="{:sysuri()}" method="post" data-auto="true" class="layui-form layui-card">
<div class="layui-card-body padding-top-20">
<div class="color-text layui-code text-center layui-bg-gray" style="border-left-width:1px;margin:0 0 15px 40px">
<p class="margin-bottom-5 font-w7">文件将上传到 <a target="_blank" href="https://alist.nn.ci/zh/">Alist</a> 自建存储,需要自行搭建 <a target="_blank" href="https://alist.nn.ci/zh/">Alist</a> 存储服务器。</p>
<p>Alist 是一个支持多种存储的文件列表程序,可将各种云盘及本地磁盘资源进行整合。</p>
<p>建议不要开放匿名用户访问,尽量使用独立账号管理,需要关闭 “签名所有” 让文件可以直接访问。</p>
</div>
{include file='config/storage-0'}
<div class="layui-form-item">
<label class="layui-form-label label-required">
<b class="color-green">访问协议</b><br><span class="nowrap color-desc">Protocol</span>
</label>
<div class="layui-input-block">
{if !sysconf('storage.alist_http_protocol')}{php}sysconf('storage.alist_http_protocol','http');{/php}{/if}
<div class="layui-input help-checks">
{foreach ['http'=>'HTTP','https'=>'HTTPS','auto'=>"AUTO"] as $protocol=>$remark}
<label class="think-radio">
{if sysconf('storage.alist_http_protocol') eq $protocol}
<input checked type="radio" name="storage.alist_http_protocol" value="{$protocol}" lay-ignore> {$remark}
{else}
<input type="radio" name="storage.alist_http_protocol" value="{$protocol}" lay-ignore> {$remark}
{/if}
</label>
{/foreach}
</div>
<p class="help-block">请选择 Alist 存储访问协议,其中 HTTPS 需要配置证书才能使用( AUTO 为相对协议 </p>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label" for="storage.alist_http_domain">
<b class="color-green">访问域名</b><br><span class="nowrap color-desc">Domain</span>
</label>
<div class="layui-input-block">
<input id="storage.alist_http_domain" type="text" name="storage.alist_http_domain" value="{:sysconf('storage.alist_http_domain')}" required vali-name="访问域名" placeholder="请输入 Alist 存储的访问域名" class="layui-input">
<p class="help-block">请填写 Alist 存储访问域名不需要填写访问协议storage.thinkadmin.top</p>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label" for="storage.alist_savepath">
<b class="color-green">存储目录</b><br><span class="nowrap color-desc">Directory</span>
</label>
<div class="layui-input-block">
<input id="storage.alist_savepath" type="text" name="storage.alist_savepath" value="{:sysconf('storage.alist_savepath')}" required vali-name="存储目录" placeholder="请输入 Alist 存储目录" class="layui-input">
<p class="help-block">请填写 Alist 用户基本目录的相对存储位置,填写 / 表示用户基本目录( 需要拥有读写权限 </p>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label" for="storage.alist_username">
<b class="color-green">用户账号</b><br><span class="nowrap color-desc">Username</span>
</label>
<div class="layui-input-block">
<input id="storage.alist_username" name="storage.alist_username" value="{:sysconf('storage.alist_username')}" maxlength="100" required vali-name="用户账号" placeholder="请输入 Alist 存储的用户账号" class="layui-input">
<p class="help-block">请填写 Alist 用户账号,注意此账号需要拥有上面存储目录的访问权限。</p>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label" for="storage.alist_password">
<b class="color-green">用户密码</b><br><span class="nowrap color-desc">Password</span>
</label>
<div class="layui-input-block">
<input id="storage.alist_password" name="storage.alist_password" value="{:sysconf('storage.alist_password')}" maxlength="100" required vali-name="用户密码" placeholder="请输入 Alist 存储的用户密码" class="layui-input">
<p class="help-block">请填写 Alist 用户登录密码,用于生成文件上传的接口认证令牌,如果填写错误将无法上传文件。</p>
</div>
</div>
<div class="hr-line-dashed margin-left-40"></div>
<input type="hidden" name="storage.type" value="alist">
<div class="layui-form-item text-center padding-left-40">
<button class="layui-btn" type="submit">保存配置</button>
<button class="layui-btn layui-btn-danger" type='button' data-confirm="确定要取消修改吗?" data-close>取消修改</button>
</div>
</div>
</form>

View File

@@ -0,0 +1,51 @@
<form action="{:sysuri()}" method="post" data-auto="true" class="layui-form layui-card">
<div class="layui-card-body padding-top-20">
<div class="color-text layui-code text-center layui-bg-gray" style="border-left-width:1px;margin:0 0 15px 40px">
<p class="margin-bottom-5 font-w7">文件将存储在本地服务器,默认保存在 public/upload 目录,文件以 HASH 命名。</p>
<p>文件存储的目录需要有读写权限,有足够的存储空间。<span class="color-red">特别注意,本地存储暂不支持图片压缩!</span></p>
</div>
{include file='config/storage-0'}
<div class="layui-form-item">
<label class="layui-form-label label-required">
<b class="color-green">访问协议</b><br><span class="nowrap color-desc">Protocol</span>
</label>
<div class="layui-input-block">
{if !sysconf('storage.local_http_protocol')}{php}sysconf('storage.local_http_protocol','follow');{/php}{/if}
<div class="layui-input help-checks">
{foreach ['follow'=>'FOLLOW','http'=>'HTTP','https'=>'HTTPS','path'=>'PATH','auto'=>'AUTO'] as $protocol=>$remark}
<label class="think-radio">
{if sysconf('storage.local_http_protocol') eq $protocol}
<input checked type="radio" name="storage.local_http_protocol" value="{$protocol}" lay-ignore> {$remark}
{else}
<input type="radio" name="storage.local_http_protocol" value="{$protocol}" lay-ignore> {$remark}
{/if}
</label>
{/foreach}
</div>
<p class="help-block">本地存储访问协议,其中 HTTPS 需要配置证书才能使用( FOLLOW 跟随系统PATH 文件路径AUTO 相对协议 </p>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label" for="storage.local_http_domain">
<b class="color-green">访问域名</b><br><span class="nowrap color-desc">Domain</span>
</label>
<div class="layui-input-block">
<input id="storage.local_http_domain" type="text" name="storage.local_http_domain" value="{:sysconf('storage.local_http_domain')}" placeholder="请输入上传后的访问域名 (非必填项)" class="layui-input">
<p class="help-block">填写上传后的访问域名不指定时根据当前访问地址自动计算不需要填写访问协议static.thinkadmin.top</p>
</div>
</div>
<div class="hr-line-dashed margin-left-40"></div>
<input type="hidden" name="storage.type" value="local">
<div class="layui-form-item text-center padding-left-40">
<button class="layui-btn" type="submit">保存配置</button>
<button class="layui-btn layui-btn-danger" type='button' data-confirm="确定要取消修改吗?" data-close>取消修改</button>
</div>
</div>
</form>

View File

@@ -0,0 +1,97 @@
<form action="{:sysuri()}" method="post" data-auto="true" class="layui-form layui-card">
<div class="layui-card-body padding-top-20">
<div class="color-text layui-code text-center layui-bg-gray" style="border-left-width:1px;margin:0 0 15px 40px">
<p class="margin-bottom-5 font-w7">文件将上传到 <a target="_blank" href="https://s.qiniu.com/rYr26v">七牛云</a> 存储,对象存储需要配置为公开访问的 Bucket 空间</p>
完成实名认证后可获得 10G 免费存储空间哦!<a target="_blank" href="https://s.qiniu.com/rYr26v">我要免费申请</a>
</div>
{include file='config/storage-0'}
<div class="layui-form-item">
<label class="layui-form-label label-required">
<b class="color-green">访问协议</b><br><span class="nowrap color-desc">Protocol</span>
</label>
<div class="layui-input-block">
{if !sysconf('storage.qiniu_http_protocol')}{php}sysconf('storage.qiniu_http_protocol','http');{/php}{/if}
<div class="layui-input help-checks">
{foreach ['http'=>'HTTP','https'=>'HTTPS','auto'=>"AUTO"] as $protocol=>$remark}
<label class="think-radio">
{if sysconf('storage.qiniu_http_protocol') eq $protocol}
<input checked type="radio" name="storage.qiniu_http_protocol" value="{$protocol}" lay-ignore> {$remark}
{else}
<input type="radio" name="storage.qiniu_http_protocol" value="{$protocol}" lay-ignore> {$remark}
{/if}
</label>
{/foreach}
</div>
<p class="help-block">七牛云存储访问协议,其中 HTTPS 需要配置证书才能使用( AUTO 为相对协议 </p>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">
<b class="color-green">存储区域</b><br><span class="nowrap color-desc label-required">Region</span>
</label>
<div class="layui-input-block">
<select class="layui-select" name="storage.qiniu_region" lay-search>
{foreach $points as $point => $title}
{if sysconf('storage.qiniu_region') eq $point}
<option selected value="{$point}">{$title} {$point} </option>
{else}
<option value="{$point}">{$title} {$point} </option>
{/if}{/foreach}
</select>
<p class="help-block">七牛云存储空间所在区域,需要严格对应储存所在区域才能上传文件</p>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label" for="storage.qiniu_bucket">
<b class="color-green">空间名称</b><br><span class="nowrap color-desc">Bucket</span>
</label>
<div class="layui-input-block">
<input id="storage.qiniu_bucket" type="text" name="storage.qiniu_bucket" value="{:sysconf('storage.qiniu_bucket')}" required vali-name="空间名称" placeholder="请输入七牛云存储 Bucket (空间名称)" class="layui-input">
<p class="help-block">填写七牛云存储空间名称static</p>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label" for="storage.qiniu_http_domain">
<b class="color-green">访问域名</b><br><span class="nowrap color-desc">Domain</span>
</label>
<div class="layui-input-block">
<input id="storage.qiniu_http_domain" type="text" name="storage.qiniu_http_domain" value="{:sysconf('storage.qiniu_http_domain')}" required vali-name="访问域名" placeholder="请输入七牛云存储 Domain (访问域名)" class="layui-input">
<p class="help-block">填写七牛云存储访问域名不需要填写访问协议static.qiniu.thinkadmin.top</p>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label" for="storage.qiniu_access_key">
<b class="color-green">访问密钥</b><br><span class="nowrap color-desc">AccessKey</span>
</label>
<div class="layui-input-block">
<input id="storage.qiniu_access_key" type="text" name="storage.qiniu_access_key" value="{:sysconf('storage.qiniu_access_key')}" required vali-name="访问密钥" placeholder="请输入七牛云授权 AccessKey (访问密钥)" class="layui-input">
<p class="help-block">可以在 [ 七牛云 > 个人中心 ] 设置并获取到访问密钥</p>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label" for="storage.qiniu_secret_key">
<b class="color-green">安全密钥</b><br><span class="nowrap color-desc">SecretKey</span>
</label>
<div class="layui-input-block">
<input id="storage.qiniu_secret_key" type="text" name="storage.qiniu_secret_key" value="{:sysconf('storage.qiniu_secret_key')}" maxlength="43" required vali-name="安全密钥" placeholder="请输入七牛云授权 SecretKey (安全密钥)" class="layui-input">
<p class="help-block">可以在 [ 七牛云 > 个人中心 ] 设置并获取到安全密钥</p>
</div>
</div>
<div class="hr-line-dashed margin-left-40"></div>
<input type="hidden" name="storage.type" value="qiniu">
<div class="layui-form-item text-center padding-left-40">
<button class="layui-btn" type="submit">保存配置</button>
<button class="layui-btn layui-btn-danger" type='button' data-confirm="确定要取消修改吗?" data-close>取消修改</button>
</div>
</div>
</form>

View File

@@ -0,0 +1,96 @@
<form action="{:sysuri()}" method="post" data-auto="true" class="layui-form layui-card">
<div class="layui-card-body padding-top-20">
<div class="color-text layui-code text-center layui-bg-gray" style="border-left-width:1px;margin:0 0 15px 40px">
<p class="margin-bottom-5 font-w7">文件将上传到 <a target="_blank" href="https://curl.qcloud.com/4t0Mbw2K">腾讯云</a> COS 存储,需要配置 COS 公有读私有写访问权限及跨域策略</p>
<p>配置跨域访问 CORS 规则,设置来源 Origin *,允许 Methods POST允许 Headers *</p>
</div>
{include file='config/storage-0'}
<div class="layui-form-item">
<label class="layui-form-label label-required">
<b class="color-green">访问协议</b><br><span class="nowrap color-desc">Protocol</span>
</label>
<div class="layui-input-block">
{if !sysconf('storage.txcos_http_protocol')}{php}sysconf('storage.txcos_http_protocol','http');{/php}{/if}
{foreach ['http'=>'HTTP','https'=>'HTTPS','auto'=>"AUTO"] as $protocol=>$remark}
<label class="think-radio">
{if sysconf('storage.txcos_http_protocol') eq $protocol}
<input checked type="radio" name="storage.txcos_http_protocol" value="{$protocol}" lay-ignore> {$remark}
{else}
<input type="radio" name="storage.txcos_http_protocol" value="{$protocol}" lay-ignore> {$remark}
{/if}
</label>
{/foreach}
<p class="help-block">腾讯云COS存储访问协议其中 HTTPS 需要配置证书才能使用( AUTO 为相对协议 </p>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">
<b class="color-green">存储区域</b><br><span class="nowrap color-desc label-required">Region</span>
</label>
<div class="layui-input-block">
<select class="layui-select" name="storage.txcos_point" lay-search>
{foreach $points as $point => $title}
{if sysconf('storage.txcos_point') eq $point}
<option selected value="{$point}">{$title} {$point} </option>
{else}
<option value="{$point}">{$title} {$point} </option>
{/if}{/foreach}
</select>
<p class="help-block">腾讯云COS存储空间所在区域需要严格对应储存所在区域才能上传文件</p>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label" for="storage.txcos_bucket">
<b class="color-green">空间名称</b><br><span class="nowrap color-desc">Bucket</span>
</label>
<div class="layui-input-block">
<input id="storage.txcos_bucket" type="text" name="storage.txcos_bucket" value="{:sysconf('storage.txcos_bucket')}" required vali-name="空间名称" placeholder="请输入腾讯云COS存储 Bucket" class="layui-input">
<p class="help-block">填写腾讯云COS存储空间名称thinkadmin-1251143395</p>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label" for="storage.txcos_http_domain">
<b class="color-green">访问域名</b><br><span class="nowrap color-desc">Domain</span>
</label>
<div class="layui-input-block">
<input id="storage.txcos_http_domain" type="text" name="storage.txcos_http_domain" value="{:sysconf('storage.txcos_http_domain')}" required vali-name="访问域名" placeholder="请输入腾讯云COS存储 Domain" class="layui-input">
<p class="help-block">填写腾讯云COS存储外部访问域名不需要填写访问协议static.txcos.thinkadmin.top</p>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label" for="storage.txcos_access_key">
<b class="color-green">访问密钥</b><br><span class="nowrap color-desc">AccessKey</span>
</label>
<div class="layui-input-block">
<input id="storage.txcos_access_key" type="text" name="storage.txcos_access_key" value="{:sysconf('storage.txcos_access_key')}" required vali-name="访问密钥" placeholder="请输入腾讯云COS存储 AccessKey" class="layui-input">
<p class="help-block">可以在 [ 腾讯云 > 个人中心 ] 设置并获取到访问密钥</p>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label" for="storage.txcos_secret_key">
<b class="color-green">安全密钥</b><br><span class="nowrap color-desc">SecretKey</span>
</label>
<div class="layui-input-block">
<input id="storage.txcos_secret_key" type="text" name="storage.txcos_secret_key" value="{:sysconf('storage.txcos_secret_key')}" maxlength="43" required vali-name="安全密钥" placeholder="请输入腾讯云COS存储 SecretKey" class="layui-input">
<p class="help-block">可以在 [ 腾讯云 > 个人中心 ] 设置并获取到安全密钥</p>
</div>
</div>
<div class="hr-line-dashed margin-left-40"></div>
<input type="hidden" name="storage.type" value="txcos">
<div class="layui-form-item text-center padding-left-40">
<button class="layui-btn" type="submit">保存配置</button>
<button class="layui-btn layui-btn-danger" type='button' data-confirm="确定要取消修改吗?" data-close>取消修改</button>
</div>
</div>
</form>

View File

@@ -0,0 +1,81 @@
<form action="{:sysuri()}" method="post" data-auto="true" class="layui-form layui-card">
<div class="layui-card-body padding-top-20">
<div class="color-text layui-code text-center layui-bg-gray" style="border-left-width:1px;margin:0 0 15px 40px">
<p class="margin-bottom-5 font-w7">文件将上传到 <a target="_blank" href="https://console.upyun.com/register/?invite=PN1cRmjRb">又拍云</a> USS 存储,需要配置 USS 公开访问及跨域策略</p>
<p>配置跨域访问 CORS 规则,设置来源 Origin *,允许 Methods POST允许 Headers *</p>
</div>
{include file='config/storage-0'}
<div class="layui-form-item">
<label class="layui-form-label label-required">
<b class="color-green">访问协议</b><br><span class="nowrap color-desc">Protocol</span>
</label>
<div class="layui-input-block">
{if !sysconf('storage.upyun_http_protocol')}{php}sysconf('storage.upyun_http_protocol','http');{/php}{/if}
<div class="layui-input help-checks">
{foreach ['http'=>'HTTP','https'=>'HTTPS','auto'=>"AUTO"] as $protocol=>$remark}
<label class="think-radio">
{if sysconf('storage.upyun_http_protocol') eq $protocol}
<input checked type="radio" name="storage.upyun_http_protocol" value="{$protocol}" lay-ignore> {$remark}
{else}
<input type="radio" name="storage.upyun_http_protocol" value="{$protocol}" lay-ignore> {$remark}
{/if}
</label>
{/foreach}
</div>
<p class="help-block">又拍云存储访问协议,其中 HTTPS 需要配置证书才能使用AUTO 为相对协议)</p>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label" for="storage.upyun_bucket">
<b class="color-green">空间名称</b><br><span class="nowrap color-desc">Bucket</span>
</label>
<div class="layui-input-block">
<input id="storage.upyun_bucket" name="storage.upyun_bucket" value="{:sysconf('storage.upyun_bucket')}" required vali-name="空间名称" placeholder="请输入又拍云存储 Bucket (空间名称)" class="layui-input">
<p class="help-block">填写又拍云存储空间名称think-admin-uss需要是全区唯一的值不存在时会自动创建</p>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label" for="storage.upyun_http_domain">
<b class="color-green">访问域名</b><br><span class="nowrap color-desc">Domain</span>
</label>
<div class="layui-input-block">
<input id="storage.upyun_http_domain" name="storage.upyun_http_domain" value="{:sysconf('storage.upyun_http_domain')}" required vali-name="访问域名" placeholder="请输入又拍云存储 Domain (访问域名)" class="layui-input">
<p class="help-block">填写又拍云存储外部访问域名不需要填写访问协议static.uss.thinkadmin.top</p>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label" for="storage.upyun_access_key">
<b class="color-green">操作账号</b><br><span class="nowrap color-desc">Username</span>
</label>
<div class="layui-input-block">
<input id="storage.upyun_access_key" name="storage.upyun_access_key" value="{:sysconf('storage.upyun_access_key')}" maxlength="100" required vali-name="操作员账号" placeholder="请输入又拍云存储 Username (操作员账号)" class="layui-input">
<p class="help-block">可以在 [ 账户管理 > 操作员 ] 设置操作员账号并将空间给予授权。</p>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label" for="storage.upyun_secret_key">
<b class="color-green">操作密码</b><br><span class="nowrap color-desc">Password</span>
</label>
<div class="layui-input-block">
<input id="storage.upyun_secret_key" name="storage.upyun_secret_key" value="{:sysconf('storage.upyun_secret_key')}" maxlength="100" required vali-name="操作员密码" placeholder="请输入又拍云存储 Password (操作员密码)" class="layui-input">
<p class="help-block">可以在 [ 账户管理 > 操作员 ] 设置操作员密码并将空间给予授权</p>
</div>
</div>
<div class="hr-line-dashed margin-left-40"></div>
<input type="hidden" name="storage.type" value="upyun">
<div class="layui-form-item text-center padding-left-40">
<button class="layui-btn" type="submit">保存配置</button>
<button class="layui-btn layui-btn-danger" type='button' data-confirm="确定要取消修改吗?" data-close>取消修改</button>
</div>
</div>
</form>

View File

@@ -0,0 +1,129 @@
<form action="{:sysuri()}" method="post" data-auto="true" class="layui-form layui-card">
<div class="layui-card-body padding-left-40">
<div class="layui-row layui-col-space15 margin-bottom-5">
<div class="layui-col-xs4 padding-bottom-0">
<label class="relative block">
<span class="help-label"><b>登录表单标题</b>Login Name</span>
<input name="login_name" required placeholder="请输入登录页面的表单标题" vali-name="登录标题" value="{:sysconf('login_name')?:'系统管理'}" class="layui-input">
</label>
</div>
<div class="layui-col-xs4 padding-bottom-0">
<div class="help-label label-required-prev"><b>后台登录入口</b>Login Entry</div>
<label class="layui-input relative block nowrap label-required-null">
<span>{:sysuri('@',[],false,true)}</span>
<input autofocus required pattern="[a-zA-Z_][a-zA-Z0-9_]*" vali-name="登录入口" placeholder="请输入后台登录入口" class="layui-input inline-block padding-0 border-0" style="width:100px;background:none" value="{:substr(sysuri('admin/index/index',[],false), strlen(sysuri('@')))}" name="xpath">
</label>
</div>
<div class="layui-col-xs4 padding-bottom-0">
<div class="help-label label-required-prev"><b>后台默认配色</b>Theme Style</div>
<select class="layui-select" name="site_theme" lay-filter="SiteTheme">
{foreach $themes as $k=>$v}{if sysconf('base.site_theme') eq $k}
<option selected value="{$k}">{$v}</option>
{else}
<option value="{$k}">{$v}</option>
{/if}{/foreach}
</select>
</div>
<div class="layui-col-xs12 padding-top-0 padding-bottom-0">
<span class="help-block">后台登录入口是由英文字母开头,且不能有相同名称的模块,设置之后原地址不能继续访问,请谨慎配置 ~</span>
</div>
</div>
<div class="layui-form-item margin-bottom-5">
<div class="help-label"><b>登录背景图片</b>Background Image</div>
<div class="layui-textarea help-images">
<input type="hidden" value="{:sysconf('login_image')}" name="login_image">
</div>
</div>
<div class="layui-form-item margin-bottom-5">
<div class="help-label label-required-prev"><b>JWT 接口密钥</b>Jwt Key</div>
<label class="relative block label-required-null">
<input class="layui-input" pattern=".{32}" maxlength="32" required vali-name="接口密钥" placeholder="请输入32位JWT接口密钥" name="data.jwtkey" value="{:sysconf('data.jwtkey')?:md5(uniqid(strval(rand(1000,9999)),true))}">
<a class="input-right-icon layui-icon layui-icon-refresh" id="RefreshJwtKey"></a>
</label>
<div class="help-block sub-span-blue">
请输入 <span>32</span> <span>JWT</span> 接口密钥,在使用 <span>JWT</span> 接口时需要使用此密钥进行加密及签名!
</div>
</div>
<div class="layui-form-item margin-bottom-5">
<div class="help-label label-required-prev"><b>浏览器小图标</b>Browser Icon</div>
<div class="relative block label-required-null">
<input class="layui-input" required pattern="url" vali-name="图标文件" data-tips-image data-tips-hover placeholder="请上传浏览器图标" value="{:sysconf('site_icon')}" name="site_icon">
<a class="input-right-icon layui-icon layui-icon-upload-drag" data-file="btn" data-type="png,jpg,jpeg" data-field="site_icon"></a>
</div>
<div class="help-block sub-span-blue">
建议上传 <span>128x128</span> <span>256x256</span> <span>JPG</span>,<span>PNG</span>,<span>JPEG</span> 图片,保存后会自动生成 <span>48x48</span> <span>ICO</span> 文件 ~
</div>
</div>
<div class="layui-row layui-col-space15 margin-bottom-5">
<div class="layui-col-xs4 padding-bottom-0">
<label class="layui-form-item margin-bottom-5 relative block">
<span class="help-label"><b>网站名称</b>Site Name</span>
<input name="site_name" required placeholder="请输入网站名称" vali-name="网站名称" value="{:sysconf('site_name')}" class="layui-input">
<span class="help-block">网站名称将显示在浏览器的标签上 ~</span>
</label>
</div>
<div class="layui-col-xs4 padding-bottom-0">
<label class="layui-form-item margin-bottom-5 relative block">
<span class="help-label"><b>后台程序名称</b>App Name</span>
<input name="app_name" required placeholder="请输入程序名称" vali-name="程序名称" value="{:sysconf('app_name')}" class="layui-input">
<span class="help-block">管理程序名称显示在后台左上标题处 ~</span>
</label>
</div>
<div class="layui-col-xs4 padding-bottom-0">
<label class="layui-form-item margin-bottom-5 relative block">
<span class="help-label"><b>后台程序版本</b>App Version</span>
<input name="app_version" placeholder="请输入程序版本" value="{:sysconf('app_version')}" class="layui-input">
<span class="help-block">管理程序版本显示在后台左上标题处 ~</span>
</label>
</div>
<div class="layui-col-xs4 padding-top-0 padding-bottom-0">
<label class="relative block">
<span class="help-label"><b>公安安备号</b>Beian</span>
<input name="beian" placeholder="请输入公安安备号" value="{:sysconf('beian')}" class="layui-input">
</label>
</div>
<div class="layui-col-xs4 padding-top-0 padding-bottom-0">
<label class="relative block">
<span class="help-label"><b>网站备案号</b>Miitbeian</span>
<input name="miitbeian" placeholder="请输入网站备案号" value="{:sysconf('miitbeian')}" class="layui-input">
</label>
</div>
<div class="layui-col-xs4 padding-top-0 padding-bottom-0">
<label class="relative block">
<span class="help-label"><b>网站版权信息</b>Copyright</span>
<input name="site_copy" required placeholder="请输入版权信息" vali-name="版权信息" value="{:sysconf('site_copy')}" class="layui-input">
</label>
</div>
<div class="layui-col-xs12 help-block padding-top-0">
网站备案号和公安备案号可以在<a target="_blank" href="https://beian.miit.gov.cn">备案管理中心</a>查询并获取,网站上线时必需配置备案号,备案号会链接到信息备案管理系统 ~
</div>
</div>
</div>
<div class="hr-line-dashed"></div>
<div class="layui-form-item text-center">
<button class="layui-btn" type="submit">保存配置</button>
<button class="layui-btn layui-btn-danger" type='button' data-confirm="确定要取消修改吗?" data-close>取消修改</button>
</div>
</form>
<script>
$('[name=login_image]').uploadMultipleImage();
require(['md5'], function (md5) {
$('body').off('click', '#RefreshJwtKey').on('click', '#RefreshJwtKey', function () {
$(this).parent().find('input').val(md5.hash(Date.toString() + Math.random()));
});
});
layui.form.on('select(SiteTheme)', function (data) {
var alls = '', prox = 'layui-layout-theme-', curt = prox + data.value;
$(data.elem.options).map(function () {
if (this.value !== data.value) alls += ' ' + prox + this.value;
});
$('.layui-layout-body').removeClass(alls).addClass(curt)
});
</script>

568
app/admin/view/error.php Normal file
View File

@@ -0,0 +1,568 @@
<?php
if (!function_exists('parse_padding')) {
function parse_padding($source)
{
$length = strlen(strval(count($source['source']) + $source['first']));
return 40 + ($length - 1) * 8;
}
}
if (!function_exists('parse_class')) {
function parse_class($name): string
{
$names = explode('\\', $name);
return '<abbr title="' . $name . '">' . end($names) . '</abbr>';
}
}
if (!function_exists('parse_file')) {
function parse_file($file, $line): string
{
return '<a class="toggle" title="' . "{$file} line {$line}" . '">' . basename($file) . " line {$line}" . '</a>';
}
}
if (!function_exists('parse_args')) {
function parse_args($args): string
{
$result = [];
foreach ($args as $key => $item) {
switch (true) {
case is_object($item):
$value = sprintf('<em>object</em>(%s)', parse_class(get_class($item)));
break;
case is_array($item):
if (count($item) > 3) {
$value = sprintf('[%s, ...]', parse_args(array_slice($item, 0, 3)));
} else {
$value = sprintf('[%s]', parse_args($item));
}
break;
case is_string($item):
if (strlen($item) > 20) {
$value = sprintf(
'\'<a class="toggle" title="%s">%s...</a>\'',
htmlentities($item),
htmlentities(substr($item, 0, 20))
);
} else {
$value = sprintf("'%s'", htmlentities($item));
}
break;
case is_int($item):
case is_float($item):
$value = $item;
break;
case is_null($item):
$value = '<em>null</em>';
break;
case is_bool($item):
$value = '<em>' . ($item ? 'true' : 'false') . '</em>';
break;
case is_resource($item):
$value = '<em>resource</em>';
break;
default:
$value = htmlentities(str_replace("\n", '', var_export(strval($item), true)));
break;
}
$result[] = is_int($key) ? $value : "'{$key}' => {$value}";
}
return implode(', ', $result);
}
}
if (!function_exists('echo_value')) {
function echo_value($val)
{
if (is_array($val) || is_object($val)) {
echo htmlentities(json_encode($val, JSON_PRETTY_PRINT));
} elseif (is_bool($val)) {
echo $val ? 'true' : 'false';
} elseif (is_scalar($val)) {
echo htmlentities($val);
} else {
echo 'Resource';
}
}
}
?>
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>系统发生错误</title>
<meta name="robots" content="noindex,nofollow"/>
<style>
/* Base */
body {
color: #333;
font: 16px Verdana, "Helvetica Neue", helvetica, Arial, 'Microsoft YaHei', sans-serif;
margin: 0;
padding: 0 20px 20px;
}
h1 {
margin: 10px 0 0;
font-size: 28px;
font-weight: 500;
line-height: 32px;
}
h2 {
color: #4288ce;
font-weight: 400;
padding: 6px 0;
margin: 6px 0 0;
font-size: 18px;
border-bottom: 1px solid #eee;
}
h3 {
margin: 12px;
font-size: 16px;
font-weight: bold;
}
abbr {
cursor: help;
text-decoration: underline;
text-decoration-style: dotted;
}
a {
color: #868686;
cursor: pointer;
}
a:hover {
text-decoration: underline;
}
.line-error {
background: #f8cbcb;
}
.echo table {
width: 100%;
}
.echo pre {
padding: 16px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
background-color: #f7f7f7;
border: 0;
border-radius: 3px;
font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
}
.echo pre > pre {
padding: 0;
margin: 0;
}
/* Exception Info */
.exception {
margin-top: 20px;
}
.exception .message {
padding: 12px;
border: 1px solid #ddd;
border-bottom: 0 none;
line-height: 18px;
font-size: 16px;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
font-family: Consolas, "Liberation Mono", Courier, Verdana, "微软雅黑", serif;
}
.exception .code {
float: left;
text-align: center;
color: #fff;
margin-right: 12px;
padding: 16px;
border-radius: 4px;
background: #999;
}
.exception .source-code {
padding: 6px;
border: 1px solid #ddd;
background: #f9f9f9;
overflow-x: auto;
}
.exception .source-code pre {
margin: 0;
}
.exception .source-code pre ol {
margin: 0;
color: #4288ce;
display: inline-block;
min-width: 100%;
box-sizing: border-box;
font-size: 14px;
font-family: "Century Gothic", Consolas, "Liberation Mono", Courier, Verdana, serif;
padding-left: <?php echo (isset($source) && ! empty($source)) ? parse_padding($source): 40;?> px;
}
.exception .source-code pre li {
border-left: 1px solid #ddd;
height: 18px;
line-height: 18px;
}
.exception .source-code pre code {
color: #333;
height: 100%;
display: inline-block;
border-left: 1px solid #fff;
font-size: 14px;
font-family: Consolas, "Liberation Mono", Courier, Verdana, "微软雅黑", serif;
}
.exception .trace {
padding: 6px;
border: 1px solid #ddd;
border-top: 0 none;
line-height: 16px;
font-size: 14px;
font-family: Consolas, "Liberation Mono", Courier, Verdana, "微软雅黑", serif;
}
.exception .trace h2:hover {
text-decoration: underline;
cursor: pointer;
}
.exception .trace ol {
margin: 12px;
}
.exception .trace ol li {
padding: 2px 4px;
}
.exception div:last-child {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
/* Exception Variables */
.exception-var table {
width: 100%;
margin: 12px 0;
box-sizing: border-box;
table-layout: fixed;
word-wrap: break-word;
}
.exception-var table caption {
text-align: left;
font-size: 16px;
font-weight: bold;
padding: 6px 0;
}
.exception-var table caption small {
font-weight: 300;
display: inline-block;
margin-left: 10px;
color: #ccc;
}
.exception-var table tbody {
font-size: 13px;
font-family: Consolas, "Liberation Mono", Courier, "微软雅黑", serif;
}
.exception-var table td {
padding: 0 6px;
vertical-align: top;
word-break: break-all;
}
.exception-var table td:first-child {
width: 28%;
font-weight: bold;
white-space: nowrap;
}
.exception-var table td pre {
margin: 0;
}
/* Copyright Info */
.copyright {
margin-top: 24px;
padding: 12px 0;
border-top: 1px solid #eee;
}
/* SPAN elements with the classes below are added by prettyprint. */
pre.prettyprint .pln {
color: #000
}
/* plain text */
pre.prettyprint .str {
color: #080
}
/* string content */
pre.prettyprint .kwd {
color: #008
}
/* a keyword */
pre.prettyprint .com {
color: #800
}
/* a comment */
pre.prettyprint .typ {
color: #606
}
/* a type name */
pre.prettyprint .lit {
color: #066
}
/* a literal value */
/* punctuation, lisp open bracket, lisp close bracket */
pre.prettyprint .pun, pre.prettyprint .opn, pre.prettyprint .clo {
color: #660
}
pre.prettyprint .tag {
color: #008
}
/* a markup tag name */
pre.prettyprint .atn {
color: #606
}
/* a markup attribute name */
pre.prettyprint .atv {
color: #080
}
/* a markup attribute value */
pre.prettyprint .dec, pre.prettyprint .var {
color: #606
}
/* a declaration; a variable name */
pre.prettyprint .fun {
color: red
}
/* a function name */
</style>
</head>
<body>
<?php if (\think\facade\App::isDebug()) { ?>
<?php foreach ($traces as $index => $trace) { ?>
<div class="exception">
<div class="message">
<div class="info">
<div>
<h2><?php echo "#{$index} [{$trace['code']}]" . sprintf('%s in %s', parse_class($trace['name']), parse_file($trace['file'], $trace['line'])); ?></h2>
</div>
<div><h1><?php echo nl2br(htmlentities($trace['message'])); ?></h1></div>
</div>
</div>
<?php if (!empty($trace['source'])) { ?>
<div class="source-code">
<pre class="prettyprint lang-php">
<ol start="<?php echo $trace['source']['first']; ?>"><!--<?php foreach ((array)$trace['source']['source'] as $key => $value) { ?>--><li class="line-<?php echo " {$index}-" . ($key + $trace['source']['first']) . ($trace['line'] === $key + $trace['source']['first'] ? ' line-error' : ''); ?>"><code><?php echo htmlentities($value); ?></code></li><!--<?php } ?>--></ol>
</pre>
</div>
<?php } ?>
<div class="trace">
<h2 data-expand="<?php echo 0 === $index ? '1' : '0'; ?>">Call Stack</h2>
<ol>
<li><?php echo sprintf('in %s', parse_file($trace['file'], $trace['line'])); ?></li>
<?php foreach ((array)$trace['trace'] as $value) { ?>
<li>
<?php
// Show Function
if ($value['function']) {
echo sprintf('at %s%s%s(%s)', isset($value['class']) ? parse_class($value['class']) : '', $value['type'] ?? '', $value['function'], isset($value['args']) ? parse_args($value['args']) : '');
}
// Show line
if (isset($value['file']) && isset($value['line'])) {
echo sprintf(' in %s', parse_file($value['file'], $value['line']));
}
?>
</li>
<?php } ?>
</ol>
</div>
</div>
<?php } ?>
<?php } else { ?>
<div class="exception">
<div class="info"><h1><?php echo htmlentities(isset($message) ? $message : ''); ?></h1></div>
</div>
<?php } ?>
<?php if (!empty($datas)) { ?>
<div class="exception-var">
<h2>Exception Datas</h2>
<?php foreach ((array)$datas as $label => $value) { ?>
<table>
<?php if (empty($value)) { ?>
<caption><?php echo $label; ?><small>empty</small></caption>
<?php } else { ?>
<caption><?php echo $label; ?></caption>
<tbody>
<?php foreach ((array)$value as $key => $val) { ?>
<tr>
<td><?php echo htmlentities($key); ?></td>
<td><?php echo_value($val); ?></td>
</tr>
<?php } ?>
</tbody>
<?php } ?>
</table>
<?php } ?>
</div>
<?php } ?>
<?php if (!empty($tables)) { ?>
<div class="exception-var">
<h2>Environment Variables</h2>
<?php foreach ((array)$tables as $label => $value) { ?>
<table>
<?php if (empty($value)) { ?>
<caption><?php echo $label; ?><small>empty</small></caption>
<?php } else { ?>
<caption><?php echo $label; ?></caption>
<tbody>
<?php foreach ((array)$value as $key => $val) { ?>
<tr>
<td><?php echo htmlentities($key); ?></td>
<td><?php echo_value($val); ?></td>
</tr>
<?php } ?>
</tbody>
<?php } ?>
</table>
<?php } ?>
</div>
<?php } ?>
<?php if (\think\facade\App::isDebug()) { ?>
<script>
function $(selector, node) {
var elements;
node = node || document;
if (document.querySelectorAll) {
elements = node.querySelectorAll(selector);
} else {
switch (selector.substr(0, 1)) {
case '#':
elements = [node.getElementById(selector.substr(1))];
break;
case '.':
if (document.getElementsByClassName) {
elements = node.getElementsByClassName(selector.substr(1));
} else {
elements = get_elements_by_class(selector.substr(1), node);
}
break;
default:
elements = node.getElementsByTagName();
}
}
return elements;
function get_elements_by_class(search_class, node, tag) {
var elements = [], eles,
pattern = new RegExp('(^|\\s)' + search_class + '(\\s|$)');
node = node || document;
tag = tag || '*';
eles = node.getElementsByTagName(tag);
for (var i = 0; i < eles.length; i++) {
if (pattern.test(eles[i].className)) {
elements.push(eles[i])
}
}
return elements;
}
}
$.getScript = function (src, func) {
var script = document.createElement('script');
script.async = 'async';
script.src = src;
script.onload = func || function () {
};
$('head')[0].appendChild(script);
}
;(function () {
var files = $('.toggle');
var ol = $('ol', $('.prettyprint')[0]);
var li = $('li', ol[0]);
// 短路径和长路径变换
for (var i = 0; i < files.length; i++) {
files[i].ondblclick = function () {
var title = this.title;
this.title = this.innerHTML;
this.innerHTML = title;
}
}
(function () {
var expand = function (dom, expand) {
var ol = $('ol', dom.parentNode)[0];
expand = undefined === expand ? dom.attributes['data-expand'].value === '0' : undefined;
if (expand) {
dom.attributes['data-expand'].value = '1';
ol.style.display = 'none';
dom.innerText = 'Call Stack (展开)';
} else {
dom.attributes['data-expand'].value = '0';
ol.style.display = 'block';
dom.innerText = 'Call Stack (折叠)';
}
};
var traces = $('.trace');
for (var i = 0; i < traces.length; i++) {
var h2 = $('h2', traces[i])[0];
expand(h2);
h2.onclick = function () {
expand(this);
};
}
})();
$.getScript('//cdn.bootcss.com/prettify/r298/prettify.min.js', function () {
prettyPrint();
});
})();
</script>
<?php } ?>
</body>
</html>

View File

@@ -0,0 +1,40 @@
<form action="{:sysuri()}" method="post" data-auto="true" class="layui-form layui-card" data-table-id="FileTable">
<div class="layui-card-body padding-left-40">
<label class="layui-form-item relative block">
<span class="help-label"><b>{:lang('文件名称')}</b>Name</span>
<input maxlength="100" class="layui-input" name="name" value='{$vo.name|default=""}' required vali-name="文件名称" placeholder="请输入文件名称">
</label>
<label class="layui-form-item relative block">
<span class="help-label"><b>{:lang('文件大小')}</b>Size</span>
<input maxlength="100" class="layui-input layui-bg-gray" value='{$vo.size|default=0|format_bytes}' readonly>
</label>
<label class="layui-form-item relative block">
<span class="help-label"><b>{:lang('存储方式')}</b>Type</span>
<input maxlength="100" class="layui-input layui-bg-gray" value='{$types[$vo.type]??""}' readonly>
</label>
<label class="layui-form-item relative block">
<span class="help-label"><b>{:lang('文件哈希')}</b>Hash</span>
<input maxlength="100" class="layui-input layui-bg-gray" value='{$vo.hash|default=""}' readonly>
</label>
<label class="layui-form-item relative block">
<span class="help-label"><b>{:lang('文件链接')}</b>Link</span>
<input maxlength="100" class="layui-input layui-bg-gray" value='{$vo.xurl|default=""}' readonly>
</label>
</div>
<div class="hr-line-dashed"></div>
{notempty name='vo.id'}<input type='hidden' value='{$vo.id}' name='id'>{/notempty}
<div class="layui-form-item text-center">
<button class="layui-btn" type='submit'>{:lang('保存数据')}</button>
<button class="layui-btn layui-btn-danger" type='button' data-confirm="{:lang('确定要取消编辑吗?')}" data-close>{:lang('取消编辑')}</button>
</div>
</form>

View File

@@ -0,0 +1,64 @@
{extend name='table'}
{block name="button"}
<!--{if auth("distinct")}-->
<a data-table-id="FileTable" data-load='{:url("distinct")}' class='layui-btn layui-btn-sm layui-btn-primary'>{:lang('清理重复')}</a>
<!--{/if}-->
<!--{if auth("remove")}-->
<a data-confirm="{:lang('确定删除这些记录吗?')}" data-table-id="FileTable" data-action='{:url("remove")}' data-rule="id#{id}" class='layui-btn layui-btn-sm layui-btn-primary'>{:lang('批量删除')}</a>
<!--{/if}-->
{/block}
{block name="content"}
<div class="think-box-shadow">
{include file='file/index_search'}
<table id="FileTable" data-url="{:sysuri('index')}" data-target-search="form.form-search"></table>
</div>
<script>
$(function () {
$('#FileTable').layTable({
even: true, height: 'full',
sort: {field: 'id', type: 'desc'},
cols: [[
{checkbox: true, fixed: true},
{field: 'id', title: 'ID', width: 80, align: 'center', sort: true},
{field: 'name', title: '{:lang("文件名称")}', width: '12%', align: 'center'},
{field: 'hash', title: '{:lang("文件哈希")}', width: '15%', align: 'center', templet: '<div><code>{{d.hash}}</code></div>'},
{field: 'size', title: '{:lang("文件大小")}', align: 'center', width: '7%', sort: true, templet: '<div>{{-$.formatFileSize(d.size)}}</div>'},
{field: 'xext', title: '{:lang("文件后缀")}', align: 'center', width: '7%', sort: true},
{
field: 'xurl', title: '{:lang("查看文件")}', width: '7%', align: 'center', templet: function (d) {
if (typeof d.mime === 'string' && /^image\//.test(d.mime)) {
return laytpl('<div><a target="_blank" data-tips-hover data-tips-image="{{d.xurl}}"><i class="layui-icon layui-icon-picture"></i></a></div>').render(d)
}
if (typeof d.mime === 'string' && /^video\//.test(d.mime)) {
return laytpl('<div><a target="_blank" data-video-player="{{d.xurl}}" data-tips-text="{:lang(\'播放视频\')}"><i class="layui-icon layui-icon-video"></i></a></div>').render(d);
}
if (typeof d.mime === 'string' && /^audio\//.test(d.mime)) {
return laytpl('<div><a target="_blank" data-video-player="{{d.xurl}}" data-tips-text="{:lang(\'播放音频\')}"><i class="layui-icon layui-icon-headset"></i></a></div>').render(d);
}
return laytpl('<div><a target="_blank" href="{{d.xurl}}" data-tips-text="{:lang(\'查看下载\')}"><i class="layui-icon layui-icon-file"></i></a></div>').render(d);
}
},
{
field: 'isfast', title: '{:lang("上传方式")}', align: 'center', width: '8%', templet: function (d) {
return d.isfast ? '<b class="color-green">{:lang("秒传")}</b>' : '<b class="color-blue">{:lang("普通")}</b>';
}
},
{field: 'ctype', title: '{:lang("存储方式")}', align: 'center', width: '10%'},
{field: 'create_at', title: '{:lang("创建时间")}', align: 'center', width: '15%', sort: true},
{toolbar: '#toolbar', title: '{:lang("操作面板")}', align: 'center', minWidth: 150, fixed: 'right'}
]]
});
});
</script>
<script type="text/html" id="toolbar">
<!--{if auth("edit")}-->
<a class="layui-btn layui-btn-sm" data-modal="{:url('edit')}?id={{d.id}}" data-title="编辑文件信息">{:lang("编 辑")}</a>
<!--{/if}-->
<!--{if auth("remove")}-->
<a class="layui-btn layui-btn-sm layui-btn-danger" data-action="{:url('remove')}" data-value="id#{{d.id}}">{:lang("删 除")}</a>
<!--{/if}-->
</script>
{/block}

View File

@@ -0,0 +1,58 @@
<fieldset>
<legend>{:lang('条件搜索')}</legend>
<form class="layui-form layui-form-pane form-search" action="{:sysuri()}" onsubmit="return false" method="get" autocomplete="off">
<div class="layui-form-item layui-inline">
<label class="layui-form-label">{:lang('文件名称')}</label>
<label class="layui-input-inline">
<input name="name" value="{$get.name|default=''}" placeholder="{:lang('请输入文件名称')}" class="layui-input">
</label>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">{:lang('文件哈希')}</label>
<label class="layui-input-inline">
<input name="hash" value="{$get.hash|default=''}" placeholder="{:lang('请输入文件哈希')}" class="layui-input">
</label>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">{:lang('文件后缀')}</label>
<div class="layui-input-inline">
<select name="xext" lay-search class="layui-select">
<option value=''>-- {:lang('全部')} --</option>
{foreach $xexts as $v}{if isset($get.xext) and $k eq $get.xext}
<option selected value="{$v}">{$v}</option>
{else}
<option value="{$v}">{$v}</option>
{/if}{/foreach}
</select>
</div>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">{:lang('存储方式')}</label>
<div class="layui-input-inline">
<select name="type" lay-search class="layui-select">
<option value=''>-- {:lang('全部')} --</option>
{foreach $types as $k=>$v}{if isset($get.type) and $k eq $get.type}
<option selected value="{$k}">{$v}</option>
{else}
<option value="{$k}">{$v}</option>
{/if}{/foreach}
</select>
</div>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">{:lang('创建时间')}</label>
<div class="layui-input-inline">
<input data-date-range name="create_at" value="{$get.create_at|default=''}" placeholder="{:lang('请选择创建时间')}" class="layui-input">
</div>
</div>
<div class="layui-form-item layui-inline">
<button class="layui-btn layui-btn-primary"><i class="layui-icon">&#xe615;</i> {:lang('搜 索')}</button>
</div>
</form>
</fieldset>

32
app/admin/view/full.html Normal file
View File

@@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<title>{block name="title"}{$title|default=''}{if !empty($title)} · {/if}{:sysconf('site_name')}{/block}</title>
<meta charset="utf-8">
<meta name="renderer" content="webkit">
<meta name="format-detection" content="telephone=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=0.4">
<link rel="stylesheet" href="__ROOT__/static/plugs/layui/css/layui.css?at={:date('md')}">
<link rel="stylesheet" href="__ROOT__/static/theme/css/iconfont.css?at={:date('md')}">
<link rel="stylesheet" href="__ROOT__/static/theme/css/console.css?at={:date('md')}">
<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 src="{:url('admin/api.plugs/script',[],false,false)}"></script>
</head>
<body class="layui-layout-body">
{block name='body'}
<div class="layui-layout layui-layout-admin layui-layout-left-hide">
<div class="layui-body think-bg-white margin-0 padding-0" style="top:0">{block name='content'}{/block}</div>
</div>
{/block}
<script src="__ROOT__/static/plugs/layui/layui.js"></script>
<script src="__ROOT__/static/plugs/require/require.js"></script>
<script src="__ROOT__/static/admin.js"></script>
<script src="__ROOT__/static/extra/script.js"></script>
{block name='script'}{/block}
</body>
</html>

View File

@@ -0,0 +1,50 @@
<div class="layui-side">
<a class="layui-side-target" data-target-menu-type></a>
<a class="layui-logo layui-elip" href="{:sysuri('@')}" title="{:sysconf('app_name')}">
<span class="headimg headimg-no headimg-xs" data-lazy-src="{:sysconf('site_icon')}"></span>
<span class="headtxt">{:sysconf('app_name')} {if sysconf('app_version')}<sup>{:sysconf('app_version')}</sup>{/if}</span>
</a>
<div class="layui-side-scroll">
<div class="layui-side-icon">
{foreach $menus as $one}
<div>
<a data-menu-node="m-{$one.id}" data-open="{$one.url}" data-target-tips="{$one.title|default=''}">
{notempty name='one.icon'}<i class="{$one.icon|default=''}"></i>{/notempty}
<span>{$one.title|default=''}</span>
</a>
</div>
{/foreach}
</div>
<div class="layui-side-tree">
{foreach $menus as $one}{notempty name='one.sub'}
<ul class="layui-nav layui-nav-tree layui-hide" data-menu-layout="m-{$one.id}">
{foreach $one.sub as $two}{empty name='two.sub'}
<li class="layui-nav-item">
<a data-target-tips="{$two.title}" data-menu-node="m-{$one.id}-{$two.id}" data-open="{$two.url}">
<span class='nav-icon {$two.icon|default="layui-icon layui-icon-senior"}'></span>
<span class="nav-text">{$two.title|default=''}</span>
</a>
</li>
{else}
<li class="layui-nav-item" data-submenu-layout='m-{$one.id}-{$two.id}'>
<a data-target-tips="{$two.title}">
<span class='nav-icon layui-hide {$two.icon|default="layui-icon layui-icon-triangle-d"}'></span>
<span class="nav-text">{$two.title|default=''}</span>
</a>
<dl class="layui-nav-child">
{foreach $two.sub as $thr}
<dd>
<a data-target-tips="{$thr.title}" data-open="{$thr.url}" data-menu-node="m-{$one.id}-{$two.id}-{$thr.id}">
<span class='nav-icon {$thr.icon|default="layui-icon layui-icon-senior"}'></span>
<span class="nav-text">{$thr.title|default=''}</span>
</a>
</dd>
{/foreach}
</dl>
</li>
{/empty}{/foreach}
</ul>
{/notempty}{/foreach}
</div>
</div>
</div>

View File

@@ -0,0 +1,49 @@
<div class="layui-header">
<ul class="layui-nav layui-layout-left">
<li class="layui-nav-item" lay-unselect>
<a class="text-center" data-target-menu-type>
<i class="layui-icon layui-icon-spread-left"></i>
</a>
</li>
<li class="layui-nav-item" lay-unselect>
<a class="layui-logo-hide layui-elip" href="{:sysuri('@')}" title="{:sysconf('app_name')}">
<span class="headimg headimg-no headimg-xs" data-lazy-src="{:sysconf('site_icon')}"></span>
</a>
</li>
{foreach $menus as $one}
<li class="layui-nav-item">
<a data-menu-node="m-{$one.id}" data-open="{$one.url}"><span>{$one.title|default=''}</span></a>
</li>
{/foreach}
</ul>
<ul class="layui-nav layui-layout-right">
<li lay-unselect class="layui-nav-item"><a data-reload><i class="layui-icon layui-icon-refresh-3"></i></a></li>
{if session('user.username')}
<li class="layui-nav-item">
<dl class="layui-nav-child">
{if isset($super) and $super}
<dd lay-unselect><a data-modal="{:sysuri('admin/index/info',['id'=>session('user.id')])}"><i class="layui-icon layui-icon-set-fill"></i> {:lang('基本资料')}</a></dd>
{/if}
<dd lay-unselect><a data-modal="{:sysuri('admin/index/pass',['id'=>session('user.id')])}"><i class="layui-icon layui-icon-component"></i> {:lang('安全设置')}</a></dd>
{if isset($super) and $super}
<dd lay-unselect><a data-load="{:sysuri('admin/api.system/push')}"><i class="layui-icon layui-icon-template-1"></i> {:lang('缓存加速')}</a></dd>
<dd lay-unselect><a data-load="{:sysuri('admin/api.system/clear')}"><i class="layui-icon layui-icon-fonts-clear"></i> {:lang('清理缓存')}</a></dd>
{/if}
{if isset($super) and $super}
<dd lay-unselect><a data-width="520px" data-modal="{:sysuri('admin/index/theme')}"><i class="layui-icon layui-icon-theme"></i> {:lang('配色方案')}</a></dd>
{/if}
<dd lay-unselect><a data-load="{:sysuri('admin/login/out')}" data-confirm="{:lang('确定要退出登录吗?')}"><i class="layui-icon layui-icon-release"></i> {:lang('退出登录')}</a></dd>
</dl>
<a class="layui-elip">
<span class="headimg" data-lazy-src="{:htmlentities(session('user.headimg'))}"></span>
<span>{:htmlentities(lang(session('user.nickname')?:session('user.username')))}</span>
</a>
</li>
{else}
<li class="layui-nav-item">
<a data-href="{:sysuri('admin/login/index')}"><i class="layui-icon layui-icon-username"></i> {:lang('立即登录')}</a>
</li>
{/if}
</ul>
</div>

View File

@@ -0,0 +1,62 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<title>{block name="title"}{$title|default=''}{if !empty($title)} · {/if}{:sysconf('site_name')}{/block}</title>
<meta charset="utf-8">
<meta name="renderer" content="webkit">
<meta name="format-detection" content="telephone=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=0.4">
<link rel="stylesheet" href="__ROOT__/static/plugs/layui/css/layui.css?at={:date('md')}">
<link rel="stylesheet" href="__ROOT__/static/theme/css/iconfont.css?at={:date('md')}">
<link rel="stylesheet" href="__ROOT__/static/theme/css/console.css?at={:date('md')}">
<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 src="{:url('admin/api.plugs/script',[],false,false)}"></script>
</head>
<body class="layui-layout-body layui-layout-theme-{$theme|default='default'}">
{block name='body'}
<div class="layui-layout layui-layout-admin layui-layout-left-hide">
<!-- 左则菜单 开始 -->
{include file="index/index-left"}
<!-- 左则菜单 结束 -->
<!-- 顶部菜单 开始 -->
{include file='index/index-top'}
<!-- 顶部菜单 结束 -->
<!-- 主体内容 开始 -->
<div class="layui-body">
<div class="think-page-body">
{block name='content'}{/block}
</div>
<!-- 页面加载动画 -->
<div class="think-page-loader layui-hide">
<div class="loader"></div>
</div>
</div>
<!-- 主体内容 结束 -->
</div>
<!-- 加载动画 开始 -->
<div class="think-page-loader">
<div class="loader"></div>
</div>
<!-- 加载动画 结束 -->
{/block}
<script src="__ROOT__/static/plugs/layui/layui.js"></script>
<script src="__ROOT__/static/plugs/require/require.js"></script>
<script src="__ROOT__/static/admin.js"></script>
<script src="__ROOT__/static/extra/script.js"></script>
{block name='script'}{/block}
</body>
</html>

View File

@@ -0,0 +1,36 @@
<form action="{:sysuri()}" method="post" data-auto="true" class="layui-form layui-card" id="theme">
<div class="layui-card-body padding-left-40">
<div class="layui-form-item margin-bottom-5 label-required-prev">
<div class="help-label"><b>后台配色方案</b>Theme Style</div>
<div class="layui-textarea think-bg-gray" style="min-height:unset">
{foreach $themes as $k=>$v}
<label class="think-radio">
{if isset($theme) and $theme eq $k}
<input name="site_theme" type="radio" value="{$k}" lay-ignore checked> {$v}
{else}
<input name="site_theme" type="radio" value="{$k}" lay-ignore> {$v}
{/if}
</label>
{/foreach}
</div>
<p class="help-block">切换配色方案,需要保存成功后配色方案才会永久生效,下次登录也会有效哦 ~</p>
</div>
</div>
<div class="hr-line-dashed"></div>
<div class="layui-form-item text-center">
<button class="layui-btn" type="submit">保存配置</button>
<button class="layui-btn layui-btn-danger" type='button' data-close>取消修改</button>
</div>
</form>
<script>
$('form#theme input[name=site_theme]').on('click', function () {
var alls = '', that = this, prox = 'layui-layout-theme-', curt = prox + that.value;
$('form#theme input[name=site_theme]').map(function () {
if (this.value !== that.value) alls += ' ' + prox + this.value;
});
$('.layui-layout-body').removeClass(alls).addClass(curt)
});
</script>

View File

@@ -0,0 +1,57 @@
{extend name="index/index"}
{block name='style'}
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1">
<script>if (location.href.indexOf('#') > -1) location.replace(location.href.split('#')[0])</script>
<link rel="stylesheet" href="__ROOT__/static/theme/css/login.css">
{/block}
{block name="body"}
<div class="login-container" {$loginStyle|raw}>
<div class="header notselect layui-hide-xs">
<a href="{:url('@')}" class="title">{:sysconf('app_name')}<span>{:sysconf('app_version')}</span></a>
{notempty name='runtimeMode'}
<a class="pull-right layui-anim layui-anim-fadein" target="_blank" href='https://gitee.com/zoujingli/ThinkAdmin'>
<img src='https://gitee.com/zoujingli/ThinkAdmin/widgets/widget_1.svg' alt='Fork me on Gitee'>
</a>
{/notempty}
</div>
<form data-login-form onsubmit="return false" method="post" class="layui-anim layui-anim-upbit" autocomplete="off">
<h2 class="notselect">{:sysconf('login_name')?:'系统管理'}</h2>
<ul>
<li class="username">
<label class="label-required-null">
<i class="layui-icon layui-icon-username"></i>
<input class="layui-input" required pattern="^\S{4,}$" vali-name="登录账号" name="username" autofocus autocomplete="off" placeholder="登录账号">
</label>
</li>
<li class="password">
<label class="label-required-null">
<i class="layui-icon layui-icon-password"></i>
<input class="layui-input" required pattern="^\S{4,}$" vali-name="登录密码" name="password" maxlength="32" type="password" autocomplete="off" placeholder="登录密码" lay-affix="eye">
</label>
</li>
<li class="verify layui-hide">
<label class="inline-block relative label-required-null">
<i class="layui-icon layui-icon-picture-fine"></i>
<input class="layui-input" required pattern="^\S{4,}$" name="verify" maxlength="4" autocomplete="off" vali-name="验证码" placeholder="验证码">
</label>
<label data-captcha="{:url('admin/login/captcha',[],false)}" data-field-verify="verify" data-field-uniqid="uniqid" data-captcha-type="{$captchaType}" data-captcha-token="{$captchaToken}"></label>
</li>
<li class="text-center padding-top-20">
<button type="submit" class="layui-btn layui-disabled full-width" data-form-loaded="立即登入">正在载入</button>
</li>
</ul>
</form>
<div class="footer notselect">
<p class="layui-hide-xs">推荐使用 <a target="_blank" href="https://www.google.cn/chrome">Google Chrome</a> <a target="_blank" href="https://www.microsoft.com/zh-cn/edge#platform">Microsoft Edge</a> 浏览器访问</p>
{:sysconf('site_copy')}
{if sysconf('beian')}<span class="padding-5">|</span><a target="_blank" href="https://www.beian.gov.cn/portal/registerSystemInfo">{:sysconf('beian')}</a>{/if}
{if sysconf('miitbeian')}<span class="padding-5">|</span><a target="_blank" href="https://beian.miit.gov.cn/">{:sysconf('miitbeian')}</a>{/if}
</div>
</div>
{/block}
{block name='script'}
<script src="__ROOT__/static/login.js"></script>
{/block}

23
app/admin/view/main.html Normal file
View File

@@ -0,0 +1,23 @@
<div class="layui-card">
{block name='style'}{/block}
{block name='header'}
{notempty name='title'}
<div class="layui-card-header">
<span class="layui-icon font-s10 color-desc margin-right-5">&#xe65b;</span>{$title|lang}
<div class="pull-right">{block name='button'}{/block}</div>
</div>
{/notempty}
{/block}
<div class="layui-card-line"></div>
<div class="layui-card-body">
<div class="layui-card-html">
{notempty name='showErrorMessage'}
<div class="think-box-notify" type="error">
<b>{:lang('系统提示:')}</b><span>{$showErrorMessage|raw}</span>
</div>
{/notempty}
{block name='content'}{/block}
</div>
</div>
{block name='script'}{/block}
</div>

View File

@@ -0,0 +1,97 @@
<form action="{:sysuri()}" method="post" data-auto="true" class="layui-form layui-card" data-table-id="MenuTable">
<div class="layui-card-body">
<div class="layui-form-item">
<label class="layui-form-label label-required-next">{:lang('上级菜单')}</label>
<div class="layui-input-block">
<select name='pid' class='layui-select' lay-search>
{foreach $menus as $menu}{eq name='menu.id' value='$vo.pid|default=0'}
<option selected value='{$menu.id}'>{$menu.spl|raw}{$menu.title}</option>
{else}
<option value='{$menu.id}'>{$menu.spl|raw}{$menu.title}</option>
{/eq}{/foreach}
</select>
<p class="help-block"><b>必选</b>,请选择上级菜单或顶级菜单 ( 目前最多支持三级菜单 )</p>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">{:lang('菜单名称')}</label>
<div class="layui-input-block">
<input name="title" value='{$vo.title|default=""}' required vali-name="菜单名称" placeholder="请输入菜单名称" class="layui-input">
<p class="help-block"><b>必选</b>,请填写菜单名称 ( 如:系统管理 ),建议字符不要太长,一般 4-6 个汉字</p>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">{:lang('菜单链接')}</label>
<div class="layui-input-block">
<input onblur="this.value=this.value === ''?'#':this.value" name="url" required vali-name="菜单链接" placeholder="请输入菜单链接" value="{$vo.url|default='#'}" class="layui-input">
<p class="help-block">
<b>必选</b>,请填写链接地址或选择系统节点 ( https://domain.com/admin/user/index.html admin/user/index )
<br>当填写链接地址时,以下面的 “权限节点” 来判断菜单自动隐藏或显示,注意未填写 “权限节点” 时将不会隐藏该菜单哦
</p>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">{:lang('链接参数')}</label>
<div class="layui-input-block">
<input name="params" placeholder="请输入链接参数" value="{$vo.params|default=''}" class="layui-input">
<p class="help-block"><b>可选</b>,设置菜单链接的 GET 访问参数 ( name=1&age=3 )</p>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">{:lang('权限节点')}</label>
<div class="layui-input-block">
<input name="node" placeholder="请输入权限节点" value="{$vo.node|default=''}" class="layui-input">
<p class="help-block"><b>可选</b>,请填写系统权限节点 ( admin/user/index ),未填写时默认解释"菜单链接"判断是否拥有访问权限;</p>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">{:lang('菜单图标')}</label>
<div class="layui-input-block">
<div class="layui-input-inline">
<input placeholder="请输入或选择图标" name="icon" value='{$vo.icon|default=""}' class="layui-input">
</div>
<span style="padding:0 12px;min-width:45px" class='layui-btn layui-btn-primary'>
<i style="font-size:1.2em;margin:0;float:none" class='{$vo.icon|default=""}'></i>
</span>
<button data-icon='icon' type='button' class='layui-btn layui-btn-primary'>{:lang('选择图标')}</button>
<p class="help-block"><b>可选</b>,设置菜单选项前置图标,目前支持 layui 字体图标及 iconfont 定制字体图标。</p>
</div>
</div>
</div>
<div class="hr-line-dashed"></div>
{notempty name='vo.id'}<input type='hidden' value='{$vo.id}' name='id'>{/notempty}
<div class="layui-form-item text-center">
<button class="layui-btn" type='submit'>{:lang('保存数据')}</button>
<button class="layui-btn layui-btn-danger" type='button' data-confirm="{:lang('确定要取消编辑吗?')}" data-close>{:lang('取消编辑')}</button>
</div>
</form>
<script>
require(['jquery.autocompleter'], function () {
$('[name="icon"]').on('change', function () {
$(this).parent().next().find('i').get(0).className = this.value
}), $('input[name=url]').autocompleter({
limit: 6, highlightMatches: true, template: '{{ label }} <span> {{ title }} </span>', callback: function (node) {
$('input[name=node]').val(node);
}, source: (function (subjects, data) {
for (var i in subjects) data.push({value: subjects[i].node, label: subjects[i].node, title: subjects[i].title});
return data;
})(JSON.parse('{$nodes|raw|json_encode}'), [])
}), $('input[name=node]').autocompleter({
limit: 5, highlightMatches: true, template: '{{ label }} <span> {{ title }} </span>', source: (function (subjects, data) {
for (var i in subjects) data.push({value: subjects[i].node, label: subjects[i].node, title: subjects[i].title});
return data;
})(JSON.parse('{$auths|raw|json_encode}'), [])
});
});
</script>

View File

@@ -0,0 +1,114 @@
{extend name='table'}
{block name="button"}
<!--{if $type eq 'index' and auth("add")}-->
<button data-modal='{:url("add")}' data-table-id="MenuTable" class='layui-btn layui-btn-sm layui-btn-primary'>{:lang('添加菜单')}</button>
<!--{/if}-->
<!--{if $type eq 'index' and auth("state")}-->
<button data-action='{:url("state")}' data-table-id="MenuTable" data-rule="id#{sps};status#0" class='layui-btn layui-btn-sm layui-btn-primary'>{:lang('禁用菜单')}</button>
<!--{/if}-->
<!--{if $type eq 'recycle' and auth("state")}-->
<button data-action='{:url("state")}' data-table-id="MenuTable" data-rule="id#{spp};status#1" class='layui-btn layui-btn-sm layui-btn-primary'>{:lang('激活菜单')}</button>
<!--{/if}-->
{/block}
{block name="content"}
<div class="layui-tab layui-tab-card">
<ul class="layui-tab-title">
{foreach ['index'=>lang('系统菜单'),'recycle'=>lang('回 收 站')] as $k=>$v}
{if isset($type) and $type eq $k}
<li class="layui-this" data-open="{:url('index')}?type={$k}">{$v}</li>
{else}
<li data-open="{:url('index')}?type={$k}">{$v}</li>
{/if}{/foreach}
</ul>
<div class="layui-tab-content">
<table id="MenuTable" data-url="{:request()->url()}" data-target-search="form.form-search"></table>
</div>
</div>
<script>
$(function () {
$('#MenuTable').layTable({
even: true, height: 'full', page: false,
sort: {field: 'sort desc,id', type: 'asc'},
where: {type: '{$type|default="index"}'},
filter: function (items) {
var type = this.where.type;
return items.filter(function (item) {
return !(type === 'index' && parseInt(item.status) === 0);
});
},
cols: [[
{checkbox: true, field: 'sps'},
{field: 'sort', title: '{:lang("排序权重")}', width: 100, align: 'center', templet: '#SortInputTpl'},
{field: 'icon', title: '{:lang("图 标")}', width: 80, align: 'center', templet: '<div><i class="{{d.icon}} font-s18"></i></div>'},
{field: 'title', title: '{:lang("菜单名称")}', minWidth: 220, templet: '<div><span class="color-desc">{{d.spl}}</span>{{d.title}}</div>'},
{field: 'url', title: '{:lang("跳转链接")}', minWidth: 200},
{field: 'status', title: '{:lang("使用状态")}', minWidth: 120, align: 'center', templet: '#StatusSwitchTpl'},
{toolbar: '#toolbar', title: '{:lang("操作面板")}', minWidth: 150, align: 'center', fixed: 'right'},
]]
});
// 数据状态切换操作
layui.form.on('switch(StatusSwitch)', function (object) {
object.data = {status: object.elem.checked > 0 ? 1 : 0};
object.data.id = object.value.split('|')[object.data.status] || object.value;
$.form.load("{:url('state')}", object.data, 'post', function (ret) {
if (ret.code < 1) $.msg.error(ret.info, 3, function () {
$('#MenuTable').trigger('reload');
}); else {
$('#MenuTable').trigger('reload');
}
return false;
}, false);
});
});
</script>
<!-- 数据状态切换模板 -->
<script type="text/html" id="StatusSwitchTpl">
<!--{if auth("state")}-->
{{# if( "{$type|default='index'}"==='index' || (d.spc<1 || d.status<1)){ }}
<input type="checkbox" value="{{d.sps}}|{{d.spp}}" lay-text="{:lang('已激活')}|{:lang('已禁用')}" lay-filter="StatusSwitch" lay-skin="switch" {{-d.status>0?'checked':''}}>
{{# }else{ }}
{{-d.status ? '<b class="color-green">{:lang('已激活')}</b>' : '<b class="color-red">{:lang('已禁用')}</b>'}}
{{# } }}
<!--{else}-->
{{-d.status ? '<b class="color-green">{:lang('已激活')}</b>' : '<b class="color-red">{:lang('已禁用')}</b>'}}
<!--{/if}-->
</script>
<!-- 列表排序权重模板 -->
<script type="text/html" id="SortInputTpl">
<input type="number" min="0" data-blur-number="0" data-action-blur="{:sysuri()}" data-value="id#{{d.id}};action#sort;sort#{value}" data-loading="false" value="{{d.sort}}" class="layui-input text-center">
</script>
<!-- 操控面板的模板 -->
<script type="text/html" id="toolbar">
<!-- {if isset($type) and $type eq 'index'} -->
<!-- {if auth('add')} -->
{{# if(d.spt<2){ }}
<a class="layui-btn layui-btn-sm layui-btn-primary" data-title="添加系统菜单" data-modal='{:url("add")}?pid={{d.id}}'>{:lang('添 加')}</a>
{{# }else{ }}
<a class="layui-btn layui-btn-sm layui-btn-disabled">{:lang('添 加')}</a>
{{# } }}
<!-- {/if} -->
{if auth('edit')}
<a class="layui-btn layui-btn-sm" data-event-dbclick data-title="编辑系统菜单" data-modal='{:url("edit")}?id={{d.id}}'>{:lang('编 辑')}</a>
{/if}
<!-- {else} -->
{if auth('remove')}
{{# if( (d.spc<1 || d.status<1)){ }}
<a class="layui-btn layui-btn-sm layui-btn-danger" data-confirm="确定要删除菜单吗?" data-action="{:url('remove')}" data-value="id#{{d.sps}}"> </a>
{{# }else{ }}
<a class="layui-btn layui-btn-disabled layui-btn-sm"> </a>
{{# } }}
{/if}
<!-- {/if} -->
</script>
{/block}

View File

@@ -0,0 +1,48 @@
{extend name='table'}
{block name="button"}
<!--{if auth("remove")}-->
<button data-table-id="OplogTable" data-action='{:url("remove")}' data-rule="id#{id}" data-confirm="确定要删除选中的日志吗?" class='layui-btn layui-btn-sm layui-btn-primary'>{:lang('批量删除')}</button>
<!--{/if}-->
<!--{if auth("clear")}-->
<button data-table-id="OplogTable" data-load='{:url("clear")}' data-confirm="确定要清空所有日志吗?" class='layui-btn layui-btn-sm layui-btn-primary'>{:lang('清空数据')}</button>
<!--{/if}-->
{/block}
{block name="content"}
<div class="think-box-shadow">
{include file='oplog/index_search'}
<table id="OplogTable" data-url="{:request()->url()}" data-target-search="form.form-search"></table>
</div>
{/block}
{block name='script'}
<script>
$(function () {
$('#OplogTable').layTable({
even: true, height: 'full',
sort: {field: 'id', type: 'desc'},
cols: [[
{checkbox: true},
{field: 'id', title: 'ID', width: 80, sort: true, align: 'center'},
{field: 'username', title: '{:lang("操作账号")}', minWidth: 100, width: '8%', sort: true, align: 'center'},
{field: 'node', title: '{:lang("操作节点")}', minWidth: 120},
{field: 'action', title: '{:lang("操作行为")}', minWidth: 120},
{field: 'content', title: '{:lang("操作内容")}', minWidth: 150},
{field: 'request_content', title: '{:lang("请求内容")}', minWidth: 150},
{field: 'geoip', title: '{:lang("访问地址")}', minWidth: 100, width: '10%'},
{field: 'geoisp', title: '{:lang("网络服务商")}', minWidth: 100},
{field: 'create_at', title: '{:lang("创建时间")}', minWidth: 170, align: 'center', sort: true},
{toolbar: '#toolbar', title: '{:lang("操作面板")}', align: 'center', minWidth: 80, width: '8%', fixed: 'right'}
]]
});
});
</script>
<script type="text/html" id="toolbar">
<!--{if auth('remove')}-->
<a data-action='{:url("remove")}' data-value="id#{{d.id}}" data-confirm="确认要删除这条记录吗?" class="layui-btn layui-btn-sm layui-btn-danger">{:lang('删 除')}</a>
<!--{/if}-->
</script>
{/block}

View File

@@ -0,0 +1,87 @@
<fieldset>
<legend>{:lang('条件搜索')}</legend>
<form class="layui-form layui-form-pane form-search" action="{:sysuri()}" onsubmit="return false" method="get" autocomplete="off">
<div class="layui-form-item layui-inline">
<label class="layui-form-label">{:lang('操作账号')}</label>
<div class="layui-input-inline">
<select name='username' lay-search class="layui-select">
<option value=''>-- {:lang('全部')} --</option>
{foreach $users as $user}{if $user eq input('get.username')}
<option selected value="{$user}">{$user}</option>
{else}
<option value="{$user}">{$user}</option>
{/if}{/foreach}
</select>
</div>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">{:lang('操作行为')}</label>
<div class="layui-input-inline">
<select name="action" lay-search class="layui-select">
<option value=''>-- {:lang('全部')} --</option>
{foreach $actions as $action}{if $action eq input('get.action')}
<option selected value="{$action}">{$action}</option>
{else}
<option value="{$action}">{$action}</option>
{/if}{/foreach}
</select>
</div>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">{:lang('操作节点')}</label>
<label class="layui-input-inline">
<input name="node" value="{$get.node|default=''}" placeholder="{:lang('请输入操作节点')}" class="layui-input">
</label>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">{:lang('操作内容')}</label>
<label class="layui-input-inline">
<input name="content" value="{$get.content|default=''}" placeholder="{:lang('请输入操作内容')}" class="layui-input">
</label>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">{:lang('访问地址')}</label>
<label class="layui-input-inline">
<input name="geoip" value="{$get.geoip|default=''}" placeholder="{:lang('请输入访问地址')}" class="layui-input">
</label>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">{:lang('创建时间')}</label>
<label class="layui-input-inline">
<input data-date-range name="create_at" value="{$get.create_at|default=''}" placeholder="{:lang('请选择创建时间')}" class="layui-input">
</label>
</div>
<div class="layui-form-item layui-inline">
<button type="submit" class="layui-btn layui-btn-primary"><i class="layui-icon">&#xe615;</i> {:lang('搜 索')}</button>
<button type="button" data-form-export="{:url('index')}?type={$type|default=''}" class="layui-btn layui-btn-primary">
<i class="layui-icon layui-icon-export"></i> {:lang('导 出')}
</button>
</div>
</form>
</fieldset>
<script>
require(['excel'], function (excel) {
excel.bind(function (data) {
// 设置表格内容
data.forEach(function (item, index) {
data[index] = [item.id, item.username, item.node, item.geoip, item.geoisp, item.action, item.content, item.create_at];
});
// 设置表头内容
data.unshift(['ID', '{:lang("操作账号")}', '{:lang("操作节点")}', '{:lang("访问地址")}', '{:lang("网络服务商")}', '{:lang("操作行为")}', '{:lang("操作内容")}', '{:lang("创建时间")}']);
// 应用表格样式
return this.withStyle(data, {A: 60, B: 80, C: 99, E: 120, G: 120});
}, '{:lang("操作日志")}' + layui.util.toDateString(Date.now(), '_yyyyMMdd_HHmmss'));
});
</script>

View File

@@ -0,0 +1,131 @@
{extend name='table'}
{block name="button"}
{if isset($super) and $super}
<a data-table-id="QueueTable" class="layui-btn layui-btn-sm layui-btn-primary" data-queue="{:url('admin/api.plugs/optimize')}">{:lang('优化数据库')}</a>
{if isset($iswin) and ($iswin or php_sapi_name() eq 'cli')}
<button data-load='{:url("admin/api.queue/start")}' class='layui-btn layui-btn-sm layui-btn-primary'>{:lang('开启服务')}</button>
<button data-load='{:url("admin/api.queue/stop")}' class='layui-btn layui-btn-sm layui-btn-primary'>{:lang('关闭服务')}</button>
{/if}
{if auth("clean")}
<button data-table-id="QueueTable" data-queue='{:url("clean")}' class='layui-btn layui-btn-sm layui-btn-primary'>{:lang('定时清理')}</button>
{/if}
{/if}
{if auth("remove")}
<button data-table-id="QueueTable" data-action='{:url("remove")}' data-rule="id#{id}" data-confirm="{:lang('确定批量删除记录吗?')}" class='layui-btn layui-btn-sm layui-btn-primary'>{:lang('批量删除')}</button>
{/if}
{/block}
{block name="content"}
<div class="think-box-notify" type="info">
<!--{if isset($super) and $super}-->
<b>{:lang('服务状态')}</b><b class="margin-right-5" data-queue-message><span class="color-desc">{:lang('检查中')}</span></b>
<b data-tips-text="{:lang('点击可复制【服务启动指令】')}" class="layui-icon pointer margin-right-20" data-copy="{$command|default=''}">&#xe633;</b>
<script>$('[data-queue-message]').load('{:sysuri("admin/api.queue/status")}');</script>
<!--{/if}-->
<b>{:lang('任务统计')}</b>{:lang('待处理 %s 个任务,处理中 %s 个任务,已完成 %s 个任务,已失败 %s 个任务。', [
'<b class="color-text" data-extra="pre">..</b>',
'<b class="color-blue" data-extra="dos">..</b>',
'<b class="color-green" data-extra="oks">..</b>',
'<b class="color-red" data-extra="ers">..</b>'
])}
</div>
<div class="think-box-shadow">
{include file='queue/index_search'}
<table id="QueueTable" data-line="2" data-url="{:request()->url()}" data-target-search="form.form-search"></table>
</div>
{/block}
{block name='script'}
<script>
$(function () {
$('#QueueTable').layTable({
even: true, height: 'full',
sort: {field: 'loops_time desc,id', type: 'desc'},
// 扩展数据处理,需要返回原 items 数据
filter: function (items, result) {
return result && result.extra && $('[data-extra]').map(function () {
this.innerHTML = result.extra[this.dataset.extra] || 0;
}), items;
},
cols: [[
{checkbox: true, fixed: 'left'},
{
field: 'id', title: '{:lang("任务名称")}', width: '25%', sort: true, templet: function (d) {
if (d.loops_time > 0) {
d.one = '<span class="layui-badge think-bg-blue">循</span>';
} else {
d.one = '<span class="layui-badge think-bg-red">次</span>';
}
if (parseInt(d.rscript) === 1) {
d.two = '<span class="layui-badge layui-bg-green">复</span>';
} else {
d.two = '<span class="layui-badge think-bg-violet">单</span>';
}
return laytpl('{{-d.one}}任务编号:<b>{{d.code}}</b><br>{{-d.two}}任务名称:{{d.title}}').render(d);
}
},
{
field: 'exec_time', title: '{:lang("任务计划")}', width: '25%', templet: function (d) {
d.html = '执行指令:' + d.command + '<br>计划执行:' + d.exec_time;
if (d.loops_time > 0) {
return d.html + ' ( 每 <b class="color-blue">' + d.loops_time + '</b> 秒 ) ';
} else {
return d.html + ' <span class="color-desc">( 单次任务 )</span> ';
}
}
},
{
field: 'loops_time', title: '{:lang("任务状态")}', width: '30%', templet: function (d) {
d.html = ([
'<span class="pull-left layui-badge layui-badge-middle layui-bg-gray">未知</span>',
'<span class="pull-left layui-badge layui-badge-middle layui-bg-black">等待</span>',
'<span class="pull-left layui-badge layui-badge-middle layui-bg-blue">执行</span>',
'<span class="pull-left layui-badge layui-badge-middle layui-bg-green">完成</span>',
'<span class="pull-left layui-badge layui-badge-middle layui-bg-red">失败</span>',
][d.status] || '') + '执行时间:';
d.enter_time = d.enter_time || '';
d.outer_time = d.outer_time || '0.0000';
if (d.enter_time.length > 12) {
d.html += d.enter_time.substring(12) + '<span class="color-desc"> ( ' + d.outer_time + ' ) </span>';
d.html += ' 已执行 <b class="color-blue">' + (d.attempts || 0) + '</b> 次';
} else {
d.html += '<span class="color-desc">任务未执行</span>'
}
return d.html + '<br>执行结果:<span class="color-blue">' + (d.exec_desc || '<span class="color-desc">未获取到执行结果</span>') + '</span>';
}
},
{toolbar: '#toolbar', title: '{:lang("操作面板")}', align: 'center', minWidth: 210, fixed: 'right'}
]]
});
});
</script>
<script type="text/html" id="toolbar">
<!--{if auth('redo')}-->
{{# if(d.status===4||d.status===3){ }}
<a class="layui-btn layui-btn-sm" data-confirm="确定要重置该任务吗?" data-queue="{:url('redo')}?code={{d.code}}">{:lang('重 置')}</a>
{{# }else{ }}
<a class="layui-btn layui-btn-sm layui-btn-disabled">{:lang('重 置')}</a>
{{# } }}
<!--{/if}-->
<!--{if auth('remove')}-->
<a class='layui-btn layui-btn-sm layui-btn-danger' data-confirm="{:lang('确定要删除该记录吗?')}" data-action='{:url("remove")}' data-value="id#{{d.id}}">{:lang('删 除')}</a>
<!--{/if}-->
<a class='layui-btn layui-btn-sm layui-btn-normal' onclick="$.loadQueue('{{d.code}}',false,this)">{:lang('日 志')}</a>
</script>
{/block}

View File

@@ -0,0 +1,45 @@
<fieldset>
<legend>{:lang('条件搜索')}</legend>
<form class="layui-form layui-form-pane form-search" action="{:sysuri()}" onsubmit="return false" method="get" autocomplete="off">
<div class="layui-form-item layui-inline">
<label class="layui-form-label">{:lang('编号名称')}</label>
<label class="layui-input-inline">
<input name="title" value="{$get.title|default=''}" placeholder="{:lang('请输入名称或编号')}" class="layui-input">
</label>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">{:lang('任务指令')}</label>
<label class="layui-input-inline">
<input name="command" value="{$get.command|default=''}" placeholder="{:lang('请输入任务指令')}" class="layui-input">
</label>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">{:lang('任务状态')}</label>
<label class="layui-input-inline">
<select name="status" class="layui-select">
<option value=''>-- {:lang('全部')} --</option>
{foreach ['1'=>lang('等待处理'),'2'=>lang('正在处理'),'3'=>lang('处理完成'),'4'=>lang('处理失败')] as $k=>$v}
{if isset($get.status) and $get.status eq $k}
<option selected value="{$k}">{$v}</option>
{else}
<option value="{$k}">{$v}</option>
{/if}{/foreach}
</select>
</label>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">{:lang('计划时间')}</label>
<label class="layui-input-inline">
<input data-date-range name="exec_time" value="{$get.exec_time|default=''}" placeholder="{:lang('请选择计划时间')}" class="layui-input">
</label>
</div>
<div class="layui-form-item layui-inline">
<button class="layui-btn layui-btn-primary"><i class="layui-icon">&#xe615;</i> {:lang('搜 索')}</button>
</div>
</form>
</fieldset>

23
app/admin/view/table.html Normal file
View File

@@ -0,0 +1,23 @@
<div class="layui-card">
{block name='style'}{/block}
{block name='header'}
{notempty name='title'}
<div class="layui-card-header">
<span class="layui-icon font-s10 color-desc margin-right-5">&#xe65b;</span>{$title|lang}
<div class="pull-right">{block name='button'}{/block}</div>
</div>
{/notempty}
{/block}
<div class="layui-card-line"></div>
<div class="layui-card-body">
<div class="layui-card-table">
{notempty name='showErrorMessage'}
<div class="think-box-notify" type="error">
<b>{:lang('系统提示:')}</b><span>{$showErrorMessage|raw}</span>
</div>
{/notempty}
{block name='content'}{/block}
</div>
</div>
{block name='script'}{/block}
</div>

View File

@@ -0,0 +1,114 @@
<form action="{:sysuri()}" method="post" data-auto="true" class="layui-form layui-card" data-table-id="UserTable">
<div class="layui-card-body padding-left-40">
<fieldset class="layui-bg-gray">
<legend><b class="layui-badge think-bg-violet">用户账号</b></legend>
<div class="layui-row layui-col-space15">
<div class="layui-col-xs2 text-center padding-top-15">
<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">
<label class="block relative">
<span class="help-label"><b>登录账号</b>User Name</span>
{if isset($vo) and isset($vo.username)}
<input name="username" value='{$vo.username|default=""}' required readonly class="layui-input think-bg-gray">
{else}
<input name="username" value='{$vo.username|default=""}' required pattern="^.{4,}$" vali-name="登录账号" placeholder="请输入登录账号" class="layui-input">
{/if}
<span class="help-block">登录账号不能少于4位字符创建后不能再次修改.</span>
</label>
</div>
<div class="layui-col-xs5">
<label class="block relative">
<span class="help-label"><b>用户名称</b>Nick Name</span>
<input name="nickname" value='{$vo.nickname|default=""}' required vali-name="用户名称" placeholder="请输入用户名称" class="layui-input">
<span class="help-block">用于区分用户数据的用户名称,请尽量不要重复.</span>
</label>
</div>
</div>
</fieldset>
{if !empty($bases) || !empty($auths)}
<fieldset class="layui-bg-gray">
<legend><b class="layui-badge think-bg-violet">用户权限</b></legend>
{if !empty($bases)}
<div class="layui-form-item">
<div class="help-label"><b>角色身份</b>Role Identity</div>
<div class="layui-textarea help-checks">
{foreach $bases as $base}
<label class="think-checkbox">
{if isset($vo.usertype) and $vo.usertype eq $base.code}
<input type="radio" checked name="usertype" value="{$base.code}" lay-ignore>{$base.name}
{else}
<input type="radio" name="usertype" value="{$base.code}" lay-ignore>{$base.name}
{/if}
</label>
{/foreach}
</div>
</div>
{/if}
{if !empty($auths)}
<div class="layui-form-item">
<div class="help-label"><b>访问权限</b>Role Permission</div>
<div class="layui-textarea help-checks">
{if isset($vo.username) and $vo.username eq $super}
<span class="color-desc padding-left-5">超级用户拥所有访问权限,不需要配置权限。</span>
{else}{foreach $auths as $authorize}
<label class="think-checkbox">
{if in_array($authorize.id, $vo.authorize)}
<input type="checkbox" checked name="authorize[]" value="{$authorize.id}" lay-ignore>{$authorize.title}
{else}
<input type="checkbox" name="authorize[]" value="{$authorize.id}" lay-ignore>{$authorize.title}
{/if}
</label>
{/foreach}{/if}
</div>
</div>
{/if}
</fieldset>
{/if}
<fieldset class="layui-bg-gray">
<legend><b class="layui-badge think-bg-violet">用户资料</b></legend>
<div class="layui-row layui-col-space15">
<div class="layui-col-xs4">
<label class="relative block">
<span class="help-label"><b>联系邮箱</b>Contact Email</span>
<input name="contact_mail" value='{$vo.contact_mail|default=""}' pattern="email" vali-name="联系邮箱" placeholder="请输入联系电子邮箱" class="layui-input">
<span class="color-desc">可选,请填写用户常用的电子邮箱</span>
</label>
</div>
<div class="layui-col-xs4">
<label class="relative block">
<span class="help-label"><b>联系手机</b>Contact Mobile</span>
<input type="tel" maxlength="11" name="contact_phone" value='{$vo.contact_phone|default=""}' pattern="phone" vali-name="联系手机" placeholder="请输入用户联系手机" class="layui-input">
<span class="color-desc">可选,请填写用户常用的联系手机号</span>
</label>
</div>
<div class="layui-col-xs4">
<label class="relative block">
<span class="help-label"><b>联系QQ</b>Contact QQ</span>
<input name="contact_qq" maxlength="11" value='{$vo.contact_qq|default=""}' pattern="qq" vali-name="联系QQ" placeholder="请输入常用的联系QQ" class="layui-input">
<span class="color-desc">可选,请填写用户常用的联系QQ号</span>
</label>
</div>
</div>
<label class="layui-form-item block relative margin-top-10">
<span class="help-label"><b>用户描述</b>User Remark</span>
<textarea placeholder="请输入用户描述" class="layui-textarea" name="describe">{$vo.describe|default=""}</textarea>
</label>
</fieldset>
</div>
<div class="hr-line-dashed"></div>
{notempty name='vo.id'}<input type='hidden' value='{$vo.id}' name='id'>{/notempty}
<div class="layui-form-item text-center">
<button class="layui-btn" type='submit'>保存数据</button>
<button class="layui-btn layui-btn-danger" type='button' data-confirm="确定要取消编辑吗?" data-close>取消编辑</button>
</div>
</form>

View File

@@ -0,0 +1,116 @@
{extend name='table'}
{block name="button"}
{if isset($type) and $type eq 'index'}
<!--{if auth("add")}-->
<button data-modal='{:url("add")}' data-title="{:lang('添加用户')}" class='layui-btn layui-btn-sm layui-btn-primary'>{:lang('添加用户')}</button>
<!--{/if}-->
<!--{if auth("state")}-->
<a data-confirm="确定要禁用这些用户吗?" data-table-id="UserTable" data-action="{:url('state')}" data-rule="id#{id};status#0" class='layui-btn layui-btn-sm layui-btn-primary'>{:lang('批量禁用')}</a>
<!--{/if}-->
{else}
<!--{if auth("state")}-->
<a data-confirm="确定要恢复这些账号吗?" data-table-id="UserTable" data-action="{:url('state')}" data-rule="id#{id};status#1" class='layui-btn layui-btn-sm layui-btn-primary'>{:lang('批量恢复')}</a>
<!--{/if}-->
<!--{if auth("remove")}-->
<a data-confirm="确定永久删除这些账号吗?" data-table-id="UserTable" data-action='{:url("remove")}' data-rule="id#{id}" class='layui-btn layui-btn-sm layui-btn-primary'>{:lang('批量删除')}</a>
<!--{/if}-->
{/if}
{/block}
{block name="content"}
<div class="layui-tab layui-tab-card">
<ul class="layui-tab-title">
{foreach ['index'=>lang('系统用户'),'recycle'=>lang('回 收 站')] as $k=>$v}{if isset($type) and $type eq $k}
<li data-open="{:url('index')}?type={$k}" class="layui-this">{$v}</li>
{else}
<li data-open="{:url('index')}?type={$k}">{$v}</li>
{/if}{/foreach}
</ul>
<div class="layui-tab-content">
{include file='user/index_search'}
<table id="UserTable" data-url="{:sysuri('index')}" data-target-search="form.form-search"></table>
</div>
</div>
<script>
$(function () {
$('#UserTable').layTable({
even: true, height: 'full',
sort: {field: 'sort desc,id', type: 'desc'},
where: {type: '{$type|default="index"}'},
cols: [[
{checkbox: true, fixed: true},
{field: 'sort', title: '{:lang("排序权重")}', width: 100, align: 'center', sort: true, templet: '#SortInputTpl'},
{
field: 'headimg', title: '{:lang("头像")}', width: 60, align: 'center', templet: function (d) {
if (!d.headimg) return '-';
return layui.laytpl('<div class="headimg headimg-ss shadow-inset margin-0" data-tips-image data-tips-hover data-lazy-src="{{d.headimg}}"></div>').render(d);
}
},
{field: 'username', title: '{:lang("登录账号")}', minWidth: 100, align: 'center', templet: '<div>{{d.username||"-"}}</div>'},
{field: 'nickname', title: '{:lang("用户名称")}', minWidth: 100, align: 'center', templet: '<div>{{d.nickname||"-"}}</div>'},
/* {notempty name='bases'} */
{
field: 'usertype', title: '{:lang("角色身份")}', minWidth: 100, align: 'center', templet: function (d) {
d.userinfo = d.userinfo || {};
return d.userinfo.code ? (d.userinfo.name + ' ( ' + d.userinfo.code + ' ) ') : '-';
}
},
/* {/notempty} */
// {field: 'contact_mail', title: '联系邮箱', minWidth: 80, templet: '<div>{{d.contact_mail||"-"}}</div>'},
// {field: 'contact_phone', title: '联系电话', minWidth: 80, templet: '<div>{{d.contact_phone||"-"}}</div>'},
{field: 'status', title: '{:lang("使用状态")}', align: 'center', minWidth: 110, templet: '#StatusSwitchTpl'},
{field: 'login_num', title: '{:lang("登录次数")}', align: 'center', minWidth: 100, sort: true},
{field: 'login_at', title: '{:lang("最后登录")}', align: 'center', minWidth: 170, sort: true},
{field: 'create_at', title: '{:lang("创建时间")}', align: 'center', minWidth: 170, sort: true},
{toolbar: '#toolbar', title: '{:lang("操作面板")}', align: 'center', minWidth: 180, fixed: 'right'}
]]
});
// 数据状态切换操作
layui.form.on('switch(StatusSwitch)', function (obj) {
var data = {id: obj.value, status: obj.elem.checked > 0 ? 1 : 0};
$.form.load("{:url('state')}", data, 'post', function (ret) {
if (ret.code < 1) $.msg.error(ret.info, 3, function () {
$('#UserTable').trigger('reload');
}); else {
$('#UserTable').trigger('reload')
}
return false;
}, false);
});
});
</script>
<!-- 数据状态切换模板 -->
<script type="text/html" id="StatusSwitchTpl">
<!--{if auth("state")}-->
<input type="checkbox" value="{{d.id}}" lay-skin="switch" lay-text="{:lang('已激活')}|{:lang('已禁用')}" lay-filter="StatusSwitch" {{-d.status>0?'checked':''}}>
<!--{else}-->
{{-d.status ? '<b class="color-green">{:lang("已激活")}</b>' : '<b class="color-red">{:lang("已禁用")}</b>'}}
<!--{/if}-->
</script>
<!-- 列表排序权重模板 -->
<script type="text/html" id="SortInputTpl">
<input type="number" min="0" data-blur-number="0" data-action-blur="{:sysuri()}" data-value="id#{{d.id}};action#sort;sort#{value}" data-loading="false" value="{{d.sort}}" class="layui-input text-center">
</script>
<script type="text/html" id="toolbar">
{if isset($type) and $type eq 'index'}
<!--{if auth("edit")}-->
<a class="layui-btn layui-btn-sm" data-event-dbclick data-title="{:lang('编辑用户')}" data-modal='{:url("edit")}?id={{d.id}}'>{:lang('编 辑')}</a>
<!--{/if}-->
<!--{if auth("pass")}-->
<a class="layui-btn layui-btn-sm layui-btn-normal" data-title="{:lang('设置密码')}" data-modal='{:url("pass")}?id={{d.id}}'>{:lang('密 码')}</a>
<!--{/if}-->
{else}
<!--{if auth("edit")}-->
<a class="layui-btn layui-btn-sm" data-event-dbclick data-title="{:lang('编辑用户')}" data-modal='{:url("edit")}?id={{d.id}}'>{:lang('编 辑')}</a>
<!--{/if}-->
<!--{if auth("remove")}-->
<a class="layui-btn layui-btn-sm layui-btn-danger" data-confirm="{:lang('确定要永久删除吗?')}" data-action="{:url('remove')}" data-value="id#{{d.id}}">{:lang('删 除')}</a>
<!--{/if}-->
{/if}
</script>
{/block}

View File

@@ -0,0 +1,44 @@
<form class="layui-form layui-form-pane form-search" action="{:sysuri()}" onsubmit="return false" method="get" autocomplete="off">
<div class="layui-form-item layui-inline">
<label class="layui-form-label">{:lang('账号名称')}</label>
<label class="layui-input-inline">
<input name="username" value="{$get.username|default=''}" placeholder="{:lang('请输入账号或名称')}" class="layui-input">
</label>
</div>
<!--{notempty name='bases'}-->
<div class="layui-form-item layui-inline">
<label class="layui-form-label">{:lang('角色身份')}</label>
<div class="layui-input-inline">
<select name="usertype" lay-search class="layui-select">
<option value=''>-- {:lang('全部')} --</option>
{foreach $bases as $base}{if $base.code eq input('get.usertype')}
<option selected value="{$base.code|default=''}">{$base.name|default=''} ( {$base.code|default=''} )</option>
{else}
<option value="{$base.code|default=''}">{$base.name|default=''} ( {$base.code|default=''} )</option>
{/if}{/foreach}
</select>
</div>
</div>
<!--{/notempty}-->
<div class="layui-form-item layui-inline">
<label class="layui-form-label">{:lang('最后登录')}</label>
<div class="layui-input-inline">
<input data-date-range name="login_at" value="{$get.login_at|default=''}" placeholder="{:lang('请选择登录时间')}" class="layui-input">
</div>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">{:lang('创建时间')}</label>
<div class="layui-input-inline">
<input data-date-range name="create_at" value="{$get.create_at|default=''}" placeholder="{:lang('请选择创建时间')}" class="layui-input">
</div>
</div>
<div class="layui-form-item layui-inline">
<input type="hidden" name="type" value="{$type|default='index'}">
<button class="layui-btn layui-btn-primary"><i class="layui-icon">&#xe615;</i> {:lang('搜 索')}</button>
</div>
</form>

View File

@@ -0,0 +1,40 @@
<form action="{:sysuri()}" method="post" data-auto="true" class="layui-form layui-card" data-table-id="UserTable">
<div class="layui-card-body padding-left-40">
<label class="layui-form-item relative block">
<span class="help-label"><b>登录用户账号</b>Username</span>
<input disabled value='{$vo.username|default=""}' class="layui-input think-bg-gray">
<span class="help-block">登录用户账号创建后,不允许再次修改。</span>
</label>
<!--{if $verify}-->
<label class="layui-form-item relative block">
<span class="help-label"><b>旧的登录密码</b>Old Password</span>
<input type="password" autofocus name="oldpassword" value='' pattern="^\S{1,}$" required vali-name="验证密码" placeholder="请输入旧的登录密码" class="layui-input">
<span class="color-desc">请输入旧密码来验证修改权限,旧密码不限制格式。</span>
</label>
<!--{/if}-->
<label class="layui-form-item relative block">
<span class="help-label"><b>新的登录密码</b>New Password</span>
<input type="password" name="password" maxlength="32" pattern="^(?![\d]+$)(?![a-zA-Z]+$)(?![^\da-zA-Z]+$).{6,32}$" required vali-name="登录密码" placeholder="请输入新的登录密码" class="layui-input">
<span class="color-desc">密码必须包含大小写字母、数字、符号的任意两者组合。</span>
</label>
<label class="layui-form-item relative block">
<span class="help-label"><b>重复登录密码</b>Repeat Password</span>
<input type="password" name="repassword" maxlength="32" pattern="^(?![\d]+$)(?![a-zA-Z]+$)(?![^\da-zA-Z]+$).{6,32}$" required vali-name="重复密码" placeholder="请重复输入登录密码" class="layui-input">
<span class="color-desc">密码必须包含大小写字母、数字、符号的任意两者组合。</span>
</label>
</div>
<div class="hr-line-dashed"></div>
{notempty name='vo.id'}<input type='hidden' value='{$vo.id}' name='id'>{/notempty}
<div class="layui-form-item text-center">
<button class="layui-btn" type='submit'>保存数据</button>
<button class="layui-btn layui-btn-danger" type='button' data-confirm="确定要取消编辑吗?" data-close>取消编辑</button>
</div>
</form>

View File

@@ -0,0 +1,27 @@
<?php
// +----------------------------------------------------------------------
// | Static Plugin for ThinkAdmin
// +----------------------------------------------------------------------
// | 版权所有 2014~2023 ThinkAdmin [ thinkadmin.top ]
// +----------------------------------------------------------------------
// | 官方网站: https://thinkadmin.top
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// | 免责声明 ( https://thinkadmin.top/disclaimer )
// +----------------------------------------------------------------------
// | gitee 代码仓库https://gitee.com/zoujingli/think-plugs-static
// | github 代码仓库https://github.com/zoujingli/think-plugs-static
// +----------------------------------------------------------------------
namespace app\index\controller;
use think\admin\Controller;
class Index extends Controller
{
public function index()
{
$this->redirect(sysuri('admin/login/index'));
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace app\manager\controller;
use think\admin\Controller;
/**
* 后台管理基础控制器
*/
class Base extends Controller
{
/**
* 初始化方法
*/
protected function initialize()
{
parent::initialize();
// 直接使用 admin 的权限验证
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace app\manager\controller;
class Error extends Base
{
public function forbidden()
{
return $this->fetch();
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace app\manager\controller;
use app\manager\sys\RedisManager;
use think\admin\Controller;
use think\admin\service\AdminService;
/**
* 后台首页控制器
*/
class Index extends Controller
{
/**
* 显示后台首页
*/
public function index()
{
$userid = AdminService::getUserId();
// 获取实例
$redis = RedisManager::getInstance();
$this->assign("userid", $userid);
$this->fetch();
}
}

View File

@@ -0,0 +1,216 @@
<?php
declare (strict_types = 1);
namespace app\manager\controller;
use app\manager\model\Member as MemberModel;
use think\admin\Controller;
use think\facade\Db;
/**
* 会员管理
* @auth true
* @menu true
*/
class Member extends Controller
{
/**
* 会员列表
* @auth true
* @menu true
*/
public function index()
{
$this->title = '会员管理';
if ($this->request->isGet() && $this->request->get('output') === 'layui.table') {
$where = [];
// 搜索条件
$keyword = input('keyword', '', 'trim');
if (!empty($keyword)) {
$where[] = ['email|order_id', 'like', "%{$keyword}%"];
}
// 状态筛选
$status = input('status', '', 'trim');
if ($status !== '') {
$where[] = ['status', '=', intval($status)];
}
// 查询数据
$query = MemberModel::where($where)->order('id desc');
$result = $query->paginate([
'list_rows' => input('limit', 20),
'page' => input('page', 1),
]);
return json([
'code' => 0,
'msg' => '',
'count' => $result->total(),
'data' => $result->items()
]);
}
return $this->fetch();
}
/**
* 添加会员
* @auth true
*/
public function add()
{
if ($this->request->isPost()) {
$data = $this->_vali([
'email.require' => '邮箱不能为空!',
'email.email' => '邮箱格式错误!',
'order_id.require' => '闲鱼订单号不能为空!',
'expire_time.require' => '有效期不能为空!',
'usage_limit.require' => '可用次数不能为空!',
'usage_limit.number' => '可用次数必须为数字!',
]);
// 检查唯一性
$map = [
['email', '=', $data['email']]
];
if (MemberModel::where($map)->count() > 0) {
$this->error('邮箱已存在!');
}
$map = [
['order_id', '=', $data['order_id']]
];
if (MemberModel::where($map)->count() > 0) {
$this->error('闲鱼订单号已存在!');
}
// 设置默认值
$data['status'] = isset($data['status']) ? 1 : 0;
$data['used_count'] = 0;
// 保存数据
if (MemberModel::create($data) !== false) {
$this->success('添加成功!');
} else {
$this->error('添加失败!');
}
}
$this->title = '添加会员';
return $this->fetch('form');
}
/**
* 编辑会员
* @auth true
*/
public function edit()
{
$id = input('id');
if (empty($id)) $this->error('参数错误!');
if ($this->request->isPost()) {
$data = $this->_vali([
'id.require' => 'ID不能为空',
'email.require' => '邮箱不能为空!',
'email.email' => '邮箱格式错误!',
'order_id.require' => '闲鱼订单号不能为空!',
'expire_time.require' => '有效期不能为空!',
'usage_limit.require' => '可用次数不能为空!',
'usage_limit.number' => '可用次数必须为数字!',
]);
// 检查唯一性
$map = [
['email', '=', $data['email']],
['id', '<>', $id]
];
if (MemberModel::where($map)->count() > 0) {
$this->error('邮箱已存在!');
}
$map = [
['order_id', '=', $data['order_id']],
['id', '<>', $id]
];
if (MemberModel::where($map)->count() > 0) {
$this->error('闲鱼订单号已存在!');
}
// 设置默认值
$data['status'] = isset($data['status']) ? 1 : 0;
$data['used_count'] = isset($data['used_count']) ? intval($data['used_count']) : 0;
// 保存数据
if (MemberModel::update($data) !== false) {
$this->success('编辑成功!');
} else {
$this->error('编辑失败!');
}
}
$this->title = '编辑会员';
$this->member = MemberModel::find($id);
return $this->fetch('form');
}
/**
* 修改状态
* @auth true
*/
public function state()
{
MemberModel::mSave($this->_vali([
'status.in:0,1' => '状态值范围异常!',
'status.require' => '状态值不能为空!',
]));
}
/**
* 删除会员
* @auth true
*/
public function remove()
{
MemberModel::mDelete();
}
/**
* 表单数据处理
* @param array $data
*/
protected function _form_filter(array &$data)
{
if ($this->request->isPost()) {
// 检查数据完整性
empty($data['username']) && $this->error('用户名不能为空!');
empty($data['order_id']) && $this->error('闲鱼订单号不能为空!');
empty($data['expire_time']) && $this->error('有效期不能为空!');
// 检查唯一性
$map = [];
if (empty($data['id'])) {
$map[] = ['username', '=', $data['username']];
$map[] = ['order_id', '=', $data['order_id']];
if (MemberModel::where($map)->count() > 0) {
$this->error('用户名或闲鱼订单号已存在!');
}
} else {
$map[] = ['id', '<>', $data['id']];
$map[] = ['username|order_id', '=', $data['username']];
if (MemberModel::where($map)->count() > 0) {
$this->error('用户名或闲鱼订单号已存在!');
}
}
// 设置默认值
$data['status'] = isset($data['status']) ? 1 : 0;
$data['used_count'] = isset($data['used_count']) ? intval($data['used_count']) : 0;
$data['balance'] = isset($data['balance']) ? floatval($data['balance']) : 0;
$data['points'] = isset($data['points']) ? intval($data['points']) : 0;
$data['level'] = isset($data['level']) ? intval($data['level']) : 1;
}
}
}

View File

@@ -0,0 +1,503 @@
<?php
declare(strict_types=1);
namespace app\manager\controller;
use think\admin\Controller;
use think\admin\service\AdminService;
/**
* 包名管理
* @auth true # 表示需要验证权限
* @menu true # 表示可以生成菜单
*/
class Package extends Controller
{
/**
* 绑定数据表
* @var string
*/
protected $table = 'offer_package';
/**
* 检查包名权限
* @param int $packageId 包名ID
* @return bool
*/
protected function checkPackageAuth($packageId)
{
// 超级管理员直接放行
if (AdminService::instance()->isSuper()) {
return true;
}
// 检查用户是否有包名管理模块的权限
if (!AdminService::instance()->check('package/index')) {
return false;
}
// 检查具体包名权限
$userId = AdminService::instance()->getUserId();
$exists = $this->app->db->name('offer_package_auth')
->where(['package_id' => $packageId, 'user_id' => $userId])
->find();
return !empty($exists);
}
/**
* 获取当前用户可访问的包名ID列表
*/
protected function getAuthPackageIds()
{
// 超级管理员可以访问所有
if (AdminService::instance()->isSuper()) {
return [];
}
// 如果没有包名管理模块权限,则不能访问任何包名
if (!AdminService::instance()->check('package/index')) {
return [-1];
}
// 获取用户被授权的包名列表
$userId = AdminService::instance()->getUserId();
$packageIds = $this->app->db->name('offer_package_auth')
->where(['user_id' => $userId])
->column('package_id');
return $packageIds ?: [-1];
}
/**
* 包名列表
* @auth true # 需要验证权限
* @menu true # 菜单可见
*/
public function index()
{
if ($this->request->isPost()) {
try {
// 获取请求参数
$page = $this->request->post('page/d', 1);
$limit = $this->request->post('limit/d', 15);
$where = [];
// 包名搜索
if ($package_name = $this->request->post('package_name/s', '')) {
$where[] = ['package_name', 'like', "%{$package_name}%"];
}
// 应用名称搜索
if ($name = $this->request->post('name/s', '')) {
$where[] = ['name', 'like', "%{$name}%"];
}
// 状态筛选
if ($status = $this->request->post('status/s', '')) {
$where[] = ['status', '=', $status];
}
// 创建人搜索
if ($username = $this->request->post('username/s', '')) {
$where[] = ['username', 'like', "%{$username}%"];
}
// 时间范围处理
if ($add_time = $this->request->post('add_time/s', '')) {
$times = explode(' - ', $add_time);
if (count($times) == 2) {
$start_time = strtotime($times[0] . ' 00:00:00');
$end_time = strtotime($times[1] . ' 23:59:59');
$where[] = ['add_time', 'between', [$start_time, $end_time]];
}
}
// 查询数据
$query = $this->app->db->name('offer_package')->alias('p');
// 非超级管理员需要进行权限过滤
if (!AdminService::instance()->isSuper()) {
// 检查包名管理模块权限
if (!AdminService::instance()->check('package/index')) {
return json([
'code' => 0,
'msg' => '',
'count' => 0,
'data' => []
]);
}
$userId = AdminService::instance()->getUserId();
// 通过左连接查询有权限的包名
$query->leftJoin('offer_package_auth pa', 'p.id = pa.package_id')
->where('pa.user_id', $userId);
}
// 先获取总数
$total = $query->where($where)->count();
// 分页查询数据
$list = $query->where($where)
->field('p.*') // 只查询包名表的字段
->order('p.id desc')
->group('p.id') // 防止重复数据
->page($page, $limit)
->select()
->toArray();
return json([
'code' => 0,
'msg' => '',
'count' => $total,
'data' => $list
]);
} catch (\Exception $e) {
trace("获取包名列表异常:" . $e->getMessage());
return json([
'code' => 1,
'msg' => '获取数据失败,请稍后重试',
'count' => 0,
'data' => []
]);
}
}
$this->fetch();
}
/**
* 添加包名
* @auth true
* @menu true
*/
public function add()
{
if ($this->request->isPost()) {
// 开启事务
$this->app->db->startTrans();
try {
$data = $this->_vali([
'package_name.require' => '包名不能为空!',
'name.require' => '应用名称不能为空!',
'status.require' => '状态不能为空!',
'status.in:0,1' => '状态值不正确!'
]);
// 检查包名是否已存在
$exists = $this->app->db->name('offer_package')
->where(['package_name' => $data['package_name']])
->find();
if ($exists) {
$this->app->db->rollback();
return json(['code' => 0, 'info' => '包名已存在,请更换!']);
}
// 添加时间字段
$now = time();
$data['add_time'] = $now;
$data['update_time'] = $now;
$data['username'] = session('user.username') ?? '';
// 插入包名数据
$result = $this->app->db->name('offer_package')->insertGetId($data);
if ($result === false) {
$this->app->db->rollback();
return json(['code' => 0, 'info' => '添加失败,请重试!']);
}
// 非超级管理员需要添加权限记录
if (!AdminService::instance()->isSuper()) {
$authData = [
'package_id' => $result,
'user_id' => AdminService::instance()->getUserId(),
'create_at' => date('Y-m-d H:i:s')
];
$authResult = $this->app->db->name('offer_package_auth')->insert($authData);
if ($authResult === false) {
$this->app->db->rollback();
return json(['code' => 0, 'info' => '添加权限记录失败!']);
}
}
$this->app->db->commit();
sysoplog('积分墙列表', '添加成功');
return json(['code' => 1, 'info' => '添加成功!']);
} catch (\Exception $e) {
$this->app->db->rollback();
trace("添加包名异常:" . $e->getMessage());
return json(['code' => 0, 'info' => '系统异常,请稍后重试!']);
}
}
}
/**
* 编辑包名
* @auth true
* @menu true
*/
public function edit()
{
if ($this->request->isPost()) {
try {
$data = $this->_vali([
'id.require' => '记录ID不能为空',
'package_name.require' => '包名不能为空!',
'name.require' => '应用名称不能为空!',
'status.require' => '状态不能为空!',
'status.in:0,1' => '状态值不正确!'
]);
// 检查权限
if (!$this->checkPackageAuth($data['id'])) {
return json(['code' => 0, 'info' => '您没有操作此包名的权限!']);
}
// 检查包名是否已存在(排除当前记录)
$exists = $this->app->db->name('offer_package')
->where('package_name', $data['package_name'])
->where('id', '<>', $data['id'])
->find();
if ($exists) {
return json(['code' => 0, 'info' => '包名已存在,请更换!']);
}
// 更新时间
$data['update_time'] = time();
$data['username'] = session('user.username') ?? '';
$result = $this->app->db->name('offer_package')
->where(['id' => $data['id']])
->update($data);
if ($result === false) {
return json(['code' => 0, 'info' => '更新失败,请重试!']);
}
sysoplog('积分墙列表', '更新成功');
return json(['code' => 1, 'info' => '更新成功!']);
} catch (\Exception $e) {
trace("编辑包名异常:" . $e->getMessage());
return json(['code' => 0, 'info' => '系统异常,请稍后重试!']);
}
}
}
/**
* 删除包名
* @auth true
* @menu true
*/
public function remove()
{
if ($this->request->isPost()) {
// 开启事务
$this->app->db->startTrans();
try {
$id = $this->request->param('id');
if (empty($id)) {
return json(['code' => 0, 'info' => '参数错误,请刷新重试!']);
}
// 检查权限
if (!$this->checkPackageAuth($id)) {
return json(['code' => 0, 'info' => '您没有操作此包名的权限!']);
}
// 删除包名记录
$result = $this->app->db->name('offer_package')
->where(['id' => $id])
->delete();
if ($result === false) {
$this->app->db->rollback();
return json(['code' => 0, 'info' => '删除失败,请重试!']);
}
// 同步删除权限表中的相关记录
$this->app->db->name('offer_package_auth')
->where(['package_id' => $id])
->delete();
$this->app->db->commit();
sysoplog('积分墙列表', '删除成功');
return json(['code' => 1, 'info' => '删除成功!']);
} catch (\Exception $e) {
$this->app->db->rollback();
trace("删除包名异常:" . $e->getMessage());
return json(['code' => 0, 'info' => '系统异常,请稍后重试!']);
}
}
}
/**
* 切换状态
* @auth true
* @menu true
*/
public function state()
{
if ($this->request->isPost()) {
try {
$data = $this->_vali([
'id.require' => '记录ID不能为空',
'status.in:0,1' => '状态值不正确!'
]);
// 检查权限
if (!$this->checkPackageAuth($data['id'])) {
return json(['code' => 0, 'info' => '您没有操作此包名的权限!']);
}
// 检查记录是否存在
$exists = $this->app->db->name('offer_package')
->where(['id' => $data['id']])
->find();
if (empty($exists)) {
return json(['code' => 0, 'info' => '记录不存在!']);
}
// 更新状态
$updateData = [
'status' => $data['status'],
'update_time' => time(),
'username' => session('user.username') ?? ''
];
$result = $this->app->db->name('offer_package')
->where(['id' => $data['id']])
->update($updateData);
if ($result === false) {
return json(['code' => 0, 'info' => '状态更新失败,请重试!']);
}
sysoplog('积分墙列表', '状态更新成功');
return json(['code' => 1, 'info' => '状态更新成功!']);
} catch (\Exception $e) {
trace("状态切换异常:" . $e->getMessage() . "\n" . $e->getTraceAsString());
return json(['code' => 0, 'info' => '系统异常,请稍后重试!']);
}
}
}
/**
* 生成测试数据
* @auth true
*/
public function generateTestData()
{
try {
// 设置执行时间为无限制
set_time_limit(0);
ini_set('memory_limit', '1024M');
// 开启事务
$this->app->db->startTrans();
try {
$insertData = [];
$existPackages = [];
$total = 8000; // 总记录数
$batchSize = 500; // 每批插入数量
$startTime = time();
// 获取已存在的包名,避免重复
$exists = $this->app->db->name($this->table)
->column('package_name');
$existPackages = array_flip($exists);
// 预生成一些常用词,提高应用名称的真实性
$prefixes = ['Super', 'My', 'Fast', 'Smart', 'Easy', 'Pro', 'Best', 'Quick'];
$suffixes = ['Browser', 'Player', 'Cleaner', 'Manager', 'Tool', 'Helper', 'App', 'Game'];
$domains = ['com', 'net', 'org', 'app', 'tech', 'io'];
// 生成记录
for ($i = 0; $i < $total; $i++) {
// 生成唯一包名
do {
// 生成更真实的包名 xxx.xxx.xxx.xxx
$domain = $domains[array_rand($domains)];
$company = $this->generateRandomString(rand(3, 8));
$product = $this->generateRandomString(rand(3, 8));
$module = $this->generateRandomString(rand(3, 8));
$packageName = "{$domain}.{$company}.{$product}.{$module}";
} while (isset($existPackages[$packageName]));
$existPackages[$packageName] = 1;
// 生成更真实的应用名称
$prefix = $prefixes[array_rand($prefixes)];
$suffix = $suffixes[array_rand($suffixes)];
$appName = $prefix . ' ' . $suffix . ' ' . ($i + 1);
// 生成数据
$insertData[] = [
'package_name' => $packageName,
'name' => $appName,
'status' => rand(0, 1), // 随机状态
'add_time' => time() - rand(0, 86400 * 30), // 随机时间最近30天内
'update_time' => time(),
'username' => session('user.username') ?? 'system'
];
// 批量插入
if (count($insertData) >= $batchSize) {
$this->app->db->name($this->table)->insertAll($insertData);
$insertData = [];
// 输出进度
$progress = round(($i + 1) / $total * 100, 2);
trace("数据生成进度:{$progress}%");
}
}
// 插入剩余数据
if (!empty($insertData)) {
$this->app->db->name($this->table)->insertAll($insertData);
}
$endTime = time();
$timeUsed = $endTime - $startTime;
$this->app->db->commit();
return json([
'code' => 1,
'info' => "测试数据生成成功!共生成 {$total} 条记录,耗时 {$timeUsed}"
]);
} catch (\Exception $e) {
$this->app->db->rollback();
throw $e;
}
} catch (\Exception $e) {
trace("生成测试数据异常:" . $e->getMessage() . "\n" . $e->getTraceAsString());
return json(['code' => 0, 'info' => '系统异常:' . $e->getMessage()]);
}
}
/**
* 生成随机字符串
* @param int $length 长度
* @return string
*/
private function generateRandomString($length = 5)
{
// 添加数字,使包名更真实
$characters = 'abcdefghijklmnopqrstuvwxyz0123456789';
$result = '';
// 第一个字符必须是字母
$result .= $characters[rand(0, 25)];
// 生成剩余字符
for ($i = 1; $i < $length; $i++) {
$result .= $characters[rand(0, strlen($characters) - 1)];
}
return $result;
}
}

View File

@@ -0,0 +1,918 @@
<?php
declare(strict_types=1);
namespace app\manager\controller;
use think\admin\Controller;
use think\admin\service\AdminService;
class PackageAuth extends Controller
{
/**
* 绑定数据表
* @var string
*/
protected $table = 'offer_package_auth';
/**
* 包名权限列表
* @auth true
* @menu true
*/
public function index()
{
if ($this->request->isGet()) {
// 获取所有管理员(排除超级管理员)
$users = $this->app->db->name('system_user')
->where([
['status', '=', 1], // 状态正常的用户
['authorize', 'not like', '%admin%'], // 排除admin权限组的用户
['id', '<>', '10000'] // 排除系统超管账号
])
->field('id,username,authorize')
->order('id asc')
->select()
->filter(function($user) {
// 只显示有包名管理权限的用户
return AdminService::instance()->check('package/index', $user['id']);
})
->toArray();
// 获取现有权限配置
$auths = $this->app->db->name($this->table)
->select()
->toArray();
// 整理权限数据
$authMap = [];
foreach ($auths as $auth) {
$authMap[$auth['user_id']][] = $auth['package_id'];
}
$this->assign([
'users' => $users,
'authMap' => $authMap
]);
$this->fetch();
}
}
/**
* 获取包名列表(分页)
* @auth true
*/
public function getPackageList()
{
if ($this->request->isPost()) {
try {
$page = $this->request->post('page/d', 1);
$limit = $this->request->post('limit/d', 20);
$keyword = $this->request->post('keyword/s', '');
$startTime = $this->request->post('start_time/s', '');
$endTime = $this->request->post('end_time/s', '');
$where = [];
// 只显示启用的包名
$where[] = ['status', '=', 1];
if ($keyword !== '') {
$where[] = ['package_name|name', 'like', "%{$keyword}%"];
}
// 修改时间筛选条件处理方式
if ($startTime && $endTime) {
$where[] = ['add_time', 'between', [
strtotime($startTime),
strtotime($endTime)
]];
}
// 使用正确的表名
$query = $this->app->db->name('offer_package');
// 先获取总数
$total = $query->where($where)->count();
// 获取列表数据
$list = $query->where($where)
->field('id,package_name,name,add_time,update_time,status') // 修改字段名
->order('id desc')
->page($page, $limit)
->select()
->toArray();
// 格式化时间戳
foreach ($list as &$item) {
$item['add_time'] = date('Y-m-d H:i:s', intval($item['add_time']));
$item['update_time'] = date('Y-m-d H:i:s', intval($item['update_time']));
}
// 记录调试信息
trace("包名列表查询:" . json_encode([
'where' => $where,
'page' => $page,
'limit' => $limit,
'total' => $total,
'list_count' => count($list),
'sql' => $query->getLastSql()
], JSON_UNESCAPED_UNICODE));
return json([
'code' => 0,
'msg' => '',
'count' => $total,
'data' => $list
]);
} catch (\Exception $e) {
// 详细记录异常信息
trace("获取包名列表异常:" . $e->getMessage() . "\n" . $e->getTraceAsString());
return json(['code' => 1, 'msg' => '系统异常,请稍后重试!']);
}
}
}
/**
* 获取用户包名权限
* @auth true
* @api true
*/
public function getUserAuth()
{
if ($this->request->isPost()) {
try {
$userId = $this->request->post('user_id/d', 0);
if (empty($userId)) {
return json(['code' => 0, 'info' => '请选择管理员!']);
}
// 只获取包名ID列表
$packageIds = $this->app->db->name($this->table)
->where('user_id', $userId)
->column('package_id'); // 直接返回ID数组
return json([
'code' => 1,
'info' => '获取成功!',
'data' => $packageIds // 直接返回ID数组供 getPackageDetails 使用
]);
} catch (\Exception $e) {
trace("获取用户权限异常:{$e->getMessage()}\n" . $e->getTraceAsString());
return json(['code' => 0, 'info' => '系统异常,请稍后重试!']);
}
}
}
/**
* 批量授权
* @auth true
*/
public function batchAuth()
{
if ($this->request->isPost()) {
try {
$data = $this->_vali([
'user_ids.require' => '请选择管理员!',
'package_ids.require' => '请选择包名!'
]);
// 验证所选用户是否都有包名管理权限
foreach ($data['user_ids'] as $userId) {
if (!AdminService::instance()->check('package/index', $userId)) {
return json(['code' => 0, 'info' => '选中的用户中有人没有包名管理权限!']);
}
}
// 开启事务
$this->app->db->startTrans();
try {
foreach ($data['user_ids'] as $userId) {
// 删除原有权限
$this->app->db->name($this->table)
->where('user_id', $userId)
->delete();
// 添加新权限
$insertData = [];
foreach ($data['package_ids'] as $packageId) {
$insertData[] = [
'user_id' => $userId,
'package_id' => $packageId,
'create_at' => date('Y-m-d H:i:s')
];
}
if (!empty($insertData)) {
$this->app->db->name($this->table)->insertAll($insertData);
}
}
$this->app->db->commit();
sysoplog('积分墙权限', '批量授权成功');
return json(['code' => 1, 'info' => '批量授权成功!']);
} catch (\Exception $e) {
$this->app->db->rollback();
throw $e;
}
} catch (\Exception $e) {
trace("批量授权异常:" . $e->getMessage());
return json(['code' => 0, 'info' => '系统异常请稍后重<E5908E><E9878D><EFBFBD>']);
}
}
}
/**
* 清空
* @auth true
*/
public function clearAuth()
{
if ($this->request->isPost()) {
try {
$post = $this->request->post();
// 验证用户ID
if (empty($post['user_ids']) || !is_array($post['user_ids'])) {
return json(['code' => 0, 'info' => '选择管理员!']);
}
// 开启事务
$this->app->db->startTrans();
try {
foreach ($post['user_ids'] as $userId) {
// 删除用户的所有权限
$this->app->db->name($this->table)
->where('user_id', $userId)
->delete();
}
$this->app->db->commit();
sysoplog('积分墙权限', '权限清空成功');
return json(['code' => 1, 'info' => '权限清空成功!']);
} catch (\Exception $e) {
$this->app->db->rollback();
trace('清空权限事务异常:' . $e->getMessage());
throw $e;
}
} catch (\Exception $e) {
trace("清空权限异常:" . $e->getMessage());
return json(['code' => 0, 'info' => '系统异常,请稍后重试!']);
}
}
}
/**
* 获取包名详情
*/
public function getPackageDetails()
{
if ($this->request->isPost()) {
try {
$packageIds = $this->request->post('package_ids/a', []);
// 记录请求参数
trace("getPackageDetails 请求参数:" . json_encode([
'package_ids' => $packageIds
], JSON_UNESCAPED_UNICODE));
// 如果没有包名ID返回空数组
if (empty($packageIds)) {
trace("getPackageDetails包名ID为空");
return json([
'code' => 1,
'info' => '获取成功!',
'data' => []
]);
}
// 获取包名详情
$packages = $this->app->db->name('offer_package')
->whereIn('id', $packageIds)
->field('id,package_name,name,status')
->select()
->toArray();
return json([
'code' => 1,
'info' => '获取成功!',
'data' => $packages
]);
} catch (\Exception $e) {
trace("getPackageDetails 异常:" . $e->getMessage());
trace("getPackageDetails 异常堆栈:" . $e->getTraceAsString());
return json(['code' => 0, 'info' => '系统异常,请稍后重试!#3']);
}
}
}
/**
* 获取未授权的包名列表
* @auth true
*/
public function getUnauthorizedPackages()
{
// 同时支持 GET 和 POST 请求
$userId = $this->request->param('user_id/d', 0);
$keyword = $this->request->param('keyword/s', '');
$page = $this->request->param('page/d', 1);
$limit = $this->request->param('limit/d', 10);
try {
if (empty($userId)) {
return json(['code' => 0, 'info' => '参数错误!']);
}
// 获取已授权的包名ID
$authorizedIds = $this->app->db->name($this->table)
->where('user_id', $userId)
->column('package_id');
// 构建查询条件
$where = [];
// 排除已授权的包名
if (!empty($authorizedIds)) {
$where[] = ['id', 'not in', $authorizedIds];
}
// 搜索条件
if ($keyword !== '') {
$where[] = ['package_name|name', 'like', "%{$keyword}%"];
}
// 只显示启用的包名
$where[] = ['status', '=', 1];
// 查询数据
$query = $this->app->db->name('offer_package');
// 获取总数
$total = $query->where($where)->count();
// 获取列表
$list = $query->where($where)
->field('id,package_name,name,status')
->order('status desc,id desc')
->limit(($page - 1) * $limit, $limit)
->select()
->toArray();
// 记录调试信息
trace('未授权包名查询:' . json_encode([
'user_id' => $userId,
'keyword' => $keyword,
'where' => $where,
'sql' => $query->getLastSql(),
'total' => $total,
'list_count' => count($list)
], JSON_UNESCAPED_UNICODE));
return json([
'code' => 0, // layui table 要求成功码为 0
'msg' => '', // layui table 使用 msg 而不是 info
'count' => $total,
'data' => $list
]);
} catch (\Exception $e) {
trace("获取未授权包名异常:" . $e->getMessage() . "\n" . $e->getTraceAsString());
return json(['code' => 1, 'msg' => '系统异常,请稍后重试!']); // layui table 要求失败码为非 0
}
}
/**
* 批量添加授权
* @auth true
*/
public function addAuth()
{
if ($this->request->isPost()) {
try {
$userId = $this->request->post('user_id/d', 0);
$packageIds = $this->request->post('package_ids/a');
$isAll = $this->request->post('is_all/d', 0); // 是否全部授权
if (empty($userId)) {
return json(['code' => 0, 'info' => '参数错误!']);
}
// 开启事务
$this->app->db->startTrans();
try {
if ($isAll) {
// 优化: 使用INSERT INTO SELECT语法直接插入
$sql = "INSERT INTO {$this->table} (user_id, package_id, create_at)
SELECT :user_id, id, :create_at
FROM offer_package
WHERE status = 1
AND id NOT IN (
SELECT package_id
FROM {$this->table}
WHERE user_id = :user_id2
)";
$this->app->db->execute($sql, [
'user_id' => $userId,
'user_id2' => $userId,
'create_at' => date('Y-m-d H:i:s')
]);
} else {
if (empty($packageIds)) {
return json(['code' => 0, 'info' => '请选择要授权的包名!']);
}
// 过滤掉已授权的包名
$existIds = $this->app->db->name($this->table)
->where('user_id', $userId)
->whereIn('package_id', $packageIds)
->column('package_id');
$newPackageIds = array_diff($packageIds, $existIds);
if (!empty($newPackageIds)) {
$insertData = array_map(function($packageId) use ($userId) {
return [
'user_id' => $userId,
'package_id' => $packageId,
'create_at' => date('Y-m-d H:i:s')
];
}, $newPackageIds);
$this->app->db->name($this->table)->insertAll($insertData);
}
}
$this->app->db->commit();
sysoplog('积分墙权限', '授权添加成功!');
return json(['code' => 1, 'info' => '授权添加成功!']);
} catch (\Exception $e) {
$this->app->db->rollback();
throw $e;
}
} catch (\Exception $e) {
trace("添加授权异常:" . $e->getMessage());
return json(['code' => 0, 'info' => '系统异常,请稍后重试!']);
}
}
}
/**
* 移除单个包名权限
* @auth true
*/
public function removeAuth()
{
if ($this->request->isPost()) {
try {
$userId = $this->request->post('user_id/d', 0);
$packageId = $this->request->post('package_id/d', 0);
if (empty($userId) || empty($packageId)) {
return json(['code' => 0, 'info' => '参数错误!']);
}
$result = $this->app->db->name($this->table)
->where([
'user_id' => $userId,
'package_id' => $packageId
])
->delete();
if ($result !== false) {
sysoplog('积分墙权限', '权限移除成功!');
return json(['code' => 1, 'info' => '权限移除成功!']);
} else {
return json(['code' => 0, 'info' => '权限移除失败!']);
}
} catch (\Exception $e) {
trace("移除权限异常:" . $e->getMessage());
return json(['code' => 0, 'info' => '系统异常,请稍后重试!']);
}
}
}
/**
* 首页批量授权保存
* @auth true
*/
public function batchSaveAuth()
{
if ($this->request->isPost()) {
try {
$data = $this->_vali([
'user_ids.require' => '请选择管理员!',
'package_ids.require' => '请选择包名!'
]);
// 开启事务
$this->app->db->startTrans();
try {
foreach ($data['user_ids'] as $userId) {
// 删除原有权限
$this->app->db->name($this->table)
->where('user_id', $userId)
->delete();
// 添加新权限
$insertData = [];
foreach ($data['package_ids'] as $packageId) {
$insertData[] = [
'user_id' => $userId,
'package_id' => $packageId,
'create_at' => date('Y-m-d H:i:s')
];
}
if (!empty($insertData)) {
$this->app->db->name($this->table)->insertAll($insertData);
}
}
$this->app->db->commit();
sysoplog('积分墙权限', '批量授权成功!');
return json(['code' => 1, 'info' => '批量授权成功!']);
} catch (\Exception $e) {
$this->app->db->rollback();
throw $e;
}
} catch (\Exception $e) {
trace("首页批量授权异常:" . $e->getMessage());
return json(['code' => 0, 'info' => '系统异常,请稍后重试!']);
}
}
}
/**
* 批量移除包名权限
* @auth true
*/
public function batchRemoveAuth()
{
if ($this->request->isPost()) {
try {
$userId = $this->request->post('user_id/d', 0);
$packageIds = $this->request->post('package_ids/a');
if (empty($userId) || empty($packageIds)) {
return json(['code' => 0, 'info' => '参数错误!']);
}
// 开启事务
$this->app->db->startTrans();
try {
// 批量删除权限
$result = $this->app->db->name($this->table)
->where('user_id', $userId)
->whereIn('package_id', $packageIds)
->delete();
if ($result !== false) {
$this->app->db->commit();
sysoplog('积分墙权限', '批量移除成功!');
return json(['code' => 1, 'info' => '批量移除成功!']);
} else {
$this->app->db->rollback();
return json(['code' => 0, 'info' => '批量移除失败!']);
}
} catch (\Exception $e) {
$this->app->db->rollback();
throw $e;
}
} catch (\Exception $e) {
trace("批移除权限异:" . $e->getMessage());
return json(['code' => 0, 'info' => '系统异常,请稍后重试!']);
}
}
}
/**
* 显示权限详情页面
* @auth true
*/
public function authDetail()
{
try {
$userId = input('user_id/d');
if (!$userId) {
$this->error('参数错误');
}
// 获取用户基本信息
$user = $this->app->db->name('system_user')
->where('id', $userId)
->field('id,username,status,create_at')
->find();
if (!$user) {
$this->error('用户不存在');
}
// 获取用户权限统计
$stats = [
'total' => $this->app->db->name($this->table)
->where('user_id', $userId)
->count(),
'active' => $this->app->db->name('offer_package')
->alias('p')
->join("{$this->table} a", 'p.id = a.package_id')
->where([
'a.user_id' => $userId,
'p.status' => 1
])
->count()
];
$this->assign([
'user' => $user,
'stats' => $stats,
'title' => '授权包名管理'
]);
return $this->fetch();
} catch (\Exception $e) {
$this->app->log->error("显示权限详情页面异常:{$e->getMessage()}");
$this->error('系统异常,请稍后重试!');
}
}
/**
* 获取权限详情数据
* @auth true
* @api true
*/
public function getAuthDetailData()
{
try {
$get = $this->_vali([
'user_id.require' => '用户ID不能为空',
'type.require' => '类型不能为空',
'page.default' => 1,
'limit.default' => 20,
'keyword.default' => '',
'sort.default' => 'id',
'order.default' => 'desc'
]);
// 确保分页参数为整数
$page = intval($get['page']);
$limit = intval($get['limit']);
// 构建基础查询
$query = $this->app->db->name('offer_package')
->alias('p');
if ($get['type'] === 'authorized') {
// 已授权包名查询
$query->join("{$this->table} a", 'p.id = a.package_id')
->where('a.user_id', $get['user_id']);
} else {
// 未授权包名查询
$query->whereNotExists(function($query) use ($get) {
$query->table($this->table)
->where('package_id=p.id')
->where('user_id', $get['user_id']);
})
->where('p.status', 1);
}
// 关键词搜索
if ($get['keyword'] !== '') {
$query->whereLike('p.package_name|p.name', "%{$get['keyword']}%");
}
// 获取总数
$total = $query->count();
// 获取列表数据
$list = $query->field([
'p.id',
'p.package_name',
'p.name',
'p.status',
$get['type'] === 'authorized' ? 'a.create_at' : 'p.add_time as create_at'
])
->order("{$get['sort']} {$get['order']}")
->page($page, $limit) // 使用转换后的整数值
->select()
->toArray();
// 格式化时间
foreach ($list as &$item) {
$item['create_at'] = format_datetime($item['create_at']);
$item['status_text'] = $item['status'] ? '启用' : '停用';
}
return json([
'code' => 0,
'msg' => '',
'count' => $total,
'data' => $list
]);
} catch (\Exception $e) {
trace("获取权限详情数据异常:" . $e->getMessage() . "\n" . $e->getTraceAsString());
return json(['code' => 1, 'msg' => '系统异常,请稍后重试!']);
}
}
/**
* 获取已授权的包名列表
* @auth true
*/
public function getAuthorizedPackages()
{
try {
$userId = $this->request->param('user_id/d', 0);
$keyword = $this->request->param('keyword/s', '');
$page = $this->request->param('page/d', 1);
$limit = $this->request->param('limit/d', 10);
if (empty($userId)) {
return json(['code' => 1, 'msg' => '参数错误']);
}
// 优化1: 使用子查询优化 JOIN
$query = $this->app->db->name('offer_package')
->whereExists(function($query) use ($userId) {
$query->table($this->table)
->where('package_id=offer_package.id')
->where('user_id', $userId);
})
->where('status', 1);
// 优化2: 添加索引字段的索条件
if ($keyword !== '') {
$query->where(function($query) use ($keyword) {
$query->whereOr([
['package_name', 'like', "%{$keyword}%"],
['name', 'like', "%{$keyword}%"]
]);
});
}
// 优化3: 使用子查询获取总数,避免重复JOIN
$total = $query->count();
// 优化4: 只查询需要的字段
$list = $query->field([
'id',
'package_name',
'name',
'status',
"(SELECT create_at FROM {$this->table} WHERE package_id=offer_package.id AND user_id={$userId} LIMIT 1) as create_at"
])
->order('id desc')
->page($page, $limit)
->select()
->toArray();
// 优化5: 添加缓存
$cacheKey = "auth_packages_{$userId}_{$page}_{$limit}_" . md5($keyword);
cache($cacheKey, $list, 300); // 缓存5分钟
return json([
'code' => 0,
'msg' => '',
'count' => $total,
'data' => $list
]);
} catch (\Exception $e) {
trace("获取已授权包名列表异常:" . $e->getMessage() . "\n" . $e->getTraceAsString());
return json(['code' => 1, 'msg' => '系统异常,请稍后重试!']);
}
}
/**
* 获取授权包名列表(分页)
* @auth true
* @api true
*/
public function getAuthPackageList()
{
try {
$get = $this->_vali([
'user_id.require' => '用户ID不能为空',
'page.default' => 1,
'limit.default' => 10,
'keyword.default' => '',
'status.default' => 1,
'sort.default' => 'id',
'order.default' => 'desc'
]);
// 构建基础查询
$query = $this->app->db->name('offer_package')
->alias('p')
->join("{$this->table} a", 'p.id = a.package_id')
->where([
'a.user_id' => $get['user_id'],
'p.status' => $get['status']
]);
// 关键词搜索
if ($get['keyword'] !== '') {
$query->whereLike('p.package_name|p.name', "%{$get['keyword']}%");
}
// 获取总数
$total = $query->count();
// 获取分页数据
$list = $query->field([
'p.id',
'p.package_name',
'p.name',
'p.status',
'a.create_at',
'p.update_time'
])
->order("{$get['sort']} {$get['order']}")
->limit(($get['page'] - 1) * $get['limit'], $get['limit'])
->select()
->toArray();
// 格式化时间
foreach ($list as &$item) {
$item['create_at'] = format_datetime($item['create_at']);
$item['update_time'] = format_datetime($item['update_time']);
}
// 使用缓存
$cacheKey = "auth_package_list_{$get['user_id']}_{$get['page']}_{$get['limit']}_" . md5($get['keyword']);
cache($cacheKey, [
'total' => $total,
'list' => $list
], 300); // 缓存5分钟
return json([
'code' => 0,
'msg' => 'success',
'total' => $total,
'list' => $list,
'page' => $get['page'],
'limit' => $get['limit']
]);
} catch (\Exception $e) {
$this->app->log->error("获取授权包名列表异常:{$e->getMessage()}");
return json([
'code' => 1,
'msg' => '获取数据失败:' . $e->getMessage()
]);
}
}
/**
* 获取授权包名统计
* @auth true
* @api true
*/
public function getAuthPackageStats()
{
try {
$userId = input('user_id/d', 0);
if (empty($userId)) {
return json(['code' => 1, 'msg' => '用户ID不能为空']);
}
// 获取统计数据
$stats = [
'total' => $this->app->db->name($this->table)
->where('user_id', $userId)
->count(),
'active' => $this->app->db->name('offer_package')
->alias('p')
->join("{$this->table} a", 'p.id = a.package_id')
->where([
'a.user_id' => $userId,
'p.status' => 1
])
->count(),
'latest' => $this->app->db->name($this->table)
->where('user_id', $userId)
->order('create_at desc')
->value('create_at')
];
$stats['latest'] = $stats['latest'] ? format_datetime($stats['latest']) : '';
return json([
'code' => 0,
'msg' => 'success',
'data' => $stats
]);
} catch (\Exception $e) {
$this->app->log->error("获取授权包名统计异常:{$e->getMessage()}");
return json([
'code' => 1,
'msg' => '获取统计失败:' . $e->getMessage()
]);
}
}
}

View File

@@ -0,0 +1,495 @@
<?php
declare(strict_types=1);
namespace app\manager\controller;
use think\admin\Controller;
use think\admin\service\AdminService;
/**
* 包名回传配置管理
* @auth true
* @menu true
*/
class PackageCallback extends Controller
{
/**
* 绑定数据表
* @var string
*/
protected $table = 'offer_package_callback';
/**
* 回传配置列表
* @auth true
* @menu true
*/
public function index()
{
// 设置页面标题
$this->title = '回传配置管理';
$this->fetch();
}
/**
* 获取回传配置列表数据
* @auth true
*/
public function get_list()
{
try {
// 获取请求参数
$page = input('page/d', 1);
$limit = input('limit/d', 15);
$where = [];
// 包名搜索
if ($package_name = input('package_name/s', '')) {
$where[] = ['package_name', 'like', "%{$package_name}%"];
}
// 事件名称搜索
if ($event_name = input('event_name/s', '')) {
$where[] = ['event_name', 'like', "%{$event_name}%"];
}
// 状态筛选
if ($status = input('status/s', '')) {
$where[] = ['status', '=', $status];
}
// 非超级管理员需要进行权限过滤
if (!AdminService::instance()->isSuper()) {
// 检查包名管理模块权限
if (!AdminService::instance()->check('package/index')) {
return json([
'code' => 0,
'msg' => '',
'count' => 0,
'data' => []
]);
}
$userId = AdminService::instance()->getUserId();
// 获取有权限的包名ID列表
$packageIds = $this->app->db->name('offer_package_auth')
->where('user_id', $userId)
->column('package_id');
if (empty($packageIds)) {
return json([
'code' => 0,
'msg' => '',
'count' => 0,
'data' => []
]);
}
$where[] = ['package_id', 'in', $packageIds];
}
// 查询数据
$query = $this->app->db->name($this->table);
// 先获取总数
$total = $query->where($where)->count();
// 分页查询数据
$list = $query->where($where)
->field('*')
->order('id desc')
->limit(($page - 1) * $limit, $limit)
->select()
->toArray();
return json([
'code' => 0,
'msg' => '',
'count' => $total,
'data' => $list
]);
} catch (\Exception $e) {
trace("获取回传配置列表异常:" . $e->getMessage());
return json([
'code' => 1,
'msg' => '获取数据失败,请稍后重试',
'count' => 0,
'data' => []
]);
}
}
/**
* 添加回传配置
* @auth true
*/
public function add()
{
if ($this->request->isGet()) {
// 设置页面标题
$this->title = '添加回传配置';
$this->fetch('add'); // 明确指定渲染 add.html
}
if ($this->request->isPost()) {
try {
$data = $this->_vali([
'package_id.require' => '包名ID不能为空',
'package_name.require' => '包名不能为空!',
'event_name.require' => '事件名称不能为空!',
'callback_url.require' => '回传地址不能为空!',
'status.in:0,1' => '状态值不正确!'
]);
// 检查权限
if (!$this->checkPackageAuth($data['package_id'])) {
return json(['code' => 0, 'info' => '您没有操作此包名的权限!']);
}
// 检查是否已存在相同配置
$exists = $this->app->db->name($this->table)
->where([
'package_id' => $data['package_id'],
'event_name' => $data['event_name']
])
->find();
if ($exists) {
return json(['code' => 0, 'info' => '该包名下已存在相同事件名称的配置!']);
}
// 添加创建信息
$data['create_by'] = AdminService::instance()->getUserId();
$data['create_at'] = date('Y-m-d H:i:s');
$data['update_at'] = date('Y-m-d H:i:s');
$result = $this->app->db->name($this->table)->insert($data);
if ($result === false) {
return json(['code' => 0, 'info' => '添加失败,请重试!']);
}
sysoplog('积分墙包名回传', '添加成功!');
return json(['code' => 1, 'info' => '添加成功!']);
} catch (\Exception $e) {
trace("添加回传配置异常:" . $e->getMessage());
return json(['code' => 0, 'info' => '系统异常,请稍后重试!']);
}
}
}
/**
* 编辑回传配置
* @auth true
*/
public function edit()
{
if ($this->request->isGet()) {
$id = $this->request->get('id/d', 0);
if (empty($id)) {
$this->error('参数错误');
}
// 获取配置信息
$vo = $this->app->db->name($this->table)->where('id', $id)->find();
if (empty($vo)) {
$this->error('配置不存在');
}
// 检查权限
if (!$this->checkPackageAuth($vo['package_id'])) {
$this->error('您没有操作此包名的权限!');
}
$this->assign('vo', $vo);
$this->fetch(); // 直接渲染 edit.html
}
if ($this->request->isPost()) {
try {
$data = $this->_vali([
'id.require' => '配置ID不能为空',
'package_id.require' => '包名ID不能为空',
'package_name.require' => '包名不能为空!',
'event_name.require' => '事件名称不能为空!',
'callback_url.require' => '回传地址不能为空!',
'status.in:0,1' => '状态值不正确!'
]);
// 检查权限
if (!$this->checkPackageAuth($data['package_id'])) {
return json(['code' => 0, 'info' => '您没有操作此包名的权限!']);
}
// 检查是否已存在相同配置(排除自身)
$exists = $this->app->db->name($this->table)
->where([
['package_id', '=', $data['package_id']],
['event_name', '=', $data['event_name']],
['id', '<>', $data['id']]
])
->find();
if ($exists) {
return json(['code' => 0, 'info' => '该包名下已存在相同事件名称的配置!']);
}
// 更新时间
$data['update_at'] = date('Y-m-d H:i:s');
$result = $this->app->db->name($this->table)
->where(['id' => $data['id']])
->update($data);
if ($result === false) {
return json(['code' => 0, 'info' => '更新失败,请重试!']);
}
sysoplog('积分墙包名回传', '更新成功!');
return json(['code' => 1, 'info' => '更新成功!']);
} catch (\Exception $e) {
trace("编辑回传配置异常:" . $e->getMessage());
return json(['code' => 0, 'info' => '系统异常,请稍后重试!']);
}
}
}
/**
* 删除回传配置
* @auth true
*/
public function remove()
{
if ($this->request->isPost()) {
try {
$id = $this->request->param('id/d');
if (empty($id)) {
return json(['code' => 0, 'info' => '参数错误!']);
}
// 获取配置信息
$config = $this->app->db->name($this->table)->where('id', $id)->find();
if (empty($config)) {
return json(['code' => 0, 'info' => '配置不存在!']);
}
// 检查权限
if (!$this->checkPackageAuth($config['package_id'])) {
return json(['code' => 0, 'info' => '您没有操作此包名的权限!']);
}
$result = $this->app->db->name($this->table)->where('id', $id)->delete();
if ($result === false) {
return json(['code' => 0, 'info' => '删除失败,请重试!']);
}
sysoplog('积分墙包名回传', '删除成功!');
return json(['code' => 1, 'info' => '删除成功!']);
} catch (\Exception $e) {
trace("删除回传配置异常:" . $e->getMessage());
return json(['code' => 0, 'info' => '系统异常,请稍后重试!']);
}
}
}
/**
* 检查包名权限
* @param int $packageId 包名ID
* @return bool
*/
protected function checkPackageAuth($packageId)
{
// 超级管理员直接放行
if (AdminService::instance()->isSuper()) {
return true;
}
// 检查用户是否有包名管理模块的权限
if (!AdminService::instance()->check('package/index')) {
return false;
}
// 检查具体包名权限
$userId = AdminService::instance()->getUserId();
$exists = $this->app->db->name('offer_package_auth')
->where(['package_id' => $packageId, 'user_id' => $userId])
->find();
return !empty($exists);
}
/**
* 修改状态
* @auth true
*/
public function state()
{
if ($this->request->isPost()) {
try {
$data = $this->_vali([
'id.require' => '配置ID不能为空',
'status.in:0,1' => '状态值不正确!'
]);
// 获取配置信息
$config = $this->app->db->name($this->table)->where('id', $data['id'])->find();
if (empty($config)) {
return json(['code' => 0, 'info' => '配置不存在!']);
}
// 检查权限
if (!$this->checkPackageAuth($config['package_id'])) {
return json(['code' => 0, 'info' => '您没有操作此包名的权限!']);
}
// 更新状态
$result = $this->app->db->name($this->table)
->where(['id' => $data['id']])
->update([
'status' => $data['status'],
'update_at' => date('Y-m-d H:i:s')
]);
if ($result === false) {
return json(['code' => 0, 'info' => '状态更新失败!']);
}
sysoplog('积分墙包名回传', '状态更新成功!');
return json(['code' => 1, 'info' => '状态更新成功!']);
} catch (\Exception $e) {
trace("修改状态异常:" . $e->getMessage());
return json(['code' => 0, 'info' => '系统异常,请稍后重试!']);
}
}
}
/**
* 搜索包名
* @auth true
*/
public function searchPackages()
{
try {
$keyword = input('keyword/s', '');
$init = input('init/d', 0); // 是否是初始化加载
$event_name = input('event_name/s', ''); // 事件名称,用于排除已配置的包名
$edit_id = input('edit_id/d', 0); // 编辑时的ID用于排除自身
// 构建查询
$query = $this->app->db->name('offer_package')
->alias('p')
->where('p.status', 1);
// 非超级管理员需要进行权限过滤
if (!AdminService::instance()->isSuper()) {
$userId = AdminService::instance()->getUserId();
$query->join('offer_package_auth a', 'p.id = a.package_id')
->where('a.user_id', $userId);
}
// 排除已经配置过此事件的包名
if (!empty($event_name)) {
$existsQuery = $this->app->db->name($this->table)
->where('event_name', $event_name);
// 编辑时排除自身
if ($edit_id > 0) {
$existsQuery->where('id', '<>', $edit_id);
}
$existsPackageIds = $existsQuery->column('package_id');
if (!empty($existsPackageIds)) {
$query->whereNotIn('p.id', $existsPackageIds);
}
}
// 如果是搜索则添加搜索条件
if (!$init && !empty($keyword)) {
$query->where(function($query) use ($keyword) {
$query->whereOr([
['p.package_name', 'like', "%{$keyword}%"],
['p.name', 'like', "%{$keyword}%"]
]);
});
}
// 限制返回数量
$list = $query->field(['p.id', 'p.package_name', 'p.name']) // 明确指定字段
->group('p.id') // 防止重复
->order('p.id desc') // 按ID倒序
->limit(20)
->select()
->toArray();
return json(['code' => 1, 'msg' => '获取成功', 'data' => $list]);
} catch (\Exception $e) {
trace("搜索包名异常:" . $e->getMessage() . "\n" . $e->getTraceAsString());
return json(['code' => 0, 'msg' => '系统异常,请稍后重试']);
}
}
/**
* 检查配置是否存在
* @auth true
*/
public function checkExists()
{
if ($this->request->isPost()) {
try {
$data = $this->_vali([
'package_id.require' => '包名ID不能为空',
'event_name.require' => '事件名称不能为空!'
]);
// 检查是否已存在相同配置
$exists = $this->app->db->name($this->table)
->where([
'package_id' => $data['package_id'],
'event_name' => $data['event_name']
])
->find();
return json([
'code' => 1,
'msg' => '检查成功',
'data' => !empty($exists)
]);
} catch (\Exception $e) {
return json(['code' => 0, 'msg' => '检查失败']);
}
}
}
/**
* 获取已存在的配置详情
* @auth true
*/
public function getExistingConfig()
{
if ($this->request->isPost()) {
try {
$data = $this->_vali([
'package_id.require' => '包名ID不能为空',
'event_name.require' => '事件名称不能为空!'
]);
// 获取已存在的配置
$config = $this->app->db->name($this->table)
->where([
'package_id' => $data['package_id'],
'event_name' => $data['event_name']
])
->find();
return json([
'code' => 1,
'msg' => '获取成功',
'data' => $config
]);
} catch (\Exception $e) {
return json(['code' => 0, 'msg' => '获取失败']);
}
}
}
}

View File

@@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
namespace app\manager\controller;
use think\admin\Controller;
use think\admin\service\AdminService;
/**
* 回传记录管理
* @auth true
* @menu true
*/
class PackageCallbackRecords extends Controller
{
/**
* 绑定数据表
* @var string
*/
protected $table = 'offer_package_callback_records';
/**
* 回传记录列表
* @auth true
* @menu true
*/
public function index()
{
$this->title = '回传记录列表';
$this->fetch();
}
/**
* 获取回传记录列表
* @auth true
*/
public function get_list()
{
try {
$page = input('page/d', 1);
$limit = input('limit/d', 15);
$where = [];
// 包名搜索
if ($package_name = input('package_name/s', '')) {
$where[] = ['package_name', 'like', "%{$package_name}%"];
}
// 事件名称搜索
if ($event_name = input('event_name/s', '')) {
$where[] = ['event_name', 'like', "%{$event_name}%"];
}
// 推送ID搜索
if ($push_record_id = input('push_record_id/d', '')) {
$where[] = ['push_record_id', '=', $push_record_id];
}
// 状态筛选
if ($status = input('status/s', '')) {
$where[] = ['status', '=', $status];
}
// 时间范围筛选
if ($start_time = input('start_time/s', '')) {
$where[] = ['callback_time', '>=', $start_time];
}
if ($end_time = input('end_time/s', '')) {
$where[] = ['callback_time', '<=', $end_time];
}
// 查询数据
$query = $this->app->db->name($this->table)
->alias('a')
->join('offer_package_push_records b', 'a.push_record_id = b.id', 'left')
->field('a.*, b.source_type');
// 获取总数
$total = $query->where($where)->count();
// 分页查询数据
$list = $query->where($where)
->order('a.id desc')
->limit(($page - 1) * $limit, $limit)
->select()
->toArray();
return json([
'code' => 0,
'msg' => '',
'count' => $total,
'data' => $list
]);
} catch (\Exception $e) {
trace("获取回传记录列表异常:" . $e->getMessage());
return json([
'code' => 1,
'msg' => '获取数据失败,请稍后重试',
'count' => 0,
'data' => []
]);
}
}
/**
* 查看详情
* @auth true
*/
public function detail()
{
$id = input('id/d', 0);
if (empty($id)) {
$this->error('参数错误');
}
$vo = $this->app->db->name($this->table)->where('id', $id)->find();
if (empty($vo)) {
$this->error('记录不存在');
}
$this->vo = $vo;
$this->fetch();
}
}

View File

@@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
namespace app\manager\controller;
use think\admin\Controller;
use think\admin\service\AdminService;
/**
* 推送记录管理
* @auth true
* @menu true
*/
class PackagePushRecords extends Controller
{
/**
* 绑定数据表
* @var string
*/
protected $table = 'offer_package_push_records';
/**
* 推送记录列表
* @auth true
* @menu true
*/
public function index()
{
$this->title = '推送记录列表';
$this->fetch();
}
/**
* 获取推送记录列表
* @auth true
*/
public function get_list()
{
try {
$page = input('page/d', 1);
$limit = input('limit/d', 15);
$where = [];
// 包名搜索
if ($package_name = input('package_name/s', '')) {
$where[] = ['package_name', 'like', "%{$package_name}%"];
}
// 事件名称搜索
if ($event_name = input('event_name/s', '')) {
$where[] = ['event_name', 'like', "%{$event_name}%"];
}
// 来源筛选
if ($source_type = input('source_type/s', '')) {
$where[] = ['source_type', '=', $source_type];
}
// 时间范围筛选
if ($start_time = input('start_time/s', '')) {
$where[] = ['receive_time', '>=', $start_time];
}
if ($end_time = input('end_time/s', '')) {
$where[] = ['receive_time', '<=', $end_time];
}
// 查询数据
$query = $this->app->db->name($this->table);
// 获取总数
$total = $query->where($where)->count();
// 分页查询数据
$list = $query->where($where)
->field('*')
->order('id desc')
->limit(($page - 1) * $limit, $limit)
->select()
->toArray();
return json([
'code' => 0,
'msg' => '',
'count' => $total,
'data' => $list
]);
} catch (\Exception $e) {
trace("获取推送记录列表异常:" . $e->getMessage());
return json([
'code' => 1,
'msg' => '获取数据失败,请稍后重试',
'count' => 0,
'data' => []
]);
}
}
/**
* 查看详情
* @auth true
*/
public function detail()
{
$id = input('id/d', 0);
if (empty($id)) {
$this->error('参数错误');
}
$vo = $this->app->db->name($this->table)->where('id', $id)->find();
if (empty($vo)) {
$this->error('记录不存在');
}
$this->vo = $vo;
$this->fetch();
}
}

View File

@@ -0,0 +1,194 @@
<?php
declare (strict_types = 1);
namespace app\manager\model;
use think\admin\extend\DataExtend;
use think\Model;
/**
* 会员模型
*/
class Member extends Model
{
/**
* 数据表名称
* @var string
*/
protected $name = 'members';
/**
* 自动写入时间戳
* @var bool
*/
protected $autoWriteTimestamp = true;
/**
* 时间字段输出格式
* @var string
*/
protected $dateFormat = 'Y-m-d H:i:s';
/**
* 创建时间字段
* @var string
*/
protected $createTime = 'create_time';
/**
* 更新时间字段
* @var string
*/
protected $updateTime = 'update_time';
/**
* 追加属性
* @var array
*/
protected $append = [
'status_text'
];
/**
* 字段类型
* @var array
*/
protected $type = [
'id' => 'integer',
'status' => 'integer',
'usage_limit' => 'integer',
'used_count' => 'integer',
'points' => 'integer',
'level' => 'integer',
'balance' => 'float',
'expire_time' => 'datetime',
'last_login_time' => 'datetime',
'create_time' => 'datetime',
'update_time' => 'datetime'
];
/**
* 状态获取器
*/
public function getStatusTextAttr($value, $data)
{
return $data['status'] == 1 ? '正常' : '禁用';
}
/**
* 获取分页列表
* @param array $map 查询条件
* @param string $sort 排序规则
* @return \think\db\Query
*/
public static function mQuery($map = [], $sort = 'id desc')
{
$query = static::where($map);
// 会员搜索器
$query->withSearch(['username', 'nickname', 'mobile', 'email', 'status', 'order_id'], [
'username' => function ($query, $value) {
$query->whereLike('username|nickname', "%{$value}%");
},
'nickname' => function ($query, $value) {
$query->whereLike('nickname', "%{$value}%");
},
'mobile' => function ($query, $value) {
$query->whereLike('mobile', "%{$value}%");
},
'email' => function ($query, $value) {
$query->whereLike('email', "%{$value}%");
},
'order_id' => function ($query, $value) {
$query->whereLike('order_id', "%{$value}%");
},
'status' => function ($query, $value) {
$query->where(['status' => $value]);
}
]);
return $query->order($sort);
}
/**
* 表单处理
* @param string $template 模板名称
* @param array $data 表单数据
* @param bool $isUpdate 是否更新
* @return array|mixed
*/
public static function mForm($template = '', $data = [], $isUpdate = false)
{
if (request()->isPost()) {
$data = request()->post();
// 密码处理
if (isset($data['password']) && empty($data['password'])) {
unset($data['password']);
} elseif (isset($data['password'])) {
$data['password'] = password_hash($data['password'], PASSWORD_DEFAULT);
}
// 保存数据
if (isset($data['id']) && $data['id'] > 0) {
return static::update($data) !== false;
} else {
return static::create($data) !== false;
}
}
return $template;
}
/**
* 修改状态
* @return bool
*/
public static function mSave()
{
$data = input('post.');
return static::update($data) !== false;
}
/**
* 删除记录
* @return bool
*/
public static function mDelete()
{
$id = input('post.id');
return static::destroy($id) !== false;
}
/**
* 检查会员是否有效
* @return array
*/
public function isValid()
{
// 检查状态
if (!$this->status) {
return ['valid' => false, 'reason' => '账号已被禁用'];
}
// 检查有效期
if ($this->expire_time && $this->expire_time < date('Y-m-d H:i:s')) {
return ['valid' => false, 'reason' => '账号已过期'];
}
// 检查使用次数
if ($this->usage_limit > 0 && $this->used_count >= $this->usage_limit) {
return ['valid' => false, 'reason' => '使用次数已达上限'];
}
return ['valid' => true, 'reason' => ''];
}
/**
* 更新登录信息
* @param string $ip
* @return bool
*/
public function updateLoginInfo($ip = '')
{
$this->last_login_time = date('Y-m-d H:i:s');
$this->last_login_ip = $ip ?: request()->ip();
$this->used_count += 1;
return $this->save();
}
}

View File

@@ -0,0 +1,265 @@
<?php
declare(strict_types=1);
namespace app\manager\sys;
use think\facade\Config;
use RedisException;
use Redis;
use RedisCluster;
class RedisManager
{
private static ?self $instance = null;
private Redis|RedisCluster|null $redis = null;
private string $prefix;
/**
* 获取RedisManager实例
*/
public static function getInstance(?array $config = null): self
{
if (self::$instance === null) {
self::$instance = new self($config);
}
return self::$instance;
}
/**
* 构造函数
*/
private function __construct(?array $config = null)
{
$this->initializeConnection($config);
$this->prefix = Config::get('database.connections.redis.prefix', '');
}
/**
* 初始化Redis连接
*/
private function initializeConnection(?array $config = null): void
{
$config ??= Config::get('database.connections.redis', []);
$hostName = config("app.host_name");
try {
match($hostName) {
'HHKK' => $this->initCluster([
'192.168.1.3:7000',
'192.168.1.2:7001',
'192.168.1.31:7002'
]),
'H36HK' => $this->initCluster([
'192.168.1.3:7000',
'192.168.1.2:7001',
'192.168.1.5:7002',
'192.168.1.41:7003',
'192.168.1.51:7004'
]),
default => $this->initSingle($config)
};
} catch (\Throwable $e) {
throw new RedisException("Redis连接失败: " . $e->getMessage());
}
}
/**
* 初始化单机Redis
*/
private function initSingle(array $config): void
{
$this->redis = new Redis();
$this->redis->connect(
$config['host'] ?? '127.0.0.1',
$config['port'] ?? 6379,
$config['timeout'] ?? 10
);
if (!empty($config['password'])) {
$this->redis->auth($config['password']);
}
if (isset($config['db']) && is_numeric($config['db'])) {
$this->redis->select((int)$config['db']);
}
}
/**
* 初始化Redis集群
*/
private function initCluster(array $nodes): void
{
$this->redis = new RedisCluster(null, $nodes, 5);
}
/**
* 获取Redis实例
*/
public function getRedis(): Redis|RedisCluster
{
return $this->redis;
}
/**
* String 操作
*/
public function get(string $key): mixed
{
try {
$value = $this->redis?->get($this->prefix . $key);
return $this->unserialize($value);
} catch (\Throwable) {
return null;
}
}
public function set(string $key, mixed $value, int $expire = 0): bool
{
try {
$value = $this->serialize($value);
if ($expire > 0) {
return (bool)$this->redis?->setex($this->prefix . $key, $expire, $value);
}
return (bool)$this->redis?->set($this->prefix . $key, $value);
} catch (\Throwable) {
return false;
}
}
/**
* List 操作
*/
public function lPush(string $key, mixed $value): int
{
try {
return $this->redis?->lPush($this->prefix . $key, $this->serialize($value)) ?? 0;
} catch (\Throwable) {
return 0;
}
}
public function lRange(string $key, int $start = 0, int $end = -1): array
{
try {
$data = $this->redis?->lRange($this->prefix . $key, $start, $end) ?? [];
return array_map(fn($item) => $this->unserialize($item), $data);
} catch (\Throwable) {
return [];
}
}
/**
* Hash 操作
*/
public function hSet(string $key, string $field, mixed $value): bool
{
try {
return (bool)$this->redis?->hSet(
$this->prefix . $key,
$field,
$this->serialize($value)
);
} catch (\Throwable) {
return false;
}
}
public function hGet(string $key, string|array|null $field = null): mixed
{
try {
if ($field === null) {
$data = $this->redis?->hGetAll($this->prefix . $key) ?? [];
return array_map(fn($item) => $this->unserialize($item), $data);
}
if (is_array($field)) {
$data = $this->redis?->hMGet($this->prefix . $key, $field) ?? [];
return array_map(fn($item) => $this->unserialize($item), $data);
}
$value = $this->redis?->hGet($this->prefix . $key, $field);
return $this->unserialize($value);
} catch (\Throwable) {
return null;
}
}
/**
* Set 操作
*/
public function sAdd(string $key, mixed $value): int
{
try {
if (is_array($value)) {
$count = 0;
foreach ($value as $v) {
$count += $this->redis?->sAdd(
$this->prefix . $key,
$this->serialize($v)
) ?? 0;
}
return $count;
}
return $this->redis?->sAdd($this->prefix . $key, $this->serialize($value)) ?? 0;
} catch (\Throwable) {
return 0;
}
}
public function sMembers(string $key): array
{
try {
$members = $this->redis?->sMembers($this->prefix . $key) ?? [];
return array_map(fn($item) => $this->unserialize($item), $members);
} catch (\Throwable) {
return [];
}
}
/**
* 通用操作
*/
public function delete(string|array $key): bool
{
try {
$keys = is_array($key)
? array_map(fn($k) => $this->prefix . $k, $key)
: $this->prefix . $key;
return (bool)$this->redis?->del($keys);
} catch (\Throwable) {
return false;
}
}
public function exists(string $key): bool
{
try {
return (bool)$this->redis?->exists($this->prefix . $key);
} catch (\Throwable) {
return false;
}
}
/**
* 序列化处理
*/
private function serialize(mixed $data): string
{
if (is_array($data)) {
return json_encode($data, JSON_UNESCAPED_UNICODE) ?: '';
}
return (string)$data;
}
private function unserialize(mixed $data): mixed
{
if (!is_string($data)) {
return $data;
}
$decoded = json_decode($data, true);
return json_last_error() === JSON_ERROR_NONE ? $decoded : $data;
}
private function __clone() {}
}

568
app/manager/view/error.php Normal file
View File

@@ -0,0 +1,568 @@
<?php
if (!function_exists('parse_padding')) {
function parse_padding($source)
{
$length = strlen(strval(count($source['source']) + $source['first']));
return 40 + ($length - 1) * 8;
}
}
if (!function_exists('parse_class')) {
function parse_class($name): string
{
$names = explode('\\', $name);
return '<abbr title="' . $name . '">' . end($names) . '</abbr>';
}
}
if (!function_exists('parse_file')) {
function parse_file($file, $line): string
{
return '<a class="toggle" title="' . "{$file} line {$line}" . '">' . basename($file) . " line {$line}" . '</a>';
}
}
if (!function_exists('parse_args')) {
function parse_args($args): string
{
$result = [];
foreach ($args as $key => $item) {
switch (true) {
case is_object($item):
$value = sprintf('<em>object</em>(%s)', parse_class(get_class($item)));
break;
case is_array($item):
if (count($item) > 3) {
$value = sprintf('[%s, ...]', parse_args(array_slice($item, 0, 3)));
} else {
$value = sprintf('[%s]', parse_args($item));
}
break;
case is_string($item):
if (strlen($item) > 20) {
$value = sprintf(
'\'<a class="toggle" title="%s">%s...</a>\'',
htmlentities($item),
htmlentities(substr($item, 0, 20))
);
} else {
$value = sprintf("'%s'", htmlentities($item));
}
break;
case is_int($item):
case is_float($item):
$value = $item;
break;
case is_null($item):
$value = '<em>null</em>';
break;
case is_bool($item):
$value = '<em>' . ($item ? 'true' : 'false') . '</em>';
break;
case is_resource($item):
$value = '<em>resource</em>';
break;
default:
$value = htmlentities(str_replace("\n", '', var_export(strval($item), true)));
break;
}
$result[] = is_int($key) ? $value : "'{$key}' => {$value}";
}
return implode(', ', $result);
}
}
if (!function_exists('echo_value')) {
function echo_value($val)
{
if (is_array($val) || is_object($val)) {
echo htmlentities(json_encode($val, JSON_PRETTY_PRINT));
} elseif (is_bool($val)) {
echo $val ? 'true' : 'false';
} elseif (is_scalar($val)) {
echo htmlentities($val);
} else {
echo 'Resource';
}
}
}
?>
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>系统发生错误</title>
<meta name="robots" content="noindex,nofollow"/>
<style>
/* Base */
body {
color: #333;
font: 16px Verdana, "Helvetica Neue", helvetica, Arial, 'Microsoft YaHei', sans-serif;
margin: 0;
padding: 0 20px 20px;
}
h1 {
margin: 10px 0 0;
font-size: 28px;
font-weight: 500;
line-height: 32px;
}
h2 {
color: #4288ce;
font-weight: 400;
padding: 6px 0;
margin: 6px 0 0;
font-size: 18px;
border-bottom: 1px solid #eee;
}
h3 {
margin: 12px;
font-size: 16px;
font-weight: bold;
}
abbr {
cursor: help;
text-decoration: underline;
text-decoration-style: dotted;
}
a {
color: #868686;
cursor: pointer;
}
a:hover {
text-decoration: underline;
}
.line-error {
background: #f8cbcb;
}
.echo table {
width: 100%;
}
.echo pre {
padding: 16px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
background-color: #f7f7f7;
border: 0;
border-radius: 3px;
font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
}
.echo pre > pre {
padding: 0;
margin: 0;
}
/* Exception Info */
.exception {
margin-top: 20px;
}
.exception .message {
padding: 12px;
border: 1px solid #ddd;
border-bottom: 0 none;
line-height: 18px;
font-size: 16px;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
font-family: Consolas, "Liberation Mono", Courier, Verdana, "微软雅黑", serif;
}
.exception .code {
float: left;
text-align: center;
color: #fff;
margin-right: 12px;
padding: 16px;
border-radius: 4px;
background: #999;
}
.exception .source-code {
padding: 6px;
border: 1px solid #ddd;
background: #f9f9f9;
overflow-x: auto;
}
.exception .source-code pre {
margin: 0;
}
.exception .source-code pre ol {
margin: 0;
color: #4288ce;
display: inline-block;
min-width: 100%;
box-sizing: border-box;
font-size: 14px;
font-family: "Century Gothic", Consolas, "Liberation Mono", Courier, Verdana, serif;
padding-left: <?php echo (isset($source) && ! empty($source)) ? parse_padding($source): 40;?> px;
}
.exception .source-code pre li {
border-left: 1px solid #ddd;
height: 18px;
line-height: 18px;
}
.exception .source-code pre code {
color: #333;
height: 100%;
display: inline-block;
border-left: 1px solid #fff;
font-size: 14px;
font-family: Consolas, "Liberation Mono", Courier, Verdana, "微软雅黑", serif;
}
.exception .trace {
padding: 6px;
border: 1px solid #ddd;
border-top: 0 none;
line-height: 16px;
font-size: 14px;
font-family: Consolas, "Liberation Mono", Courier, Verdana, "微软雅黑", serif;
}
.exception .trace h2:hover {
text-decoration: underline;
cursor: pointer;
}
.exception .trace ol {
margin: 12px;
}
.exception .trace ol li {
padding: 2px 4px;
}
.exception div:last-child {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
/* Exception Variables */
.exception-var table {
width: 100%;
margin: 12px 0;
box-sizing: border-box;
table-layout: fixed;
word-wrap: break-word;
}
.exception-var table caption {
text-align: left;
font-size: 16px;
font-weight: bold;
padding: 6px 0;
}
.exception-var table caption small {
font-weight: 300;
display: inline-block;
margin-left: 10px;
color: #ccc;
}
.exception-var table tbody {
font-size: 13px;
font-family: Consolas, "Liberation Mono", Courier, "微软雅黑", serif;
}
.exception-var table td {
padding: 0 6px;
vertical-align: top;
word-break: break-all;
}
.exception-var table td:first-child {
width: 28%;
font-weight: bold;
white-space: nowrap;
}
.exception-var table td pre {
margin: 0;
}
/* Copyright Info */
.copyright {
margin-top: 24px;
padding: 12px 0;
border-top: 1px solid #eee;
}
/* SPAN elements with the classes below are added by prettyprint. */
pre.prettyprint .pln {
color: #000
}
/* plain text */
pre.prettyprint .str {
color: #080
}
/* string content */
pre.prettyprint .kwd {
color: #008
}
/* a keyword */
pre.prettyprint .com {
color: #800
}
/* a comment */
pre.prettyprint .typ {
color: #606
}
/* a type name */
pre.prettyprint .lit {
color: #066
}
/* a literal value */
/* punctuation, lisp open bracket, lisp close bracket */
pre.prettyprint .pun, pre.prettyprint .opn, pre.prettyprint .clo {
color: #660
}
pre.prettyprint .tag {
color: #008
}
/* a markup tag name */
pre.prettyprint .atn {
color: #606
}
/* a markup attribute name */
pre.prettyprint .atv {
color: #080
}
/* a markup attribute value */
pre.prettyprint .dec, pre.prettyprint .var {
color: #606
}
/* a declaration; a variable name */
pre.prettyprint .fun {
color: red
}
/* a function name */
</style>
</head>
<body>
<?php if (\think\facade\App::isDebug()) { ?>
<?php foreach ($traces as $index => $trace) { ?>
<div class="exception">
<div class="message">
<div class="info">
<div>
<h2><?php echo "#{$index} [{$trace['code']}]" . sprintf('%s in %s', parse_class($trace['name']), parse_file($trace['file'], $trace['line'])); ?></h2>
</div>
<div><h1><?php echo nl2br(htmlentities($trace['message'])); ?></h1></div>
</div>
</div>
<?php if (!empty($trace['source'])) { ?>
<div class="source-code">
<pre class="prettyprint lang-php">
<ol start="<?php echo $trace['source']['first']; ?>"><!--<?php foreach ((array)$trace['source']['source'] as $key => $value) { ?>--><li class="line-<?php echo " {$index}-" . ($key + $trace['source']['first']) . ($trace['line'] === $key + $trace['source']['first'] ? ' line-error' : ''); ?>"><code><?php echo htmlentities($value); ?></code></li><!--<?php } ?>--></ol>
</pre>
</div>
<?php } ?>
<div class="trace">
<h2 data-expand="<?php echo 0 === $index ? '1' : '0'; ?>">Call Stack</h2>
<ol>
<li><?php echo sprintf('in %s', parse_file($trace['file'], $trace['line'])); ?></li>
<?php foreach ((array)$trace['trace'] as $value) { ?>
<li>
<?php
// Show Function
if ($value['function']) {
echo sprintf('at %s%s%s(%s)', isset($value['class']) ? parse_class($value['class']) : '', $value['type'] ?? '', $value['function'], isset($value['args']) ? parse_args($value['args']) : '');
}
// Show line
if (isset($value['file']) && isset($value['line'])) {
echo sprintf(' in %s', parse_file($value['file'], $value['line']));
}
?>
</li>
<?php } ?>
</ol>
</div>
</div>
<?php } ?>
<?php } else { ?>
<div class="exception">
<div class="info"><h1><?php echo htmlentities(isset($message) ? $message : ''); ?></h1></div>
</div>
<?php } ?>
<?php if (!empty($datas)) { ?>
<div class="exception-var">
<h2>Exception Datas</h2>
<?php foreach ((array)$datas as $label => $value) { ?>
<table>
<?php if (empty($value)) { ?>
<caption><?php echo $label; ?><small>empty</small></caption>
<?php } else { ?>
<caption><?php echo $label; ?></caption>
<tbody>
<?php foreach ((array)$value as $key => $val) { ?>
<tr>
<td><?php echo htmlentities($key); ?></td>
<td><?php echo_value($val); ?></td>
</tr>
<?php } ?>
</tbody>
<?php } ?>
</table>
<?php } ?>
</div>
<?php } ?>
<?php if (!empty($tables)) { ?>
<div class="exception-var">
<h2>Environment Variables</h2>
<?php foreach ((array)$tables as $label => $value) { ?>
<table>
<?php if (empty($value)) { ?>
<caption><?php echo $label; ?><small>empty</small></caption>
<?php } else { ?>
<caption><?php echo $label; ?></caption>
<tbody>
<?php foreach ((array)$value as $key => $val) { ?>
<tr>
<td><?php echo htmlentities($key); ?></td>
<td><?php echo_value($val); ?></td>
</tr>
<?php } ?>
</tbody>
<?php } ?>
</table>
<?php } ?>
</div>
<?php } ?>
<?php if (\think\facade\App::isDebug()) { ?>
<script>
function $(selector, node) {
var elements;
node = node || document;
if (document.querySelectorAll) {
elements = node.querySelectorAll(selector);
} else {
switch (selector.substr(0, 1)) {
case '#':
elements = [node.getElementById(selector.substr(1))];
break;
case '.':
if (document.getElementsByClassName) {
elements = node.getElementsByClassName(selector.substr(1));
} else {
elements = get_elements_by_class(selector.substr(1), node);
}
break;
default:
elements = node.getElementsByTagName();
}
}
return elements;
function get_elements_by_class(search_class, node, tag) {
var elements = [], eles,
pattern = new RegExp('(^|\\s)' + search_class + '(\\s|$)');
node = node || document;
tag = tag || '*';
eles = node.getElementsByTagName(tag);
for (var i = 0; i < eles.length; i++) {
if (pattern.test(eles[i].className)) {
elements.push(eles[i])
}
}
return elements;
}
}
$.getScript = function (src, func) {
var script = document.createElement('script');
script.async = 'async';
script.src = src;
script.onload = func || function () {
};
$('head')[0].appendChild(script);
}
;(function () {
var files = $('.toggle');
var ol = $('ol', $('.prettyprint')[0]);
var li = $('li', ol[0]);
// 短路径和长路径变换
for (var i = 0; i < files.length; i++) {
files[i].ondblclick = function () {
var title = this.title;
this.title = this.innerHTML;
this.innerHTML = title;
}
}
(function () {
var expand = function (dom, expand) {
var ol = $('ol', dom.parentNode)[0];
expand = undefined === expand ? dom.attributes['data-expand'].value === '0' : undefined;
if (expand) {
dom.attributes['data-expand'].value = '1';
ol.style.display = 'none';
dom.innerText = 'Call Stack (展开)';
} else {
dom.attributes['data-expand'].value = '0';
ol.style.display = 'block';
dom.innerText = 'Call Stack (折叠)';
}
};
var traces = $('.trace');
for (var i = 0; i < traces.length; i++) {
var h2 = $('h2', traces[i])[0];
expand(h2);
h2.onclick = function () {
expand(this);
};
}
})();
$.getScript('//cdn.bootcss.com/prettify/r298/prettify.min.js', function () {
prettyPrint();
});
})();
</script>
<?php } ?>
</body>
</html>

View File

@@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<title>{block name="title"}{$title|default=''}{if !empty($title)} · {/if}{:sysconf('site_name')}{/block}</title>
<meta charset="utf-8">
<meta name="renderer" content="webkit">
<meta name="format-detection" content="telephone=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=0.4">
<link rel="stylesheet" href="__ROOT__/static/plugs/layui/css/layui.css?at={:date('md')}">
<link rel="stylesheet" href="__ROOT__/static/theme/css/iconfont.css?at={:date('md')}">
<link rel="stylesheet" href="__ROOT__/static/theme/css/console.css?at={:date('md')}">
<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 src="{:url('admin/api.plugs/script',[],false,false)}"></script>
</head>
<body class="layui-layout-body">
{block name='body'}
<div class="layui-layout layui-layout-admin layui-layout-left-hide">
<div class="layui-body think-bg-white margin-0 padding-0" style="top:0">{block name='content'}{/block}</div>
</div>
{/block}
<script src="__ROOT__/static/plugs/layui/layui.js"></script>
<script src="__ROOT__/static/plugs/require/require.js"></script>
<script src="__ROOT__/static/admin.js"></script>
<script src="__ROOT__/static/extra/script.js"></script>
{block name='script'}{/block}
</body>
</html>

View File

@@ -0,0 +1,16 @@
{extend name="main"}
{block name="content"}
<div class="layui-fluid">
<div class="layui-row layui-col-space15">
<div class="layui-col-md12">
<div class="layui-card">
<div class="layui-card-header">欢迎页面</div>
<div class="layui-card-body">
欢迎使用管理系统{$userid}
</div>
</div>
</div>
</div>
</div>
{/block}

View File

@@ -0,0 +1,23 @@
<div class="layui-card">
{block name='style'}{/block}
{block name='header'}
{notempty name='title'}
<div class="layui-card-header">
<span class="layui-icon font-s10 color-desc margin-right-5">&#xe65b;</span>{$title|lang}
<div class="pull-right">{block name='button'}{/block}</div>
</div>
{/notempty}
{/block}
<div class="layui-card-line"></div>
<div class="layui-card-body">
<div class="layui-card-html">
{notempty name='showErrorMessage'}
<div class="think-box-notify" type="error">
<b>{:lang('系统提示:')}</b><span>{$showErrorMessage|raw}</span>
</div>
{/notempty}
{block name='content'}{/block}
</div>
</div>
{block name='script'}{/block}
</div>

View File

@@ -0,0 +1,74 @@
{extend name='../../admin/view/main'}
{block name='content'}
<form class="layui-form" action="{:sysuri()}" data-auto="true" method="post" autocomplete="off" onsubmit="return false" lay-filter="FormData">
<div class="layui-card">
<div class="layui-card-body">
<div class="layui-form-item">
<label class="layui-form-label">邮箱</label>
<div class="layui-input-block">
<input type="text" name="email" value='{$member.email|default=""}' required lay-verify="required|email" placeholder="请输入邮箱" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">闲鱼订单号</label>
<div class="layui-input-block">
<input type="text" name="order_id" value='{$member.order_id|default=""}' required lay-verify="required" placeholder="请输入闲鱼订单号" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">有效期</label>
<div class="layui-input-block">
<input type="text" name="expire_time" value='{$member.expire_time|default=""}' required lay-verify="required" placeholder="请选择有效期" class="layui-input" id="expire_time">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">可用次数</label>
<div class="layui-input-block">
<input type="number" name="usage_limit" value='{$member.usage_limit|default="0"}' required lay-verify="required|number" placeholder="请输入可用次数" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">已用次数</label>
<div class="layui-input-block">
<input type="number" name="used_count" value='{$member.used_count|default="0"}' lay-verify="number" placeholder="请输入已用次数" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">状态</label>
<div class="layui-input-block">
<input type="radio" name="status" value="1" title="正常" {if !isset($member.status) or $member.status eq 1}checked{/if}>
<input type="radio" name="status" value="0" title="禁用" {if isset($member.status) and $member.status eq 0}checked{/if}>
</div>
</div>
</div>
<div class="hr-line-dashed"></div>
<div class="layui-form-item text-center">
{notempty name='member.id'}<input type='hidden' value='{$member.id}' name='id'>{/notempty}
<button class="layui-btn" type="submit" lay-submit lay-filter="*">保存数据</button>
<button type="reset" class="layui-btn layui-btn-primary">重置数据</button>
</div>
</div>
</form>
{/block}
{block name='script'}
<script>
layui.use(['form', 'laydate'], function(){
var form = layui.form;
// 渲染日期时间选择器
layui.laydate.render({
elem: '#expire_time',
type: 'datetime'
});
});
</script>
{/block}

View File

@@ -0,0 +1,92 @@
{extend name='../../admin/view/main'}
{block name='button'}
<!-- 表格工具栏 -->
<div class="layui-btn-container">
<button class='layui-btn layui-btn-sm layui-btn-primary' data-modal='{:url("add")}' data-width="800px" data-height="600px">添加会员</button>
</div>
{/block}
{block name='content'}
<div class="think-box-shadow">
<form class="layui-form layui-form-pane form-search" action="">
<div class="layui-form-item layui-inline">
<label class="layui-form-label">搜索</label>
<div class="layui-input-inline">
<input name="keyword" value="{$get.keyword|default=''}" placeholder="请输入邮箱/订单号" class="layui-input">
</div>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">状态</label>
<div class="layui-input-inline">
<select name="status">
<option value=''>-- 全部 --</option>
<option value='1'>正常</option>
<option value='0'>禁用</option>
</select>
</div>
</div>
<div class="layui-form-item layui-inline">
<button class="layui-btn layui-btn-primary" lay-submit lay-filter="search"><i class="layui-icon">&#xe615;</i> 搜 索</button>
</div>
</form>
<table id="TableList" lay-filter="TableList"></table>
</div>
{/block}
{block name='script'}
<script type="text/html" id="toolbar">
<a class="layui-btn layui-btn-sm" data-modal='{:url("edit")}?id={{d.id}}' data-width="800px" data-height="600px">编辑</a>
<a class="layui-btn layui-btn-sm layui-btn-danger" data-confirm="确定要删除此会员吗?" data-action='{:url("remove")}' data-value="id#{{d.id}}">删除</a>
</script>
<script>
layui.use(['table','form'], function(){
var table = layui.table;
var form = layui.form;
// 表格渲染
table.render({
elem: '#TableList'
,url: '{:sysuri()}?output=layui.table'
,page: true
,cellMinWidth: 200
,cols: [[
{field: 'id', title: 'ID', width: 70, align: 'center'},
{field: 'email', title: '邮箱', minWidth: 80},
{field: 'order_id', title: '闲鱼订单号', minWidth: 100},
{field: 'expire_time', title: '有效期', minWidth: 100},
{field: 'usage_limit', title: '可用次数', width: 70, align: 'center'},
{field: 'used_count', title: '已用次数', width: 70, align: 'center'},
{field: 'last_login_time', title: '最后登录时间', minWidth: 100},
{field: 'status', title: '状态', width: 100, align: 'center', templet: function(d){
return '<input type="checkbox" name="status" value="'+d.id+'" lay-skin="switch" lay-text="正常|禁用" lay-filter="statusSwitch" '+(d.status == 1 ? 'checked' : '')+'>';
}},
{field: 'create_time', title: '创建时间', minWidth: 180},
{title: '操作', toolbar: '#toolbar', width: 250, align: 'center', fixed: 'right'}
]]
});
// 搜索提交
form.on('submit(search)', function(data){
table.reload('TableList', {where: data.field});
return false;
});
// 监听状态切换
form.on('switch(statusSwitch)', function(obj){
let id = this.value;
let status = obj.elem.checked ? 1 : 0;
$.post('{:url("state")}', {id: id, status: status}, function(res){
if (res.code != 1) {
obj.elem.checked = !obj.elem.checked;
form.render('checkbox');
layer.msg(res.info, {icon: 2});
}
}, 'json');
});
});
</script>
{/block}

View File

@@ -0,0 +1,32 @@
{if isset($list)}
<fieldset>
<legend>条件搜索</legend>
<form class="layui-form layui-form-pane form-search" action="{:request()->url()}" onsubmit="return false" method="get" autocomplete="off">
<div class="layui-form-item layui-inline">
<label class="layui-form-label">搜索关键词</label>
<div class="layui-input-inline">
<input name="keyword" value="{$get.keyword|default=''}" placeholder="请输入用户名/昵称/手机/邮箱" class="layui-input">
</div>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">会员状态</label>
<div class="layui-input-inline">
<select name="status">
<option value=''>-- 全部状态 --</option>
<option value='1' {if isset($get.status) and $get.status eq 1}selected{/if}>正常</option>
<option value='0' {if isset($get.status) and $get.status eq 0}selected{/if}>禁用</option>
</select>
</div>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">注册时间</label>
<div class="layui-input-inline">
<input data-date-range name="create_time" value="{$get.create_time|default=''}" placeholder="请选择注册时间" class="layui-input">
</div>
</div>
<div class="layui-form-item layui-inline">
<button class="layui-btn layui-btn-primary"><i class="layui-icon">&#xe615;</i> 搜 索</button>
</div>
</form>
</fieldset>
{/if}

View File

@@ -0,0 +1,294 @@
{extend name="../../admin/view/main"}
{block name='content'}
<div class="think-box-shadow">
<!-- 表单搜索 -->
<form class="layui-form layui-form-pane form-search" action="">
<div class="layui-form-item layui-inline">
<label class="layui-form-label">包名</label>
<div class="layui-input-inline">
<input name="package_name" placeholder="请输入包名" class="layui-input">
</div>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">应用名称</label>
<div class="layui-input-inline">
<input name="name" placeholder="请输入应用名称" class="layui-input">
</div>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">状态</label>
<div class="layui-input-inline">
<select name="status">
<option value="">所有状态</option>
<option value="1">启用</option>
<option value="0">禁用</option>
</select>
</div>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">创建人</label>
<div class="layui-input-inline">
<input name="username" placeholder="请输入创建人" class="layui-input">
</div>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">添加时间</label>
<div class="layui-input-inline">
<input name="add_time" id="add_time" placeholder="请选择添加时间" class="layui-input">
</div>
</div>
<div class="layui-form-item layui-inline">
<button class="layui-btn layui-btn-primary" type="button" lay-submit lay-filter="search-btn">
<i class="layui-icon">&#xe615;</i> 搜 索
</button>
<button class="layui-btn layui-btn-primary" type="reset" id="resetSearch">重置</button>
</div>
</form>
<!-- 数据表格 -->
<table id="currentTable" lay-filter="currentTable"></table>
</div>
<!-- 表单模板 -->
<script type="text/html" id="editForm">
<form class="layui-form" lay-filter="editForm" style="padding: 30px;">
<div class="layui-form-item">
<label class="layui-form-label" style="width: 100px;">应用名称</label>
<div class="layui-input-block" style="margin-left: 130px;">
<input type="text" name="name" lay-verify="required" placeholder="请输入应用名称" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label" style="width: 100px;">包名</label>
<div class="layui-input-block" style="margin-left: 130px;">
<input type="text" name="package_name" lay-verify="required" placeholder="请输入包名" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label" style="width: 100px;">状态</label>
<div class="layui-input-block" style="margin-left: 130px;">
<input type="radio" name="status" value="1" title="启用" checked>
<input type="radio" name="status" value="0" title="禁用">
</div>
</div>
<div class="layui-form-item text-center" style="margin-top: 40px;">
<input type="hidden" name="id">
<button class="layui-btn layui-btn-lg" lay-submit lay-filter="formSubmit">保存</button>
<button type="button" class="layui-btn layui-btn-lg layui-btn-danger" onclick="layer.closeAll()">取消</button>
</div>
</form>
</script>
{/block}
{block name='script'}
<!-- 表格操作列 -->
<script type="text/html" id="toolbar">
<div class="layui-btn-container">
{if auth("edit")}
<a class="layui-btn layui-btn-sm" lay-event="edit">编辑</a>
{/if}
{if auth("remove")}
<a class="layui-btn layui-btn-sm layui-btn-danger" lay-event="delete">删除</a>
{/if}
</div>
</script>
<script>
layui.use(['table', 'form', 'laydate'], function () {
var table = layui.table;
var form = layui.form;
var laydate = layui.laydate;
// 日期范围
laydate.render({
elem: '#add_time',
type: 'date',
range: true,
trigger: 'click'
});
// 数据表格初始化
table.render({
elem: '#currentTable',
url: '{:request()->url()}',
method: 'post',
defaultToolbar: ['filter', 'print', 'exports'],
toolbar: ['<div class="layui-btn-container">',
'{if auth("add")}',
'<button class="layui-btn layui-btn-sm layui-btn-normal" lay-event="add"><i class="layui-icon">&#xe654;</i> 添加包名</button>',
'{/if}',
'</div>'
].join(''),
cols: [[
{type: "checkbox"},
{field: 'id', width: 80, title: 'ID', sort: true},
{field: 'name', minWidth: 150, title: '应用名称'},
{field: 'package_name', minWidth: 200, title: '包名'},
{field: 'username', width: 100, title: '创建人'},
{field: 'status', width: 100, title: '状态', templet: function(d){
return '<input type="checkbox" name="status" value="' + d.id + '" lay-skin="switch" lay-text="启用|禁用" lay-filter="status" ' + (d.status == 1 ? 'checked' : '') + '>';
}},
{field: 'add_time', minWidth: 170, title: '添加时间', templet: function(d){
return d.add_time ? layui.util.toDateString(d.add_time * 1000, 'yyyy-MM-dd HH:mm:ss') : '';
}},
{field: 'update_time', minWidth: 170, title: '更新时间', templet: function(d){
return d.update_time ? layui.util.toDateString(d.update_time * 1000, 'yyyy-MM-dd HH:mm:ss') : '';
}},
{title: '操作', minWidth: 120, toolbar: '#toolbar', align: "center", fixed: 'right'}
]],
page: true,
limit: 15,
limits: [10, 15, 20, 25, 50, 100]
});
// 搜索提交
form.on('submit(search-btn)', function (data) {
table.reload('currentTable', {
page: {curr: 1},
where: data.field,
method: 'post'
});
return false;
});
// 重置搜索
$('#resetSearch').click(function() {
// 重置表单
$('.form-search')[0].reset();
// 重载表格
table.reload('currentTable', {
page: {curr: 1},
where: {},
method: 'post'
});
});
// 监听表单提交
form.on('submit(formSubmit)', function (data) {
var url = data.field.id ? '{:url("edit")}' : '{:url("add")}';
$.ajax({
url: url,
type: 'post',
data: data.field,
success: function(res) {
if (res.code === 1) {
layer.closeAll();
layer.msg(res.info, {icon: 1, time: 1000});
table.reload('currentTable');
} else {
layer.msg(res.info, {icon: 2, time: 2000});
}
},
error: function() {
layer.msg('网络错误,请稍后重试!', {icon: 2, time: 2000});
}
});
return false;
});
// 监听工具条
table.on('tool(currentTable)', function (obj) {
var data = obj.data;
if (obj.event === 'edit') {
showForm('编辑包名', data);
} else if (obj.event === 'delete') {
layer.confirm('确定要删除该包名吗?', {
title: '删除确认',
btn: ['确定', '取消']
}, function (index) {
$.ajax({
url: '{:url("remove")}',
type: 'post',
data: {id: data.id},
success: function(res) {
if (res.code === 1) {
layer.msg(res.info, {icon: 1, time: 1000});
table.reload('currentTable');
} else {
layer.msg(res.info, {icon: 2, time: 2000});
}
layer.close(index);
},
error: function() {
layer.msg('网络错误,请稍后重试!', {icon: 2, time: 2000});
layer.close(index);
}
});
});
}
});
// 监听头工具栏事件
table.on('toolbar(currentTable)', function(obj){
if(obj.event === 'add'){
showForm('添加包名');
}
});
// 显示表单
function showForm(title, data) {
layer.open({
type: 1,
title: title,
area: ['800px', '500px'],
offset: '50px',
shadeClose: true,
content: $('#editForm').html(),
success: function(layero, index) {
$(layero).find('.layui-form').css('margin', '15px');
$(layero).find('.layui-input-block').css('max-width', '500px');
form.render();
if(data) {
form.val('editForm', data);
}
}
});
}
// 监听状态切换
form.on('switch(status)', function(obj){
var id = this.value;
var status = obj.elem.checked ? 1 : 0;
layer.confirm('确定要' + (status == 1 ? '启用' : '禁用') + '该包名吗?', {
title: '操作确认',
btn: ['确定', '取消']
}, function(index){
$.ajax({
url: '{:url("state")}',
type: 'post',
data: {
id: id,
status: status
},
success: function(res) {
if (res.code === 1) {
layer.msg(res.info, {icon: 1, time: 1000});
table.reload('currentTable');
} else {
layer.msg(res.info, {icon: 2, time: 2000});
// 状态切换失败,恢复开关状态
obj.elem.checked = !obj.elem.checked;
form.render('checkbox');
}
layer.close(index);
},
error: function() {
layer.msg('网络错误请稍后重试2', {icon: 2, time: 2000});
// 发生错误,恢复开关状态
obj.elem.checked = !obj.elem.checked;
form.render('checkbox');
layer.close(index);
}
});
}, function(){
// 取消操作,恢复开关状态
obj.elem.checked = !obj.elem.checked;
form.render('checkbox');
});
});
});
</script>
{/block}

View File

@@ -0,0 +1,451 @@
<!-- 权限详情弹窗模板 -->
<div class="detail-container" style="padding: 20px;" data-user-id="{{d.userId}}">
<div class="layui-row layui-col-space15">
<!-- 左侧已授权包名列表 -->
<div class="layui-col-md6">
<div class="layui-card">
<div class="layui-card-header">
授权的包名列表
<div class="layui-inline" style="width: 200px; margin-left: 10px;">
<input type="text" id="searchAuthorized" placeholder="输入包名或应用名称搜索" class="layui-input">
</div>
</div>
<div class="layui-card-body auth-table-container">
<table id="authorizedTable" lay-filter="authorizedTable"></table>
</div>
</div>
</div>
<!-- 右侧可授权包名列表 -->
<div class="layui-col-md6">
<div class="layui-card">
<div class="layui-card-header">
可授权的包名列表
<span class="layui-badge layui-bg-blue">仅显示启用状态的包名</span>
<div class="layui-inline" style="width: 200px; margin-left: 10px;">
<input type="text" id="searchPackage" placeholder="输入包名或应用名称搜索" class="layui-input">
</div>
</div>
<div class="layui-card-body auth-table-container">
<table id="unauthorizedTable" lay-filter="unauthorizedTable"></table>
</div>
</div>
</div>
</div>
<!-- 底部按钮 -->
<div class="layui-form-item text-center" style="margin-top:15px;">
<button class="layui-btn layui-btn-primary" onclick="closeAndRefresh()">关闭窗口</button>
</div>
</div>
<!-- 已授权表格工具栏 -->
<script type="text/html" id="authDetailAuthorizedToolbar">
<div class="layui-btn-container">
<button class="layui-btn layui-btn-sm layui-btn-danger" lay-event="batchRemove" lay-tips="移除选中的包名权限">
<i class="layui-icon">&#xe640;</i> 批量移除
</button>
<button class="layui-btn layui-btn-sm layui-btn-danger" lay-event="removeAll" lay-tips="移除该管理员的所有包名权限">
<i class="layui-icon">&#xe640;</i> 一键全部移除
</button>
</div>
</script>
<!-- 未授权表格工具栏 -->
<script type="text/html" id="authDetailUnauthorizedToolbar">
<div class="layui-btn-container">
<button class="layui-btn layui-btn-sm layui-btn-normal" lay-event="batchAuth" lay-tips="授权选中的包名">
<i class="layui-icon">&#xe654;</i> 批量授权
</button>
<button class="layui-btn layui-btn-sm layui-btn-warm" lay-event="authAll" lay-tips="授权所有未授权的包名">
<i class="layui-icon">&#xe674;</i> 一键全部授权
</button>
</div>
</script>
<!-- 表格行工具条 -->
<script type="text/html" id="authDetailTableBar">
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="remove">移除</a>
</script>
<script type="text/html" id="authDetailUnAuthTableBar">
<a class="layui-btn layui-btn-normal layui-btn-xs" lay-event="auth">授权</a>
</script>
<!-- 表格初始化脚本 -->
<script>
window.initAuthDetailTables = function(userId) {
layui.use(['table', 'form'], function() {
let table = layui.table;
// 初始化已授权表格
table.render({
elem: '#authorizedTable',
url: '{:url("getAuthDetailData")}',
where: {
user_id: userId,
type: 'authorized'
},
toolbar: '#authDetailAuthorizedToolbar',
defaultToolbar: ['filter'],
page: true,
limit: 10,
limits: [10, 20, 50, 100],
cols: [[
{type: 'checkbox', width: 50},
{field: 'package_name', title: '包名', minWidth: 250},
{field: 'name', title: '名称', minWidth: 200},
{field: 'create_at', title: '授权时间', width: 160},
{title: '操作', toolbar: '#authDetailTableBar', width: 80, align: 'center', fixed: 'right'}
]]
});
// 初始化未授权表格
table.render({
elem: '#unauthorizedTable',
url: '{:url("getAuthDetailData")}',
where: {
user_id: userId,
type: 'unauthorized'
},
toolbar: '#authDetailUnauthorizedToolbar',
defaultToolbar: ['filter'],
page: true,
limit: 10,
limits: [10, 20, 50, 100],
cols: [[
{type: 'checkbox', width: 50},
{field: 'package_name', title: '包名', minWidth: 250},
{field: 'name', title: '名称', minWidth: 200},
{title: '操作', toolbar: '#authDetailUnAuthTableBar', width: 80, align: 'center', fixed: 'right'}
]]
});
// 绑定搜索事件
bindSearchEvents(table, userId);
// 绑定工具条事件
bindTableEvents(table, userId);
});
};
// 绑定搜索事件
function bindSearchEvents(table, userId) {
let searchTimeout;
$('#searchAuthorized').on('input', function() {
clearTimeout(searchTimeout);
let value = this.value;
searchTimeout = setTimeout(function() {
table.reload('authorizedTable', {
where: {
user_id: userId,
type: 'authorized',
keyword: value
},
page: {curr: 1}
});
}, 300);
});
$('#searchPackage').on('input', function() {
clearTimeout(searchTimeout);
let value = this.value;
searchTimeout = setTimeout(function() {
table.reload('unauthorizedTable', {
where: {
user_id: userId,
type: 'unauthorized',
keyword: value
},
page: {curr: 1}
});
}, 300);
});
}
// 绑定工具条事件
function bindTableEvents(table, userId) {
// 已授权表格工具条事件
table.on('toolbar(authorizedTable)', function(obj) {
if (obj.event === 'batchRemove') {
let checkStatus = table.checkStatus('authorizedTable');
if (checkStatus.data.length === 0) {
layer.msg('请选择要移除的包名', {icon: 2});
return;
}
layer.confirm('确定要移除选中的 ' + checkStatus.data.length + ' 个包名权限吗?', {
title: '批量移除确认',
btn: ['确定移除', '取消操作'],
skin: 'layer-danger'
}, function(index) {
let loadIndex = layer.load(2);
let packageIds = checkStatus.data.map(item => item.id);
$.post('{:url("batchRemoveAuth")}', {
user_id: userId,
package_ids: packageIds
}, function(res) {
layer.close(loadIndex);
layer.msg(res.info);
if (res.code === 1) {
// 刷新两个表格
table.reload('authorizedTable');
table.reload('unauthorizedTable');
}
});
layer.close(index);
});
} else if (obj.event === 'removeAll') {
layer.confirm('确定要移除该管理员的所有包名权限吗?<br>此操作不可恢复!', {
title: '危险操作',
btn: ['确定移除', '取消操作'],
icon: 2,
skin: 'layer-danger'
}, function(index) {
let loadIndex = layer.load(2);
$.post('{:url("clearAuth")}', {
user_ids: [userId]
}, function(res) {
layer.close(loadIndex);
layer.msg(res.info);
if (res.code === 1) {
// 刷新两个表格
table.reload('authorizedTable');
table.reload('unauthorizedTable');
}
});
layer.close(index);
});
}
});
// 未授权表格工具条事件
table.on('toolbar(unauthorizedTable)', function(obj) {
if (obj.event === 'batchAuth') {
let checkStatus = table.checkStatus('unauthorizedTable');
if (checkStatus.data.length === 0) {
layer.msg('请选择要授权的包名', {icon: 2});
return;
}
layer.confirm('确定要授权选中的 ' + checkStatus.data.length + ' 个包名吗?', {
title: '批量授权确认',
btn: ['确定授权', '取消操作']
}, function(index) {
let loadIndex = layer.load(2);
let packageIds = checkStatus.data.map(item => item.id);
$.post('{:url("addAuth")}', {
user_id: userId,
package_ids: packageIds
}, function(res) {
layer.close(loadIndex);
layer.msg(res.info);
if (res.code === 1) {
// 刷新两个表格
table.reload('authorizedTable');
table.reload('unauthorizedTable');
}
});
layer.close(index);
});
} else if (obj.event === 'authAll') {
layer.confirm('确定要授权所有可用的包名吗?', {
title: '一键全部授权',
btn: ['确定授权', '取消操作'],
icon: 3
}, function(index) {
let loadIndex = layer.load(2);
$.post('{:url("addAuth")}', {
user_id: userId,
is_all: 1
}, function(res) {
layer.close(loadIndex);
layer.msg(res.info);
if (res.code === 1) {
// 刷新两个表格
table.reload('authorizedTable');
table.reload('unauthorizedTable');
}
});
layer.close(index);
});
}
});
// 单个移除事件
table.on('tool(authorizedTable)', function(obj) {
if (obj.event === 'remove') {
layer.confirm('确定要移除此包名权限吗?', function(index) {
let loadIndex = layer.load(2);
$.post('{:url("removeAuth")}', {
user_id: userId,
package_id: obj.data.id
}, function(res) {
layer.close(loadIndex);
layer.msg(res.info);
if (res.code === 1) {
obj.del();
// 刷新未授权表格
table.reload('unauthorizedTable');
}
});
layer.close(index);
});
}
});
// 单个授权事件
table.on('tool(unauthorizedTable)', function(obj) {
if (obj.event === 'auth') {
let loadIndex = layer.load(2);
$.post('{:url("addAuth")}', {
user_id: userId,
package_ids: [obj.data.id]
}, function(res) {
layer.close(loadIndex);
layer.msg(res.info);
if (res.code === 1) {
// 刷新两个表格
table.reload('authorizedTable');
table.reload('unauthorizedTable');
}
});
}
});
}
// 添加关闭并刷新的函数
function closeAndRefresh() {
// 关闭当前弹窗
layer.closeAll();
// 刷新父页面
parent.location.reload();
}
</script>
<!-- 弹窗专用样式 -->
<style>
.detail-container {
min-height: 600px;
display: flex;
flex-direction: column;
position: relative;
z-index: 1;
}
.detail-container .layui-row {
flex: 1;
}
.detail-container .layui-card {
margin-bottom: 0;
height: 100%;
display: flex;
flex-direction: column;
}
.detail-container .layui-card-body {
flex: 1;
padding: 15px;
display: flex;
flex-direction: column;
}
.auth-table-container {
min-height: 550px;
position: relative;
}
/* 搜索框样式 */
.detail-container .layui-inline {
margin: 0;
}
.detail-container .layui-input {
height: 30px;
line-height: 30px;
}
/* 底部按钮样式 */
.detail-container .layui-form-item.text-center {
padding: 10px 0 0;
margin: 0;
border-top: 1px solid #f0f0f0;
}
/* 表格美化样式 */
.auth-table-container .layui-table-tool {
background-color: #f8f8f8;
}
.auth-table-container .layui-table-page {
background-color: #f8f8f8;
}
/* 表格头部样式 */
.auth-table-container .layui-table-header {
background-color: #fafafa;
}
/* 表格内容区域样式 */
.auth-table-container .layui-table-body {
background: #fff;
}
/* 工具栏按钮样式优化 */
.auth-table-container .layui-table-tool {
padding: 10px 15px;
}
.auth-table-container .layui-table-tool .layui-btn {
height: 30px;
line-height: 30px;
padding: 0 15px;
font-size: 12px;
margin-right: 8px;
}
.auth-table-container .layui-table-tool .layui-btn:last-child {
margin-right: 0;
}
.auth-table-container .layui-table-tool .layui-btn .layui-icon {
font-size: 14px;
margin-right: 3px;
}
/* 危险操作按钮样式 */
.auth-table-container .layui-table-tool .layui-btn-danger {
background-color: #FF5722;
}
.auth-table-container .layui-table-tool .layui-btn-danger:hover {
background-color: #ff4208;
}
/* 一键全部授权按钮样式 */
.auth-table-container .layui-table-tool .layui-btn-warm {
background-color: #FFB800;
}
.auth-table-container .layui-table-tool .layui-btn-warm:hover {
background-color: #ff9900;
}
/* 弹窗样式 */
.layer-danger .layui-layer-title {
background-color: #FF5722 !important;
color: #fff !important;
}
.layer-danger .layui-layer-btn0 {
background-color: #FF5722 !important;
border-color: #FF5722 !important;
}
/* 确保弹窗控制按钮在正确位置 */
.layui-layer-page .layui-layer-setwin {
position: absolute !important;
right: 15px !important;
top: 16px !important;
}
</style>

View File

@@ -0,0 +1,786 @@
{extend name="../../admin/view/main"}
{block name='content'}
<style>
/* 基础样式 */
.layui-card-body {
padding: 15px;
}
.layui-table-tool {
background-color: #f8f8f8;
}
.layui-table-page {
background-color: #f8f8f8;
}
.layui-table-box {
background: #fff;
border-radius: 2px;
box-shadow: 0 1px 2px rgba(0,0,0,.1);
}
/* 时间选择器样式 */
#dateRange {
width: 280px;
}
.layui-inline .layui-input {
height: 32px;
line-height: 32px;
}
.layui-inline .layui-btn {
height: 32px;
line-height: 32px;
}
</style>
<!-- 在主容器前添加操作指南 -->
<div class="think-box-shadow" style="margin-bottom: 15px; padding: 15px;">
<h3 class="layui-inline" style="margin-right: 15px;">操作指南</h3>
<button class="layui-btn layui-btn-sm" id="showGuide">展开/收起</button>
<div class="guide-content layui-hide" style="margin-top: 10px;">
<div class="layui-card">
<div class="layui-card-body">
<div class="layui-elem-quote" style="border-left: 5px solid #009688;">
<h4>基本说明:</h4>
<p>本页面用于管理不同管理员的包名权限,可以进行查看、添加、移除等操作。</p>
</div>
<div class="layui-elem-quote" style="border-left: 5px solid #FFB800;">
<h4>操作步骤:</h4>
<ol>
<li>左侧列表显示所有可配置权限的管理员</li>
<li>绿色标签表示已授权包名数量,灰色表示未授权</li>
<li>点击【查看权限】可以查看和修改该管理员的包名权限</li>
<li>击【批量授权】可以同时给多个管理员授权相同的包名</li>
<li>点击【清空权限】可以清除选中管理员的所有包名权限</li>
</ol>
</div>
<div class="layui-elem-quote" style="border-left: 5px solid #FF5722;">
<h4>权限配置窗口说明:</h4>
<ol>
<li>左侧显示已授权的包名列表,可以搜索和移除</li>
<li>右侧显示可授权的包名列表,仅显示启用状态的包名</li>
<li>在右侧勾选包名后点击【保存配置】即可添加授权</li>
<li>两侧都支持搜索功能,可以快速查找包名</li>
<li>所有操作都会实时更新显示结果</li>
</ol>
</div>
<div class="layui-elem-quote" style="border-left: 5px solid #01AAED;">
<h4>注意事项:</h4>
<ol>
<li>批量授权会覆盖已有的权限配置,请谨慎操作</li>
<li>清空权限操作不可恢复,请确认后再操作</li>
<li>建议先使用搜索功能查找包名,再进行授权作</li>
<li>可以使用全选功能快速选择多个管理员或包名</li>
<li>如有疑问请联系技术支持</li>
</ol>
</div>
</div>
</div>
</div>
</div>
<div class="think-box-shadow main-container">
<div class="layui-row layui-col-space15">
<!-- 左侧管理员列表 -->
<div class="layui-col-md3">
<div class="layui-card">
<div class="layui-card-header">
管理员列表
<div class="layui-layout-right" style="margin:5px;">
<span class="layui-badge layui-bg-blue" style="padding: 5px 10px;"> {$users|count} </span>
</div>
</div>
<div class="layui-card-body user-list-container">
<form class="layui-form" lay-filter="userForm">
<!-- 添加搜索框 -->
<div class="search-box">
<div class="layui-form-item" style="margin-bottom: 10px;">
<div class="layui-input-block" style="margin-left: 0;">
<input type="text" id="searchUsers" placeholder="输入管理员名称搜索" class="layui-input">
</div>
</div>
</div>
<!-- 全选复选框 -->
<div class="layui-form-item" style="padding: 10px;">
<input type="checkbox" lay-filter="checkAllUsers" title="全选" lay-skin="primary" lay-tips="选中/取消选中所有管理员">
</div>
<!-- 管理员列表 -->
<div class="user-list">
{foreach $users as $user}
<div class="layui-form-item">
<input type="checkbox" name="user_ids[]" value="{$user.id}" title="{$user.username}" lay-skin="primary">
{if isset($authMap[$user.id])}
<span class="layui-badge layui-bg-green" style="margin-left:10px; padding: 4px 8px;">已授权 {$authMap[$user.id]|count} 个包名</span>
<a class="layui-btn layui-btn-normal layui-btn-sm" data-user="{$user.id}" lay-event="showAuth" style="margin-left:5px;" lay-tips="点击查看和修改该管理员的包名权限配置">
<i class="layui-icon layui-icon-edit"></i> 查看权限
</a>
{else}
<span class="layui-badge layui-bg-gray" style="margin-left:10px; padding: 4px 8px;">未授权</span>
<a class="layui-btn layui-btn-warm layui-btn-sm" data-user="{$user.id}" lay-event="showAuth" style="margin-left:5px;" lay-tips="点击为该管理员添加包名权限">
<i class="layui-icon layui-icon-add-1"></i> 添加权限
</a>
{/if}
</div>
{/foreach}
</div>
</form>
</div>
</div>
</div>
<!-- 右侧包名列表 -->
<div class="layui-col-md9">
<div class="layui-card">
<div class="layui-card-header">
包名权限配置
<div class="layui-layout-right header-btns">
<button class="layui-btn layui-btn-normal" lay-submit lay-filter="batchAuth" lay-tips="选中管理员和包名后,点击此处进行批量授权操作">
<i class="layui-icon layui-icon-add-circle"></i> 批量授权
</button>
<button class="layui-btn layui-btn-danger" id="clearAuth" lay-tips="清空选中管理员的所有包名权限,请谨慎操作">
<i class="layui-icon layui-icon-delete"></i> 清空权限
</button>
</div>
</div>
<div class="layui-card-body">
<!-- 搜索框 -->
<div class="layui-form-item" style="margin-bottom: 10px;">
<div class="layui-inline" style="width: 250px;">
<input type="text" id="searchPackages" placeholder="输入包名或应用名称搜索" class="layui-input">
</div>
<div class="layui-inline">
<input type="text" id="dateRange" placeholder="选择时间范围" class="layui-input" readonly lay-tips="选择时间范围进行筛选">
</div>
<div class="layui-inline">
<button class="layui-btn layui-btn-primary" id="resetFilter" lay-tips="清空所有筛选条件">重置</button>
</div>
</div>
<!-- 表格 -->
<table id="packageTable" lay-filter="packageTable"></table>
</div>
</div>
</div>
</div>
</div>
<!-- 修改权限详情模板引用 -->
<script type="text/html" id="authDetailTpl">
{include file="package_auth/auth_detail"}
</script>
<!-- JavaScript 部分 -->
<script>
// 全局配置
var CONFIG = {
pageSize: 10,
loadingTime: 1000,
debounceDelay: 300
};
// 消息提示
var MSG = {
networkError: '网络错误,请稍后重试!',
selectAdmin: '请选择管理员',
selectPackage: '请选择包名',
confirmClear: '确定要清空权限吗?此操作不可恢复!',
confirmBatchAuth: '确定要批量授权吗?<br>此操作将覆盖已有权限!',
getUserFailed: '获取用户信息失败',
authSuccess: '授权成功!',
clearSuccess: '清空成功!'
};
// 防抖函数
function debounce(fn, delay) {
let timer = null;
return function() {
let context = this, args = arguments;
clearTimeout(timer);
timer = setTimeout(function() {
fn.apply(context, args);
}, delay);
};
}
// 成功提示
function showSuccess(msg, callback) {
layer.msg(msg, {
icon: 1,
time: CONFIG.loadingTime,
shade: 0.3
}, callback);
}
// 错误提示
function showError(msg) {
layer.msg(msg, {
icon: 2,
time: 2000,
shade: 0.3
});
}
// 表格事件处理对象
var TableEventHandler = {
// 工具条事件
tool: function(obj, callback) {
var event = obj.event;
var handlers = {
remove: function() {
layer.confirm('确定要移除此包名权限吗?', {
title: '操作确认',
btn: ['确定', '取消']
}, function(index) {
callback && callback(obj, index);
});
}
};
handlers[event] && handlers[event]();
},
// 表头工具栏事件
toolbar: function(obj, callback) {
var event = obj.event;
var handlers = {
batchRemove: function() {
var checkStatus = table.checkStatus(obj.config.id);
if (!checkStatus.data.length) {
layer.msg('请选择要移除的包名', {icon: 2});
return;
}
layer.confirm('确定要移除选中的 ' + checkStatus.data.length + ' 个包名权限吗?', {
title: '批量移除确认',
btn: ['确定', '取消']
}, function(index) {
callback && callback(checkStatus.data, index);
});
}
};
handlers[event] && handlers[event]();
}
};
// 表格缓存对象
var TableCache = {
data: {},
set: function(key, value) {
this.data[key] = value;
// 最多缓存100条数据
var keys = Object.keys(this.data);
if (keys.length > 100) {
delete this.data[keys[0]];
}
},
get: function(key) {
return this.data[key];
},
clear: function() {
this.data = {};
}
};
// 基础表格配置
var baseTableConfig = {
page: true,
limit: 15,
loading: true,
text: {
none: '暂无数据'
},
size: 'sm'
};
layui.use(['table', 'form', 'laydate'], function() {
let table = layui.table,
form = layui.form,
laydate = layui.laydate;
// 将 table 绑定到全局,以便其他函数使用
window.packageAuthTable = table;
// 初始化时间选择器
laydate.render({
elem: '#dateRange',
type: 'datetime',
range: true,
trigger: 'click',
done: function(value, date){
if(value) {
var dates = value.split(' - ');
table.reload('packageTable', {
where: {
start_time: dates[0],
end_time: dates[1]
},
page: {curr: 1}
});
}
}
});
// 重置筛选
$('#resetFilter').on('click', function(){
$('#searchPackages').val('');
$('#dateRange').val('');
table.reload('packageTable', {
where: {
keyword: '',
start_time: '',
end_time: ''
},
page: {curr: 1}
});
});
// 优化搜索功能
$('#searchPackages').on('input', debounce(function(){
var value = this.value;
var dateRange = $('#dateRange').val();
var dates = dateRange ? dateRange.split(' - ') : ['', ''];
table.reload('packageTable', {
where: {
keyword: value,
start_time: dates[0],
end_time: dates[1]
},
page: {curr: 1}
});
}, 300));
// 初始化包名权限配置表格
table.render({
elem: '#packageTable',
url: '{:url("getPackageList")}',
method: 'post',
toolbar: true,
defaultToolbar: ['filter', 'exports'],
cols: [[
{type: 'checkbox', width: 50},
{field: 'package_name', title: '包名', sort: true},
{field: 'name', title: '应用名称'}
]],
page: true,
limit: 20,
limits: [10, 20, 50, 100],
height: 'full-120',
text: {
none: '暂无包名数据'
},
size: 'sm'
});
// 管理员全选
form.on('checkbox(checkAllUsers)', function(data){
var checked = data.elem.checked;
$('input[name="user_ids[]"]').prop('checked', checked);
form.render('checkbox');
});
// 启用包名全选
form.on('checkbox(checkAllEnabled)', function(data){
var checked = data.elem.checked;
$(data.elem).closest('.layui-form-item').nextUntil('.layui-form-item:has(label:contains("禁用包名"))')
.find('input[type="checkbox"]').prop('checked', checked);
form.render('checkbox');
});
// 禁用包全选
form.on('checkbox(checkAllDisabled)', function(data){
var checked = data.elem.checked;
$(data.elem).closest('.layui-form-item').nextAll()
.find('input[type="checkbox"]').prop('checked', checked);
form.render('checkbox');
});
// 批量授权按钮事件
form.on('submit(batchAuth)', function(data) {
let userIds = [];
$('input[name="user_ids[]"]:checked').each(function() {
userIds.push($(this).val());
});
if(userIds.length === 0) {
layer.msg('请选择要授权的管理员', {icon: 2});
return false;
}
// 使用全局的 table 变量
let checkStatus = window.packageAuthTable.checkStatus('packageTable');
let packageIds = checkStatus.data.map(item => item.id);
if(packageIds.length === 0) {
layer.msg('请选择要授权的包名', {icon: 2});
return false;
}
layer.confirm('确定要批量授权吗?<br>此操作将覆盖已有权限!', {
title: '操作确认',
btn: ['确定', '取消'],
icon: 3
}, function(index) {
let loadIndex = layer.load(2);
$.ajax({
url: '{:url("batchAuth")}',
type: 'post',
data: {
user_ids: userIds,
package_ids: packageIds
},
success: function(res) {
layer.close(loadIndex);
if(res.code === 1) {
layer.msg(res.info, {icon: 1, time: 1000}, function() {
location.reload();
});
} else {
layer.msg(res.info, {icon: 2, time: 2000});
}
layer.close(index);
}
});
});
return false;
});
// 清空权限按钮事件
$('#clearAuth').click(function() {
let userIds = [];
$('input[name="user_ids[]"]:checked').each(function() {
userIds.push($(this).val());
});
if(userIds.length === 0) {
layer.msg('请选择要清空权限的管理员', {icon: 2});
return false;
}
layer.confirm('确定要清空所选管理员的权限吗?<br>此操作不可恢复!', {
title: '危险操作',
btn: ['确定清空', '取消'],
icon: 2
}, function(index) {
let loadIndex = layer.load(2);
$.ajax({
url: '{:url("clearAuth")}',
type: 'post',
data: {
user_ids: userIds
},
success: function(res) {
layer.close(loadIndex);
if(res.code === 1) {
layer.msg(res.info, {icon: 1, time: 1000}, function() {
location.reload();
});
} else {
layer.msg(res.info, {icon: 2, time: 2000});
}
layer.close(index);
}
});
});
});
// 操作指南展开/收起
$('#showGuide').click(function(){
var $content = $('.guide-content');
if ($content.hasClass('layui-hide')) {
$content.removeClass('layui-hide').hide().slideDown();
// 存储状态到 localStorage
localStorage.setItem('packageAuthGuideShow', '1');
} else {
$content.slideUp(function(){
$(this).addClass('layui-hide');
});
// 存储状态到 localStorage
localStorage.setItem('packageAuthGuideShow', '0');
}
});
// 页面加载时检查是否需要显示操作指南
if (localStorage.getItem('packageAuthGuideShow') === '1') {
$('.guide-content').removeClass('layui-hide');
}
// 初始化按钮提示
$('[lay-tips]').each(function(){
var tips = $(this).attr('lay-tips');
if(tips) {
layui.use('layer', function(){
var layer = layui.layer;
$(this).on('mouseenter', function(){
this.index = layer.tips(tips, this, {
tips: [1, '#3595CC'],
time: 0
});
}).on('mouseleave', function(){
layer.close(this.index);
});
}.bind(this));
}
});
});
// 辅助函数
function viewAuth(userId, username){
$('[data-user="' + userId + '"][lay-event="showAuth"]').click();
}
function clearSingleAuth(userId){
layer.confirm('确定要清空此管理员的权限吗?', {
title: '操作确认',
btn: ['确定', '取消'],
icon: 3,
skin: 'layui-layer-molv'
}, function(index){
var loadIndex = layer.load(2);
$.ajax({
url: '{:url("clearAuth")}',
type: 'post',
data: {
user_ids: [userId]
},
success: function(res){
layer.close(loadIndex);
if(res.code === 1){
layer.msg(res.info, {icon: 1, time: 1000}, function(){
location.reload();
});
} else {
layer.msg(res.info, {icon: 2, time: 2000});
}
layer.close(index);
}
});
});
}
// 将权限详情相关的函数抽取出来
function showAuthDetail(userId, username) {
layer.open({
type: 1,
title: username + ' 的权限配置',
area: ['1200px', '80%'],
offset: '50px',
moveType: 1,
content: $('#authDetailTpl').html(),
success: function(layero) {
window.initAuthDetailTables(userId);
// 隐藏默认的关闭按钮
$(layero).find('.layui-layer-setwin').css({
'display': 'none'
});
}
});
}
// 绑定查看权限按钮事件
$(document).on('click', '[lay-event="showAuth"]', function() {
let userId = $(this).data('user');
let username = $(this).closest('.layui-form-item').find('input[type="checkbox"]').attr('title');
showAuthDetail(userId, username);
});
</script>
<!-- 添加一些样式 -->
<style>
.guide-content ol {
padding-left: 20px;
margin: 10px 0;
}
.guide-content ol li {
line-height: 28px;
color: #666;
}
.guide-content h4 {
margin: 5px 0;
color: #333;
}
.layui-elem-quote {
margin-bottom: 10px;
}
.context-menu-layer {
box-shadow: 0 2px 8px rgba(0,0,0,.1);
background: none;
}
.context-menu {
background: #fff;
border-radius: 2px;
padding: 5px 0;
}
.context-menu .menu-item {
padding: 8px 15px;
cursor: pointer;
font-size: 14px;
color: #666;
transition: all .2s;
}
.context-menu .menu-item:hover {
background: #f2f2f2;
color: #009688;
}
.context-menu .menu-item .layui-icon {
margin-right: 5px;
font-size: 14px;
}
.context-menu .menu-item[onclick*="clearSingleAuth"] {
color: #FF5722;
}
.context-menu .menu-item[onclick*="clearSingleAuth"]:hover {
background: #fff1f0;
}
/* 管理员列表样式优化 */
.user-list .layui-form-item {
padding: 8px 0;
margin-bottom: 5px;
border-bottom: 1px solid #f0f0f0;
}
.user-list .layui-form-item:last-child {
border-bottom: none;
}
.user-list .layui-btn-sm {
height: 28px;
line-height: 28px;
padding: 0 12px;
}
.user-list .layui-badge {
font-size: 12px;
font-weight: normal;
}
.user-list .layui-btn-normal {
background-color: #1E9FFF;
}
.user-list .layui-btn-warm {
background-color: #FFB800;
}
.user-list .layui-icon {
font-size: 14px;
margin-right: 3px;
}
/* 美化搜索框 */
.search-box .layui-input {
height: 36px;
line-height: 36px;
border-radius: 2px;
}
/* 美化全选框 */
.layui-form-item .layui-form-checkbox[lay-skin=primary] {
margin-top: 0;
}
/* 右侧顶部按钮样式优化 */
.header-btns {
margin: 3px 0;
padding-right: 5px;
}
.header-btns .layui-btn {
height: 34px;
line-height: 34px;
padding: 0 15px;
font-size: 13px;
margin-left: 8px;
}
.header-btns .layui-btn .layui-icon {
font-size: 14px;
margin-right: 3px;
}
.header-btns .layui-btn-normal {
background-color: #1E9FFF;
}
.header-btns .layui-btn-normal:hover {
background-color: #0d8aff;
}
.header-btns .layui-btn-danger {
background-color: #FF5722;
}
.header-btns .layui-btn-danger:hover {
background-color: #ff4208;
}
/* 提示框样式优化 */
.layui-layer-tips {
font-size: 13px !important;
}
.layui-layer-tips .layui-layer-content {
padding: 8px 12px !important;
line-height: 1.5 !important;
border-radius: 2px !important;
}
.layui-layer-tips i.layui-layer-TipsT {
border-right-color: #3595CC !important;
}
/* 危险操作弹窗样式 */
.layer-danger .layui-layer-title {
background-color: #FF5722 !important;
color: #fff !important;
}
.layer-danger .layui-layer-btn0 {
background-color: #FF5722 !important;
border-color: #FF5722 !important;
}
/* 工具栏按钮样式 */
.auth-table-container .layui-table-tool .layui-btn {
margin-right: 8px;
}
.auth-table-container .layui-table-tool .layui-btn:last-child {
margin-right: 0;
}
.auth-table-container .layui-table-tool .layui-btn-danger:hover {
background-color: #ff4208;
}
/* 正常操作弹窗样式 */
.layer-normal .layui-layer-title {
background-color: #1E9FFF !important;
color: #fff !important;
}
.layer-normal .layui-layer-btn0 {
background-color: #1E9FFF !important;
border-color: #1E9FFF !important;
}
/* 工具栏按钮样式补充 */
.auth-table-container .layui-table-tool .layui-btn-normal {
background-color: #1E9FFF;
}
.auth-table-container .layui-table-tool .layui-btn-normal:hover {
background-color: #0d8aff;
}
</style>
{/block}

View File

@@ -0,0 +1,258 @@
{extend name="../../admin/view/main"}
{block name="content"}
<form class="layui-form layui-card" action="{:request()->url()}" data-auto="true" method="post">
<div class="layui-card-body">
<div class="layui-form-item">
<label class="layui-form-label">选择包名</label>
<div class="layui-input-block">
<div id="packageSelect"></div>
<input type="hidden" name="package_id" id="selectedPackageId">
<input type="hidden" name="package_name" id="selectedPackageName">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">事件名称</label>
<div class="layui-input-block">
<input type="text" name="event_name" required lay-verify="required" placeholder="请输入事件名称" class="layui-input">
<tip>例如install, register, purchase </tip>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">回传地址</label>
<div class="layui-input-block">
<input type="text" name="callback_url" required lay-verify="required|callback_url" placeholder="请输入回传地址" class="layui-input">
<tip>
完整的回传接口地址例如http://api.example.com/callback<br>
支持变量替换,如 {package_name}, {event_name} <br>
详细说明请查看完整回传指南
</tip>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">状态</label>
<div class="layui-input-block">
<input type="radio" name="status" value="1" title="启用" checked>
<input type="radio" name="status" value="0" title="停用">
</div>
</div>
</div>
<div class="hr-line-dashed"></div>
<div class="layui-form-item text-center">
<button class="layui-btn" lay-submit lay-filter="formSubmit">保存数据</button>
<button class="layui-btn layui-btn-danger" type="button" data-close>取消添加</button>
</div>
</form>
{/block}
{block name="script"}
<style>
.layui-form tip {
display: block;
padding: 10px;
margin-top: 10px;
color: #666;
background-color: #f8f8f8;
border-radius: 2px;
line-height: 1.8;
}
.layui-badge {
margin: 2px;
font-family: Consolas, monospace;
}
</style>
<script>
$(function () {
layui.use(['form', 'xmSelect', 'layer'], function () {
let form = layui.form;
let layer = layui.layer;
let existingConfig = null; // 用于存储已存在的配置信息
// 定义包名选择器
const packageSelect = xmSelect.render({
el: '#packageSelect',
name: 'package_id',
radio: true,
clickClose: true,
filterable: true,
tips: '请选择包名',
data: [],
direction: 'auto',
model: { label: { type: 'text' } },
on: function(data) {
if(data.arr.length > 0) {
let selected = data.arr[0];
$('#selectedPackageId').val(selected.value);
$('#selectedPackageName').val(selected.package_name);
// 选择包名后,如果已有事件名称,则检查是否存在
checkEventExists();
} else {
$('#selectedPackageId').val('');
$('#selectedPackageName').val('');
}
form.render();
}
});
let checkTimer = null; // 添加防抖定时器
// 监听事件名称输入框
$('input[name="event_name"]').on('input propertychange', function() {
let currentValue = $(this).val();
console.log('事件名称输入:', currentValue);
// 使用防抖,避免频繁请求
if (checkTimer) clearTimeout(checkTimer);
checkTimer = setTimeout(function() {
checkEventExists(currentValue);
}, 300);
});
// 检查事件是否存在
function checkEventExists(eventValue) {
// 获取当前弹窗内的元素
let $dialog = $('.layui-layer-page').last();
let packageId = $('#selectedPackageId').val();
let $eventInput = $dialog.find('input[name="event_name"]');
// 清除现有提示和验证状态
$eventInput.removeClass('layui-form-danger');
$eventInput.nextAll('.layui-form-mid,.layui-word-aux').remove();
$dialog.find('tip').show();
// 重置existingConfig
existingConfig = null;
if (packageId && eventValue) {
// 直接获取配置详情
$.ajax({
url: '{:url("getExistingConfig")}',
type: 'POST',
data: {
package_id: packageId,
event_name: eventValue
},
dataType: 'json',
success: function(configRes) {
if (configRes.code === 1 && configRes.data) {
// 保存已存在的配置信息
existingConfig = configRes.data; // 保存完整的配置信息
// 添加验证失败样式
$eventInput.addClass('layui-form-danger');
// 隐藏当前弹窗内的tip
$dialog.find('.layui-input-block tip').hide();
// 添加提示信息
$eventInput.after(`
<div class="layui-form-mid" style="margin-top: 5px;">
<div style="color: #FF5722; font-weight: bold; margin-bottom: 5px;">
<i class="layui-icon layui-icon-warning" style="font-size: 16px; margin-right: 5px;"></i>
该包名下已存在此事件名称的配置,提交时将覆盖现有配置
</div>
<div style="color: #666; background: #f8f8f8; padding: 8px; border-radius: 2px; margin-top: 5px;">
<i class="layui-icon layui-icon-link" style="margin-right: 5px;"></i>
已配置的回调地址:${configRes.data.callback_url}
</div>
</div>
`);
}
}
});
}
}
// 加载包名列表
function loadPackageList() {
$.get('{:url("searchPackages")}', {
init: 1
}, function(res) {
if (res.code === 1) {
let data = res.data.map(function(item) {
return {
name: item.package_name + ' (' + item.name + ')',
value: item.id,
package_name: item.package_name
};
});
packageSelect.update({
data: data,
autoRow: true
});
}
}, 'json');
}
// 初始加载包名列表
loadPackageList();
// 表单验证
form.verify({
callback_url: function(value) {
if (!/^https?:\/\/.+/.test(value)) {
return '请输入正确的URL地址';
}
}
});
// 修改表单提交处理
form.on('submit(formSubmit)', function(data) {
// 如果存在配置,显示确认框
if (existingConfig) {
layer.confirm('该包名下已存在此事件配置,是否确认覆盖?', {
title: '覆盖确认',
btn: ['确认覆盖', '取消'],
icon: 3
}, function(index) {
// 确认覆盖,将请求转为更新操作
let formData = data.field;
formData.id = existingConfig.id; // 添加已存在配置的ID
$.ajax({
url: '{:url("edit")}',
type: 'POST',
data: formData,
dataType: 'json',
success: function(res) {
if (res.code === 1) {
layer.msg(res.info, {icon: 1, time: 1500}, function() {
parent.layer.close(parent.layer.getFrameIndex(window.name));
parent.location.reload();
});
} else {
layer.msg(res.info, {icon: 2, time: 1500});
}
}
});
layer.close(index);
});
} else {
// 如果不存在配置,使用默认表单提交
let formData = data.field;
$.ajax({
url: '{:url("add")}',
type: 'POST',
data: formData,
dataType: 'json',
success: function(res) {
if (res.code === 1) {
layer.msg(res.info, {icon: 1, time: 1500}, function() {
parent.layer.close(parent.layer.getFrameIndex(window.name));
parent.location.reload();
});
} else {
layer.msg(res.info, {icon: 2, time: 1500});
}
}
});
}
return false; // 阻止表单默认提交
});
});
});
</script>
{/block}

View File

@@ -0,0 +1,160 @@
{extend name="../../admin/view/main"}
{block name="content"}
<form class="layui-form layui-card" action="{:request()->url()}" data-auto="true" method="post">
<div class="layui-card-body">
<input type="hidden" name="id" value="{$vo.id|default=''}">
<div class="layui-form-item">
<label class="layui-form-label">选择包名</label>
<div class="layui-input-block">
<div id="packageSelect"></div>
<input type="hidden" name="package_id" id="selectedPackageId" value="{$vo.package_id|default=''}">
<input type="hidden" name="package_name" id="selectedPackageName" value="{$vo.package_name|default=''}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">事件名称</label>
<div class="layui-input-block">
<input type="text" name="event_name" required lay-verify="required" placeholder="请输入事件名称" class="layui-input" value="{$vo.event_name|default=''}">
<tip>例如install, register, purchase </tip>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">回传地址</label>
<div class="layui-input-block">
<input type="text" name="callback_url" required lay-verify="required|callback_url" placeholder="请输入回传地址" class="layui-input" value="{$vo.callback_url|default=''}">
<tip>
完整的回传接口地址例如http://api.example.com/callback<br>
支持变量替换,如 {package_name}, {event_name} <br>
详细说明请查看完整回传指南
</tip>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">状态</label>
<div class="layui-input-block">
<input type="radio" name="status" value="1" title="启用" {if $vo.status eq 1}checked{/if}>
<input type="radio" name="status" value="0" title="停用" {if $vo.status eq 0}checked{/if}>
</div>
</div>
</div>
<div class="hr-line-dashed"></div>
<div class="layui-form-item text-center">
<button class="layui-btn" lay-submit lay-filter="formSubmit">保存数据</button>
<button class="layui-btn layui-btn-danger" type="button" data-close>取消编辑</button>
</div>
</form>
{/block}
{block name="script"}
<style>
.layui-form tip {
display: block;
padding: 10px;
margin-top: 10px;
color: #666;
background-color: #f8f8f8;
border-radius: 2px;
line-height: 1.8;
}
.layui-badge {
margin: 2px;
font-family: Consolas, monospace;
}
</style>
<script>
$(function () {
layui.use(['form', 'xmSelect'], function () {
let form = layui.form;
// 定义包名选择器
const packageSelect = xmSelect.render({
el: '#packageSelect',
name: 'package_id',
radio: true,
clickClose: true,
filterable: true,
tips: '请选择包名',
data: [],
direction: 'auto',
model: { label: { type: 'text' } },
initValue: [{$vo.package_id|default=0}], // 设置初始选中值
on: function(data) {
if(data.arr.length > 0) {
let selected = data.arr[0];
$('#selectedPackageId').val(selected.value);
$('#selectedPackageName').val(selected.package_name);
} else {
$('#selectedPackageId').val('');
$('#selectedPackageName').val('');
}
form.render();
}
});
// 加载包名列表并选中当前包名
function loadPackageList() {
$.get('{:url("searchPackages")}', {
init: 1,
edit_id: '{$vo.id|default=0}' // 传递当前编辑的ID
}, function(res) {
if (res.code === 1) {
let data = res.data.map(function(item) {
return {
name: item.package_name + ' (' + item.name + ')',
value: item.id,
package_name: item.package_name,
selected: item.id == '{$vo.package_id|default=0}' // 标记当前选中项
};
});
packageSelect.update({
data: data,
autoRow: true
});
}
}, 'json');
}
// 初始加载包名列表
loadPackageList();
// 表单验证
form.verify({
callback_url: function(value) {
if (!/^https?:\/\/.+/.test(value)) {
return '请输入正确的URL地址';
}
}
});
// 表单提交
form.on('submit(formSubmit)', function(data) {
let formData = data.field;
$.ajax({
url: '{:url("edit")}',
type: 'POST',
data: formData,
dataType: 'json',
success: function(res) {
if (res.code === 1) {
layer.msg(res.info, {icon: 1, time: 1500}, function() {
parent.layer.close(parent.layer.getFrameIndex(window.name));
parent.location.reload();
});
} else {
layer.msg(res.info, {icon: 2, time: 1500});
}
}
});
return false;
});
});
});
</script>
{/block}

View File

@@ -0,0 +1,232 @@
{extend name="../../admin/view/main"}
{block name="content"}
<!-- 操作指南开始 -->
<div class="think-box-shadow" style="margin-bottom: 15px; padding: 15px;">
<h3 class="layui-inline" style="margin-right: 15px;">回传配置指南</h3>
<button class="layui-btn layui-btn-sm" id="showGuide">展开/收起</button>
<div class="guide-content layui-hide" style="margin-top: 10px;">
<div class="layui-card">
<div class="layui-card-body">
<div class="layui-elem-quote" style="border-left: 5px solid #009688;">
<h4>基本说明:</h4>
<p>本页面用于管理事件回传配置,支持变量替换功能,可以灵活配置回传地址。</p>
</div>
<div class="layui-elem-quote" style="border-left: 5px solid #FFB800;">
<h4>支持的变量:</h4>
<ol>
<li><code>{package_name}</code> - 应用包名com.example.app</li>
<li><code>{event_name}</code> - 事件名称install, register</li>
<li><code>{event_time}</code> - 事件发生时间格式YYYY-MM-DD HH:mm:ss</li>
<li><code>{gaid}</code> - 广告ID</li>
<li><code>{network_name}</code> - 网络名称AppsFlyer, Kochava</li>
</ol>
</div>
<div class="layui-elem-quote" style="border-left: 5px solid #FF5722;">
<h4>配置示例:</h4>
<ol>
<li>基础URLhttp://api.example.com/callback</li>
<li>
完整示例http://api.example.com/callback?package={package_name}&event={event_name}&time={event_time}&gaid={gaid}&network={network_name}
</li>
<li>实际回传时,花括号中的变量会被替换为实际值</li>
<li>所有特殊字符会自动进行URL编码</li>
</ol>
</div>
<div class="layui-elem-quote" style="border-left: 5px solid #01AAED;">
<h4>注意事项:</h4>
<ol>
<li>变量名称必须使用大括号{}包裹,且区分大小写</li>
<li>同一个包名下不能配置重复的事件名称</li>
<li>回传地址必须以http://或https://开头</li>
<li>建议在测试环境验证回传地址的正确性</li>
<li>可以通过状态开关临时停用某个回传配置</li>
</ol>
</div>
</div>
</div>
</div>
</div>
<!-- 操作指南结束 -->
<!-- 表单搜索区域 -->
<div class="think-box-shadow">
<div class="layui-row">
<div class="layui-col-md11">
<form class="layui-form layui-form-pane form-search" action="">
<div class="layui-form-item layui-inline">
<label class="layui-form-label">包名</label>
<div class="layui-input-inline">
<input name="package_name" placeholder="请输入包名" class="layui-input">
</div>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">事件名称</label>
<div class="layui-input-inline">
<input name="event_name" placeholder="请输入事件名称" class="layui-input">
</div>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">状态</label>
<div class="layui-input-inline">
<select name="status" lay-search>
<option value="">所有状态</option>
<option value="1">启用</option>
<option value="0">停用</option>
</select>
</div>
</div>
<div class="layui-form-item layui-inline">
<button class="layui-btn layui-btn-primary" lay-submit lay-filter="search_form">
<i class="layui-icon">&#xe615;</i> 搜 索
</button>
</div>
</form>
</div>
<div class="layui-col-md1 text-right">
<!--{if auth("add")}-->
<button class='layui-btn layui-btn-normal' data-modal='{:url("add")}'
data-title="添加回传配置">
<i class="layui-icon layui-icon-add-circle"></i> 添加事件
</button>
<!--{/if}-->
</div>
</div>
</div>
<!-- 数据表格区域 -->
<div class="think-box-shadow">
<table class="layui-table" id="CallbackTable" data-url="{:url('get_list')}"
data-target-search="form.form-search"></table>
</div>
<!-- 数据操作工具条模板 -->
<script type="text/html" id="toolbar">
<!--{if auth("edit")}-->
<a class="layui-btn layui-btn-sm" data-modal="{:url('edit')}?id={{d.id}}" data-title="编辑回传配置">
<i class="layui-icon layui-icon-edit"></i> 编辑
</a>
<!--{/if}-->
<!--{if auth("remove")}-->
<a class="layui-btn layui-btn-sm layui-btn-danger" data-action="{:url('remove')}" data-value="id#{{d.id}}"
data-confirm="确定要删除此配置吗?">
<i class="layui-icon layui-icon-delete"></i> 删除
</a>
<!--{/if}-->
</script>
<!-- 数据状态切换模板 -->
<script type="text/html" id="statusTpl">
<!--{if auth("state")}-->
<input type="checkbox" value="{{d.id}}" lay-skin="switch" lay-text="启用|停用" lay-filter="status"
{{d.status>0?'checked':''}}>
<!--{else}-->
{{d.status ? '<b class="color-green">启用</b>' : '<b class="color-red">停用</b>'}}
<!--{/if}-->
</script>
{/block}
{block name="script"}
<script>
$(function () {
let table; // 定义表格变量
// 初始化表格组件
table = $('#CallbackTable').layTable({
url: '{:url("get_list")}',
method: 'get',
even: true,
page: true,
limit: 15,
limits: [10, 15, 20, 25, 50, 100],
cols: [[
{field: 'id', title: 'ID', width: 80, align: 'center'},
{field: 'package_name', title: '包名', minWidth: 200},
{field: 'event_name', title: '事件名称', minWidth: 150},
{field: 'callback_url', title: '回传地址', minWidth: 300},
{field: 'status', title: '状态', width: 100, align: 'center', templet: '#statusTpl'},
{field: 'create_at', title: '创建时间', width: 180, align: 'center'},
{title: '操作', toolbar: '#toolbar', width: 180, align: 'center', fixed: 'right'}
]],
text: {
none: '暂无回传配置数据'
}
});
// 数据状态切换操作
form.on('switch(status)', function (obj) {
$.ajax({
url: '{:url("state")}',
type: 'POST',
data: {
id: obj.value,
status: obj.elem.checked ? 1 : 0
},
dataType: 'json',
success: function (res) {
if (res.code === 1) {
layer.msg(res.info, {icon: 1});
// 仅重载表格数据
table.reload('CallbackTable', {
page: {
curr: $(".layui-laypage-em").next().html() // 保持在当前页
}
});
} else {
layer.msg(res.info, {icon: 2});
// 状态切换失败回滚switch状态
obj.elem.checked = !obj.elem.checked;
form.render('checkbox');
}
},
error: function () {
layer.msg('操作失败,请重试', {icon: 2});
// 发生错误时也回滚switch状态
obj.elem.checked = !obj.elem.checked;
form.render('checkbox');
}
});
});
// 操作指南展开/收起
$('#showGuide').on('click', function () {
$('.guide-content').toggleClass('layui-hide');
});
});
</script>
<style>
/* 操作指南样式 */
.guide-content .layui-elem-quote {
margin: 10px 0;
padding: 15px;
background-color: #f8f8f8;
}
.guide-content h4 {
margin-bottom: 10px;
font-weight: bold;
}
.guide-content code {
padding: 2px 4px;
background-color: #f1f1f1;
border-radius: 3px;
color: #333;
font-family: Consolas, monospace;
}
.guide-content ol li {
margin: 5px 0;
color: #666;
}
</style>
{/block}

View File

@@ -0,0 +1,58 @@
{extend name="../../admin/view/main"}
{block name="content"}
<form class="layui-form layui-card" action="javascript:void(0)" autocomplete="off">
<div class="layui-card-body padding-left-40">
<div class="layui-form-item layui-row layui-col-space15">
<label class="layui-col-xs2 text-right">包名</label>
<div class="layui-col-xs10">
<div class="layui-input-line">{$vo.package_name|default=''}</div>
</div>
</div>
<div class="layui-form-item layui-row layui-col-space15">
<label class="layui-col-xs2 text-right">事件名称</label>
<div class="layui-col-xs10">
<div class="layui-input-line">{$vo.event_name|default=''}</div>
</div>
</div>
<div class="layui-form-item layui-row layui-col-space15">
<label class="layui-col-xs2 text-right">回传URL模板</label>
<div class="layui-col-xs10">
<div class="layui-input-line">{$vo.callback_url|default=''}</div>
</div>
</div>
<div class="layui-form-item layui-row layui-col-space15">
<label class="layui-col-xs2 text-right">实际回传URL</label>
<div class="layui-col-xs10">
<div class="layui-input-line">{$vo.final_url|default=''}</div>
</div>
</div>
<div class="layui-form-item layui-row layui-col-space15">
<label class="layui-col-xs2 text-right">响应状态码</label>
<div class="layui-col-xs10">
<div class="layui-input-line">{$vo.response_code|default=''}</div>
</div>
</div>
<div class="layui-form-item layui-row layui-col-space15">
<label class="layui-col-xs2 text-right">响应内容</label>
<div class="layui-col-xs10">
<pre class="layui-code">{$vo.response_body|default=''}</pre>
</div>
</div>
<div class="layui-form-item layui-row layui-col-space15">
<label class="layui-col-xs2 text-right">状态</label>
<div class="layui-col-xs10">
<div class="layui-input-line">
{if $vo.status==1}<span class="layui-badge layui-bg-green">成功</span>{else}<span class="layui-badge layui-bg-red">失败</span>{/if}
</div>
</div>
</div>
<div class="layui-form-item layui-row layui-col-space15">
<label class="layui-col-xs2 text-right">回传时间</label>
<div class="layui-col-xs10">
<div class="layui-input-line">{$vo.callback_time|default=''}</div>
</div>
</div>
</div>
</form>
{/block}

View File

@@ -0,0 +1,87 @@
{extend name="../../admin/view/main"}
{block name="button"}
{/block}
{block name="content"}
<div class="think-box-shadow">
<fieldset>
<legend>条件搜索</legend>
<form class="layui-form layui-form-pane form-search" action="javascript:void(0)" method="get" autocomplete="off">
<div class="layui-form-item layui-inline">
<label class="layui-form-label">包名</label>
<div class="layui-input-inline">
<input name="package_name" value="{$get.package_name|default=''}" placeholder="请输入包名" class="layui-input">
</div>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">事件名称</label>
<div class="layui-input-inline">
<input name="event_name" value="{$get.event_name|default=''}" placeholder="请输入事件名称" class="layui-input">
</div>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">推送ID</label>
<div class="layui-input-inline">
<input name="push_record_id" value="{$get.push_record_id|default=''}" placeholder="请输入推送ID" class="layui-input">
</div>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">状态</label>
<div class="layui-input-inline">
<select name="status" class="layui-select">
<option value="">所有状态</option>
<option value="1">成功</option>
<option value="0">失败</option>
</select>
</div>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">回传时间</label>
<div class="layui-input-inline">
<input data-date-range name="callback_time" value="{$get.callback_time|default=''}" placeholder="请选择回传时间" class="layui-input">
</div>
</div>
<div class="layui-form-item layui-inline">
<button class="layui-btn layui-btn-primary"><i class="layui-icon">&#xe615;</i> 搜 索</button>
</div>
</form>
</fieldset>
<table id="RecordTable" data-url="{:sysuri('get_list')}" data-target-search="form.form-search"></table>
</div>
{/block}
{block name="script"}
<script>
$(function () {
$('#RecordTable').layTable({
even: true, height: 'full',
sort: {field: 'id', type: 'desc'},
cols: [[
{field: 'id', title: 'ID', width: 80, sort: true},
{field: 'push_record_id', title: '推送ID', width: 150, templet: function(d){
var source = d.source_type || '-';
return '<a class="layui-link" data-title="查看推送详情" data-open="{:url("package_push_records/detail")}?id='+ d.push_record_id +'">'+ d.push_record_id +'('+ source +')</a>';
}},
{field: 'package_name', title: '包名', minWidth: 100},
{field: 'event_name', title: '事件名称', minWidth: 100},
{field: 'callback_url', title: '回传URL模板', minWidth: 200, templet: function(d){
return '<div class="layui-elip" title="'+ d.callback_url +'">'+ d.callback_url +'</div>';
}},
{field: 'final_url', title: '实际回传URL', minWidth: 200, templet: function(d){
return '<div class="layui-elip" title="'+ d.final_url +'">'+ d.final_url +'</div>';
}},
{field: 'response_code', title: '响应码', width: 100},
{field: 'status', title: '状态', width: 100, templet: function(d){
return d.status == 1 ? '<span class="layui-badge layui-bg-green">成功</span>' : '<span class="layui-badge layui-bg-red">失败</span>';
}},
{field: 'callback_time', title: '回传时间', width: 180, sort: true},
{title: '操作', toolbar: '#toolbar', width: 100, align: 'center', fixed: 'right'}
]]
});
});
</script>
<script type="text/html" id="toolbar">
<a class="layui-btn layui-btn-xs" data-title="查看详情" data-open="{:url('detail')}?id={{d.id}}">详情</a>
</script>
{/block}

View File

@@ -0,0 +1,44 @@
{extend name="../../admin/view/main"}
{block name="content"}
<form class="layui-form layui-card" action="javascript:void(0)" autocomplete="off">
<div class="layui-card-body padding-left-40">
<div class="layui-form-item layui-row layui-col-space15">
<label class="layui-col-xs2 text-right">来源</label>
<div class="layui-col-xs10">
<div class="layui-input-line">{$vo.source_type|default=''}</div>
</div>
</div>
<div class="layui-form-item layui-row layui-col-space15">
<label class="layui-col-xs2 text-right">包名</label>
<div class="layui-col-xs10">
<div class="layui-input-line">{$vo.package_name|default=''}</div>
</div>
</div>
<div class="layui-form-item layui-row layui-col-space15">
<label class="layui-col-xs2 text-right">事件名称</label>
<div class="layui-col-xs10">
<div class="layui-input-line">{$vo.event_name|default=''}</div>
</div>
</div>
<div class="layui-form-item layui-row layui-col-space15">
<label class="layui-col-xs2 text-right">原始请求URL</label>
<div class="layui-col-xs10">
<div class="layui-input-line">{$vo.original_url|default=''}</div>
</div>
</div>
<div class="layui-form-item layui-row layui-col-space15">
<label class="layui-col-xs2 text-right">接收时间</label>
<div class="layui-col-xs10">
<div class="layui-input-line">{$vo.receive_time|default=''}</div>
</div>
</div>
<div class="layui-form-item layui-row layui-col-space15">
<label class="layui-col-xs2 text-right">创建时间</label>
<div class="layui-col-xs10">
<div class="layui-input-line">{$vo.created_at|default=''}</div>
</div>
</div>
</div>
</form>
{/block}

View File

@@ -0,0 +1,72 @@
{extend name="../../admin/view/main"}
{block name="button"}
{/block}
{block name="content"}
<div class="think-box-shadow">
<fieldset>
<legend>条件搜索</legend>
<form class="layui-form layui-form-pane form-search" action="javascript:void(0)" method="get" autocomplete="off">
<div class="layui-form-item layui-inline">
<label class="layui-form-label">包名</label>
<div class="layui-input-inline">
<input name="package_name" value="{$get.package_name|default=''}" placeholder="请输入包名" class="layui-input">
</div>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">事件名称</label>
<div class="layui-input-inline">
<input name="event_name" value="{$get.event_name|default=''}" placeholder="请输入事件名称" class="layui-input">
</div>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">来源</label>
<div class="layui-input-inline">
<select name="source_type" class="layui-select">
<option value="">所有来源</option>
<option value="AppsFlyer">AppsFlyer</option>
<option value="Kochava">Kochava</option>
</select>
</div>
</div>
<div class="layui-form-item layui-inline">
<label class="layui-form-label">接收时间</label>
<div class="layui-input-inline">
<input data-date-range name="receive_time" value="{$get.receive_time|default=''}" placeholder="请选择接收时间" class="layui-input">
</div>
</div>
<div class="layui-form-item layui-inline">
<button class="layui-btn layui-btn-primary"><i class="layui-icon">&#xe615;</i> 搜 索</button>
</div>
</form>
</fieldset>
<table id="RecordTable" data-url="{:sysuri('get_list')}" data-target-search="form.form-search"></table>
</div>
{/block}
{block name="script"}
<script>
$(function () {
$('#RecordTable').layTable({
even: true, height: 'full',
sort: {field: 'id', type: 'desc'},
cols: [[
{field: 'id', title: 'ID', width: 80, sort: true},
{field: 'source_type', title: '来源', width: 100},
{field: 'package_name', title: '包名', minWidth: 100},
{field: 'event_name', title: '事件名称', minWidth: 100},
{field: 'original_url', title: '原始请求URL', minWidth: 200, templet: function(d){
return '<div class="layui-elip" title="'+ d.original_url +'">'+ d.original_url +'</div>';
}},
{field: 'receive_time', title: '接收时间', width: 180, sort: true},
{field: 'created_at', title: '创建时间', width: 180},
{title: '操作', toolbar: '#toolbar', width: 100, align: 'center', fixed: 'right'}
]]
});
});
</script>
<script type="text/html" id="toolbar">
<a class="layui-btn layui-btn-xs" data-title="查看详情" data-open="{:url('detail')}?id={{d.id}}">详情</a>
</script>
{/block}

View File

@@ -0,0 +1,23 @@
<div class="layui-card">
{block name='style'}{/block}
{block name='header'}
{notempty name='title'}
<div class="layui-card-header">
<span class="layui-icon font-s10 color-desc margin-right-5">&#xe65b;</span>{$title|lang}
<div class="pull-right">{block name='button'}{/block}</div>
</div>
{/notempty}
{/block}
<div class="layui-card-line"></div>
<div class="layui-card-body">
<div class="layui-card-table">
{notempty name='showErrorMessage'}
<div class="think-box-notify" type="error">
<b>{:lang('系统提示:')}</b><span>{$showErrorMessage|raw}</span>
</div>
{/notempty}
{block name='content'}{/block}
</div>
</div>
{block name='script'}{/block}
</div>