Compare commits

...

15 Commits

Author SHA1 Message Date
augushong
008e781d6b 增加菜单到导出和导入;增加粘贴全局操作;优化表单错误表现; 2024-10-15 16:06:36 +08:00
augushong
b04321e380 生成数据库迁移文件支持生成更新的迁移文件 2024-10-15 13:49:23 +08:00
augushong
afd8b81712 更新版本提示优先自定义的路径 2024-10-15 13:25:31 +08:00
augushong
9128194891 发布新版本 2024-10-14 18:46:50 +08:00
augushong
63766ea48d 发布新版本 2024-10-14 18:46:35 +08:00
augushong
cf407a70e8 修复更新机制错误 2024-10-14 18:46:18 +08:00
augushong
04028a4809 修复获取版本错误 2024-10-14 18:42:15 +08:00
augushong
9e0535ef6a 发布新版本 2024-10-14 18:25:33 +08:00
augushong
0f9607620c 支持推送产品版本 2024-10-14 18:23:21 +08:00
augushong
40a00be9d6 为基于框架的产品预留更新机制; 2024-10-14 18:01:10 +08:00
augushong
090ab096c0 生成数据库迁移代码时,如果是开发环境就生成一个到当前目录 2024-10-14 17:15:55 +08:00
augushong
1a640b446e 表单提交按钮支持不关闭页面的设置 2024-10-11 10:57:21 +08:00
augushong
83536d8309 优化定时器逻辑 2024-10-06 17:07:59 +08:00
augushong
384980b3d9 发布新版本 2024-10-04 23:51:33 +08:00
augushong
e4fe7b421b 修复多进程定时器的重复执行错误 2024-10-04 23:51:00 +08:00
14 changed files with 326 additions and 76 deletions

View File

@@ -10,6 +10,7 @@ use app\admin\service\TriggerService;
use app\common\constants\MenuConstant;
use app\common\controller\AdminController;
use think\App;
use think\facade\Db;
/**
* Class Menu.
@@ -216,4 +217,68 @@ class MenuBase extends AdminController
'type' => 'success',
]);
}
/**
* @NodeAnotation(title="导出")
*/
public function export()
{
$list_menu_json = SystemMenu::select()->hidden([
'create_time',
'update_time',
'delete_time',
])->toJson();
$download = $this->request->param('download');
if ($download == 1) {
$file_name = 'admin_menu.json';
$file_name = $this->request->host() . '_' . $file_name;
return download($list_menu_json, $file_name, true, 0);
}
$this->assign('list_menu_json', $list_menu_json);
return $this->fetch();
}
/**
* @NodeAnotation(title="import")
*/
public function import()
{
if ($this->request->isPost()) {
$data = $this->request->post('data');
$data_arr = json_decode($data, true);
$rule = [
'pid|上级菜单' => 'require',
'title|菜单名称' => 'require',
'icon|菜单图标' => 'require',
];
foreach ($data_arr as $data_item) {
$this->validate($data_item, $rule);
}
Db::startTrans();
try {
$list_old_menu = SystemMenu::withTrashed()->select();
foreach ($list_old_menu as $menu) {
$menu->force()->delete();
}
foreach ($data_arr as $data_item) {
$menu = new SystemMenu();
$menu->save($data_item);
}
Db::commit();
} catch (\Throwable $th) {
Db::rollback();
$this->error('导入失败:' . $th->getMessage());
}
$this->success('导入成功');
}
return $this->fetch();
}
}

View File

@@ -17,6 +17,14 @@ class AdminUpdateServiceBase
{
public const REPO = 'https://gitee.com/ulthon/ulthon_admin.git';
public const PRODUCT_REPO = '';
public $useRepo = null;
public $useVersion = null;
public $type = 'ulthon_admin';
/**
* @var Input
*/
@@ -27,8 +35,15 @@ class AdminUpdateServiceBase
*/
public $output;
public function __construct()
public function __construct($type = 'ulthon_admin')
{
if ($type == 'ulthon_admin') {
$this->useRepo = self::REPO;
$this->useVersion = Version::VERSION;
} else {
$this->useRepo = static::PRODUCT_REPO;
$this->useVersion = Version::PRODUCT_VERSION;
}
}
public function update()
@@ -38,7 +53,7 @@ class AdminUpdateServiceBase
$this->cleanWorkpaceDir();
$current_version = Version::VERSION;
$current_version = $this->useVersion;
$current_version_dir = App::getRuntimePath() . '/update/' . $current_version;
$last_version_dir = App::getRuntimePath() . '/update/last';
@@ -47,7 +62,7 @@ class AdminUpdateServiceBase
$output->writeln('获取最新代码');
$last_version_git = new Git();
$last_version_repo = $last_version_git->cloneRepository(self::REPO, $last_version_dir);
$last_version_repo = $last_version_git->cloneRepository($this->useRepo, $last_version_dir);
$tags = $last_version_repo->getTags();
@@ -87,7 +102,7 @@ class AdminUpdateServiceBase
$current_version_git = new Git();
$output->writeln('获取当前版本代码');
$current_version_repo = $current_version_git->cloneRepository(self::REPO, $current_version_dir);
$current_version_repo = $current_version_git->cloneRepository($this->useRepo, $current_version_dir);
$output->writeln('切换版本' . $current_version);
$current_version_repo->checkout($current_version);
@@ -127,21 +142,21 @@ class AdminUpdateServiceBase
// 当前版本的应该被处理所有文件
$current_version_files = $current_version_filesystem->listContents('/', true)
->filter($filter_files_function)
->map(fn (StorageAttributes $attributes) => $attributes->path())
->toArray();
->filter($filter_files_function)
->map(fn (StorageAttributes $attributes) => $attributes->path())
->toArray();
// 最新版本的所有文件
$last_version_files = $last_version_filesystem->listContents('/', true)
->filter($filter_files_function)
->map(fn (StorageAttributes $attributes) => $attributes->path())
->toArray();
->filter($filter_files_function)
->map(fn (StorageAttributes $attributes) => $attributes->path())
->toArray();
// 本身的所有文件
$now_files = $now_filesystem->listContents('/', true)
->filter($filter_files_function)
->map(fn (StorageAttributes $attributes) => $attributes->path())
->toArray();
->filter($filter_files_function)
->map(fn (StorageAttributes $attributes) => $attributes->path())
->toArray();
$changed_files = [];
@@ -228,8 +243,7 @@ class AdminUpdateServiceBase
// 如果现存版本和当前版本一致,则直接处理
if (PathTools::compareFiles($now_file_path, $current_file_path)) {
if(PathTools::compareFiles($current_file_path,$last_file_path)){
if (PathTools::compareFiles($current_file_path, $last_file_path)) {
// 如果当前版本和新版本一致,则无需处理
continue;
}
@@ -318,7 +332,11 @@ class AdminUpdateServiceBase
$output->writeln('更新完成');
// 更新完成
$update_tips = include $last_version_dir . '/extend/base/admin/service/adminUpdateData/tips.php';
$update_tips_file_path = $last_version_dir . '/app/admin/service/adminUpdateData/tips.php';
if(!file_exists($update_tips_file_path)){
$update_tips_file_path = $last_version_dir . '/extend/base/admin/service/adminUpdateData/tips.php';
}
$update_tips = include $update_tips_file_path;
// 按照版本号排序
usort($update_tips, function ($a, $b) {
@@ -348,7 +366,7 @@ class AdminUpdateServiceBase
'app',
'config',
'route',
'docker'
'docker',
];
foreach ($optional_files_prefix as $prefix) {

View File

@@ -0,0 +1,15 @@
<div class="layuimini-container">
<form id="app-form" class="layui-form layuimini-form">
<div class="layui-form-item layui-row layui-col-xs12">
<label class="layui-form-label ">数据预览</label>
<div class="layui-input-block">
<textarea id="data" class="layui-textarea" readonly>{$list_menu_json}</textarea>
<div class="layui-btn-container" style="margin-top: 5px;">
<div class="layui-btn layui-btn-sm" data-toggle="copy-text" data-clipboard-target="#data">复制</div>
<a href="{:url('export',['download'=>1])}" class="layui-btn layui-btn-sm">下载</a>
</div>
<tip>可以通过复制或下载的方式导出配置,然后在另一个系统中导入。</tip>
</div>
</div>
</form>
</div>

View File

@@ -0,0 +1,3 @@
$(function () {
ua.listen();
});

View File

@@ -0,0 +1,22 @@
<div class="layuimini-container">
<form id="app-form" class="layui-form layuimini-form">
<div class="layui-form-item layui-row layui-col-xs12">
<label class="layui-form-label">数据预览</label>
<div class="layui-input-block">
<textarea id="data" name="data" class="layui-textarea"></textarea>
<div class="layui-btn-container" style="margin-top: 5px;">
<div class="layui-btn layui-btn-sm" data-toggle="paste-text" data-paste-target="#data">复制</div>
<div class="layui-btn layui-btn-sm " id="process-json-file">上传</div>
<input id="json-file-input" type="file" accept=".json" style="display: none;"/>
</div>
<tip>从另一个系统中复制或下载,导出配置文件,在本页面导入或上传即可。请不要随意编辑内容,否则可能导致出错。</tip>
<tip style="color: red;">注意:这将覆盖当前系统的配置文件,请注意导出备份。</tip>
</div>
</div>
<div class="hr-line"></div>
<div class="layui-form-item text-center">
<button type="submit" class="layui-btn layui-btn-normal layui-btn-sm" lay-submit>确认</button>
<button type="reset" class="layui-btn layui-btn-primary layui-btn-sm">重置</button>
</div>
</form>
</div>

View File

@@ -0,0 +1,38 @@
$(function () {
var fileInput = document.getElementById('json-file-input');
fileInput.addEventListener('change', function (e) {
var files = e.target.files;
if (files.length > 0) {
var file = files[0];
var reader = new FileReader();
reader.onload = function (e) {
var jsonStr = e.target.result;
// 解析json字符串判断是否出错
try {
var jsonData = JSON.parse(jsonStr);
} catch (error) {
layer.msg('JSON 解析出错,请检查 JSON 格式是否正确。');
return;
}
$('#data').val(jsonStr);
};
reader.readAsText(file);
}
});
$('#process-json-file').click(function () {
fileInput.click();
});
ua.listen(function (data) {
return data;
}, function (res) {
ua.msg.success(res.msg, function () {
var index = parent.layer.getFrameIndex(window.name);
parent.layer.close(index);
parent.$('[data-treetable-refresh]').trigger("click");
});
});
});

View File

@@ -21,4 +21,6 @@
<button class="layui-btn layui-btn-sm layuimini-btn-primary" data-treetable-refresh><i class="fa fa-refresh"></i> </button>
<button class="layui-btn layui-btn-normal layui-btn-sm {if !auth('system.menu/add')}layui-hide{/if}" data-open="system.menu/add" data-title="添加" data-full="true"><i class="fa fa-plus"></i> 添加</button>
<button class="layui-btn layui-btn-sm layui-btn-danger {if !auth('system.menu/del')}layui-hide{/if}" data-url="system.menu/del" data-treetable-delete="currentTableRenderId"><i class="fa fa-trash-o"></i> 删除</button>
<button class="layui-btn layui-btn-sm {if !auth('system.menu/export')}layui-hide{/if}" data-open="system.menu/export" data-title="导出"> 导出</button>
<button class="layui-btn layui-btn-primary layui-btn-sm {if !auth('system.menu/import')}layui-hide{/if}" data-open="system.menu/import" data-title="导入"> 导入</button>
</script>

View File

@@ -105,6 +105,11 @@ class TimerBase extends Command
public function runParallel()
{
// 重新构造命令行参数,以便兼容workerman的命令
global $argv;
$argv = [];
array_unshift($argv, 'think', 'start');
$host = $this->host;
$site_host = $this->siteHost;
$output = $this->output;
@@ -116,13 +121,27 @@ class TimerBase extends Command
$worker->timerRequestList = $this->requestList;
$worker->onWorkerStart = function () use ($worker, $host, $site_host, $output, $input) {
Timer::add(1, function () use ($worker, $host, $site_host, $output, $input) {
$options = [
'max_conn_per_addr' => Config::get('timer.max_conn_per_addr', 1000),
'keepalive_timeout' => Config::get('timer.keepalive_timeout', 86400),
'connect_timeout' => Config::get('timer.connect_timeout', 86400),
'timeout' => Config::get('timer.timeout', 86400),
];
$http = new HttpClient($options);
Timer::add(1, function () use ($worker, $host, $site_host, $output, $input, $http) {
$request_list = $worker->timerRequestList;
foreach ($request_list as $request_item) {
$output->writeln(date('Y-m-d H:i:s') . ': build site request async:' . $request_item['target']);
if (!isset($request_item['is_running'])) {
$request_item['is_running'] = false;
}
if ($request_item['is_running']) {
$output->writeln('进行中,跳过');
continue;
}
if (!isset($request_item['last_run_time'])) {
$request_item['last_run_time'] = 0;
}
@@ -134,30 +153,20 @@ class TimerBase extends Command
$request_item['is_running'] = true;
$request_item['last_run_time'] = time();
$output->writeln(date('Y-m-d H:i:s') . ': build site request async:' . $request_item['target']);
//
$options = [
'max_conn_per_addr' => Config::get('timer.max_conn_per_addr', 1000),
'keepalive_timeout' => Config::get('timer.keepalive_timeout', 86400),
'connect_timeout' => Config::get('timer.connect_timeout', 86400),
'timeout' => Config::get('timer.timeout', 86400),
];
$http = new HttpClient($options);
$http->request($host . $request_item['target'], [
'headers' => [
'Host' => $site_host,
'Accept' => 'application/json,text/plain',
],
'success' => function ($response) use ($request_item) {
'success' => function ($response) use ($request_item, $output) {
$request_item['is_running'] = false;
echo $response->getBody();
$output->writeln((string) $response->getBody());
},
'error' => function ($exception) use ($request_item) {
'error' => function ($exception) use ($request_item, $output) {
$request_item['is_running'] = false;
echo $exception;
$output->writeln($exception);
},
]);
}

View File

@@ -12,11 +12,15 @@ use think\console\Output;
class UpdateBase extends Command
{
public const REPO = null;
protected function configure()
{
// 指令配置
$this->setName('admin:update')
->addOption('reinstall', null, Option::VALUE_NONE, '重装版本')
->addOption('update-ulthon', null, Option::VALUE_NONE, '重装版本')
->setDescription('the admin:update command');
}
@@ -25,7 +29,13 @@ class UpdateBase extends Command
// 指令输出
$output->writeln('admin:update');
$update_service = new AdminUpdateService();
$repo = static::REPO;
if($input->hasOption('update-ulthon')){
$repo = 'ulthon_admin';
}
$update_service = new AdminUpdateService($repo);
$update_service->input = $input;
$update_service->output = $output;
$update_service->update();

View File

@@ -12,14 +12,16 @@ use think\console\Output;
class VersionBase extends Command
{
public const VERSION = 'v2.0.102';
public const VERSION = 'v2.0.109';
public const PRODUCT_VERSION = '';
public const LAYUI_VERSION = '2.8.17';
public const COMMENT = [
'新增通用的虚拟数据模型',
'新增定时任务多进程',
'新增多进程阻塞模式的定时器',
'增加菜单到导出和导入',
'增加粘贴全局操作',
'优化表单错误表现',
'发布新版本',
];
@@ -34,14 +36,17 @@ class VersionBase extends Command
protected function execute(Input $input, Output $output)
{
// 指令输出
$output->info('当前版本号为:' . $this::VERSION);
$output->info('当前Layui版本号为:' . $this::LAYUI_VERSION);
if (!empty(static::PRODUCT_VERSION)) {
$output->info('当前版本号为:' . static::PRODUCT_VERSION);
}
$output->info('当前ulthon_admin版本号为' . static::VERSION);
$output->info('当前Layui版本号为' . static::LAYUI_VERSION);
$output->info('当前ThinkPHP版本号为' . ThinkApp::VERSION);
$output->writeln('当前的修改说明:');
$output->writeln('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>');
foreach ($this::COMMENT as $comment) {
foreach (static::COMMENT as $comment) {
$output->info($comment);
}
$output->writeln('<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<');
@@ -54,8 +59,11 @@ class VersionBase extends Command
if ($is_push_tag) {
$output->writeln('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>');
$version = $this::VERSION;
$comment = implode(';', $this::COMMENT);
$version = static::VERSION;
if(!empty(static::PRODUCT_VERSION)) {
$version = static::PRODUCT_VERSION;
}
$comment = implode(';', static::COMMENT);
$output->info('生成标签:' . $version);
$output->info('标签描述:' . $comment);
exec("git tag -a $version -m \"$comment\"");

View File

@@ -26,6 +26,7 @@ class MigrateBase extends Command
->addOption('tableName', '', Option::VALUE_OPTIONAL, '要生成的表名')
->addOption('fileName', '', Option::VALUE_OPTIONAL, '要生成的文件名')
->addOption('force', 'f', Option::VALUE_NONE, '强制生成')
->addOption('update', 'u', Option::VALUE_NONE, '生成新代码')
->setDescription('the curd:migrate command');
}
@@ -38,6 +39,7 @@ class MigrateBase extends Command
$file_name = $input->getOption('fileName');
$table_name = $input->getOption('tableName');
$force = $input->getOption('force');
$update = $input->hasOption('update');
if (empty($table)) {
$output->error('请输入表名');
@@ -68,34 +70,33 @@ class MigrateBase extends Command
}
$dist_file_path = App::getRootPath() . '/database/migrations/' . date('YmdHis') . '_' . $file_name . '.php';
$patt_path = App::getRootPath() . '/database/migrations/*' . $file_name . '.php';
$patt_files = glob($patt_path);
$is_extis = false;
foreach ($patt_files as $patt_file) {
$patt = '/.*database\/migrations\/\d+_' . $file_name . '.php$/';
$preg_result = preg_match($patt, $patt_file);
if ($preg_result) {
$is_extis = true;
}
}
if ($is_extis) {
$output->error('文件已存在:' . $patt_files[0]);
if (!$force) {
$confirm_force = $output->confirm($input, '确定要覆盖文件吗?', false);
if (!$confirm_force) {
return;
if(!$update){
$patt_path = App::getRootPath() . '/database/migrations/*' . $file_name . '.php';
$patt_files = glob($patt_path);
$is_extis = false;
foreach ($patt_files as $patt_file) {
$patt = '/.*database\/migrations\/\d+_' . $file_name . '.php$/';
$preg_result = preg_match($patt, $patt_file);
if ($preg_result) {
$is_extis = true;
}
}
$output->highlight('执行覆盖操作');
$dist_file_path = $patt_files[0];
if ($is_extis) {
$output->error('文件已存在:' . $patt_files[0]);
if (!$force) {
$confirm_force = $output->confirm($input, '确定要覆盖文件吗?如果您想生成更新文件请添加-u参数', false);
if (!$confirm_force) {
return;
}
}
$output->highlight('执行覆盖操作');
$dist_file_path = $patt_files[0];
}
}
$columns = Db::query("SHOW FULL COLUMNS FROM {$this->tablePrefix}{$this->table}");
@@ -198,10 +199,25 @@ class MigrateBase extends Command
$data['table_keys'] = $table_keys;
$data['table_keys_uni'] = $table_keys_uni;
$data['table_keys_text'] = $table_keys_text;
$data['method'] = 'create';
if($update){
$data['method'] = 'update';
}
$migrate_content = View::fetch(app_file_path('common/command/curd/migrate.tpl'), $data);
file_put_contents(__DIR__ . '/migrate_output.php', "<?php\n\n" . $migrate_content);
if(App::isDebug()){
file_put_contents(__DIR__ . '/migrate_output.php', "<?php\n\n" . $migrate_content);
}
file_put_contents($dist_file_path, "<?php\n\n" . $migrate_content);
if($update){
$output->info('已为您生成数据库迁移代码的更新代码,但仍然包含了所有的字段,因此您需要删除多余的添加指令,并根据您的表结构修改更新的指令');
$output->info('请查看:'. $dist_file_path);
}else{
$output->info('已为您生成数据库迁移代码');
$output->info('请查看:'. $dist_file_path);
}
}
}

View File

@@ -35,6 +35,6 @@ class {$class_name} extends Migrator
{volist name="table_keys_text" id="vo"}->addIndex('{$vo}',['type'=>'fulltext'])
{/volist}
{volist name="table_keys" id="vo"}->addIndex('{$vo}')
{/volist}->create();
{/volist}->{$method}();
}
}

View File

@@ -364,6 +364,6 @@ function set_store_value($key, $value)
if (!function_exists('get_site_version_key')) {
function get_site_version_key()
{
return sysconfig('site', 'site_version') . '-' . Version::VERSION . '-' . Version::LAYUI_VERSION;
return sysconfig('site', 'site_version') . '-' . Version::VERSION . '-' . Version::PRODUCT_VERSION . '-' . Version::LAYUI_VERSION;
}
}

View File

@@ -171,7 +171,12 @@
}
},
error: function (xhr, textstatus, thrown) {
admin.msg.error('Status:' + xhr.status + '' + xhr.statusText + ',请稍后再试!', function () {
var errorMsg = '';
if(xhr.responseJSON.message){
errorMsg = xhr.responseJSON.message;
}
loading.hide();
admin.msg.error('Status:' + xhr.status + '' + xhr.statusText + ',请稍后再试!<br/>'+errorMsg, function () {
ex(this);
});
return false;
@@ -1570,6 +1575,9 @@
// 监听点击复制
admin.api.copyText();
// 监听点击粘贴
admin.api.pasteText();
// 监听tab操作
miniTab.listen();
@@ -1822,16 +1830,19 @@
window.open(admin.url($(this).attr('href')));
});
},
form: function (url, data, ok, no, ex, refreshTable) {
form: function (url, data, ok, no, ex, refreshTable, close) {
if (refreshTable === undefined) {
refreshTable = true;
}
ok = ok || function (res) {
res.msg = res.msg || '';
admin.msg.success(res.msg, function () {
admin.api.closeCurrentOpen({
refreshTable: refreshTable
});
if (close) {
admin.api.closeCurrentOpen({
refreshTable: refreshTable
});
}
});
return false;
};
@@ -1914,6 +1925,7 @@
var filter = $(this).attr('lay-filter'),
type = $(this).attr('data-type'),
refresh = $(this).attr('data-refresh'),
close = $(this).attr('data-close'),
url = $(this).attr('lay-submit');
// 表格搜索不做自动提交
if (type === 'tableSearch') {
@@ -1925,6 +1937,11 @@
} else {
refresh = true;
}
if (close === 'false') {
close = false;
} else {
close = true;
}
// 自动添加layui事件过滤器
if (filter === undefined || filter === '') {
filter = 'save_form_' + (i + 1);
@@ -2405,6 +2422,33 @@
});
});
},
pasteText(elem) {
if (elem == undefined) {
elem = 'body';
}
var list = $(elem).find('[data-toggle="paste-text"]');
$.each(list, function (i, v) {
if ($(v).hasClass('paste-rendered')) {
return false;
}
$(v).addClass('paste-rendered');
var targetElemName = $(v).data('paste-target');
$(v).on('click', function () {
navigator.clipboard.readText()
.then(text => {
$(targetElemName).val(text);
layer.msg('粘贴成功');
})
.catch(err => {
console.error('Failed to read clipboard contents: ', err);
layer.msg('粘贴失败,请手动粘贴');
});
});
});
}
},
getQueryVariable(variable, defaultValue) {